PluginProbe
Redirection / 5.5.0
Redirection v5.5.0
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.5.0, at models/url/url-query.php

423 lines 11.2 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 $query = preg_replace( '@%5B\d*%5D@', '[]', $query ); // Make these look like []
155
156 foreach ( $query_array as $key => $value ) {
157 if ( $value === null ) {
158 $search = str_replace( '%20', '+', rawurlencode( $key ) . '=' );
159 $replace = str_replace( '%20', '+', rawurlencode( $key ) );
160
161 $query = str_replace( $search, $replace, $query );
162 }
163 }
164
165 $query = str_replace( '%252B', '+', $query );
166
167 if ( $query ) {
168 // Get any fragment
169 $target_fragment = wp_parse_url( $url, PHP_URL_FRAGMENT );
170
171 // If we have a fragment we need to ensure it comes after the query parameters, not before
172 if ( $target_fragment ) {
173 // Remove fragment
174 $url = str_replace( '#' . $target_fragment, '', $url );
175
176 // Add to the end of the query
177 $query .= '#' . $target_fragment;
178 }
179
180 return $url . ( strpos( $url, '?' ) === false ? '?' : '&' ) . $query;
181 }
182
183 return $url;
184 }
185
186 /**
187 * Get a URL with the given base and query parameters from this Url_Query
188 *
189 * @param string $url Base URL.
190 * @return string
191 */
192 public function get_url_with_query( $url ) {
193 return self::build_url( $url, $this->original_query );
194 }
195
196 /**
197 * Get the query parameters
198 *
199 * @return array
200 */
201 public function get() {
202 return $this->original_query;
203 }
204
205 /**
206 * Does the URL and the query params contain no parameters?
207 *
208 * @param string $url URL.
209 * @param Array $params Query params.
210 * @return boolean
211 */
212 private function is_exact_match( $url, $params ) {
213 // No parsed query params but we have query params on the URL - some parsing error with wp_parse_str
214 if ( count( $params ) === 0 && $this->has_query_params( $url ) ) {
215 return true;
216 }
217
218 return false;
219 }
220
221 /**
222 * Get query parameters from a URL
223 *
224 * @param string $url URL.
225 * @return array
226 */
227 private function get_url_query( $url ) {
228 $params = [];
229 $query = $this->get_query_after( $url );
230 $internal = $this->parse_str( $query );
231
232 wp_parse_str( $query ? $query : '', $params );
233
234 // For exactness and due to the way parse_str works we go through and check any query param without a value
235 foreach ( $params as $key => $value ) {
236 if ( is_string( $value ) && strlen( $value ) === 0 && strpos( $url, $key . '=' ) === false ) {
237 $params[ $key ] = null;
238 }
239 }
240
241 // A work-around until we replace parse_str with internal function
242 foreach ( $internal as $pos => $internal_param ) {
243 if ( $internal_param['parse_str'] !== $internal_param['name'] ) {
244 foreach ( $params as $key => $value ) {
245 if ( $key === $internal_param['parse_str'] ) {
246 unset( $params[ $key ] );
247 unset( $internal[ $pos ] );
248 $params[ $internal_param['name'] ] = $value;
249 }
250 }
251 }
252 }
253
254 if ( $this->is_exact_match( $url, $params ) ) {
255 $this->match_exact = $query;
256 }
257
258 return $params;
259 }
260
261 /**
262 * A replacement for parse_str, which behaves oddly in some situations (spaces and no param value)
263 *
264 * TODO: use this in preference to parse_str
265 *
266 * @param string $query Query.
267 * @return string
268 */
269 private function parse_str( $query ) {
270 $params = [];
271
272 if ( strlen( $query ) === 0 ) {
273 return $params;
274 }
275
276 $parts = explode( '&', $query ? $query : '' );
277
278 foreach ( $parts as $part ) {
279 $param = explode( '=', $part );
280 $parse_str = [];
281
282 wp_parse_str( $part, $parse_str );
283
284 $params[] = [
285 'name' => str_replace( [ '[', ']', '%5B', '%5D' ], '', str_replace( '+', ' ', $param[0] ) ),
286 'value' => isset( $param[1] ) ? str_replace( '+', ' ', $param[1] ) : null,
287 'parse_str' => implode( '', array_keys( $parse_str ) ),
288 ];
289 }
290
291 return $params;
292 }
293
294 /**
295 * Does the URL contain query parameters?
296 *
297 * @param string $url URL.
298 * @return boolean
299 */
300 public function has_query_params( $url ) {
301 $qpos = strpos( $url, '?' );
302
303 if ( $qpos === false ) {
304 return false;
305 }
306
307 return true;
308 }
309
310 /**
311 * Get parameters after the ?
312 *
313 * @param string $url URL.
314 * @return string
315 */
316 public function get_query_after( $url ) {
317 $qpos = strpos( $url, '?' );
318 $qrpos = strpos( $url, '\\?' );
319
320 // No ? anywhere - no query
321 if ( $qpos === false ) {
322 return '';
323 }
324
325 // Found an escaped ? and it comes before the non-escaped ?
326 if ( $qrpos !== false && $qrpos < $qpos ) {
327 return substr( $url, $qrpos + 2 );
328 }
329
330 // Standard query param
331 return substr( $url, $qpos + 1 );
332 }
333
334 private function get_query_case( array $query ) {
335 $keys = [];
336 foreach ( array_keys( $query ) as $key ) {
337 $keys[ Red_Url_Path::to_lower( $key ) ] = $key;
338 }
339
340 return $keys;
341 }
342
343 /**
344 * Get query parameters that are the same in both query arrays
345 *
346 * @param array $source_query Source query params.
347 * @param array $target_query Target query params.
348 * @param bool $is_ignore_case Ignore case.
349 * @param integer $depth Current recursion depth.
350 * @return array
351 */
352 public function get_query_same( array $source_query, array $target_query, $is_ignore_case, $depth = 0 ) {
353 if ( $depth > self::RECURSION_LIMIT ) {
354 return [];
355 }
356
357 $source_keys = $this->get_query_case( $source_query );
358 $target_keys = $this->get_query_case( $target_query );
359
360 $same = [];
361 foreach ( $source_keys as $key => $original_key ) {
362 // Does the key exist in the target
363 if ( isset( $target_keys[ $key ] ) ) {
364 // Key exists. Now match the value
365 $source_value = $source_query[ $original_key ];
366 $target_value = $target_query[ $target_keys[ $key ] ];
367 $add = false;
368
369 if ( is_array( $source_value ) && is_array( $target_value ) ) {
370 $add = $this->get_query_same( $source_value, $target_value, $is_ignore_case, $depth + 1 );
371
372 if ( count( $add ) !== count( $source_value ) ) {
373 $add = false;
374 }
375 } elseif ( is_string( $source_value ) && is_string( $target_value ) ) {
376 $add = $this->is_string_match( $source_value, $target_value, $is_ignore_case ) ? $source_value : false;
377 } elseif ( $source_value === null && $target_value === null ) {
378 $add = null;
379 }
380
381 if ( ! empty( $add ) || is_numeric( $add ) || $add === '' || $add === null ) {
382 $same[ $original_key ] = $add;
383 }
384 }
385 }
386
387 return $same;
388 }
389
390 /**
391 * Get the difference in query parameters
392 *
393 * @param array $source_query Source query params.
394 * @param array $target_query Target query params.
395 * @param integer $depth Current recursion depth.
396 * @return array
397 */
398 public function get_query_diff( array $source_query, array $target_query, $depth = 0 ) {
399 if ( $depth > self::RECURSION_LIMIT ) {
400 return [];
401 }
402
403 $diff = [];
404 foreach ( $source_query as $key => $value ) {
405 if ( array_key_exists( $key, $target_query ) && is_array( $value ) && is_array( $target_query[ $key ] ) ) {
406 $add = $this->get_query_diff( $source_query[ $key ], $target_query[ $key ], $depth + 1 );
407
408 if ( ! empty( $add ) ) {
409 $diff[ $key ] = $add;
410 }
411 } elseif ( ! array_key_exists( $key, $target_query ) || ! $this->is_value( $value ) || ! $this->is_value( $target_query[ $key ] ) || $target_query[ $key ] !== $source_query[ $key ] ) {
412 $diff[ $key ] = $value;
413 }
414 }
415
416 return $diff;
417 }
418
419 private function is_value( $value ) {
420 return is_string( $value ) || $value === null;
421 }
422 }
423