| 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', trim( $from ), trim( $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 || 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 |
$text = str_replace( [ "\r", "\n", "\t" ], '', $text ); |
| 437 |
$text = preg_replace( '/[^\PC\s]/u', '', $text ); |
| 438 |
|
| 439 |
return str_replace( [ '<?', '>' ], '', $text ); |
| 440 |
} |
| 441 |
|
| 442 |
/** |
| 443 |
* Sanitize the filename |
| 444 |
* |
| 445 |
* @param string $filename Filename. |
| 446 |
* @return string |
| 447 |
*/ |
| 448 |
public function sanitize_filename( $filename ) { |
| 449 |
return str_replace( '.php', '', sanitize_text_field( $filename ) ); |
| 450 |
} |
| 451 |
|
| 452 |
/** |
| 453 |
* Save the .htaccess to a file |
| 454 |
* |
| 455 |
* @param string $filename Filename to save. |
| 456 |
* @param boolean $content_to_save Content to save. |
| 457 |
* @return bool |
| 458 |
*/ |
| 459 |
public function save( $filename, $content_to_save = false ) { |
| 460 |
$existing = false; |
| 461 |
$filename = $this->sanitize_filename( $filename ); |
| 462 |
|
| 463 |
if ( file_exists( $filename ) ) { |
| 464 |
$existing = file_get_contents( $filename ); |
| 465 |
} |
| 466 |
|
| 467 |
$file = @fopen( $filename, 'w' ); |
| 468 |
if ( $file ) { |
| 469 |
$result = fwrite( $file, $this->get( $existing ) ); |
| 470 |
fclose( $file ); |
| 471 |
|
| 472 |
return $result !== false; |
| 473 |
} |
| 474 |
|
| 475 |
return false; |
| 476 |
} |
| 477 |
} |
| 478 |
|