PluginProbe
Redirection / 5.3.1
Redirection v5.3.1
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.3.1, at modules/wordpress.php

552 lines 14.0 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 ( 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 $this->is_url_and_page_type();
208
209 $options = red_get_options();
210
211 if ( isset( $options['expire_404'] ) && $options['expire_404'] >= 0 && $this->can_log() ) {
212 $details = [
213 'agent' => Redirection_Request::get_user_agent(),
214 'referrer' => Redirection_Request::get_referrer(),
215 'request_method' => Redirection_Request::get_request_method(),
216 'http_code' => 404,
217 ];
218
219 if ( $options['log_header'] ) {
220 $details['request_data'] = [
221 'headers' => Redirection_Request::get_request_headers(),
222 ];
223 }
224
225 Red_404_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
226 }
227 }
228
229 /**
230 * Return `true` if any of the matched redirects is a 'url and page type', `false` otherwise
231 *
232 * @return boolean
233 */
234 private function is_url_and_page_type() {
235 $page_types = array_values( array_filter( $this->redirects, function( Red_Item $redirect ) {
236 return $redirect->match && $redirect->match->get_type() === 'page';
237 } ) );
238
239 if ( count( $page_types ) > 0 ) {
240 $request = new Red_Url_Request( Redirection_Request::get_request_url() );
241 $action = $page_types[0]->get_match( $request->get_decoded_url(), $request->get_original_url() );
242 if ( $action ) {
243 $action->run();
244 }
245
246 return true;
247 }
248
249 return false;
250 }
251
252 /**
253 * Called by a 'do nothing' action. Return true to stop further processing of the 'do nothing'
254 *
255 * @return boolean
256 */
257 public function redirection_do_nothing() {
258 $this->can_log = false;
259 return true;
260 }
261
262 /**
263 * Action fired when a redirect is performed, and used to log the data
264 *
265 * @param Red_Item $redirect The redirect.
266 * @param String $url The source URL.
267 * @param String $target The target URL.
268 * @return void
269 */
270 public function redirection_visit( $redirect, $url, $target ) {
271 $redirect->visit( $url, $target );
272 }
273
274 /**
275 * Get canonical target
276 *
277 * @return string|false
278 */
279 public function get_canonical_target() {
280 $options = red_get_options();
281 $canonical = new Redirection_Canonical( $options['https'], $options['preferred_domain'], $options['aliases'], get_home_url() );
282
283 // Relocate domain?
284 if ( $options['relocate'] ) {
285 return $canonical->relocate_request( $options['relocate'], Redirection_Request::get_server_name(), Redirection_Request::get_request_url() );
286 }
287
288 // Force HTTPS or www
289 return $canonical->get_redirect( Redirection_Request::get_request_server_name(), Redirection_Request::get_request_url() );
290 }
291
292 /**
293 * Checks for canonical domain requests
294 *
295 * @return void
296 */
297 public function canonical_domain() {
298 $target = $this->get_canonical_target();
299
300 if ( $target ) {
301 // phpcs:ignore
302 wp_redirect( $target, 301, 'redirection' );
303 die();
304 }
305 }
306
307 /**
308 * Redirection 'main loop'. Checks the currently requested URL against the database and perform a redirect, if necessary.
309 *
310 * @return void
311 */
312 public function init() {
313 if ( $this->matched ) {
314 return;
315 }
316
317 $request = new Red_Url_Request( Redirection_Request::get_request_url() );
318
319 // Make sure we don't try and redirect something essential
320 if ( $request->is_valid() && ! $request->is_protected_url() ) {
321 do_action( 'redirection_first', $request->get_decoded_url(), $this );
322
323 // Get all redirects that match the URL
324 $redirects = Red_Item::get_for_url( $request->get_decoded_url() );
325
326 // Redirects will be ordered by position. Run through the list until one fires
327 foreach ( (array) $redirects as $item ) {
328 $action = $item->get_match( $request->get_decoded_url(), $request->get_original_url() );
329
330 if ( $action ) {
331 $this->matched = $item;
332
333 do_action( 'redirection_matched', $request->get_decoded_url(), $item, $redirects );
334
335 $action->run();
336 break;
337 }
338 }
339
340 // We will only get here if there is no match (check $this->matched) or the action does not result in redirecting away
341 do_action( 'redirection_last', $request->get_decoded_url(), $this, $redirects );
342
343 if ( ! $this->matched ) {
344 // Keep them for later
345 $this->redirects = $redirects;
346 }
347 }
348 }
349
350 /**
351 * Fix for incorrect headers sent when using FastCGI/IIS
352 *
353 * @param String $status HTTP status line.
354 * @return String
355 */
356 public function status_header( $status ) {
357 if ( substr( php_sapi_name(), 0, 3 ) === 'cgi' ) {
358 return str_replace( 'HTTP/1.1', 'Status:', $status );
359 }
360
361 return $status;
362 }
363
364 /**
365 * Add any custom HTTP headers to the response.
366 *
367 * @param array $obj Some object.
368 * @return void
369 */
370 public function send_headers( $obj ) {
371 if ( ! empty( $this->matched ) && $this->matched->action && $this->matched->action->get_code() === 410 ) {
372 add_filter( 'status_header', [ $this, 'set_header_410' ] );
373 }
374
375 // Add any custom headers
376 $options = red_get_options();
377 $headers = new Red_Http_Headers( $options['headers'] );
378 $headers->run( $headers->get_site_headers() );
379 }
380
381 /**
382 * Add support for a 410 response.
383 *
384 * @return String
385 */
386 public function set_header_410() {
387 return 'HTTP/1.1 410 Gone';
388 }
389
390 /**
391 * IIS fix. Don't know if this is still needed
392 *
393 * @param String $url URL.
394 * @return void
395 */
396 private function iis_fix( $url ) {
397 global $is_IIS;
398
399 if ( $is_IIS ) {
400 header( "Refresh: 0;url=$url" );
401 }
402 }
403
404 /**
405 * Don't know if this is still needed
406 *
407 * @param String $url URL.
408 * @param integer $status HTTP status code.
409 * @return void
410 */
411 private function cgi_fix( $url, $status ) {
412 if ( $status === 301 && php_sapi_name() === 'cgi-fcgi' ) {
413 $servers_to_check = [ 'lighttpd', 'nginx' ];
414
415 foreach ( $servers_to_check as $name ) {
416 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && stripos( $_SERVER['SERVER_SOFTWARE'], $name ) !== false ) {
417 status_header( $status );
418 header( "Location: $url" );
419 exit( 0 );
420 }
421 }
422 }
423 }
424
425 /**
426 * Get a 'source' for a redirect by digging through the backtrace.
427 *
428 * @return string[]
429 */
430 private function get_redirect_source() {
431 $ignore = [
432 'WP_Hook',
433 'template-loader.php',
434 'wp-blog-header.php',
435 ];
436
437 // phpcs:ignore
438 $source = wp_debug_backtrace_summary( null, 5, false );
439
440 return array_filter( $source, function( $item ) use ( $ignore ) {
441 foreach ( $ignore as $ignore_item ) {
442 if ( strpos( $item, $ignore_item ) !== false ) {
443 return false;
444 }
445 }
446
447 return true;
448 } );
449 }
450
451 /**
452 * Record a redirect.
453 *
454 * @param String $agent Redirect agent.
455 * @return string
456 */
457 public function record_redirect_by( $agent ) {
458 // Have we already redirected with Redirection?
459 if ( $this->matched || $agent === 'redirection' ) {
460 return $agent;
461 }
462
463 $options = red_get_options();
464
465 if ( ! $options['log_external'] ) {
466 return $agent;
467 }
468
469 $details = [
470 'target' => $this->redirect_url,
471 'agent' => Redirection_Request::get_user_agent(),
472 'referrer' => Redirection_Request::get_referrer(),
473 'request_method' => Redirection_Request::get_request_method(),
474 'redirect_by' => $agent ? $agent : 'wordpress',
475 'http_code' => $this->redirect_code,
476 'request_data' => [
477 'source' => array_values( $this->get_redirect_source() ),
478 ],
479 ];
480
481 if ( $options['log_header'] ) {
482 $details['request_data']['headers'] = Redirection_Request::get_request_headers();
483 }
484
485 Red_Redirect_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
486
487 return $agent;
488 }
489
490 /**
491 * Perform any pre-redirect processing, such as logging and header fixing.
492 *
493 * @param String $url Target URL.
494 * @param integer $status HTTP status.
495 * @return string
496 */
497 public function wp_redirect( $url, $status = 302 ) {
498 global $wp_version;
499
500 $this->redirect_url = $url;
501 $this->redirect_code = $status;
502
503 $this->iis_fix( $url );
504 $this->cgi_fix( $url, $status );
505
506 if ( intval( $status, 10 ) === 307 ) {
507 status_header( $status );
508 nocache_headers();
509 return $url;
510 }
511
512 $options = red_get_options();
513 $headers = new Red_Http_Headers( $options['headers'] );
514 $headers->run( $headers->get_redirect_headers() );
515
516 // Do we need to set the cache header?
517 if ( ! headers_sent() && isset( $options['redirect_cache'] ) && $options['redirect_cache'] !== 0 && intval( $status, 10 ) === 301 ) {
518 if ( $options['redirect_cache'] === -1 ) {
519 // No cache - just use WP function
520 nocache_headers();
521 } else {
522 // Custom cache
523 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s T', time() + $options['redirect_cache'] * 60 * 60 ) );
524 header( 'Cache-Control: max-age=' . $options['redirect_cache'] * 60 * 60 );
525 }
526 }
527
528 status_header( $status );
529 return $url;
530 }
531
532 /**
533 * Reset the module. Used for unit tests
534 *
535 * @param Red_Item|false $matched Set the `matched` var.
536 * @return void
537 */
538 public function reset( $matched = false ) {
539 $this->can_log = true;
540 $this->matched = $matched;
541 }
542
543 /**
544 * Can we log a redirect?
545 *
546 * @return boolean
547 */
548 public function can_log() {
549 return apply_filters( 'redirection_log_404', $this->can_log );
550 }
551 }
552