refactor: DiviSaver writes content through one guarded writeContent() (M4)

writeContent($postId, $content, ?$before) holds the kses window plus
wp_update_post(wp_slash([ID, post_content]), true); save() passes the restore
point capture as $before, so the window still holds exactly RevisionPoint::
capture() and wp_update_post() (S37), saveNew() and the restore() rollback use it
without. refusal() turns WP_Error / 0 into the message ("returned 0" as before)
for save() and saveNew(); restore() keeps its own RuntimeException text. The
class docblock (the M-5 window description) is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 18:15:28 +02:00
co-authored by Claude Opus 5.5
parent 3eeb7eee90
commit 6cf0a565e6
+37 -17
View File
@@ -68,20 +68,19 @@ class DiviSaver
$revisionId = null; $revisionId = null;
try { try {
$result = $this->withoutKses(static function () use ($postId, $content, &$revisionId): int|WP_Error { // Ruling S37: the restore point is captured inside the same kses window.
$result = $this->writeContent($postId, $content, static function () use ($postId, &$revisionId): void {
$revisionId = RevisionPoint::capture($postId); $revisionId = RevisionPoint::capture($postId);
return wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $content]), true);
}); });
} catch (Throwable $e) { } catch (Throwable $e) {
$this->restore($postId, (string) $post->post_content, $revisionId, $e); $this->restore($postId, (string) $post->post_content, $revisionId, $e);
} }
if ($result instanceof WP_Error || $result === 0) { $refusal = self::refusal($result);
$message = $result instanceof WP_Error ? $result->get_error_message() : 'returned 0'; if ($refusal !== null) {
throw WriteFailed::rolledBack( throw WriteFailed::rolledBack(
self::REASON, self::REASON,
detail: sprintf('wp_update_post(%d) refused: %s', $postId, $message), detail: sprintf('wp_update_post(%d) refused: %s', $postId, $refusal),
state: self::keeping($revisionId), state: self::keeping($revisionId),
postId: $postId, postId: $postId,
revisionId: $revisionId revisionId: $revisionId
@@ -129,12 +128,9 @@ class DiviSaver
*/ */
public function saveNew(int $postId, string $content): array public function saveNew(int $postId, string $content): array
{ {
$result = $this->withoutKses( $refusal = self::refusal($this->writeContent($postId, $content));
static fn (): int|WP_Error => wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $content]), true) if ($refusal !== null) {
); throw new RuntimeException(sprintf('wp_update_post(%d) refused the Divi layout: %s', $postId, $refusal));
if ($result instanceof WP_Error || $result === 0) {
$message = $result instanceof WP_Error ? $result->get_error_message() : 'returned 0';
throw new RuntimeException(sprintf('wp_update_post(%d) refused the Divi layout: %s', $postId, $message));
} }
DiviFlags::set($postId); DiviFlags::set($postId);
@@ -144,6 +140,34 @@ class DiviSaver
return $stored instanceof WP_Post && (string) $stored->post_content !== $content ? [self::CHANGED_ON_SAVE] : []; return $stored instanceof WP_Post && (string) $stored->post_content !== $content ? [self::CHANGED_ON_SAVE] : [];
} }
/**
* The one guarded content write: wp_update_post() (wp_slash, status unchanged) of $content in
* the kses window. $before runs first inside the same window – save() captures the restore
* point there (ruling S37); nothing else ever runs in it (see the class docblock).
*
* @param (callable(): void)|null $before
*/
private function writeContent(int $postId, string $content, ?callable $before = null): int|WP_Error
{
return $this->withoutKses(static function () use ($postId, $content, $before): int|WP_Error {
if ($before !== null) {
$before();
}
return wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $content]), true);
});
}
/** Why wp_update_post() refused ($result WP_Error or 0), null when it saved. */
private static function refusal(int|WP_Error $result): ?string
{
if ($result instanceof WP_Error) {
return $result->get_error_message();
}
return $result === 0 ? 'returned 0' : null;
}
/** /**
* Runs $write with kses' filters off for users without unfiltered_html (D7); restored in * Runs $write with kses' filters off for users without unfiltered_html (D7); restored in
* finally, right when $write returns or throws. * finally, right when $write returns or throws.
@@ -174,11 +198,7 @@ class DiviSaver
$failures = Rollback::attempt(function () use ($postId, $before): void { $failures = Rollback::attempt(function () use ($postId, $before): void {
PostGuard::assertNoMissedSchedule($postId, 'restoring would publish a missed schedule'); PostGuard::assertNoMissedSchedule($postId, 'restoring would publish a missed schedule');
$restored = $this->withoutKses( if (self::refusal($this->writeContent($postId, $before)) !== null) {
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'); throw new RuntimeException('previous content could not be written back');
} }
}); });