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

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

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