PluginProbe
Redirection / 4.9
Redirection v4.9
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
← All changes | modules/wordpress.php +290 -55 4.84.9 View file →
@@ -1,20 +1,75 @@
1 1 <?php
2 2
3 +/**
4 + * WordPress redirect module.
5 + *
6 + * Provides PHP controlled redirects and monitoring and is the core of the front-end redirection.
7 + */
3 8 class WordPress_Module extends Red_Module {
9 + /**
10 + * @var integer
11 + */
4 12 const MODULE_ID = 1;
5 13
6 - private $matched = false;
14 + /**
15 + * Can we log?
16 + *
17 + * @var boolean
18 + */
7 19 private $can_log = true;
8 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|null
46 + */
47 + private $matched = null;
48 +
49 + /**
50 + * Return the module ID
51 + *
52 + * @return integer
53 + */
9 54 public function get_id() {
10 55 return self::MODULE_ID;
11 56 }
12 57
58 + /**
59 + * Return the module name
60 + *
61 + * @return string
62 + */
13 63 public function get_name() {
14 64 return 'WordPress';
15 65 }
16 66
67 + /**
68 + * Start the module. Hooks any filters and actions
69 + *
70 + * @return void
71 + */
17 72 public function start() {
18 73 // Only run redirect rules if we're not disabled
19 74 if ( ! red_is_disabled() ) {
20 75 // Canonical site settings - https, www, relocate, and aliases
@@ -38,13 +93,43 @@
38 93 add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ], 10, 2 );
39 94
40 95 // Log 404s and perform 'URL and WordPress page type' redirects
41 96 add_action( 'template_redirect', [ $this, 'template_redirect' ] );
97 +
98 + // Back-compat for < database 4.2
99 + add_filter( 'redirection_404_data', [ $this, 'log_back_compat' ] );
100 + add_filter( 'redirection_log_data', [ $this, 'log_back_compat' ] );
101 +
102 + // Record the redirect agent
103 + add_filter( 'x_redirect_by', [ $this, 'record_redirect_by' ], 90 );
42 104 }
43 105
44 - /*
106 + /**
107 + * Back-compatability for Redirection databases older than 4.2. Prevents errors from storing data that has no DB column
108 + *
109 + * @param array $insert Data to log.
110 + * @return array
111 + */
112 + public function log_back_compat( $insert ) {
113 + // Remove columns not supported in older versions
114 + $status = new Red_Database_Status();
115 +
116 + if ( ! $status->does_support( '4.2' ) ) {
117 + foreach ( [ 'request_data', 'request_method', 'http_code', 'domain', 'redirect_by' ] as $ignore ) {
118 + unset( $insert[ $ignore ] );
119 + }
120 + }
121 +
122 + return $insert;
123 + }
124 +
125 + /**
45 126 * This ensures that a matched URL is not overriddden by WordPress, if the URL happens to be a WordPress URL of some kind
46 127 * For example: /?author=1 will be redirected to /author/name unless this returns false
128 + *
129 + * @param String $redirect_url The redirected URL.
130 + * @param String $requested_url The requested URL.
131 + * @return String|false
47 132 */
48 133 public function redirect_canonical( $redirect_url, $requested_url ) {
49 134 if ( $this->matched ) {
50 135 return false;
@@ -52,88 +137,122 @@
52 137
53 138 return $redirect_url;
54 139 }
55 140
141 + /**
142 + * WordPress 'template_redirect' hook. Used to check for 404s
143 + *
144 + * @return void
145 + */
56 146 public function template_redirect() {
57 147 if ( ! is_404() || $this->matched ) {
58 148 return;
59 149 }
60 150
61 - if ( $this->match_404_type() ) {
62 - // Don't log an intentionally redirected 404
151 + // We are on a 404. Check if we have a 'URL and page type' match in any of the matched redirects.
152 + if ( $this->is_url_and_page_type() ) {
153 + // Don't log an intentionally redirected 404 as part of the 'url and page type'
63 154 return;
64 155 }
65 156
66 157 $options = red_get_options();
67 158
68 - if ( isset( $options['expire_404'] ) && $options['expire_404'] >= 0 && apply_filters( 'redirection_log_404', $this->can_log ) ) {
69 - RE_404::create( Redirection_Request::get_request_url(), Redirection_Request::get_user_agent(), Redirection_Request::get_ip(), Redirection_Request::get_referrer() );
159 + if ( isset( $options['expire_404'] ) && $options['expire_404'] >= 0 && $this->can_log() ) {
160 + $details = [
161 + 'agent' => Redirection_Request::get_user_agent(),
162 + 'referrer' => Redirection_Request::get_referrer(),
163 + 'request_method' => Redirection_Request::get_request_method(),
164 + 'http_code' => 404,
165 + ];
166 +
167 + if ( $options['log_header'] ) {
168 + $details['request_data'] = [
169 + 'headers' => Redirection_Request::get_request_headers(),
170 + ];
171 + }
172 +
173 + Red_404_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
70 174 }
71 175 }
72 176
73 - private function match_404_type() {
74 - if ( ! property_exists( $this, 'redirects' ) || count( $this->redirects ) === 0 ) {
75 - return false;
76 - }
177 + /**
178 + * Return `true` if any of the matched redirects is a 'url and page type', `false` otherwise
179 + *
180 + * @return boolean
181 + */
182 + private function is_url_and_page_type() {
183 + $page_types = array_values( array_filter( $this->redirects, function( Red_Item $redirect ) {
184 + return $redirect->match->get_type() === 'page';
185 + } ) );
77 186
78 - $page_types = array_values( array_filter( $this->redirects, [ $this, 'only_404' ] ) );
79 -
80 187 if ( count( $page_types ) > 0 ) {
81 - $url = apply_filters( 'redirection_url_source', Redirection_Request::get_request_url() );
82 -
83 - foreach ( $page_types as $page_type ) {
84 - if ( $page_type->is_match( $url ) ) {
85 - return true;
86 - }
87 - }
188 + $request = new Red_Url_Request( Redirection_Request::get_request_url() );
189 + $page_types[0]->is_match( $request->get_decoded_url(), $request->get_original_url() );
190 + return true;
88 191 }
89 192
90 193 return false;
91 194 }
92 195
93 - private function only_404( $redirect ) {
94 - return $redirect->match->get_type() === 'page';
95 - }
96 -
97 - // Return true to stop further processing of the 'do nothing'
196 + /**
197 + * Called by a 'do nothing' action. Return true to stop further processing of the 'do nothing'
198 + *
199 + * @return boolean
200 + */
98 201 public function redirection_do_nothing() {
99 202 $this->can_log = false;
100 203 return true;
101 204 }
102 205
206 + /**
207 + * Action fired when a redirect is performed, and used to log the data
208 + *
209 + * @param Red_Item $redirect The redirect.
210 + * @param String $url The source URL.
211 + * @param String $target The target URL.
212 + * @return void
213 + */
103 214 public function redirection_visit( $redirect, $url, $target ) {
104 215 $redirect->visit( $url, $target );
105 216 }
106 217
107 - public function canonical_domain() {
218 + /**
219 + * Get canonical target
220 + *
221 + * @return string|false
222 + */
223 + public function get_canonical_target() {
108 224 $options = red_get_options();
109 225 $canonical = new Redirection_Canonical( $options['https'], $options['preferred_domain'], $options['aliases'], get_home_url() );
110 226
111 227 // Relocate domain?
112 - $target = false;
113 228 if ( $options['relocate'] ) {
114 - $target = $canonical->relocate_request( $options['relocate'], Redirection_Request::get_server_name(), Redirection_Request::get_request_url() );
229 + return $canonical->relocate_request( $options['relocate'], Redirection_Request::get_server_name(), Redirection_Request::get_request_url() );
115 230 }
116 231
117 232 // Force HTTPS or www
118 - if ( ! $target ) {
119 - $target = $canonical->get_redirect( Redirection_Request::get_server_name(), Redirection_Request::get_request_url() );
120 - }
233 + return $canonical->get_redirect( Redirection_Request::get_request_server_name(), Redirection_Request::get_request_url() );
234 + }
121 235
236 + /**
237 + * Checks for canonical domain requests
238 + *
239 + * @return void
240 + */
241 + public function canonical_domain() {
242 + $target = $this->get_canonical_target();
243 +
122 244 if ( $target ) {
123 - add_filter( 'x_redirect_by', [ $this, 'x_redirect_by' ] );
124 245 // phpcs:ignore
125 - wp_redirect( $target, 301 );
246 + wp_redirect( $target, 301, 'redirection' );
126 247 die();
127 248 }
128 249 }
129 250
130 - public function x_redirect_by() {
131 - return 'redirection';
132 - }
133 -
134 251 /**
135 - * This is the key to Redirection and where requests are matched to redirects
252 + * Redirection 'main loop'. Checks the currently requested URL against the database and perform a redirect, if necessary.
253 + *
254 + * @return void
136 255 */
137 256 public function init() {
138 257 if ( $this->matched ) {
139 258 return;
@@ -164,10 +283,15 @@
164 283 }
165 284 }
166 285 }
167 286
287 + /**
288 + * Fix for incorrect headers sent when using FastCGI/IIS
289 + *
290 + * @param String $status HTTP status line.
291 + * @return String
292 + */
168 293 public function status_header( $status ) {
169 - // Fix for incorrect headers sent when using FastCGI/IIS
170 294 if ( substr( php_sapi_name(), 0, 3 ) === 'cgi' ) {
171 295 return str_replace( 'HTTP/1.1', 'Status:', $status );
172 296 }
173 297
@@ -173,8 +297,14 @@
173 297
174 298 return $status;
175 299 }
176 300
301 + /**
302 + * Add any custom HTTP headers to the response.
303 + *
304 + * @param array $obj Some object.
305 + * @return void
306 + */
177 307 public function send_headers( $obj ) {
178 308 if ( ! empty( $this->matched ) && $this->matched->action->get_code() === 410 ) {
179 309 add_filter( 'status_header', [ $this, 'set_header_410' ] );
180 310 }
@@ -184,24 +314,39 @@
184 314 $headers = new Red_Http_Headers( $options['headers'] );
185 315 $headers->run( $headers->get_site_headers() );
186 316 }
187 317
318 + /**
319 + * Add support for a 410 response.
320 + *
321 + * @return String
322 + */
188 323 public function set_header_410() {
189 324 return 'HTTP/1.1 410 Gone';
190 325 }
191 326
192 - public function wp_redirect( $url, $status = 302 ) {
193 - global $wp_version, $is_IIS;
327 + /**
328 + * IIS fix. Don't know if this is still needed
329 + *
330 + * @param String $url URL.
331 + * @return void
332 + */
333 + private function iis_fix( $url ) {
334 + global $is_IIS;
194 335
195 - $options = red_get_options();
196 - $headers = new Red_Http_Headers( $options['headers'] );
197 - $headers->run( $headers->get_redirect_headers() );
198 -
199 336 if ( $is_IIS ) {
200 337 header( "Refresh: 0;url=$url" );
201 - return $url;
202 338 }
339 + }
203 340
341 + /**
342 + * Don't know if this is still needed
343 + *
344 + * @param String $url URL.
345 + * @param integer $status HTTP status code.
346 + * @return void
347 + */
348 + private function cgi_fix( $url, $status ) {
204 349 if ( $status === 301 && php_sapi_name() === 'cgi-fcgi' ) {
205 350 $servers_to_check = [ 'lighttpd', 'nginx' ];
206 351
207 352 foreach ( $servers_to_check as $name ) {
@@ -211,9 +356,91 @@
211 356 exit( 0 );
212 357 }
213 358 }
214 359 }
360 + }
215 361
362 + /**
363 + * Get a 'source' for a redirect by digging through the backtrace.
364 + *
365 + * @return string[]
366 + */
367 + private function get_redirect_source() {
368 + $ignore = [
369 + 'WP_Hook',
370 + 'template-loader.php',
371 + 'wp-blog-header.php',
372 + ];
373 +
374 + // phpcs:ignore
375 + $source = wp_debug_backtrace_summary( null, 5, false );
376 +
377 + return array_filter( $source, function( $item ) use ( $ignore ) {
378 + foreach ( $ignore as $ignore_item ) {
379 + if ( strpos( $item, $ignore_item ) !== false ) {
380 + return false;
381 + }
382 + }
383 +
384 + return true;
385 + } );
386 + }
387 +
388 + /**
389 + * Record a redirect.
390 + *
391 + * @param String $agent Redirect agent.
392 + * @return string
393 + */
394 + public function record_redirect_by( $agent ) {
395 + // Have we already redirected with Redirection?
396 + if ( $this->matched || $agent === 'redirection' ) {
397 + return $agent;
398 + }
399 +
400 + $options = red_get_options();
401 +
402 + if ( ! $options['log_external'] ) {
403 + return $agent;
404 + }
405 +
406 + $details = [
407 + 'target' => $this->redirect_url,
408 + 'agent' => Redirection_Request::get_user_agent(),
409 + 'referrer' => Redirection_Request::get_referrer(),
410 + 'request_method' => Redirection_Request::get_request_method(),
411 + 'redirect_by' => $agent ? $agent : 'wordpress',
412 + 'http_code' => $this->redirect_code,
413 + 'request_data' => [
414 + 'source' => array_values( $this->get_redirect_source() ),
415 + ],
416 + ];
417 +
418 + if ( $options['log_header'] ) {
419 + $details['request_data']['headers'] = Redirection_Request::get_request_headers();
420 + }
421 +
422 + Red_Redirect_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
423 +
424 + return $agent;
425 + }
426 +
427 + /**
428 + * Perform any pre-redirect processing, such as logging and header fixing.
429 + *
430 + * @param String $url Target URL.
431 + * @param integer $status HTTP status.
432 + * @return string
433 + */
434 + public function wp_redirect( $url, $status = 302 ) {
435 + global $wp_version;
436 +
437 + $this->redirect_url = $url;
438 + $this->redirect_code = $status;
439 +
440 + $this->iis_fix( $url );
441 + $this->cgi_fix( $url, $status );
442 +
216 443 if ( intval( $status, 10 ) === 307 ) {
217 444 status_header( $status );
218 445 nocache_headers();
219 446 return $url;
@@ -219,8 +446,10 @@
219 446 return $url;
220 447 }
221 448
222 449 $options = red_get_options();
450 + $headers = new Red_Http_Headers( $options['headers'] );
451 + $headers->run( $headers->get_redirect_headers() );
223 452
224 453 // Do we need to set the cache header?
225 454 if ( ! headers_sent() && isset( $options['redirect_cache'] ) && $options['redirect_cache'] !== 0 && intval( $status, 10 ) === 301 ) {
226 455 if ( $options['redirect_cache'] === -1 ) {
@@ -236,18 +465,24 @@
236 465 status_header( $status );
237 466 return $url;
238 467 }
239 468
240 - public function update( array $data ) {
241 - return false;
469 + /**
470 + * Reset the module. Used for unit tests
471 + *
472 + * @param Red_Item|null $matched Set the `matched` var.
473 + * @return void
474 + */
475 + public function reset( $matched = null ) {
476 + $this->can_log = true;
477 + $this->matched = $matched;
242 478 }
243 479
244 - protected function load( $options ) {
245 - }
246 -
247 - protected function flush_module() {
248 - }
249 -
250 - public function reset() {
251 - $this->can_log = true;
480 + /**
481 + * Can we log a redirect?
482 + *
483 + * @return boolean
484 + */
485 + public function can_log() {
486 + return apply_filters( 'redirection_log_404', $this->can_log );
252 487 }
253 488 }