PluginProbe
Redirection / 5.2.2
Redirection v5.2.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 / models / htaccess.php

htaccess.php in Redirection 5.2.2, at models/htaccess.php

475 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Convert redirects to .htaccess format
5 *
6 * Ignores:
7 * - Trailing slash flag
8 * - Query flags
9 */
10 class Red_Htaccess {
11 /**
12 * Array of redirect lines
13 *
14 * @var array<string>
15 */
16 private $items = array();
17
18 const INSERT_REGEX = '@\n?# Created by Redirection(?:.*?)# End of Redirection\n?@sm';
19
20 /**
21 * Encode the 'from' URL
22 *
23 * @param string $url From URL.
24 * @param bool $ignore_trailing Ignore trailing slashes.
25 * @return string
26 */
27 private function encode_from( $url, $ignore_trailing ) {
28 $url = $this->encode( $url );
29
30 // Apache 2 does not need a leading slashing
31 $url = ltrim( $url, '/' );
32
33 if ( $ignore_trailing ) {
34 $url = rtrim( $url, '/' ) . '/?';
35 }
36
37 // Exactly match the URL
38 return '^' . $url . '$';
39 }
40
41 /**
42 * URL encode some things, but other things can be passed through
43 *
44 * @param string $url URL.
45 * @return string
46 */
47 private function encode2nd( $url ) {
48 $allowed = [
49 '%2F' => '/',
50 '%3F' => '?',
51 '%3A' => ':',
52 '%3D' => '=',
53 '%26' => '&',
54 '%25' => '%',
55 '+' => '%20',
56 '%24' => '$',
57 '%23' => '#',
58 ];
59
60 $url = rawurlencode( $url );
61 return $this->replace_encoding( $url, $allowed );
62 }
63
64 /**
65 * Replace encoded characters in a URL
66 *
67 * @param string $str Source string.
68 * @param array $allowed Allowed encodings.
69 * @return string
70 */
71 private function replace_encoding( $str, $allowed ) {
72 foreach ( $allowed as $before => $after ) {
73 $str = str_replace( $before, $after, $str );
74 }
75
76 return $str;
77 }
78
79 /**
80 * Encode a URL
81 *
82 * @param string $url URL.
83 * @return string
84 */
85 private function encode( $url ) {
86 $allowed = [
87 '%2F' => '/',
88 '%3F' => '?',
89 '+' => '%20',
90 '.' => '\\.',
91 ];
92
93 return $this->replace_encoding( rawurlencode( $url ), $allowed );
94 }
95
96 /**
97 * Encode a regex URL
98 *
99 * @param string $url URL.
100 * @return string
101 */
102 private function encode_regex( $url ) {
103 // Remove any newlines
104 $url = preg_replace( "/[\r\n\t].*?$/s", '', $url );
105
106 // Remove invalid characters
107 $url = preg_replace( '/[^\PC\s]/u', '', $url );
108
109 // Make sure spaces are quoted
110 $url = str_replace( ' ', '%20', $url );
111 $url = str_replace( '%24', '$', $url );
112
113 // No leading slash
114 $url = ltrim( $url, '/' );
115
116 // If pattern has a ^ at the start then ensure we don't have a slash immediatley after
117 $url = preg_replace( '@^\^/@', '^', $url );
118
119 return $url;
120 }
121
122 /**
123 * Add a referrer redirect
124 *
125 * @param Red_Item $item Redirect item.
126 * @param Referrer_Match $match Redirect match.
127 * @return void
128 */
129 private function add_referrer( $item, $match ) {
130 $from = $this->encode_from( ltrim( $item->get_url(), '/' ), $item->source_flags && $item->source_flags->is_ignore_trailing() );
131 if ( $item->is_regex() ) {
132 $from = $this->encode_regex( ltrim( $item->get_url(), '/' ) );
133 }
134
135 if ( ( $match->url_from || $match->url_notfrom ) && $match->referrer ) {
136 $referrer = $match->regex ? $this->encode_regex( $match->referrer ) : $this->encode_from( $match->referrer, false );
137 $to = false;
138
139 if ( $match->url_from ) {
140 $to = $this->target( $item->get_action_type(), $match->url_from, $item->get_action_code(), $item->get_match_data() );
141 }
142
143 if ( $match->url_notfrom ) {
144 $to = $this->target( $item->get_action_type(), $match->url_notfrom, $item->get_action_code(), $item->get_match_data() );
145 }
146
147 $this->items[] = sprintf( 'RewriteCond %%{HTTP_REFERER} %s [NC]', $referrer );
148 if ( $to ) {
149 $this->items[] = sprintf( 'RewriteRule %s %s', $from, $to );
150 }
151 }
152 }
153
154 /**
155 * Add a useragent redirect
156 *
157 * @param Red_Item $item Redirect item.
158 * @param Agent_Match $match Redirect match.
159 * @return void
160 */
161 private function add_agent( $item, $match ) {
162 $from = $this->encode( ltrim( $item->get_url(), '/' ) );
163 if ( $item->is_regex() ) {
164 $from = $this->encode_regex( ltrim( $item->get_url(), '/' ) );
165 }
166
167 if ( ( $match->url_from || $match->url_notfrom ) && $match->agent ) {
168 $agent = ( $match->regex ? $this->encode_regex( $match->agent ) : $this->encode2nd( $match->agent ) );
169 $to = false;
170
171 if ( $match->url_from ) {
172 $to = $this->target( $item->get_action_type(), $match->url_from, $item->get_action_code(), $item->get_match_data() );
173 }
174
175 if ( $match->url_notfrom ) {
176 $to = $this->target( $item->get_action_type(), $match->url_notfrom, $item->get_action_code(), $item->get_match_data() );
177 }
178
179 $this->items[] = sprintf( 'RewriteCond %%{HTTP_USER_AGENT} %s [NC]', $agent );
180 if ( $to ) {
181 $this->items[] = sprintf( 'RewriteRule %s %s', $from, $to );
182 }
183 }
184 }
185
186 /**
187 * Add a server redirect
188 *
189 * @param Red_Item $item Redirect item.
190 * @param Server_Match $match Redirect match.
191 * @return void
192 */
193 private function add_server( $item, $match ) {
194 $match->url = $match->url_from;
195 $this->items[] = sprintf( 'RewriteCond %%{HTTP_HOST} ^%s$ [NC]', preg_quote( wp_parse_url( $match->server, PHP_URL_HOST ), '/' ) );
196 $this->add_url( $item, $match );
197 }
198
199 /**
200 * Add a redirect
201 *
202 * @param Red_Item $item Redirect item.
203 * @param Red_Match $match Redirect match.
204 * @return void
205 */
206 private function add_url( $item, $match ) {
207 $url = $item->get_url();
208
209 if ( $item->is_regex() === false && strpos( $url, '?' ) !== false ) {
210 $url_parts = wp_parse_url( $url );
211
212 if ( isset( $url_parts['path'] ) ) {
213 $url = $url_parts['path'];
214 $query = isset( $url_parts['query'] ) ? $url_parts['query'] : '';
215 $this->items[] = sprintf( 'RewriteCond %%{QUERY_STRING} ^%s$', $query );
216 }
217 }
218
219 $to = $this->target( $item->get_action_type(), $match->url, $item->get_action_code(), $item->get_match_data() );
220 $from = $this->encode_from( $url, $item->source_flags && $item->source_flags->is_ignore_trailing() );
221
222 if ( $item->is_regex() ) {
223 $from = $this->encode_regex( $item->get_url() );
224 }
225
226 if ( $to ) {
227 $this->items[] = sprintf( 'RewriteRule %s %s', $from, $to );
228 }
229 }
230
231 /**
232 * Add a redirect flags
233 *
234 * @return string
235 */
236 private function add_flags( $current, array $flags ) {
237 return $current . ' [' . implode( ',', $flags ) . ']';
238 }
239
240 /**
241 * Get source flags
242 *
243 * @param array<string> $existing Existing flags.
244 * @param array<string> $source Source flags.
245 * @param string $url URL.
246 * @return array<string>
247 */
248 private function get_source_flags( array $existing, array $source, $url ) {
249 $flags = [];
250
251 if ( isset( $source['flag_case'] ) && $source['flag_case'] ) {
252 $flags[] = 'NC';
253 }
254
255 if ( isset( $source['flag_query'] ) && $source['flag_query'] === 'pass' ) {
256 $flags[] = 'QSA';
257 }
258
259 if ( strpos( $url, '#' ) !== false ) {
260 $flags[] = 'NE';
261 }
262
263 return array_merge( $existing, $flags );
264 }
265
266 /**
267 * Add a random target.
268 *
269 * @param [type] $data
270 * @param [type] $code
271 * @param [type] $match_data
272 * @return string
273 */
274 private function action_random( $data, $code, $match_data ) {
275 // Pick a WP post at random
276 global $wpdb;
277
278 $post = $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} ORDER BY RAND() LIMIT 0,1" );
279 $url = wp_parse_url( get_permalink( $post ) );
280
281 $flags = [ sprintf( 'R=%d', $code ) ];
282 $flags[] = 'L';
283 $flags = $this->get_source_flags( $flags, $match_data['source'], $data );
284
285 return $this->add_flags( $this->encode( $url['path'] ), $flags );
286 }
287
288 /**
289 * Add a passthrough target.
290 *
291 * @param [type] $data
292 * @param [type] $code
293 * @param [type] $match_data
294 * @return string
295 */
296 private function action_pass( $data, $code, $match_data ) {
297 $flags = $this->get_source_flags( [ 'L' ], $match_data['source'], $data );
298
299 return $this->add_flags( $this->encode2nd( $data ), $flags );
300 }
301
302 /**
303 * Add an error target.
304 *
305 * @param [type] $data
306 * @param [type] $code
307 * @param [type] $match_data
308 * @return string
309 */
310 private function action_error( $data, $code, $match_data ) {
311 $flags = $this->get_source_flags( [ 'F' ], $match_data['source'], $data );
312
313 if ( $code === 410 ) {
314 $flags = $this->get_source_flags( [ 'G' ], $match_data['source'], $data );
315 }
316
317 return $this->add_flags( '/', $flags );
318 }
319
320 /**
321 * Add a URL target.
322 *
323 * @param [type] $data
324 * @param [type] $code
325 * @param [type] $match_data
326 * @return string
327 */
328 private function action_url( $data, $code, $match_data ) {
329 $flags = [ sprintf( 'R=%d', $code ) ];
330 $flags[] = 'L';
331 $flags = $this->get_source_flags( $flags, $match_data['source'], $data );
332
333 return $this->add_flags( $this->encode2nd( $data ), $flags );
334 }
335
336 /**
337 * Return URL target
338 *
339 * @param [type] $data
340 * @param [type] $code
341 * @param [type] $match_data
342 * @return string
343 */
344 private function target( $action, $data, $code, $match_data ) {
345 $target = 'action_' . $action;
346
347 if ( method_exists( $this, $target ) ) {
348 return $this->$target( $data, $code, $match_data );
349 }
350
351 return '';
352 }
353
354 /**
355 * Generate the .htaccess file in memory
356 *
357 * @return string
358 */
359 private function generate() {
360 $version = red_get_plugin_data( dirname( dirname( __FILE__ ) ) . '/redirection.php' );
361
362 if ( count( $this->items ) === 0 ) {
363 return '';
364 }
365
366 $text = [
367 '# Created by Redirection',
368 '# ' . date( 'r' ),
369 '# Redirection ' . trim( $version['Version'] ) . ' - https://redirection.me',
370 '',
371 '<IfModule mod_rewrite.c>',
372 ];
373
374 // Add http => https option
375 $options = red_get_options();
376 if ( $options['https'] ) {
377 $text[] = 'RewriteCond %{HTTPS} off';
378 $text[] = 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]';
379 }
380
381 // Add redirects
382 $text = array_merge( $text, array_filter( array_map( [ $this, 'sanitize_redirect' ], $this->items ) ) );
383
384 // End of mod_rewrite
385 $text[] = '</IfModule>';
386 $text[] = '';
387
388 // End of redirection section
389 $text[] = '# End of Redirection';
390
391 $text = implode( "\n", $text );
392 return "\n" . $text . "\n";
393 }
394
395 /**
396 * Add a redirect to the file
397 *
398 * @param Red_Item $item Redirect.
399 * @return void
400 */
401 public function add( $item ) {
402 $target = 'add_' . $item->get_match_type();
403
404 if ( method_exists( $this, $target ) && $item->is_enabled() ) {
405 $this->$target( $item, $item->match );
406 }
407 }
408
409 /**
410 * Get the .htaccess file
411 *
412 * @param boolean $existing Existing .htaccess data.
413 * @return string
414 */
415 public function get( $existing = false ) {
416 $text = $this->generate();
417
418 if ( $existing ) {
419 if ( preg_match( self::INSERT_REGEX, $existing ) > 0 ) {
420 $text = preg_replace( self::INSERT_REGEX, str_replace( '$', '\\$', $text ), $existing );
421 } else {
422 $text = $text . "\n" . trim( $existing );
423 }
424 }
425
426 return trim( $text );
427 }
428
429 /**
430 * Sanitize the redirect
431 *
432 * @param string $text Text.
433 * @return string
434 */
435 public function sanitize_redirect( $text ) {
436 return str_replace( [ '<?', '>' ], '', $text );
437 }
438
439 /**
440 * Sanitize the filename
441 *
442 * @param string $filename Filename.
443 * @return string
444 */
445 public function sanitize_filename( $filename ) {
446 return str_replace( '.php', '', $filename );
447 }
448
449 /**
450 * Save the .htaccess to a file
451 *
452 * @param string $filename Filename to save.
453 * @param boolean $content_to_save Content to save.
454 * @return bool
455 */
456 public function save( $filename, $content_to_save = false ) {
457 $existing = false;
458 $filename = $this->sanitize_filename( $filename );
459
460 if ( file_exists( $filename ) ) {
461 $existing = file_get_contents( $filename );
462 }
463
464 $file = @fopen( $filename, 'w' );
465 if ( $file ) {
466 $result = fwrite( $file, $this->get( $existing ) );
467 fclose( $file );
468
469 return $result !== false;
470 }
471
472 return false;
473 }
474 }
475