PluginProbe
Redirection / 5.3.3
Redirection v5.3.3
5.10.0 5.9.0 5.8.1 5.8.0 3.7.2 3.7.3 4.0 4.0.1 4.1 4.1.1 4.2 4.2.1 4.2.2 4.2.3 4.3 4.3.1 4.3.2 4.3.3 4.4 4.4.1 4.4.2 4.5 4.5.1 4.6.2 4.7.1 All 130 releases
redirection / models / url / url-query.php

url-query.php in Redirection 5.3.3, at models/url/url-query.php

365 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Query parameter martching
5 */
6 class Red_Url_Query {
7 /**
8 * @type Integer
9 */
10 const RECURSION_LIMIT = 10;
11
12 /**
13 * Original query parameters (used when passing)
14 *
15 * @var array
16 */
17 private $original_query = [];
18
19 /**
20 * Match query parameters (used only for matching, and maybe be lowercased)
21 *
22 * @var array
23 */
24 private $match_query = [];
25
26 /**
27 * Is this an exact match?
28 *
29 * @var boolean|string
30 */
31 private $match_exact = false;
32
33 /**
34 * Constructor
35 *
36 * @param String $url URL.
37 * @param Red_Source_Flags $flags URL flags.
38 */
39 public function __construct( $url, $flags ) {
40 $this->original_query = $this->get_url_query( $url );
41 $this->match_query = $this->original_query;
42
43 if ( $flags->is_ignore_case() ) {
44 $this->match_query = $this->get_url_query( Red_Url_Path::to_lower( $url ) );
45 }
46 }
47
48 /**
49 * Does this object match the URL?
50 *
51 * @param String $url URL to match.
52 * @param Red_Source_Flags $flags Source flags.
53 * @return boolean
54 */
55 public function is_match( $url, Red_Source_Flags $flags ) {
56 if ( $flags->is_ignore_case() ) {
57 $url = Red_Url_Path::to_lower( $url );
58 }
59
60 // If we can't parse the query params then match the params exactly
61 if ( $this->match_exact !== false ) {
62 return $this->is_string_match( $this->get_query_after( $url ), $this->match_exact, $flags->is_ignore_case() );
63 }
64
65 $target = $this->get_url_query( $url );
66
67 // All params in the source have to exist in the request, but in any order
68 $matched = $this->get_query_same( $this->match_query, $target, $flags->is_ignore_case() );
69
70 if ( count( $matched ) !== count( $this->match_query ) ) {
71 // Source params arent matched exactly
72 return false;
73 };
74
75 // Get list of whatever is left over
76 $query_diff = $this->get_query_diff( $this->match_query, $target );
77 $query_diff = array_merge( $query_diff, $this->get_query_diff( $target, $this->match_query ) );
78
79 if ( $flags->is_query_ignore() || $flags->is_query_pass() ) {
80 return true; // This ignores all other query params
81 }
82
83 // In an exact match there shouldn't be any more params
84 return count( $query_diff ) === 0;
85 }
86
87 /**
88 * Return true if the two strings match, false otherwise. Pays attention to case sensitivity
89 *
90 * @param string $first First string.
91 * @param string $second Second string.
92 * @param boolean $case Case sensitivity.
93 * @return boolean
94 */
95 private function is_string_match( $first, $second, $case ) {
96 if ( $case ) {
97 return Red_Url_Path::to_lower( $first ) === Red_Url_Path::to_lower( $second );
98 }
99
100 return $first === $second;
101 }
102
103 /**
104 * Pass query params from one URL to another URL, ignoring any params that already exist on the target.
105 *
106 * @param string $target_url The target URL to add params to.
107 * @param string $requested_url The source URL to pass params from.
108 * @param Red_Source_Flags $flags Any URL flags.
109 * @return string URL, modified or not.
110 */
111 public static function add_to_target( $target_url, $requested_url, Red_Source_Flags $flags ) {
112 if ( $flags->is_query_pass() && $target_url ) {
113 $source_query = new Red_Url_Query( $target_url, $flags );
114 $request_query = new Red_Url_Query( $requested_url, $flags );
115
116 // Now add any remaining params
117 $query_diff = $source_query->get_query_diff( $source_query->original_query, $request_query->original_query );
118 $request_diff = $request_query->get_query_diff( $request_query->original_query, $source_query->original_query );
119
120 foreach ( $request_diff as $key => $value ) {
121 $query_diff[ $key ] = $value;
122 }
123
124 // Remove any params from $source that are present in $request - we dont allow
125 // predefined params to be overridden
126 foreach ( array_keys( $query_diff ) as $key ) {
127 if ( isset( $source_query->original_query[ $key ] ) ) {
128 unset( $query_diff[ $key ] );
129 }
130 }
131
132 return self::build_url( $target_url, $query_diff );
133 }
134
135 return $target_url;
136 }
137
138 /**
139 * Build a URL from a base and query parameters
140 *
141 * @param String $url Base URL.
142 * @param Array $query_array Query parameters.
143 * @return String
144 */
145 public static function build_url( $url, $query_array ) {
146 $query = http_build_query( array_map( function( $value ) {
147 if ( $value === null ) {
148 return '';
149 }
150
151 return $value;
152 }, $query_array ) );
153
154 foreach ( $query_array as $key => $value ) {
155 if ( $value === null ) {
156 $query = str_replace( $key . '=', $key, $query );
157 }
158 }
159
160 $query = preg_replace( '@%5B\d*%5D@', '[]', $query ); // Make these look like []
161
162 if ( $query ) {
163 // Get any fragment
164 $target_fragment = wp_parse_url( $url, PHP_URL_FRAGMENT );
165
166 // If we have a fragment we need to ensure it comes after the query parameters, not before
167 if ( $target_fragment ) {
168 // Remove fragment
169 $url = str_replace( '#' . $target_fragment, '', $url );
170
171 // Add to the end of the query
172 $query .= '#' . $target_fragment;
173 }
174
175 return $url . ( strpos( $url, '?' ) === false ? '?' : '&' ) . $query;
176 }
177
178 return $url;
179 }
180
181 /**
182 * Get a URL with the given base and query parameters from this Url_Query
183 *
184 * @param String $url Base URL.
185 * @return String
186 */
187 public function get_url_with_query( $url ) {
188 return self::build_url( $url, $this->original_query );
189 }
190
191 /**
192 * Get the query parameters
193 *
194 * @return Array
195 */
196 public function get() {
197 return $this->original_query;
198 }
199
200 /**
201 * Does the URL and the query params contain no parameters?
202 *
203 * @param String $url URL.
204 * @param Array $params Query params.
205 * @return boolean
206 */
207 private function is_exact_match( $url, $params ) {
208 // No parsed query params but we have query params on the URL - some parsing error with wp_parse_str
209 if ( count( $params ) === 0 && $this->has_query_params( $url ) ) {
210 return true;
211 }
212
213 return false;
214 }
215
216 /**
217 * Get query parameters from a URL
218 *
219 * @param String $url URL.
220 * @return array
221 */
222 private function get_url_query( $url ) {
223 $params = [];
224 $query = $this->get_query_after( $url );
225
226 wp_parse_str( $query ? $query : '', $params );
227
228 // For exactness and due to the way parse_str works we go through and check any query param without a value
229 foreach ( $params as $key => $value ) {
230 if ( is_string( $value ) && strlen( $value ) === 0 && strpos( $url, $value . '=' ) === false ) {
231 $params[ $key ] = null;
232 }
233 }
234
235 if ( $this->is_exact_match( $url, $params ) ) {
236 $this->match_exact = $query;
237 }
238
239 return $params;
240 }
241
242 /**
243 * Does the URL contain query parameters?
244 *
245 * @param String $url URL.
246 * @return boolean
247 */
248 public function has_query_params( $url ) {
249 $qpos = strpos( $url, '?' );
250
251 if ( $qpos === false ) {
252 return false;
253 }
254
255 return true;
256 }
257
258 /**
259 * Get parameters after the ?
260 *
261 * @param String $url URL.
262 * @return String
263 */
264 public function get_query_after( $url ) {
265 $qpos = strpos( $url, '?' );
266 $qrpos = strpos( $url, '\\?' );
267
268 // No ? anywhere - no query
269 if ( $qpos === false ) {
270 return '';
271 }
272
273 // Found an escaped ? and it comes before the non-escaped ?
274 if ( $qrpos !== false && $qrpos < $qpos ) {
275 return substr( $url, $qrpos + 2 );
276 }
277
278 // Standard query param
279 return substr( $url, $qpos + 1 );
280 }
281
282 private function get_query_case( array $query ) {
283 $keys = [];
284 foreach ( array_keys( $query ) as $key ) {
285 $keys[ Red_Url_Path::to_lower( $key ) ] = $key;
286 }
287
288 return $keys;
289 }
290
291 /**
292 * Get query parameters that are the same in both query arrays
293 *
294 * @param array $source_query Source query params.
295 * @param array $target_query Target query params.
296 * @param bool $is_ignore_case Ignore case.
297 * @param integer $depth Current recursion depth.
298 * @return array
299 */
300 public function get_query_same( array $source_query, array $target_query, $is_ignore_case, $depth = 0 ) {
301 if ( $depth > self::RECURSION_LIMIT ) {
302 return [];
303 }
304
305 $source_keys = $this->get_query_case( $source_query );
306 $target_keys = $this->get_query_case( $target_query );
307
308 $same = [];
309 foreach ( $source_keys as $key => $original_key ) {
310 // Does the key exist in the target
311 if ( isset( $target_keys[ $key ] ) ) {
312 // Key exists. Now match the value
313 $source_value = $source_query[ $original_key ];
314 $target_value = $target_query[ $target_keys[ $key ] ];
315 $add = false;
316
317 if ( is_array( $source_value ) && is_array( $target_value ) ) {
318 $add = $this->get_query_same( $source_value, $target_value, $is_ignore_case, $depth + 1 );
319
320 if ( count( $add ) !== count( $source_value ) ) {
321 $add = false;
322 }
323 } elseif ( is_string( $source_value ) && is_string( $target_value ) ) {
324 $add = $this->is_string_match( $source_value, $target_value, $is_ignore_case ) ? $source_value : false;
325 }
326
327 if ( ! empty( $add ) || is_numeric( $add ) || $add === '' ) {
328 $same[ $original_key ] = $add;
329 }
330 }
331 }
332
333 return $same;
334 }
335
336 /**
337 * Get the difference in query parameters
338 *
339 * @param array $source_query Source query params.
340 * @param array $target_query Target query params.
341 * @param integer $depth Current recursion depth.
342 * @return array
343 */
344 public function get_query_diff( array $source_query, array $target_query, $depth = 0 ) {
345 if ( $depth > self::RECURSION_LIMIT ) {
346 return [];
347 }
348
349 $diff = [];
350 foreach ( $source_query as $key => $value ) {
351 if ( isset( $target_query[ $key ] ) && is_array( $value ) && is_array( $target_query[ $key ] ) ) {
352 $add = $this->get_query_diff( $source_query[ $key ], $target_query[ $key ], $depth + 1 );
353
354 if ( ! empty( $add ) ) {
355 $diff[ $key ] = $add;
356 }
357 } elseif ( ! isset( $target_query[ $key ] ) || ! is_string( $value ) || ! is_string( $target_query[ $key ] ) || $target_query[ $key ] !== $source_query[ $key ] ) {
358 $diff[ $key ] = $value;
359 }
360 }
361
362 return $diff;
363 }
364 }
365