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

557 lines 14.1 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 $server = '';
415 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && is_string( $_SERVER['SERVER_SOFTWARE'] ) ) {
416 $server = sanitize_text_field( $_SERVER['SERVER_SOFTWARE'] );
417 }
418
419 foreach ( $servers_to_check as $name ) {
420
421 if ( stripos( $server, $name ) !== false ) {
422 status_header( $status );
423 header( "Location: $url" );
424 exit( 0 );
425 }
426 }
427 }
428 }
429
430 /**
431 * Get a 'source' for a redirect by digging through the backtrace.
432 *
433 * @return string[]
434 */
435 private function get_redirect_source() {
436 $ignore = [
437 'WP_Hook',
438 'template-loader.php',
439 'wp-blog-header.php',
440 ];
441
442 // phpcs:ignore
443 $source = wp_debug_backtrace_summary( null, 5, false );
444
445 return array_filter( $source, function( $item ) use ( $ignore ) {
446 foreach ( $ignore as $ignore_item ) {
447 if ( strpos( $item, $ignore_item ) !== false ) {
448 return false;
449 }
450 }
451
452 return true;
453 } );
454 }
455
456 /**
457 * Record a redirect.
458 *
459 * @param string $agent Redirect agent.
460 * @return string
461 */
462 public function record_redirect_by( $agent ) {
463 // Have we already redirected with Redirection?
464 if ( $this->matched || $agent === 'redirection' ) {
465 return $agent;
466 }
467
468 $options = red_get_options();
469
470 if ( ! $options['log_external'] ) {
471 return $agent;
472 }
473
474 $details = [
475 'target' => $this->redirect_url,
476 'agent' => Redirection_Request::get_user_agent(),
477 'referrer' => Redirection_Request::get_referrer(),
478 'request_method' => Redirection_Request::get_request_method(),
479 'redirect_by' => $agent ? $agent : 'wordpress',
480 'http_code' => $this->redirect_code,
481 'request_data' => [
482 'source' => array_values( $this->get_redirect_source() ),
483 ],
484 ];
485
486 if ( $options['log_header'] ) {
487 $details['request_data']['headers'] = Redirection_Request::get_request_headers();
488 }
489
490 Red_Redirect_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
491
492 return $agent;
493 }
494
495 /**
496 * Perform any pre-redirect processing, such as logging and header fixing.
497 *
498 * @param string $url Target URL.
499 * @param integer $status HTTP status.
500 * @return string
501 */
502 public function wp_redirect( $url, $status = 302 ) {
503 global $wp_version;
504
505 $this->redirect_url = $url;
506 $this->redirect_code = $status;
507
508 $this->iis_fix( $url );
509 $this->cgi_fix( $url, $status );
510
511 if ( intval( $status, 10 ) === 307 ) {
512 status_header( $status );
513 nocache_headers();
514 return $url;
515 }
516
517 $options = red_get_options();
518 $headers = new Red_Http_Headers( $options['headers'] );
519 $headers->run( $headers->get_redirect_headers() );
520
521 // Do we need to set the cache header?
522 if ( ! headers_sent() && isset( $options['redirect_cache'] ) && $options['redirect_cache'] !== 0 && intval( $status, 10 ) === 301 ) {
523 if ( $options['redirect_cache'] === -1 ) {
524 // No cache - just use WP function
525 nocache_headers();
526 } else {
527 // Custom cache
528 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s T', time() + $options['redirect_cache'] * 60 * 60 ) );
529 header( 'Cache-Control: max-age=' . $options['redirect_cache'] * 60 * 60 );
530 }
531 }
532
533 status_header( $status );
534 return $url;
535 }
536
537 /**
538 * Reset the module. Used for unit tests
539 *
540 * @param Red_Item|false $matched Set the `matched` var.
541 * @return void
542 */
543 public function reset( $matched = false ) {
544 $this->can_log = true;
545 $this->matched = $matched;
546 }
547
548 /**
549 * Can we log a redirect?
550 *
551 * @return boolean
552 */
553 public function can_log() {
554 return apply_filters( 'redirection_log_404', $this->can_log );
555 }
556 }
557