PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / core / UrlEncoder.php

UrlEncoder.php in 404 Solution 4.3.0, at includes/core/UrlEncoder.php

182 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Percent-encodes URLs for storage, legacy matching, and cache-key normalization.
10 *
11 * Responsibilities:
12 * - selectivelyURLEncode: selective percent-encoding of non-Latin1 characters
13 * - encodeUrlForLegacyMatch: rawurlencode then restore URL delimiters
14 * - urlencodeEmojis: percent-encode only emoji characters within a URL
15 * - normalizeURLForCacheKey: canonical key used for cache/transient lookups
16 *
17 * Extracted from ABJ_404_Solution_Functions per design-audit-2026-06-02
18 * M201 (Functions.php grab-bag split, parent task i802). This is a
19 * percent-encoding concern, not a generic string utility.
20 *
21 * Depends on ABJ_404_Solution_MbStringAdapter for ord() (sibling task
22 * i825) and ABJ_404_Solution_RegexHelper for regexReplace() (sibling
23 * task i826). The constructor accepts the focused adapters directly, or
24 * a legacy ABJ_404_Solution_Functions instance for backward compatibility
25 * with existing test fixtures - it will extract both adapters from it.
26 */
27 class ABJ_404_Solution_UrlEncoder {
28
29 /** @var ABJ_404_Solution_MbStringAdapter */
30 private $mbAdapter;
31
32 /** @var ABJ_404_Solution_RegexHelper */
33 private $regexHelper;
34
35 /**
36 * @param ABJ_404_Solution_MbStringAdapter|ABJ_404_Solution_Functions $adapter
37 * @param ABJ_404_Solution_RegexHelper|null $regexHelper
38 */
39 public function __construct($adapter, $regexHelper = null) {
40 if ($adapter instanceof ABJ_404_Solution_MbStringAdapter) {
41 $this->mbAdapter = $adapter;
42 if (!($regexHelper instanceof ABJ_404_Solution_RegexHelper)) {
43 throw new InvalidArgumentException(
44 'ABJ_404_Solution_UrlEncoder requires a RegexHelper when constructed with an MbStringAdapter; got '
45 . (is_object($regexHelper) ? get_class($regexHelper) : gettype($regexHelper))
46 );
47 }
48 $this->regexHelper = $regexHelper;
49 } else if ($adapter instanceof ABJ_404_Solution_Functions) {
50 $this->mbAdapter = $adapter->getMbStringAdapter();
51 $this->regexHelper = $adapter->getRegexHelper();
52 } else {
53 throw new InvalidArgumentException(
54 'ABJ_404_Solution_UrlEncoder requires an MbStringAdapter or Functions instance; got '
55 . (is_object($adapter) ? get_class($adapter) : gettype($adapter))
56 );
57 }
58 }
59
60 /**
61 * This function selectively urlencodes a string. Characters outside of the latin1
62 * range (0-255) are urlencoded, while characters inside the range are kept as is.
63 * @param string|array<int|string, mixed> $input The string to be selectively urlencoded.
64 * @return string|array<int|string, mixed> The urlencoded string or array of strings.
65 */
66 public function selectivelyURLEncode($input) {
67 // Handle array input
68 if (is_array($input)) {
69 /** @var callable(mixed): mixed $callback */
70 $callback = [$this, 'selectivelyURLEncode'];
71 return array_map($callback, $input);
72 }
73
74 if (!is_string($input)) {
75 $input = strval($input);
76 }
77
78 // Define replacements for unsafe characters
79 $replacements = [
80 '<' => '%3C',
81 '>' => '%3E',
82 '"' => '%22',
83 "'" => '%27',
84 '`' => '%60',
85 '{' => '%7B',
86 '}' => '%7D',
87 '(' => '%28',
88 ')' => '%29',
89 ];
90
91 // Perform replacements
92 $input = strtr($input, $replacements);
93
94 $encodedString = '';
95 // Iterate through each character in the string
96 for ($i = 0; $i < strlen($input); $i++) {
97 $char = $input[$i];
98 $ord = $this->mbAdapter->ord($char);
99
100 // If the character is outside of latin1 range or is not representable
101 if ($ord > 255) {
102 // Convert to hexadecimal representation
103 $encodedString .= urlencode($char);
104 } else {
105 // Keep the original character if it's in the latin1 range
106 $encodedString .= $char;
107 }
108 }
109
110 return $encodedString;
111 }
112
113 /**
114 * Encode a URL for legacy matching while preserving URL delimiters.
115 *
116 * @param string|null $url
117 * @return string
118 */
119 public function encodeUrlForLegacyMatch($url) {
120 if ($url === null || $url === '') {
121 return '';
122 }
123
124 if (!is_string($url)) {
125 $url = strval($url);
126 }
127
128 $encoded = rawurlencode($url);
129 $encoded = str_replace(
130 array('%2F', '%3F', '%26', '%3D', '%23', '%3A', '%40'),
131 array('/', '?', '&', '=', '#', ':', '@'),
132 $encoded
133 );
134
135 return $encoded;
136 }
137
138 /**
139 * Normalize a URL for use as a cache/transient key.
140 *
141 * This function ensures consistent URL normalization across the codebase:
142 * - Strips query strings (removes everything after '?')
143 * - Applies esc_url for security and consistency
144 *
145 * IMPORTANT: All code that computes cache keys or transient keys from URLs
146 * should use this function to ensure keys match across different code paths.
147 *
148 * Used by: SpellChecker, ShortCode, Ajax_SuggestionPolling, PluginLogic
149 *
150 * @param string $url The URL to normalize
151 * @return string The normalized URL (query string stripped, esc_url applied)
152 */
153 public function normalizeURLForCacheKey($url) {
154 $url = abj_service('sanitizer')->normalizeUrlString($url);
155 // Strip query string (everything after '?')
156 $normalized = $this->regexHelper->regexReplace('\?.*', '', $url) ?? $url;
157 // Apply esc_url for security and consistency
158 return esc_url($normalized);
159 }
160
161 /** Only URL encode emojis from a string.
162 * @param string $url
163 * @return string
164 */
165 public function urlencodeEmojis($url) {
166 // Get all emojis in the string.
167 $matches = [];
168 $emojiPattern = '/[\x{1F000}-\x{1F6FF}\x{1F900}-\x{1F9FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E6}-\x{1F1FF}]/u';
169 $emojis = preg_match_all($emojiPattern, $url, $matches);
170
171 // If there are any emojis in the string, urlencode them.
172 if ($emojis > 0) {
173 foreach ($matches[0] as $emoji) {
174 $url = str_replace($emoji, urlencode($emoji), $url);
175 }
176 }
177
178 // Return the urlencoded string.
179 return $url;
180 }
181 }
182