PluginProbe
Redirection / 5.2
Redirection v5.2
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
← All changes | modules/wordpress.php +407 -69 4.05.2 View file →
@@ -1,38 +1,191 @@
1 1 <?php
2 2
3 +/**
4 + * WordPress redirect module.
5 + *
6 + * Provides PHP controlled redirects and monitoring and is the core of the front-end redirection.
7 + */
3 8 class WordPress_Module extends Red_Module {
9 + /**
10 + * @var integer
11 + */
4 12 const MODULE_ID = 1;
5 13
14 + /**
15 + * Can we log?
16 + *
17 + * @var boolean
18 + */
19 + private $can_log = true;
20 +
21 + /**
22 + * The target redirect URL
23 + *
24 + * @var string|false
25 + */
26 + private $redirect_url = false;
27 +
28 + /**
29 + * The target redirect code
30 + *
31 + * @var integer
32 + */
33 + private $redirect_code = 0;
34 +
35 + /**
36 + * Copy of redirects that match the requested URL
37 + *
38 + * @var Red_Item[]
39 + */
40 + private $redirects = [];
41 +
42 + /**
43 + * Matched redirect
44 + *
45 + * @var Red_Item|false
46 + */
6 47 private $matched = false;
7 - private $can_log = true;
8 48
49 + /**
50 + * Return the module ID
51 + *
52 + * @return integer
53 + */
9 54 public function get_id() {
10 55 return self::MODULE_ID;
11 56 }
12 57
58 + /**
59 + * Return the module name
60 + *
61 + * @return string
62 + */
13 63 public function get_name() {
14 64 return 'WordPress';
15 65 }
16 66
67 + /**
68 + * Start the module. Hooks any filters and actions
69 + *
70 + * @return void
71 + */
17 72 public function start() {
73 + // Only run redirect rules if we're not disabled
74 + if ( ! red_is_disabled() ) {
75 + // Canonical site settings - https, www, relocate, and aliases
76 + add_action( 'init', [ $this, 'canonical_domain' ] );
77 +
78 + // The main redirect loop
79 + add_action( 'init', [ $this, 'init' ] );
80 +
81 + // Send site HTTP headers as well as 410 error codes
82 + add_action( 'send_headers', [ $this, 'send_headers' ] );
83 +
84 + // Redirect HTTP headers and server-specific overrides
85 + add_filter( 'wp_redirect', [ $this, 'wp_redirect' ], 1, 2 );
86 +
87 + // Allow permalinks to be redirected
88 + add_filter( 'pre_handle_404', [ $this, 'pre_handle_404' ], 10, 2 );
89 +
90 + // Cache support
91 + add_action( 'redirection_matched', [ $this, 'cache_redirects' ], 10, 3 );
92 + add_action( 'redirection_last', [ $this, 'cache_unmatched_redirects' ], 10, 3 );
93 + }
94 +
18 95 // Setup the various filters and actions that allow Redirection to happen
19 - add_action( 'init', array( $this, 'init' ) );
20 - add_action( 'init', array( $this, 'force_https' ) );
21 - add_action( 'send_headers', array( $this, 'send_headers' ) );
22 - add_filter( 'wp_redirect', array( $this, 'wp_redirect' ), 1, 2 );
23 - add_action( 'redirection_visit', array( $this, 'redirection_visit' ), 10, 3 );
24 - add_action( 'redirection_do_nothing', array( $this, 'redirection_do_nothing' ) );
25 - add_filter( 'redirect_canonical', array( $this, 'redirect_canonical' ), 10, 2 );
26 - add_action( 'template_redirect', array( $this, 'template_redirect' ) );
96 + add_action( 'redirection_visit', [ $this, 'redirection_visit' ], 10, 3 );
97 + add_action( 'redirection_do_nothing', [ $this, 'redirection_do_nothing' ] );
27 98
28 - // Remove WordPress 2.3 redirection
29 - remove_action( 'template_redirect', 'wp_old_slug_redirect' );
99 + // Prevent WordPress overriding a canonical redirect
100 + add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ], 10, 2 );
101 +
102 + // Log 404s and perform 'URL and WordPress page type' redirects
103 + add_action( 'template_redirect', [ $this, 'template_redirect' ] );
104 +
105 + // Back-compat for < database 4.2
106 + add_filter( 'redirection_404_data', [ $this, 'log_back_compat' ] );
107 + add_filter( 'redirection_log_data', [ $this, 'log_back_compat' ] );
108 +
109 + // Record the redirect agent
110 + add_filter( 'x_redirect_by', [ $this, 'record_redirect_by' ], 90 );
30 111 }
31 112
32 113 /**
114 + * Called after no redirect is matched. This allows us to cache a negative result/
115 + *
116 + * @param String $url URL.
117 + * @param WordPress_Module $wp This.
118 + * @param array $redirects Array of redirects.
119 + * @return void
120 + */
121 + public function cache_unmatched_redirects( $url, $wp, $redirects ) {
122 + if ( $this->matched ) {
123 + return;
124 + }
125 +
126 + $this->cache_redirects( $url, $this->matched, $redirects );
127 + }
128 +
129 + /**
130 + * Called when a redirect is matched. This allows us to cache a positive result.
131 + *
132 + * @param String $url URL.
133 + * @param Red_Item|false $matched_redirect Matched redirect.
134 + * @param array $redirects Array of redirects.
135 + * @return void
136 + */
137 + public function cache_redirects( $url, $matched_redirect, $redirects ) {
138 + $cache = Redirect_Cache::init();
139 + $cache->set( $url, $matched_redirect, $redirects );
140 + }
141 +
142 + /**
143 + * If we have a 404 then check for any permalink migrations
144 + *
145 + * @param boolean $result Return result.
146 + * @param WP_Query $query WP_Query object.
147 + * @return boolean
148 + */
149 + public function pre_handle_404( $result, WP_Query $query ) {
150 + $options = red_get_options();
151 +
152 + if ( is_404() && count( $options['permalinks'] ) > 0 ) {
153 + include_once dirname( dirname( __FILE__ ) ) . '/models/permalinks.php';
154 +
155 + $permalinks = new Red_Permalinks( $options['permalinks'] );
156 + $permalinks->migrate( $query );
157 + }
158 +
159 + return $result;
160 + }
161 +
162 + /**
163 + * Back-compatability for Redirection databases older than 4.2. Prevents errors from storing data that has no DB column
164 + *
165 + * @param array $insert Data to log.
166 + * @return array
167 + */
168 + public function log_back_compat( $insert ) {
169 + // Remove columns not supported in older versions
170 + $status = new Red_Database_Status();
171 +
172 + if ( ! $status->does_support( '4.2' ) ) {
173 + foreach ( [ 'request_data', 'request_method', 'http_code', 'domain', 'redirect_by' ] as $ignore ) {
174 + unset( $insert[ $ignore ] );
175 + }
176 + }
177 +
178 + return $insert;
179 + }
180 +
181 + /**
33 182 * This ensures that a matched URL is not overriddden by WordPress, if the URL happens to be a WordPress URL of some kind
34 183 * For example: /?author=1 will be redirected to /author/name unless this returns false
184 + *
185 + * @param String $redirect_url The redirected URL.
186 + * @param String $requested_url The requested URL.
187 + * @return String|false
35 188 */
36 189 public function redirect_canonical( $redirect_url, $requested_url ) {
37 190 if ( $this->matched ) {
38 191 return false;
@@ -40,82 +193,157 @@
40 193
41 194 return $redirect_url;
42 195 }
43 196
197 + /**
198 + * WordPress 'template_redirect' hook. Used to check for 404s
199 + *
200 + * @return void
201 + */
44 202 public function template_redirect() {
45 203 if ( ! is_404() || $this->matched ) {
46 204 return;
47 205 }
48 206
49 - if ( $this->match_404_type() ) {
50 - // Don't log an intentionally redirected 404
207 + // We are on a 404. Check if we have a 'URL and page type' match in any of the matched redirects.
208 + if ( $this->is_url_and_page_type() ) {
209 + // Don't log an intentionally redirected 404 as part of the 'url and page type'
51 210 return;
52 211 }
53 212
54 213 $options = red_get_options();
55 214
56 - if ( isset( $options['expire_404'] ) && $options['expire_404'] >= 0 && apply_filters( 'redirection_log_404', $this->can_log ) ) {
57 - RE_404::create( Redirection_Request::get_request_url(), Redirection_Request::get_user_agent(), Redirection_Request::get_ip(), Redirection_Request::get_referrer() );
215 + if ( isset( $options['expire_404'] ) && $options['expire_404'] >= 0 && $this->can_log() ) {
216 + $details = [
217 + 'agent' => Redirection_Request::get_user_agent(),
218 + 'referrer' => Redirection_Request::get_referrer(),
219 + 'request_method' => Redirection_Request::get_request_method(),
220 + 'http_code' => 404,
221 + ];
222 +
223 + if ( $options['log_header'] ) {
224 + $details['request_data'] = [
225 + 'headers' => Redirection_Request::get_request_headers(),
226 + ];
227 + }
228 +
229 + Red_404_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
58 230 }
59 231 }
60 232
61 - private function match_404_type() {
62 - if ( ! property_exists( $this, 'redirects' ) || count( $this->redirects ) === 0 ) {
63 - return false;
64 - }
233 + /**
234 + * Return `true` if any of the matched redirects is a 'url and page type', `false` otherwise
235 + *
236 + * @return boolean
237 + */
238 + private function is_url_and_page_type() {
239 + $page_types = array_values( array_filter( $this->redirects, function( Red_Item $redirect ) {
240 + return $redirect->match && $redirect->match->get_type() === 'page';
241 + } ) );
65 242
66 - $page_type = array_values( array_filter( $this->redirects, array( $this, 'only_404' ) ) );
243 + if ( count( $page_types ) > 0 ) {
244 + $request = new Red_Url_Request( Redirection_Request::get_request_url() );
245 + $action = $page_types[0]->get_match( $request->get_decoded_url(), $request->get_original_url() );
246 + if ( $action ) {
247 + $action->run();
248 + }
67 249
68 - if ( count( $page_type ) > 0 ) {
69 - $url = apply_filters( 'redirection_url_source', Redirection_Request::get_request_url() );
70 - $first = $page_type[0];
71 - return $first->matches( $url );
250 + return true;
72 251 }
73 252
74 253 return false;
75 254 }
76 255
77 - private function only_404( $redirect ) {
78 - return $redirect->match->get_type() === 'page';
79 - }
80 -
81 - // Return true to stop further processing of the 'do nothing'
256 + /**
257 + * Called by a 'do nothing' action. Return true to stop further processing of the 'do nothing'
258 + *
259 + * @return boolean
260 + */
82 261 public function redirection_do_nothing() {
83 262 $this->can_log = false;
84 263 return true;
85 264 }
86 265
266 + /**
267 + * Action fired when a redirect is performed, and used to log the data
268 + *
269 + * @param Red_Item $redirect The redirect.
270 + * @param String $url The source URL.
271 + * @param String $target The target URL.
272 + * @return void
273 + */
87 274 public function redirection_visit( $redirect, $url, $target ) {
88 275 $redirect->visit( $url, $target );
89 276 }
90 277
91 - public function force_https() {
278 + /**
279 + * Get canonical target
280 + *
281 + * @return string|false
282 + */
283 + public function get_canonical_target() {
92 284 $options = red_get_options();
285 + $canonical = new Redirection_Canonical( $options['https'], $options['preferred_domain'], $options['aliases'], get_home_url() );
93 286
94 - if ( $options['https'] && ! is_ssl() ) {
95 - $target = rtrim( Redirection_Request::get_server_name(), '/' ) . esc_url_raw( Redirection_Request::get_request_url() );
96 - wp_safe_redirect( 'https://' . $target, 301 );
287 + // Relocate domain?
288 + if ( $options['relocate'] ) {
289 + return $canonical->relocate_request( $options['relocate'], Redirection_Request::get_server_name(), Redirection_Request::get_request_url() );
290 + }
291 +
292 + // Force HTTPS or www
293 + return $canonical->get_redirect( Redirection_Request::get_request_server_name(), Redirection_Request::get_request_url() );
294 + }
295 +
296 + /**
297 + * Checks for canonical domain requests
298 + *
299 + * @return void
300 + */
301 + public function canonical_domain() {
302 + $target = $this->get_canonical_target();
303 +
304 + if ( $target ) {
305 + // phpcs:ignore
306 + wp_redirect( $target, 301, 'redirection' );
97 307 die();
98 308 }
99 309 }
100 310
311 + /**
312 + * Redirection 'main loop'. Checks the currently requested URL against the database and perform a redirect, if necessary.
313 + *
314 + * @return void
315 + */
101 316 public function init() {
102 - $url = apply_filters( 'redirection_url_source', Redirection_Request::get_request_url() );
317 + if ( $this->matched ) {
318 + return;
319 + }
103 320
321 + $request = new Red_Url_Request( Redirection_Request::get_request_url() );
322 +
104 323 // Make sure we don't try and redirect something essential
105 - if ( $url && ! $this->protected_url( $url ) && $this->matched === false ) {
106 - do_action( 'redirection_first', $url, $this );
324 + if ( $request->is_valid() && ! $request->is_protected_url() ) {
325 + do_action( 'redirection_first', $request->get_decoded_url(), $this );
107 326
108 - $redirects = Red_Item::get_for_url( $url );
327 + // Get all redirects that match the URL
328 + $redirects = Red_Item::get_for_url( $request->get_decoded_url() );
109 329
330 + // Redirects will be ordered by position. Run through the list until one fires
110 331 foreach ( (array) $redirects as $item ) {
111 - if ( $item->matches( $url ) ) {
332 + $action = $item->get_match( $request->get_decoded_url(), $request->get_original_url() );
333 +
334 + if ( $action ) {
112 335 $this->matched = $item;
336 +
337 + do_action( 'redirection_matched', $request->get_decoded_url(), $item, $redirects );
338 +
339 + $action->run();
113 340 break;
114 341 }
115 342 }
116 343
117 - do_action( 'redirection_last', $url, $this );
344 + // We will only get here if there is no match (check $this->matched) or the action does not result in redirecting away
345 + do_action( 'redirection_last', $request->get_decoded_url(), $this, $redirects );
118 346
119 347 if ( ! $this->matched ) {
120 348 // Keep them for later
121 349 $this->redirects = $redirects;
@@ -123,24 +351,14 @@
123 351 }
124 352 }
125 353
126 354 /**
127 - * Protect certain URLs from being redirected. Note we don't need to protect wp-admin, as this code doesn't run there
355 + * Fix for incorrect headers sent when using FastCGI/IIS
356 + *
357 + * @param String $status HTTP status line.
358 + * @return String
128 359 */
129 - private function protected_url( $url ) {
130 - $rest = wp_parse_url( red_get_rest_api() );
131 - $rest_api = $rest['path'] . ( isset( $rest['query'] ) ? '?' . $rest['query'] : '' );
132 -
133 - if ( substr( $url, 0, strlen( $rest_api ) ) === $rest_api ) {
134 - // Never redirect the REST API
135 - return true;
136 - }
137 -
138 - return false;
139 - }
140 -
141 360 public function status_header( $status ) {
142 - // Fix for incorrect headers sent when using FastCGI/IIS
143 361 if ( substr( php_sapi_name(), 0, 3 ) === 'cgi' ) {
144 362 return str_replace( 'HTTP/1.1', 'Status:', $status );
145 363 }
146 364
@@ -146,28 +364,58 @@
146 364
147 365 return $status;
148 366 }
149 367
368 + /**
369 + * Add any custom HTTP headers to the response.
370 + *
371 + * @param array $obj Some object.
372 + * @return void
373 + */
150 374 public function send_headers( $obj ) {
151 - if ( ! empty( $this->matched ) && $this->matched->match->action_code === '410' ) {
152 - add_filter( 'status_header', array( $this, 'set_header_410' ) );
375 + if ( ! empty( $this->matched ) && $this->matched->action && $this->matched->action->get_code() === 410 ) {
376 + add_filter( 'status_header', [ $this, 'set_header_410' ] );
153 377 }
378 +
379 + // Add any custom headers
380 + $options = red_get_options();
381 + $headers = new Red_Http_Headers( $options['headers'] );
382 + $headers->run( $headers->get_site_headers() );
154 383 }
155 384
385 + /**
386 + * Add support for a 410 response.
387 + *
388 + * @return String
389 + */
156 390 public function set_header_410() {
157 391 return 'HTTP/1.1 410 Gone';
158 392 }
159 393
160 - public function wp_redirect( $url, $status ) {
161 - global $wp_version, $is_IIS;
394 + /**
395 + * IIS fix. Don't know if this is still needed
396 + *
397 + * @param String $url URL.
398 + * @return void
399 + */
400 + private function iis_fix( $url ) {
401 + global $is_IIS;
162 402
163 403 if ( $is_IIS ) {
164 404 header( "Refresh: 0;url=$url" );
165 - return $url;
166 405 }
406 + }
167 407
408 + /**
409 + * Don't know if this is still needed
410 + *
411 + * @param String $url URL.
412 + * @param integer $status HTTP status code.
413 + * @return void
414 + */
415 + private function cgi_fix( $url, $status ) {
168 416 if ( $status === 301 && php_sapi_name() === 'cgi-fcgi' ) {
169 - $servers_to_check = array( 'lighttpd', 'nginx' );
417 + $servers_to_check = [ 'lighttpd', 'nginx' ];
170 418
171 419 foreach ( $servers_to_check as $name ) {
172 420 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && stripos( $_SERVER['SERVER_SOFTWARE'], $name ) !== false ) {
173 421 status_header( $status );
@@ -175,9 +423,91 @@
175 423 exit( 0 );
176 424 }
177 425 }
178 426 }
427 + }
179 428
429 + /**
430 + * Get a 'source' for a redirect by digging through the backtrace.
431 + *
432 + * @return string[]
433 + */
434 + private function get_redirect_source() {
435 + $ignore = [
436 + 'WP_Hook',
437 + 'template-loader.php',
438 + 'wp-blog-header.php',
439 + ];
440 +
441 + // phpcs:ignore
442 + $source = wp_debug_backtrace_summary( null, 5, false );
443 +
444 + return array_filter( $source, function( $item ) use ( $ignore ) {
445 + foreach ( $ignore as $ignore_item ) {
446 + if ( strpos( $item, $ignore_item ) !== false ) {
447 + return false;
448 + }
449 + }
450 +
451 + return true;
452 + } );
453 + }
454 +
455 + /**
456 + * Record a redirect.
457 + *
458 + * @param String $agent Redirect agent.
459 + * @return string
460 + */
461 + public function record_redirect_by( $agent ) {
462 + // Have we already redirected with Redirection?
463 + if ( $this->matched || $agent === 'redirection' ) {
464 + return $agent;
465 + }
466 +
467 + $options = red_get_options();
468 +
469 + if ( ! $options['log_external'] ) {
470 + return $agent;
471 + }
472 +
473 + $details = [
474 + 'target' => $this->redirect_url,
475 + 'agent' => Redirection_Request::get_user_agent(),
476 + 'referrer' => Redirection_Request::get_referrer(),
477 + 'request_method' => Redirection_Request::get_request_method(),
478 + 'redirect_by' => $agent ? $agent : 'wordpress',
479 + 'http_code' => $this->redirect_code,
480 + 'request_data' => [
481 + 'source' => array_values( $this->get_redirect_source() ),
482 + ],
483 + ];
484 +
485 + if ( $options['log_header'] ) {
486 + $details['request_data']['headers'] = Redirection_Request::get_request_headers();
487 + }
488 +
489 + Red_Redirect_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
490 +
491 + return $agent;
492 + }
493 +
494 + /**
495 + * Perform any pre-redirect processing, such as logging and header fixing.
496 + *
497 + * @param String $url Target URL.
498 + * @param integer $status HTTP status.
499 + * @return string
500 + */
501 + public function wp_redirect( $url, $status = 302 ) {
502 + global $wp_version;
503 +
504 + $this->redirect_url = $url;
505 + $this->redirect_code = $status;
506 +
507 + $this->iis_fix( $url );
508 + $this->cgi_fix( $url, $status );
509 +
180 510 if ( intval( $status, 10 ) === 307 ) {
181 511 status_header( $status );
182 512 nocache_headers();
183 513 return $url;
@@ -183,8 +513,10 @@
183 513 return $url;
184 514 }
185 515
186 516 $options = red_get_options();
517 + $headers = new Red_Http_Headers( $options['headers'] );
518 + $headers->run( $headers->get_redirect_headers() );
187 519
188 520 // Do we need to set the cache header?
189 521 if ( ! headers_sent() && isset( $options['redirect_cache'] ) && $options['redirect_cache'] !== 0 && intval( $status, 10 ) === 301 ) {
190 522 if ( $options['redirect_cache'] === -1 ) {
@@ -200,18 +532,24 @@
200 532 status_header( $status );
201 533 return $url;
202 534 }
203 535
204 - public function update( array $data ) {
205 - return false;
536 + /**
537 + * Reset the module. Used for unit tests
538 + *
539 + * @param Red_Item|false $matched Set the `matched` var.
540 + * @return void
541 + */
542 + public function reset( $matched = false ) {
543 + $this->can_log = true;
544 + $this->matched = $matched;
206 545 }
207 546
208 - protected function load( $options ) {
209 - }
210 -
211 - protected function flush_module() {
212 - }
213 -
214 - public function reset() {
215 - $this->can_log = true;
547 + /**
548 + * Can we log a redirect?
549 + *
550 + * @return boolean
551 + */
552 + public function can_log() {
553 + return apply_filters( 'redirection_log_404', $this->can_log );
216 554 }
217 555 }