fix: Task 1 review round 3 - re-check $variable marker after sanitizing, meta-key rule
Critical-1/S28: sanitizers that strip <x>, NUL or %XX could rebuild the $variable( marker after the raw check; every cleaned string (AttrWriter at every depth, VariableRef settings) is checked again. S29: custom_meta_ map names and *meta_key setting values obey META_KEY. Minor-2: malformed dynamic content maps fail with InvalidArgumentException. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -180,15 +180,24 @@ final class AttrWriter
|
||||
|
||||
$kind = $this->kind($module, $path, $subName, $key);
|
||||
if (VariableRef::contains($value)) {
|
||||
// The only way a reference is stored: whole, straight from canonical().
|
||||
return $this->placeholders->references()->canonical($value, $this->sanitizer, $kind === 'url');
|
||||
}
|
||||
|
||||
return match ($kind) {
|
||||
$clean = match ($kind) {
|
||||
'html' => $this->sanitizer->html($value),
|
||||
'code' => $this->sanitizer->code($value),
|
||||
'url' => $this->sanitizer->url($value),
|
||||
default => $this->sanitizer->text($value),
|
||||
};
|
||||
|
||||
// Ruling S28: sanitizers strip characters ("<x>", NUL, "%41") and can thereby build a
|
||||
// marker the raw check above never saw – so the final string is checked again.
|
||||
if (VariableRef::contains($clean)) {
|
||||
throw VariableRef::notAlone();
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,7 +34,10 @@ use stdClass;
|
||||
* Default (null): DEFAULT_DYNAMIC_CONTENT. Task 5 builds the map from Divi's live registry
|
||||
* 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, no string may contain a reference or ")$".
|
||||
* 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.
|
||||
*/
|
||||
final class VariableRef
|
||||
{
|
||||
@@ -83,9 +86,15 @@ final class VariableRef
|
||||
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.
|
||||
*/
|
||||
public function __construct(private DesignSystem $design, ?array $dynamicContent = null)
|
||||
{
|
||||
$this->dynamicContent = $dynamicContent ?? self::DEFAULT_DYNAMIC_CONTENT;
|
||||
$this->dynamicContent = $dynamicContent === null
|
||||
? self::DEFAULT_DYNAMIC_CONTENT
|
||||
: self::checkMap($dynamicContent);
|
||||
}
|
||||
|
||||
/** Whether $value contains a reference anywhere (such a value may only be a whole reference). */
|
||||
@@ -254,16 +263,17 @@ final class VariableRef
|
||||
*/
|
||||
private function contentFields(string $name): array
|
||||
{
|
||||
if (isset($this->dynamicContent[$name]) && is_array($this->dynamicContent[$name])) {
|
||||
return $this->dynamicContent[$name];
|
||||
}
|
||||
|
||||
// Ruling S29: the meta key rule holds for map entries too.
|
||||
if (str_starts_with($name, self::META_PREFIX)) {
|
||||
if (preg_match(self::META_KEY, substr($name, strlen(self::META_PREFIX))) !== 1) {
|
||||
if (! self::isMetaKey($name)) {
|
||||
throw self::unknownName($name, 'content');
|
||||
}
|
||||
|
||||
return self::META_SETTINGS;
|
||||
return $this->dynamicContent[$name] ?? self::META_SETTINGS;
|
||||
}
|
||||
|
||||
if (isset($this->dynamicContent[$name])) {
|
||||
return $this->dynamicContent[$name];
|
||||
}
|
||||
|
||||
if (str_starts_with($name, 'loop_')) {
|
||||
@@ -301,7 +311,8 @@ final class VariableRef
|
||||
throw new InvalidArgumentException(sprintf('Einstellung "%s" im Divi-Verweis: nur Text.', $key));
|
||||
}
|
||||
|
||||
return $fieldType === 'text' ? $sanitizer->text($value) : $sanitizer->html($value);
|
||||
$clean = $fieldType === 'text' ? $sanitizer->text($value) : $sanitizer->html($value);
|
||||
break;
|
||||
default:
|
||||
if (! is_scalar($value) || (is_string($value) && preg_match('/[<>]/', $value) === 1)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
@@ -310,8 +321,60 @@ final class VariableRef
|
||||
));
|
||||
}
|
||||
|
||||
return is_string($value) ? $sanitizer->text($value) : $value;
|
||||
$clean = is_string($value) ? $sanitizer->text($value) : $value;
|
||||
}
|
||||
|
||||
// Ruling S28: sanitizing may rebuild a marker ("$variable<x>(" → "$variable(").
|
||||
if (is_string($clean) && (self::contains($clean) || str_contains($clean, ')$'))) {
|
||||
throw self::notAlone();
|
||||
}
|
||||
|
||||
// Ruling S29: meta key settings (post_meta_key's meta_key/select_meta_key, loop_meta_key …)
|
||||
// name a meta key – never a protected one.
|
||||
if (str_ends_with($key, 'meta_key') && $clean !== '' && ! self::isMetaKey((string) $clean)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Einstellung "%s" im Divi-Verweis: ungültiger Meta-Schlüssel (Buchstaben, Ziffern, "_" und "-", '
|
||||
. 'nicht mit "_" am Anfang).',
|
||||
$key
|
||||
));
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/** "telefon", "custom_meta_telefon" yes; "_edit_lock", "custom_meta__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));
|
||||
}
|
||||
|
||||
return preg_match(self::META_KEY, $key) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $map
|
||||
* @return array<string, array<string, string>>
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private static function checkMap(array $map): array
|
||||
{
|
||||
foreach ($map as $name => $fields) {
|
||||
if (! is_string($name) || ! is_array($fields)) {
|
||||
throw new InvalidArgumentException('Dynamic content map: erwartet Name => [Einstellung => Feldtyp].');
|
||||
}
|
||||
foreach ($fields as $key => $type) {
|
||||
if (! is_string($key) || ! is_string($type)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Dynamic content map "%s": erwartet Einstellung => Feldtyp (Text).',
|
||||
$name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, string>> $map */
|
||||
return $map;
|
||||
}
|
||||
|
||||
private static function unknownName(string $name, string $type): InvalidArgumentException
|
||||
|
||||
Reference in New Issue
Block a user