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
@@ -29,4 +29,10 @@ interface ValueSanitizer
* and attribute fields (*.advanced.html, *.decoration.attributes) need it (ruling S7).
*/
public function allowsUnfilteredHtml(): bool;
/**
* Whether the current user may read post $id: select_post settings of dynamic content
* references (post_link_url_*) name a post whose link Divi renders (ruling S37).
*/
public function canReadPost(int $id): bool;
}
+39 -4
View File
@@ -29,8 +29,10 @@ use stdClass;
* Dynamic content map (ruling S22), constructor argument $dynamicContent:
* array<string, array<string, string>> option name => [setting key => field type]
* e.g. ['post_title' => ['before' => 'text', 'after' => 'text'], 'post_date' => [… 'date_format' => 'select']]
* Field types: "text" → ValueSanitizer::text(); "richtext" or "html" → html(); anything else
* (number, select, …) → a scalar without markup ("<", ">"), strings cleaned with text().
* Field types: "text" → ValueSanitizer::text(); "richtext" or "html" → html(); "url" → url()
* (refused when it empties a value); "select_post" → a numeric id of a post the user may read;
* "select", "yes_no_button", "number" → a scalar without markup ("<", ">"), strings cleaned with
* text(); any other type is refused (the map comes from a filter – untrusted, ruling S37).
* 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
@@ -371,13 +373,38 @@ final class VariableRef
case 'text':
case 'richtext':
case 'html':
case 'url':
if (! is_string($value)) {
throw new InvalidArgumentException(sprintf('Einstellung "%s" im Divi-Verweis: nur Text.', $key));
}
$clean = $fieldType === 'text' ? $sanitizer->text($value) : $sanitizer->html($value);
$clean = match ($fieldType) {
'text' => $sanitizer->text($value),
'url' => $sanitizer->url($value),
default => $sanitizer->html($value),
};
if ($fieldType === 'url' && $clean === '' && trim($value) !== '') {
throw new InvalidArgumentException(
sprintf('Einstellung "%s" im Divi-Verweis: ungültige URL.', $key)
);
}
break;
default:
case 'select_post':
// Ruling S37: a numeric post id the user may read – no slugs (Divi resolves those itself).
$digits = is_string($value) && preg_match('/^[1-9][0-9]*$/D', $value) === 1;
$id = is_int($value) ? $value : ($digits ? (int) $value : 0);
if ($id <= 0 || ! $sanitizer->canReadPost($id)) {
throw new InvalidArgumentException(sprintf(
'Einstellung "%s" im Divi-Verweis: Beitrags-ID eines lesbaren Eintrags erwartet.',
$key
));
}
$clean = $value;
break;
case 'select':
case 'yes_no_button':
case 'number':
if (! is_scalar($value) || (is_string($value) && preg_match('/[<>]/', $value) === 1)) {
throw new InvalidArgumentException(sprintf(
'Einstellung "%s" im Divi-Verweis muss ein einfacher Wert ohne Markup sein.',
@@ -386,6 +413,14 @@ final class VariableRef
}
$clean = is_string($value) ? $sanitizer->text($value) : $value;
break;
default:
// Ruling S37: map types come from a filter (untrusted) – unknown types are refused.
throw new InvalidArgumentException(sprintf(
'Einstellung "%s" im Divi-Verweis: Feldtyp "%s" wird nicht unterstützt.',
$key,
$fieldType
));
}
// Ruling S28: sanitizing may rebuild a marker ("$variable<x>(" → "$variable("). After the