<?php
/**
* General plugin functions
*
* @package safe-redirect-manager
*/
/**
* Get redirects from the database
*
* @since 1.8
* @param array $args Any arguments to filter by
* @param bool $hard Force cache refresh
* @return array $redirects An array of redirects
*/
function srm_get_redirects( $args = array(), $hard = false ) {
$default_max_redirects = srm_get_max_redirects();
$transient_key = '_srm_redirects_' . $default_max_redirects;
$redirects = get_transient( $transient_key );
if ( $hard || false === $redirects ) {
$redirects = array();
$posts_per_page = 100;
$i = 1;
while ( true ) {
if ( count( $redirects ) >= $default_max_redirects ) {
break;
}
$defaults = array(
'posts_per_page' => $posts_per_page,
'post_status' => 'publish',
'paged' => $i,
'fields' => 'ids',
'orderby' => 'menu_order ID',
'order' => 'ASC',
);
$query_args = array_merge( $defaults, $args );
// Some arguments that don't need to be configurable
$query_args['post_type'] = 'redirect_rule';
$query_args['update_term_cache'] = false;
$redirect_query = new WP_Query( $query_args );
if ( ! $redirect_query->have_posts() ) {
break;
}
// Check whether include post_status in result
$include_post_status_in_result = is_array( $query_args['post_status'] ) || 'any' === $query_args['post_status'];
foreach ( $redirect_query->posts as $redirect_id ) {
if ( count( $redirects ) >= $default_max_redirects ) {
break 2;
}
$redirect_data = array(
'ID' => $redirect_id,
'redirect_from' => get_post_meta( $redirect_id, '_redirect_rule_from', true ),
'redirect_to' => get_post_meta( $redirect_id, '_redirect_rule_to', true ),
'status_code' => (int) get_post_meta( $redirect_id, '_redirect_rule_status_code', true ),
'message' => get_post_meta( $redirect_id, '_redirect_rule_message', true ),
'enable_regex' => (bool) get_post_meta( $redirect_id, '_redirect_rule_from_regex', true ),
'force_https' => get_post_meta( $redirect_id, '_force_https', true ),
);
if ( $include_post_status_in_result ) {
$redirect_data['post_status'] = get_post_status( $redirect_id );
}
$redirects[] = $redirect_data;
}
$i++;
}
// Set transient to 30 days to remove old transients if the max redirects changes.
set_transient( $transient_key, $redirects, 30 * DAY_IN_SECONDS );
}
return $redirects;
}
/**
* Returns true if max redirects have been reached
*
* @since 1.8
* @return bool
*/
function srm_max_redirects_reached() {
$default_max_redirects = srm_get_max_redirects();
$redirects = srm_get_redirects();
return ( count( $redirects ) >= $default_max_redirects );
}
/**
* Get valid HTTP status codes
*
* @since 1.8
* @return array
*/
function srm_get_valid_status_codes() {
/**
* Valid status codes to redirect with.
*
* @hook srm_valid_status_codes
* @param {array} $status_codes Valid status codes to redirect with. Default array( 301, 302, 303, 307, 403, 404, 410 ) and other codes returned by `srm_additional_status_codes` filter hook.
* @returns {array} Filtered valid status codes.
*/
return apply_filters( 'srm_valid_status_codes', array_keys( srm_get_valid_status_codes_data() ) );
}
/**
* Get valid HTTP status codes and their labels.
*
* @since 2.0.0
* @return array
*/
function srm_get_valid_status_codes_data() {
$status_codes = array(
301 => esc_html__( 'Moved Permanently', 'safe-redirect-manager' ),
302 => esc_html__( 'Found', 'safe-redirect-manager' ),
303 => esc_html__( 'See Other', 'safe-redirect-manager' ),
307 => esc_html__( 'Temporary Redirect', 'safe-redirect-manager' ),
403 => esc_html__( 'Forbidden', 'safe-redirect-manager' ),
404 => esc_html__( 'Not Found', 'safe-redirect-manager' ),
410 => esc_html__( 'Gone', 'safe-redirect-manager' ),
);
/**
* Include additional status codes as valid to redirect with.
*
* @hook srm_additional_status_codes
* @param {array} $status_codes Status codes to add in valid array. Default is empty array.
* @returns {array} Status codes.
*/
$additional_status_codes = apply_filters(
'srm_additional_status_codes',
array()
);
if ( empty( $additional_status_codes ) ) {
return $status_codes;
}
$status_code_array = $status_codes + $additional_status_codes;
ksort( $status_code_array, SORT_NUMERIC );
return $status_code_array;
}
/**
* Flush redirect cache
*
* @since 1.8
*/
function srm_flush_cache() {
delete_transient( '_srm_redirects_' . srm_get_max_redirects() );
delete_transient( '_srm_redirects_graph' );
}
/**
* Check for potential redirect loops or chains
*
* @since 1.8
* @return boolean
*/
function srm_check_for_possible_redirect_loops() {
$redirects = srm_get_redirects();
if ( function_exists( 'wp_parse_url' ) ) {
$current_url = wp_parse_url( home_url() );
} else {
$current_url = parse_url( home_url() );
}
$this_host = ( is_array( $current_url ) && ! empty( $current_url['host'] ) ) ? $current_url['host'] : '';
foreach ( $redirects as $redirect ) {
$redirect_from = $redirect['redirect_from'];
// check redirect from against all redirect to's
foreach ( $redirects as $compare_redirect ) {
$redirect_to = $compare_redirect['redirect_to'];
if ( function_exists( 'wp_parse_url' ) ) {
$redirect_url = wp_parse_url( $redirect_to );
} else {
$redirect_url = parse_url( $redirect_to );
}
$redirect_host = ( is_array( $redirect_url ) && ! empty( $redirect_url['host'] ) ) ? $redirect_url['host'] : '';
// check if we are redirecting locally
if ( empty( $redirect_host ) || $redirect_host === $this_host ) {
$redirect_from_url = preg_replace( '/(http:\/\/|https:\/\/|www\.)/i', '', home_url() . $redirect_from );
$redirect_to_url = $redirect_to;
if ( ! preg_match( '/https?:\/\//i', $redirect_to_url ) ) {
$redirect_to_url = $this_host . $redirect_to_url;
} else {
$redirect_to_url = preg_replace( '/(http:\/\/|https:\/\/|www\.)/i', '', $redirect_to_url );
}
// possible loop/chain found
if ( $redirect_to_url === $redirect_from_url ) {
return true;
}
}
}
}
return false;
}
/**
* Creates a redirect post, this function will be useful for import/exports scripts
*
* @param string $redirect_from Redirect from location
* @param string $redirect_to Redirect to location
* @param int $status_code Redirect status code
* @param bool $enable_regex Whether to enable regex or not
* @param string $post_status Post status
* @param int $menu_order Menu order
* @param string $notes Notes
* @param int $author_id Author ID
* @since 1.8
* @uses wp_insert_post, update_post_meta
* @return int|WP_Error
*/
function srm_create_redirect( $redirect_from, $redirect_to, $status_code = 302, $enable_regex = false, $post_status = 'publish', $menu_order = 0, $notes = '', $author_id = 1 ) {
global $wpdb;
$sanitized_redirect_from = srm_sanitize_redirect_from( $redirect_from );
$sanitized_redirect_to = srm_sanitize_redirect_to( $redirect_to );
$sanitized_status_code = absint( $status_code );
$sanitized_enable_regex = (bool) $enable_regex;
$sanitized_post_status = sanitize_key( $post_status );
$sanitized_menu_order = absint( $menu_order );
$sanitized_notes = sanitize_text_field( $notes );
$sanitized_author_id = absint( $author_id );
// check and make sure no parameters are empty or invalid after sanitation
if ( empty( $sanitized_redirect_from ) || empty( $sanitized_redirect_to ) ) {
return new WP_Error( 'invalid-argument', esc_html__( 'Redirect from and/or redirect to arguments are invalid.', 'safe-redirect-manager' ) );
}
if ( ! in_array( $sanitized_status_code, srm_get_valid_status_codes(), true ) ) {
return new WP_Error( 'invalid-argument', esc_html__( 'Invalid status code.', 'safe-redirect-manager' ) );
}
// Check if the redirect already exists.
$existing_redirect = $wpdb->get_row(
$wpdb->prepare(
"SELECT
fromMeta.post_id AS post_id,
fromMeta.meta_value AS _redirect_rule_from,
toMeta.meta_value AS _redirect_rule_to,
statusCodeMeta.meta_value AS _redirect_rule_status_code,
fromRegexMeta.meta_value AS _redirect_rule_from_regex,
notesMeta.meta_value AS _redirect_rule_notes
FROM
$wpdb->postmeta AS fromMeta
LEFT JOIN
$wpdb->postmeta AS toMeta ON fromMeta.post_id = toMeta.post_id AND toMeta.meta_key = %s
LEFT JOIN
$wpdb->postmeta AS statusCodeMeta ON fromMeta.post_id = statusCodeMeta.post_id AND statusCodeMeta.meta_key = %s
LEFT JOIN
$wpdb->postmeta AS fromRegexMeta ON fromMeta.post_id = fromRegexMeta.post_id AND fromRegexMeta.meta_key = %s
LEFT JOIN
$wpdb->postmeta AS notesMeta ON fromMeta.post_id = notesMeta.post_id AND notesMeta.meta_key = %s
WHERE
fromMeta.meta_key = %s AND fromMeta.meta_value = %s",
'_redirect_rule_to',
'_redirect_rule_status_code',
'_redirect_rule_from_regex',
'_redirect_rule_notes',
'_redirect_rule_from',
$sanitized_redirect_from
)
);
// Create the post arguments.
$post_args = array(
'post_type' => 'redirect_rule',
'post_status' => $sanitized_post_status,
'post_author' => $sanitized_author_id,
'menu_order' => $sanitized_menu_order,
);
if ( $existing_redirect ) {
// Redirect exists, so update it.
$post_args['ID'] = $existing_redirect->post_id;
$post_id = wp_update_post( $post_args );
if ( 0 >= $post_id ) {
return new WP_Error( 'error-updating', esc_html__( 'An error occurred updating the redirect.', 'safe-redirect-manager' ) );
}
} else {
$post_id = wp_insert_post( $post_args );
if ( 0 >= $post_id ) {
return new WP_Error( 'error-creating', esc_html__( 'An error occurred creating the redirect.', 'safe-redirect-manager' ) );
}
}
// Update the posts meta info.
update_post_meta( $post_id, '_redirect_rule_from', wp_slash( $sanitized_redirect_from ) );
update_post_meta( $post_id, '_redirect_rule_to', $sanitized_redirect_to );
update_post_meta( $post_id, '_redirect_rule_status_code', $sanitized_status_code );
update_post_meta( $post_id, '_redirect_rule_from_regex', $sanitized_enable_regex );
update_post_meta( $post_id, '_redirect_rule_notes', $sanitized_notes );
// We need to update the cache after creating this redirect.
srm_flush_cache();
return $post_id;
}
/**
* Sanitize redirect to path
*
* The only difference between this function and just calling esc_url_raw is
* esc_url_raw( 'test' ) == 'http://test', whereas sanitize_redirect_path( 'test' ) == '/test'
*
* @since 1.8
* @param string $path Path to sanitize
* @return string
*/
function srm_sanitize_redirect_to( $path ) {
$path = trim( $path );
if ( preg_match( '/^www\./i', $path ) ) {
$path = 'http://' . $path;
}
if ( ! preg_match( '/^https?:\/\//i', $path ) ) {
if ( strpos( $path, '/' ) !== 0 ) {
$path = '/' . $path;
}
}
return esc_url_raw( $path );
}
/**
* Sanitize redirect from path
*
* @since 1.8
* @param string $path Path to sanitize
* @param boolean $allow_regex Whether to allow regex
* @return string
*/
function srm_sanitize_redirect_from( $path, $allow_regex = false ) {
$path = trim( $path );
if ( empty( $path ) ) {
return '';
}
// dont accept paths starting with a .
if ( ! $allow_regex && strpos( $path, '.' ) === 0 ) {
return '';
}
// turn path in to absolute
if ( preg_match( '/https?:\/\//i', $path ) ) {
$path = preg_replace( '/^(http:\/\/|https:\/\/)(www\.)?[^\/?]+\/?(.*)/i', '/$3', $path );
} elseif ( ! $allow_regex && strpos( $path, '/' ) !== 0 ) {
$path = '/' . $path;
}
// the @ symbol will break our regex engine
$path = str_replace( '@', '', $path );
return $path;
}
/**
* Imports redirects from CSV file or stream.
*
* @since 1.8
*
* @access public
* @param string|resource $file File path, file pointer or stream to read redirects from.
* @param array $args The array of arguments. Includes column mapping and CSV settings.
* @return array Returns importing statistic on success, otherwise FALSE.
*/
function srm_import_file( $file, $args ) {
$handle = $file;
$close_handle = false;
$doing_wp_cli = defined( 'WP_CLI' ) && WP_CLI;
/**
* Import file arguments
*
* @hook srm_import_file_args
* @param {array} $file_arguments File arguments.
* @returns {array} Filtered file arguments.
*/
$args = apply_filters( 'srm_import_file_args', $args );
// enable line endings auto detection
if ( version_compare( PHP_VERSION, '8.1', '<' ) ) {
@ini_set( 'auto_detect_line_endings', true ); // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.auto_detect_line_endingsDeprecated
}
// open file pointer if $file is not a resource
if ( ! is_resource( $file ) ) {
$handle = fopen( $file, 'rb' );
if ( ! $handle ) {
$doing_wp_cli && WP_CLI::error( sprintf( 'Error retrieving %s file', basename( $file ) ) );
return false;
}
$close_handle = true;
}
// process all rows of the file
$created = 0;
$skipped = 0;
$headers = fgetcsv( $handle );
while ( ( $row = fgetcsv( $handle ) ) ) {
// validate
$rule = is_array( $row ) ? array_combine( $headers, $row ) : array();
if ( empty( $rule ) || empty( $rule[ $args['source'] ] ) || empty( $rule[ $args['target'] ] ) ) {
$doing_wp_cli && WP_CLI::warning( 'Skipping - redirection rule is formatted improperly.' );
$skipped++;
continue;
}
// sanitize
$redirect_from = srm_sanitize_redirect_from( $rule[ $args['source'] ] );
$redirect_to = srm_sanitize_redirect_to( $rule[ $args['target'] ] );
$status_code = ! empty( $rule[ $args['code'] ] ) ? $rule[ $args['code'] ] : 302;
$regex = ! empty( $rule[ $args['regex'] ] ) ? filter_var( $rule[ $args['regex'] ], FILTER_VALIDATE_BOOLEAN ) : false;
$menu_order = ! empty( $rule[ $args['order'] ] ) ? $rule[ $args['order'] ] : 0;
$notes = ! empty( $rule[ $args['notes'] ] ) ? $rule[ $args['notes'] ] : '';
// import
$id = srm_create_redirect( $redirect_from, $redirect_to, $status_code, $regex, 'publish', $menu_order, $notes );
if ( is_wp_error( $id ) ) {
$doing_wp_cli && WP_CLI::warning( $id );
$skipped++;
} else {
$doing_wp_cli && WP_CLI::line( "Success - Created redirect from '{$redirect_from}' to '{$redirect_to}'" );
$created++;
}
}
// close an open file pointer if we've opened it
if ( $close_handle ) {
fclose( $handle );
}
// return result statistic
return array(
'created' => $created,
'skipped' => $skipped,
);
}
/**
* Tries to match a redirect given a path. Return the redirect array or false on failure.
*
* @param string $path The path to check redirects for.
*
* @return array|bool {
* Redirect array config.
*
* @type string $redirect_to The redirect to url.
* @type int $status_code The redirect status code.
* @type bool $enable_regex Whether this redirect has regex enabled or not.
* }
*/
function srm_match_redirect( $path ) {
return SRM_Redirect::factory()->match_redirect( $path );
}
/**
* Get maximum supported redirects.
*
* @since 2.0.0
* @return int
*/
function srm_get_max_redirects() {
/**
* Filter maximum supported redirects.
*
* @hook srm_max_redirects
* @param {int} $max_redirect Maximum supported redirects. Default is `1000`.
* @returns {int} Maximum supported redirects.
*/
return apply_filters( 'srm_max_redirects', 1000 );
}