Non-sync-enabled emails receive a content-only reset and the return shape carries
`null` for the four sync fields (BC contract with the pre-RSM-139 reset endpoint).
The four meta writes are skipped entirely if wp_update_post fails, so a WP_Error return leaves the post and existing meta untouched. Matches the pre-RSM-139 reset endpoint shape (see PR #64355 review on 2fa660b3b9).
public static function apply_to_post( \WC_Email $email, int $post_id, array $opts = array() ) {
$require_uncustomized = ! isset( $opts['require_uncustomized'] ) || (bool) $opts['require_uncustomized'];
$sync_config = WCEmailTemplateSyncRegistry::get_email_sync_config( (string) $email->id );
if ( $require_uncustomized && null === $sync_config ) {
return new \WP_Error(
'not_sync_enabled',
sprintf(
/* translators: %s: email ID */
__( 'Email "%s" is not registered for template sync.', 'woocommerce' ),
(string) $email->id
)
);
}
$post = get_post( $post_id );
if ( ! $post instanceof \WP_Post || \Automattic\WooCommerce\Internal\EmailEditor\Integration::EMAIL_POST_TYPE !== $post->post_type ) {
return new \WP_Error(
'post_not_found',
sprintf(
/* translators: %d: post ID */
__( 'No woo_email post found for ID %d.', 'woocommerce' ),
$post_id
)
);
}
$stored_source_hash = '';
if ( $require_uncustomized ) {
$stored_source_hash = (string) get_post_meta( $post_id, WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY, true );
if ( '' === $stored_source_hash || ! self::is_sha1_hash( $stored_source_hash ) ) {
return new \WP_Error(
'no_stored_hash',
sprintf(
/* translators: %d: post ID */
__( 'Post %d has no stored source hash; cannot safely auto-apply.', 'woocommerce' ),
$post_id
)
);
}
}//end if
$canonical = WCTransactionalEmailPostsGenerator::compute_canonical_post_content( $email );
$source_hash = null;
$synced_at = null;
$status = null;
$version = null;
self::$is_auto_applying = true;
try {
// Re-hash post_content immediately before the write to minimise the
// TOCTOU gap between the snapshot and wp_update_post. The first $post
// load above is too early — `compute_canonical_post_content` runs in
// between and yields the window where a merchant save could otherwise
// be silently overwritten.
if ( $require_uncustomized ) {
$latest_post = get_post( $post_id );
if ( ! $latest_post instanceof \WP_Post
|| sha1( (string) $latest_post->post_content ) !== $stored_source_hash
) {
return new \WP_Error(
'post_modified_since_stamp',
sprintf(
/* translators: %d: post ID */
__( 'Post %d has been modified since the last sync stamp; skipping auto-apply.', 'woocommerce' ),
$post_id
)
);
}
}
$updated = wp_update_post(
array(
'ID' => $post_id,
'post_content' => $canonical,
),
true
);
if ( is_wp_error( $updated ) ) {
return $updated;
}
// Read back the persisted post_content. The `content_save_pre` filter
// chain can mutate `$canonical` between the in-memory string and what
// lands in the DB, so both the returned `content` field and the
// stamped source hash must reflect what the database actually holds.
// See the same note in `WCEmailTemplateSelectiveApplier::apply_selectively()`.
$saved_post = get_post( $post_id );
$saved_body = $saved_post instanceof \WP_Post ? (string) $saved_post->post_content : $canonical;
$canonical = $saved_body;
if ( null !== $sync_config ) {
$source_hash = sha1( $canonical );
$synced_at = gmdate( 'Y-m-d H:i:s' );
$version = (string) $sync_config['version'];
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::VERSION_META_KEY, $version );
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::SOURCE_HASH_META_KEY, $source_hash );
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_SYNCED_AT_META_KEY, $synced_at );
update_post_meta( $post_id, WCEmailTemplateDivergenceDetector::LAST_CORE_RENDER_META_KEY, $canonical );
// Status comes from the classifier so all writers stay consistent.
// In this path we always write canonical, so the classifier returns
// IN_SYNC, but going through the same helper as the selective applier
// avoids drift if a future partial-apply path is added here.
$status = WCEmailTemplateDivergenceDetector::reclassify( $post_id );
}//end if
} finally {
self::$is_auto_applying = false;
}//end try
// Fire `_update_applied` for the auto-applier path. Static extensions:
// the auto-applier only acts on `core_updated_uncustomized` posts, so
// `had_customizations` is always false and `auto_resolved` is always true.
// Gate on `$require_uncustomized`: this method is also reused by the
// reset endpoint (with `require_uncustomized = false`) — the reset
// surface is not in RSM-145's event taxonomy and must not be tagged
// as `applied_from='auto'`.
if ( $require_uncustomized ) {
WCEmailTemplateSyncTracker::record_auto_applied( $post_id );
}
return array(
'content' => $canonical,
'version' => $version,
'source_hash' => $source_hash,
'synced_at' => $synced_at,
'status' => $status,
);
}