PluginProbe
Redirection / 5.6.0
Redirection v5.6.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.6.0, at modules/wordpress.php

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