Commit inicial - WordPress Análisis de Precios Unitarios

- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-03 21:04:30 -06:00
commit a22573bf0b
24068 changed files with 4993111 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
<?php
/**
* A redirect action - what happens after a URL is matched.
*/
abstract class Red_Action {
/**
* The action code (i.e. HTTP code)
*
* @var integer
*/
protected $code = 0;
/**
* The action type
*
* @var string
*/
protected $type = '';
/**
* Target URL, if any
*
* @var String|null
*/
protected $target = null;
/**
* Constructor
*
* @param array $values Values.
*/
public function __construct( $values = [] ) {
if ( is_array( $values ) ) {
foreach ( $values as $key => $value ) {
$this->$key = $value;
}
}
}
abstract public function name();
/**
* Create an action object
*
* @param string $name Action type.
* @param integer $code Action code.
* @return Red_Action|null
*/
public static function create( $name, $code ) {
$avail = self::available();
if ( isset( $avail[ $name ] ) ) {
if ( ! class_exists( strtolower( $avail[ $name ][1] ) ) ) {
include_once dirname( __FILE__ ) . '/../actions/' . $avail[ $name ][0];
}
/**
* @var Red_Action
*/
$obj = new $avail[ $name ][1]( [ 'code' => $code ] );
$obj->type = $name;
return $obj;
}
return null;
}
/**
* Get list of available actions
*
* @return array
*/
public static function available() {
return [
'url' => [ 'url.php', 'Url_Action' ],
'error' => [ 'error.php', 'Error_Action' ],
'nothing' => [ 'nothing.php', 'Nothing_Action' ],
'random' => [ 'random.php', 'Random_Action' ],
'pass' => [ 'pass.php', 'Pass_Action' ],
];
}
/**
* Get the action code
*
* @return integer
*/
public function get_code() {
return $this->code;
}
/**
* Get action type
*
* @return string
*/
public function get_type() {
return $this->type;
}
/**
* Set the target for this action
*
* @param string $target_url The original URL from the client.
* @return void
*/
public function set_target( $target_url ) {
$this->target = $target_url;
}
/**
* Get the target for this action
*
* @return string|null
*/
public function get_target() {
return $this->target;
}
/**
* Does this action need a target?
*
* @return boolean
*/
public function needs_target() {
return false;
}
/**
* Run this action. May not return from this function.
*
* @return void
*/
abstract public function run();
}

View File

@@ -0,0 +1,194 @@
<?php
/**
* Canonical redirects.
*/
class Redirection_Canonical {
/**
* Aliased domains. These are domains that should be redirected to the WP domain.
*
* @var string[]
*/
private $aliases = [];
/**
* Force HTTPS.
*
* @var boolean
*/
private $force_https = false;
/**
* Preferred domain. WWW or no WWW.
*
* @var string
*/
private $preferred_domain = '';
/**
* Current WP domain.
*
* @var string
*/
private $actual_domain = '';
/**
* Constructor
*
* @param boolean $force_https `true` to force https, `false` otherwise.
* @param string $preferred_domain `www`, `nowww`, or empty string.
* @param string[] $aliases Array of domain aliases.
* @param string $configured_domain Current domain.
*/
public function __construct( $force_https, $preferred_domain, $aliases, $configured_domain ) {
$this->force_https = $force_https;
$this->aliases = $aliases;
$this->preferred_domain = $preferred_domain;
$this->actual_domain = $configured_domain;
}
/**
* Get the canonical redirect.
*
* @param string $server Current server URL.
* @param string $request Current request.
* @return string|false
*/
public function get_redirect( $server, $request ) {
$aliases = array_merge(
$this->get_preferred_aliases( $server ),
$this->aliases
);
if ( $this->force_https && ! is_ssl() ) {
$aliases[] = $server;
}
$aliases = array_unique( $aliases );
if ( count( $aliases ) > 0 ) {
foreach ( $aliases as $alias ) {
if ( $server === $alias ) {
// Redirect this to the WP url
$target = $this->get_canonical_target( get_bloginfo( 'url' ) );
if ( ! $target ) {
return false;
}
$target = esc_url_raw( $target ) . $request;
return apply_filters( 'redirect_canonical_target', $target );
}
}
}
return false;
}
/**
* Get the preferred alias
*
* @param string $server Current server.
* @return string[]
*/
private function get_preferred_aliases( $server ) {
if ( $this->need_force_www( $server ) || $this->need_remove_www( $server ) ) {
return [ $server ];
}
return [];
}
/**
* A final check to prevent obvious site errors.
*
* @param string $server Current server.
* @return boolean
*/
private function is_configured_domain( $server ) {
return $server === $this->actual_domain;
}
/**
* Get the canonical target
*
* @param string $server Current server.
* @return string|false
*/
private function get_canonical_target( $server ) {
$canonical = rtrim( red_parse_domain_only( $server ), '/' );
if ( $this->need_force_www( $server ) ) {
$canonical = 'www.' . ltrim( $canonical, 'www.' );
} elseif ( $this->need_remove_www( $server ) ) {
$canonical = ltrim( $canonical, 'www.' );
}
$canonical = ( is_ssl() ? 'https://' : 'http://' ) . $canonical;
if ( $this->force_https ) {
$canonical = str_replace( 'http://', 'https://', $canonical );
}
if ( $this->is_configured_domain( $canonical ) ) {
return $canonical;
}
return false;
}
/**
* Do we need to force WWW?
*
* @param string $server Current server.
* @return boolean
*/
private function need_force_www( $server ) {
$has_www = substr( $server, 0, 4 ) === 'www.';
return $this->preferred_domain === 'www' && ! $has_www;
}
/**
* Do we need to remove WWW?
*
* @param string $server Current server.
* @return boolean
*/
private function need_remove_www( $server ) {
$has_www = substr( $server, 0, 4 ) === 'www.';
return $this->preferred_domain === 'nowww' && $has_www;
}
/**
* Return the full URL relocated to another domain. Certain URLs are protected from this.
*
* @param string $relocate Target domain.
* @param string $domain Current domain.
* @param string $request Current request.
* @return string|false
*/
public function relocate_request( $relocate, $domain, $request ) {
$relocate = rtrim( $relocate, '/' );
$protected = apply_filters( 'redirect_relocate_protected', [
'/wp-admin',
'/wp-login.php',
'/wp-json/',
] );
$not_protected = array_filter( $protected, function( $base ) use ( $request ) {
if ( substr( $request, 0, strlen( $base ) ) === $base ) {
return true;
}
return false;
} );
if ( $domain !== red_parse_domain_only( $relocate ) && count( $not_protected ) === 0 ) {
return apply_filters( 'redirect_relocate_target', $relocate . $request );
}
return false;
}
}

View File

@@ -0,0 +1,103 @@
<?php
abstract class Red_FileIO {
public static function create( $type ) {
$exporter = false;
if ( $type === 'rss' ) {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/rss.php';
$exporter = new Red_Rss_File();
} elseif ( $type === 'csv' ) {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/csv.php';
$exporter = new Red_Csv_File();
} elseif ( $type === 'apache' ) {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/apache.php';
$exporter = new Red_Apache_File();
} elseif ( $type === 'nginx' ) {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/nginx.php';
$exporter = new Red_Nginx_File();
} elseif ( $type === 'json' ) {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/json.php';
$exporter = new Red_Json_File();
}
return $exporter;
}
public static function import( $group_id, $file ) {
$parts = pathinfo( $file['name'] );
$extension = isset( $parts['extension'] ) ? $parts['extension'] : '';
$extension = strtolower( $extension );
if ( $extension === 'csv' || $extension === 'txt' ) {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/csv.php';
$importer = new Red_Csv_File();
$data = '';
} elseif ( $extension === 'json' ) {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/json.php';
$importer = new Red_Json_File();
$data = @file_get_contents( $file['tmp_name'] );
} else {
include_once dirname( dirname( __FILE__ ) ) . '/fileio/apache.php';
$importer = new Red_Apache_File();
$data = @file_get_contents( $file['tmp_name'] );
}
if ( $extension !== 'json' ) {
$group = Red_Group::get( $group_id );
if ( ! $group ) {
return false;
}
}
return $importer->load( $group_id, $file['tmp_name'], $data );
}
public function force_download() {
header( 'Cache-Control: no-cache, must-revalidate' );
header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' );
}
protected function export_filename( $extension ) {
$name = wp_parse_url( home_url(), PHP_URL_HOST );
$name = sanitize_text_field( $name );
$name = str_replace( '.', '-', $name );
$date = strtolower( date_i18n( get_option( 'date_format' ) ) );
$date = str_replace( [ ',', ' ', '--' ], '-', $date );
return 'redirection-' . $name . '-' . $date . '.' . sanitize_text_field( $extension );
}
public static function export( $module_name_or_id, $format ) {
$groups = false;
$items = false;
if ( $module_name_or_id === 'all' || $module_name_or_id === 0 ) {
$groups = Red_Group::get_all();
$items = Red_Item::get_all();
} else {
$module_name_or_id = is_numeric( $module_name_or_id ) ? $module_name_or_id : Red_Module::get_id_for_name( $module_name_or_id );
$module = Red_Module::get( intval( $module_name_or_id, 10 ) );
if ( $module ) {
$groups = Red_Group::get_all_for_module( $module->get_id() );
$items = Red_Item::get_all_for_module( $module->get_id() );
}
}
$exporter = self::create( $format );
if ( $exporter && $items !== false && $groups !== false ) {
return [
'data' => $exporter->get_data( $items, $groups ),
'total' => count( $items ),
'exporter' => $exporter,
];
}
return false;
}
abstract public function get_data( array $items, array $groups );
abstract public function load( $group, $filename, $data );
}

View File

@@ -0,0 +1,183 @@
<?php
require_once dirname( REDIRECTION_FILE ) . '/database/database.php';
class Red_Fixer {
const REGEX_LIMIT = 200;
public function get_json() {
return [
'status' => $this->get_status(),
'debug' => $this->get_debug(),
];
}
public function get_debug() {
$status = new Red_Database_Status();
$ip = [];
foreach ( Redirection_Request::get_ip_headers() as $var ) {
$ip[ $var ] = isset( $_SERVER[ $var ] ) ? sanitize_text_field( $_SERVER[ $var ] ) : false;
}
return [
'database' => [
'current' => $status->get_current_version(),
'latest' => REDIRECTION_DB_VERSION,
],
'ip_header' => $ip,
];
}
public function save_debug( $name, $value ) {
if ( $name === 'database' ) {
$database = new Red_Database();
$status = new Red_Database_Status();
foreach ( $database->get_upgrades() as $upgrade ) {
if ( $value === $upgrade['version'] ) {
$status->finish();
$status->save_db_version( $value );
// Switch to prompt mode
red_set_options( [ 'plugin_update' => 'prompt' ] );
break;
}
}
}
}
public function get_status() {
global $wpdb;
$options = red_get_options();
$groups = intval( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}redirection_groups" ), 10 );
$bad_group = $this->get_missing();
$monitor_group = $options['monitor_post'];
$valid_monitor = Red_Group::get( $monitor_group ) || $monitor_group === 0;
$status = [
array_merge( [
'id' => 'db',
'name' => __( 'Database tables', 'redirection' ),
], $this->get_database_status( Red_Database::get_latest_database() ) ),
[
'name' => __( 'Valid groups', 'redirection' ),
'id' => 'groups',
'message' => $groups === 0 ? __( 'No valid groups, so you will not be able to create any redirects', 'redirection' ) : __( 'Valid groups detected', 'redirection' ),
'status' => $groups === 0 ? 'problem' : 'good',
],
[
'name' => __( 'Valid redirect group', 'redirection' ),
'id' => 'redirect_groups',
'message' => count( $bad_group ) > 0 ? __( 'Redirects with invalid groups detected', 'redirection' ) : __( 'All redirects have a valid group', 'redirection' ),
'status' => count( $bad_group ) > 0 ? 'problem' : 'good',
],
[
'name' => __( 'Post monitor group', 'redirection' ),
'id' => 'monitor',
'message' => $valid_monitor === false ? __( 'Post monitor group is invalid', 'redirection' ) : __( 'Post monitor group is valid', 'redirection' ),
'status' => $valid_monitor === false ? 'problem' : 'good',
],
$this->get_http_settings(),
];
$regex_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}redirection_items WHERE regex=1" );
if ( $regex_count > self::REGEX_LIMIT ) {
$status[] = [
'name' => __( 'Regular Expressions', 'redirection' ),
'id' => 'regex',
'message' => __( 'Too many regular expressions may impact site performance', 'redirection' ),
'status' => 'problem',
];
}
return $status;
}
private function get_database_status( $database ) {
$missing = $database->get_missing_tables();
return array(
'status' => count( $missing ) === 0 ? 'good' : 'error',
'message' => count( $missing ) === 0 ? __( 'All tables present', 'redirection' ) : __( 'The following tables are missing:', 'redirection' ) . ' ' . join( ',', $missing ),
);
}
private function get_http_settings() {
$site = wp_parse_url( get_site_url(), PHP_URL_SCHEME );
$home = wp_parse_url( get_home_url(), PHP_URL_SCHEME );
$message = __( 'Site and home are consistent', 'redirection' );
if ( $site !== $home ) {
/* translators: 1: Site URL, 2: Home URL */
$message = sprintf( __( 'Site and home URL are inconsistent. Please correct from your Settings > General page: %1$1s is not %2$2s', 'redirection' ), get_site_url(), get_home_url() );
}
return array(
'name' => __( 'Site and home protocol', 'redirection' ),
'id' => 'redirect_url',
'message' => $message,
'status' => $site === $home ? 'good' : 'problem',
);
}
public function fix( $status ) {
foreach ( $status as $item ) {
if ( $item['status'] !== 'good' ) {
$fixer = 'fix_' . $item['id'];
$result = true;
if ( method_exists( $this, $fixer ) ) {
$result = $this->$fixer();
}
if ( is_wp_error( $result ) ) {
return $result;
}
}
}
return $this->get_status();
}
private function get_missing() {
global $wpdb;
return $wpdb->get_results( "SELECT {$wpdb->prefix}redirection_items.id FROM {$wpdb->prefix}redirection_items LEFT JOIN {$wpdb->prefix}redirection_groups ON {$wpdb->prefix}redirection_items.group_id = {$wpdb->prefix}redirection_groups.id WHERE {$wpdb->prefix}redirection_groups.id IS NULL" );
}
private function fix_db() {
$database = Red_Database::get_latest_database();
return $database->install();
}
private function fix_groups() {
if ( Red_Group::create( 'new group', 1 ) === false ) {
return new WP_Error( 'Unable to create group' );
}
return true;
}
private function fix_redirect_groups() {
global $wpdb;
$missing = $this->get_missing();
foreach ( $missing as $row ) {
$wpdb->update( $wpdb->prefix . 'redirection_items', array( 'group_id' => $this->get_valid_group() ), array( 'id' => $row->id ) );
}
}
private function fix_monitor() {
red_set_options( array( 'monitor_post' => $this->get_valid_group() ) );
}
private function get_valid_group() {
$groups = Red_Group::get_all();
return $groups[0]['id'];
}
}

View File

@@ -0,0 +1,73 @@
<?php
class Red_Flusher {
const DELETE_HOOK = 'redirection_log_delete';
const DELETE_FREQ = 'daily';
const DELETE_MAX = 20000;
const DELETE_KEEP_ON = 10; // 10 minutes
public function flush() {
$options = red_get_options();
$total = $this->expire_logs( 'redirection_logs', $options['expire_redirect'] );
$total += $this->expire_logs( 'redirection_404', $options['expire_404'] );
if ( $total >= self::DELETE_MAX ) {
$next = time() + ( self::DELETE_KEEP_ON * 60 );
// There are still more logs to clear - keep on doing until we're clean or until the next normal event
if ( $next < wp_next_scheduled( self::DELETE_HOOK ) ) {
wp_schedule_single_event( $next, self::DELETE_HOOK );
}
}
$this->optimize_logs();
}
private function optimize_logs() {
global $wpdb;
$rand = wp_rand( 1, 5000 );
if ( $rand === 11 ) {
$wpdb->query( "OPTIMIZE TABLE {$wpdb->prefix}redirection_logs" );
} elseif ( $rand === 12 ) {
$wpdb->query( "OPTIMIZE TABLE {$wpdb->prefix}redirection_404" );
}
}
private function expire_logs( $table, $expiry_time ) {
global $wpdb;
if ( $expiry_time > 0 ) {
// Known values
// phpcs:ignore
$logs = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}{$table} WHERE created < DATE_SUB(NOW(), INTERVAL %d DAY)", $expiry_time ) );
if ( $logs > 0 ) {
// Known values
// phpcs:ignore
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}{$table} WHERE created < DATE_SUB(NOW(), INTERVAL %d DAY) LIMIT %d", $expiry_time, self::DELETE_MAX ) );
return min( self::DELETE_MAX, $logs );
}
}
return 0;
}
public static function schedule() {
$options = red_get_options();
if ( $options['expire_redirect'] > 0 || $options['expire_404'] > 0 ) {
if ( ! wp_next_scheduled( self::DELETE_HOOK ) ) {
wp_schedule_event( time(), self::DELETE_FREQ, self::DELETE_HOOK );
}
} else {
self::clear();
}
}
public static function clear() {
wp_clear_scheduled_hook( self::DELETE_HOOK );
}
}

View File

@@ -0,0 +1,397 @@
<?php
/**
* A group of redirects
*/
class Red_Group {
/**
* Group ID
*
* @var integer
*/
private $id = 0;
/**
* Group name
*
* @var String
*/
private $name = '';
/**
* Module ID
*
* @var integer
*/
private $module_id = 0;
/**
* Group status - 'enabled' or 'disabled'
*
* @var String
*/
private $status = 'enabled';
/**
* Group position. Currently not used
*
* @var integer
*/
private $position = 0;
/**
* Constructor
*
* @param string|Object $values Values.
*/
public function __construct( $values = '' ) {
if ( is_object( $values ) ) {
$this->name = sanitize_text_field( $values->name );
$this->id = intval( $values->id, 10 );
if ( isset( $values->module_id ) ) {
$this->module_id = intval( $values->module_id, 10 );
}
if ( isset( $values->status ) ) {
$this->status = $values->status;
}
if ( isset( $values->position ) ) {
$this->position = intval( $values->position, 10 );
}
}
}
/**
* Get group name
*
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* Get group ID
*
* @return integer
*/
public function get_id() {
return $this->id;
}
/**
* Is the group enabled or disabled?
*
* @return boolean
*/
public function is_enabled() {
return $this->status === 'enabled' ? true : false;
}
/**
* Get a group given an ID
*
* @param integer $id Group ID.
* @param bool $clear Clear cache.
* @return Red_Group|boolean
*/
public static function get( $id, $clear = false ) {
static $groups = [];
global $wpdb;
if ( isset( $groups[ $id ] ) && ! $clear ) {
$row = $groups[ $id ];
} else {
$row = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->prefix}redirection_groups.*,COUNT( {$wpdb->prefix}redirection_items.id ) AS items,SUM( {$wpdb->prefix}redirection_items.last_count ) AS redirects FROM {$wpdb->prefix}redirection_groups LEFT JOIN {$wpdb->prefix}redirection_items ON {$wpdb->prefix}redirection_items.group_id={$wpdb->prefix}redirection_groups.id WHERE {$wpdb->prefix}redirection_groups.id=%d GROUP BY {$wpdb->prefix}redirection_groups.id", $id ) );
}
if ( $row ) {
$groups[ $id ] = $row;
return new Red_Group( $row );
}
return false;
}
/**
* Get all groups
*
* @return Red_Group[]
*/
public static function get_all( $params = [] ) {
global $wpdb;
$where = '';
if ( isset( $params['filterBy'] ) && is_array( $params['filterBy'] ) ) {
$filters = new Red_Group_Filters( $params['filterBy'] );
$where = $filters->get_as_sql();
}
$data = [];
$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}redirection_groups $where" );
if ( $rows ) {
foreach ( $rows as $row ) {
$group = new Red_Group( $row );
$data[] = $group->to_json();
}
}
return $data;
}
public static function get_all_for_module( $module_id ) {
global $wpdb;
$data = array();
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}redirection_groups WHERE module_id=%d", $module_id ) );
if ( $rows ) {
foreach ( $rows as $row ) {
$group = new Red_Group( $row );
$data[] = $group->to_json();
}
}
return $data;
}
public static function get_for_select() {
global $wpdb;
$data = array();
$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}redirection_groups" );
if ( $rows ) {
foreach ( $rows as $row ) {
$module = Red_Module::get( $row->module_id );
if ( $module ) {
$data[ $module->get_name() ][ intval( $row->id, 10 ) ] = $row->name;
}
}
}
return $data;
}
public static function create( $name, $module_id, $enabled = true ) {
global $wpdb;
$name = trim( wp_kses( sanitize_text_field( $name ), 'strip' ) );
$name = substr( $name, 0, 50 );
$module_id = intval( $module_id, 10 );
if ( $name !== '' && Red_Module::is_valid_id( $module_id ) ) {
$position = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->prefix}redirection_groups WHERE module_id=%d", $module_id ) );
$data = array(
'name' => trim( $name ),
'module_id' => intval( $module_id ),
'position' => intval( $position ),
'status' => $enabled ? 'enabled' : 'disabled',
);
$wpdb->insert( $wpdb->prefix . 'redirection_groups', $data );
return Red_Group::get( $wpdb->insert_id );
}
return false;
}
public function update( $data ) {
global $wpdb;
$old_id = $this->module_id;
$this->name = trim( wp_kses( sanitize_text_field( $data['name'] ), 'strip' ) );
$this->name = substr( $this->name, 0, 50 );
if ( Red_Module::is_valid_id( intval( $data['moduleId'], 10 ) ) ) {
$this->module_id = intval( $data['moduleId'], 10 );
}
$wpdb->update( $wpdb->prefix . 'redirection_groups', array( 'name' => $this->name, 'module_id' => $this->module_id ), array( 'id' => intval( $this->id ) ) );
if ( $old_id !== $this->module_id ) {
Red_Module::flush_by_module( $old_id );
Red_Module::flush_by_module( $this->module_id );
}
return true;
}
public function delete() {
global $wpdb;
// Delete all items in this group
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}redirection_items WHERE group_id=%d", $this->id ) );
Red_Module::flush( $this->id );
// Delete the group
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}redirection_groups WHERE id=%d", $this->id ) );
if ( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}redirection_groups" ) === 0 ) {
$wpdb->insert( $wpdb->prefix . 'redirection_groups', array( 'name' => __( 'Redirections', 'redirection' ), 'module_id' => 1, 'position' => 0 ) );
}
}
public function get_total_redirects() {
global $wpdb;
return intval( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}redirection_items WHERE group_id=%d", $this->id ) ), 10 );
}
public function enable() {
global $wpdb;
$wpdb->update( $wpdb->prefix . 'redirection_groups', array( 'status' => 'enabled' ), array( 'id' => $this->id ) );
$wpdb->update( $wpdb->prefix . 'redirection_items', array( 'status' => 'enabled' ), array( 'group_id' => $this->id ) );
Red_Module::flush( $this->id );
}
public function disable() {
global $wpdb;
$wpdb->update( $wpdb->prefix . 'redirection_groups', array( 'status' => 'disabled' ), array( 'id' => $this->id ) );
$wpdb->update( $wpdb->prefix . 'redirection_items', array( 'status' => 'disabled' ), array( 'group_id' => $this->id ) );
Red_Module::flush( $this->id );
}
public function get_module_id() {
return $this->module_id;
}
public static function get_filtered( array $params ) {
global $wpdb;
$orderby = 'name';
$direction = 'DESC';
$limit = RED_DEFAULT_PER_PAGE;
$offset = 0;
$where = '';
if ( isset( $params['orderby'] ) && in_array( $params['orderby'], array( 'name', 'id' ), true ) ) {
$orderby = $params['orderby'];
}
if ( isset( $params['direction'] ) && in_array( $params['direction'], array( 'asc', 'desc' ), true ) ) {
$direction = strtoupper( $params['direction'] );
}
if ( isset( $params['filterBy'] ) && is_array( $params['filterBy'] ) ) {
$filters = new Red_Group_Filters( $params['filterBy'] );
$where = $filters->get_as_sql();
}
if ( isset( $params['per_page'] ) ) {
$limit = intval( $params['per_page'], 10 );
$limit = min( RED_MAX_PER_PAGE, $limit );
$limit = max( 5, $limit );
}
if ( isset( $params['page'] ) ) {
$offset = intval( $params['page'], 10 );
$offset = max( 0, $offset );
$offset *= $limit;
}
$rows = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}redirection_groups $where " . $wpdb->prepare( "ORDER BY $orderby $direction LIMIT %d,%d", $offset, $limit )
);
$total_items = intval( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}redirection_groups " . $where ) );
$items = array();
$options = red_get_options();
foreach ( $rows as $row ) {
$group = new Red_Group( $row );
$group_json = $group->to_json();
if ( $group->get_id() === $options['last_group_id'] ) {
$group_json['default'] = true;
}
$items[] = $group_json;
}
return array(
'items' => $items,
'total' => intval( $total_items, 10 ),
);
}
public function to_json() {
$module = Red_Module::get( $this->get_module_id() );
return array(
'id' => $this->get_id(),
'name' => $this->get_name(),
'redirects' => $this->get_total_redirects(),
'module_id' => $this->get_module_id(),
'moduleName' => $module ? $module->get_name() : '',
'enabled' => $this->is_enabled(),
);
}
public static function delete_all( array $params ) {
global $wpdb;
$filters = new Red_Group_Filters( isset( $params['filterBy'] ) ? $params['filterBy'] : [] );
$query = $filters->get_as_sql();
$sql = "DELETE FROM {$wpdb->prefix}redirection_groups {$query}";
// phpcs:ignore
$wpdb->query( $sql );
}
public static function set_status_all( $action, array $params ) {
global $wpdb;
$filters = new Red_Group_Filters( isset( $params['filterBy'] ) ? $params['filterBy'] : [] );
$query = $filters->get_as_sql();
$sql = $wpdb->prepare( "UPDATE {$wpdb->prefix}redirection_groups SET status=%s {$query}", $action === 'enable' ? 'enable' : 'disable' );
// phpcs:ignore
$wpdb->query( $sql );
}
}
class Red_Group_Filters {
private $filters = [];
public function __construct( $filter_params ) {
global $wpdb;
foreach ( $filter_params as $filter_by => $filter ) {
$filter_by = sanitize_text_field( $filter_by );
$filter = sanitize_text_field( $filter );
if ( $filter_by === 'status' ) {
if ( $filter === 'enabled' ) {
$this->filters[] = "status='enabled'";
} else {
$this->filters[] = "status='disabled'";
}
} elseif ( $filter_by === 'module' ) {
$this->filters[] = $wpdb->prepare( 'module_id=%d', intval( $filter, 10 ) );
} elseif ( $filter_by === 'name' ) {
$this->filters[] = $wpdb->prepare( 'name LIKE %s', '%' . $wpdb->esc_like( trim( $filter ) ) . '%' );
}
}
}
public function get_as_sql() {
if ( count( $this->filters ) > 0 ) {
return ' WHERE ' . implode( ' AND ', $this->filters );
}
return '';
}
}

View File

@@ -0,0 +1,149 @@
<?php
class Red_Http_Headers {
private $headers = [];
public function __construct( $options = [] ) {
if ( is_array( $options ) ) {
$this->headers = array_filter( array_map( [ $this, 'normalize' ], $options ) );
}
}
private function normalize( $header ) {
$location = 'site';
if ( isset( $header['location'] ) && $header['location'] === 'redirect' ) {
$location = 'redirect';
}
$name = $this->sanitize( isset( $header['headerName'] ) ? sanitize_text_field( $header['headerName'] ) : '' );
$type = $this->sanitize( isset( $header['type'] ) ? sanitize_text_field( $header['type'] ) : '' );
$value = $this->sanitize( isset( $header['headerValue'] ) ? sanitize_text_field( $header['headerValue'] ) : '' );
$settings = [];
if ( isset( $header['headerSettings'] ) && is_array( $header['headerSettings'] ) ) {
foreach ( $header['headerSettings'] as $key => $setting_value ) {
if ( is_array( $setting_value ) ) {
if ( isset( $setting_value['value'] ) ) {
$settings[ $this->sanitize( sanitize_text_field( $key ) ) ] = $this->sanitize( $setting_value['value'] );
} elseif ( isset( $setting_value['choices'] ) ) {
$settings[ $this->sanitize( sanitize_text_field( $key ) ) ] = array_map(
function ( $choice ) {
return [
'label' => $this->sanitize( isset( $choice['label'] ) ? $choice['label'] : '' ),
'value' => $this->sanitize( isset( $choice['value'] ) ? $choice['value'] : '' ),
];
},
$setting_value['choices']
);
}
} else {
$settings[ $this->sanitize( sanitize_text_field( $key ) ) ] = $this->sanitize( $setting_value );
}
}
}
if ( strlen( $name ) > 0 && strlen( $type ) > 0 ) {
return [
'type' => $this->dash_case( $type ),
'headerName' => $this->dash_case( $name ),
'headerValue' => $value,
'location' => $location,
'headerSettings' => $settings,
];
}
return null;
}
public function get_json() {
return $this->headers;
}
private function dash_case( $name ) {
$name = preg_replace( '/[^A-Za-z0-9]/', ' ', $name );
$name = preg_replace( '/\s{2,}/', ' ', $name );
$name = trim( $name, ' ' );
$name = ucwords( $name );
$name = str_replace( ' ', '-', $name );
return $name;
}
private function remove_dupes( $headers ) {
$new_headers = [];
foreach ( $headers as $header ) {
$new_headers[ $header['headerName'] ] = $header;
}
return array_values( $new_headers );
}
public function get_site_headers() {
$headers = array_values( $this->remove_dupes( array_filter( $this->headers, [ $this, 'is_site_header' ] ) ) );
return apply_filters( 'redirection_headers_site', $headers );
}
public function get_redirect_headers() {
// Site ones first, then redirect - redirect will override any site ones
$headers = $this->get_site_headers();
$headers = array_merge( $headers, array_values( array_filter( $this->headers, [ $this, 'is_redirect_header' ] ) ) );
$headers = array_values( $this->remove_dupes( $headers ) );
return apply_filters( 'redirection_headers_redirect', $headers );
}
private function is_site_header( $header ) {
return $header['location'] === 'site';
}
private function is_redirect_header( $header ) {
return $header['location'] === 'redirect';
}
public function run( $headers ) {
$done = [];
foreach ( $headers as $header ) {
if ( ! in_array( $header['headerName'], $done, true ) ) {
$name = $this->sanitize( $this->dash_case( $header['headerName'] ) );
$value = $this->sanitize( $header['headerValue'] );
// Trigger some other action
do_action( 'redirection_header', $name, $value );
header( sprintf( '%s: %s', $name, $value ) );
$done[] = $header['headerName'];
}
}
}
/**
* Sanitize that string
*
* @param string $text
* @return string
*/
private function sanitize( $text ) {
if ( is_array( $text ) ) {
return '';
}
// No new lines
$text = (string) preg_replace( "/[\r\n\t].*?$/s", '', $text );
// Clean control codes
$text = (string) preg_replace( '/[^\PC\s]/u', '', $text );
// Try and remove bad decoding
if ( function_exists( 'iconv' ) && is_string( $text ) ) {
$converted = @iconv( 'UTF-8', 'UTF-8//IGNORE', $text );
if ( $converted !== false ) {
$text = $converted;
}
}
return $text;
}
}

View File

@@ -0,0 +1,477 @@
<?php
/**
* Convert redirects to .htaccess format
*
* Ignores:
* - Trailing slash flag
* - Query flags
*/
class Red_Htaccess {
/**
* Array of redirect lines
*
* @var array<string>
*/
private $items = array();
const INSERT_REGEX = '@\n?# Created by Redirection(?:.*?)# End of Redirection\n?@sm';
/**
* Encode the 'from' URL
*
* @param string $url From URL.
* @param bool $ignore_trailing Ignore trailing slashes.
* @return string
*/
private function encode_from( $url, $ignore_trailing ) {
$url = $this->encode( $url );
// Apache 2 does not need a leading slashing
$url = ltrim( $url, '/' );
if ( $ignore_trailing ) {
$url = rtrim( $url, '/' ) . '/?';
}
// Exactly match the URL
return '^' . $url . '$';
}
/**
* URL encode some things, but other things can be passed through
*
* @param string $url URL.
* @return string
*/
private function encode2nd( $url ) {
$allowed = [
'%2F' => '/',
'%3F' => '?',
'%3A' => ':',
'%3D' => '=',
'%26' => '&',
'%25' => '%',
'+' => '%20',
'%24' => '$',
'%23' => '#',
];
$url = rawurlencode( $url );
return $this->replace_encoding( $url, $allowed );
}
/**
* Replace encoded characters in a URL
*
* @param string $str Source string.
* @param array $allowed Allowed encodings.
* @return string
*/
private function replace_encoding( $str, $allowed ) {
foreach ( $allowed as $before => $after ) {
$str = str_replace( $before, $after, $str );
}
return $str;
}
/**
* Encode a URL
*
* @param string $url URL.
* @return string
*/
private function encode( $url ) {
$allowed = [
'%2F' => '/',
'%3F' => '?',
'+' => '%20',
'.' => '\\.',
];
return $this->replace_encoding( rawurlencode( $url ), $allowed );
}
/**
* Encode a regex URL
*
* @param string $url URL.
* @return string
*/
private function encode_regex( $url ) {
// Remove any newlines
$url = preg_replace( "/[\r\n\t].*?$/s", '', $url );
// Remove invalid characters
$url = preg_replace( '/[^\PC\s]/u', '', $url );
// Make sure spaces are quoted
$url = str_replace( ' ', '%20', $url );
$url = str_replace( '%24', '$', $url );
// No leading slash
$url = ltrim( $url, '/' );
// If pattern has a ^ at the start then ensure we don't have a slash immediatley after
$url = preg_replace( '@^\^/@', '^', $url );
return $url;
}
/**
* Add a referrer redirect
*
* @param Red_Item $item Redirect item.
* @param Referrer_Match $match Redirect match.
* @return void
*/
private function add_referrer( $item, $match ) {
$from = $this->encode_from( ltrim( $item->get_url(), '/' ), $item->source_flags && $item->source_flags->is_ignore_trailing() );
if ( $item->is_regex() ) {
$from = $this->encode_regex( ltrim( $item->get_url(), '/' ) );
}
if ( ( $match->url_from || $match->url_notfrom ) && $match->referrer ) {
$referrer = $match->regex ? $this->encode_regex( $match->referrer ) : $this->encode_from( $match->referrer, false );
$to = false;
if ( $match->url_from ) {
$to = $this->target( $item->get_action_type(), $match->url_from, $item->get_action_code(), $item->get_match_data() );
}
if ( $match->url_notfrom ) {
$to = $this->target( $item->get_action_type(), $match->url_notfrom, $item->get_action_code(), $item->get_match_data() );
}
$this->items[] = sprintf( 'RewriteCond %%{HTTP_REFERER} %s [NC]', $referrer );
if ( $to ) {
$this->items[] = sprintf( 'RewriteRule %s %s', $from, $to );
}
}
}
/**
* Add a useragent redirect
*
* @param Red_Item $item Redirect item.
* @param Agent_Match $match Redirect match.
* @return void
*/
private function add_agent( $item, $match ) {
$from = $this->encode( ltrim( $item->get_url(), '/' ) );
if ( $item->is_regex() ) {
$from = $this->encode_regex( ltrim( $item->get_url(), '/' ) );
}
if ( ( $match->url_from || $match->url_notfrom ) && $match->agent ) {
$agent = ( $match->regex ? $this->encode_regex( $match->agent ) : $this->encode2nd( $match->agent ) );
$to = false;
if ( $match->url_from ) {
$to = $this->target( $item->get_action_type(), $match->url_from, $item->get_action_code(), $item->get_match_data() );
}
if ( $match->url_notfrom ) {
$to = $this->target( $item->get_action_type(), $match->url_notfrom, $item->get_action_code(), $item->get_match_data() );
}
$this->items[] = sprintf( 'RewriteCond %%{HTTP_USER_AGENT} %s [NC]', $agent );
if ( $to ) {
$this->items[] = sprintf( 'RewriteRule %s %s', $from, $to );
}
}
}
/**
* Add a server redirect
*
* @param Red_Item $item Redirect item.
* @param Server_Match $match Redirect match.
* @return void
*/
private function add_server( $item, $match ) {
$match->url = $match->url_from;
$this->items[] = sprintf( 'RewriteCond %%{HTTP_HOST} ^%s$ [NC]', preg_quote( wp_parse_url( $match->server, PHP_URL_HOST ), '/' ) );
$this->add_url( $item, $match );
}
/**
* Add a redirect
*
* @param Red_Item $item Redirect item.
* @param Red_Match $match Redirect match.
* @return void
*/
private function add_url( $item, $match ) {
$url = $item->get_url();
if ( $item->is_regex() === false && strpos( $url, '?' ) !== false ) {
$url_parts = wp_parse_url( $url );
if ( isset( $url_parts['path'] ) ) {
$url = $url_parts['path'];
$query = isset( $url_parts['query'] ) ? $url_parts['query'] : '';
$this->items[] = sprintf( 'RewriteCond %%{QUERY_STRING} ^%s$', $query );
}
}
$to = $this->target( $item->get_action_type(), $match->url, $item->get_action_code(), $item->get_match_data() );
$from = $this->encode_from( $url, $item->source_flags && $item->source_flags->is_ignore_trailing() );
if ( $item->is_regex() ) {
$from = $this->encode_regex( $item->get_url() );
}
if ( $to ) {
$this->items[] = sprintf( 'RewriteRule %s %s', trim( $from ), trim( $to ) );
}
}
/**
* Add a redirect flags
*
* @return string
*/
private function add_flags( $current, array $flags ) {
return $current . ' [' . implode( ',', $flags ) . ']';
}
/**
* Get source flags
*
* @param array<string> $existing Existing flags.
* @param array<string> $source Source flags.
* @param string $url URL.
* @return array<string>
*/
private function get_source_flags( array $existing, array $source, $url ) {
$flags = [];
if ( isset( $source['flag_case'] ) && $source['flag_case'] ) {
$flags[] = 'NC';
}
if ( isset( $source['flag_query'] ) && $source['flag_query'] === 'pass' ) {
$flags[] = 'QSA';
}
if ( strpos( $url, '#' ) !== false || strpos( $url, '%' ) !== false ) {
$flags[] = 'NE';
}
return array_merge( $existing, $flags );
}
/**
* Add a random target.
*
* @param [type] $data
* @param [type] $code
* @param [type] $match_data
* @return string
*/
private function action_random( $data, $code, $match_data ) {
// Pick a WP post at random
global $wpdb;
$post = $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} ORDER BY RAND() LIMIT 0,1" );
$url = wp_parse_url( get_permalink( $post ) );
$flags = [ sprintf( 'R=%d', $code ) ];
$flags[] = 'L';
$flags = $this->get_source_flags( $flags, $match_data['source'], $data );
return $this->add_flags( $this->encode( $url['path'] ), $flags );
}
/**
* Add a passthrough target.
*
* @param [type] $data
* @param [type] $code
* @param [type] $match_data
* @return string
*/
private function action_pass( $data, $code, $match_data ) {
$flags = $this->get_source_flags( [ 'L' ], $match_data['source'], $data );
return $this->add_flags( $this->encode2nd( $data ), $flags );
}
/**
* Add an error target.
*
* @param [type] $data
* @param [type] $code
* @param [type] $match_data
* @return string
*/
private function action_error( $data, $code, $match_data ) {
$flags = $this->get_source_flags( [ 'F' ], $match_data['source'], $data );
if ( $code === 410 ) {
$flags = $this->get_source_flags( [ 'G' ], $match_data['source'], $data );
}
return $this->add_flags( '/', $flags );
}
/**
* Add a URL target.
*
* @param [type] $data
* @param [type] $code
* @param [type] $match_data
* @return string
*/
private function action_url( $data, $code, $match_data ) {
$flags = [ sprintf( 'R=%d', $code ) ];
$flags[] = 'L';
$flags = $this->get_source_flags( $flags, $match_data['source'], $data );
return $this->add_flags( $this->encode2nd( $data ), $flags );
}
/**
* Return URL target
*
* @param [type] $data
* @param [type] $code
* @param [type] $match_data
* @return string
*/
private function target( $action, $data, $code, $match_data ) {
$target = 'action_' . $action;
if ( method_exists( $this, $target ) ) {
return $this->$target( $data, $code, $match_data );
}
return '';
}
/**
* Generate the .htaccess file in memory
*
* @return string
*/
private function generate() {
$version = red_get_plugin_data( dirname( dirname( __FILE__ ) ) . '/redirection.php' );
if ( count( $this->items ) === 0 ) {
return '';
}
$text = [
'# Created by Redirection',
'# ' . date( 'r' ),
'# Redirection ' . trim( $version['Version'] ) . ' - https://redirection.me',
'',
'<IfModule mod_rewrite.c>',
];
// Add http => https option
$options = red_get_options();
if ( $options['https'] ) {
$text[] = 'RewriteCond %{HTTPS} off';
$text[] = 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]';
}
// Add redirects
$text = array_merge( $text, array_filter( array_map( [ $this, 'sanitize_redirect' ], $this->items ) ) );
// End of mod_rewrite
$text[] = '</IfModule>';
$text[] = '';
// End of redirection section
$text[] = '# End of Redirection';
$text = implode( "\n", $text );
return "\n" . $text . "\n";
}
/**
* Add a redirect to the file
*
* @param Red_Item $item Redirect.
* @return void
*/
public function add( $item ) {
$target = 'add_' . $item->get_match_type();
if ( method_exists( $this, $target ) && $item->is_enabled() ) {
$this->$target( $item, $item->match );
}
}
/**
* Get the .htaccess file
*
* @param boolean $existing Existing .htaccess data.
* @return string
*/
public function get( $existing = false ) {
$text = $this->generate();
if ( $existing ) {
if ( preg_match( self::INSERT_REGEX, $existing ) > 0 ) {
$text = preg_replace( self::INSERT_REGEX, str_replace( '$', '\\$', $text ), $existing );
} else {
$text = $text . "\n" . trim( $existing );
}
}
return trim( $text );
}
/**
* Sanitize the redirect
*
* @param string $text Text.
* @return string
*/
public function sanitize_redirect( $text ) {
$text = str_replace( [ "\r", "\n", "\t" ], '', $text );
$text = preg_replace( '/[^\PC\s]/u', '', $text );
return str_replace( [ '<?', '>' ], '', $text );
}
/**
* Sanitize the filename
*
* @param string $filename Filename.
* @return string
*/
public function sanitize_filename( $filename ) {
return str_replace( '.php', '', sanitize_text_field( $filename ) );
}
/**
* Save the .htaccess to a file
*
* @param string $filename Filename to save.
* @param boolean $content_to_save Content to save.
* @return bool
*/
public function save( $filename, $content_to_save = false ) {
$existing = false;
$filename = $this->sanitize_filename( $filename );
if ( file_exists( $filename ) ) {
$existing = file_get_contents( $filename );
}
$file = @fopen( $filename, 'w' );
if ( $file ) {
$result = fwrite( $file, $this->get( $existing ) );
fclose( $file );
return $result !== false;
}
return false;
}
}

View File

@@ -0,0 +1,480 @@
<?php
class Red_Plugin_Importer {
public static function get_plugins() {
$results = array();
$importers = array(
'wp-simple-redirect',
'seo-redirection',
'safe-redirect-manager',
'wordpress-old-slugs',
'rank-math',
'quick-redirects',
'pretty-links',
);
foreach ( $importers as $importer ) {
$importer = self::get_importer( $importer );
$results[] = $importer->get_data();
}
return array_values( array_filter( $results ) );
}
public static function get_importer( $id ) {
if ( $id === 'wp-simple-redirect' ) {
return new Red_Simple301_Importer();
}
if ( $id === 'seo-redirection' ) {
return new Red_SeoRedirection_Importer();
}
if ( $id === 'safe-redirect-manager' ) {
return new Red_SafeRedirectManager_Importer();
}
if ( $id === 'wordpress-old-slugs' ) {
return new Red_WordPressOldSlug_Importer();
}
if ( $id === 'rank-math' ) {
return new Red_RankMath_Importer();
}
if ( $id === 'quick-redirects' ) {
return new Red_QuickRedirect_Importer();
}
if ( $id === 'pretty-links' ) {
return new Red_PrettyLinks_Importer();
}
return false;
}
public static function import( $plugin, $group_id ) {
$importer = self::get_importer( $plugin );
if ( $importer ) {
return $importer->import_plugin( $group_id );
}
return 0;
}
}
class Red_PrettyLinks_Importer extends Red_Plugin_Importer {
public function import_plugin( $group_id ) {
global $wpdb;
$count = 0;
$redirects = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}prli_links" );
foreach ( $redirects as $redirect ) {
$created = $this->create_for_item( $group_id, $redirect );
if ( $created ) {
$count++;
}
}
return $count;
}
private function create_for_item( $group_id, $link ) {
$item = array(
'url' => '/' . $link->slug,
'action_data' => array( 'url' => $link->url ),
'regex' => false,
'group_id' => $group_id,
'match_type' => 'url',
'action_type' => 'url',
'title' => $link->name,
'action_code' => $link->redirect_type,
);
return Red_Item::create( $item );
}
public function get_data() {
$data = get_option( 'prli_db_version' );
if ( $data ) {
global $wpdb;
return [
'id' => 'pretty-links',
'name' => 'PrettyLinks',
'total' => $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}prli_links" ),
];
}
return false;
}
}
class Red_QuickRedirect_Importer extends Red_Plugin_Importer {
public function import_plugin( $group_id ) {
$redirects = get_option( 'quickppr_redirects' );
$count = 0;
foreach ( $redirects as $source => $target ) {
$item = $this->create_for_item( $group_id, $source, $target );
if ( $item ) {
$count++;
}
}
return $count;
}
private function create_for_item( $group_id, $source, $target ) {
$item = array(
'url' => $source,
'action_data' => array( 'url' => $target ),
'regex' => false,
'group_id' => $group_id,
'match_type' => 'url',
'action_type' => 'url',
'action_code' => 301,
);
return Red_Item::create( $item );
}
public function get_data() {
$data = get_option( 'quickppr_redirects' );
if ( $data ) {
return array(
'id' => 'quick-redirects',
'name' => 'Quick Page/Post Redirects',
'total' => count( $data ),
);
}
return false;
}
}
class Red_RankMath_Importer extends Red_Plugin_Importer {
public function import_plugin( $group_id ) {
global $wpdb;
$count = 0;
$redirects = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}rank_math_redirections" );
foreach ( $redirects as $redirect ) {
$created = $this->create_for_item( $group_id, $redirect );
$count += $created;
}
return $count;
}
private function create_for_item( $group_id, $redirect ) {
// phpcs:ignore
$sources = unserialize( $redirect->sources );
$items = [];
foreach ( $sources as $source ) {
$url = $source['pattern'];
if ( substr( $url, 0, 1 ) !== '/' ) {
$url = '/' . $url;
}
$data = array(
'url' => $url,
'action_data' => array( 'url' => str_replace( '\\\\', '\\', $redirect->url_to ) ),
'regex' => $source['comparison'] === 'regex' ? true : false,
'group_id' => $group_id,
'match_type' => 'url',
'action_type' => 'url',
'action_code' => $redirect->header_code,
);
$items[] = Red_Item::create( $data );
}
return count( $items );
}
public function get_data() {
global $wpdb;
if ( defined( 'REDIRECTION_TESTS' ) && REDIRECTION_TESTS ) {
return 0;
}
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$total = 0;
if ( is_plugin_active( 'seo-by-rank-math/rank-math.php' ) ) {
$total = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}rank_math_redirections" );
}
if ( $total ) {
return array(
'id' => 'rank-math',
'name' => 'RankMath',
'total' => intval( $total, 10 ),
);
}
return 0;
}
}
class Red_Simple301_Importer extends Red_Plugin_Importer {
public function import_plugin( $group_id ) {
$redirects = get_option( '301_redirects' );
$count = 0;
foreach ( $redirects as $source => $target ) {
$item = $this->create_for_item( $group_id, $source, $target );
if ( $item ) {
$count++;
}
}
return $count;
}
private function create_for_item( $group_id, $source, $target ) {
$item = array(
'url' => str_replace( '*', '(.*?)', $source ),
'action_data' => array( 'url' => str_replace( '*', '$1', trim( $target ) ) ),
'regex' => strpos( $source, '*' ) === false ? false : true,
'group_id' => $group_id,
'match_type' => 'url',
'action_type' => 'url',
'action_code' => 301,
);
return Red_Item::create( $item );
}
public function get_data() {
$data = get_option( '301_redirects' );
if ( $data ) {
return array(
'id' => 'wp-simple-redirect',
'name' => 'Simple 301 Redirects',
'total' => count( $data ),
);
}
return false;
}
}
class Red_WordPressOldSlug_Importer extends Red_Plugin_Importer {
public function import_plugin( $group_id ) {
global $wpdb;
$count = 0;
$redirects = $wpdb->get_results(
"SELECT {$wpdb->prefix}postmeta.* FROM {$wpdb->prefix}postmeta INNER JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID={$wpdb->prefix}postmeta.post_id " .
"WHERE {$wpdb->prefix}postmeta.meta_key = '_wp_old_slug' AND {$wpdb->prefix}postmeta.meta_value != '' AND {$wpdb->prefix}posts.post_status='publish' AND {$wpdb->prefix}posts.post_type IN ('page', 'post')"
);
foreach ( $redirects as $redirect ) {
$item = $this->create_for_item( $group_id, $redirect );
if ( $item ) {
$count++;
}
}
return $count;
}
private function create_for_item( $group_id, $redirect ) {
$new = get_permalink( $redirect->post_id );
if ( is_wp_error( $new ) ) {
return false;
}
$new_path = wp_parse_url( $new, PHP_URL_PATH );
$old = rtrim( dirname( $new_path ), '/' ) . '/' . rtrim( $redirect->meta_value, '/' ) . '/';
$old = str_replace( '\\', '', $old );
$old = str_replace( '//', '/', $old );
$data = array(
'url' => $old,
'action_data' => array( 'url' => $new ),
'regex' => false,
'group_id' => $group_id,
'match_type' => 'url',
'action_type' => 'url',
'action_code' => 301,
);
return Red_Item::create( $data );
}
public function get_data() {
global $wpdb;
$total = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}postmeta INNER JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID={$wpdb->prefix}postmeta.post_id WHERE {$wpdb->prefix}postmeta.meta_key = '_wp_old_slug' AND {$wpdb->prefix}postmeta.meta_value != '' AND {$wpdb->prefix}posts.post_status='publish' AND {$wpdb->prefix}posts.post_type IN ('page', 'post')"
);
if ( $total ) {
return array(
'id' => 'wordpress-old-slugs',
'name' => __( 'Default WordPress "old slugs"', 'redirection' ),
'total' => intval( $total, 10 ),
);
}
return false;
}
}
class Red_SeoRedirection_Importer extends Red_Plugin_Importer {
public function import_plugin( $group_id ) {
global $wpdb;
if ( defined( 'REDIRECTION_TESTS' ) && REDIRECTION_TESTS ) {
return 0;
}
$count = 0;
$redirects = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}WP_SEO_Redirection" );
foreach ( $redirects as $redirect ) {
$item = $this->create_for_item( $group_id, $redirect );
if ( $item ) {
$count++;
}
}
return $count;
}
private function create_for_item( $group_id, $seo ) {
if ( intval( $seo->enabled, 10 ) === 0 ) {
return false;
}
$data = array(
'url' => $seo->regex ? $seo->regex : $seo->redirect_from,
'action_data' => array( 'url' => $seo->redirect_to ),
'regex' => $seo->regex ? true : false,
'group_id' => $group_id,
'match_type' => 'url',
'action_type' => 'url',
'action_code' => intval( $seo->redirect_type, 10 ),
);
return Red_Item::create( $data );
}
public function get_data() {
global $wpdb;
$plugins = get_option( 'active_plugins', array() );
$found = false;
foreach ( $plugins as $plugin ) {
if ( strpos( $plugin, 'seo-redirection.php' ) !== false ) {
$found = true;
break;
}
}
if ( $found ) {
$total = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}WP_SEO_Redirection" );
return array(
'id' => 'seo-redirection',
'name' => 'SEO Redirection',
'total' => $total,
);
}
return false;
}
}
class Red_SafeRedirectManager_Importer extends Red_Plugin_Importer {
public function import_plugin( $group_id ) {
global $wpdb;
$count = 0;
$redirects = $wpdb->get_results(
"SELECT {$wpdb->prefix}postmeta.* FROM {$wpdb->prefix}postmeta INNER JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID={$wpdb->prefix}postmeta.post_id WHERE {$wpdb->prefix}postmeta.meta_key LIKE '_redirect_rule_%' AND {$wpdb->prefix}posts.post_status='publish'"
);
// Group them by post ID
$by_post = array();
foreach ( $redirects as $redirect ) {
if ( ! isset( $by_post[ $redirect->post_id ] ) ) {
$by_post[ $redirect->post_id ] = array();
}
$by_post[ $redirect->post_id ][ str_replace( '_redirect_rule_', '', $redirect->meta_key ) ] = $redirect->meta_value;
}
// Now go through the redirects
foreach ( $by_post as $post ) {
$item = $this->create_for_item( $group_id, $post );
if ( $item ) {
$count++;
}
}
return $count;
}
private function create_for_item( $group_id, $post ) {
$regex = false;
$source = $post['from'];
if ( strpos( $post['from'], '*' ) !== false ) {
$regex = true;
$source = str_replace( '*', '.*', $source );
} elseif ( isset( $post['from_regex'] ) && $post['from_regex'] === '1' ) {
$regex = true;
}
$data = array(
'url' => $source,
'action_data' => array( 'url' => $post['to'] ),
'regex' => $regex,
'group_id' => $group_id,
'match_type' => 'url',
'action_type' => 'url',
'action_code' => intval( $post['status_code'], 10 ),
);
return Red_Item::create( $data );
}
public function get_data() {
global $wpdb;
$total = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}postmeta INNER JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID={$wpdb->prefix}postmeta.post_id WHERE {$wpdb->prefix}postmeta.meta_key = '_redirect_rule_from' AND {$wpdb->prefix}posts.post_status='publish'"
);
if ( $total ) {
return array(
'id' => 'safe-redirect-manager',
'name' => 'Safe Redirect Manager',
'total' => intval( $total, 10 ),
);
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
<?php
class Redirection_IP {
private $ip;
public function __construct( $ip = '' ) {
$this->ip = '';
$ip = sanitize_text_field( $ip );
$ip = explode( ',', $ip );
$ip = array_shift( $ip );
$ip = filter_var( $ip, FILTER_VALIDATE_IP );
// Convert to binary
// phpcs:ignore
$ip = @inet_pton( trim( $ip ) );
if ( $ip !== false ) {
// phpcs:ignore
$this->ip = @inet_ntop( $ip ); // Convert back to string
}
}
public function get() {
return $this->ip;
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* 404 error logging. Extends the base log class with specifics for 404s
*/
class Red_404_Log extends Red_Log {
/**
* Get's the table name for this log object
*
* @param Object $wpdb WPDB object.
* @return string
*/
protected static function get_table_name( $wpdb ) {
return "{$wpdb->prefix}redirection_404";
}
/**
* Create a 404 log entry
*
* @param string $domain Domain name of request.
* @param string $url URL of request.
* @param string $ip IP of client.
* @param array $details Other log details.
* @return integer|false Log ID, or false
*/
public static function create( $domain, $url, $ip, $details ) {
global $wpdb;
$insert = static::sanitize_create( $domain, $url, $ip, $details );
$insert = apply_filters( 'redirection_404_data', $insert );
if ( $insert ) {
do_action( 'redirection_404', $insert );
$wpdb->insert( $wpdb->prefix . 'redirection_404', $insert );
if ( $wpdb->insert_id ) {
return $wpdb->insert_id;
}
}
return false;
}
/**
* Get the CSV filename for this log object
*
* @return string
*/
public static function get_csv_filename() {
return 'redirection-404';
}
/**
* Get the CSV headers for this log object
*
* @return array
*/
public static function get_csv_header() {
return [ 'date', 'source', 'ip', 'referrer', 'useragent' ];
}
/**
* Get the CSV headers for this log object
*
* @param object $row Log row.
* @return array
*/
public static function get_csv_row( $row ) {
return [
$row->created,
$row->url,
$row->ip,
$row->referrer,
$row->agent,
];
}
}
// phpcs:ignore
class RE_404 {
public static function create( $url, $agent, $ip, $referrer ) {
_deprecated_function( __FUNCTION__, '4.6', 'Red_404_Log::create( $domain, $url, $ip, $details )' );
return Red_404_Log::create( Redirection_Request::get_server(), $url, $ip, [
'agent' => $agent,
'referrer' => $referrer,
'request_method' => Redirection_Request::get_request_method(),
] );
}
}

View File

@@ -0,0 +1,177 @@
<?php
/**
* Redirect logging. Extends the base log class with specifics for redirects
*/
class Red_Redirect_Log extends Red_Log {
/**
* The redirect associated with this log entry.
*
* @var integer
*/
protected $redirection_id = 0;
/**
* The URL the client was redirected to.
*
* @var string
*/
protected $sent_to = '';
/**
* Who redirected this URL?
*
* @var string
*/
protected $redirect_by = '';
/**
* Get's the table name for this log object
*
* @param Object $wpdb WPDB object.
* @return string
*/
protected static function get_table_name( $wpdb ) {
return "{$wpdb->prefix}redirection_logs";
}
/**
* Create a redirect log entry
*
* @param string $domain Domain name of request.
* @param string $url URL of request.
* @param string $ip IP of client.
* @param array $details Other log details.
* @return integer|false Log ID, or false
*/
public static function create( $domain, $url, $ip, $details ) {
global $wpdb;
$insert = self::sanitize_create( $domain, $url, $ip, $details );
$insert['redirection_id'] = 0;
if ( isset( $details['redirect_id'] ) ) {
$insert['redirection_id'] = intval( $details['redirect_id'], 10 );
}
if ( isset( $details['target'] ) ) {
$insert['sent_to'] = $details['target'];
}
if ( isset( $details['redirect_by'] ) ) {
$insert['redirect_by'] = strtolower( substr( $details['redirect_by'], 0, 50 ) );
}
$insert = apply_filters( 'redirection_log_data', $insert );
if ( $insert ) {
do_action( 'redirection_log', $insert );
$wpdb->insert( $wpdb->prefix . 'redirection_logs', $insert );
if ( $wpdb->insert_id ) {
return $wpdb->insert_id;
}
}
return false;
}
/**
* Get query filters as a SQL `WHERE` statement. SQL will be sanitized
*
* @param array $filter Array of filter params.
* @return array
*/
protected static function get_query_filter( array $filter ) {
global $wpdb;
$where = parent::get_query_filter( $filter );
if ( isset( $filter['target'] ) ) {
$where[] = $wpdb->prepare( 'sent_to LIKE %s', '%' . $wpdb->esc_like( trim( $filter['target'] ) ) . '%' );
}
if ( isset( $filter['redirect_by'] ) ) {
$where[] = $wpdb->prepare( 'redirect_by = %s', $filter['redirect_by'] );
}
return $where;
}
/**
* Get the CSV filename for this log object
*
* @return string
*/
public static function get_csv_filename() {
return 'redirection-log';
}
/**
* Get the CSV headers for this log object
*
* @return array
*/
public static function get_csv_header() {
return [ 'date', 'source', 'target', 'ip', 'referrer', 'agent' ];
}
/**
* Get the CSV headers for this log object
*
* @param object $row Log row.
* @return array
*/
public static function get_csv_row( $row ) {
return [
$row->created,
$row->url,
$row->sent_to,
$row->ip,
$row->referrer,
$row->agent,
];
}
/**
* Get a displayable name for the originator of the redirect.
*
* @param string $agent Redirect agent.
* @return string
*/
private function get_redirect_name( $agent ) {
// phpcs:ignore
if ( $agent === 'wordpress' ) {
return 'WordPress';
}
return ucwords( $agent );
}
/**
* Convert a log entry to JSON
*
* @return array
*/
public function to_json() {
return array_merge( parent::to_json(), [
'sent_to' => $this->sent_to,
'redirection_id' => intval( $this->redirection_id, 10 ),
'redirect_by_slug' => $this->redirect_by,
'redirect_by' => $this->get_redirect_name( $this->redirect_by ),
] );
}
}
// phpcs:ignore
class RE_Log {
public static function create( $url, $target, $agent, $ip, $referrer, $extra = array() ) {
_deprecated_function( __FUNCTION__, '4.6', 'Red_Redirect_Log::create( $domain, $url, $ip, $details )' );
return Red_Redirect_Log::create( Redirection_Request::get_server(), $url, $ip, array_merge( [
'agent' => $agent,
'referrer' => $referrer,
'target' => $target,
'request_method' => Redirection_Request::get_request_method(),
], $extra ) );
}
}

View File

@@ -0,0 +1,522 @@
<?php
require_once __DIR__ . '/log-404.php';
require_once __DIR__ . '/log-redirect.php';
/**
* Base log class
*/
abstract class Red_Log {
const MAX_IP_LENGTH = 45;
const MAX_DOMAIN_LENGTH = 255;
const MAX_URL_LENGTH = 2000;
const MAX_AGENT_LENGTH = 255;
const MAX_REFERRER_LENGTH = 255;
/**
* Supported HTTP methods
*
* @var array
*/
protected static $supported_methods = [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH' ];
/**
* Log ID
*
* @var integer
*/
protected $id = 0;
/**
* Created date time
*
* @var integer
*/
protected $created = 0;
/**
* Requested URL
*
* @var string
*/
protected $url = '';
/**
* Client user agent
*
* @var string
*/
protected $agent = '';
/**
* Client referrer
*
* @var string
*/
protected $referrer = '';
/**
* Client IP
*
* @var string
*/
protected $ip = '';
/**
* Requested domain
*
* @var string
*/
protected $domain = '';
/**
* Response HTTP code
*
* @var integer
*/
protected $http_code = 0;
/**
* Request method
*
* @var string
*/
protected $request_method = '';
/**
* Additional request data
*
* @var string
*/
protected $request_data = '';
/**
* Constructor
*
* @param array $values Array of log values.
*/
final public function __construct( array $values ) {
foreach ( $values as $key => $value ) {
$this->$key = $value;
}
if ( isset( $values['created'] ) ) {
$converted = mysql2date( 'U', $values['created'] );
if ( $converted ) {
$this->created = intval( $converted, 10 );
}
}
}
/**
* Get's the table name for this log object
*
* @param Object $wpdb WPDB object.
* @return string
*/
protected static function get_table_name( $wpdb ) {
return '';
}
/**
* Get a log item by ID
*
* @param integer $id Log ID.
* @return Red_Log|false
*/
public static function get_by_id( $id ) {
global $wpdb;
$table = static::get_table_name( $wpdb );
// Table name is internally generated.
// phpcs:ignore
$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id=%d", $id ), ARRAY_A );
if ( $row ) {
return new static( $row );
}
return false;
}
/**
* Delete a log entry
*
* @param integer $id Log ID.
* @return integer|false
*/
public static function delete( $id ) {
global $wpdb;
return $wpdb->delete( static::get_table_name( $wpdb ), [ 'id' => $id ] );
}
/**
* Delete all matching log entries
*
* @param array $params Array of filter parameters.
* @return integer|false
*/
public static function delete_all( array $params = [] ) {
global $wpdb;
$query = self::get_query( $params );
$table = static::get_table_name( $wpdb );
$sql = "DELETE FROM {$table} {$query['where']}";
// phpcs:ignore
return $wpdb->query( $sql );
}
/**
* Convert a log entry to JSON
*
* @return array
*/
public function to_json() {
return [
'id' => intval( $this->id, 10 ),
'created' => date_i18n( get_option( 'date_format' ), $this->created ),
'created_time' => gmdate( get_option( 'time_format' ), $this->created ),
'url' => $this->url,
'agent' => $this->agent,
'referrer' => $this->referrer,
'domain' => $this->domain,
'ip' => $this->ip,
'http_code' => intval( $this->http_code, 10 ),
'request_method' => $this->request_method,
'request_data' => $this->request_data ? json_decode( $this->request_data, true ) : '',
];
}
/**
* Get filtered log entries
*
* @param array $params Filters.
* @return array{items: Array, total: integer}
*/
public static function get_filtered( array $params ) {
global $wpdb;
$query = self::get_query( $params );
$table = static::get_table_name( $wpdb );
$sql = "SELECT * FROM {$table} {$query['where']}";
// Already escaped
// phpcs:ignore
$sql .= $wpdb->prepare( ' ORDER BY ' . $query['orderby'] . ' ' . $query['direction'] . ' LIMIT %d,%d', $query['offset'], $query['limit'] );
// Already escaped
// phpcs:ignore
$rows = $wpdb->get_results( $sql, ARRAY_A );
// Already escaped
// phpcs:ignore
$total_items = $wpdb->get_var( "SELECT COUNT(*) FROM $table " . $query['where'] );
$items = array();
foreach ( $rows as $row ) {
$item = new static( $row );
$items[] = $item->to_json();
}
return [
'items' => $items,
'total' => intval( $total_items, 10 ),
];
}
/**
* Get grouped log entries
*
* @param string $group Group type.
* @param array $params Filter params.
* @return array{items: mixed, total: integer}
*/
public static function get_grouped( $group, array $params ) {
global $wpdb;
$table = static::get_table_name( $wpdb );
$query = self::get_query( $params );
if ( ! in_array( $group, [ 'ip', 'url', 'agent' ], true ) ) {
$group = 'url';
}
// Already escaped
// phpcs:ignore
$sql = $wpdb->prepare( "SELECT COUNT(*) as count,$group FROM {$table} {$query['where']} GROUP BY $group ORDER BY count {$query['direction']}, $group LIMIT %d,%d", $query['offset'], $query['limit'] );
// Already escaped
// phpcs:ignore
$rows = $wpdb->get_results( $sql );
// Already escaped
// phpcs:ignore
$total_items = $wpdb->get_var( "SELECT COUNT(DISTINCT $group) FROM {$table} {$query['where']}" );
foreach ( $rows as $row ) {
$row->count = intval( $row->count, 10 );
if ( isset( $row->url ) ) {
$row->id = $row->url;
} elseif ( isset( $row->ip ) ) {
$row->id = $row->ip;
} elseif ( isset( $row->agent ) ) {
$row->id = $row->agent;
}
}
return array(
'items' => $rows,
'total' => intval( $total_items, 10 ),
);
}
/**
* Convert a set of filters to a SQL query.
*
* @param array $params Filters.
* @return array{orderby: string, direction: string, limit: integer, offset: integer, where: string}
*/
public static function get_query( array $params ) {
$query = [
'orderby' => 'id',
'direction' => 'DESC',
'limit' => RED_DEFAULT_PER_PAGE,
'offset' => 0,
'where' => '',
];
if ( isset( $params['orderby'] ) && in_array( $params['orderby'], array( 'ip', 'url' ), true ) ) {
$query['orderby'] = $params['orderby'];
}
if ( isset( $params['direction'] ) && in_array( strtoupper( $params['direction'] ), array( 'ASC', 'DESC' ), true ) ) {
$query['direction'] = strtoupper( $params['direction'] );
}
if ( isset( $params['per_page'] ) ) {
$limit = intval( $params['per_page'], 10 );
if ( $limit >= 5 && $limit <= RED_MAX_PER_PAGE ) {
$query['limit'] = $limit;
}
}
if ( isset( $params['page'] ) ) {
$offset = intval( $params['page'], 10 );
if ( $offset >= 0 ) {
$query['offset'] = $offset * $query['limit'];
}
}
if ( isset( $params['filterBy'] ) && is_array( $params['filterBy'] ) ) {
$where = static::get_query_filter( $params['filterBy'] );
if ( count( $where ) > 0 ) {
$query['where'] = 'WHERE ' . implode( ' AND ', $where );
}
}
return $query;
}
/**
* Get query filters as a SQL `WHERE` statement. SQL will be sanitized
*
* @param array $filter Array of filter params.
* @return array
*/
protected static function get_query_filter( array $filter ) {
global $wpdb;
$where = [];
if ( isset( $filter['ip'] ) ) {
// phpcs:ignore
$ip = @inet_pton( trim( $filter['ip'] ) );
if ( $ip !== false ) {
// Full IP match
// phpcs:ignore
$ip = @inet_ntop( $ip ); // Convert back to string
$where[] = $wpdb->prepare( 'ip = %s', $ip );
} else {
// Partial IP match
$where[] = $wpdb->prepare( 'ip LIKE %s', '%' . $wpdb->esc_like( trim( $filter['ip'] ) ) . '%' );
}
}
if ( isset( $filter['domain'] ) ) {
$where[] = $wpdb->prepare( 'domain LIKE %s', '%' . $wpdb->esc_like( trim( $filter['domain'] ) ) . '%' );
}
if ( isset( $filter['url-exact'] ) ) {
$where[] = $wpdb->prepare( 'url = %s', $filter['url-exact'] );
} elseif ( isset( $filter['url'] ) ) {
$where[] = $wpdb->prepare( 'url LIKE %s', '%' . $wpdb->esc_like( trim( $filter['url'] ) ) . '%' );
}
if ( isset( $filter['referrer'] ) ) {
$where[] = $wpdb->prepare( 'referrer LIKE %s', '%' . $wpdb->esc_like( trim( $filter['referrer'] ) ) . '%' );
}
if ( isset( $filter['agent'] ) ) {
$agent = trim( $filter['agent'] );
if ( empty( $agent ) ) {
$where[] = $wpdb->prepare( 'agent = %s', $agent );
} else {
$where[] = $wpdb->prepare( 'agent LIKE %s', '%' . $wpdb->esc_like( $agent ) . '%' );
}
}
if ( isset( $filter['http'] ) ) {
$where[] = $wpdb->prepare( 'http_code = %d', $filter['http'] );
}
if ( isset( $filter['method'] ) && in_array( strtoupper( $filter['method'] ), static::$supported_methods, true ) ) {
$where[] = $wpdb->prepare( 'request_method = %s', strtoupper( $filter['method'] ) );
}
return $where;
}
/**
* Sanitize a new log entry
*
* @param string $domain Requested Domain.
* @param string $url Requested URL.
* @param string $ip Client IP. This is assumed to be a valid IP and won't be checked.
* @param array $details Extra log details.
* @return array
*/
protected static function sanitize_create( $domain, $url, $ip, array $details = [] ) {
$url = urldecode( $url );
$insert = [
'url' => substr( sanitize_text_field( $url ), 0, self::MAX_URL_LENGTH ),
'domain' => substr( sanitize_text_field( $domain ), 0, self::MAX_DOMAIN_LENGTH ),
'ip' => substr( sanitize_text_field( $ip ), 0, self::MAX_IP_LENGTH ),
'created' => current_time( 'mysql' ),
];
// Unfortunatley these names dont match up
$allowed = [
'agent' => 'agent',
'referrer' => 'referrer',
'request_method' => 'request_method',
'http_code' => 'http_code',
'request_data' => 'request_data',
];
foreach ( $allowed as $name => $replace ) {
if ( ! empty( $details[ $name ] ) ) {
$insert[ $replace ] = $details[ $name ];
}
}
if ( isset( $insert['agent'] ) ) {
$insert['agent'] = substr( sanitize_text_field( $insert['agent'] ), 0, self::MAX_AGENT_LENGTH );
}
if ( isset( $insert['referrer'] ) ) {
$insert['referrer'] = substr( sanitize_text_field( $insert['referrer'] ), 0, self::MAX_REFERRER_LENGTH );
}
if ( isset( $insert['request_data'] ) ) {
$insert['request_data'] = wp_json_encode( $insert['request_data'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK );
}
if ( isset( $insert['http_code'] ) ) {
$insert['http_code'] = intval( $insert['http_code'], 10 );
}
if ( isset( $insert['request_method'] ) ) {
$insert['request_method'] = strtoupper( sanitize_text_field( $insert['request_method'] ) );
if ( ! in_array( $insert['request_method'], static::$supported_methods, true ) ) {
$insert['request_method'] = '';
}
}
return $insert;
}
/**
* Get the CSV filename for this log object
*
* @return string
*/
public static function get_csv_filename() {
return '';
}
/**
* Get the CSV headers for this log object
*
* @return array
*/
public static function get_csv_header() {
return [];
}
/**
* Get the CSV headers for this log object
*
* @param object $row Log row.
* @return array
*/
public static function get_csv_row( $row ) {
return [];
}
/**
* Export the log entry to CSV
*
* @return void
*/
public static function export_to_csv() {
$filename = static::get_csv_filename() . '-' . date_i18n( get_option( 'date_format' ) ) . '.csv';
header( 'Content-Type: text/csv' );
header( 'Cache-Control: no-cache, must-revalidate' );
header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
// phpcs:ignore
$stdout = fopen( 'php://output', 'w' );
if ( ! $stdout ) {
return;
}
fputcsv( $stdout, static::get_csv_header() );
global $wpdb;
$table = static::get_table_name( $wpdb );
// phpcs:ignore
$total_items = $wpdb->get_var( "SELECT COUNT(*) FROM $table" );
$exported = 0;
$limit = 100;
while ( $exported < $total_items ) {
// phpcs:ignore
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table LIMIT %d,%d", $exported, $limit ) );
$exported += count( $rows );
foreach ( $rows as $row ) {
$csv = static::get_csv_row( $row );
fputcsv( $stdout, $csv );
}
if ( count( $rows ) < $limit ) {
break;
}
}
}
}

View File

@@ -0,0 +1,186 @@
<?php
require_once dirname( __DIR__ ) . '/matches/from-notfrom.php';
require_once dirname( __DIR__ ) . '/matches/from-url.php';
/**
* Matches a URL and some other condition
*/
abstract class Red_Match {
/**
* Match type
*
* @var string
*/
protected $type = '';
/**
* Constructor
*
* @param string $values Initial values.
*/
public function __construct( $values = '' ) {
if ( $values ) {
$this->load( $values );
}
}
/**
* Get match type
*
* @return string
*/
public function get_type() {
return $this->type;
}
/**
* Save the match
*
* @param array $details Details to save.
* @param boolean $no_target_url The URL when no target.
* @return array|null
*/
abstract public function save( array $details, $no_target_url = false );
/**
* Get the match name
*
* @return string
*/
abstract public function name();
/**
* Match the URL against the specific matcher conditions
*
* @param string $url Requested URL.
* @return boolean
*/
abstract public function is_match( $url );
/**
* Get the target URL for this match. Some matches may have a matched/unmatched target.
*
* @param string $original_url The client URL (not decoded).
* @param string $matched_url The URL in the redirect.
* @param Red_Source_Flags $flag Source flags.
* @param boolean $is_matched Was the match successful.
* @return string|false
*/
abstract public function get_target_url( $original_url, $matched_url, Red_Source_Flags $flag, $is_matched );
/**
* Get the match data
*
* @return array|null
*/
abstract public function get_data();
/**
* Load the match data into this instance.
*
* @param string $values Match values, as read from the database (plain text or serialized PHP).
* @return void
*/
abstract public function load( $values );
/**
* Sanitize a match URL
*
* @param string $url URL.
* @return string
*/
public function sanitize_url( $url ) {
// No new lines
$url = preg_replace( "/[\r\n\t].*?$/s", '', $url );
// Clean control codes
$url = preg_replace( '/[^\PC\s]/u', '', $url );
return $url;
}
/**
* Apply a regular expression to the target URL, replacing any values.
*
* @param string $source_url Redirect source URL.
* @param string $target_url Target URL.
* @param string $requested_url The URL being requested (decoded).
* @param Red_Source_Flags $flags Source URL flags.
* @return string
*/
protected function get_target_regex_url( $source_url, $target_url, $requested_url, Red_Source_Flags $flags ) {
$regex = new Red_Regex( $source_url, $flags->is_ignore_case() );
return $regex->replace( $target_url, $requested_url );
}
/**
* Create a Red_Match object, given a type
*
* @param string $name Match type.
* @param string $data Match data.
* @return Red_Match|null
*/
public static function create( $name, $data = '' ) {
$avail = self::available();
if ( isset( $avail[ strtolower( $name ) ] ) ) {
$classname = $name . '_match';
if ( ! class_exists( strtolower( $classname ) ) ) {
include dirname( __FILE__ ) . '/../matches/' . $avail[ strtolower( $name ) ];
}
/**
* @var Red_Match
*/
$class = new $classname( $data );
$class->type = $name;
return $class;
}
return null;
}
/**
* Get all Red_Match objects
*
* @return string[]
*/
public static function all() {
$data = [];
$avail = self::available();
foreach ( array_keys( $avail ) as $name ) {
/**
* @var Red_Match
*/
$obj = self::create( $name );
$data[ $name ] = $obj->name();
}
return $data;
}
/**
* Get list of available matches
*
* @return array
*/
public static function available() {
return [
'url' => 'url.php',
'referrer' => 'referrer.php',
'agent' => 'user-agent.php',
'login' => 'login.php',
'header' => 'http-header.php',
'custom' => 'custom-filter.php',
'cookie' => 'cookie.php',
'role' => 'user-role.php',
'server' => 'server.php',
'ip' => 'ip.php',
'page' => 'page.php',
'language' => 'language.php',
];
}
}

View File

@@ -0,0 +1,146 @@
<?php
require_once dirname( dirname( __FILE__ ) ) . '/modules/wordpress.php';
require_once dirname( dirname( __FILE__ ) ) . '/modules/apache.php';
require_once dirname( dirname( __FILE__ ) ) . '/modules/nginx.php';
/**
* Base class for redirect module.
*/
abstract class Red_Module {
/**
* Constructor. Loads options
*
* @param array $options Any module options.
*/
public function __construct( $options = [] ) {
if ( ! empty( $options ) ) {
$this->load( $options );
}
}
/**
* Get a module based on the supplied ID, and loads it with appropriate options.
*
* @param integer $id Module ID.
* @return Red_Module|false
*/
public static function get( $id ) {
$id = intval( $id, 10 );
$options = red_get_options();
if ( $id === Apache_Module::MODULE_ID ) {
return new Apache_Module( isset( $options['modules'][ Apache_Module::MODULE_ID ] ) ? $options['modules'][ Apache_Module::MODULE_ID ] : array() );
}
if ( $id === WordPress_Module::MODULE_ID ) {
return new WordPress_Module( isset( $options['modules'][ WordPress_Module::MODULE_ID ] ) ? $options['modules'][ WordPress_Module::MODULE_ID ] : array() );
}
if ( $id === Nginx_Module::MODULE_ID ) {
return new Nginx_Module( isset( $options['modules'][ Nginx_Module::MODULE_ID ] ) ? $options['modules'][ Nginx_Module::MODULE_ID ] : array() );
}
return false;
}
/**
* Check that an ID is valid.
*
* @param integer $id Module ID.
* @return boolean
*/
public static function is_valid_id( $id ) {
if ( $id === Apache_Module::MODULE_ID || $id === WordPress_Module::MODULE_ID || $id === Nginx_Module::MODULE_ID ) {
return true;
}
return false;
}
/**
* Return a module ID given the module name
*
* @param string $name Module name.
* @return integer|false
*/
public static function get_id_for_name( $name ) {
$names = array(
'wordpress' => WordPress_Module::MODULE_ID,
'apache' => Apache_Module::MODULE_ID,
'nginx' => Nginx_Module::MODULE_ID,
);
if ( isset( $names[ $name ] ) ) {
return $names[ $name ];
}
return false;
}
/**
* Flush the module that a group belongs to
*
* @param integer $group_id Module group ID.
* @return void
*/
public static function flush( $group_id ) {
$group = Red_Group::get( $group_id );
if ( is_object( $group ) ) {
$module = self::get( $group->get_module_id() );
if ( $module ) {
$module->flush_module();
}
}
}
/**
* Flush the module
*
* @param integer $module_id Module ID.
* @return void
*/
public static function flush_by_module( $module_id ) {
$module = self::get( $module_id );
if ( $module ) {
$module->flush_module();
}
}
/**
* Get the module ID
*
* @return integer
*/
abstract public function get_id();
/**
* Update
*
* @param array $data Data.
* @return false
*/
public function update( array $data ) {
return false;
}
/**
* Load
*
* @param array $options Options.
* @return void
*/
protected function load( $options ) {
}
/**
* Flush
*
* @return void
*/
protected function flush_module() {
}
}

View File

@@ -0,0 +1,148 @@
<?php
class Red_Monitor {
private $monitor_group_id;
private $updated_posts = array();
private $monitor_types = array();
private $associated = '';
public function __construct( $options ) {
$this->monitor_types = apply_filters( 'redirection_monitor_types', isset( $options['monitor_types'] ) ? $options['monitor_types'] : array() );
if ( count( $this->monitor_types ) > 0 && $options['monitor_post'] > 0 ) {
$this->monitor_group_id = intval( $options['monitor_post'], 10 );
$this->associated = isset( $options['associated_redirect'] ) ? $options['associated_redirect'] : '';
// Only monitor if permalinks enabled
if ( get_option( 'permalink_structure' ) ) {
add_action( 'pre_post_update', array( $this, 'pre_post_update' ), 10, 2 );
add_action( 'post_updated', array( $this, 'post_updated' ), 11, 3 );
add_filter( 'redirection_remove_existing', array( $this, 'remove_existing_redirect' ) );
add_filter( 'redirection_permalink_changed', array( $this, 'has_permalink_changed' ), 10, 3 );
if ( in_array( 'trash', $this->monitor_types ) ) {
add_action( 'wp_trash_post', array( $this, 'post_trashed' ) );
}
}
}
}
public function remove_existing_redirect( $url ) {
Red_Item::disable_where_matches( $url );
}
public function can_monitor_post( $post, $post_before ) {
// Check this is for the expected post
if ( ! isset( $post->ID ) || ! isset( $this->updated_posts[ $post->ID ] ) ) {
return false;
}
// Don't do anything if we're not published
if ( $post->post_status !== 'publish' || $post_before->post_status !== 'publish' ) {
return false;
}
$type = get_post_type( $post->ID );
if ( ! in_array( $type, $this->monitor_types ) ) {
return false;
}
return true;
}
/**
* Called when a post has been updated - check if the slug has changed
*/
public function post_updated( $post_id, $post, $post_before ) {
if ( isset( $this->updated_posts[ $post_id ] ) && $this->can_monitor_post( $post, $post_before ) ) {
$this->check_for_modified_slug( $post_id, $this->updated_posts[ $post_id ] );
}
}
/**
* Remember the previous post permalink
*/
public function pre_post_update( $post_id, $data ) {
$this->updated_posts[ $post_id ] = get_permalink( $post_id );
}
public function post_trashed( $post_id ) {
$data = array(
'url' => wp_parse_url( get_permalink( $post_id ), PHP_URL_PATH ),
'action_data' => array( 'url' => '/' ),
'match_type' => 'url',
'action_type' => 'url',
'action_code' => 301,
'group_id' => $this->monitor_group_id,
'status' => 'disabled',
);
// Create a new redirect for this post, but only if not draft
if ( $data['url'] !== '/' ) {
Red_Item::create( $data );
}
}
/**
* Changed if permalinks are different and the before wasn't the site url (we don't want to redirect the site URL)
*/
public function has_permalink_changed( $result, $before, $after ) {
// Check it's not redirecting from the root
if ( $this->get_site_path() === $before || $before === '/' ) {
return false;
}
// Are the URLs the same?
if ( $before === $after ) {
return false;
}
return true;
}
private function get_site_path() {
$path = wp_parse_url( get_site_url(), PHP_URL_PATH );
if ( $path ) {
return rtrim( $path, '/' ) . '/';
}
return '/';
}
public function check_for_modified_slug( $post_id, $before ) {
$after = wp_parse_url( get_permalink( $post_id ), PHP_URL_PATH );
$before = wp_parse_url( esc_url( $before ), PHP_URL_PATH );
if ( apply_filters( 'redirection_permalink_changed', false, $before, $after ) ) {
do_action( 'redirection_remove_existing', $after, $post_id );
$data = array(
'url' => $before,
'action_data' => array( 'url' => $after ),
'match_type' => 'url',
'action_type' => 'url',
'action_code' => 301,
'group_id' => $this->monitor_group_id,
);
// Create a new redirect for this post
$new_item = Red_Item::create( $data );
if ( ! is_wp_error( $new_item ) ) {
do_action( 'redirection_monitor_created', $new_item, $before, $post_id );
if ( ! empty( $this->associated ) ) {
// Create an associated redirect for this post
$data['url'] = trailingslashit( $data['url'] ) . ltrim( $this->associated, '/' );
$data['action_data'] = array( 'url' => trailingslashit( $data['action_data']['url'] ) . ltrim( $this->associated, '/' ) );
Red_Item::create( $data );
}
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* Provides permalink migration facilities
*/
class Red_Permalinks {
/**
* List of migrated permalink structures
*
* @var string[]
*/
private $permalinks = [];
/**
* Current permalink structure
*
* @var string|null
*/
private $current_permalink = null;
/**
* Constructor
*
* @param string[] $permalinks List of migrated permalinks.
*/
public function __construct( $permalinks ) {
$this->permalinks = $permalinks;
}
/**
* Match and migrate any permalinks
*
* @param WP_Query $query Query.
* @return void
*/
public function migrate( WP_Query $query ) {
global $wp, $wp_query;
if ( count( $this->permalinks ) === 0 ) {
return;
}
if ( ! $this->needs_migrating() ) {
return;
}
$this->intercept_permalinks();
$query_copy = clone $query;
foreach ( $this->permalinks as $old ) {
// Set the current permalink
$this->current_permalink = $old;
// Run the WP query again
$wp->init();
$wp->parse_request();
// Anything matched?
if ( $wp->matched_rule ) {
// Perform the post query
$wp->query_posts();
// A single post?
if ( is_single() && count( $query->posts ) > 0 ) {
// Restore permalinks
$this->release_permalinks();
// Get real URL from the post ID
$url = get_permalink( $query->posts[0]->ID );
if ( $url ) {
wp_safe_redirect( $url, 301, 'redirection' );
die();
}
}
// Reset the query back to the original
// phpcs:ignore
$wp_query = $query_copy;
break;
}
}
$this->release_permalinks();
}
/**
* Determine if the current request needs migrating. This is based on `WP::handle_404` in class-wp.php
*
* @return boolean
*/
private function needs_migrating() {
global $wp_query;
// It's a 404 - shortcut to yes
if ( is_404() ) {
return true;
}
// Not admin pages
if ( is_admin() || is_robots() || is_favicon() ) {
return false;
}
if ( $wp_query->posts && ! $wp_query->is_posts_page && empty( $this->query_vars['page'] ) ) {
return false;
}
if ( ! is_paged() ) {
$author = get_query_var( 'author' );
// Don't 404 for authors without posts as long as they matched an author on this site.
if ( is_author() && is_numeric( $author ) && $author > 0 && is_user_member_of_blog( $author )
// Don't 404 for these queries if they matched an object.
|| ( is_tag() || is_category() || is_tax() || is_post_type_archive() ) && get_queried_object()
// Don't 404 for these queries either.
|| is_home() || is_search() || is_feed()
) {
return false;
}
}
// If we've got this far then it's a 404
return true;
}
/**
* Hook the permalink options and return the migrated one
*
* @return void
*/
private function intercept_permalinks() {
add_filter( 'pre_option_rewrite_rules', [ $this, 'get_old_rewrite_rules' ] );
add_filter( 'pre_option_permalink_structure', [ $this, 'get_old_permalink' ] );
}
/**
* Restore the hooked option
*
* @return void
*/
private function release_permalinks() {
remove_filter( 'pre_option_rewrite_rules', [ $this, 'get_old_rewrite_rules' ] );
remove_filter( 'pre_option_permalink_structure', [ $this, 'get_old_permalink' ] );
}
/**
* Returns rewrite rules for the current migrated permalink
*
* @param array $rules Current rules.
* @return array
*/
public function get_old_rewrite_rules( $rules ) {
global $wp_rewrite;
if ( $this->current_permalink ) {
$wp_rewrite->init();
$wp_rewrite->matches = 'matches';
return $wp_rewrite->rewrite_rules();
}
return $rules;
}
/**
* Get the current migrated permalink structure
*
* @param string $result Current value.
* @return string
*/
public function get_old_permalink( $result ) {
if ( $this->current_permalink ) {
return $this->current_permalink;
}
return $result;
}
}

View File

@@ -0,0 +1,217 @@
<?php
/**
* Redirect caching.
*
* This is based on server requests and not database requests.
*
* The client requests a URL. We use the requested URL, the `cache_key` setting, and the plugin version, to look for a cache entry.
*
* The `cache_key` is updated each time *any* redirect is updated. This is because a URL can be affected by other redirects, such as regular expressions
* and redirects with dynamic conditions (i.e. cookie, login status etc).
*
* We include the plugin version as data can change between plugin versions, and it is safest to use new cache entries.
*
* If we have a cache hit then the data is used to perform the redirect.php
*
* If we do not have a cache hit then we request the URL from the database and perform redirect matches.
*
* After matching has been performed we then try and update the cache:
* - if no match was found, cache an empty result
* - if a match was found and no dynamic redirects were encountered, then cache that redirect only
* - if a match was found and dynamic redirects were involved then cache all redirects
*
* We have a maximum number of redirects that can be cached to avoid saturating the cache.
*/
class Redirect_Cache {
const EMPTY_VALUE = 'empty';
const CACHE_MAX = 10;
/**
* Singleton
*
* @var Redirect_Cache|null
*/
private static $instance = null;
/**
* Array of URLs that have been cached
*
* @var array
*/
private $cached = [];
/**
* Cache key. Changed to current time whenever a redirect is updated.
*
* @var integer
*/
private $key = 0;
/**
* Initialiser
*
* @return Redirect_Cache
*/
public static function init() {
if ( is_null( self::$instance ) ) {
self::$instance = new Redirect_Cache();
}
return self::$instance;
}
/**
* Constructor
*/
public function __construct() {
$this->reset();
}
public function reset() {
$settings = red_get_options();
$this->key = $settings['cache_key'];
$this->cached = [];
}
/**
* Is the cache enabled?
*
* @return boolean
*/
public function can_cache() {
return $this->key > 0;
}
/**
* Get the current cache key
*
* @param string $url URL we are looking at.
* @return string
*/
private function get_key( $url ) {
return apply_filters( 'redirection_cache_key', md5( $url ) . '-' . (string) $this->key . '-' . REDIRECTION_VERSION );
}
/**
* Get the cache entry for a URL
*
* @param string $url Requested URL.
* @return Red_Item[]|bool
*/
public function get( $url ) {
if ( ! $this->can_cache() ) {
return false;
}
$cache_key = $this->get_key( $url );
// Look in cache
$false = false;
$result = wp_cache_get( $cache_key, 'redirection', false, $false );
// If a result was found then remember we are using the cache so we don't need to re-save it later
if ( $result !== false ) {
$this->cached[ $url ] = true;
}
// Empty value is a special case. Storing [] in the cache doesn't work, so we store the special EMPTY_VALUE to represent []
if ( $result === self::EMPTY_VALUE ) {
return [];
}
return $result;
}
/**
* Set the cache for a URL
*
* @param string $url URL to cache.
* @param Red_Item|false $matched The matched redirect.
* @param Red_Item[] $redirects All of the redirects the match the URL.
* @return boolean
*/
public function set( $url, $matched, $redirects ) {
if ( ! $this->can_cache() || isset( $this->cached[ $url ] ) ) {
return false;
}
$cache_key = $this->get_key( $url );
// Default store the match redirect
$rows = [];
if ( $matched ) {
$rows[] = $matched;
}
// Are any of the redirects before, and including, the match a dynamic redirect?
$dynamic = $this->get_dynamic_matched( $redirects, $matched );
if ( count( $dynamic ) > 0 ) {
// Store all dynamic redirects
$rows = $dynamic;
}
// Have we exceeded our limit?
if ( count( $rows ) > self::CACHE_MAX ) {
return false;
}
$converted = $this->convert_to_rows( $rows );
$value = count( $converted ) === 0 ? self::EMPTY_VALUE : $converted;
wp_cache_set( $cache_key, $value, 'redirection' );
return true;
}
/**
* Convert a Red_Item to a format suitable for storing in the cache
*
* @param Red_Item[] $rows Redirects.
* @return array
*/
private function convert_to_rows( array $rows ) {
$converted = [];
foreach ( $rows as $row ) {
$converted[] = $row->to_sql();
}
return $converted;
}
/**
* If there are dynamic redirects before the matched redirect then return all dynamic redirects (including the matched one), otherwise return nothing.
*
* If the matched redirect is a static redirect then we include it in the list, but don't include any redirects after.
*
* @param Red_Item[] $redirects Array of redirects.
* @param Red_Item|false $matched The matched item.
* @return Red_Item[]
*/
private function get_dynamic_matched( array $redirects, $matched ) {
$dynamic = [];
foreach ( $redirects as $redirect ) {
if ( $redirect->is_dynamic() ) {
$dynamic[] = $redirect;
}
// Is this the matched redirect?
if ( $matched === $redirect ) {
// Yes. Do we have any dynamic redirects so far?
if ( count( $dynamic ) === 0 ) {
// No. Just return an empty array
return [];
}
if ( ! $matched->is_dynamic() ) {
// We need to include the non-dynamic redirect in the list
return array_merge( $dynamic, [ $matched ] );
}
}
}
return $dynamic;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Filter the redirects
*/
class Red_Item_Filters {
/**
* List of filters
*
* @var array
*/
private $filters = [];
/**
* Constructor
*
* @param Array $filter_params Filters.
*/
public function __construct( $filter_params ) {
global $wpdb;
foreach ( $filter_params as $filter_by => $filter ) {
$filter = trim( sanitize_text_field( $filter ) );
$filter_by = sanitize_text_field( $filter_by );
if ( $filter_by === 'status' ) {
if ( $filter === 'enabled' ) {
$this->filters[] = "status='enabled'";
} else {
$this->filters[] = "status='disabled'";
}
} elseif ( $filter_by === 'url-match' ) {
if ( $filter === 'regular' ) {
$this->filters[] = 'regex=1';
} else {
$this->filters[] = 'regex=0';
}
} elseif ( $filter_by === 'id' ) {
$this->filters[] = $wpdb->prepare( 'id=%d', intval( $filter, 10 ) );
} elseif ( $filter_by === 'match' && in_array( $filter, array_keys( Red_Match::available() ), true ) ) {
$this->filters[] = $wpdb->prepare( 'match_type=%s', $filter );
} elseif ( $filter_by === 'action' && in_array( $filter, array_keys( Red_Action::available() ), true ) ) {
$this->filters[] = $wpdb->prepare( 'action_type=%s', $filter );
} elseif ( $filter_by === 'http' ) {
$sanitizer = new Red_Item_Sanitize();
$filter = intval( $filter, 10 );
if ( $sanitizer->is_valid_error_code( $filter ) || $sanitizer->is_valid_redirect_code( $filter ) ) {
$this->filters[] = $wpdb->prepare( 'action_code=%d', $filter );
}
} elseif ( $filter_by === 'access' ) {
if ( $filter === 'year' ) {
$this->filters[] = 'last_access < DATE_SUB(NOW(),INTERVAL 1 YEAR)';
} elseif ( $filter === 'month' ) {
$this->filters[] = 'last_access < DATE_SUB(NOW(),INTERVAL 1 MONTH)';
} else {
$this->filters[] = "( last_access < '1970-01-01 00:00:01' )";
}
} elseif ( $filter_by === 'url' ) {
$this->filters[] = $wpdb->prepare( 'url LIKE %s', '%' . $wpdb->esc_like( $filter ) . '%' );
} elseif ( $filter_by === 'target' ) {
$this->filters[] = $wpdb->prepare( 'action_data LIKE %s', '%' . $wpdb->esc_like( $filter ) . '%' );
} elseif ( $filter_by === 'title' ) {
$this->filters[] = $wpdb->prepare( 'title LIKE %s', '%' . $wpdb->esc_like( $filter ) . '%' );
} elseif ( $filter_by === 'group' ) {
$this->filters[] = $wpdb->prepare( 'group_id=%d', intval( $filter, 10 ) );
}
}
}
/**
* Get the filters as sanitized SQL.
*
* @return string
*/
public function get_as_sql() {
if ( count( $this->filters ) > 0 ) {
return 'WHERE ' . implode( ' AND ', $this->filters );
}
return '';
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Options for a redirect source URL
*/
class Red_Source_Options {
/**
* Exclude this from logging.
*
* @var boolean
*/
private $log_exclude = false;
/**
* Constructor
*
* @param array|null $options Options.
*/
public function __construct( $options = null ) {
if ( $options ) {
$this->set_options( $options );
}
}
/**
* Set options
*
* @param array $options Options.
* @return void
*/
public function set_options( $options ) {
if ( isset( $options['log_exclude'] ) && $options['log_exclude'] === true ) {
$this->log_exclude = true;
}
}
/**
* Can this source be logged?
*
* @return boolean
*/
public function can_log() {
$options = red_get_options();
if ( isset( $options['expire_redirect'] ) && $options['expire_redirect'] !== -1 ) {
return ! $this->log_exclude;
}
return false;
}
/**
* Get options as JSON
*
* @return array
*/
public function get_json() {
return array_filter( [
'log_exclude' => $this->log_exclude,
] );
}
}

View File

@@ -0,0 +1,293 @@
<?php
class Red_Item_Sanitize {
private function clean_array( $array ) {
foreach ( $array as $name => $value ) {
if ( is_array( $value ) ) {
$array[ $name ] = $this->clean_array( $value );
} elseif ( is_string( $value ) ) {
$value = trim( $value );
$array[ $name ] = $value;
} else {
$array[ $name ] = $value;
}
};
return $array;
}
private function set_server( $url, array $details ) {
$return = [];
$domain = wp_parse_url( $url, PHP_URL_HOST );
// Auto-convert an absolute URL to relative + server match
if ( $domain && $domain !== Redirection_Request::get_server_name() ) {
$return['match_type'] = 'server';
if ( isset( $details['action_data']['url'] ) ) {
$return['action_data'] = [
'server' => $domain,
'url_from' => $details['action_data']['url'],
];
} else {
$return['action_data'] = [ 'server' => $domain ];
}
$url = wp_parse_url( $url, PHP_URL_PATH );
if ( is_wp_error( $url ) || $url === null ) {
$url = '/';
}
}
$return['url'] = $url;
return $return;
}
public function get( array $details ) {
$data = [];
$details = $this->clean_array( $details );
// Set regex
$data['regex'] = isset( $details['regex'] ) && intval( $details['regex'], 10 ) === 1 ? 1 : 0;
// Auto-migrate the regex to the source flags
$data['match_data'] = [ 'source' => [ 'flag_regex' => $data['regex'] === 1 ? true : false ] ];
$flags = new Red_Source_Flags();
// Set flags
if ( isset( $details['match_data'] ) && isset( $details['match_data']['source'] ) ) {
$defaults = red_get_options();
// Parse the source flags
$flags = new Red_Source_Flags( $details['match_data']['source'] );
// Remove defaults
$data['match_data']['source'] = $flags->get_json_without_defaults( $defaults );
$data['regex'] = $flags->is_regex() ? 1 : 0;
}
// If match_data is empty then don't save anything
if ( isset( $data['match_data']['source'] ) && count( $data['match_data']['source'] ) === 0 ) {
$data['match_data']['source'] = [];
}
if ( isset( $details['match_data']['options'] ) && is_array( $details['match_data']['options'] ) ) {
$source = new Red_Source_Options( $details['match_data']['options'] );
$data['match_data']['options'] = $source->get_json();
}
$data['match_data'] = array_filter( $data['match_data'] );
if ( empty( $data['match_data'] ) ) {
$data['match_data'] = null;
}
// Parse URL
$url = empty( $details['url'] ) ? $this->auto_generate() : $details['url'];
if ( strpos( $url, 'http:' ) !== false || strpos( $url, 'https:' ) !== false ) {
$details = array_merge( $details, $this->set_server( $url, $details ) );
}
$data['match_type'] = isset( $details['match_type'] ) ? sanitize_text_field( $details['match_type'] ) : 'url';
$data['url'] = $this->get_url( $url, $data['regex'] );
if ( isset( $details['hits'] ) ) {
$data['last_count'] = intval( $details['hits'], 10 );
}
if ( isset( $details['last_access'] ) ) {
$data['last_access'] = date( 'Y-m-d H:i:s', strtotime( sanitize_text_field( $details['last_access'] ) ) );
}
if ( ! is_wp_error( $data['url'] ) ) {
$matcher = new Red_Url_Match( $data['url'] );
$data['match_url'] = $matcher->get_url();
// If 'exact order' then save the match URL with query params
if ( $flags->is_query_exact_order() ) {
$data['match_url'] = $matcher->get_url_with_params();
}
}
$data['title'] = ! empty( $details['title'] ) ? $details['title'] : null;
$data['group_id'] = $this->get_group( isset( $details['group_id'] ) ? $details['group_id'] : 0 );
$data['position'] = $this->get_position( $details );
// Set match_url to 'regex'
if ( $data['regex'] ) {
$data['match_url'] = 'regex';
}
if ( $data['title'] ) {
$data['title'] = trim( substr( sanitize_text_field( $data['title'] ), 0, 500 ) );
$data['title'] = wp_kses( $data['title'], 'strip' );
if ( strlen( $data['title'] ) === 0 ) {
$data['title'] = null;
}
}
$matcher = Red_Match::create( isset( $details['match_type'] ) ? sanitize_text_field( $details['match_type'] ) : false );
if ( ! $matcher ) {
return new WP_Error( 'redirect', 'Invalid redirect matcher' );
}
$action_code = isset( $details['action_code'] ) ? intval( $details['action_code'], 10 ) : 0;
$action = Red_Action::create( isset( $details['action_type'] ) ? sanitize_text_field( $details['action_type'] ) : false, $action_code );
if ( ! $action ) {
return new WP_Error( 'redirect', 'Invalid redirect action' );
}
$data['action_type'] = sanitize_text_field( $details['action_type'] );
$data['action_code'] = $this->get_code( $details['action_type'], $action_code );
if ( isset( $details['action_data'] ) && is_array( $details['action_data'] ) ) {
$match_data = $matcher->save( $details['action_data'] ? $details['action_data'] : array(), ! $this->is_url_type( $data['action_type'] ) );
$data['action_data'] = is_array( $match_data ) ? serialize( $match_data ) : $match_data;
}
// Any errors?
foreach ( $data as $value ) {
if ( is_wp_error( $value ) ) {
return $value;
}
}
return apply_filters( 'redirection_validate_redirect', $data );
}
protected function get_position( $details ) {
if ( isset( $details['position'] ) ) {
return max( 0, intval( $details['position'], 10 ) );
}
return 0;
}
protected function is_url_type( $type ) {
if ( $type === 'url' || $type === 'pass' ) {
return true;
}
return false;
}
public function is_valid_redirect_code( $code ) {
return in_array( $code, array( 301, 302, 303, 304, 307, 308 ), true );
}
public function is_valid_error_code( $code ) {
return in_array( $code, array( 400, 401, 403, 404, 410, 418, 451, 500, 501, 502, 503, 504 ), true );
}
protected function get_code( $action_type, $code ) {
if ( $action_type === 'url' || $action_type === 'random' ) {
if ( $this->is_valid_redirect_code( $code ) ) {
return $code;
}
return 301;
}
if ( $action_type === 'error' ) {
if ( $this->is_valid_error_code( $code ) ) {
return $code;
}
return 404;
}
return 0;
}
protected function get_group( $group_id ) {
$group_id = intval( $group_id, 10 );
if ( ! Red_Group::get( $group_id ) ) {
return new WP_Error( 'redirect', 'Invalid group when creating redirect' );
}
return $group_id;
}
protected function get_url( $url, $regex ) {
$url = self::sanitize_url( $url, $regex );
if ( $url === '' ) {
return new WP_Error( 'redirect', 'Invalid source URL' );
}
return $url;
}
protected function auto_generate() {
$options = red_get_options();
$url = '';
if ( isset( $options['auto_target'] ) && $options['auto_target'] ) {
$id = time();
$url = str_replace( '$dec$', $id, $options['auto_target'] );
$url = str_replace( '$hex$', sprintf( '%x', $id ), $url );
}
return $url;
}
public function sanitize_url( $url, $regex = false ) {
$url = wp_kses( $url, 'strip' );
$url = str_replace( '&amp;', '&', $url );
// Make sure that the old URL is relative
$url = preg_replace( '@^https?://(.*?)/@', '/', $url );
$url = preg_replace( '@^https?://(.*?)$@', '/', $url );
// No new lines
$url = preg_replace( "/[\r\n\t].*?$/s", '', $url );
// Clean control codes
$url = preg_replace( '/[^\PC\s]/u', '', $url );
// Ensure a slash at start
if ( substr( $url, 0, 1 ) !== '/' && (bool) $regex === false ) {
$url = '/' . $url;
}
// Try and URL decode any i10n characters
$decoded = $this->remove_bad_encoding( rawurldecode( $url ) );
// Was there any invalid characters?
if ( $decoded === false ) {
// Yes. Use the url as an undecoded URL, and check for invalid characters
$decoded = $this->remove_bad_encoding( $url );
// Was there any invalid characters?
if ( $decoded === false ) {
// Yes, it's still a problem. Use the URL as-is and hope for the best
return $url;
}
}
if ( $regex ) {
$decoded = str_replace( '?&lt;!', '?<!', $decoded );
}
// Return the URL
return $decoded;
}
/**
* Remove any bad encoding, where possible
*
* @param string $text Text.
* @return string|false
*/
private function remove_bad_encoding( $text ) {
// Try and remove bad decoding
if ( function_exists( 'iconv' ) ) {
return @iconv( 'UTF-8', 'UTF-8//IGNORE', sanitize_textarea_field( $text ) );
}
return sanitize_textarea_field( $text );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
<?php
/**
* Regular expression helper
*/
class Red_Regex {
private $pattern;
private $case;
public function __construct( $pattern, $case_insensitive = false ) {
$this->pattern = rawurldecode( $pattern );
$this->case = $case_insensitive;
}
/**
* Does $target match the regex pattern, applying case insensitivity if set.
*
* Note: if the pattern is invalid it will not match
*
* @param string $target Text to match the regex against.
* @return boolean match
*/
public function is_match( $target ) {
return @preg_match( $this->get_regex(), $target, $matches ) > 0;
}
private function encode_path( $path ) {
return str_replace( ' ', '%20', $path );
}
private function encode_query( $path ) {
return str_replace( ' ', '+', $path );
}
/**
* Regex replace the current pattern with $replace_pattern, applied to $target
*
* Note: if the pattern is invalid it will return $target
*
* @param string $replace_pattern The regex replace pattern.
* @param string $target Text to match the regex against.
* @return string Replaced text
*/
public function replace( $replace_pattern, $target ) {
$regex = $this->get_regex();
$result = @preg_replace( $regex, $replace_pattern, $target );
if ( is_null( $result ) ) {
return $target;
}
// Space encode the target
$split = explode( '?', $result );
if ( count( $split ) === 2 ) {
$result = implode( '?', [ $this->encode_path( $split[0] ), $this->encode_query( $split[1] ) ] );
} else {
$result = $this->encode_path( $result );
}
return $result;
}
private function get_regex() {
$at_escaped = str_replace( '@', '\\@', $this->pattern );
$case = '';
if ( $this->is_ignore_case() ) {
$case = 'i';
}
return '@' . $at_escaped . '@s' . $case;
}
public function is_ignore_case() {
return $this->case;
}
}

View File

@@ -0,0 +1,257 @@
<?php
require_once __DIR__ . '/ip.php';
class Redirection_Request {
/**
* URL friendly sanitize_text_fields which lets encoded characters through and doesn't trim
*
* @param string $value Value.
* @return string
*/
public static function sanitize_url( $value ) {
// Remove invalid UTF
$url = wp_check_invalid_utf8( $value, true );
// No new lines
$url = preg_replace( "/[\r\n\t].*?$/s", '', $url );
// Clean control codes
$url = preg_replace( '/[^\PC\s]/u', '', $url );
return $url;
}
/**
* Get HTTP headers
*
* @return array
*/
public static function get_request_headers() {
$ignore = apply_filters( 'redirection_request_headers_ignore', [
'cookie',
'host',
] );
$headers = [];
foreach ( $_SERVER as $name => $value ) {
$value = sanitize_text_field( $value );
$name = sanitize_text_field( $name );
if ( substr( $name, 0, 5 ) === 'HTTP_' ) {
$name = strtolower( substr( $name, 5 ) );
$name = str_replace( '_', ' ', $name );
$name = ucwords( $name );
$name = str_replace( ' ', '-', $name );
if ( ! in_array( strtolower( $name ), $ignore, true ) ) {
$headers[ $name ] = $value;
}
}
}
return apply_filters( 'redirection_request_headers', $headers );
}
/**
* Get request method
*
* @return string
*/
public static function get_request_method() {
$method = '';
if ( isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ) {
$method = sanitize_text_field( $_SERVER['REQUEST_METHOD'] );
}
return apply_filters( 'redirection_request_method', $method );
}
/**
* Get the server name (from $_SERVER['SERVER_NAME]), or use the request name ($_SERVER['HTTP_HOST']) if not present
*
* @return string
*/
public static function get_server_name() {
$host = self::get_request_server_name();
if ( isset( $_SERVER['SERVER_NAME'] ) && is_string( $_SERVER['SERVER_NAME'] ) ) {
$host = sanitize_text_field( $_SERVER['SERVER_NAME'] );
}
return apply_filters( 'redirection_request_server', $host );
}
/**
* Get the request server name (from $_SERVER['HTTP_HOST])
*
* @return string
*/
public static function get_request_server_name() {
$host = '';
if ( isset( $_SERVER['HTTP_HOST'] ) && is_string( $_SERVER['HTTP_HOST'] ) ) {
$host = sanitize_text_field( $_SERVER['HTTP_HOST'] );
}
$parts = explode( ':', $host );
return apply_filters( 'redirection_request_server_host', $parts[0] );
}
/**
* Get server name + protocol
*
* @return string
*/
public static function get_server() {
return self::get_protocol() . '://' . self::get_server_name();
}
/**
* Get protocol
*
* @return string
*/
public static function get_protocol() {
return is_ssl() ? 'https' : 'http';
}
/**
* Get request protocol
*
* @return string
*/
public static function get_request_url() {
$url = '';
if ( isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ) {
$url = self::sanitize_url( $_SERVER['REQUEST_URI'] );
}
return apply_filters( 'redirection_request_url', stripslashes( $url ) );
}
/**
* Get user agent
*
* @return string
*/
public static function get_user_agent() {
$agent = '';
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && is_string( $_SERVER['HTTP_USER_AGENT'] ) ) {
$agent = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] );
}
return apply_filters( 'redirection_request_agent', $agent );
}
/**
* Get referrer
*
* @return string
*/
public static function get_referrer() {
$referrer = '';
if ( isset( $_SERVER['HTTP_REFERER'] ) && is_string( $_SERVER['HTTP_REFERER'] ) ) {
$referrer = self::sanitize_url( $_SERVER['HTTP_REFERER'] );
}
return apply_filters( 'redirection_request_referrer', $referrer );
}
/**
* Get standard IP header names
*
* @return string[]
*/
public static function get_ip_headers() {
return [
'HTTP_CF_CONNECTING_IP',
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'HTTP_VIA',
'REMOTE_ADDR',
];
}
/**
* Get browser IP
*
* @return string
*/
public static function get_ip() {
$options = red_get_options();
$ip = new Redirection_IP();
// This is set by the server, but may not be the actual IP
if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
$ip = new Redirection_IP( $_SERVER['REMOTE_ADDR'] );
}
if ( in_array( $ip->get(), $options['ip_proxy'], true ) || empty( $options['ip_proxy'] ) ) {
foreach ( $options['ip_headers'] as $header ) {
if ( isset( $_SERVER[ $header ] ) ) {
$ip = new Redirection_IP( $_SERVER[ $header ] );
break;
}
}
}
return apply_filters( 'redirection_request_ip', $ip->get() );
}
/**
* Get a cookie
*
* @param string $cookie Name.
* @return string|false
*/
public static function get_cookie( $cookie ) {
if ( isset( $_COOKIE[ $cookie ] ) && is_string( $_COOKIE[ $cookie ] ) ) {
return apply_filters( 'redirection_request_cookie', sanitize_text_field( $_COOKIE[ $cookie ] ), $cookie );
}
return false;
}
/**
* Get a HTTP header
*
* @param string $name Header name.
* @return string|false
*/
public static function get_header( $name ) {
$name = 'HTTP_' . strtoupper( $name );
$name = str_replace( '-', '_', $name );
if ( isset( $_SERVER[ $name ] ) && is_string( $_SERVER[ $name ] ) ) {
return apply_filters( 'redirection_request_header', sanitize_text_field( $_SERVER[ $name ] ), $name );
}
return false;
}
/**
* Get browser accept language
*
* @return string[]
*/
public static function get_accept_language() {
if ( isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) && is_string( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) {
$languages = preg_replace( '/;.*$/', '', sanitize_text_field( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) );
$languages = str_replace( ' ', '', $languages );
return apply_filters( 'redirection_request_accept_language', explode( ',', $languages ) );
}
return [];
}
}

View File

@@ -0,0 +1,126 @@
<?php
class Red_Url_Encode {
/**
* URL
*
* @var string
*/
private $url;
/**
* Is regex?
*
* @var boolean
*/
private $is_regex;
/**
* Constructor
*
* @param string $url URL.
* @param boolean $is_regex Is Regex.
*/
public function __construct( $url, $is_regex = false ) {
// Remove any newlines
$url = preg_replace( "/[\r\n\t].*?$/s", '', $url );
// Remove invalid characters
$url = preg_replace( '/[^\PC\s]/u', '', $url );
// Make sure spaces are quoted
$url = str_replace( ' ', '%20', $url );
$url = str_replace( '%24', '$', $url );
$this->url = $url;
$this->is_regex = $is_regex;
}
/**
* URL encode some things, but other things can be passed through
*
* @return string
*/
public function get_as_target() {
$allowed = [
'%2F' => '/',
'%3F' => '?',
'%3A' => ':',
'%3D' => '=',
'%26' => '&',
'%25' => '%',
'+' => '%20',
'%24' => '$',
'%23' => '#',
];
$url = rawurlencode( $this->url );
$url = $this->replace_encoding( $url, $allowed );
return $this->encode_regex( $url );
}
/**
* Encode a URL
*
* @return string
*/
public function get_as_source() {
$allowed = [
'%2F' => '/',
'%3F' => '?',
'+' => '%20',
'.' => '\\.',
];
$url = $this->replace_encoding( rawurlencode( $this->url ), $allowed );
return $this->encode_regex( $url );
}
/**
* Replace encoded characters in a URL
*
* @param string $str Source string.
* @param array $allowed Allowed encodings.
* @return string
*/
private function replace_encoding( $str, $allowed ) {
foreach ( $allowed as $before => $after ) {
$str = str_replace( $before, $after, $str );
}
return $str;
}
/**
* Encode a regex URL
*
* @param string $url URL.
* @return string
*/
private function encode_regex( $url ) {
if ( $this->is_regex ) {
// No leading slash
$url = ltrim( $url, '/' );
// If pattern has a ^ at the start then ensure we don't have a slash immediatley after
$url = preg_replace( '@^\^/@', '^', $url );
$url = $this->replace_encoding( $url, [
'%2A' => '*',
'%3F' => '?',
'%28' => '(',
'%29' => ')',
'%5B' => '[',
'%5C' => ']',
'%24' => '$',
'%2B' => '+',
'%7C' => '|',
'\\.' => '.',
] );
}
return $url;
}
}

View File

@@ -0,0 +1,229 @@
<?php
/**
* Represent URL source flags.
*/
class Red_Source_Flags {
const QUERY_IGNORE = 'ignore';
const QUERY_EXACT = 'exact';
const QUERY_PASS = 'pass';
const QUERY_EXACT_ORDER = 'exactorder';
const FLAG_QUERY = 'flag_query';
const FLAG_CASE = 'flag_case';
const FLAG_TRAILING = 'flag_trailing';
const FLAG_REGEX = 'flag_regex';
/**
* Case insensitive matching
*
* @var boolean
*/
private $flag_case = false;
/**
* Ignored trailing slashes
*
* @var boolean
*/
private $flag_trailing = false;
/**
* Regular expression
*
* @var boolean
*/
private $flag_regex = false;
/**
* Query parameter matching
*
* @var self::QUERY_EXACT|self::QUERY_IGNORE|self::QUERY_PASS|self::QUERY_EXACT_ORDER
*/
private $flag_query = self::QUERY_EXACT;
/**
* Values that have been set
*
* @var array
*/
private $values_set = [];
/**
* Constructor
*
* @param array|null $json JSON object.
*/
public function __construct( $json = null ) {
if ( $json !== null ) {
$this->set_flags( $json );
}
}
/**
* Get list of valid query types as an array
*
* @return string[]
*/
private function get_allowed_query() {
return [
self::QUERY_IGNORE,
self::QUERY_EXACT,
self::QUERY_PASS,
self::QUERY_EXACT_ORDER,
];
}
/**
* Parse flag data.
*
* @param array $json Flag data.
* @return void
*/
public function set_flags( array $json ) {
if ( isset( $json[ self::FLAG_QUERY ] ) && in_array( $json[ self::FLAG_QUERY ], $this->get_allowed_query(), true ) ) {
$this->flag_query = $json[ self::FLAG_QUERY ];
}
if ( isset( $json[ self::FLAG_CASE ] ) && is_bool( $json[ self::FLAG_CASE ] ) ) {
$this->flag_case = $json[ self::FLAG_CASE ] ? true : false;
}
if ( isset( $json[ self::FLAG_TRAILING ] ) && is_bool( $json[ self::FLAG_TRAILING ] ) ) {
$this->flag_trailing = $json[ self::FLAG_TRAILING ] ? true : false;
}
if ( isset( $json[ self::FLAG_REGEX ] ) && is_bool( $json[ self::FLAG_REGEX ] ) ) {
$this->flag_regex = $json[ self::FLAG_REGEX ] ? true : false;
if ( $this->flag_regex ) {
// Regex auto-disables other things
$this->flag_query = self::QUERY_EXACT;
}
}
// Keep track of what values have been set, so we know what to override with defaults later
$this->values_set = array_intersect( array_keys( $json ), array_keys( $this->get_json() ) );
}
/**
* Return `true` if ignore trailing slash, `false` otherwise
*
* @return boolean
*/
public function is_ignore_trailing() {
return $this->flag_trailing;
}
/**
* Return `true` if ignore case, `false` otherwise
*
* @return boolean
*/
public function is_ignore_case() {
return $this->flag_case;
}
/**
* Return `true` if ignore trailing slash, `false` otherwise
*
* @return boolean
*/
public function is_regex() {
return $this->flag_regex;
}
/**
* Return `true` if exact query match, `false` otherwise
*
* @return boolean
*/
public function is_query_exact() {
return $this->flag_query === self::QUERY_EXACT;
}
/**
* Return `true` if exact query match in set order, `false` otherwise
*
* @return boolean
*/
public function is_query_exact_order() {
return $this->flag_query === self::QUERY_EXACT_ORDER;
}
/**
* Return `true` if ignore query params, `false` otherwise
*
* @return boolean
*/
public function is_query_ignore() {
return $this->flag_query === self::QUERY_IGNORE;
}
/**
* Return `true` if ignore and pass query params, `false` otherwise
*
* @return boolean
*/
public function is_query_pass() {
return $this->flag_query === self::QUERY_PASS;
}
/**
* Return the flags as a JSON object
*
* @return array
*/
public function get_json() {
return [
self::FLAG_QUERY => $this->flag_query,
self::FLAG_CASE => $this->is_ignore_case(),
self::FLAG_TRAILING => $this->is_ignore_trailing(),
self::FLAG_REGEX => $this->is_regex(),
];
}
/**
* Return flag data, with defaults removed from the data.
*
* @param array $defaults Defaults to remove.
* @return array
*/
public function get_json_without_defaults( $defaults ) {
$json = $this->get_json();
if ( count( $defaults ) > 0 ) {
foreach ( $json as $key => $value ) {
if ( isset( $defaults[ $key ] ) && $value === $defaults[ $key ] ) {
unset( $json[ $key ] );
}
}
}
return $json;
}
/**
* Return flag data, with defaults filling in any gaps not set.
*
* @return array
*/
public function get_json_with_defaults() {
$settings = red_get_options();
$json = $this->get_json();
$defaults = [
self::FLAG_QUERY => $settings[ self::FLAG_QUERY ],
self::FLAG_CASE => $settings[ self::FLAG_CASE ],
self::FLAG_TRAILING => $settings[ self::FLAG_TRAILING ],
self::FLAG_REGEX => $settings[ self::FLAG_REGEX ],
];
foreach ( $this->values_set as $key ) {
if ( ! isset( $json[ $key ] ) ) {
$json[ $key ] = $defaults[ $key ];
}
}
return $json;
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* Get a URL suitable for matching in the database
*/
class Red_Url_Match {
/**
* URL
*
* @var String
*/
private $url;
/**
* Constructor
*
* @param string $url The URL to match.
*/
public function __construct( $url ) {
$this->url = $url;
}
/**
* Get the plain 'matched' URL:
*
* - Lowercase
* - No trailing slashes
*
* @return string URL
*/
public function get_url() {
// Remove query params, and decode any encoded characters
$url = new Red_Url_Path( $this->url );
$path = $url->get_without_trailing_slash();
// URL encode
$decode = [
'/',
':',
'[',
']',
'@',
'~',
',',
'(',
')',
';',
];
// URL encode everything - this converts any i10n to the proper encoding
$path = rawurlencode( $path );
// We also converted things we dont want encoding, such as a /. Change these back
foreach ( $decode as $char ) {
$path = str_replace( rawurlencode( $char ), $char, $path );
}
// Lowercase everything
$path = Red_Url_Path::to_lower( $path );
return $path ? $path : '/';
}
/**
* Get the URL with parameters re-ordered into alphabetical order
*
* @return string
*/
public function get_url_with_params() {
$query = new Red_Url_Query( $this->url, new Red_Source_Flags( [ Red_Source_Flags::FLAG_CASE => true ] ) );
return $query->get_url_with_query( $this->get_url() );
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* The path part of a URL
*/
class Red_Url_Path {
/**
* URL path
*
* @var String
*/
private $path;
/**
* Constructor
*
* @param string $path URL.
*/
public function __construct( $path ) {
$this->path = $this->get_path_component( $path );
}
/**
* Is the supplied `url` a match for this object?
*
* @param string $url URL to match against.
* @param Red_Source_Flags $flags Source flags to use in match.
* @return boolean
*/
public function is_match( $url, Red_Source_Flags $flags ) {
$target = new Red_Url_Path( $url );
$target_path = $target->get();
$source_path = $this->get();
if ( $flags->is_ignore_trailing() ) {
// Ignore trailing slashes
$source_path = $this->get_without_trailing_slash();
$target_path = $target->get_without_trailing_slash();
}
if ( $flags->is_ignore_case() ) {
// Case insensitive match
$source_path = self::to_lower( $source_path );
$target_path = self::to_lower( $target_path );
}
return $target_path === $source_path;
}
/**
* Convert a URL to lowercase
*
* @param string $url URL.
* @return string
*/
public static function to_lower( $url ) {
if ( function_exists( 'mb_strtolower' ) ) {
return mb_strtolower( $url, 'UTF-8' );
}
return strtolower( $url );
}
/**
* Get the path value
*
* @return string
*/
public function get() {
return $this->path;
}
/**
* Get the path value without trailing slash, or `/` if home
*
* @return string
*/
public function get_without_trailing_slash() {
// Return / or // as-is
if ( $this->path === '/' ) {
return $this->path;
}
// Anything else remove the last /
return preg_replace( '@/$@', '', $this->get() );
}
/**
* `parse_url` doesn't handle 'incorrect' URLs, such as those with double slashes
* These are often used in redirects, so we fall back to our own parsing
*
* @param string $url URL.
* @return string
*/
private function get_path_component( $url ) {
$path = $url;
if ( preg_match( '@^https?://@', $url, $matches ) > 0 ) {
$parts = explode( '://', $url );
if ( count( $parts ) > 1 ) {
$rest = explode( '/', $parts[1] );
$path = '/' . implode( '/', array_slice( $rest, 1 ) );
}
}
return urldecode( $this->get_query_before( $path ) );
}
/**
* Get the path component up to the query string
*
* @param string $url URL.
* @return string
*/
private function get_query_before( $url ) {
$qpos = strpos( $url, '?' );
$qrpos = strpos( $url, '\\?' );
// Have we found an escaped query and it occurs before a normal query?
if ( $qrpos !== false && $qrpos < $qpos ) {
// Yes, the path is everything up to the escaped query
return substr( $url, 0, $qrpos );
}
// No query - return everything as path
if ( $qpos === false ) {
return $url;
}
// Query found - return everything up to it
return substr( $url, 0, $qpos );
}
}

View File

@@ -0,0 +1,422 @@
<?php
/**
* Query parameter martching
*/
class Red_Url_Query {
/**
* @type Integer
*/
const RECURSION_LIMIT = 10;
/**
* Original query parameters (used when passing)
*
* @var array
*/
private $original_query = [];
/**
* Match query parameters (used only for matching, and maybe be lowercased)
*
* @var array
*/
private $match_query = [];
/**
* Is this an exact match?
*
* @var boolean|string
*/
private $match_exact = false;
/**
* Constructor
*
* @param string $url URL.
* @param Red_Source_Flags $flags URL flags.
*/
public function __construct( $url, $flags ) {
$this->original_query = $this->get_url_query( $url );
$this->match_query = $this->original_query;
if ( $flags->is_ignore_case() ) {
$this->match_query = $this->get_url_query( Red_Url_Path::to_lower( $url ) );
}
}
/**
* Does this object match the URL?
*
* @param string $url URL to match.
* @param Red_Source_Flags $flags Source flags.
* @return boolean
*/
public function is_match( $url, Red_Source_Flags $flags ) {
if ( $flags->is_ignore_case() ) {
$url = Red_Url_Path::to_lower( $url );
}
// If we can't parse the query params then match the params exactly
if ( $this->match_exact !== false ) {
return $this->is_string_match( $this->get_query_after( $url ), $this->match_exact, $flags->is_ignore_case() );
}
$target = $this->get_url_query( $url );
// All params in the source have to exist in the request, but in any order
$matched = $this->get_query_same( $this->match_query, $target, $flags->is_ignore_case() );
if ( count( $matched ) !== count( $this->match_query ) ) {
// Source params arent matched exactly
return false;
};
// Get list of whatever is left over
$query_diff = $this->get_query_diff( $this->match_query, $target );
$query_diff = array_merge( $query_diff, $this->get_query_diff( $target, $this->match_query ) );
if ( $flags->is_query_ignore() || $flags->is_query_pass() ) {
return true; // This ignores all other query params
}
// In an exact match there shouldn't be any more params
return count( $query_diff ) === 0;
}
/**
* Return true if the two strings match, false otherwise. Pays attention to case sensitivity
*
* @param string $first First string.
* @param string $second Second string.
* @param boolean $case Case sensitivity.
* @return boolean
*/
private function is_string_match( $first, $second, $case ) {
if ( $case ) {
return Red_Url_Path::to_lower( $first ) === Red_Url_Path::to_lower( $second );
}
return $first === $second;
}
/**
* Pass query params from one URL to another URL, ignoring any params that already exist on the target.
*
* @param string $target_url The target URL to add params to.
* @param string $requested_url The source URL to pass params from.
* @param Red_Source_Flags $flags Any URL flags.
* @return string URL, modified or not.
*/
public static function add_to_target( $target_url, $requested_url, Red_Source_Flags $flags ) {
if ( $flags->is_query_pass() && $target_url ) {
$source_query = new Red_Url_Query( $target_url, $flags );
$request_query = new Red_Url_Query( $requested_url, $flags );
// Now add any remaining params
$query_diff = $source_query->get_query_diff( $source_query->original_query, $request_query->original_query );
$request_diff = $request_query->get_query_diff( $request_query->original_query, $source_query->original_query );
foreach ( $request_diff as $key => $value ) {
$query_diff[ $key ] = $value;
}
// Remove any params from $source that are present in $request - we dont allow
// predefined params to be overridden
foreach ( array_keys( $query_diff ) as $key ) {
if ( isset( $source_query->original_query[ $key ] ) ) {
unset( $query_diff[ $key ] );
}
}
return self::build_url( $target_url, $query_diff );
}
return $target_url;
}
/**
* Build a URL from a base and query parameters
*
* @param string $url Base URL.
* @param Array $query_array Query parameters.
* @return string
*/
public static function build_url( $url, $query_array ) {
$query = http_build_query( array_map( function( $value ) {
if ( $value === null ) {
return '';
}
return $value;
}, $query_array ) );
$query = preg_replace( '@%5B\d*%5D@', '[]', $query ); // Make these look like []
foreach ( $query_array as $key => $value ) {
if ( $value === null ) {
$search = str_replace( '%20', '+', rawurlencode( $key ) . '=' );
$replace = str_replace( '%20', '+', rawurlencode( $key ) );
$query = str_replace( $search, $replace, $query );
}
}
$query = str_replace( '%252B', '+', $query );
if ( $query ) {
// Get any fragment
$target_fragment = wp_parse_url( $url, PHP_URL_FRAGMENT );
// If we have a fragment we need to ensure it comes after the query parameters, not before
if ( $target_fragment ) {
// Remove fragment
$url = str_replace( '#' . $target_fragment, '', $url );
// Add to the end of the query
$query .= '#' . $target_fragment;
}
return $url . ( strpos( $url, '?' ) === false ? '?' : '&' ) . $query;
}
return $url;
}
/**
* Get a URL with the given base and query parameters from this Url_Query
*
* @param string $url Base URL.
* @return string
*/
public function get_url_with_query( $url ) {
return self::build_url( $url, $this->original_query );
}
/**
* Get the query parameters
*
* @return array
*/
public function get() {
return $this->original_query;
}
/**
* Does the URL and the query params contain no parameters?
*
* @param string $url URL.
* @param Array $params Query params.
* @return boolean
*/
private function is_exact_match( $url, $params ) {
// No parsed query params but we have query params on the URL - some parsing error with wp_parse_str
if ( count( $params ) === 0 && $this->has_query_params( $url ) ) {
return true;
}
return false;
}
/**
* Get query parameters from a URL
*
* @param string $url URL.
* @return array
*/
private function get_url_query( $url ) {
$params = [];
$query = $this->get_query_after( $url );
$internal = $this->parse_str( $query );
wp_parse_str( $query ? $query : '', $params );
// For exactness and due to the way parse_str works we go through and check any query param without a value
foreach ( $params as $key => $value ) {
if ( is_string( $value ) && strlen( $value ) === 0 && strpos( $url, $key . '=' ) === false ) {
$params[ $key ] = null;
}
}
// A work-around until we replace parse_str with internal function
foreach ( $internal as $pos => $internal_param ) {
if ( $internal_param['parse_str'] !== $internal_param['name'] ) {
foreach ( $params as $key => $value ) {
if ( $key === $internal_param['parse_str'] ) {
unset( $params[ $key ] );
unset( $internal[ $pos ] );
$params[ $internal_param['name'] ] = $value;
}
}
}
}
if ( $this->is_exact_match( $url, $params ) ) {
$this->match_exact = $query;
}
return $params;
}
/**
* A replacement for parse_str, which behaves oddly in some situations (spaces and no param value)
*
* TODO: use this in preference to parse_str
*
* @param string $query Query.
* @return string
*/
private function parse_str( $query ) {
$params = [];
if ( strlen( $query ) === 0 ) {
return $params;
}
$parts = explode( '&', $query ? $query : '' );
foreach ( $parts as $part ) {
$param = explode( '=', $part );
$parse_str = [];
wp_parse_str( $part, $parse_str );
$params[] = [
'name' => str_replace( [ '[', ']', '%5B', '%5D' ], '', str_replace( '+', ' ', $param[0] ) ),
'value' => isset( $param[1] ) ? str_replace( '+', ' ', $param[1] ) : null,
'parse_str' => implode( '', array_keys( $parse_str ) ),
];
}
return $params;
}
/**
* Does the URL contain query parameters?
*
* @param string $url URL.
* @return boolean
*/
public function has_query_params( $url ) {
$qpos = strpos( $url, '?' );
if ( $qpos === false ) {
return false;
}
return true;
}
/**
* Get parameters after the ?
*
* @param string $url URL.
* @return string
*/
public function get_query_after( $url ) {
$qpos = strpos( $url, '?' );
$qrpos = strpos( $url, '\\?' );
// No ? anywhere - no query
if ( $qpos === false ) {
return '';
}
// Found an escaped ? and it comes before the non-escaped ?
if ( $qrpos !== false && $qrpos < $qpos ) {
return substr( $url, $qrpos + 2 );
}
// Standard query param
return substr( $url, $qpos + 1 );
}
private function get_query_case( array $query ) {
$keys = [];
foreach ( array_keys( $query ) as $key ) {
$keys[ Red_Url_Path::to_lower( $key ) ] = $key;
}
return $keys;
}
/**
* Get query parameters that are the same in both query arrays
*
* @param array $source_query Source query params.
* @param array $target_query Target query params.
* @param bool $is_ignore_case Ignore case.
* @param integer $depth Current recursion depth.
* @return array
*/
public function get_query_same( array $source_query, array $target_query, $is_ignore_case, $depth = 0 ) {
if ( $depth > self::RECURSION_LIMIT ) {
return [];
}
$source_keys = $this->get_query_case( $source_query );
$target_keys = $this->get_query_case( $target_query );
$same = [];
foreach ( $source_keys as $key => $original_key ) {
// Does the key exist in the target
if ( isset( $target_keys[ $key ] ) ) {
// Key exists. Now match the value
$source_value = $source_query[ $original_key ];
$target_value = $target_query[ $target_keys[ $key ] ];
$add = false;
if ( is_array( $source_value ) && is_array( $target_value ) ) {
$add = $this->get_query_same( $source_value, $target_value, $is_ignore_case, $depth + 1 );
if ( count( $add ) !== count( $source_value ) ) {
$add = false;
}
} elseif ( is_string( $source_value ) && is_string( $target_value ) ) {
$add = $this->is_string_match( $source_value, $target_value, $is_ignore_case ) ? $source_value : false;
} elseif ( $source_value === null && $target_value === null ) {
$add = null;
}
if ( ! empty( $add ) || is_numeric( $add ) || $add === '' || $add === null ) {
$same[ $original_key ] = $add;
}
}
}
return $same;
}
/**
* Get the difference in query parameters
*
* @param array $source_query Source query params.
* @param array $target_query Target query params.
* @param integer $depth Current recursion depth.
* @return array
*/
public function get_query_diff( array $source_query, array $target_query, $depth = 0 ) {
if ( $depth > self::RECURSION_LIMIT ) {
return [];
}
$diff = [];
foreach ( $source_query as $key => $value ) {
if ( array_key_exists( $key, $target_query ) && is_array( $value ) && is_array( $target_query[ $key ] ) ) {
$add = $this->get_query_diff( $source_query[ $key ], $target_query[ $key ], $depth + 1 );
if ( ! empty( $add ) ) {
$diff[ $key ] = $add;
}
} elseif ( ! array_key_exists( $key, $target_query ) || ! $this->is_value( $value ) || ! $this->is_value( $target_query[ $key ] ) || $target_query[ $key ] !== $source_query[ $key ] ) {
$diff[ $key ] = $value;
}
}
return $diff;
}
private function is_value( $value ) {
return is_string( $value ) || $value === null;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Decode request URLs
*/
class Red_Url_Request {
/**
* Original URL
*
* @var String
*/
private $original_url;
/**
* Decoded URL
*
* @var String
*/
private $decoded_url;
/**
* Constructor
*
* @param string $url URL.
*/
public function __construct( $url ) {
$this->original_url = apply_filters( 'redirection_url_source', $url );
$this->decoded_url = rawurldecode( $this->original_url );
// Replace the decoded query params with the original ones
$this->original_url = $this->replace_query_params( $this->original_url, $this->decoded_url );
}
/**
* Take the decoded path part, but keep the original query params. This ensures any redirects keep the encoding.
*
* @param string $original_url Original unencoded URL.
* @param string $decoded_url Decoded URL.
* @return string
*/
private function replace_query_params( $original_url, $decoded_url ) {
$decoded = explode( '?', $decoded_url );
if ( count( $decoded ) > 1 ) {
$original = explode( '?', $original_url );
if ( count( $original ) > 1 ) {
return $decoded[0] . '?' . $original[1];
}
}
return $decoded_url;
}
/**
* Get the original URL
*
* @return string
*/
public function get_original_url() {
return $this->original_url;
}
/**
* Get the decoded URL
*
* @return string
*/
public function get_decoded_url() {
return $this->decoded_url;
}
/**
* Is this a valid URL?
*
* @return boolean
*/
public function is_valid() {
return strlen( $this->get_decoded_url() ) > 0;
}
/**
* Protect certain URLs from being redirected. Note we don't need to protect wp-admin, as this code doesn't run there
*
* @return boolean
*/
public function is_protected_url() {
$rest = wp_parse_url( red_get_rest_api() );
$rest_api = $rest['path'] . ( isset( $rest['query'] ) ? '?' . $rest['query'] : '' );
if ( substr( $this->get_decoded_url(), 0, strlen( $rest_api ) ) === $rest_api ) {
// Never redirect the REST API
return true;
}
return false;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* Transform URL shortcodes
*/
class Red_Url_Transform {
/**
* Replace special tags in the target URL.
*
* From the distant Redirection past. Undecided whether to keep
*
* @param string $url Target URL.
* @return string
*/
public function transform( $url ) {
// Deprecated number post ID
if ( is_numeric( $url ) ) {
$permalink = get_permalink( intval( $url, 10 ) );
if ( $permalink ) {
return $permalink;
}
}
global $shortcode_tags;
$shortcode_copy = array_merge( [], $shortcode_tags );
remove_all_shortcodes();
$shortcodes = apply_filters( 'redirection_shortcodes', [
'userid',
'userlogin',
'unixtime', // Also replaces $dec$
// These require content
'md5',
'upper',
'lower',
'dashes',
'underscores',
] );
foreach ( $shortcodes as $code ) {
add_shortcode( $code, [ $this, 'do_shortcode' ] );
}
// Support deprecated tags
$url = $this->transform_deprecated( $url );
$url = do_shortcode( $url );
// Restore shortcodes
// phpcs:ignore
$shortcode_tags = array_merge( [], $shortcode_copy );
return $url;
}
/**
* Peform a shortcode
*
* @param array $attrs Shortcode attributes.
* @param string $content Shortcode content.
* @param string $tag Shortcode tag.
* @return string
*/
public function do_shortcode( $attrs, $content, $tag ) {
$user = wp_get_current_user();
switch ( $tag ) {
case 'userid':
return (string) $user->ID;
case 'userlogin':
return $user->ID > 0 ? $user->user_login : '';
case 'unixtime':
return (string) time();
case 'md5':
return md5( $content );
case 'upper':
return strtoupper( $content );
case 'lower':
return strtolower( $content );
case 'dashes':
return str_replace( [ '_', ' ' ], '-', $content );
case 'underscores':
return str_replace( [ '-', ' ' ], '_', $content );
}
return apply_filters( 'redirection_url_transform', '', $tag, $attrs, $content );
}
/**
* Convert deprecated inline tags to shortcodes.
*
* @param string $url URL.
* @return string
*/
private function transform_deprecated( $url ) {
$url = str_replace( '%userid%', '[userid]', $url );
$url = str_replace( '%userlogin%', '[userlogin]', $url );
$url = str_replace( '%userurl%', '[userurl]', $url );
return $url;
}
}

View File

@@ -0,0 +1,57 @@
<?php
require_once __DIR__ . '/url-query.php';
require_once __DIR__ . '/url-path.php';
require_once __DIR__ . '/url-match.php';
require_once __DIR__ . '/url-flags.php';
require_once __DIR__ . '/url-request.php';
require_once __DIR__ . '/url-transform.php';
require_once __DIR__ . '/url-encode.php';
class Red_Url {
/**
* URL
*
* @var String
*/
private $url;
/**
* Constructor
*
* @param string $url URL.
*/
public function __construct( $url = '' ) {
$this->url = $url;
$this->url = str_replace( ' ', '%20', $this->url ); // deprecated
}
/**
* Get the raw URL
*
* @return string URL
*/
public function get_url() {
return $this->url;
}
/**
* Match a target URL against the current URL, using any match flags
*
* @param string $requested_url Target URL.
* @param Red_Source_Flags $flags Match flags.
* @return boolean
*/
public function is_match( $requested_url, Red_Source_Flags $flags ) {
if ( $flags->is_regex() ) {
$regex = new Red_Regex( $this->url, $flags->is_ignore_case() );
return $regex->is_match( $requested_url );
}
$path = new Red_Url_Path( $this->url );
$query = new Red_Url_Query( $this->url, $flags );
return $path->is_match( $requested_url, $flags ) && $query->is_match( $requested_url, $flags );
}
}