fix: anchored input patterns end at the last character (/D)

Without /D, "$" also matches before a trailing "\n", so "custom_meta_telefon\n",
a color setting "5\n" (stored as sent) or a whole reference followed by "\n"
passed VariableRef. Added /D to every ^...$ pattern that validates client or
admin input:
- VariableRef META_KEY, NUMBER, WHOLE (canonical/typeOf; Placeholders::field
  shares META_KEY)
- Cursor::decode, GuideRepository topic names (MCP input and filter keys)
- ExposurePolicy::sanitize ability keys and group ids, LogTab list filters

Not changed: Placeholders::PATTERN (runs on trim()), Warnings hex checks (hints
only), NodeSummary/AttrPathIndex/LogListTable (stored or Divi data, no client
input). Each fixed pattern has a test with a trailing "\n" that is rejected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 18:02:51 +02:00
co-authored by Claude Opus 5.5
parent 60290c85a5
commit e420145cbe
10 changed files with 81 additions and 9 deletions
@@ -138,6 +138,33 @@ final class VariableRefTest extends TestCase
self::ref()->canonical($input, ComposeFixture::sanitizer());
}
/**
* "$" without /D also matches before a trailing newline: the whole-reference pattern, a meta
* key name and a color setting number must each end with their last character.
*/
public function testATrailingNewlineIsNeverAnAnchorMatch(): void
{
$color = VariableRef::serialize('color', 'gcid-a1b2', []);
$rejects = [
'whole reference' => [$color . "\n", 'Verweise nur allein im Wert'],
'meta key name' => [
VariableRef::serialize('content', "custom_meta_telefon\n", ['before' => '', 'after' => '']),
'Unbekannter Name',
],
'color number' => [VariableRef::serialize('color', 'gcid-a1b2', ['hue' => "5\n"]), 'nur Zahlen'],
];
foreach ($rejects as $label => [$input, $message]) {
try {
self::ref()->canonical($input, ComposeFixture::sanitizer());
self::fail($label . ': accepted');
} catch (InvalidArgumentException $e) {
self::assertStringContainsString($message, $e->getMessage(), $label);
}
}
self::assertNull(VariableRef::typeOf($color . "\n"));
}
/** I-1: the escape really is in the input, and nothing with ")$" inside comes out. */
public function testEscapedEndMarkerIsBytesNotTheCharacter(): void
{