*/
public static function rules() {
$r = get_option( self::OPTION, array() );
return is_array( $r ) ? $r : array();
}
/**
* @param string $path Path.
* @param array $rule Rule.
* @return string|null Destination or null.
*/
public static function match_rule( $path, array $rule ) {
$from = isset( $rule['from'] ) ? (string) $rule['from'] : '';
$to = isset( $rule['to'] ) ? (string) $rule['to'] : '';
$match = isset( $rule['match'] ) ? (string) $rule['match'] : 'exact';
if ( ! $from || ! $to ) {
return null;
}
$path = untrailingslashit( $path );
if ( '' === $path ) {
$path = '/';
}
if ( 'exact' === $match ) {
$from_n = untrailingslashit( $from );
if ( '' === $from_n ) {
$from_n = '/';
}
return ( $from_n === $path ) ? $to : null;
}
if ( 'wildcard' === $match ) {
$pattern = '#^' . str_replace( array( '\*', '\?' ), array( '.*', '.' ), preg_quote( $from, '#' ) ) . '$#i';
if ( preg_match( $pattern, $path ) ) {
return $to;
}
return null;
}
if ( 'regex' === $match ) {
$delim = $from[0] ?? '';
$pat = $from;
if ( '~' !== $delim && '/' !== $delim && '#' !== $delim ) {
$pat = '#' . str_replace( '#', '\#', $from ) . '#';
}
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- invalid user regex must not fatal.
$result = @preg_match( $pat, $path, $m );
if ( 1 === $result ) {
$dest = $to;
foreach ( $m as $i => $val ) {
if ( is_int( $i ) ) {
$dest = str_replace( '$' . $i, $val, $dest );
}
}
return $dest;
}
}
return null;
}
public function maybe_redirect() {
if ( is_admin() ) {
return;
}
$uri = isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
$path = (string) strtok( $uri, '?' );
if ( strlen( $path ) > 4096 || preg_match( '/[\x00-\x1F\x7F]/', $path ) ) {
return;
}
foreach ( self::rules() as $rule ) {
if ( ! is_array( $rule ) ) {
continue;
}
$dest = self::match_rule( $path, $rule );
if ( null === $dest ) {
continue;
}
$code = isset( $rule['code'] ) ? (int) $rule['code'] : 301;
if ( ! in_array( $code, array( 301, 302, 307 ), true ) ) {
$code = 301;
}
if ( preg_match( '#^https?://#i', $dest ) ) {
$dest = esc_url_raw( $dest, array( 'http', 'https' ) );
} elseif ( 0 !== strpos( $dest, '/' ) ) {
$dest = home_url( '/' . ltrim( $dest, '/' ) );
} elseif ( 0 === strpos( $dest, '/' ) ) {
$dest = home_url( $dest );
}
if ( ! $dest || ! wp_http_validate_url( $dest ) ) {
continue;
}
wp_safe_redirect( $dest, $code );
exit;
}
}
public function menu() {
add_submenu_page(
'qw-speed-cache',
__( 'Redirects', 'qw-optimize' ),
__( 'Redirects', 'qw-optimize' ),
'manage_options',
'qw-speed-cache-redirects',
array( $this, 'render' )
);
}
public function render() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$rules = self::rules();
$rows = max( self::MAX, count( $rules ) + 5 );
$rows = min( $rows, self::MAX );
echo '
' . esc_html__( 'Redirects', 'qw-optimize' ) . '
';
echo '
' . esc_html__( 'Match: exact path, wildcard (*), or regex (use $1 in target for captures).', 'qw-optimize' ) . '
';
echo '
';
echo '
' . esc_html__( 'Export CSV', 'qw-optimize' ) . '
';
echo '
';
}
public function save() {
check_admin_referer( 'qwsc_save_redirects' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Unauthorized', 'qw-optimize' ), '', array( 'response' => 403 ) );
}
$raw = isset( $_POST['rules'] ) && is_array( $_POST['rules'] ) ? wp_unslash( $_POST['rules'] ) : array();
$rules = array();
foreach ( $raw as $row ) {
if ( ! is_array( $row ) ) {
continue;
}
$from = isset( $row['from'] ) ? sanitize_text_field( $row['from'] ) : '';
$to = isset( $row['to'] ) ? esc_url_raw( $row['to'] ) : '';
if ( ! $to && isset( $row['to'] ) ) {
$to = sanitize_text_field( $row['to'] );
}
if ( ! $from || ! $to ) {
continue;
}
$code = isset( $row['code'] ) ? (int) $row['code'] : 301;
$match = isset( $row['match'] ) ? sanitize_key( $row['match'] ) : 'exact';
if ( ! in_array( $match, array( 'exact', 'wildcard', 'regex' ), true ) ) {
$match = 'exact';
}
$rules[] = array(
'from' => $from,
'to' => $to,
'code' => in_array( $code, array( 301, 302, 307 ), true ) ? $code : 301,
'match' => $match,
);
if ( count( $rules ) >= self::MAX ) {
break;
}
}
update_option( self::OPTION, $rules, false );
wp_safe_redirect( admin_url( 'admin.php?page=qw-speed-cache-redirects&saved=1' ) );
exit;
}
public function export_csv() {
check_admin_referer( 'qwsc_redirects_export' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Unauthorized', 'qw-optimize' ), '', array( 'response' => 403 ) );
}
nocache_headers();
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=qwsc-redirects.csv' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- php://output stream is required for fputcsv() download output.
$out = fopen( 'php://output', 'w' );
fputcsv( $out, array( 'from', 'to', 'code', 'match' ) );
foreach ( self::rules() as $r ) {
fputcsv( $out, array( $r['from'] ?? '', $r['to'] ?? '', $r['code'] ?? 301, $r['match'] ?? 'exact' ) );
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing php://output CSV stream.
fclose( $out );
exit;
}
public function import_csv() {
check_admin_referer( 'qwsc_redirects_import' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Unauthorized', 'qw-optimize' ), '', array( 'response' => 403 ) );
}
$file = isset( $_FILES['csv'] ) && is_array( $_FILES['csv'] ) ? $_FILES['csv'] : array();
// Upload tmp_name is a local filesystem path generated by PHP, not a URL.
// Validate its exact value and uploaded-file origin before opening it.
$tmp_name = isset( $file['tmp_name'] ) && is_string( $file['tmp_name'] ) ? $file['tmp_name'] : '';
$size = isset( $file['size'] ) && is_numeric( $file['size'] ) ? (int) $file['size'] : 0;
$error = isset( $file['error'] ) && is_numeric( $file['error'] ) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
if ( UPLOAD_ERR_OK !== $error || '' === $tmp_name || $size <= 0 || $size > 1048576 || ! is_uploaded_file( $tmp_name ) || ! is_readable( $tmp_name ) ) {
wp_safe_redirect( admin_url( 'admin.php?page=qw-speed-cache-redirects&import=fail' ) );
exit;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Validated local uploaded CSV stream, used by fgetcsv().
$fh = fopen( $tmp_name, 'r' );
if ( ! $fh ) {
wp_safe_redirect( admin_url( 'admin.php?page=qw-speed-cache-redirects&import=fail' ) );
exit;
}
$rules = array();
$header = fgetcsv( $fh );
while ( ( $row = fgetcsv( $fh ) ) !== false ) {
if ( count( $row ) < 2 ) {
continue;
}
$to = isset( $row[1] ) ? (string) $row[1] : '';
$to_s = esc_url_raw( $to );
if ( ! $to_s ) {
$to_s = sanitize_text_field( $to );
}
$from_s = sanitize_text_field( $row[0] );
$code = isset( $row[2] ) ? (int) $row[2] : 301;
$match = isset( $row[3] ) ? sanitize_key( $row[3] ) : 'exact';
if ( '' === $from_s || '' === $to_s || ! in_array( $code, array( 301, 302, 307 ), true ) || ! in_array( $match, array( 'exact', 'wildcard', 'regex' ), true ) ) {
continue;
}
$rules[] = array(
'from' => $from_s,
'to' => $to_s,
'code' => $code,
'match' => $match,
);
if ( count( $rules ) >= self::MAX ) {
break;
}
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing uploaded CSV stream.
fclose( $fh );
update_option( self::OPTION, $rules, false );
wp_safe_redirect( admin_url( 'admin.php?page=qw-speed-cache-redirects&import=1' ) );
exit;
}
}