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
@@ -98,6 +98,12 @@ final class ComposeFixture
{
return $this->unfiltered;
}
/** Every post is readable but 666 (ruling S37, select_post settings). */
public function canReadPost(int $id): bool
{
return $id !== 666;
}
};
}
@@ -431,4 +431,51 @@ final class VariableRefTest extends TestCase
self::assertTrue(VariableRef::contains('a $variable( b'));
self::assertFalse(VariableRef::contains('Preis: 5 $'));
}
/**
* Ruling S37 (M-1/M-3): map field types come from a filter – untrusted. url → url() (a URL the
* sanitizer empties is refused), select_post → a readable numeric post id, select /
* yes_no_button / number → plain values; every other type is refused.
*/
public function testMapFieldTypesAreRoutedStrictly(): void
{
$ref = self::ref([
'evil' => ['link' => 'url', 'color' => 'color', 'blank' => '', 'flag' => 'yes_no_button', 'n' => 'number'],
'post_link_url_page' => ['post_id' => 'select_post'],
]);
$sanitizer = ComposeFixture::sanitizer();
self::assertSame(
self::content('evil', '{"link":"https://kunde.test/a%20b","flag":"on","n":3}'),
$ref->canonical(self::content('evil', '{"link":" https://kunde.test/a b","flag":"on","n":3}'), $sanitizer)
);
self::assertSame(
self::content('post_link_url_page', '{"post_id":"12"}'),
$ref->canonical(self::content('post_link_url_page', '{"post_id":"12"}'), $sanitizer)
);
self::assertSame(
self::content('post_link_url_page', '{"post_id":12}'),
$ref->canonical(self::content('post_link_url_page', '{"post_id":12}'), $sanitizer)
);
$rejected = [
'javascript: in a url setting' => [self::content('evil', '{"link":"javascript:alert(1)"}'), 'URL'],
'url setting not a string' => [self::content('evil', '{"link":5}'), 'nur Text'],
'unknown field type' => [self::content('evil', '{"color":"red"}'), 'Feldtyp'],
'empty field type' => [self::content('evil', '{"blank":"x"}'), 'Feldtyp'],
'post slug' => [self::content('post_link_url_page', '{"post_id":"impressum"}'), 'Beitrags-ID'],
'zero post id' => [self::content('post_link_url_page', '{"post_id":0}'), 'Beitrags-ID'],
'negative post id' => [self::content('post_link_url_page', '{"post_id":"-3"}'), 'Beitrags-ID'],
'unreadable post' => [self::content('post_link_url_page', '{"post_id":"666"}'), 'Beitrags-ID'],
];
foreach ($rejected as $case => [$value, $fragment]) {
try {
$ref->canonical($value, $sanitizer);
self::fail('Accepted: ' . $case);
} catch (InvalidArgumentException $e) {
self::assertStringContainsString($fragment, $e->getMessage(), $case);
}
}
}
}
@@ -20,11 +20,17 @@ final class DiviFlagsTest extends TestCase
return false;
});
Functions\when('get_post_meta')->justReturn('on');
Functions\when('get_post_meta')->alias(
static fn (int $id, string $key): string => $key === '_et_pb_show_page_creation' ? 'off' : 'on'
);
DiviFlags::set(7);
self::assertSame(['7:_et_pb_use_divi_5=on', '7:_et_pb_use_builder=on'], $written);
// Like Divi's own save (SyncToServerController.php:382-387), incl. the page creation flow flag.
self::assertSame(
['7:_et_pb_use_divi_5=on', '7:_et_pb_use_builder=on', '7:_et_pb_show_page_creation=off'],
$written
);
}
public function testThrowsWhenAFlagDoesNotReadBack(): void
@@ -38,4 +44,15 @@ final class DiviFlagsTest extends TestCase
$this->expectExceptionMessage('_et_pb_use_builder');
DiviFlags::set(7);
}
public function testThrowsWhenThePageCreationFlagDoesNotReadBack(): void
{
Functions\when('update_post_meta')->justReturn(true);
Functions\when('get_post_meta')->justReturn('on');
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('_et_pb_show_page_creation');
DiviFlags::set(7);
}
}
@@ -30,7 +30,11 @@ final class DiviSaverTest extends TestCase
{
parent::setUp();
$this->post = new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_content' => 'ALT']);
Functions\when('wp_save_post_revision')->justReturn(811);
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";
@@ -73,7 +77,12 @@ final class DiviSaverTest extends TestCase
self::assertSame(['revision_id' => 811, 'warnings' => []], $saved);
self::assertSame(
['kses off', 'update', 'kses on', 'meta:_et_pb_use_divi_5=on', 'meta:_et_pb_use_builder=on'],
// 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
);
}
@@ -87,6 +96,7 @@ final class DiviSaverTest extends TestCase
$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']);
}
@@ -141,7 +151,10 @@ final class DiviSaverTest extends TestCase
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', 'update', 'kses on', 'kses off', 'update', 'kses on'], $this->calls);
self::assertSame(
['kses off', 'revision', 'update', 'kses on', 'kses off', 'update', 'kses on'],
$this->calls
);
}
}
@@ -187,6 +187,9 @@ final class DynamicContentMapTest extends TestCase
}
self::assertNull($map);
self::assertStringContainsString('filter exploded', (string) file_get_contents($log));
// The message only – no stack trace in the log (review M-4).
self::assertStringNotContainsString('#0 ', (string) file_get_contents($log));
self::assertStringNotContainsString('RuntimeException', (string) file_get_contents($log));
unlink($log);
// Without Divi (no DynamicContentOptions class, as in these unit tests).
@@ -46,4 +46,14 @@ final class WpValueSanitizerTest extends TestCase
self::assertSame('Hallo', $sanitizer->text('<b>Hallo</b>'));
self::assertSame('url(https://x.test/a b)', $sanitizer->url(' https://x.test/a b '));
}
public function testCanReadPostAsksWordPress(): void
{
Functions\when('current_user_can')->alias(static fn (string $cap, int $id = 0): bool => $cap === 'read_post' && $id === 12);
$sanitizer = new WpValueSanitizer();
self::assertTrue($sanitizer->canReadPost(12));
self::assertFalse($sanitizer->canReadPost(13));
}
}