*/ 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 ''; wp_nonce_field( 'qwsc_save_redirects' ); echo ''; for ( $i = 0; $i < $rows; $i++ ) { $from = isset( $rules[ $i ]['from'] ) ? $rules[ $i ]['from'] : ''; $to = isset( $rules[ $i ]['to'] ) ? $rules[ $i ]['to'] : ''; $code = isset( $rules[ $i ]['code'] ) ? (int) $rules[ $i ]['code'] : 301; $match = isset( $rules[ $i ]['match'] ) ? $rules[ $i ]['match'] : 'exact'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
FromToCodeMatch
'; submit_button( __( 'Save redirects', 'qw-optimize' ) ); echo '
'; echo '

' . esc_html__( 'Export CSV', 'qw-optimize' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'qwsc_redirects_import' ); echo ' '; submit_button( __( 'Import CSV', 'qw-optimize' ), 'secondary', 'submit', false ); 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; } }