PluginProbe
Redirection / 5.1.3
Redirection v5.1.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 / modules / wordpress.php

wordpress.php in Redirection 5.1.3, at modules/wordpress.php

556 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * WordPress redirect module.
5 *
6 * Provides PHP controlled redirects and monitoring and is the core of the front-end redirection.
7 */
8 class WordPress_Module extends Red_Module {
9 /**
10 * @var integer
11 */
12 const MODULE_ID = 1;
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 */
47 private $matched = false;
48
49 /**
50 * Return the module ID
51 *
52 * @return integer
53 */
54 public function get_id() {
55 return self::MODULE_ID;
56 }
57
58 /**
59 * Return the module name
60 *
61 * @return string
62 */
63 public function get_name() {
64 return 'WordPress';
65 }
66
67 /**
68 * Start the module. Hooks any filters and actions
69 *
70 * @return void
71 */
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
95 // Setup the various filters and actions that allow Redirection to happen
96 add_action( 'redirection_visit', [ $this, 'redirection_visit' ], 10, 3 );
97 add_action( 'redirection_do_nothing', [ $this, 'redirection_do_nothing' ] );
98
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 );
111 }
112
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 /**
182 * This ensures that a matched URL is not overriddden by WordPress, if the URL happens to be a WordPress URL of some kind
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
188 */
189 public function redirect_canonical( $redirect_url, $requested_url ) {
190 if ( $this->matched ) {
191 return false;
192 }
193
194 return $redirect_url;
195 }
196
197 /**
198 * WordPress 'template_redirect' hook. Used to check for 404s
199 *
200 * @return void
201 */
202 public function template_redirect() {
203 if ( ! is_404() || $this->matched ) {
204 return;
205 }
206
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'
210 return;
211 }
212
213 $options = red_get_options();
214
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 );
230 }
231 }
232
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 } ) );
242
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 }
249
250 return true;
251 }
252
253 return false;
254 }
255
256 /**
257 * Called by a 'do nothing' action. Return true to stop further processing of the 'do nothing'
258 *
259 * @return boolean
260 */
261 public function redirection_do_nothing() {
262 $this->can_log = false;
263 return true;
264 }
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 */
274 public function redirection_visit( $redirect, $url, $target ) {
275 $redirect->visit( $url, $target );
276 }
277
278 /**
279 * Get canonical target
280 *
281 * @return string|false
282 */
283 public function get_canonical_target() {
284 $options = red_get_options();
285 $canonical = new Redirection_Canonical( $options['https'], $options['preferred_domain'], $options['aliases'], get_home_url() );
286
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' );
307 die();
308 }
309 }
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 */
316 public function init() {
317 if ( $this->matched ) {
318 return;
319 }
320
321 $request = new Red_Url_Request( Redirection_Request::get_request_url() );
322
323 // Make sure we don't try and redirect something essential
324 if ( $request->is_valid() && ! $request->is_protected_url() ) {
325 do_action( 'redirection_first', $request->get_decoded_url(), $this );
326
327 // Get all redirects that match the URL
328 $redirects = Red_Item::get_for_url( $request->get_decoded_url() );
329
330 // Redirects will be ordered by position. Run through the list until one fires
331 foreach ( (array) $redirects as $item ) {
332 $action = $item->get_match( $request->get_decoded_url(), $request->get_original_url() );
333
334 if ( $action ) {
335 $this->matched = $item;
336
337 do_action( 'redirection_matched', $request->get_decoded_url(), $item, $redirects );
338
339 $action->run();
340 break;
341 }
342 }
343
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 );
346
347 if ( ! $this->matched ) {
348 // Keep them for later
349 $this->redirects = $redirects;
350 }
351 }
352 }
353
354 /**
355 * Fix for incorrect headers sent when using FastCGI/IIS
356 *
357 * @param String $status HTTP status line.
358 * @return String
359 */
360 public function status_header( $status ) {
361 if ( substr( php_sapi_name(), 0, 3 ) === 'cgi' ) {
362 return str_replace( 'HTTP/1.1', 'Status:', $status );
363 }
364
365 return $status;
366 }
367
368 /**
369 * Add any custom HTTP headers to the response.
370 *
371 * @param array $obj Some object.
372 * @return void
373 */
374 public function send_headers( $obj ) {
375 if ( ! empty( $this->matched ) && $this->matched->action && $this->matched->action->get_code() === 410 ) {
376 add_filter( 'status_header', [ $this, 'set_header_410' ] );
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() );
383 }
384
385 /**
386 * Add support for a 410 response.
387 *
388 * @return String
389 */
390 public function set_header_410() {
391 return 'HTTP/1.1 410 Gone';
392 }
393
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;
402
403 if ( $is_IIS ) {
404 header( "Refresh: 0;url=$url" );
405 }
406 }
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 ) {
416 if ( $status === 301 && php_sapi_name() === 'cgi-fcgi' ) {
417 $servers_to_check = [ 'lighttpd', 'nginx' ];
418
419 foreach ( $servers_to_check as $name ) {
420 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && stripos( $_SERVER['SERVER_SOFTWARE'], $name ) !== false ) {
421 status_header( $status );
422 header( "Location: $url" );
423 exit( 0 );
424 }
425 }
426 }
427 }
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
510 if ( intval( $status, 10 ) === 307 ) {
511 status_header( $status );
512 nocache_headers();
513 return $url;
514 }
515
516 $options = red_get_options();
517 $headers = new Red_Http_Headers( $options['headers'] );
518 $headers->run( $headers->get_redirect_headers() );
519
520 // Do we need to set the cache header?
521 if ( ! headers_sent() && isset( $options['redirect_cache'] ) && $options['redirect_cache'] !== 0 && intval( $status, 10 ) === 301 ) {
522 if ( $options['redirect_cache'] === -1 ) {
523 // No cache - just use WP function
524 nocache_headers();
525 } else {
526 // Custom cache
527 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s T', time() + $options['redirect_cache'] * 60 * 60 ) );
528 header( 'Cache-Control: max-age=' . $options['redirect_cache'] * 60 * 60 );
529 }
530 }
531
532 status_header( $status );
533 return $url;
534 }
535
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;
545 }
546
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 );
554 }
555 }
556