['innerContent' => ['desktop' => ['value' => 'Müller GmbH'], 'phone' => ['value' => 'Müller']]], 'content' => ['innerContent' => ['desktop' => ['value' => '
Seit 1987: Müller & Söhne. Anruf bei MÜLLER.
']]], 'button' => ['innerContent' => ['desktop' => ['value' => ['text' => 'Müller anrufen', 'linkUrl' => '/mueller-kontakt']]]], 'module' => ['meta' => ['adminLabel' => ['desktop' => ['value' => 'Müller-Hero']]]], 'dyn' => ['innerContent' => ['desktop' => ['value' => '$variable({"type":"content","value":{"name":"Müller","settings":{}}})$']]], ]; private const REF = '$variable({"type":"content","value":{"name":"post_title","settings":{}}})$'; /** @return callable(string, ?string, string): string */ private static function same(): callable { return static fn (string $p, ?string $k, string $v): string => $v; } /** @param arraySeit 1987: Schmidt & Söhne. Anruf bei MÜLLER.
', $result['attrs']['content']['innerContent']['desktop']['value']); self::assertSame(['text' => 'Schmidt anrufen', 'linkUrl' => '/mueller-kontakt'], $result['attrs']['button']['innerContent']['desktop']['value']); self::assertSame('Müller-Hero', $result['attrs']['module']['meta']['adminLabel']['desktop']['value']); self::assertSame(self::ATTRS['dyn'], $result['attrs']['dyn']); self::assertContains('button.innerContent|text', $seen); self::assertSame(['path' => 'button.innerContent.text', 'before' => 'Müller anrufen', 'after' => 'Schmidt anrufen'], $result['samples'][0]); } public function testCaseInsensitiveIncludingUmlauts(): void { $result = (new TextReplacer('müller', 'Schmidt', false))->apply(self::ATTRS, self::same()); self::assertSame(5, $result['count']); self::assertStringContainsString('Anruf bei Schmidt.', $result['attrs']['content']['innerContent']['desktop']['value']); } public function testNothingFound(): void { $result = (new TextReplacer('Meier', 'X'))->apply(self::ATTRS, self::same()); self::assertSame(0, $result['count']); self::assertSame(self::ATTRS, $result['attrs']); } /** Ruling S8: tags with their attributes, entities and references are never touched. */ public function testTagsEntitiesAndReferencesStayUntouched(): void { $value = 'Kontakt & ' . 'kontakt@kunde.test ' . self::REF . ' amp
'; $result = (new TextReplacer('kontakt', 'Anfrage', false))->apply(self::text($value), self::same()); self::assertSame( 'Anfrage & ' . 'Anfrage@kunde.test ' . self::REF . ' amp
', self::valueOf($result['attrs']) ); self::assertSame(2, $result['count']); $entity = (new TextReplacer('amp', 'X'))->apply(self::text($value), self::same()); self::assertStringContainsString('& ', (string) self::valueOf($entity['attrs'])); self::assertStringEndsWith(self::REF . ' X', (string) self::valueOf($entity['attrs'])); self::assertSame(1, $entity['count']); $inside = (new TextReplacer('post_title', 'post_date'))->apply(self::text('Titel: ' . self::REF), self::same()); self::assertSame(0, $inside['count'], 'text inside an embedded reference is not replaced'); } /** Script and style bodies are code, not visible text (D10: never styles). */ public function testScriptAndStyleBodiesAreNotText(): void { $value = 'Müller
' . ' Müller'; $result = (new TextReplacer('Müller', 'Schmidt'))->apply(self::text($value), self::same()); self::assertSame( 'Schmidt
' . ' Schmidt', self::valueOf($result['attrs']) ); self::assertSame(2, $result['count']); $unclosed = (new TextReplacer('Müller', 'Schmidt'))->apply(self::text('' . ''; $result = (new TextReplacer('Kontakt', 'Anfrage'))->apply(self::text($value), self::same()); self::assertSame(1, $result['count']); self::assertStringEndsWith('', (string) self::valueOf($result['attrs'])); } /** Search == replacement (or a cleaned replacement equal to the match) is no change and no hit. */ public function testNoEffectiveChangeIsNoHit(): void { $same = (new TextReplacer('Müller', 'Müller'))->apply(self::ATTRS, self::same()); $cleanedAway = (new TextReplacer('Müller', 'Müller'))->apply( self::ATTRS, static fn (string $p, ?string $k, string $v): string => strip_tags($v) ); $partly = (new TextReplacer('müller', 'Müller', false))->apply(self::ATTRS, self::same()); self::assertSame(0, $same['count']); self::assertSame(self::ATTRS, $same['attrs']); self::assertSame(0, $cleanedAway['count']); self::assertSame(self::ATTRS, $cleanedAway['attrs']); self::assertSame(1, $partly['count'], 'only MÜLLER changes'); } /** * Ruling S45 malicious rows: either refused (exception) or no change at all. * * @return array
", ' B', "' onerror='alert(1)"],
'href rewrite after quoted ">"' => ['Link', 'kontakt', 'javascript:alert(1)'],
'comment with ">"' => ['x
', ' B', '-->x
', ' B', 'x'], 'unterminated comment' => ['x B
', ' B', 'y'], 'unterminated reference' => ['$variable({"type":"content"} B', ' B', ')$'], ]; } /** @dataProvider s45 */ public function testS45RowsAreRefusedOrUnchanged(string $value, string $search, string $replace): void { try { $result = (new TextReplacer($search, $replace))->apply(self::text($value), self::same()); } catch (InvalidArgumentException $e) { self::assertStringContainsString('content.innerContent', $e->getMessage()); return; } self::assertSame(0, $result['count']); self::assertSame($value, self::valueOf($result['attrs'])); } /** @return array