fix: Task 1 review round 4 - meta keys checked after Divi's prefix derivation (S30)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,9 +35,10 @@ use stdClass;
|
||||
* DynamicContentOptions::get_options($post_id, 'edit') – option name => its "fields" (key =>
|
||||
* field "type"). Always, whatever the map says: "enable_html" is rejected, loop_* names only
|
||||
* when the map names them, custom_meta_<key> names and the values of settings whose key ends in
|
||||
* "meta_key" (meta_key, select_meta_key, loop_meta_key …) obey META_KEY (ruling S29), and no
|
||||
* string – before or after cleaning (ruling S28) – may contain a reference or ")$". A map
|
||||
* of another shape fails in the constructor with InvalidArgumentException.
|
||||
* "meta_key" (meta_key, select_meta_key, loop_meta_key …) obey META_KEY after Divi's own prefix
|
||||
* stripping (rulings S29, S30), and no string – before or after cleaning (ruling S28) – may
|
||||
* contain a reference or ")$". A map of another shape fails in the constructor with
|
||||
* InvalidArgumentException.
|
||||
*/
|
||||
final class VariableRef
|
||||
{
|
||||
@@ -80,12 +81,23 @@ final class VariableRef
|
||||
|
||||
private const META_PREFIX = 'custom_meta_';
|
||||
|
||||
/**
|
||||
* Prefixes Divi strips from a meta key option with str_replace – every occurrence, not just
|
||||
* the leading one (DynamicContentOptionPostMetaKey.php:232,249,275,286;
|
||||
* DynamicContentOptionLoopPostMetaKey.php:366-373). Ruling S30.
|
||||
*/
|
||||
private const META_KEY_PREFIXES = [
|
||||
self::META_PREFIX,
|
||||
'loop_post_meta_key_',
|
||||
'loop_user_meta_key_',
|
||||
'loop_term_meta_key_',
|
||||
];
|
||||
|
||||
private const NUMBER = '/^-?\d+(\.\d+)?$/';
|
||||
|
||||
/** @var array<string, array<string, string>> */
|
||||
private array $dynamicContent;
|
||||
|
||||
/** @param array<string, array<string, string>>|null $dynamicContent See the class docblock. */
|
||||
/**
|
||||
* @param array<array-key, mixed>|null $dynamicContent See the class docblock.
|
||||
* @throws InvalidArgumentException When the map does not have that shape.
|
||||
@@ -342,11 +354,26 @@ final class VariableRef
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/** "telefon", "custom_meta_telefon" yes; "_edit_lock", "custom_meta__edit_lock", "a b" no. */
|
||||
/**
|
||||
* Checks the meta key Divi derives from $key (ruling S30): a leading known prefix is removed the
|
||||
* way Divi does it – str_replace of every occurrence – and a prefix still present afterwards is
|
||||
* rejected (defensive). "telefon", "custom_meta_telefon", "loop_post_meta_key_telefon" yes;
|
||||
* "_edit_lock", "custom_meta__edit_lock", "custom_meta_custom_meta__edit_lock",
|
||||
* "loop_post_meta_key__edit_lock", "a b" no.
|
||||
*/
|
||||
private static function isMetaKey(string $key): bool
|
||||
{
|
||||
if (str_starts_with($key, self::META_PREFIX)) {
|
||||
$key = substr($key, strlen(self::META_PREFIX));
|
||||
foreach (self::META_KEY_PREFIXES as $prefix) {
|
||||
if (str_starts_with($key, $prefix)) {
|
||||
$key = str_replace($prefix, '', $key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::META_KEY_PREFIXES as $prefix) {
|
||||
if (str_contains($key, $prefix)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return preg_match(self::META_KEY, $key) === 1;
|
||||
|
||||
@@ -144,6 +144,10 @@ final class VariableRefTest extends TestCase
|
||||
'opacity',
|
||||
],
|
||||
'protected meta key' => [self::content('custom_meta__edit_lock', '{}'), 'custom_meta__edit_lock'],
|
||||
'doubled meta prefix' => [
|
||||
self::content('custom_meta_custom_meta__edit_lock', '{}'),
|
||||
'custom_meta_custom_meta__edit_lock',
|
||||
],
|
||||
'loop content outside a loop' => [self::content('loop_post_title', '{}'), 'loop_post_title'],
|
||||
'unknown dynamic content' => [self::content('post_meta_key', '{}'), 'post_meta_key'],
|
||||
'extra top-level key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{}},"x":1})$', 'x'],
|
||||
@@ -296,6 +300,77 @@ final class VariableRefTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, array{string}> */
|
||||
public static function derivedMetaKeyBypasses(): array
|
||||
{
|
||||
return [
|
||||
'doubled prefix in the name' => [self::content('custom_meta_custom_meta__edit_lock', '{}')],
|
||||
'tripled prefix in the name' => [self::content('custom_meta_custom_meta_custom_meta__edit_lock', '{}')],
|
||||
'prefix rebuilt by the strip' => [self::content('custom_meta_custom_mcustom_meta_eta_telefon', '{}')],
|
||||
'loop prefix in the name' => [self::content('custom_meta_loop_post_meta_key__edit_lock', '{}')],
|
||||
'doubled prefix in select_meta_key' => [
|
||||
self::content('post_meta_key', '{"select_meta_key":"custom_meta_custom_meta__wp_page_template"}'),
|
||||
],
|
||||
'doubled prefix in meta_key' => [self::content('post_meta_key', '{"meta_key":"custom_meta_custom_meta__x"}')],
|
||||
'loop post prefix' => [self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key__edit_lock"}')],
|
||||
'loop user prefix' => [self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_user_meta_key__session_tokens"}')],
|
||||
'loop term prefix' => [self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_term_meta_key__secret"}')],
|
||||
'doubled loop prefix' => [
|
||||
self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key_loop_post_meta_key__edit_lock"}'),
|
||||
],
|
||||
'mixed loop and meta prefix' => [
|
||||
self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key_custom_meta__edit_lock"}'),
|
||||
],
|
||||
'mixed meta and loop prefix' => [
|
||||
self::content('post_meta_key', '{"select_meta_key":"custom_meta_loop_term_meta_key__secret"}'),
|
||||
],
|
||||
'two loop prefixes' => [
|
||||
self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_user_meta_key_loop_post_meta_key_telefon"}'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling S30: Divi derives the meta key with str_replace of every prefix occurrence
|
||||
* (DynamicContentOptionPostMetaKey.php:232,275; DynamicContentOptionLoopPostMetaKey.php:366-373).
|
||||
*
|
||||
* @dataProvider derivedMetaKeyBypasses
|
||||
*/
|
||||
public function testMetaKeysAreCheckedAfterDivisDerivation(string $value): void
|
||||
{
|
||||
$ref = self::ref([
|
||||
'post_meta_key' => ['meta_key' => 'text', 'select_meta_key' => 'select'],
|
||||
'loop_post_meta_key' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'],
|
||||
]);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
$ref->canonical($value, ComposeFixture::sanitizer());
|
||||
}
|
||||
|
||||
public function testDerivedMetaKeysThatAreFineStayAccepted(): void
|
||||
{
|
||||
$ref = self::ref([
|
||||
'post_meta_key' => ['meta_key' => 'text', 'select_meta_key' => 'select'],
|
||||
'loop_post_meta_key' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'],
|
||||
]);
|
||||
$sanitizer = ComposeFixture::sanitizer();
|
||||
|
||||
foreach ([
|
||||
self::content('custom_meta_telefon', '{}'),
|
||||
self::content('post_meta_key', '{"select_meta_key":"custom_meta_fax"}'),
|
||||
self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key_telefon"}'),
|
||||
self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_user_meta_key_first_name"}'),
|
||||
self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_term_meta_key_farbe"}'),
|
||||
self::content(
|
||||
'loop_post_meta_key',
|
||||
'{"select_loop_meta_key":"loop_post_meta_key_manual_custom_field_value","loop_meta_key":"telefon"}'
|
||||
),
|
||||
] as $value) {
|
||||
self::assertSame($value, $ref->canonical($value, $sanitizer));
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, array{array<array-key, mixed>}> */
|
||||
public static function malformedMaps(): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user