fix: Task 5 review round 1 - restore point inside the kses window, strict map field types (S37)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 15:08:48 +02:00
co-authored by Claude Opus 5.5
parent 7762b76305
commit b8cb01812e
12 changed files with 200 additions and 26 deletions
+34 -11
View File
@@ -15,17 +15,28 @@ use WP_Error;
use WP_Post;
/**
* Save pipeline of spec §5.6, steps 3–5: restore point, wp_update_post() with wp_slash() (the
* Save pipeline of spec §5.6, steps 3–5: restore point and wp_update_post() with wp_slash() (the
* status stays), Divi 5 flags (DiviFlags), Divi's static CSS cache. A failure after saving is
* only a warning (FollowUp); a filter that changed the stored content is reported as well.
*
* kses (spec D7): for users without unfiltered_html WordPress' kses would run
* filter_block_content(), which re-parses and re-serializes EVERY block – untouched content
* would no longer be byte-identical (spec §1.3, §5.2) and global modules could break. kses'
* filters (kses_remove_filters()) are therefore switched off for exactly this call and restored
* in finally (kses_init_filters()) – only when they were active. Every value the Divi tools
* write was sanitized and every $variable(…)$ re-serialized by the plugin before (AttrWriter,
* VariableRef, WpValueSanitizer).
* filters (kses_remove_filters()) are therefore switched off – only when they were active – and
* restored in finally (kses_init_filters()) right after wp_update_post() returns, before flags,
* cache and every other follow-up. Every value the Divi tools write was sanitized and every
* $variable(…)$ re-serialized by the plugin before (AttrWriter, VariableRef, WpValueSanitizer).
*
* The window (review M-5): kses_remove_filters() removes ALL core kses filters – post content,
* title, excerpt, content_filtered and comments (pre_comment_content) – while exactly two calls
* run: RevisionPoint::capture() (ruling S37: the restore point copies the STORED content; kses
* would strip an admin's embeds from it, so undoing via that revision would lose them) and
* wp_update_post() with every hook it fires: wp_insert_post_data, save_post(_{type}),
* wp_insert_post, post_updated (→ wp_save_post_revision / _wp_put_post_revision),
* transition_post_status, wp_after_insert_post and third-party listeners. No MCP input reaches
* those hooks except the content itself (already cleaned); anything a third-party hook writes
* there is saved without kses, as it would be for an administrator. The rollback write in
* restore() uses the same window with the previous stored content.
*
* Divi's own save filters are NOT kses and are never removed (ruling S7, defense in depth):
* Security::initialize() (Divi 5.2.1 Security.php:74-81) registers on wp_insert_post_data
@@ -52,10 +63,14 @@ class DiviSaver
public function save(WP_Post $post, string $content): array
{
$postId = (int) $post->ID;
$revisionId = RevisionPoint::capture($postId);
$revisionId = null;
try {
$result = $this->update($postId, $content);
$result = $this->withoutKses(static function () use ($postId, $content, &$revisionId): int|WP_Error {
$revisionId = RevisionPoint::capture($postId);
return wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $content]), true);
});
} catch (Throwable $e) {
$this->restore($postId, (string) $post->post_content, $revisionId, $e);
}
@@ -101,8 +116,13 @@ class DiviSaver
return ['revision_id' => $revisionId, 'warnings' => $warnings];
}
/** wp_update_post() with kses' filters off for users without unfiltered_html (D7). */
private function update(int $postId, string $content): int|WP_Error
/**
* Runs $write with kses' filters off for users without unfiltered_html (D7); restored in
* finally, right when $write returns or throws.
*
* @param callable(): (int|WP_Error) $write
*/
private function withoutKses(callable $write): int|WP_Error
{
$kses = ! current_user_can('unfiltered_html')
&& has_filter('content_save_pre', 'wp_filter_post_kses') !== false;
@@ -111,7 +131,7 @@ class DiviSaver
}
try {
return wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $content]), true);
return $write();
} finally {
if ($kses) {
kses_init_filters();
@@ -129,7 +149,10 @@ class DiviSaver
throw new RuntimeException('restoring would publish a missed schedule');
}
$restored = $this->update($postId, $before);
$restored = $this->withoutKses(
static fn (): int|WP_Error
=> wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $before]), true)
);
if ($restored instanceof WP_Error || $restored === 0) {
throw new RuntimeException('previous content could not be written back');
}