*/ private array $calls = []; /** @var array */ private array $meta = []; private string $log = ''; private string|false $previousLog = false; protected function setUp(): void { parent::setUp(); $this->post = new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_content' => 'ALT']); Functions\when('wp_save_post_revision')->alias(function (): int { $this->calls[] = 'revision'; return 811; }); Functions\when('wp_get_post_revisions')->justReturn([811 => new WP_Post(['ID' => 811])]); Functions\when('update_post_meta')->alias(function (int $id, string $key, string $value): bool { $this->calls[] = "meta:$key=$value"; $this->meta[$key] = $value; return true; }); Functions\when('get_post_meta')->alias(fn (int $id, string $key): string => $this->meta[$key] ?? ''); Functions\when('kses_remove_filters')->alias(function (): void { $this->calls[] = 'kses off'; }); Functions\when('kses_init_filters')->alias(function (): void { $this->calls[] = 'kses on'; }); // FollowUp logs failures with error_log() (a PHP internal): keep the test output clean. $this->log = (string) tempnam(sys_get_temp_dir(), 'lit-log'); $this->previousLog = ini_set('error_log', $this->log); } protected function tearDown(): void { ini_set('error_log', (string) $this->previousLog); @unlink($this->log); parent::tearDown(); } public function testSavesWithoutKsesForFilteredUsersAndSetsDiviFlags(): void { Functions\when('current_user_can')->justReturn(false); Functions\when('has_filter')->justReturn(10); Functions\expect('wp_update_post')->once()->with(['ID' => 7, 'post_content' => 'NEU'], true) ->andReturnUsing(function (): int { $this->calls[] = 'update'; return 7; }); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU'])); $saved = (new DiviSaver())->save($this->post, 'NEU'); self::assertSame(['revision_id' => 811, 'warnings' => []], $saved); self::assertSame( // Ruling S37: the restore-point revision is captured inside the same kses-off window – // it copies the stored content, and kses would strip an admin's embeds from it. [ 'kses off', 'revision', 'update', 'kses on', 'meta:_et_pb_use_divi_5=on', 'meta:_et_pb_use_builder=on', 'meta:_et_pb_show_page_creation=off', ], $this->calls ); } public function testKeepsKsesUntouchedForUnfilteredUsersAndWarnsWhenAFilterChangedTheContent(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('wp_update_post')->justReturn(7); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU, gefiltert'])); $saved = (new DiviSaver())->save($this->post, 'NEU'); self::assertNotContains('kses off', $this->calls); self::assertSame(['revision'], array_slice($this->calls, 0, 1)); self::assertSame([DiviSaver::CHANGED_ON_SAVE], $saved['warnings']); } public function testKsesStaysUntouchedWhenItIsNotActive(): void { Functions\when('current_user_can')->justReturn(false); Functions\when('has_filter')->justReturn(false); Functions\when('wp_update_post')->justReturn(7); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU'])); (new DiviSaver())->save($this->post, 'NEU'); self::assertNotContains('kses off', $this->calls); self::assertNotContains('kses on', $this->calls); } public function testRefusedUpdateIsACleanFailure(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('wp_update_post')->justReturn(new WP_Error('db_update_error', 'DB kaputt')); try { (new DiviSaver())->save($this->post, 'NEU'); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); self::assertSame(811, $e->revisionId); self::assertStringContainsString('Revision 811 bleibt erhalten', $e->toError()->get_error_message()); } } public function testAThrowingHookRestoresThePreviousContentAndKsesComesBack(): void { Functions\when('current_user_can')->justReturn(false); Functions\when('has_filter')->justReturn(10); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_status' => 'draft'])); $written = []; Functions\when('wp_update_post')->alias(function (array $data) use (&$written): int { $written[] = $data['post_content']; $this->calls[] = 'update'; if ($data['post_content'] === 'NEU') { throw new RuntimeException('save_post hook failed'); } return 7; }); try { (new DiviSaver())->save($this->post, 'NEU'); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); self::assertSame(['NEU', 'ALT'], $written); // The kses bypass is restored in finally – after the failed save and after the restore. self::assertSame( ['kses off', 'revision', 'update', 'kses on', 'kses off', 'update', 'kses on'], $this->calls ); } } public function testFailedRestoreIsAPartialWrite(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_status' => 'draft'])); Functions\when('wp_update_post')->alias(static function (): int { throw new RuntimeException('database gone'); }); try { (new DiviSaver())->save($this->post, 'NEU'); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertFalse($e->rolledBack); self::assertToolError($e->toError(), 'partial_write'); } } public function testRestoreNeverPublishesAMissedSchedule(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('get_post')->justReturn(new WP_Post([ 'ID' => 7, 'post_status' => 'future', 'post_date_gmt' => '2020-01-01 00:00:00', ])); $written = []; Functions\when('wp_update_post')->alias(static function (array $data) use (&$written): int { $written[] = $data['post_content']; throw new RuntimeException('save_post hook failed'); }); try { (new DiviSaver())->save($this->post, 'NEU'); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertFalse($e->rolledBack); self::assertSame(['NEU'], $written); } } public function testFlagFailureIsOnlyAWarning(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('wp_update_post')->justReturn(7); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 7, 'post_content' => 'NEU'])); Functions\when('get_post_meta')->justReturn(''); $saved = (new DiviSaver())->save($this->post, 'NEU'); self::assertSame(811, $saved['revision_id']); self::assertCount(1, $saved['warnings']); self::assertStringContainsString('Divi-Kennzeichen', $saved['warnings'][0]); self::assertStringContainsString('divi builder flags failed', (string) file_get_contents($this->log)); } public function testSaveNewWritesTheFirstContentWithoutKsesAndSetsTheFlagsStrictly(): void { Functions\when('current_user_can')->justReturn(false); Functions\when('has_filter')->justReturn(10); Functions\expect('wp_update_post')->once()->with(['ID' => 42, 'post_content' => 'NEU'], true) ->andReturnUsing(function (): int { $this->calls[] = 'update'; return 42; }); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 42, 'post_content' => 'NEU'])); Functions\expect('wp_save_post_revision')->never(); self::assertSame([], (new DiviSaver())->saveNew(42, 'NEU')); self::assertSame( // Same kses window as save(), but no restore point: the new draft has no earlier state. [ 'kses off', 'update', 'kses on', 'meta:_et_pb_use_divi_5=on', 'meta:_et_pb_use_builder=on', 'meta:_et_pb_show_page_creation=off', ], $this->calls ); } public function testSaveNewReportsAContentChangedOnSaveAndKeepsKsesForUnfilteredUsers(): void { Functions\when('current_user_can')->justReturn(true); Functions\when('has_filter')->justReturn(10); Functions\when('wp_update_post')->justReturn(42); Functions\when('get_post')->justReturn(new WP_Post(['ID' => 42, 'post_content' => 'ANDERS'])); self::assertSame([DiviSaver::CHANGED_ON_SAVE], (new DiviSaver())->saveNew(42, 'NEU')); self::assertNotContains('kses off', $this->calls); } public function testSaveNewThrowsWhenTheUpdateIsRefusedOrAFlagIsMissing(): void { Functions\when('current_user_can')->justReturn(false); Functions\when('has_filter')->justReturn(10); Functions\when('wp_update_post')->justReturn(new WP_Error('db', 'kaputt')); try { (new DiviSaver())->saveNew(42, 'NEU'); self::fail('RuntimeException expected'); } catch (RuntimeException $e) { self::assertStringContainsString('kaputt', $e->getMessage()); } self::assertSame(['kses off', 'kses on'], $this->calls, 'kses comes back, no flags on a failed write'); Functions\when('wp_update_post')->justReturn(42); Functions\when('get_post_meta')->justReturn(''); $this->expectException(RuntimeException::class); (new DiviSaver())->saveNew(42, 'NEU'); } }