PluginProbe
Redirection / 5.5.1
Redirection v5.5.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.5.1, at modules/wordpress.php

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