* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace HtmlSanitizer\Sanitizer; use HtmlSanitizer\UrlParser\UrlParser; use function League\Uri\build; /** * @internal */ trait UrlSanitizerTrait { /** * @var UrlParser|null */ private $parser; private function sanitizeUrl(?string $input, array $allowedSchemes, ?array $allowedHosts, bool $forceHttps = false): ?string { if (!$input) { return null; } if (!$this->parser) { $this->parser = new UrlParser(); } $url = $this->parser->parse($input); // Malformed URL if (!\is_array($url) || !$url) { return null; } // Invalid scheme if (!\in_array($url['scheme'], $allowedSchemes, true)) { return null; } // Invalid host if (null !== $allowedHosts && !$this->isAllowedHost($url['host'], $allowedHosts)) { return null; } // Force HTTPS if ($forceHttps && 'http' === $url['scheme']) { $url['scheme'] = 'https'; } return build($url); } private function isAllowedHost(?string $host, array $allowedHosts): bool { if (null === $host) { return \in_array(null, $allowedHosts, true); } $parts = array_reverse(explode('.', $host)); foreach ($allowedHosts as $allowedHost) { if ($this->matchAllowedHostParts($parts, array_reverse(explode('.', $allowedHost)))) { return true; } } return false; } private function matchAllowedHostParts(array $uriParts, array $trustedParts): bool { // Check each chunk of the domain is valid foreach ($trustedParts as $key => $trustedPart) { if ($uriParts[$key] !== $trustedPart) { return false; } } return true; } }