diff --git a/src/Integrations/Divi/Pages/DiviSaver.php b/src/Integrations/Divi/Pages/DiviSaver.php index 9e643cf..36dd9f2 100644 --- a/src/Integrations/Divi/Pages/DiviSaver.php +++ b/src/Integrations/Divi/Pages/DiviSaver.php @@ -68,20 +68,19 @@ class DiviSaver $revisionId = null; 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); - - return wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $content]), true); }); } catch (Throwable $e) { $this->restore($postId, (string) $post->post_content, $revisionId, $e); } - if ($result instanceof WP_Error || $result === 0) { - $message = $result instanceof WP_Error ? $result->get_error_message() : 'returned 0'; + $refusal = self::refusal($result); + if ($refusal !== null) { throw WriteFailed::rolledBack( 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), postId: $postId, revisionId: $revisionId @@ -129,12 +128,9 @@ class DiviSaver */ public function saveNew(int $postId, string $content): array { - $result = $this->withoutKses( - static fn (): int|WP_Error => wp_update_post(wp_slash(['ID' => $postId, 'post_content' => $content]), true) - ); - 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)); + $refusal = self::refusal($this->writeContent($postId, $content)); + if ($refusal !== null) { + throw new RuntimeException(sprintf('wp_update_post(%d) refused the Divi layout: %s', $postId, $refusal)); } DiviFlags::set($postId); @@ -144,6 +140,34 @@ class DiviSaver 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 * finally, right when $write returns or throws. @@ -174,11 +198,7 @@ class DiviSaver $failures = Rollback::attempt(function () use ($postId, $before): void { PostGuard::assertNoMissedSchedule($postId, 'restoring would publish a missed schedule'); - $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) { + if (self::refusal($this->writeContent($postId, $before)) !== null) { throw new RuntimeException('previous content could not be written back'); } });