PluginProbe
Redirection / 4.9.2
Redirection v4.9.2
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 4.9.2, at modules/wordpress.php

489 lines 12.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|null
46 */
47 private $matched = null;
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
88 // Setup the various filters and actions that allow Redirection to happen
89 add_action( 'redirection_visit', [ $this, 'redirection_visit' ], 10, 3 );
90 add_action( 'redirection_do_nothing', [ $this, 'redirection_do_nothing' ] );
91
92 // Prevent WordPress overriding a canonical redirect
93 add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ], 10, 2 );
94
95 // Log 404s and perform 'URL and WordPress page type' redirects
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 );
104 }
105
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 /**
126 * This ensures that a matched URL is not overriddden by WordPress, if the URL happens to be a WordPress URL of some kind
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
132 */
133 public function redirect_canonical( $redirect_url, $requested_url ) {
134 if ( $this->matched ) {
135 return false;
136 }
137
138 return $redirect_url;
139 }
140
141 /**
142 * WordPress 'template_redirect' hook. Used to check for 404s
143 *
144 * @return void
145 */
146 public function template_redirect() {
147 if ( ! is_404() || $this->matched ) {
148 return;
149 }
150
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'
154 return;
155 }
156
157 $options = red_get_options();
158
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 );
174 }
175 }
176
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 } ) );
186
187 if ( count( $page_types ) > 0 ) {
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;
191 }
192
193 return false;
194 }
195
196 /**
197 * Called by a 'do nothing' action. Return true to stop further processing of the 'do nothing'
198 *
199 * @return boolean
200 */
201 public function redirection_do_nothing() {
202 $this->can_log = false;
203 return true;
204 }
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 */
214 public function redirection_visit( $redirect, $url, $target ) {
215 $redirect->visit( $url, $target );
216 }
217
218 /**
219 * Get canonical target
220 *
221 * @return string|false
222 */
223 public function get_canonical_target() {
224 $options = red_get_options();
225 $canonical = new Redirection_Canonical( $options['https'], $options['preferred_domain'], $options['aliases'], get_home_url() );
226
227 // Relocate domain?
228 if ( $options['relocate'] ) {
229 return $canonical->relocate_request( $options['relocate'], Redirection_Request::get_server_name(), Redirection_Request::get_request_url() );
230 }
231
232 // Force HTTPS or www
233 return $canonical->get_redirect( Redirection_Request::get_request_server_name(), Redirection_Request::get_request_url() );
234 }
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
244 if ( $target ) {
245 // phpcs:ignore
246 wp_redirect( $target, 301, 'redirection' );
247 die();
248 }
249 }
250
251 /**
252 * Redirection 'main loop'. Checks the currently requested URL against the database and perform a redirect, if necessary.
253 *
254 * @return void
255 */
256 public function init() {
257 if ( $this->matched ) {
258 return;
259 }
260
261 $request = new Red_Url_Request( Redirection_Request::get_request_url() );
262
263 // Make sure we don't try and redirect something essential
264 if ( $request->is_valid() && ! $request->is_protected_url() ) {
265 do_action( 'redirection_first', $request->get_decoded_url(), $this );
266
267 // Get all redirects that match the URL
268 $redirects = Red_Item::get_for_url( $request->get_decoded_url() );
269
270 // Redirects will be ordered by position. Run through the list until one fires
271 foreach ( (array) $redirects as $item ) {
272 if ( $item->is_match( $request->get_decoded_url(), $request->get_original_url() ) ) {
273 $this->matched = $item;
274 break;
275 }
276 }
277
278 do_action( 'redirection_last', $request->get_decoded_url(), $this );
279
280 if ( ! $this->matched ) {
281 // Keep them for later
282 $this->redirects = $redirects;
283 }
284 }
285 }
286
287 /**
288 * Fix for incorrect headers sent when using FastCGI/IIS
289 *
290 * @param String $status HTTP status line.
291 * @return String
292 */
293 public function status_header( $status ) {
294 if ( substr( php_sapi_name(), 0, 3 ) === 'cgi' ) {
295 return str_replace( 'HTTP/1.1', 'Status:', $status );
296 }
297
298 return $status;
299 }
300
301 /**
302 * Add any custom HTTP headers to the response.
303 *
304 * @param array $obj Some object.
305 * @return void
306 */
307 public function send_headers( $obj ) {
308 if ( ! empty( $this->matched ) && $this->matched->action->get_code() === 410 ) {
309 add_filter( 'status_header', [ $this, 'set_header_410' ] );
310 }
311
312 // Add any custom headers
313 $options = red_get_options();
314 $headers = new Red_Http_Headers( $options['headers'] );
315 $headers->run( $headers->get_site_headers() );
316 }
317
318 /**
319 * Add support for a 410 response.
320 *
321 * @return String
322 */
323 public function set_header_410() {
324 return 'HTTP/1.1 410 Gone';
325 }
326
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;
335
336 if ( $is_IIS ) {
337 header( "Refresh: 0;url=$url" );
338 }
339 }
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 ) {
349 if ( $status === 301 && php_sapi_name() === 'cgi-fcgi' ) {
350 $servers_to_check = [ 'lighttpd', 'nginx' ];
351
352 foreach ( $servers_to_check as $name ) {
353 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && stripos( $_SERVER['SERVER_SOFTWARE'], $name ) !== false ) {
354 status_header( $status );
355 header( "Location: $url" );
356 exit( 0 );
357 }
358 }
359 }
360 }
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
443 if ( intval( $status, 10 ) === 307 ) {
444 status_header( $status );
445 nocache_headers();
446 return $url;
447 }
448
449 $options = red_get_options();
450 $headers = new Red_Http_Headers( $options['headers'] );
451 $headers->run( $headers->get_redirect_headers() );
452
453 // Do we need to set the cache header?
454 if ( ! headers_sent() && isset( $options['redirect_cache'] ) && $options['redirect_cache'] !== 0 && intval( $status, 10 ) === 301 ) {
455 if ( $options['redirect_cache'] === -1 ) {
456 // No cache - just use WP function
457 nocache_headers();
458 } else {
459 // Custom cache
460 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s T', time() + $options['redirect_cache'] * 60 * 60 ) );
461 header( 'Cache-Control: max-age=' . $options['redirect_cache'] * 60 * 60 );
462 }
463 }
464
465 status_header( $status );
466 return $url;
467 }
468
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;
478 }
479
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 );
487 }
488 }
489