UrlNormalization.php
112 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * URL Normalization Trait |
| 5 | * |
| 6 | * @package SmashBalloon\Reviews\Common\Support |
| 7 | */ |
| 8 | |
| 9 | namespace SmashBalloon\Reviews\Common\Support; |
| 10 | |
| 11 | /** |
| 12 | * Byte-stable URL normalization used to compare URLs that may differ only in |
| 13 | * casing, scheme, or trailing slash. Mirrors the server-side trait at |
| 14 | * `SmashBalloon\Backoffice\Support\UrlNormalization` in sb-relay so that |
| 15 | * the plugin's view of a URL matches whatever the relay has on file. |
| 16 | * |
| 17 | * See SMASH-1274 (server-side) and SMASH-1281 (this plugin-side companion). |
| 18 | */ |
| 19 | trait UrlNormalization |
| 20 | { |
| 21 | /** |
| 22 | * Normalize a URL to a byte-stable form. |
| 23 | * |
| 24 | * - Lowercases scheme and host |
| 25 | * - Preserves any non-default port |
| 26 | * - Strips a trailing slash on the path |
| 27 | * - Preserves query string verbatim |
| 28 | * - Leaves `www.` untouched |
| 29 | * - Returns the original string unchanged if the URL is malformed |
| 30 | * |
| 31 | * @param string $url |
| 32 | * |
| 33 | * @return string |
| 34 | */ |
| 35 | public function normalize_url($url) |
| 36 | { |
| 37 | if (!is_string($url) || $url === '') { |
| 38 | return ''; |
| 39 | } |
| 40 | $parts = parse_url($url); |
| 41 | if ($parts === false || empty($parts['host'])) { |
| 42 | return $url; |
| 43 | } |
| 44 | $scheme = strtolower($parts['scheme'] ?? 'https'); |
| 45 | $host = strtolower($parts['host']); |
| 46 | $port = isset($parts['port']) ? ':' . $parts['port'] : ''; |
| 47 | $path = rtrim($parts['path'] ?? '', '/'); |
| 48 | $query = isset($parts['query']) ? '?' . $parts['query'] : ''; |
| 49 | return "{$scheme}://{$host}{$port}{$path}{$query}"; |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * Normalized form with the scheme stripped, so http and https variants |
| 54 | * of the same URL compare equal. Used by detect_site_migration. |
| 55 | */ |
| 56 | public function normalize_url_scheme_agnostic($url) |
| 57 | { |
| 58 | $normalized = $this->normalize_url($url); |
| 59 | $stripped = preg_replace('#^https?://#i', '', $normalized); |
| 60 | return is_string($stripped) ? $stripped : $normalized; |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Host-only normalized form: scheme + path + query dropped, plus the |
| 65 | * round-8 (Patch B) strip set on the host itself: port (`:80`/`:443`), |
| 66 | * leading `www.`, and trailing dot. The port strip is critical for |
| 67 | * reverse-proxy environments (Cloudflare, AWS ALB, Nginx with `$host` |
| 68 | * vs `$http_host`) where the same site oscillates between `host` and |
| 69 | * `host:80` across requests; without it, `detect_site_migration` |
| 70 | * would falsely detect a migration on every port-drift request. |
| 71 | * |
| 72 | * Used by detect_site_migration so: |
| 73 | * - WPML/Polylang language path variants (`/pt-br/`, `/en/`, `/de/`) |
| 74 | * - reverse-proxy port drift (`:80`/`:443` injected by upstream) |
| 75 | * - canonical drift (`www.example.com` ↔ `example.com`) |
| 76 | * - FQDN form (`example.com.` ↔ `example.com`) |
| 77 | * on the same WordPress install don't register as a migration. |
| 78 | * |
| 79 | * Multisite subsites stay distinct because each subsite has its own |
| 80 | * hostname or its own `sbr_settings` store. Plugin-side this trait |
| 81 | * is byte-for-byte semantically identical to the relay's mirror at |
| 82 | * `app/Support/UrlNormalization::normalize_url_host_only` so plugin |
| 83 | * and relay agree on what counts as the same install. |
| 84 | */ |
| 85 | public function normalize_url_host_only($url) |
| 86 | { |
| 87 | if (!is_string($url) || $url === '') { |
| 88 | return ''; |
| 89 | } |
| 90 | $parts = parse_url($url); |
| 91 | if ($parts === false || empty($parts['host'])) { |
| 92 | return $url; |
| 93 | } |
| 94 | // Migration-detection compares site identity, not network endpoints. |
| 95 | // Strip: |
| 96 | // - port: a reverse-proxy-injected ":80"/":443" (Cloudflare, AWS ALB, |
| 97 | // Nginx with $host vs $http_host) is not a migration. This was the |
| 98 | // residual SMASH-1274 loop driver — same site oscillating between |
| 99 | // "host" and "host:80" depending on request path. |
| 100 | // - leading "www.": canonical-URL drift between get_home_url() |
| 101 | // returning "www.example.com" on one request and "example.com" on |
| 102 | // another (non-canonical permalinks, multisite alias mapping) is |
| 103 | // not a migration. |
| 104 | // - trailing dot: "example.com." is the same WP install as |
| 105 | // "example.com" (FQDN vs short form). |
| 106 | $host = strtolower($parts['host']); |
| 107 | $host = preg_replace('/^www\./', '', $host); |
| 108 | $host = rtrim($host, '.'); |
| 109 | return $host; |
| 110 | } |
| 111 | } |
| 112 |