'https', 'host' => 'example.com', 'path' => '/path/to/page', 'query' => 'param=value', 'fragment' => 'section' ]; $result = UrlHelper::build_url($parts); $this->assertEquals('https://example.com/path/to/page?param=value#section', $result); } public function testBuildUrlWithMinimalParts(): void { $parts = [ 'scheme' => 'http', 'host' => 'example.com' ]; $result = UrlHelper::build_url($parts); $this->assertEquals('http://example.com', $result); } public function testBuildUrlWithPathOnly(): void { $parts = [ 'scheme' => 'https', 'host' => 'example.com', 'path' => '/about' ]; $result = UrlHelper::build_url($parts); $this->assertEquals('https://example.com/about', $result); } public function testBuildUrlWithNonStandardPort(): void { $parts = [ 'scheme' => 'http', 'host' => 'example.org', 'port' => 8080 ]; $result = UrlHelper::build_url($parts); $this->assertEquals('http://example.org:8080', $result); } public function testBuildUrlWithNonStandardPortAndPath(): void { $parts = [ 'scheme' => 'http', 'host' => 'example.org', 'port' => 8080, 'path' => '/test.jpg' ]; $result = UrlHelper::build_url($parts); $this->assertEquals('http://example.org:8080/test.jpg', $result); } public function testBuildUrlWithNonStandardPortAndAllParts(): void { $parts = [ 'scheme' => 'https', 'host' => 'example.org', 'port' => 8443, 'path' => '/api/endpoint', 'query' => 'key=value', 'fragment' => 'section' ]; $result = UrlHelper::build_url($parts); $this->assertEquals('https://example.org:8443/api/endpoint?key=value#section', $result); } public function testBuildUrlWithStandardHttpPort(): void { $parts = [ 'scheme' => 'http', 'host' => 'example.com', 'port' => 80 ]; $result = UrlHelper::build_url($parts); $this->assertEquals('http://example.com:80', $result); } public function testBuildUrlWithStandardHttpsPort(): void { $parts = [ 'scheme' => 'https', 'host' => 'example.com', 'port' => 443 ]; $result = UrlHelper::build_url($parts); $this->assertEquals('https://example.com:443', $result); } public function testBuildUrlWithPortAsString(): void { $parts = [ 'scheme' => 'http', 'host' => 'example.org', 'port' => '8080' ]; $result = UrlHelper::build_url($parts); $this->assertEquals('http://example.org:8080', $result); } // ===== rewrite_relative() - Absolute URLs ===== public function testRewriteRelativeWithAbsoluteUrl(): void { $base = 'https://example.com/page'; $rel = 'https://other.com/resource'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('https://other.com/resource', $result); } public function testRewriteRelativeWithEmptyRelativeUrl(): void { $base = 'https://example.com/page'; $rel = ''; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals($base, $result); } // ===== rewrite_relative() - Protocol-Relative URLs ===== public function testRewriteRelativeWithProtocolRelativeUrl(): void { $base = 'https://example.com/page'; $rel = '//cdn.example.com/resource.js'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('https://cdn.example.com/resource.js', $result); } // ===== rewrite_relative() - Relative Paths ===== public function testRewriteRelativeWithRelativePath(): void { $base = 'https://example.com/blog/post'; $rel = 'image.jpg'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('https://example.com/blog/image.jpg', $result); } public function testRewriteRelativeWithAbsolutePath(): void { $base = 'https://example.com/blog/post'; $rel = '/assets/style.css'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('https://example.com/assets/style.css', $result); } public function testRewriteRelativeWithDotSlashPath(): void { $base = 'https://example.com/blog/post'; $rel = './image.png'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('https://example.com/blog/image.png', $result); } public function testRewriteRelativeWithBaseUrlEndingInSlash(): void { $base = 'https://example.com/blog/'; $rel = 'image.jpg'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('https://example.com/blog/image.jpg', $result); } // ===== rewrite_relative() - Port Preservation ===== public function testRewriteRelativePreservesPortFromBaseUrl(): void { $base = 'http://example.org:8080/blog/post'; $rel = 'image.jpg'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('http://example.org:8080/blog/image.jpg', $result); } public function testRewriteRelativePreservesPortWithAbsolutePath(): void { $base = 'http://example.org:8080/blog/post'; $rel = '/assets/image.jpg'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('http://example.org:8080/assets/image.jpg', $result); } public function testRewriteRelativePreservesPortWithDotSlashPath(): void { $base = 'http://example.org:8080/blog/post'; $rel = './image.jpg'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('http://example.org:8080/blog/image.jpg', $result); } public function testRewriteRelativePreservesPortWithQueryString(): void { $base = 'http://example.org:8080/blog/post'; $rel = 'api/data?key=value'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('http://example.org:8080/blog/api/data?key=value', $result); } public function testRewriteRelativePreservesNonStandardHttpsPort(): void { $base = 'https://example.org:8443/feed.xml'; $rel = 'article.html'; $result = UrlHelper::rewrite_relative($base, $rel); $this->assertEquals('https://example.org:8443/article.html', $result); } public function testRewriteRelativeWithBaseUrlPortButAbsoluteRelUrl(): void { $base = 'http://example.org:8080/blog/post'; $rel = 'https://other.com/resource'; $result = UrlHelper::rewrite_relative($base, $rel); // Absolute URLs should not inherit port from base $this->assertEquals('https://other.com/resource', $result); } public function testRewriteRelativeWithBaseUrlPortAndProtocolRelativeUrl(): void { $base = 'http://example.org:8080/blog/post'; $rel = '//cdn.example.com/resource.js'; $result = UrlHelper::rewrite_relative($base, $rel); // Protocol-relative URLs should not inherit port from base $this->assertEquals('https://cdn.example.com/resource.js', $result); } // ===== rewrite_relative() - Special Schemes ===== public function testRewriteRelativeAllowsMagnetForAnchorHref(): void { $base = 'https://example.com/page'; $rel = 'magnet:?xt=urn:btih:123'; $result = UrlHelper::rewrite_relative($base, $rel, 'a', 'href'); $this->assertEquals('magnet:?xt=urn:btih:123', $result); } public function testRewriteRelativeAllowsMailtoForAnchorHref(): void { $base = 'https://example.com/page'; $rel = 'mailto:test@example.com'; $result = UrlHelper::rewrite_relative($base, $rel, 'a', 'href'); $this->assertEquals('mailto:test@example.com', $result); } public function testRewriteRelativeAllowsTelForAnchorHref(): void { $base = 'https://example.com/page'; $rel = 'tel:+1234567890'; $result = UrlHelper::rewrite_relative($base, $rel, 'a', 'href'); $this->assertEquals('tel:+1234567890', $result); } public function testRewriteRelativeRejectsMagnetForNonAnchor(): void { $base = 'https://example.com/page'; $rel = 'magnet:?xt=urn:btih:123'; $result = UrlHelper::rewrite_relative($base, $rel, 'img', 'src'); // Should rewrite as relative path since not allowed for img $this->assertNotEquals('magnet:?xt=urn:btih:123', $result); } // ===== rewrite_relative() - Data URLs for Images ===== public function testRewriteRelativeAllowsDataUrlForImgSrc(): void { $base = 'https://example.com/page'; $rel = 'data:image/png;base64,iVBORw0KGgo='; $result = UrlHelper::rewrite_relative($base, $rel, 'img', 'src'); $this->assertEquals('data:image/png;base64,iVBORw0KGgo=', $result); } public function testRewriteRelativeAllowsDataUrlWithWebp(): void { $base = 'https://example.com/page'; $rel = 'data:image/webp;base64,UklGRiQAAABXRUJQ'; $result = UrlHelper::rewrite_relative($base, $rel, 'img', 'src'); $this->assertEquals('data:image/webp;base64,UklGRiQAAABXRUJQ', $result); } public function testRewriteRelativeAllowsDataUrlWithSvg(): void { $base = 'https://example.com/page'; $rel = 'data:image/svg;base64,PHN2ZyB4bWxucz0iaHR0cDov'; $result = UrlHelper::rewrite_relative($base, $rel, 'img', 'src'); $this->assertEquals('data:image/svg;base64,PHN2ZyB4bWxucz0iaHR0cDov', $result); } public function testRewriteRelativeRejectsDataUrlForNonImage(): void { $base = 'https://example.com/page'; $rel = 'data:text/html;base64,PHRpdGxlPg=='; $result = UrlHelper::rewrite_relative($base, $rel, 'img', 'src'); // Regex only checks for image/* at start, so non-image MIME types get rewritten as relative paths $this->assertStringContainsString('text/html', $result); $this->assertNotEquals($rel, $result); // Should not return as-is } public function testRewriteRelativeRejectsDataUrlForNonImgElement(): void { $base = 'https://example.com/page'; $rel = 'data:image/png;base64,iVBORw0KGgo='; $result = UrlHelper::rewrite_relative($base, $rel, 'a', 'href'); // Data URLs for non-img elements get rewritten as relative paths $this->assertStringContainsString('image/png', $result); $this->assertNotEquals($rel, $result); // Should not return as-is } // ===== rewrite_relative() - Content Type-Based Schemes ===== public function testRewriteRelativeAllowsMagnetForBittorrentContentType(): void { $base = 'https://example.com/page'; $rel = 'magnet:?xt=urn:btih:456'; $result = UrlHelper::rewrite_relative($base, $rel, '', '', 'application/x-bittorrent'); $this->assertEquals('magnet:?xt=urn:btih:456', $result); } // ===== rewrite_relative() - Invalid URLs ===== public function testRewriteRelativeWithInvalidCharactersInUrl(): void { $base = 'https://example.com/page'; $rel = 'ht!tp://invalid url with spaces'; $result = UrlHelper::rewrite_relative($base, $rel); // Invalid URLs without scheme/host are treated as relative paths $this->assertStringContainsString('example.com', $result); $this->assertNotEquals($rel, $result); } // ===== validate() - Basic Validation ===== public function testValidateAcceptsValidHttpsUrl(): void { $result = UrlHelper::validate('https://example.com/page'); $this->assertEquals('https://example.com/page', $result); } public function testValidateAcceptsValidHttpUrl(): void { $result = UrlHelper::validate('http://example.com/page'); $this->assertEquals('http://example.com/page', $result); } public function testValidateFixesProtocolRelativeUrl(): void { $result = UrlHelper::validate('//example.com/resource'); $this->assertEquals('https://example.com/resource', $result); } public function testValidateRejectsUrlWithoutHost(): void { $result = UrlHelper::validate('http:///path'); $this->assertFalse($result); } public function testValidateRejectsMissingScheme(): void { $result = UrlHelper::validate('example.com/page'); $this->assertFalse($result); } public function testValidateRejectsInvalidScheme(): void { $result = UrlHelper::validate('ftp://example.com/file'); $this->assertFalse($result); } public function testValidateCleansUrl(): void { $result = UrlHelper::validate(' https://example.com/page '); // clean() should strip tags and trim $this->assertEquals('https://example.com/page', $result); } // ===== has_disallowed_ip() Tests ===== public function testIsDisallowedIpDetectsLocalhost(): void { $this->assertTrue(UrlHelper::has_disallowed_ip('http://localhost/path')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://localhost:8080/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://LOCALHOST/')); } public function testIsDisallowedIpDetects127Loopback(): void { $this->assertTrue(UrlHelper::has_disallowed_ip('http://127.0.0.1/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://127.0.0.1:8080/path')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://127.1.2.3/')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://127.255.255.255/')); } public function testIsDisallowedIpDetectsIPv6Loopback(): void { $this->assertTrue(UrlHelper::has_disallowed_ip('http://[::1]/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://[::1]:8080/path')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://[::1]:443/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://[0:0:0:0:0:0:0:1]/')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://[0:0:0:0:0:0:0:1]:443/')); } public function testIsDisallowedIpAllowsExternalHosts(): void { $this->assertFalse(UrlHelper::has_disallowed_ip('http://example.com/')); $this->assertFalse(UrlHelper::has_disallowed_ip('https://example.org:8080/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://8.8.8.8/')); } public function testIsDisallowedIpReturnsFalseForInvalidUrl(): void { $this->assertFalse(UrlHelper::has_disallowed_ip('not-a-url')); $this->assertFalse(UrlHelper::has_disallowed_ip('')); } // ===== has_disallowed_ip() - Link-Local / Cloud Metadata Tests ===== public function testIsDisallowedIpBlocksLinkLocalOnAllPorts(): void { // 169.254.x.x is link-local (cloud metadata) - always blocked $this->assertTrue(UrlHelper::has_disallowed_ip('http://169.254.169.254/')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://169.254.0.1/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://169.254.169.254:80/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://169.254.169.254:8080/')); } // ===== has_disallowed_ip() - RFC 1918 Private IP Tests ===== public function testIsDisallowedIpAllowsPrivateIP10OnStandardPorts(): void { $this->assertFalse(UrlHelper::has_disallowed_ip('http://10.0.0.1/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://10.255.255.255/path')); $this->assertFalse(UrlHelper::has_disallowed_ip('https://10.0.0.1/')); $this->assertFalse(UrlHelper::has_disallowed_ip('https://10.0.0.1:443/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://10.0.0.1:80/')); } public function testIsDisallowedIpBlocksPrivateIP10OnNonStandardPorts(): void { $this->assertTrue(UrlHelper::has_disallowed_ip('http://10.0.0.1:8080/')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://10.0.0.1:8443/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://10.0.0.1:6379/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://10.255.255.255:3000/')); } public function testIsDisallowedIpAllowsPrivateIP192OnStandardPorts(): void { $this->assertFalse(UrlHelper::has_disallowed_ip('http://192.168.0.1/')); $this->assertFalse(UrlHelper::has_disallowed_ip('https://192.168.255.255/')); $this->assertFalse(UrlHelper::has_disallowed_ip('https://192.168.1.100:443/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://192.168.1.1:80/')); } public function testIsDisallowedIpBlocksPrivateIP192OnNonStandardPorts(): void { $this->assertTrue(UrlHelper::has_disallowed_ip('http://192.168.1.1:8080/')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://192.168.1.1:8443/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://192.168.0.1:9200/')); } public function testIsDisallowedIpAllowsPrivateIP172OnStandardPorts(): void { $this->assertFalse(UrlHelper::has_disallowed_ip('http://172.16.0.1/')); $this->assertFalse(UrlHelper::has_disallowed_ip('https://172.31.255.255/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://172.20.10.5:80/')); $this->assertFalse(UrlHelper::has_disallowed_ip('https://172.16.0.1:443/')); } public function testIsDisallowedIpBlocksPrivateIP172OnNonStandardPorts(): void { $this->assertTrue(UrlHelper::has_disallowed_ip('http://172.16.0.1:8080/')); $this->assertTrue(UrlHelper::has_disallowed_ip('https://172.31.0.1:8443/')); $this->assertTrue(UrlHelper::has_disallowed_ip('http://172.20.10.5:5432/')); } public function testIsDisallowedIpAllowsNonPrivateIP172(): void { // 172.15.x.x and 172.32.x.x are NOT in private range (172.16.0.0/12) $this->assertFalse(UrlHelper::has_disallowed_ip('http://172.15.0.1/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://172.32.0.1/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://172.15.0.1:8080/')); $this->assertFalse(UrlHelper::has_disallowed_ip('http://172.32.0.1:9999/')); } // ===== Edge Cases ===== public function testRewriteRelativeWithBaseUrlWithoutPath(): void { $base = 'https://example.com'; $rel = 'page.html'; $result = UrlHelper::rewrite_relative($base, $rel); // dirname('') returns '.', with_trailing_slash adds '/', so becomes '/page.html' $this->assertEquals('https://example.com/page.html', $result); } public function testRewriteRelativePreservesQueryAndFragment(): void { $base = 'https://example.com/blog/post'; $rel = 'image.jpg?size=large#top'; $result = UrlHelper::rewrite_relative($base, $rel); // Query and fragment should be preserved during URL parsing $this->assertStringContainsString('image.jpg', $result); } // ===== url_to_youtube_vid() Tests ===== public function testUrlToYoutubeVidExtractsFromStandardUrl(): void { $url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('dQw4w9WgXcQ', $result); } public function testUrlToYoutubeVidExtractsFromEmbedUrl(): void { $url = 'https://www.youtube.com/embed/dQw4w9WgXcQ'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('dQw4w9WgXcQ', $result); } public function testUrlToYoutubeVidExtractsFromVUrl(): void { $url = 'https://www.youtube.com/v/dQw4w9WgXcQ'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('dQw4w9WgXcQ', $result); } public function testUrlToYoutubeVidExtractsFromYoutuBeShortUrl(): void { $url = 'https://youtu.be/dQw4w9WgXcQ'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('dQw4w9WgXcQ', $result); } public function testUrlToYoutubeVidRewritesToNoCookieDomain(): void { $url = 'https://www.youtube.com/watch?v=test123'; // Method rewrites youtube.com to youtube-nocookie.com internally $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('test123', $result); } public function testUrlToYoutubeVidHandlesVideoIdWithHyphensAndUnderscores(): void { $url = 'https://www.youtube.com/watch?v=abc-DEF_123'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('abc-DEF_123', $result); } public function testUrlToYoutubeVidReturnsFalseForNonYoutubeUrl(): void { $url = 'https://vimeo.com/123456'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertFalse($result); } public function testUrlToYoutubeVidReturnsFalseForMalformedYoutubeUrl(): void { $url = 'https://www.youtube.com/notavalidpath'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertFalse($result); } public function testUrlToYoutubeVidHandlesWatchUrlWithAdditionalParams(): void { $url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=share&t=10'; // Regex extracts video ID from query string $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('dQw4w9WgXcQ', $result); } public function testUrlToYoutubeVidReturnsFalseForEmptyVideoId(): void { $url = 'https://www.youtube.com/watch?v='; $result = UrlHelper::url_to_youtube_vid($url); $this->assertFalse($result); } public function testUrlToYoutubeVidHandlesHttpProtocol(): void { $url = 'http://www.youtube.com/watch?v=test456'; $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('test456', $result); } public function testUrlToYoutubeVidHandlesYoutuBeWithExtraPath(): void { $url = 'https://youtu.be/dQw4w9WgXcQ?t=42'; // Regex should still extract video ID even with query params $result = UrlHelper::url_to_youtube_vid($url); $this->assertEquals('dQw4w9WgXcQ', $result); } }