PluginProbe
Redirection / 5.8.1
Redirection v5.8.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.8.1, at modules/wordpress.php

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