*/ public static function urlProvider(): array { return [ // --- akzeptiert ------------------------------------------ 'https' => ['https://www.beispiel.de/pfad?a=1#anker', true], 'http' => ['http://beispiel.de', true], 'IP-Adresse' => ['http://192.168.1.10:8080/x', true], 'IPv6' => ['https://[2001:db8::1]/x', true], 'IPv6 mit Port' => ['https://[2001:db8::1]:8443/x', true], 'localhost' => ['http://localhost:8080/test', true], 'Umlaut-Domain' => ['https://müller.de', true], 'Umlaut-Domain mit Pfad' => ['https://münchen.de/veranstaltung', true], 'Punycode-Domain' => ['https://xn--mller-kva.de', true], 'Umlaute im Pfad' => ['https://de.wikipedia.org/wiki/München', true], 'Umlaute im Query' => ['https://beispiel.de/suche?q=Grüße&x=1', true], 'interner Host ohne Punkt' => ['http://intranet/seite', true], 'interner Host mit Port' => ['http://nas:5000/freigabe', true], 'lange TLD' => ['https://beispiel.photography', true], 'Benutzerinfo' => ['https://user:pass@beispiel.de/geschuetzt', true], 'absoluter Punkt am Ende' => ['https://beispiel.de./x', true], 'Bindestrich in der Domain' => ['https://mein-shop.de', true], // --- abgelehnt ------------------------------------------- 'leer' => ['', false], 'ftp' => ['ftp://beispiel.de/datei.zip', false], 'javascript' => ['javascript:alert(1)', false], 'data-URI' => ['data:text/html,', false], 'mailto' => ['mailto:info@beispiel.de', false], 'file' => ['file:///etc/passwd', false], 'ohne Host' => ['https://', false], 'Steuerzeichen' => ["https://beispiel.de/\nX", false], 'zu lang' => ['https://beispiel.de/' . str_repeat('a', 2100), false], 'Bindestrich am Labelanfang' => ['https://-beispiel.de', false], 'Bindestrich am Labelende' => ['https://beispiel-.de', false], 'leeres Label' => ['https://beispiel..de', false], 'ungueltiger Port' => ['https://beispiel.de:99999/x', false], 'Unterstrich im Host' => ['https://mein_host.de', false], ]; } /** * @dataProvider urlProvider */ public function testValidation(string $url, bool $expected): void { self::assertSame($expected, UrlValidator::isValid($url), $url); self::assertSame($expected, UrlValidator::validationError($url) === null); } public function testNormalizeAddsHttpsWhenSchemeIsMissing(): void { self::assertSame('https://beispiel.de/pfad', UrlValidator::normalize('beispiel.de/pfad')); self::assertSame('https://www.beispiel.de', UrlValidator::normalize(' www.beispiel.de ')); self::assertSame('https://beispiel.de', UrlValidator::normalize('//beispiel.de')); self::assertSame('https://müller.de', UrlValidator::normalize('müller.de')); } public function testNormalizeKeepsExistingScheme(): void { self::assertSame('http://beispiel.de', UrlValidator::normalize('http://beispiel.de')); self::assertSame('https://beispiel.de', UrlValidator::normalize('https://beispiel.de')); } public function testNormalizeLowercasesSchemeAndHostOnly(): void { self::assertSame( 'https://beispiel.de/Pfad/Mit/Grossbuchstaben', UrlValidator::normalize('HTTPS://Beispiel.DE/Pfad/Mit/Grossbuchstaben'), ); } public function testNormalizeKeepsTheReadableHost(): void { // Punycode gehoert erst in den Location-Header, nicht in die Anzeige. self::assertSame('https://münchen.de/fest', UrlValidator::normalize('https://münchen.de/fest')); } public function testNormalizePercentEncodesPathAndQuery(): void { self::assertSame( 'https://de.wikipedia.org/wiki/M%C3%BCnchen', UrlValidator::normalize('https://de.wikipedia.org/wiki/München'), ); self::assertSame( 'https://beispiel.de/suche?q=Gr%C3%BC%C3%9Fe&x=1', UrlValidator::normalize('https://beispiel.de/suche?q=Grüße&x=1'), ); self::assertSame( 'https://beispiel.de/a%20b', UrlValidator::normalize('https://beispiel.de/a b'), ); } public function testNormalizeDoesNotDoubleEncode(): void { $alreadyEncoded = 'https://de.wikipedia.org/wiki/M%C3%BCnchen'; self::assertSame($alreadyEncoded, UrlValidator::normalize($alreadyEncoded)); // Zweifaches Normalisieren aendert nichts mehr. self::assertSame( UrlValidator::normalize('https://de.wikipedia.org/wiki/München'), UrlValidator::normalize(UrlValidator::normalize('https://de.wikipedia.org/wiki/München')), ); } public function testNormalizeKeepsReservedCharactersUsable(): void { $url = 'https://beispiel.de/a/b?x=1&y=2#teil-3'; self::assertSame($url, UrlValidator::normalize($url)); } public function testNormalizeDoesNotTurnOtherSchemesIntoHttps(): void { $normalized = UrlValidator::normalize('javascript:alert(1)'); self::assertSame('javascript:alert(1)', $normalized); self::assertFalse(UrlValidator::isValid($normalized)); } public function testNormalizedUserInputPassesValidation(): void { foreach (['beispiel.de', 'www.beispiel.de/a/b?c=d', 'müller.de', 'intranet/seite'] as $input) { self::assertTrue( UrlValidator::isValid(UrlValidator::normalize($input)), $input, ); } } public function testToAsciiUrlConvertsOnlyTheHost(): void { self::assertSame( 'https://xn--mller-kva.de/gr%C3%BC%C3%9Fe', UrlValidator::toAsciiUrl('https://müller.de/gr%C3%BC%C3%9Fe'), ); self::assertSame( 'https://www.xn--mnchen-3ya.de:8080/a?b=1#c', UrlValidator::toAsciiUrl('https://www.münchen.de:8080/a?b=1#c'), ); } public function testToAsciiUrlLeavesAsciiUrlsAlone(): void { foreach ([ 'https://beispiel.de/pfad?a=1#b', 'http://192.168.1.10:8080/x', 'https://[2001:db8::1]/x', 'http://intranet/seite', ] as $url) { self::assertSame($url, UrlValidator::toAsciiUrl($url)); } } public function testToAsciiUrlResultIsAlwaysAscii(): void { foreach (['https://müller.de/x', 'https://grüße.example.com/ä', 'https://münchen.de'] as $url) { $ascii = UrlValidator::toAsciiUrl(UrlValidator::normalize($url)); self::assertSame( 1, preg_match('/^[\x20-\x7E]+$/', $ascii), 'Location-Header muss ASCII sein: ' . $ascii, ); } } public function testErrorMessagesAreHelpful(): void { self::assertStringContainsString('http', (string) UrlValidator::validationError('ftp://beispiel.de')); self::assertStringContainsString('http', (string) UrlValidator::validationError('javascript:alert(1)')); self::assertStringContainsString('Ziel-URL', (string) UrlValidator::validationError('')); self::assertStringContainsString('Hostnamen', (string) UrlValidator::validationError('https://')); } }