1
0
Files
breadcrumb-the-shire/modules/bookmarks/lib/Module/Bookmarks/Support/BookmarkUrlNormalizer.php

155 lines
4.1 KiB
PHP
Raw Normal View History

2026-03-14 21:45:58 +01:00
<?php
namespace MintyPHP\Module\Bookmarks\Support;
2026-03-14 21:45:58 +01:00
final class BookmarkUrlNormalizer
{
public static function canonicalizeRelative(string $url): string
{
$url = trim($url);
if ($url === '' || str_contains($url, "\x00")) {
2026-03-14 21:45:58 +01:00
return '';
}
// Strip fragments: they are client-side only and should not
// differentiate bookmarks during deduplication.
2026-03-14 21:45:58 +01:00
$hashPos = strpos($url, '#');
if ($hashPos !== false) {
$url = substr($url, 0, $hashPos);
}
if ($url === '') {
return '';
}
if (preg_match('#^[a-z][a-z0-9+.-]*:#i', $url) || str_starts_with($url, '//')) {
2026-03-14 21:45:58 +01:00
return '';
}
$parts = parse_url($url);
if ($parts === false) {
return '';
}
$path = self::normalizePath((string) ($parts['path'] ?? ''));
$query = self::normalizeQuery((string) ($parts['query'] ?? ''));
if ($path === '' && $query === '') {
return '';
}
if ($query === '') {
return $path;
}
return $path !== '' ? $path . '?' . $query : '?' . $query;
}
public static function canonicalizeRequestUri(string $requestUri, string $localeBase): string
{
$requestPath = (string) (parse_url($requestUri, PHP_URL_PATH) ?? '');
$requestQuery = (string) (parse_url($requestUri, PHP_URL_QUERY) ?? '');
$basePath = (string) (parse_url($localeBase, PHP_URL_PATH) ?? '');
$basePath = '/' . trim($basePath, '/');
if ($basePath !== '/' && ($requestPath === $basePath || str_starts_with($requestPath, $basePath . '/'))) {
$requestPath = substr($requestPath, strlen($basePath));
}
$relative = ltrim($requestPath, '/');
if ($requestQuery !== '') {
$relative .= '?' . $requestQuery;
}
return self::canonicalizeRelative($relative);
}
private static function normalizePath(string $path): string
{
$path = ltrim(trim($path), '/');
if ($path === '') {
return '';
}
$collapsed = preg_replace('#/+#', '/', $path);
if ($collapsed === null) {
return '';
}
$segments = explode('/', $collapsed);
$normalized = [];
$depth = 0;
2026-03-14 21:45:58 +01:00
foreach ($segments as $segment) {
$segment = trim($segment);
if ($segment === '' || $segment === '.') {
continue;
}
if ($segment === '..') {
if ($depth <= 0) {
return '';
}
2026-03-14 21:45:58 +01:00
array_pop($normalized);
$depth--;
2026-03-14 21:45:58 +01:00
continue;
}
$normalized[] = $segment;
$depth++;
2026-03-14 21:45:58 +01:00
}
return implode('/', $normalized);
}
private static function normalizeQuery(string $query): string
{
$query = trim($query);
if ($query === '') {
return '';
}
$parsed = [];
foreach (explode('&', $query) as $pair) {
if ($pair === '') {
continue;
}
$separatorPos = strpos($pair, '=');
if ($separatorPos === false) {
$rawKey = $pair;
$rawValue = '';
} else {
$rawKey = substr($pair, 0, $separatorPos);
$rawValue = substr($pair, $separatorPos + 1);
}
$key = rawurldecode(str_replace('+', ' ', $rawKey));
$value = rawurldecode(str_replace('+', ' ', $rawValue));
if ($key === '') {
continue;
}
$parsed[$key][] = $value;
}
if ($parsed === []) {
return '';
}
ksort($parsed, SORT_STRING);
$normalizedPairs = [];
foreach ($parsed as $key => $values) {
sort($values, SORT_STRING);
foreach ($values as $value) {
$normalizedPairs[] = rawurlencode($key) . '=' . rawurlencode((string) $value);
}
}
return implode('&', $normalizedPairs);
}
}