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,61 @@
<?php
/**
* Return an error to the client, and trigger the WordPress error page
*/
class Error_Action extends Red_Action {
/**
* Set WordPress to show the error page
*
* @return void
*/
public function run() {
wp_reset_query();
// Set the query to be a 404
set_query_var( 'is_404', true );
// Return the 404 page
add_filter( 'template_include', [ $this, 'template_include' ] );
// Clear any posts if this is actually a valid URL
add_filter( 'pre_handle_404', [ $this, 'pre_handle_404' ] );
// Ensure the appropriate http code is returned
add_action( 'wp', [ $this, 'wp' ] );
}
/**
* Output selected HTTP code, as well as redirection header
*
* @return void
*/
public function wp() {
status_header( $this->code );
nocache_headers();
global $wp_version;
if ( version_compare( $wp_version, '5.1', '<' ) ) {
header( 'X-Redirect-Agent: redirection' );
} else {
header( 'X-Redirect-By: redirection' );
}
}
public function pre_handle_404() {
global $wp_query;
// Page comments plugin interferes with this
$wp_query->posts = [];
return false;
}
public function template_include() {
return get_404_template();
}
public function name() {
return __( 'Error (404)', 'redirection' );
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* The 'do nothing' action. This really does nothing, and is used to short-circuit Redirection so that it doesn't trigger other redirects.
*/
class Nothing_Action extends Red_Action {
/**
* Issue an action when nothing happens. This stops further processing.
*
* @return void
*/
public function run() {
do_action( 'redirection_do_nothing', $this->get_target() );
}
public function name() {
return __( 'Do nothing (ignore)', 'redirection' );
}
}

View File

@@ -0,0 +1,77 @@
<?php
require_once dirname( __FILE__ ) . '/url.php';
/**
* A 'pass through' action. Matches a rewrite rather than a redirect, and uses PHP to fetch data from a remote URL.
*/
class Pass_Action extends Url_Action {
/**
* Process an external passthrough - a URL that lives external to this server.
*
* @param string $url Target URL.
* @return void
*/
public function process_external( $url ) {
// This is entirely at the user's risk. The $url is set by the user
// phpcs:ignore
echo wp_remote_fopen( $url );
}
/**
* Process an internal passthrough - a URL that lives on the same server. Here we change the request URI and continue without making a remote request.
*
* @param string $target Target URL.
* @return void
*/
public function process_internal( $target ) {
// Another URL on the server
$pos = strpos( $target, '?' );
$_SERVER['REQUEST_URI'] = $target;
$_SERVER['PATH_INFO'] = $target;
if ( $pos ) {
$_SERVER['QUERY_STRING'] = substr( $target, $pos + 1 );
$_SERVER['PATH_INFO'] = $target;
// Take the query params in the target and make them the params for this request
parse_str( $_SERVER['QUERY_STRING'], $_GET );
}
}
/**
* Is a URL external?
*
* @param string $target URL to test.
* @return boolean
*/
public function is_external( $target ) {
return substr( $target, 0, 7 ) === 'http://' || substr( $target, 0, 8 ) === 'https://';
}
/**
* Pass the data from the target
*
* @return void
*/
public function run() {
// External target
$target = $this->get_target();
if ( $target === null ) {
return;
}
if ( $this->is_external( $target ) ) {
// Pass on to an external request, echo the results, and then stop
$this->process_external( $target );
exit();
}
// Change the request and carry on
$this->process_internal( $target );
}
public function name() {
return __( 'Pass-through', 'redirection' );
}
}

View File

@@ -0,0 +1,50 @@
<?php
require_once dirname( __FILE__ ) . '/url.php';
/**
* URL action - redirect to a URL
*/
class Random_Action extends Url_Action {
/**
* Get a random URL
*
* @return string|null
*/
private function get_random_url() {
// Pick a random WordPress page
global $wpdb;
$id = $wpdb->get_var( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_status='publish' AND post_password='' AND post_type='post' ORDER BY RAND() LIMIT 0,1" );
if ( $id ) {
$url = get_permalink( $id );
if ( $url ) {
return $url;
}
}
return null;
}
/**
* Run this action. May not return from this function.
*
* @return void
*/
public function run() {
$target = $this->get_random_url();
if ( $target ) {
$this->redirect_to( $target );
}
}
public function needs_target() {
return false;
}
public function name() {
return __( 'Redirect to random post', 'redirection' );
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* URL action - redirect to a URL
*/
class Url_Action extends Red_Action {
/**
* Redirect to a URL
*
* @param string $target Target URL.
* @return void
*/
protected function redirect_to( $target ) {
// This is a known redirect, possibly extenal
// phpcs:ignore
$redirect = wp_redirect( $target, $this->get_code(), 'redirection' );
if ( $redirect ) {
/** @psalm-suppress InvalidGlobal */
global $wp_version;
if ( version_compare( $wp_version, '5.1', '<' ) ) {
header( 'X-Redirect-Agent: redirection' );
}
die();
}
}
/**
* Run this action. May not return from this function.
*
* @return void
*/
public function run() {
$target = $this->get_target();
if ( $target !== null ) {
$this->redirect_to( $target );
}
}
/**
* Does this action need a target?
*
* @return boolean
*/
public function needs_target() {
return true;
}
public function name() {
return __( 'Redirect to URL', 'redirection' );
}
}

View File

@@ -0,0 +1,199 @@
<?php
/**
* @api {get} /redirection/v1/404 Get 404 logs
* @apiName GetLogs
* @apiDescription Get a paged list of 404 logs after applying a set of filters and result ordering.
* @apiGroup 404
*
* @apiUse 404QueryParams
*
* @apiUse 404List
* @apiUse 401Error
* @apiUse 404Error
*/
/**
* @api {post} /redirection/v1/bulk/404/:type Bulk action
* @apiName BulkAction
* @apiDescription Delete 404 logs by ID
* @apiGroup 404
*
* @apiParam (URL) {String="delete"} :type Type of bulk action that is applied to every log ID.
*
* @apiParam (Query Parameter) {String[]} [items] Array of group IDs to perform the action on
* @apiParam (Query Parameter) {Boolean=false} [global] Perform action globally using the filter parameters
* @apiUse 404QueryParams
*
* @apiUse 404List
* @apiUse 401Error
* @apiUse 404Error
* @apiUse 400MissingError
*/
/**
* @apiDefine 404QueryParams 404 log query parameters
*
* @apiParam (Query Parameter) {String} [filterBy[ip]] Filter the results by the supplied IP
* @apiParam (Query Parameter) {String} [filterBy[url]] Filter the results by the supplied URL
* @apiParam (Query Parameter) {String} [filterBy[url-]exact] Filter the results by the exact URL (not a substring match, as per `url`)
* @apiParam (Query Parameter) {String} [filterBy[referrer]] Filter the results by the supplied referrer
* @apiParam (Query Parameter) {String} [filterBy[agent]] Filter the results by the supplied user agent
* @apiParam (Query Parameter) {String} [filterBy[target]] Filter the results by the supplied redirect target
* @apiParam (Query Parameter) {String} [filterBy[domain]] Filter the results by the supplied domain name
* @apiParam (Query Parameter) {String="head","get","post"} [filterBy[method]] Filter the results by the supplied HTTP request method
* @apiParam (Query Parameter) {Integer} [filterBy[http]] Filter the results by the supplied redirect HTTP code
* @apiParam (Query Parameter) {string="ip","url"} [orderby] Order by IP or URL
* @apiParam (Query Parameter) {String="asc","desc"} [direction] Direction to order the results by (ascending or descending)
* @apiParam (Query Parameter) {Integer{1...200}} [per_page=25] Number of results per request
* @apiParam (Query Parameter) {Integer} [page=0] Current page of results
* @apiParam (Query Parameter) {String="ip","url"} [groupBy] Group by IP or URL
*/
/**
* @apiDefine 404List
*
* @apiSuccess {Object[]} items Array of 404 log objects
* @apiSuccess {Integer} items.id ID of 404 log entry
* @apiSuccess {String} items.created Date the 404 log entry was recorded
* @apiSuccess {Integer} items.created_time Unix time value for `created`
* @apiSuccess {Integer} items.url The requested URL that caused the 404 log entry
* @apiSuccess {String} items.agent User agent of the client initiating the request
* @apiSuccess {Integer} items.referrer Referrer of the client initiating the request
* @apiSuccess {Integer} total Number of items
*
* @apiSuccessExample {json} Success 200:
* HTTP/1.1 200 OK
* {
* "items": [
* {
* "id": 3,
* "created": "2019-01-01 12:12:00,
* "created_time": "12345678",
* "url": "/the-url",
* "agent": "FancyBrowser",
* "referrer": "http://site.com/previous/,
* }
* ],
* "total": 1
* }
*/
/**
* 404 API endpoint
*/
class Redirection_Api_404 extends Redirection_Api_Filter_Route {
/**
* 404 API endpoint constructor
*
* @param string $namespace Namespace.
*/
public function __construct( $namespace ) {
$orders = [ 'url', 'ip', 'total', 'count', '' ];
$filters = [ 'ip', 'url-exact', 'referrer', 'agent', 'url', 'domain', 'method', 'http' ];
register_rest_route( $namespace, '/404', array(
'args' => $this->get_filter_args( $orders, $filters ),
$this->get_route( WP_REST_Server::READABLE, 'route_404', [ $this, 'permission_callback_manage' ] ),
) );
register_rest_route( $namespace, '/bulk/404/(?P<bulk>delete)', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_bulk', [ $this, 'permission_callback_delete' ] ),
'args' => array_merge( $this->get_filter_args( $orders, $filters ), [
'items' => [
'description' => 'Comma separated list of item IDs to perform action on',
'type' => 'array',
'items' => [
'description' => 'Item ID',
'type' => [ 'string', 'number' ],
],
],
] ),
) );
}
/**
* Checks a manage capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_404_MANAGE );
}
/**
* Checks a delete capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_delete( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_404_DELETE );
}
/**
* Get 404 log
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_404( WP_REST_Request $request ) {
return $this->get_404( $request->get_params() );
}
/**
* Perform action on 404s
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_bulk( WP_REST_Request $request ) {
$params = $request->get_params();
if ( isset( $params['items'] ) && is_array( $params['items'] ) ) {
$items = $params['items'];
foreach ( $items as $item ) {
if ( is_numeric( $item ) ) {
Red_404_Log::delete( intval( $item, 10 ) );
} elseif ( isset( $params['groupBy'] ) ) {
$group_by = sanitize_text_field( $params['groupBy'] );
$delete_by = 'url-exact';
if ( in_array( $group_by, [ 'ip', 'agent' ], true ) ) {
$delete_by = $group_by;
}
Red_404_Log::delete_all( [ 'filterBy' => [ $delete_by => $item ] ] );
}
}
if ( isset( $params['groupBy'] ) && $params['groupBy'] === 'url-exact' ) {
unset( $params['groupBy'] );
}
} elseif ( isset( $params['global'] ) && $params['global'] ) {
Red_404_Log::delete_all( $params );
}
return $this->get_404( $params );
}
/**
* Get 404 log
*
* @param array $params The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
private function get_404( array $params ) {
if ( isset( $params['groupBy'] ) && in_array( $params['groupBy'], [ 'ip', 'url', 'agent', 'url-exact' ], true ) ) {
$group_by = sanitize_text_field( $params['groupBy'] );
if ( $group_by === 'url-exact' ) {
$group_by = 'url';
}
return Red_404_Log::get_grouped( $group_by, $params );
}
return Red_404_Log::get_filtered( $params );
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* @api {get} /redirection/v1/export/:module/:format Export redirects
* @apiName Export
* @apiDescription Export redirects for a module to Apache, CSV, Nginx, or JSON format
* @apiGroup Import/Export
*
* @apiParam (URL) {String="1","2","3","all"} :module The module to export, with 1 being WordPress, 2 is Apache, and 3 is Nginx
* @apiParam (URL) {String="csv","apache","nginx","json"} :format The format of the export
*
* @apiSuccess {String} data Exported data
* @apiSuccess {Integer} total Number of items exported
*
* @apiUse 401Error
* @apiUse 404Error
* @apiError (Error 400) redirect_export_invalid_module Invalid module
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "redirect_export_invalid_module",
* "message": "Invalid module"
* }
*/
class Redirection_Api_Export extends Redirection_Api_Route {
public function __construct( $namespace ) {
register_rest_route( $namespace, '/export/(?P<module>1|2|3|all)/(?P<format>csv|apache|nginx|json)', array(
$this->get_route( WP_REST_Server::READABLE, 'route_export', [ $this, 'permission_callback_manage' ] ),
) );
}
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE );
}
public function route_export( WP_REST_Request $request ) {
$module = sanitize_text_field( $request['module'] );
$format = 'json';
if ( in_array( $request['format'], [ 'csv', 'apache', 'nginx', 'json' ], true ) ) {
$format = sanitize_text_field( $request['format'] );
}
$export = Red_FileIO::export( $module, $format );
if ( $export === false ) {
return $this->add_error_details( new WP_Error( 'redirect_export_invalid_module', 'Invalid module' ), __LINE__ );
}
return array(
'data' => $export['data'],
'total' => $export['total'],
);
}
}

View File

@@ -0,0 +1,327 @@
<?php
/**
* @api {get} /redirection/v1/group Get groups
* @apiName GetGroups
* @apiDescription Get a paged list of groups based after applying a set of filters and result ordering.
* @apiGroup Group
*
* @apiUse GroupQueryParams
*
* @apiUse GroupList
* @apiUse 401Error
* @apiUse 404Error
*/
/**
* @api {post} /redirection/v1/group Create group
* @apiName CreateGroup
* @apiDescription Create a new group, and return a paged list of groups.
* @apiGroup Group
*
* @apiUse GroupItem
* @apiUse GroupQueryParams
*
* @apiUse GroupList
* @apiUse 401Error
* @apiUse 404Error
* @apiError (Error 400) redirect_group_invalid Invalid group or parameters
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "redirect_group_invalid",
* "message": "Invalid group or parameters"
* }
*/
/**
* @api {post} /redirection/v1/group/:id Update group
* @apiName UpdateGroup
* @apiDescription Update an existing group.
* @apiGroup Group
*
* @apiParam (URL) {Integer} :id Group ID to update
* @apiUse GroupList
*
* @apiSuccess {String} item The updated group
* @apiSuccess {Integer} item.id ID of group
* @apiSuccess {String} item.name Name of this group
* @apiSuccess {Boolean} item.enabled `true` if group (and redirects) are enabled, `false` otherwise
* @apiSuccess {Integer} item.redirects Number of redirects in this group
* @apiSuccess {String} item.moduleName Name of the module this group belongs to
* @apiSuccess {Integer} item.module_id ID of the module this group belongs to
*
* @apiUse 401Error
* @apiUse 404Error
*
* @apiError (Error 400) redirect_group_invalid Invalid group or parameters
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "redirect_group_invalid",
* "message": "Invalid group or parameters"
* }
*/
/**
* @api {post} /redirection/v1/bulk/group/:type Bulk action
* @apiName BulkAction
* @apiDescription Enable, disable, and delete a set of groups. The endpoint will return the next page of results after.
* performing the action, based on the supplied query parameters. This information can be used to refresh a list displayed to the client.
* @apiGroup Group
*
* @apiParam (URL) {String="delete","enable","disable"} :type Type of bulk action that is applied to every group ID.
* Enabling or disabling a group will also enable or disable all redirects in that group
*
* @apiParam (Query Parameter) {String[]} [items] Array of group IDs to perform the action on
* @apiParam (Query Parameter) {Boolean=false} [global] Perform action globally using the filter parameters
* @apiUse GroupQueryParams
*
* @apiUse GroupList
* @apiUse 401Error
* @apiUse 404Error
* @apiUse 400MissingError
*/
/**
* @apiDefine GroupQueryParams
*
* @apiParam (Query Parameter) {String} [filterBy[name]] Filter the results by the supplied name
* @apiParam (Query Parameter) {String="enabled","disabled"} [filterBy[status]] Filter the results by the supplied status
* @apiParam (Query Parameter) {Integer="1","2","3"} [filterBy[module]] Filter the results by the supplied module ID
* @apiParam (Query Parameter) {String="name"} [orderby] Order in which results are returned
* @apiParam (Query Parameter) {String="asc","desc"} [direction=desc] Direction to order the results by (ascending or descending)
* @apiParam (Query Parameter) {Integer{1...200}} [per_page=25] Number of results per request
* @apiParam (Query Parameter) {Integer} [page=0] Current page of results
*/
/**
* @apiDefine GroupItem
*
* @apiParam (JSON Body) {String} name Name of the group
* @apiParam (JSON Body) {Integer="1","2","3"} moduleID Module ID of the group, with 1 being WordPress, 2 is Apache, and 3 is Nginx
*/
/**
* @apiDefine GroupList
*
* @apiSuccess {Object[]} items Array of group objects
* @apiSuccess {Integer} items.id ID of group
* @apiSuccess {String} items.name Name of this group
* @apiSuccess {Boolean} items.enabled `true` if group (and redirects) are enabled, `false` otherwise
* @apiSuccess {Integer} items.redirects Number of redirects in this group
* @apiSuccess {String} items.moduleName Name of the module this group belongs to
* @apiSuccess {Integer} items.module_id ID of the module this group belongs to
* @apiSuccess {Integer} total Number of items
*
* @apiSuccessExample {json} Success 200:
* HTTP/1.1 200 OK
* {
* "items": [
* {
* "id": 3,
* "enabled": true,
* "moduleName": "WordPress",
* "module_id": 1,
* "name": "Redirections",
* "redirects": 0,
* }
* ],
* "total": 1
* }
*/
/**
* Group API endpoint
*/
class Redirection_Api_Group extends Redirection_Api_Filter_Route {
/**
* 404 API endpoint constructor
*
* @param string $namespace Namespace.
*/
public function __construct( $namespace ) {
$orders = [ 'name', 'id', '' ];
$filters = [ 'status', 'module', 'name' ];
register_rest_route( $namespace, '/group', array(
'args' => $this->get_filter_args( $orders, $filters ),
$this->get_route( WP_REST_Server::READABLE, 'route_list', [ $this, 'permission_callback_manage' ] ),
array_merge(
$this->get_route( WP_REST_Server::EDITABLE, 'route_create', [ $this, 'permission_callback_add' ] ),
array( 'args' => $this->get_group_args() )
),
) );
register_rest_route( $namespace, '/group/(?P<id>[\d]+)', array(
'args' => $this->get_group_args(),
$this->get_route( WP_REST_Server::EDITABLE, 'route_update', [ $this, 'permission_callback_add' ] ),
) );
register_rest_route( $namespace, '/bulk/group/(?P<bulk>delete|enable|disable)', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_bulk', [ $this, 'permission_callback_bulk' ] ),
'args' => array_merge( $this->get_filter_args( $orders, $filters ), [
'items' => [
'description' => 'Comma separated list of item IDs to perform action on',
'type' => 'array',
'items' => [
'description' => 'Item ID',
'type' => [ 'string', 'number' ],
],
],
] ),
) );
}
/**
* Checks a manage capability
*
* Access to group data is required by the CAP_GROUP_MANAGE and CAP_REDIRECT_MANAGE caps
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_GROUP_MANAGE ) || Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_REDIRECT_MANAGE );
}
/**
* Checks a bulk capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_bulk( WP_REST_Request $request ) {
if ( $request['bulk'] === 'delete' ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_GROUP_DELETE );
}
return $this->permission_callback_add( $request );
}
/**
* Checks a create capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_add( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_GROUP_ADD );
}
private function get_group_args() {
return array(
'moduleId' => array(
'description' => 'Module ID',
'type' => 'integer',
'minimum' => 0,
'maximum' => 3,
'required' => true,
),
'name' => array(
'description' => 'Group name',
'type' => 'string',
'required' => true,
),
'status' => [
'description' => 'Status of the group',
],
);
}
/**
* Get group list
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_list( WP_REST_Request $request ) {
return Red_Group::get_filtered( $request->get_params() );
}
/**
* Create a group
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_create( WP_REST_Request $request ) {
$params = $request->get_params( $request );
$name = '';
$module = 0;
if ( isset( $params['name'] ) ) {
$name = sanitize_text_field( $params['name'] );
}
if ( isset( $params['moduleId'] ) ) {
$module = intval( $params['moduleId'], 10 );
}
$group = Red_Group::create( $name, $module );
if ( $group ) {
return Red_Group::get_filtered( $params );
}
return $this->add_error_details( new WP_Error( 'redirect_group_invalid', 'Invalid group or parameters' ), __LINE__ );
}
/**
* Update a 404
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_update( WP_REST_Request $request ) {
$params = $request->get_params( $request );
$group = Red_Group::get( intval( $request['id'], 10 ) );
if ( $group ) {
$result = $group->update( $params );
if ( $result ) {
return array( 'item' => $group->to_json() );
}
}
return $this->add_error_details( new WP_Error( 'redirect_group_invalid', 'Invalid group details' ), __LINE__ );
}
/**
* Perform action on groups
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_bulk( WP_REST_Request $request ) {
$params = $request->get_params();
$action = $request['bulk'];
$items = [];
if ( isset( $params['items'] ) && is_array( $params['items'] ) ) {
// Array of integers, sanitized below
$items = $params['items'];
} elseif ( isset( $params['global'] ) && $params['global'] ) {
// Groups have additional actions that fire and so we need to action them individually
$groups = Red_Group::get_all( $params );
$items = array_column( $groups, 'id' );
}
foreach ( $items as $item ) {
$group = Red_Group::get( intval( $item, 10 ) );
if ( is_object( $group ) ) {
if ( $action === 'delete' ) {
$group->delete();
} elseif ( $action === 'disable' ) {
$group->disable();
} elseif ( $action === 'enable' ) {
$group->enable();
}
}
}
return $this->route_list( $request );
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @api {get} /redirection/v1/import/file/:group_id Import redirects
* @apiName Import
* @apiDescription Import redirects from CSV, JSON, or Apache .htaccess
* @apiGroup Import/Export
*
* @apiParam (URL) {Integer} :group_id The group ID to import into
* @apiParam (File) {File} file The multipart form upload containing the file to import
*
* @apiSuccess {Integer} imported Number of items imported
*
* @apiUse 401Error
* @apiUse 404Error
* @apiError (Error 400) redirect_import_invalid_group Invalid group
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "redirect_import_invalid_group",
* "message": "Invalid group"
* }
* @apiError (Error 400) redirect_import_invalid_file Invalid file upload
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "redirect_import_invalid_file",
* "message": "Invalid file upload"
* }
*/
class Redirection_Api_Import extends Redirection_Api_Route {
public function __construct( $namespace ) {
register_rest_route( $namespace, '/import/file/(?P<group_id>\d+)', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_import_file', [ $this, 'permission_callback_manage' ] ),
) );
register_rest_route( $namespace, '/import/plugin', array(
$this->get_route( WP_REST_Server::READABLE, 'route_plugin_import_list', [ $this, 'permission_callback_manage' ] ),
) );
register_rest_route( $namespace, '/import/plugin', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_plugin_import', [ $this, 'permission_callback_manage' ] ),
) );
}
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE );
}
public function route_plugin_import_list( WP_REST_Request $request ) {
include_once dirname( __DIR__ ) . '/models/importer.php';
return array( 'importers' => Red_Plugin_Importer::get_plugins() );
}
public function route_plugin_import( WP_REST_Request $request ) {
include_once dirname( __DIR__ ) . '/models/importer.php';
$params = $request->get_params( $request );
$groups = Red_Group::get_all();
$plugins = is_array( $request['plugin'] ) ? $request['plugin'] : [ $request['plugin'] ];
$plugins = array_map( 'sanitize_text_field', $plugins );
$total = 0;
foreach ( $plugins as $plugin ) {
$total += Red_Plugin_Importer::import( $plugin, $groups[0]['id'] );
}
return [ 'imported' => $total ];
}
public function route_import_file( WP_REST_Request $request ) {
$upload = $request->get_file_params();
$upload = isset( $upload['file'] ) ? $upload['file'] : false;
$group_id = intval( $request['group_id'], 10 );
if ( $upload && is_uploaded_file( $upload['tmp_name'] ) ) {
$count = Red_FileIO::import( $group_id, $upload );
if ( $count !== false ) {
return array(
'imported' => $count,
);
}
return $this->add_error_details( new WP_Error( 'redirect_import_invalid_group', 'Invalid group' ), __LINE__ );
}
return $this->add_error_details( new WP_Error( 'redirect_import_invalid_file', 'Invalid file' ), __LINE__ );
}
}

View File

@@ -0,0 +1,202 @@
<?php
/**
* @api {get} /redirection/v1/log Get logs
* @apiName GetLogs
* @apiDescription Get a paged list of redirect logs after applying a set of filters and result ordering.
* @apiGroup Log
*
* @apiUse LogQueryParams
*
* @apiUse LogList
* @apiUse 401Error
* @apiUse 404Error
*/
/**
* @api {post} /redirection/v1/log Delete logs
* @apiName DeleteLogs
* @apiDescription Delete logs by filter. If no filter is supplied then all entries will be deleted. The endpoint will return the next page of results after.
* performing the action, based on the supplied query parameters. This information can be used to refresh a list displayed to the client.
* @apiGroup Log
*
* @apiParam (Query Parameter) {String} filterBy[ip] Filter the results by the supplied IP
* @apiParam (Query Parameter) {String} filterBy[url] Filter the results by the supplied URL
* @apiParam (Query Parameter) {String} filterBy[url-exact] Filter the results by the exact URL (not a substring match, as per `url`)
* @apiParam (Query Parameter) {String} filterBy[referrer] Filter the results by the supplied referrer
* @apiParam (Query Parameter) {String} filterBy[agent] Filter the results by the supplied user agent
* @apiParam (Query Parameter) {String} filterBy[target] Filter the results by the supplied redirect target
*
* @apiUse LogList
* @apiUse 401Error
* @apiUse 404Error
*/
/**
* @api {post} /redirection/v1/bulk/log/:type Bulk action
* @apiName BulkAction
* @apiDescription Delete logs by ID
* @apiGroup Log
*
* @apiParam (URL) {String="delete"} :type Type of bulk action that is applied to every log ID.
* @apiParam (Query Parameter) {String[]} [items] Array of group IDs to perform the action on
* @apiParam (Query Parameter) {Boolean=false} [global] Perform action globally using the filter parameters
* @apiUse LogQueryParams
*
* @apiUse LogList
* @apiUse 401Error
* @apiUse 404Error
* @apiUse 400MissingError
*/
/**
* @apiDefine LogQueryParams Log query parameters
*
* @apiParam (Query Parameter) {String} [filterBy[ip]] Filter the results by the supplied IP
* @apiParam (Query Parameter) {String} [filterBy[url]] Filter the results by the supplied URL
* @apiParam (Query Parameter) {String} [filterBy[url-]exact] Filter the results by the exact URL (not a substring match, as per `url`)
* @apiParam (Query Parameter) {String} [filterBy[referrer]] Filter the results by the supplied referrer
* @apiParam (Query Parameter) {String} [filterBy[agent]] Filter the results by the supplied user agent
* @apiParam (Query Parameter) {String} [filterBy[target]] Filter the results by the supplied redirect target
* @apiParam (Query Parameter) {String} [filterBy[domain]] Filter the results by the supplied domain name
* @apiParam (Query Parameter) {String} [filterBy[redirect_by]] Filter the results by the redirect agent
* @apiParam (Query Parameter) {String="head","get","post"} [filterBy[method]] Filter the results by the supplied HTTP request method
* @apiParam (Query Parameter) {String="ip","url"} [orderby] Order by IP or URL
* @apiParam (Query Parameter) {String="asc","desc"} [direction=desc] Direction to order the results by (ascending or descending)
* @apiParam (Query Parameter) {Integer{1...200}} [per_page=25] Number of results per request
* @apiParam (Query Parameter) {Integer} [page=0] Current page of results
* @apiParam (Query Parameter) {String="ip","url"} [groupBy] Group by IP or URL
*/
/**
* @apiDefine LogList
*
* @apiSuccess {Object[]} items Array of log objects
* @apiSuccess {Integer} items.id ID of log entry
* @apiSuccess {String} items.created Date the log entry was recorded
* @apiSuccess {Integer} items.created_time Unix time value for `created`
* @apiSuccess {Integer} items.url The requested URL that caused the log entry
* @apiSuccess {String} items.agent User agent of the client initiating the request
* @apiSuccess {Integer} items.referrer Referrer of the client initiating the request
* @apiSuccess {Integer} total Number of items
*
* @apiSuccessExample {json} Success 200:
* HTTP/1.1 200 OK
* {
* "items": [
* {
* "id": 3,
* "created": "2019-01-01 12:12:00,
* "created_time": "12345678",
* "url": "/the-url",
* "agent": "FancyBrowser",
* "referrer": "http://site.com/previous/,
* }
* ],
* "total": 1
* }
*/
/**
* Log API endpoint
*/
class Redirection_Api_Log extends Redirection_Api_Filter_Route {
/**
* Log API endpoint constructor
*
* @param string $namespace Namespace.
*/
public function __construct( $namespace ) {
$orders = [ 'url', 'ip', 'total', 'count', '' ];
$filters = [ 'ip', 'url-exact', 'referrer', 'agent', 'url', 'target', 'domain', 'method', 'http', 'redirect_by' ];
register_rest_route( $namespace, '/log', array(
'args' => $this->get_filter_args( $orders, $filters ),
$this->get_route( WP_REST_Server::READABLE, 'route_log', [ $this, 'permission_callback_manage' ] ),
) );
register_rest_route( $namespace, '/bulk/log/(?P<bulk>delete)', [
$this->get_route( WP_REST_Server::EDITABLE, 'route_bulk', [ $this, 'permission_callback_delete' ] ),
'args' => array_merge( $this->get_filter_args( $orders, $filters ), [
'items' => [
'description' => 'Comma separated list of item IDs to perform action on',
'type' => 'array',
'items' => [
'description' => 'Item ID',
'type' => [ 'string', 'number' ],
],
],
] ),
] );
}
/**
* Checks a manage capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_LOG_MANAGE );
}
/**
* Checks a delete capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_delete( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_LOG_DELETE );
}
/**
* Get log list
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_log( WP_REST_Request $request ) {
return $this->get_logs( $request->get_params() );
}
/**
* Perform bulk action on logs
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_bulk( WP_REST_Request $request ) {
$params = $request->get_params();
if ( isset( $params['items'] ) && is_array( $params['items'] ) ) {
$items = $params['items'];
foreach ( $items as $item ) {
if ( is_numeric( $item ) ) {
Red_Redirect_Log::delete( intval( $item, 10 ) );
} elseif ( isset( $params['groupBy'] ) ) {
$delete_by = 'url-exact';
if ( in_array( $params['groupBy'], [ 'ip', 'agent' ], true ) ) {
$delete_by = sanitize_text_field( $params['groupBy'] );
}
Red_Redirect_Log::delete_all( [ 'filterBy' => [ $delete_by => $item ] ] );
}
}
} elseif ( isset( $params['global'] ) && $params['global'] ) {
Red_Redirect_Log::delete_all( $params );
}
return $this->route_log( $request );
}
private function get_logs( array $params ) {
if ( isset( $params['groupBy'] ) && in_array( $params['groupBy'], [ 'ip', 'url', 'agent' ], true ) ) {
return Red_Redirect_Log::get_grouped( sanitize_text_field( $params['groupBy'] ), $params );
}
return Red_Redirect_Log::get_filtered( $params );
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* 'Plugin' functions for Redirection
*/
class Redirection_Api_Plugin extends Redirection_Api_Route {
public function __construct( $namespace ) {
register_rest_route( $namespace, '/plugin', array(
$this->get_route( WP_REST_Server::READABLE, 'route_status', [ $this, 'permission_callback_manage' ] ),
) );
register_rest_route( $namespace, '/plugin', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_fixit', [ $this, 'permission_callback_manage' ] ),
'args' => [
'name' => array(
'description' => 'Name',
'type' => 'string',
),
'value' => array(
'description' => 'Value',
'type' => 'string',
),
],
) );
register_rest_route( $namespace, '/plugin/delete', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_delete', [ $this, 'permission_callback_manage' ] ),
) );
register_rest_route( $namespace, '/plugin/test', array(
$this->get_route( WP_REST_Server::ALLMETHODS, 'route_test', [ $this, 'permission_callback_manage' ] ),
) );
register_rest_route( $namespace, '/plugin/data', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_database', [ $this, 'permission_callback_manage' ] ),
'args' => [
'upgrade' => [
'description' => 'Upgrade parameter',
'type' => 'string',
'enum' => array(
'stop',
'skip',
'retry',
),
],
],
) );
}
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_SUPPORT_MANAGE );
}
public function route_status( WP_REST_Request $request ) {
include_once dirname( REDIRECTION_FILE ) . '/models/fixer.php';
$fixer = new Red_Fixer();
return $fixer->get_json();
}
public function route_fixit( WP_REST_Request $request ) {
include_once dirname( REDIRECTION_FILE ) . '/models/fixer.php';
$params = $request->get_params();
$fixer = new Red_Fixer();
if ( isset( $params['name'] ) && isset( $params['value'] ) ) {
global $wpdb;
$fixer->save_debug( sanitize_text_field( $params['name'] ), sanitize_text_field( $params['value'] ) );
$groups = intval( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}redirection_groups" ), 10 );
if ( $groups === 0 ) {
Red_Group::create( 'new group', 1 );
}
} else {
$fixer->fix( $fixer->get_status() );
}
return $fixer->get_json();
}
public function route_delete() {
if ( is_multisite() ) {
return new WP_Error( 'redirect_delete_multi', 'Multisite installations must delete the plugin from the network admin' );
}
$plugin = Redirection_Admin::init();
$plugin->plugin_uninstall();
$current = get_option( 'active_plugins' );
$plugin_position = array_search( basename( dirname( REDIRECTION_FILE ) ) . '/' . basename( REDIRECTION_FILE ), $current );
if ( $plugin_position !== false ) {
array_splice( $current, $plugin_position, 1 );
update_option( 'active_plugins', $current );
}
return array( 'location' => admin_url() . 'plugins.php' );
}
public function route_test( WP_REST_Request $request ) {
return array(
'success' => true,
);
}
public function route_database( WP_REST_Request $request ) {
$params = $request->get_params();
$status = new Red_Database_Status();
$upgrade = false;
if ( isset( $params['upgrade'] ) && in_array( $params['upgrade'], [ 'stop', 'skip' ], true ) ) {
$upgrade = sanitize_text_field( $params['upgrade'] );
}
// Check upgrade
if ( ! $status->needs_updating() && ! $status->needs_installing() ) {
/* translators: version number */
$status->set_error( sprintf( __( 'Your database does not need updating to %s.', 'redirection' ), REDIRECTION_DB_VERSION ) );
return $status->get_json();
}
if ( $upgrade === 'stop' ) {
$status->stop_update();
} elseif ( $upgrade === 'skip' ) {
$status->set_next_stage();
}
if ( $upgrade === false || $status->get_current_stage() ) {
$database = new Red_Database();
$database->apply_upgrade( $status );
}
return $status->get_json();
}
}

View File

@@ -0,0 +1,407 @@
<?php
/**
* @api {get} /redirection/v1/redirect Get redirects
* @apiName GetRedirects
* @apiDescription Get a paged list of redirects based after applying a set of filters and result ordering.
* @apiGroup Redirect
*
* @apiUse RedirectQueryParams
*
* @apiUse RedirectList
* @apiUse 401Error
* @apiUse 404Error
*/
/**
* @api {post} /redirection/v1/redirect Create redirect
* @apiName CreateRedirect
* @apiDescription Create a new redirect, and return a paged list of redirects.
* @apiGroup Redirect
*
* @apiUse RedirectItem
* @apiUse RedirectQueryParams
*
* @apiUse RedirectList
* @apiUse 401Error
* @apiUse 404Error
* @apiError (Error 400) redirect_create_failed Failed to create redirect
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "redirect_create_failed",
* "message": "Failed to create redirect"
* }
*/
/**
* @api {post} /redirection/v1/redirect/:id Update redirect
* @apiName UpdateRedirect
* @apiDescription Update an existing redirect.
* @apiGroup Redirect
*
* @apiParam (URL) {Integer} :id Redirect ID to update
*
* @apiUse RedirectItem
*
* @apiUse RedirectList
* @apiUse 401Error
* @apiUse 404Error
* @apiError (Error 400) redirect_update_failed Failed to update redirect
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "redirect_update_failed",
* "message": "Failed to update redirect"
* }
*/
/**
* @api {post} /redirection/v1/bulk/redirect/:type Bulk action
* @apiName BulkAction
* @apiDescription Enable, disable, and delete a set of redirects. The endpoint will return the next page of results after.
* performing the action, based on the supplied query parameters. This information can be used to refresh a list displayed to the client.
* @apiGroup Redirect
*
* @apiParam (URL) {String="delete","enable","disable","reset"} :type Type of bulk action that is applied to every item.
* @apiParam (Query Parameter) {String[]} [items] Array of redirect IDs to perform the action on
* @apiParam (Query Parameter) {Boolean=false} [global] Perform action globally using the filter parameters
* @apiUse RedirectQueryParams
*
* @apiUse RedirectList
* @apiUse 401Error
* @apiUse 404Error
* @apiUse 400MissingError
*/
/**
* @apiDefine RedirectItem Redirect
* All data associated with a redirect
*
* @apiParam {String="enabled","disabled"} status Status of the redirect
* @apiParam {Integer} position Redirect position, used to determine order multiple redirects occur
* @apiParam {Object} match_data Additional match parameters
* @apiParam {Object} match_data.source Match against the source
* @apiParam {Boolean} match_data.source.flag_regex `true` for regular expression, `false` otherwise
* @apiParam {String="ignore","exact","pass"} match_data.source.flag_query Which query parameter matching to use
* @apiParam {Boolean} match_data.source.flag_case] `true` for case insensitive matches, `false` otherwise
* @apiParam {Boolean} match_data.source.flag_trailing] `true` to ignore trailing slashes, `false` otherwise
* @apiParam {Object} match_data.options Options for the redirect match
* @apiParam {Boolean} match_data.options.log_exclude `true` to exclude this from any logs, `false` otherwise (default)
* @apiParam {Boolean} regex True for regular expression, `false` otherwise
* @apiParam {String} url The source URL
* @apiParam {String="url","referrer","agent","login","header","custom","cookie","role","server","ip","page","language"} match_type What URL matching to use
* @apiParam {String} [title] A descriptive title for the redirect, or empty for no title
* @apiParam {Integer} group_id The group this redirect belongs to
* @apiParam {String} action_type What to do when the URL is matched
* @apiParam {Integer} action_code The HTTP code to return
* @apiParam {Object} action_data Any data associated with the `action_type` and `match_type`. For example, the target URL
*/
/**
* @apiDefine RedirectList A list of redirects
* A list of redirects
*
* @apiSuccess {Object[]} items Array of redirect objects
* @apiSuccess {Integer} items.id ID of redirect
* @apiSuccess {String} items.url Source URL to match
* @apiSuccess {String} items.match_url Match URL
* @apiSuccess {Object} items.match_data Match against the source
* @apiSuccess {String} items.match_type What URL matching to use
* @apiSuccess {String} items.action_type What to do when the URL is matched
* @apiSuccess {Integer} items.action_code The HTTP code to return
* @apiSuccess {String} items.action_data Any data associated with the action_type. For example, the target URL
* @apiSuccess {String} items.title Optional A descriptive title for the redirect, or empty for no title
* @apiSuccess {String} items.hits Number of hits this redirect has received
* @apiSuccess {String} items.regex True for regular expression, false otherwise
* @apiSuccess {String} items.group_id The group this redirect belongs to
* @apiSuccess {String} items.position Redirect position, used to determine order multiple redirects occur
* @apiSuccess {String} items.last_access The date this redirect was last hit
* @apiSuccess {String} items.status Status of the redirect
* @apiSuccess {Integer} total Number of items
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "items": [
* {
* id: 3,
* url: "/source",
* match_url: "/source",
* match_data: "",
* action_code: "",
* action_type: "",
* action_data: "",
* match_type: "url",
* title: "Redirect title",
* hits: 5,
* regex: true,
* group_id: 15,
* position: 1,
* last_access: "2019-01-01 01:01:01"
* status: "enabled"
* }
* ],
* "total": 1
* }
*/
/**
* @apiDefine RedirectQueryParams
*
* @apiParam (Query Parameter) {String="enabled","disabled"} [filterBy[status]] Filter the results by the supplied status
* @apiParam (Query Parameter) {String} [filterBy[url]] Filter the results by the supplied URL
* @apiParam (Query Parameter) {String="regular","plain"} [filterBy[url-match]] Filter the results by `regular` expressions or non regular expressions
* @apiParam (Query Parameter) {String} [filterBy[match]] Filter the results by the supplied match type
* @apiParam (Query Parameter) {String} [filterBy[action]] Filter the results by the supplied action type
* @apiParam (Query Parameter) {Integer} [filterBy[http]] Filter the results by the supplied redirect HTTP code
* @apiParam (Query Parameter) {String="year","month","all"} [filterBy[access]] Filter the results by how long the redirect was last accessed
* @apiParam (Query Parameter) {String} [filterBy[target]] Filter the results by the supplied redirect target
* @apiParam (Query Parameter) {String} [filterBy[title]] Filter the results by the supplied redirect title
* @apiParam (Query Parameter) {Integer} [filterBy[group]] Filter the results by the supplied redirect group ID
* @apiParam (Query Parameter) {Integer} [filterBy[id]] Filter the results to the redirect ID
* @apiParam (Query Parameter) {Integer="1","2","3"} [filterBy[module]] Filter the results by the supplied module ID
* @apiParam (Query Parameter) {String="source","last_count","last_access","position","id"} [orderby=id] Order in which results are returned
* @apiParam (Query Parameter) {String="asc","desc"} [direction=desc] Direction to order the results by (ascending or descending)
* @apiParam (Query Parameter) {Integer{1...200}} [per_page=25] Number of results per request
* @apiParam (Query Parameter) {Integer} [page=0] Current page of results
*/
/**
* Redirect API endpoint
*/
class Redirection_Api_Redirect extends Redirection_Api_Filter_Route {
/**
* Redirect API endpoint constructor
*
* @param string $namespace Namespace.
*/
public function __construct( $namespace ) {
$orders = [ 'source', 'last_count', 'last_access', 'position', 'id', '' ];
$filters = [ 'status', 'url-match', 'match', 'action', 'http', 'access', 'url', 'target', 'title', 'group', 'id' ];
register_rest_route( $namespace, '/redirect', array(
'args' => $this->get_filter_args( $orders, $filters ),
$this->get_route( WP_REST_Server::READABLE, 'route_list', [ $this, 'permission_callback_manage' ] ),
$this->get_route( WP_REST_Server::EDITABLE, 'route_create', [ $this, 'permission_callback_add' ] ),
) );
register_rest_route( $namespace, '/redirect/(?P<id>[\d]+)', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_update', [ $this, 'permission_callback_add' ] ),
) );
register_rest_route( $namespace, '/redirect/post', array(
$this->get_route( WP_REST_Server::READABLE, 'route_match_post', [ $this, 'permission_callback_manage' ] ),
'args' => [
'text' => [
'description' => 'Text to match',
'type' => 'string',
'required' => true,
],
],
) );
register_rest_route( $namespace, '/bulk/redirect/(?P<bulk>delete|enable|disable|reset)', array(
$this->get_route( WP_REST_Server::EDITABLE, 'route_bulk', [ $this, 'permission_callback_bulk' ] ),
'args' => array_merge( $this->get_filter_args( $orders, $filters ), [
'global' => [
'description' => 'Apply bulk action globally, as per filters',
'type' => 'boolean',
],
'items' => [
'description' => 'Array of IDs to perform action on',
'type' => 'array',
'items' => [
'description' => 'Item ID',
'type' => [ 'string', 'number' ],
],
],
] ),
) );
}
/**
* Checks a manage capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_REDIRECT_MANAGE );
}
/**
* Checks a bulk capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_bulk( WP_REST_Request $request ) {
if ( $request['bulk'] === 'delete' ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_REDIRECT_DELETE );
}
return $this->permission_callback_add( $request );
}
/**
* Checks a create capability
*
* @param WP_REST_Request $request Request.
* @return Bool
*/
public function permission_callback_add( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_REDIRECT_ADD );
}
/**
* Get redirect list
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_list( WP_REST_Request $request ) {
return Red_Item::get_filtered( $request->get_params() );
}
/**
* Get redirect list
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_create( WP_REST_Request $request ) {
$params = $request->get_params();
$urls = array();
if ( isset( $params['url'] ) ) {
$urls = array( $params['url'] );
if ( is_array( $params['url'] ) ) {
$urls = $params['url'];
}
// Remove duplicates
$unique = [];
foreach ( $urls as $url ) {
$unique[ $url ] = $url;
}
foreach ( $unique as $url ) {
$params['url'] = $url;
// Data is sanitized in the create function
$redirect = Red_Item::create( $params );
if ( is_wp_error( $redirect ) ) {
return $this->add_error_details( $redirect, __LINE__ );
}
}
}
return $this->route_list( $request );
}
/**
* Update redirect
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_update( WP_REST_Request $request ) {
$params = $request->get_params();
$redirect = Red_Item::get_by_id( intval( $params['id'], 10 ) );
if ( $redirect ) {
$result = $redirect->update( $params );
if ( is_wp_error( $result ) ) {
return $this->add_error_details( $result, __LINE__ );
}
return [ 'item' => $redirect->to_json() ];
}
return $this->add_error_details( new WP_Error( 'redirect_update_failed', 'Invalid redirect details' ), __LINE__ );
}
/**
* Perform bulk action on redirects
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_bulk( WP_REST_Request $request ) {
$params = $request->get_params();
$action = sanitize_text_field( $request['bulk'] );
if ( isset( $params['items'] ) && is_array( $params['items'] ) ) {
$items = $params['items'];
foreach ( $items as $item ) {
$redirect = Red_Item::get_by_id( intval( $item, 10 ) );
if ( $redirect === false ) {
return $this->add_error_details( new WP_Error( 'redirect_bulk_failed', 'Invalid redirect' ), __LINE__ );
}
if ( $action === 'delete' ) {
$redirect->delete();
} elseif ( $action === 'disable' ) {
$redirect->disable();
} elseif ( $action === 'enable' ) {
$redirect->enable();
} elseif ( $action === 'reset' ) {
$redirect->reset();
}
}
} elseif ( isset( $params['global'] ) && $params['global'] ) {
// Params are sanitized in the filter class
if ( $action === 'delete' ) {
Red_Item::delete_all( $params );
} elseif ( $action === 'reset' ) {
Red_Item::reset_all( $params );
} elseif ( $action === 'enable' || $action === 'disable' ) {
Red_Item::set_status_all( $action, $params );
}
}
return $this->route_list( $request );
}
/**
* Search for a post
*
* @param WP_REST_Request $request The request.
* @return WP_Error|array Return an array of results, or a WP_Error
*/
public function route_match_post( WP_REST_Request $request ) {
global $wpdb;
$params = $request->get_params();
$search = sanitize_text_field( $params['text'] );
$results = [];
$posts = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID,post_title,post_name FROM $wpdb->posts WHERE post_status='publish' AND (post_title LIKE %s OR post_name LIKE %s) " .
"AND post_type IN ('post','page')",
'%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%'
)
);
foreach ( (array) $posts as $post ) {
$title = $post->post_name;
if ( strpos( $post->post_title, $search ) ) {
$title = $post->post_title;
}
$results[] = [
'title' => $title,
'value' => get_permalink( $post->ID ),
];
}
return $results;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* @api {get} /redirection/v1/setting Get settings
* @apiName GetSettings
* @apiDescription Get all settings for Redirection. This includes user-configurable settings, as well as necessary WordPress settings.
* @apiGroup Settings
*
* @apiUse SettingItem
* @apiUse 401Error
* @apiUse 404Error
*/
/**
* @api {post} /redirection/v1/setting Update settings
* @apiName UpdateSettings
* @apiDescription Update Redirection settings. Note you can do partial updates, and only the values specified will be changed.
* @apiGroup Settings
*
* @apiParam {Object} settings An object containing all the settings to update
* @apiParamExample {json} settings:
* {
* "expire_redirect": 14,
* "https": false
* }
*
* @apiUse SettingItem
* @apiUse 401Error
* @apiUse 404Error
*/
/**
* @apiDefine SettingItem Settings
* Redirection settings
*
* @apiSuccess {Object[]} settings An object containing all settings
* @apiSuccess {String} settings.expire_redirect
* @apiSuccess {String} settings.token
* @apiSuccess {String} settings.monitor_post
* @apiSuccess {String[]} settings.monitor_types
* @apiSuccess {String} settings.associated_redirect
* @apiSuccess {String} settings.auto_target
* @apiSuccess {String} settings.expire_redirect
* @apiSuccess {String} settings.expire_404
* @apiSuccess {String} settings.modules
* @apiSuccess {String} settings.redirect_cache
* @apiSuccess {String} settings.ip_logging
* @apiSuccess {String} settings.last_group_id
* @apiSuccess {String} settings.rest_api
* @apiSuccess {String} settings.https
* @apiSuccess {String} settings.headers
* @apiSuccess {String} settings.database
* @apiSuccess {String} settings.relocate Relocate this site to the specified domain (and path)
* @apiSuccess {String="www","nowww",""} settings.preferred_domain Preferred canonical domain
* @apiSuccess {String[]} settings.aliases Array of domains that will be redirected to the current WordPress site
* @apiSuccess {Object[]} groups An array of groups
* @apiSuccess {String} groups.label Name of the group
* @apiSuccess {Integer} groups.value Group ID
* @apiSuccess {String} installed The path that WordPress is installed in
* @apiSuccess {Boolean} canDelete True if Redirection can be deleted, false otherwise (on multisite, for example)
* @apiSuccess {String[]} post_types Array of WordPress post types
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "settings": {
* "expire_redirect": 7,
* "https": true
* },
* "groups": [
* { label: 'My group', value: 5 }
* ],
* "installed": "/var/html/wordpress",
* "canDelete": true,
* "post_types": [
* "post",
* "page"
* ]
* }
*/
class Redirection_Api_Settings extends Redirection_Api_Route {
public function __construct( $namespace ) {
register_rest_route( $namespace, '/setting', array(
$this->get_route( WP_REST_Server::READABLE, 'route_settings', [ $this, 'permission_callback_manage' ] ),
$this->get_route( WP_REST_Server::EDITABLE, 'route_save_settings', [ $this, 'permission_callback_manage' ] ),
) );
}
public function route_settings( WP_REST_Request $request ) {
if ( ! function_exists( 'get_home_path' ) ) {
include_once ABSPATH . '/wp-admin/includes/file.php';
}
return [
'settings' => red_get_options(),
'groups' => $this->groups_to_json( Red_Group::get_for_select() ),
'installed' => get_home_path(),
'canDelete' => ! is_multisite(),
'post_types' => red_get_post_types(),
];
}
public function permission_callback_manage( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_OPTION_MANAGE ) || Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_SITE_MANAGE );
}
public function route_save_settings( WP_REST_Request $request ) {
$params = $request->get_params();
$result = true;
if ( isset( $params['location'] ) && strlen( $params['location'] ) > 0 ) {
$module = Red_Module::get( 2 );
$result = $module->can_save( sanitize_text_field( $params['location'] ) );
}
red_set_options( $params );
$settings = $this->route_settings( $request );
if ( is_wp_error( $result ) ) {
$settings['warning'] = $result->get_error_message();
}
return $settings;
}
private function groups_to_json( $groups, $depth = 0 ) {
$items = array();
foreach ( $groups as $text => $value ) {
if ( is_array( $value ) && $depth === 0 ) {
$items[] = (object) array(
'label' => $text,
'value' => $this->groups_to_json( $value, 1 ),
);
} else {
$items[] = (object) array(
'label' => $value,
'value' => $text,
);
}
}
return $items;
}
}

View File

@@ -0,0 +1,196 @@
<?php
require_once __DIR__ . '/api-group.php';
require_once __DIR__ . '/api-redirect.php';
require_once __DIR__ . '/api-log.php';
require_once __DIR__ . '/api-404.php';
require_once __DIR__ . '/api-settings.php';
require_once __DIR__ . '/api-plugin.php';
require_once __DIR__ . '/api-import.php';
require_once __DIR__ . '/api-export.php';
define( 'REDIRECTION_API_NAMESPACE', 'redirection/v1' );
/**
* @apiDefine 401Error
*
* @apiError (Error 401) rest_forbidden You are not authorized to access this API endpoint
* @apiErrorExample {json} 401 Error Response:
* HTTP/1.1 401 Bad Request
* {
* "code": "rest_forbidden",
* "message": "Sorry, you are not allowed to do that."
* }
*/
/**
* @apiDefine 404Error
*
* @apiError (Error 404) rest_no_route Endpoint not found
* @apiErrorExample {json} 404 Error Response:
* HTTP/1.1 404 Not Found
* {
* "code": "rest_no_route",
* "message": "No route was found matching the URL and request method"
* }
*/
/**
* @apiDefine 400Error
*
* @apiError rest_forbidden You are not authorized to access this API endpoint
* @apiErrorExample {json} 400 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "error": "invalid",
* "message": "Invalid request"
* }
*/
/**
* @apiDefine 400MissingError
* @apiError (Error 400) rest_missing_callback_param Some required parameters are not present or not in the correct format
* @apiErrorExample {json} 400 Error Response:
* HTTP/1.1 400 Bad Request
* {
* "code": "rest_missing_callback_param",
* "message": "Missing parameter(s): PARAM"
* }
*/
class Redirection_Api_Route {
protected function add_error_details( WP_Error $error, $line, $code = 400 ) {
global $wpdb;
$data = array(
'status' => $code,
'error_code' => $line,
);
if ( isset( $wpdb->last_error ) && $wpdb->last_error ) {
$data['wpdb'] = $wpdb->last_error;
}
$error->add_data( $data );
return $error;
}
public function permission_callback( WP_REST_Request $request ) {
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_PLUGIN );
}
public function get_route( $method, $callback, $permissions = false ) {
return [
'methods' => $method,
'callback' => [ $this, $callback ],
'permission_callback' => $permissions ? $permissions : [ $this, 'permission_callback' ],
];
}
}
class Redirection_Api_Filter_Route extends Redirection_Api_Route {
public function validate_filter( $value, $request, $param ) {
$fields = $request->get_attributes()['args']['filterBy']['filter_fields'];
if ( ! is_array( $value ) ) {
return new WP_Error( 'rest_invalid_param', 'Filter is not an array', array( 'status' => 400 ) );
}
if ( ! empty( $fields ) ) {
foreach ( array_keys( $value ) as $key ) {
if ( ! in_array( $key, $fields, true ) ) {
return new WP_Error( 'rest_invalid_param', 'Filter type is not supported: ' . $key, array( 'status' => 400 ) );
}
}
}
return true;
}
protected function get_filter_args( $order_fields, $filters = [] ) {
return [
'filterBy' => [
'description' => 'Field to filter by',
'validate_callback' => [ $this, 'validate_filter' ],
'filter_fields' => $filters,
],
'orderby' => [
'description' => 'Field to order results by',
'type' => 'string',
'enum' => $order_fields,
],
'direction' => [
'description' => 'Direction of ordered results',
'type' => 'string',
'default' => 'desc',
'enum' => [ 'asc', 'desc' ],
],
'per_page' => [
'description' => 'Number of results per page',
'type' => 'integer',
'default' => 25,
'minimum' => 5,
'maximum' => RED_MAX_PER_PAGE,
],
'page' => [
'description' => 'Page offset',
'type' => 'integer',
'minimum' => 0,
'default' => 0,
],
];
}
/**
* Register a bulk action route
*
* @param string $namespace Namespace.
* @param string $route Route.
* @param Array $orders
* @param Array $filters
* @param Object $callback
* @param boolean $permissions
* @return void
*/
public function register_bulk( $namespace, $route, $orders, $filters, $callback, $permissions = false ) {
register_rest_route( $namespace, $route, array(
$this->get_route( WP_REST_Server::EDITABLE, $callback, $permissions ),
'args' => array_merge( $this->get_filter_args( $orders, $filters ), [
'items' => [
'description' => 'Comma separated list of item IDs to perform action on',
'type' => 'array',
'items' => [
'type' => 'string',
],
],
] ),
) );
}
}
class Redirection_Api {
private static $instance = null;
private $routes = array();
public static function init() {
if ( is_null( self::$instance ) ) {
self::$instance = new Redirection_Api();
}
return self::$instance;
}
public function __construct() {
global $wpdb;
$wpdb->hide_errors();
$this->routes[] = new Redirection_Api_Redirect( REDIRECTION_API_NAMESPACE );
$this->routes[] = new Redirection_Api_Group( REDIRECTION_API_NAMESPACE );
$this->routes[] = new Redirection_Api_Log( REDIRECTION_API_NAMESPACE );
$this->routes[] = new Redirection_Api_404( REDIRECTION_API_NAMESPACE );
$this->routes[] = new Redirection_Api_Settings( REDIRECTION_API_NAMESPACE );
$this->routes[] = new Redirection_Api_Plugin( REDIRECTION_API_NAMESPACE );
$this->routes[] = new Redirection_Api_Import( REDIRECTION_API_NAMESPACE );
$this->routes[] = new Redirection_Api_Export( REDIRECTION_API_NAMESPACE );
}
}

View File

@@ -0,0 +1,391 @@
<?php
class Red_Database_Status {
// Used in < 3.7 versions of Redirection, but since migrated to general settings
const OLD_DB_VERSION = 'redirection_version';
const DB_UPGRADE_STAGE = 'database_stage';
const RESULT_OK = 'ok';
const RESULT_ERROR = 'error';
const STATUS_OK = 'ok';
const STATUS_NEED_INSTALL = 'need-install';
const STATUS_NEED_UPDATING = 'need-update';
const STATUS_FINISHED_INSTALL = 'finish-install';
const STATUS_FINISHED_UPDATING = 'finish-update';
private $stage = false;
private $stages = [];
private $status = false;
private $result = false;
private $reason = false;
private $debug = [];
public function __construct() {
$this->status = self::STATUS_OK;
if ( $this->needs_installing() ) {
$this->status = self::STATUS_NEED_INSTALL;
}
$this->load_stage();
if ( $this->needs_updating() ) {
$this->status = self::STATUS_NEED_UPDATING;
}
}
public function load_stage() {
$settings = red_get_options();
if ( isset( $settings[ self::DB_UPGRADE_STAGE ] ) ) {
$this->stage = isset( $settings[ self::DB_UPGRADE_STAGE ]['stage'] ) ? $settings[ self::DB_UPGRADE_STAGE ]['stage'] : false;
$this->stages = isset( $settings[ self::DB_UPGRADE_STAGE ]['stages'] ) ? $settings[ self::DB_UPGRADE_STAGE ]['stages'] : [];
$this->status = isset( $settings[ self::DB_UPGRADE_STAGE ]['status'] ) ? $settings[ self::DB_UPGRADE_STAGE ]['status'] : false;
}
}
/**
* Does the database need install
*
* @return bool true if needs installing, false otherwise
*/
public function needs_installing() {
$settings = red_get_options();
if ( $settings['database'] === '' && $this->get_old_version() === false ) {
return true;
}
return false;
}
/**
* Does the current database need updating to the target
*
* @return bool true if needs updating, false otherwise
*/
public function needs_updating() {
// We need updating if we don't need to install, and the current version is less than target version
if ( $this->needs_installing() === false && version_compare( $this->get_current_version(), REDIRECTION_DB_VERSION, '<' ) ) {
return true;
}
// Also if we're still in the process of upgrading
if ( $this->get_current_stage() && $this->status !== self::STATUS_NEED_INSTALL ) {
return true;
}
return false;
}
/**
* Get current database version
*
* @return string Current database version
*/
public function get_current_version() {
$settings = red_get_options();
if ( $settings['database'] !== '' && is_string( $settings['database'] ) ) {
if ( $settings['database'] === '+OK' ) {
return REDIRECTION_DB_VERSION;
}
return $settings['database'];
}
if ( $this->get_old_version() !== false ) {
$version = $this->get_old_version();
// Upgrade the old value
if ( $version ) {
red_set_options( array( 'database' => $version ) );
delete_option( self::OLD_DB_VERSION );
$this->clear_cache();
return $version;
}
}
return '';
}
private function get_old_version() {
return get_option( self::OLD_DB_VERSION );
}
public function check_tables_exist() {
$latest = Red_Database::get_latest_database();
$missing = $latest->get_missing_tables();
// No tables installed - do a fresh install
if ( count( $missing ) === count( $latest->get_all_tables() ) ) {
delete_option( Red_Database_Status::OLD_DB_VERSION );
red_set_options( [ 'database' => '' ] );
$this->clear_cache();
$this->status = self::STATUS_NEED_INSTALL;
$this->stop_update();
} elseif ( count( $missing ) > 0 && version_compare( $this->get_current_version(), '2.3.3', 'ge' ) ) {
// Some tables are missing - try and fill them in
$latest->install();
}
}
/**
* Does the current database support a particular version
*
* @param string $version Target version
* @return bool true if supported, false otherwise
*/
public function does_support( $version ) {
return version_compare( $this->get_current_version(), $version, 'ge' );
}
public function is_error() {
return $this->result === self::RESULT_ERROR;
}
public function set_error( $error ) {
global $wpdb;
$this->result = self::RESULT_ERROR;
$this->reason = str_replace( "\t", ' ', $error );
if ( $wpdb->last_error ) {
$this->debug[] = $wpdb->last_error;
if ( strpos( $wpdb->last_error, 'command denied to user' ) !== false ) {
$this->reason .= ' - ' . __( 'Insufficient database permissions detected. Please give your database user appropriate permissions.', 'redirection' );
}
}
$latest = Red_Database::get_latest_database();
$this->debug = array_merge( $this->debug, $latest->get_table_schema() );
$this->debug[] = 'Stage: ' . $this->get_current_stage();
}
public function set_ok( $reason ) {
$this->reason = $reason;
$this->result = self::RESULT_OK;
$this->debug = [];
}
/**
* Stop current upgrade
*/
public function stop_update() {
$this->stage = false;
$this->stages = [];
$this->debug = [];
red_set_options( [ self::DB_UPGRADE_STAGE => false ] );
$this->clear_cache();
}
public function finish() {
$this->stop_update();
if ( $this->status === self::STATUS_NEED_INSTALL ) {
$this->status = self::STATUS_FINISHED_INSTALL;
} elseif ( $this->status === self::STATUS_NEED_UPDATING ) {
$this->status = self::STATUS_FINISHED_UPDATING;
}
}
/**
* Get current upgrade stage
* @return string|bool Current stage name, or false if not upgrading
*/
public function get_current_stage() {
return $this->stage;
}
/**
* Move current stage on to the next
*/
public function set_next_stage() {
$this->debug = [];
$stage = $this->get_current_stage();
if ( $stage ) {
$stage = $this->get_next_stage( $stage );
// Save next position
if ( $stage ) {
$this->set_stage( $stage );
} else {
$this->finish();
}
}
}
/**
* Get current upgrade status
*
* @return array Database status array
*/
public function get_json() {
// Base information
$result = [
'status' => $this->status,
'inProgress' => $this->stage !== false,
];
// Add on version status
if ( $this->status === self::STATUS_NEED_INSTALL || $this->status === self::STATUS_NEED_UPDATING ) {
$result = array_merge(
$result,
$this->get_version_upgrade(),
[ 'manual' => $this->get_manual_upgrade() ]
);
}
// Add on upgrade status
if ( $this->is_error() ) {
$result = array_merge( $result, $this->get_version_upgrade(), $this->get_progress_status(), $this->get_error_status() );
} elseif ( $result['inProgress'] ) {
$result = array_merge( $result, $this->get_progress_status() );
} elseif ( $this->status === self::STATUS_FINISHED_INSTALL || $this->status === self::STATUS_FINISHED_UPDATING ) {
$result['complete'] = 100;
$result['reason'] = $this->reason;
}
return $result;
}
private function get_error_status() {
return [
'reason' => $this->reason,
'result' => self::RESULT_ERROR,
'debug' => $this->debug,
];
}
private function get_progress_status() {
$complete = 0;
if ( $this->stage ) {
$complete = round( ( array_search( $this->stage, $this->stages, true ) / count( $this->stages ) ) * 100, 1 );
}
return [
'complete' => $complete,
'result' => self::RESULT_OK,
'reason' => $this->reason,
];
}
private function get_version_upgrade() {
return [
'current' => $this->get_current_version() ? $this->get_current_version() : '-',
'next' => REDIRECTION_DB_VERSION,
'time' => microtime( true ),
];
}
/**
* Set the status information for a database upgrade
*/
public function start_install( array $upgrades ) {
$this->set_stages( $upgrades );
$this->status = self::STATUS_NEED_INSTALL;
}
public function start_upgrade( array $upgrades ) {
$this->set_stages( $upgrades );
$this->status = self::STATUS_NEED_UPDATING;
}
private function set_stages( array $upgrades ) {
$this->stages = [];
foreach ( $upgrades as $upgrade ) {
$upgrader = Red_Database_Upgrader::get( $upgrade );
$this->stages = array_merge( $this->stages, array_keys( $upgrader->get_stages() ) );
}
if ( count( $this->stages ) > 0 ) {
$this->set_stage( $this->stages[0] );
}
}
public function set_stage( $stage ) {
$this->stage = $stage;
$this->save_details();
}
private function save_details() {
$stages = [
self::DB_UPGRADE_STAGE => [
'stage' => $this->stage,
'stages' => $this->stages,
'status' => $this->status,
],
];
red_set_options( $stages );
$this->clear_cache();
}
private function get_manual_upgrade() {
$queries = [];
$database = new Red_Database();
$upgraders = $database->get_upgrades_for_version( $this->get_current_version(), false );
foreach ( $upgraders as $upgrade ) {
$upgrade = Red_Database_Upgrader::get( $upgrade );
$stages = $upgrade->get_stages();
foreach ( array_keys( $stages ) as $stage ) {
$queries = array_merge( $queries, $upgrade->get_queries_for_stage( $stage ) );
}
}
return $queries;
}
private function get_next_stage( $stage ) {
$database = new Red_Database();
$upgraders = $database->get_upgrades_for_version( $this->get_current_version(), $this->get_current_stage() );
if ( count( $upgraders ) === 0 ) {
$upgraders = $database->get_upgrades_for_version( $this->get_current_version(), false );
}
$upgrader = Red_Database_Upgrader::get( $upgraders[0] );
// Where are we in this?
$pos = array_search( $this->stage, $this->stages, true );
if ( $pos === count( $this->stages ) - 1 ) {
$this->save_db_version( REDIRECTION_DB_VERSION );
return false;
}
// Set current DB version
$current_stages = array_keys( $upgrader->get_stages() );
if ( array_search( $this->stage, $current_stages, true ) === count( $current_stages ) - 1 ) {
$this->save_db_version( $upgraders[1]['version'] );
}
// Move on to next in current version
return $this->stages[ $pos + 1 ];
}
public function save_db_version( $version ) {
red_set_options( array( 'database' => $version ) );
delete_option( self::OLD_DB_VERSION );
$this->clear_cache();
}
private function clear_cache() {
if ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) && function_exists( 'wp_cache_flush' ) ) {
wp_cache_flush();
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
abstract class Red_Database_Upgrader {
private $queries = [];
private $live = true;
/**
* Return an array of all the stages for an upgrade
*
* @return array stage name => reason
*/
abstract public function get_stages();
public function get_reason( $stage ) {
$stages = $this->get_stages();
if ( isset( $stages[ $stage ] ) ) {
return $stages[ $stage ];
}
return 'Unknown';
}
/**
* Run a particular stage on the current upgrader
*
* @return Red_Database_Status
*/
public function perform_stage( Red_Database_Status $status ) {
global $wpdb;
$stage = $status->get_current_stage();
if ( $this->has_stage( $stage ) && method_exists( $this, $stage ) ) {
try {
$this->$stage( $wpdb );
$status->set_ok( $this->get_reason( $stage ) );
} catch ( Exception $e ) {
$status->set_error( $e->getMessage() );
}
} else {
$status->set_error( 'No stage found for upgrade ' . $stage );
}
}
public function get_queries_for_stage( $stage ) {
global $wpdb;
$this->queries = [];
$this->live = false;
$this->$stage( $wpdb, false );
$this->live = true;
return $this->queries;
}
/**
* Returns the current database charset
*
* @return string Database charset
*/
public function get_charset() {
global $wpdb;
$charset_collate = '';
if ( ! empty( $wpdb->charset ) ) {
// Fix some common invalid charset values
$fixes = [
'utf-8',
'utf',
];
$charset = $wpdb->charset;
if ( in_array( strtolower( $charset ), $fixes, true ) ) {
$charset = 'utf8';
}
$charset_collate = "DEFAULT CHARACTER SET $charset";
}
if ( ! empty( $wpdb->collate ) ) {
$charset_collate .= " COLLATE=$wpdb->collate";
}
return $charset_collate;
}
/**
* Performs a $wpdb->query, and throws an exception if an error occurs
*
* @return bool true if query is performed ok, otherwise an exception is thrown
*/
protected function do_query( $wpdb, $sql ) {
if ( ! $this->live ) {
$this->queries[] = $sql;
return true;
}
// These are known queries without user input
// phpcs:ignore
$result = $wpdb->query( $sql );
if ( $result === false ) {
/* translators: 1: SQL string */
throw new Exception( sprintf( 'Failed to perform query "%s"', $sql ) );
}
return true;
}
/**
* Load a database upgrader class
*
* @return object Database upgrader
*/
public static function get( $version ) {
include_once dirname( __FILE__ ) . '/schema/' . str_replace( [ '..', '/' ], '', $version['file'] );
return new $version['class'];
}
private function has_stage( $stage ) {
return in_array( $stage, array_keys( $this->get_stages() ), true );
}
}

View File

@@ -0,0 +1,170 @@
<?php
require_once __DIR__ . '/database-status.php';
require_once __DIR__ . '/database-upgrader.php';
class Red_Database {
/**
* Get all upgrades for a database version
*
* @return array Array of versions from self::get_upgrades()
*/
public function get_upgrades_for_version( $current_version, $current_stage ) {
if ( empty( $current_version ) ) {
return [
[
'version' => REDIRECTION_DB_VERSION,
'file' => 'latest.php',
'class' => 'Red_Latest_Database',
],
];
}
$upgraders = [];
$found = false;
foreach ( $this->get_upgrades() as $upgrade ) {
if ( ! $found ) {
$upgrader = Red_Database_Upgrader::get( $upgrade );
$stage_present = in_array( $current_stage, array_keys( $upgrader->get_stages() ), true );
$same_version = $current_stage === false && version_compare( $upgrade['version'], $current_version, 'gt' );
if ( $stage_present || $same_version ) {
$found = true;
}
}
if ( $found ) {
$upgraders[] = $upgrade;
}
}
return $upgraders;
}
/**
* Apply a particular upgrade stage
*
* @return mixed Result for upgrade
*/
public function apply_upgrade( Red_Database_Status $status ) {
$upgraders = $this->get_upgrades_for_version( $status->get_current_version(), $status->get_current_stage() );
if ( count( $upgraders ) === 0 ) {
$status->set_error( 'No upgrades found for version ' . $status->get_current_version() );
return;
}
if ( $status->get_current_stage() === false ) {
if ( $status->needs_installing() ) {
$status->start_install( $upgraders );
} else {
$status->start_upgrade( $upgraders );
}
}
// Look at first upgrade
$upgrader = Red_Database_Upgrader::get( $upgraders[0] );
// Perform the upgrade
$upgrader->perform_stage( $status );
if ( ! $status->is_error() ) {
$status->set_next_stage();
}
}
public static function apply_to_sites( $callback ) {
if ( is_multisite() && ( is_network_admin() || defined( 'WP_CLI' ) && WP_CLI ) ) {
$total = get_sites( [ 'count' => true ] );
$per_page = 100;
// Paginate through all sites and apply the callback
for ( $offset = 0; $offset < $total; $offset += $per_page ) {
array_map( function( $site ) use ( $callback ) {
switch_to_blog( $site->blog_id );
$callback();
restore_current_blog();
}, get_sites( [ 'number' => $per_page, 'offset' => $offset ] ) );
}
return;
}
$callback();
}
/**
* Get latest database installer
*
* @return object Red_Latest_Database
*/
public static function get_latest_database() {
include_once dirname( __FILE__ ) . '/schema/latest.php';
return new Red_Latest_Database();
}
/**
* List of all upgrades and their associated file
*
* @return array Database upgrade array
*/
public function get_upgrades() {
return [
[
'version' => '2.0.1',
'file' => '201.php',
'class' => 'Red_Database_201',
],
[
'version' => '2.1.16',
'file' => '216.php',
'class' => 'Red_Database_216',
],
[
'version' => '2.2',
'file' => '220.php',
'class' => 'Red_Database_220',
],
[
'version' => '2.3.1',
'file' => '231.php',
'class' => 'Red_Database_231',
],
[
'version' => '2.3.2',
'file' => '232.php',
'class' => 'Red_Database_232',
],
[
'version' => '2.3.3',
'file' => '233.php',
'class' => 'Red_Database_233',
],
[
'version' => '2.4',
'file' => '240.php',
'class' => 'Red_Database_240',
],
[
'version' => '4.0',
'file' => '400.php',
'class' => 'Red_Database_400',
],
[
'version' => '4.1',
'file' => '410.php',
'class' => 'Red_Database_410',
],
[
'version' => '4.2',
'file' => '420.php',
'class' => 'Red_Database_420',
],
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
// Note: not localised as the messages aren't important enough
class Red_Database_201 extends Red_Database_Upgrader {
public function get_stages() {
return [
'add_title_201' => 'Add titles to redirects',
];
}
protected function add_title_201( $wpdb ) {
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD `title` varchar(50) NULL" );
}
}

View File

@@ -0,0 +1,26 @@
<?php
// Note: not localised as the messages aren't important enough
class Red_Database_216 extends Red_Database_Upgrader {
public function get_stages() {
return [
'add_group_indices_216' => 'Add indices to groups',
'add_redirect_indices_216' => 'Add indices to redirects',
];
}
protected function add_group_indices_216( $wpdb ) {
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_groups` ADD INDEX(module_id)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_groups` ADD INDEX(status)" );
return true;
}
protected function add_redirect_indices_216( $wpdb ) {
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD INDEX(url(191))" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD INDEX(status)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD INDEX(regex)" );
return true;
}
}

View File

@@ -0,0 +1,26 @@
<?php
// Note: not localised as the messages aren't important enough
class Red_Database_220 extends Red_Database_Upgrader {
public function get_stages() {
return [
'add_group_indices_220' => 'Add group indices to redirects',
'add_log_indices_220' => 'Add indices to logs',
];
}
protected function add_group_indices_220( $wpdb ) {
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD INDEX `group_idpos` (`group_id`,`position`)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD INDEX `group` (`group_id`)" );
return true;
}
protected function add_log_indices_220( $wpdb ) {
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD INDEX `created` (`created`)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD INDEX `redirection_id` (`redirection_id`)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD INDEX `ip` (`ip`)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD INDEX `group_id` (`group_id`)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD INDEX `module_id` (`module_id`)" );
return true;
}
}

View File

@@ -0,0 +1,37 @@
<?php
// Note: not localised as the messages aren't important enough
class Red_Database_231 extends Red_Database_Upgrader {
public function get_stages() {
return [
'remove_404_module_231' => 'Remove 404 module',
'create_404_table_231' => 'Create 404 table',
];
}
protected function remove_404_module_231( $wpdb ) {
return $this->do_query( $wpdb, "UPDATE {$wpdb->prefix}redirection_groups SET module_id=1 WHERE module_id=3" );
}
protected function create_404_table_231( $wpdb ) {
$this->do_query( $wpdb, $this->get_404_table( $wpdb ) );
}
private function get_404_table( $wpdb ) {
$charset_collate = $this->get_charset();
return "CREATE TABLE `{$wpdb->prefix}redirection_404` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`url` varchar(255) NOT NULL DEFAULT '',
`agent` varchar(255) DEFAULT NULL,
`referrer` varchar(255) DEFAULT NULL,
`ip` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `created` (`created`),
KEY `url` (`url`(191)),
KEY `ip` (`ip`),
KEY `referrer` (`referrer`(191))
) $charset_collate";
}
}

View File

@@ -0,0 +1,15 @@
<?php
// Note: not localised as the messages aren't important enough
class Red_Database_232 extends Red_Database_Upgrader {
public function get_stages() {
return [
'remove_modules_232' => 'Remove module table',
];
}
protected function remove_modules_232( $wpdb ) {
$this->do_query( $wpdb, "DROP TABLE IF EXISTS {$wpdb->prefix}redirection_modules" );
return true;
}
}

View File

@@ -0,0 +1,17 @@
<?php
// Note: not localised as the messages aren't important enough
class Red_Database_233 extends Red_Database_Upgrader {
public function get_stages() {
return [
'fix_invalid_groups_233' => 'Migrate any groups with invalid module ID',
];
}
protected function fix_invalid_groups_233( $wpdb ) {
$this->do_query( $wpdb, "UPDATE {$wpdb->prefix}redirection_groups SET module_id=1 WHERE module_id > 2" );
$latest = Red_Database::get_latest_database();
return $latest->create_groups( $wpdb );
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* There are several problems with 2.3.3 => 2.4 that this attempts to cope with:
* - some sites have a misconfigured IP column
* - some sites don't have any IP column
*/
class Red_Database_240 extends Red_Database_Upgrader {
public function get_stages() {
return [
'convert_int_ip_to_varchar_240' => 'Convert integer IP values to support IPv6',
'expand_log_ip_column_240' => 'Expand IP size in logs to support IPv6',
'convert_title_to_text_240' => 'Expand size of redirect titles',
'add_missing_index_240' => 'Add missing IP index to 404 logs',
];
}
private function has_ip_index( $wpdb ) {
$wpdb->hide_errors();
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_404`", ARRAY_N );
$wpdb->show_errors();
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), 'key `ip` (' ) !== false ) {
return true;
}
return false;
}
protected function has_varchar_ip( $wpdb ) {
$wpdb->hide_errors();
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_404`", ARRAY_N );
$wpdb->show_errors();
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), '`ip` varchar(45)' ) !== false ) {
return true;
}
return false;
}
protected function has_int_ip( $wpdb ) {
$wpdb->hide_errors();
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_404`", ARRAY_N );
$wpdb->show_errors();
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), '`ip` int' ) !== false ) {
return true;
}
return false;
}
protected function convert_int_ip_to_varchar_240( $wpdb ) {
if ( $this->has_int_ip( $wpdb ) ) {
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` ADD `ipaddress` VARCHAR(45) DEFAULT NULL AFTER `ip`" );
$this->do_query( $wpdb, "UPDATE {$wpdb->prefix}redirection_404 SET ipaddress=INET_NTOA(ip)" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` DROP `ip`" );
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` CHANGE `ipaddress` `ip` VARCHAR(45) DEFAULT NULL" );
}
return true;
}
protected function expand_log_ip_column_240( $wpdb ) {
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` CHANGE `ip` `ip` VARCHAR(45) DEFAULT NULL" );
}
protected function add_missing_index_240( $wpdb ) {
if ( $this->has_ip_index( $wpdb ) ) {
// Remove index
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` DROP INDEX ip" );
}
// Ensure we have an IP column
$this->convert_int_ip_to_varchar_240( $wpdb );
if ( ! $this->has_varchar_ip( $wpdb ) ) {
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` ADD `ip` VARCHAR(45) DEFAULT NULL" );
}
// Finally add the index
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` ADD INDEX `ip` (`ip`)" );
}
protected function convert_title_to_text_240( $wpdb ) {
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` CHANGE `title` `title` text" );
}
}

View File

@@ -0,0 +1,72 @@
<?php
class Red_Database_400 extends Red_Database_Upgrader {
public function get_stages() {
return [
'add_match_url_400' => 'Add a matched URL column',
'add_match_url_index' => 'Add match URL index',
'add_redirect_data_400' => 'Add column to store new flags',
'convert_existing_urls_400' => 'Convert existing URLs to new format',
];
}
private function has_column( $wpdb, $column ) {
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_items`", ARRAY_N );
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), strtolower( $column ) ) !== false ) {
return true;
}
return false;
}
private function has_match_index( $wpdb ) {
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_items`", ARRAY_N );
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), 'key `match_url' ) !== false ) {
return true;
}
return false;
}
protected function add_match_url_400( $wpdb ) {
if ( ! $this->has_column( $wpdb, '`match_url` varchar(2000)' ) ) {
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD `match_url` VARCHAR(2000) NULL DEFAULT NULL AFTER `url`" );
}
return true;
}
protected function add_match_url_index( $wpdb ) {
if ( ! $this->has_match_index( $wpdb ) ) {
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD INDEX `match_url` (`match_url`(191))" );
}
}
protected function add_redirect_data_400( $wpdb ) {
if ( ! $this->has_column( $wpdb, '`match_data` TEXT' ) ) {
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD `match_data` TEXT NULL DEFAULT NULL AFTER `match_url`" );
}
return true;
}
protected function convert_existing_urls_400( $wpdb ) {
// All regex get match_url=regex
$this->do_query( $wpdb, "UPDATE `{$wpdb->prefix}redirection_items` SET match_url='regex' WHERE regex=1" );
// Remove query part from all URLs and lowercase
$this->do_query( $wpdb, "UPDATE `{$wpdb->prefix}redirection_items` SET match_url=LOWER(url) WHERE regex=0" );
// Set exact match if query param present
$this->do_query( $wpdb, $wpdb->prepare( "UPDATE `{$wpdb->prefix}redirection_items` SET match_data=%s WHERE regex=0 AND match_url LIKE '%?%'", '{"source":{"flag_query":"exactorder"}}' ) );
// Trim the last / from a URL
$this->do_query( $wpdb, "UPDATE `{$wpdb->prefix}redirection_items` SET match_url=LEFT(match_url,LENGTH(match_url)-1) WHERE regex=0 AND match_url != '/' AND RIGHT(match_url, 1) = '/'" );
$this->do_query( $wpdb, "UPDATE `{$wpdb->prefix}redirection_items` SET match_url=REPLACE(match_url, '/?', '?') WHERE regex=0" );
// Any URL that is now empty becomes /
return $this->do_query( $wpdb, "UPDATE `{$wpdb->prefix}redirection_items` SET match_url='/' WHERE match_url=''" );
}
}

View File

@@ -0,0 +1,17 @@
<?php
class Red_Database_410 extends Red_Database_Upgrader {
public function get_stages() {
return [
'handle_double_slash' => 'Support double-slash URLs',
];
}
protected function handle_double_slash( $wpdb ) {
// Update any URL with a double slash at the end
$this->do_query( $wpdb, "UPDATE `{$wpdb->prefix}redirection_items` SET match_url=LOWER(LEFT(SUBSTRING_INDEX(url, '?', 1),LENGTH(SUBSTRING_INDEX(url, '?', 1)) - 1)) WHERE RIGHT(SUBSTRING_INDEX(url, '?', 1), 2) = '//' AND regex=0" );
// Any URL that is now empty becomes /
return $this->do_query( $wpdb, "UPDATE `{$wpdb->prefix}redirection_items` SET match_url='/' WHERE match_url=''" );
}
}

View File

@@ -0,0 +1,99 @@
<?php
class Red_Database_420 extends Red_Database_Upgrader {
public function get_stages() {
return [
'add_extra_logging' => 'Add extra logging support',
'remove_module_id' => 'Remove module ID from logs',
'remove_group_id' => 'Remove group ID from logs',
'add_extra_404' => 'Add extra 404 logging support',
];
}
protected function remove_module_id( $wpdb ) {
if ( ! $this->has_module_id( $wpdb ) ) {
return true;
}
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` DROP `module_id`" );
}
protected function remove_group_id( $wpdb ) {
if ( ! $this->has_group_id( $wpdb ) ) {
return true;
}
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` DROP `group_id`" );
}
private function has_module_id( $wpdb ) {
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_logs`", ARRAY_N );
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), 'module_id' ) !== false ) {
return true;
}
return false;
}
private function has_group_id( $wpdb ) {
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_logs`", ARRAY_N );
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), 'group_id' ) !== false ) {
return true;
}
return false;
}
private function has_log_domain( $wpdb ) {
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_logs`", ARRAY_N );
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), 'domain` varchar' ) !== false ) {
return true;
}
return false;
}
private function has_404_domain( $wpdb ) {
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_404`", ARRAY_N );
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), 'domain` varchar' ) !== false ) {
return true;
}
return false;
}
protected function add_extra_logging( $wpdb ) {
if ( $this->has_log_domain( $wpdb ) ) {
return true;
}
// Update any URL with a double slash at the end
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD `domain` VARCHAR(255) NULL DEFAULT NULL AFTER `url`" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD `http_code` INT(11) unsigned NOT NULL DEFAULT 0 AFTER `referrer`" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD `request_method` VARCHAR(10) NULL DEFAULT NULL AFTER `http_code`" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD `redirect_by` VARCHAR(50) NULL DEFAULT NULL AFTER `request_method`" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` ADD `request_data` MEDIUMTEXT NULL DEFAULT NULL AFTER `request_method`" );
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_logs` CHANGE COLUMN `agent` `agent` MEDIUMTEXT NULL" );
}
protected function add_extra_404( $wpdb ) {
if ( $this->has_404_domain( $wpdb ) ) {
return true;
}
// Update any URL with a double slash at the end
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` ADD `domain` VARCHAR(255) NULL DEFAULT NULL AFTER `url`" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` ADD `http_code` INT(11) unsigned NOT NULL DEFAULT 0 AFTER `referrer`" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` ADD `request_method` VARCHAR(10) NULL DEFAULT NULL AFTER `http_code`" );
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` ADD `request_data` MEDIUMTEXT NULL DEFAULT NULL AFTER `request_method`" );
// Same as log table
$this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` DROP INDEX `url`" );
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_404` CHANGE COLUMN `url` `url` MEDIUMTEXT NOT NULL" );
}
}

View File

@@ -0,0 +1,260 @@
<?php
/**
* Latest database schema
*/
class Red_Latest_Database extends Red_Database_Upgrader {
public function get_stages() {
return [
/* translators: displayed when installing the plugin */
'create_tables' => __( 'Install Redirection tables', 'redirection' ),
/* translators: displayed when installing the plugin */
'create_groups' => __( 'Create basic data', 'redirection' ),
];
}
/**
* Install the latest database
*
* @return bool|WP_Error true if installed, WP_Error otherwise
*/
public function install() {
global $wpdb;
foreach ( $this->get_stages() as $stage => $info ) {
$result = $this->$stage( $wpdb );
if ( is_wp_error( $result ) ) {
if ( $wpdb->last_error ) {
$result->add_data( $wpdb->last_error );
}
return $result;
}
}
red_set_options( array( 'database' => REDIRECTION_DB_VERSION ) );
return true;
}
/**
* Remove the database and any options (including unused ones)
*/
public function remove() {
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}redirection_items" );
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}redirection_logs" );
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}redirection_groups" );
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}redirection_modules" );
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}redirection_404" );
delete_option( 'redirection_lookup' );
delete_option( 'redirection_post' );
delete_option( 'redirection_root' );
delete_option( 'redirection_index' );
delete_option( 'redirection_options' );
delete_option( Red_Database_Status::OLD_DB_VERSION );
}
/**
* Return any tables that are missing from the database
*
* @return array Array of missing table names
*/
public function get_missing_tables() {
global $wpdb;
$tables = array_keys( $this->get_all_tables() );
$missing = [];
foreach ( $tables as $table ) {
$result = $wpdb->query( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
if ( intval( $result, 10 ) !== 1 ) {
$missing[] = $table;
}
}
return $missing;
}
/**
* Get table schema for latest database tables
*
* @return array Database schema array
*/
public function get_table_schema() {
global $wpdb;
$tables = array_keys( $this->get_all_tables() );
$show = array();
foreach ( $tables as $table ) {
// These are known queries without user input
// phpcs:ignore
$row = $wpdb->get_row( 'SHOW CREATE TABLE ' . $table, ARRAY_N );
if ( $row ) {
$show = array_merge( $show, explode( "\n", $row[1] ) );
$show[] = '';
} else {
/* translators: 1: table name */
$show[] = sprintf( __( 'Table "%s" is missing', 'redirection' ), $table );
}
}
return $show;
}
/**
* Return array of table names and table schema
*
* @return array
*/
public function get_all_tables() {
global $wpdb;
$charset_collate = $this->get_charset();
return array(
"{$wpdb->prefix}redirection_items" => $this->create_items_sql( $wpdb->prefix, $charset_collate ),
"{$wpdb->prefix}redirection_groups" => $this->create_groups_sql( $wpdb->prefix, $charset_collate ),
"{$wpdb->prefix}redirection_logs" => $this->create_log_sql( $wpdb->prefix, $charset_collate ),
"{$wpdb->prefix}redirection_404" => $this->create_404_sql( $wpdb->prefix, $charset_collate ),
);
}
/**
* Creates default group information
*/
public function create_groups( $wpdb, $is_live = true ) {
if ( ! $is_live ) {
return true;
}
$defaults = [
[
'name' => __( 'Redirections', 'redirection' ),
'module_id' => 1,
'position' => 0,
],
[
'name' => __( 'Modified Posts', 'redirection' ),
'module_id' => 1,
'position' => 1,
],
];
$existing_groups = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}redirection_groups" );
// Default groups
if ( intval( $existing_groups, 10 ) === 0 ) {
$wpdb->insert( $wpdb->prefix . 'redirection_groups', $defaults[0] );
$wpdb->insert( $wpdb->prefix . 'redirection_groups', $defaults[1] );
}
$group = $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}redirection_groups LIMIT 1" );
if ( $group ) {
red_set_options( array( 'last_group_id' => $group->id ) );
}
return true;
}
/**
* Creates all the tables
*/
public function create_tables( $wpdb ) {
global $wpdb;
foreach ( $this->get_all_tables() as $table => $sql ) {
$sql = preg_replace( '/[ \t]{2,}/', '', $sql );
$this->do_query( $wpdb, $sql );
}
return true;
}
private function create_items_sql( $prefix, $charset_collate ) {
return "CREATE TABLE IF NOT EXISTS `{$prefix}redirection_items` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`url` mediumtext NOT NULL,
`match_url` VARCHAR(2000) DEFAULT NULL,
`match_data` TEXT,
`regex` INT(11) unsigned NOT NULL DEFAULT '0',
`position` INT(11) unsigned NOT NULL DEFAULT '0',
`last_count` INT(10) unsigned NOT NULL DEFAULT '0',
`last_access` datetime NOT NULL DEFAULT '1970-01-01 00:00:00',
`group_id` INT(11) NOT NULL DEFAULT '0',
`status` enum('enabled','disabled') NOT NULL DEFAULT 'enabled',
`action_type` VARCHAR(20) NOT NULL,
`action_code` INT(11) unsigned NOT NULL,
`action_data` MEDIUMTEXT,
`match_type` VARCHAR(20) NOT NULL,
`title` TEXT,
PRIMARY KEY (`id`),
KEY `url` (`url`(191)),
KEY `status` (`status`),
KEY `regex` (`regex`),
KEY `group_idpos` (`group_id`,`position`),
KEY `group` (`group_id`),
KEY `match_url` (`match_url`(191))
) $charset_collate";
}
private function create_groups_sql( $prefix, $charset_collate ) {
return "CREATE TABLE IF NOT EXISTS `{$prefix}redirection_groups` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`tracking` INT(11) NOT NULL DEFAULT '1',
`module_id` INT(11) unsigned NOT NULL DEFAULT '0',
`status` enum('enabled','disabled') NOT NULL DEFAULT 'enabled',
`position` INT(11) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `module_id` (`module_id`),
KEY `status` (`status`)
) $charset_collate";
}
private function create_log_sql( $prefix, $charset_collate ) {
return "CREATE TABLE IF NOT EXISTS `{$prefix}redirection_logs` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`url` MEDIUMTEXT NOT NULL,
`domain` VARCHAR(255) DEFAULT NULL,
`sent_to` MEDIUMTEXT,
`agent` MEDIUMTEXT,
`referrer` MEDIUMTEXT,
`http_code` INT(11) unsigned NOT NULL DEFAULT '0',
`request_method` VARCHAR(10) DEFAULT NULL,
`request_data` MEDIUMTEXT,
`redirect_by` VARCHAR(50) DEFAULT NULL,
`redirection_id` INT(11) unsigned DEFAULT NULL,
`ip` VARCHAR(45) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `created` (`created`),
KEY `redirection_id` (`redirection_id`),
KEY `ip` (`ip`)
) $charset_collate";
}
private function create_404_sql( $prefix, $charset_collate ) {
return "CREATE TABLE IF NOT EXISTS `{$prefix}redirection_404` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`url` MEDIUMTEXT NOT NULL,
`domain` VARCHAR(255) DEFAULT NULL,
`agent` VARCHAR(255) DEFAULT NULL,
`referrer` VARCHAR(255) DEFAULT NULL,
`http_code` INT(11) unsigned NOT NULL DEFAULT '0',
`request_method` VARCHAR(10) DEFAULT NULL,
`request_data` MEDIUMTEXT,
`ip` VARCHAR(45) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `created` (`created`),
KEY `referrer` (`referrer`(191)),
KEY `ip` (`ip`)
) $charset_collate";
}
}

View File

@@ -0,0 +1,192 @@
<?php
class Red_Apache_File extends Red_FileIO {
public function force_download() {
parent::force_download();
header( 'Content-Type: application/octet-stream' );
header( 'Content-Disposition: attachment; filename="' . $this->export_filename( 'htaccess' ) . '"' );
}
public function get_data( array $items, array $groups ) {
include_once dirname( dirname( __FILE__ ) ) . '/models/htaccess.php';
$htaccess = new Red_Htaccess();
foreach ( $items as $item ) {
$htaccess->add( $item );
}
return $htaccess->get() . PHP_EOL;
}
public function load( $group, $filename, $data ) {
// Remove any comments
$data = str_replace( "\n", "\r", $data );
// Split it into lines
$lines = array_filter( explode( "\r", $data ) );
$count = 0;
foreach ( (array) $lines as $line ) {
$item = $this->get_as_item( $line );
if ( $item ) {
$item['group_id'] = $group;
$redirect = Red_Item::create( $item );
if ( ! is_wp_error( $redirect ) ) {
$count++;
}
}
}
return $count;
}
public function get_as_item( $line ) {
$item = false;
if ( preg_match( '@rewriterule\s+(.*?)\s+(.*?)\s+(\[.*\])*@i', $line, $matches ) > 0 ) {
$item = array(
'url' => $this->regex_url( $matches[1] ),
'match_type' => 'url',
'action_type' => 'url',
'action_data' => array( 'url' => $this->decode_url( $matches[2] ) ),
'action_code' => $this->get_code( $matches[3] ),
'regex' => $this->is_regex( $matches[1] ),
);
} elseif ( preg_match( '@Redirect\s+(.*?)\s+"(.*?)"\s+(.*)@i', $line, $matches ) > 0 || preg_match( '@Redirect\s+(.*?)\s+(.*?)\s+(.*)@i', $line, $matches ) > 0 ) {
$item = array(
'url' => $this->decode_url( $matches[2] ),
'match_type' => 'url',
'action_type' => 'url',
'action_data' => array( 'url' => $this->decode_url( $matches[3] ) ),
'action_code' => $this->get_code( $matches[1] ),
);
} elseif ( preg_match( '@Redirect\s+"(.*?)"\s+(.*)@i', $line, $matches ) > 0 || preg_match( '@Redirect\s+(.*?)\s+(.*)@i', $line, $matches ) > 0 ) {
$item = array(
'url' => $this->decode_url( $matches[1] ),
'match_type' => 'url',
'action_type' => 'url',
'action_data' => array( 'url' => $this->decode_url( $matches[2] ) ),
'action_code' => 302,
);
} elseif ( preg_match( '@Redirectmatch\s+(.*?)\s+(.*?)\s+(.*)@i', $line, $matches ) > 0 ) {
$item = array(
'url' => $this->decode_url( $matches[2] ),
'match_type' => 'url',
'action_type' => 'url',
'action_data' => array( 'url' => $this->decode_url( $matches[3] ) ),
'action_code' => $this->get_code( $matches[1] ),
'regex' => true,
);
} elseif ( preg_match( '@Redirectmatch\s+(.*?)\s+(.*)@i', $line, $matches ) > 0 ) {
$item = array(
'url' => $this->decode_url( $matches[1] ),
'match_type' => 'url',
'action_type' => 'url',
'action_data' => array( 'url' => $this->decode_url( $matches[2] ) ),
'action_code' => 302,
'regex' => true,
);
}
if ( $item ) {
$item['action_type'] = 'url';
$item['match_type'] = 'url';
if ( $item['action_code'] === 0 ) {
$item['action_type'] = 'pass';
}
return $item;
}
return false;
}
private function decode_url( $url ) {
$url = rawurldecode( $url );
// Replace quoted slashes
$url = preg_replace( '@\\\/@', '/', $url );
// Ensure escaped '.' is still escaped
$url = preg_replace( '@\\\\.@', '\\\\.', $url );
return $url;
}
private function is_str_regex( $url ) {
$regex = '()[]$^?+.';
$escape = false;
$len = strlen( $url );
for ( $x = 0; $x < $len; $x++ ) {
$escape = false;
$char = substr( $url, $x, 1 );
if ( $char === '\\' ) {
$escape = true;
} elseif ( strpos( $regex, $char ) !== false && ! $escape ) {
return true;
}
}
return false;
}
private function is_regex( $url ) {
if ( $this->is_str_regex( $url ) ) {
$tmp = ltrim( $url, '^' );
$tmp = rtrim( $tmp, '$' );
if ( $this->is_str_regex( $tmp ) ) {
return true;
}
}
return false;
}
private function regex_url( $url ) {
$url = $this->decode_url( $url );
if ( $this->is_str_regex( $url ) ) {
$tmp = ltrim( $url, '^' );
$tmp = rtrim( $tmp, '$' );
if ( $this->is_str_regex( $tmp ) ) {
return '^/' . ltrim( $tmp, '/' );
}
return '/' . ltrim( $tmp, '/' );
}
return $this->decode_url( $url );
}
private function get_code( $code ) {
if ( strpos( $code, '301' ) !== false || stripos( $code, 'permanent' ) !== false ) {
return 301;
}
if ( strpos( $code, '302' ) !== false ) {
return 302;
}
if ( strpos( $code, '307' ) !== false || stripos( $code, 'seeother' ) !== false ) {
return 307;
}
if ( strpos( $code, '404' ) !== false || stripos( $code, 'forbidden' ) !== false || strpos( $code, 'F' ) !== false ) {
return 404;
}
if ( strpos( $code, '410' ) !== false || stripos( $code, 'gone' ) !== false || strpos( $code, 'G' ) !== false ) {
return 410;
}
return 302;
}
}

View File

@@ -0,0 +1,172 @@
<?php
class Red_Csv_File extends Red_FileIO {
const CSV_SOURCE = 0;
const CSV_TARGET = 1;
const CSV_REGEX = 2;
const CSV_CODE = 3;
public function force_download() {
parent::force_download();
header( 'Content-Type: text/csv' );
header( 'Content-Disposition: attachment; filename="' . $this->export_filename( 'csv' ) . '"' );
}
public function get_data( array $items, array $groups ) {
$lines = [ implode( ',', array( 'source', 'target', 'regex', 'code', 'type', 'hits', 'title', 'status' ) ) ];
foreach ( $items as $line ) {
$lines[] = $this->item_as_csv( $line );
}
return implode( PHP_EOL, $lines ) . PHP_EOL;
}
public function item_as_csv( $item ) {
$data = $item->match->get_data();
if ( isset( $data['url'] ) ) {
$data = $data['url'];
} else {
$data = '/unknown';
}
if ( $item->get_action_code() > 400 && $item->get_action_code() < 500 ) {
$data = '';
}
$csv = array(
$item->get_url(),
$data,
$item->is_regex() ? 1 : 0,
$item->get_action_code(),
$item->get_action_type(),
$item->get_hits(),
$item->get_title(),
$item->is_enabled() ? 'active' : 'disabled',
);
$csv = array_map( array( $this, 'escape_csv' ), $csv );
return implode( ',', $csv );
}
public function escape_csv( $item ) {
if ( is_numeric( $item ) ) {
return $item;
}
return '"' . str_replace( '"', '""', $item ) . '"';
}
public function load( $group, $filename, $data ) {
$file = fopen( $filename, 'r' );
if ( $file ) {
$separators = [
',',
';',
'|',
];
foreach ( $separators as $separator ) {
fseek( $file, 0 );
$count = $this->load_from_file( $group, $file, $separator );
if ( $count > 0 ) {
return $count;
}
}
}
return 0;
}
public function load_from_file( $group_id, $file, $separator ) {
global $wpdb;
$count = 0;
$group = Red_Group::get( $group_id );
if ( ! $group ) {
return 0;
}
while ( ( $csv = fgetcsv( $file, 5000, $separator ) ) ) {
$item = $this->csv_as_item( $csv, $group );
if ( $item && $this->item_is_valid( $item ) ) {
$created = Red_Item::create( $item );
// The query log can use up all the memory
$wpdb->queries = [];
if ( ! is_wp_error( $created ) ) {
$count++;
}
}
}
return $count;
}
private function item_is_valid( array $csv ) {
if ( strlen( $csv['url'] ) === 0 ) {
return false;
}
if ( $csv['action_data']['url'] === $csv['url'] ) {
return false;
}
return true;
}
private function get_valid_code( $code ) {
if ( get_status_header_desc( $code ) !== '' ) {
return intval( $code, 10 );
}
return 301;
}
private function get_action_type( $code ) {
if ( $code > 400 && $code < 500 ) {
return 'error';
}
return 'url';
}
public function csv_as_item( $csv, Red_Group $group ) {
if ( count( $csv ) > 1 && $csv[ self::CSV_SOURCE ] !== 'source' && $csv[ self::CSV_TARGET ] !== 'target' ) {
$code = isset( $csv[ self::CSV_CODE ] ) ? $this->get_valid_code( $csv[ self::CSV_CODE ] ) : 301;
return array(
'url' => trim( $csv[ self::CSV_SOURCE ] ),
'action_data' => array( 'url' => trim( $csv[ self::CSV_TARGET ] ) ),
'regex' => isset( $csv[ self::CSV_REGEX ] ) ? $this->parse_regex( $csv[ self::CSV_REGEX ] ) : $this->is_regex( $csv[ self::CSV_SOURCE ] ),
'group_id' => $group->get_id(),
'match_type' => 'url',
'action_type' => $this->get_action_type( $code ),
'action_code' => $code,
'status' => $group->is_enabled() ? 'enabled' : 'disabled',
);
}
return false;
}
private function parse_regex( $value ) {
return intval( $value, 10 ) === 1 ? true : false;
}
private function is_regex( $url ) {
$regex = '()[]$^*';
if ( strpbrk( $url, $regex ) === false ) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,84 @@
<?php
class Red_Json_File extends Red_FileIO {
public function force_download() {
parent::force_download();
header( 'Content-Type: application/json' );
header( 'Content-Disposition: attachment; filename="' . $this->export_filename( 'json' ) . '"' );
}
public function get_data( array $items, array $groups ) {
$version = red_get_plugin_data( dirname( dirname( __FILE__ ) ) . '/redirection.php' );
$items = array(
'plugin' => array(
'version' => trim( $version['Version'] ),
'date' => date( 'r' ),
),
'groups' => $groups,
'redirects' => array_map( function( $item ) {
return $item->to_json();
}, $items ),
);
return wp_json_encode( $items, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) . PHP_EOL;
}
public function load( $group, $filename, $data ) {
global $wpdb;
$count = 0;
$json = @json_decode( $data, true );
if ( $json === false ) {
return 0;
}
// Import groups
$group_map = array();
if ( isset( $json['groups'] ) ) {
foreach ( $json['groups'] as $group ) {
$old_group_id = $group['id'];
unset( $group['id'] );
$group = Red_Group::create( $group['name'], $group['module_id'], $group['enabled'] ? true : false );
if ( $group ) {
$group_map[ $old_group_id ] = $group->get_id();
}
}
}
unset( $json['groups'] );
// Import redirects
if ( isset( $json['redirects'] ) ) {
foreach ( $json['redirects'] as $pos => $redirect ) {
unset( $redirect['id'] );
if ( ! isset( $group_map[ $redirect['group_id'] ] ) ) {
$new_group = Red_Group::create( 'Group', 1 );
$group_map[ $redirect['group_id'] ] = $new_group->get_id();
}
if ( $redirect['match_type'] === 'url' && isset( $redirect['action_data'] ) && ! is_array( $redirect['action_data'] ) ) {
$redirect['action_data'] = array( 'url' => $redirect['action_data'] );
}
$redirect['group_id'] = $group_map[ $redirect['group_id'] ];
$created = Red_Item::create( $redirect );
if ( $created instanceof Red_Item ) {
$count++;
}
// Helps reduce memory usage
unset( $json['redirects'][ $pos ] );
$wpdb->queries = array();
$wpdb->num_queries = 0;
}
}
return $count;
}
}

View File

@@ -0,0 +1,114 @@
<?php
class Red_Nginx_File extends Red_FileIO {
public function force_download() {
parent::force_download();
header( 'Content-Type: application/octet-stream' );
header( 'Content-Disposition: attachment; filename="' . $this->export_filename( 'nginx' ) . '"' );
}
public function get_data( array $items, array $groups ) {
$lines = array();
$version = red_get_plugin_data( dirname( dirname( __FILE__ ) ) . '/redirection.php' );
$lines[] = '# Created by Redirection';
$lines[] = '# ' . date( 'r' );
$lines[] = '# Redirection ' . trim( $version['Version'] ) . ' - https://redirection.me';
$lines[] = '';
$lines[] = 'server {';
$parts = array();
foreach ( $items as $item ) {
if ( $item->is_enabled() ) {
$parts[] = $this->get_nginx_item( $item );
}
}
$lines = array_merge( $lines, array_filter( $parts ) );
$lines[] = '}';
$lines[] = '';
$lines[] = '# End of Redirection';
return implode( PHP_EOL, $lines ) . PHP_EOL;
}
private function get_redirect_code( Red_Item $item ) {
if ( $item->get_action_code() === 301 ) {
return 'permanent';
}
return 'redirect';
}
public function load( $group, $data, $filename = '' ) {
return 0;
}
private function get_nginx_item( Red_Item $item ) {
$target = 'add_' . $item->get_match_type();
if ( method_exists( $this, $target ) ) {
return ' ' . $this->$target( $item, $item->get_match_data() );
}
return false;
}
private function add_url( Red_Item $item, array $match_data ) {
return $this->get_redirect( $item->get_url(), $item->get_action_data(), $this->get_redirect_code( $item ), $match_data['source'], $item->source_flags->is_regex() );
}
private function add_agent( Red_Item $item, array $match_data ) {
if ( $item->match->url_from ) {
$lines[] = 'if ( $http_user_agent ~* ^' . $item->match->user_agent . '$ ) {';
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $item->match->url_from, $this->get_redirect_code( $item ), $match_data['source'] );
$lines[] = ' }';
}
if ( $item->match->url_notfrom ) {
$lines[] = 'if ( $http_user_agent !~* ^' . $item->match->user_agent . '$ ) {';
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $item->match->url_notfrom, $this->get_redirect_code( $item ), $match_data['source'] );
$lines[] = ' }';
}
return implode( "\n", $lines );
}
private function add_referrer( Red_Item $item, array $match_data ) {
if ( $item->match->url_from ) {
$lines[] = 'if ( $http_referer ~* ^' . $item->match->referrer . '$ ) {';
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $item->match->url_from, $this->get_redirect_code( $item ), $match_data['source'] );
$lines[] = ' }';
}
if ( $item->match->url_notfrom ) {
$lines[] = 'if ( $http_referer !~* ^' . $item->match->referrer . '$ ) {';
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $item->match->url_notfrom, $this->get_redirect_code( $item ), $match_data['source'] );
$lines[] = ' }';
}
return implode( "\n", $lines );
}
private function get_redirect( $line, $target, $code, $source, $regex = false ) {
$line = ltrim( $line, '^' );
$line = rtrim( $line, '$' );
$source_url = new Red_Url_Encode( $line, $regex );
$target_url = new Red_Url_Encode( $target );
// Remove any existing start/end from a regex
$from = $source_url->get_as_source();
$from = ltrim( $from, '^' );
$from = rtrim( $from, '$' );
if ( isset( $source['flag_case'] ) && $source['flag_case'] ) {
$from = '(?i)^' . $from;
} else {
$from = '^' . $from;
}
return 'rewrite ' . $from . '$ ' . $target_url->get_as_target() . ' ' . $code . ';';
}
}

View File

@@ -0,0 +1,47 @@
<?php
class Red_Rss_File extends Red_FileIO {
public function force_download() {
header( 'Content-type: text/xml; charset=' . get_option( 'blog_charset' ), true );
}
public function get_data( array $items, array $groups ) {
$xml = '<?xml version="1.0" encoding="' . get_option( 'blog_charset' ) . '"?' . ">\r\n";
ob_start();
?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Redirection - <?php bloginfo_rss( 'name' ); ?></title>
<link><?php esc_url( bloginfo_rss( 'url' ) ); ?></link>
<description><?php esc_html( bloginfo_rss( 'description' ) ); ?></description>
<pubDate><?php echo esc_html( mysql2date( 'D, d M Y H:i:s +0000', get_lastpostmodified( 'GMT' ), false ) ); ?></pubDate>
<generator>
<?php echo esc_html( 'http://wordpress.org/?v=' ); ?>
<?php bloginfo_rss( 'version' ); ?>
</generator>
<language><?php echo esc_html( get_option( 'rss_language' ) ); ?></language>
<?php foreach ( $items as $log ) : ?>
<item>
<title><?php echo esc_html( $log->get_url() ); ?></title>
<link><![CDATA[<?php echo esc_url( home_url() ) . esc_url( $log->get_url() ); ?>]]></link>
<pubDate><?php echo esc_html( date( 'D, d M Y H:i:s +0000', intval( $log->get_last_hit(), 10 ) ) ); ?></pubDate>
<guid isPermaLink="false"><?php echo esc_html( $log->get_id() ); ?></guid>
<description><?php echo esc_html( $log->get_url() ); ?></description>
</item>
<?php endforeach; ?>
</channel>
</rss>
<?php
$xml .= ob_get_contents();
ob_end_clean();
return $xml;
}
function load( $group, $data, $filename = '' ) {
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -0,0 +1,21 @@
<?php
require_once dirname( __FILE__ ) . '/http-header.php';
/**
* Check that a cookie value exists
*/
class Cookie_Match extends Header_Match {
public function name() {
return __( 'URL and cookie', 'redirection' );
}
public function is_match( $url ) {
if ( $this->regex ) {
$regex = new Red_Regex( $this->value, true );
return $regex->is_match( Redirection_Request::get_cookie( $this->name ) );
}
return Redirection_Request::get_cookie( $this->name ) === $this->value;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Perform a check against the results of a custom filter
*/
class Custom_Match extends Red_Match {
use FromNotFrom_Match;
/**
* Filter name
*
* @var string
*/
public $filter = '';
public function name() {
return __( 'URL and custom filter', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = [
'filter' => isset( $details['filter'] ) ? $this->sanitize_filter( $details['filter'] ) : '',
];
return $this->save_data( $details, $no_target_url, $data );
}
public function sanitize_filter( $name ) {
$name = preg_replace( '/[^A-Za-z0-9\-_]/', '', sanitize_text_field( $name ) );
return trim( $name );
}
public function is_match( $url ) {
return apply_filters( $this->filter, false, $url );
}
public function get_data() {
return array_merge( [
'filter' => $this->filter,
], $this->get_from_data() );
}
public function load( $values ) {
$values = $this->load_data( $values );
$this->filter = isset( $values['filter'] ) ? $values['filter'] : '';
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* Trait to add redirect matching that adds a matched target
*/
trait FromNotFrom_Match {
/**
* Target URL if matched
*
* @var string
*/
public $url_from = '';
/**
* Target URL if not matched
*
* @var string
*/
public $url_notfrom = '';
/**
* Save data to an array, ready for serializing.
*
* @param array $details New match data.
* @param boolean $no_target_url Does the action have a target URL.
* @param array $data Existing match data.
* @return array
*/
private function save_data( array $details, $no_target_url, array $data ) {
if ( $no_target_url === false ) {
return array_merge( array(
'url_from' => isset( $details['url_from'] ) ? $this->sanitize_url( $details['url_from'] ) : '',
'url_notfrom' => isset( $details['url_notfrom'] ) ? $this->sanitize_url( $details['url_notfrom'] ) : '',
), $data );
}
return $data;
}
/**
* Get target URL for this match, depending on whether we match or not
*
* @param string $requested_url Request URL.
* @param string $source_url Redirect source URL.
* @param Red_Source_Flags $flags Redirect flags.
* @param boolean $matched Has the source been matched.
* @return string|false
*/
public function get_target_url( $requested_url, $source_url, Red_Source_Flags $flags, $matched ) {
// Action needs a target URL based on whether we matched or not
$target = $this->get_matched_target( $matched );
if ( $flags->is_regex() && $target ) {
return $this->get_target_regex_url( $source_url, $target, $requested_url, $flags );
}
return $target;
}
/**
* Return the matched target if we have matched and one exists, or return the unmatched target if not matched.
*
* @param boolean $matched Is it matched.
* @return false|string
*/
private function get_matched_target( $matched ) {
if ( $this->url_from !== '' && $matched ) {
return $this->url_from;
}
if ( $this->url_notfrom !== '' && ! $matched ) {
return $this->url_notfrom;
}
return false;
}
/**
* Load the data into the instance.
*
* @param string $values Serialized PHP data.
* @return array
*/
private function load_data( $values ) {
$values = @unserialize( $values );
if ( isset( $values['url_from'] ) ) {
$this->url_from = $values['url_from'];
}
if ( isset( $values['url_notfrom'] ) ) {
$this->url_notfrom = $values['url_notfrom'];
}
return $values;
}
/**
* Get the match data
*
* @return array<url_from: string, url_notfrom: string>
*/
private function get_from_data() {
return [
'url_from' => $this->url_from,
'url_notfrom' => $this->url_notfrom,
];
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Trait to add redirect matching that adds a matched target
*/
trait FromUrl_Match {
/**
* URL to match against
*
* @var string
*/
public $url = '';
/**
* Save data to an array, ready for serializing.
*
* @param array $details New match data.
* @param boolean $no_target_url Does the action have a target URL.
* @param array $data Existing match data.
* @return array
*/
private function save_data( array $details, $no_target_url, array $data ) {
if ( $no_target_url === false ) {
return array_merge( [
'url' => isset( $details['url'] ) ? $this->sanitize_url( $details['url'] ) : '',
], $data );
}
return $data;
}
/**
* Get target URL for this match, depending on whether we match or not
*
* @param string $requested_url Request URL.
* @param string $source_url Redirect source URL.
* @param Red_Source_Flags $flags Redirect flags.
* @param boolean $matched Is the URL matched.
* @return string|false
*/
public function get_target_url( $requested_url, $source_url, Red_Source_Flags $flags, $matched ) {
$target = $this->get_matched_target( $matched );
if ( $flags->is_regex() && $target ) {
return $this->get_target_regex_url( $source_url, $target, $requested_url, $flags );
}
return $target;
}
/**
* Return the matched target, if one exists.
*
* @param boolean $matched Is it matched.
* @return false|string
*/
private function get_matched_target( $matched ) {
if ( $matched ) {
return $this->url;
}
return false;
}
/**
* Load the data into the instance.
*
* @param string $values Serialized PHP data.
* @return array
*/
private function load_data( $values ) {
$values = unserialize( $values );
if ( isset( $values['url'] ) ) {
$this->url = $values['url'];
}
return $values;
}
/**
* Get the loaded data as an array.
*
* @return array<url: string>
*/
private function get_from_data() {
return [
'url' => $this->url,
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Check a HTTP request header
*/
class Header_Match extends Red_Match {
use FromNotFrom_Match;
/**
* HTTP header name
*
* @var String
*/
public $name = '';
/**
* HTTP header value
*
* @var String
*/
public $value = '';
/**
* Is this a regex?
*
* @var boolean
*/
public $regex = false;
public function name() {
return __( 'URL and HTTP header', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array(
'regex' => isset( $details['regex'] ) && $details['regex'] ? true : false,
'name' => isset( $details['name'] ) ? $this->sanitize_name( $details['name'] ) : '',
'value' => isset( $details['value'] ) ? $this->sanitize_value( $details['value'] ) : '',
);
return $this->save_data( $details, $no_target_url, $data );
}
public function sanitize_name( $name ) {
$name = $this->sanitize_url( sanitize_text_field( $name ) );
$name = str_replace( ' ', '', $name );
$name = preg_replace( '/[^A-Za-z0-9\-_]/', '', $name );
return trim( trim( $name, ':' ) );
}
public function sanitize_value( $value ) {
return $this->sanitize_url( sanitize_text_field( $value ) );
}
public function is_match( $url ) {
if ( $this->regex ) {
$regex = new Red_Regex( $this->value, true );
return $regex->is_match( Redirection_Request::get_header( $this->name ) );
}
return Redirection_Request::get_header( $this->name ) === $this->value;
}
public function get_data() {
return array_merge( array(
'regex' => $this->regex,
'name' => $this->name,
'value' => $this->value,
), $this->get_from_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
*/
public function load( $values ) {
$values = $this->load_data( $values );
$this->regex = isset( $values['regex'] ) ? $values['regex'] : false;
$this->name = isset( $values['name'] ) ? $values['name'] : '';
$this->value = isset( $values['value'] ) ? $values['value'] : '';
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* Check the request IP
*/
class IP_Match extends Red_Match {
use FromNotFrom_Match;
/**
* Array of IP addresses
*
* @var string[]
*/
public $ip = [];
public function name() {
return __( 'URL and IP', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array( 'ip' => isset( $details['ip'] ) && is_array( $details['ip'] ) ? $this->sanitize_ips( $details['ip'] ) : [] );
return $this->save_data( $details, $no_target_url, $data );
}
/**
* Sanitize a single IP
*
* @param string $ip IP.
* @return string|false
*/
private function sanitize_single_ip( $ip ) {
$ip = @inet_pton( trim( sanitize_text_field( $ip ) ) );
if ( $ip !== false ) {
return @inet_ntop( $ip ); // Convert back to string
}
return false;
}
/**
* Sanitize a list of IPs
*
* @param string[] $ips List of IPs.
* @return string[]
*/
private function sanitize_ips( array $ips ) {
$ips = array_map( array( $this, 'sanitize_single_ip' ), $ips );
return array_values( array_filter( array_unique( $ips ) ) );
}
/**
* Get a list of IPs that match.
*
* @param string $match_ip IP to match.
* @return string[]
*/
private function get_matching_ips( $match_ip ) {
$current_ip = @inet_pton( $match_ip );
return array_filter( $this->ip, function( $ip ) use ( $current_ip ) {
return @inet_pton( $ip ) === $current_ip;
} );
}
public function is_match( $url ) {
$matched = $this->get_matching_ips( Redirection_Request::get_ip() );
return count( $matched ) > 0;
}
public function get_data() {
return array_merge( array(
'ip' => $this->ip,
), $this->get_from_data() );
}
public function load( $values ) {
$values = $this->load_data( $values );
$this->ip = isset( $values['ip'] ) ? $values['ip'] : [];
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Check the client language
*/
class Language_Match extends Red_Match {
use FromNotFrom_Match;
/**
* Language to check.
*
* @var String
*/
public $language = '';
public function name() {
return __( 'URL and language', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array( 'language' => isset( $details['language'] ) ? $this->sanitize_language( $details['language'] ) : '' );
return $this->save_data( $details, $no_target_url, $data );
}
/**
* Sanitize the language value to a CSV string
*
* @param string $language User supplied language strings.
* @return string
*/
private function sanitize_language( $language ) {
$parts = explode( ',', str_replace( ' ', '', sanitize_text_field( $language ) ) );
return implode( ',', $parts );
}
public function is_match( $url ) {
$matches = explode( ',', $this->language );
$requested = Redirection_Request::get_accept_language();
foreach ( $matches as $match ) {
if ( in_array( $match, $requested, true ) ) {
return true;
}
}
return false;
}
public function get_data() {
return array_merge( array(
'language' => $this->language,
), $this->get_from_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
*/
public function load( $values ) {
$values = $this->load_data( $values );
$this->language = isset( $values['language'] ) ? $values['language'] : '';
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* Check whether the user is logged in or out
*/
class Login_Match extends Red_Match {
/**
* Target URL when logged in.
*
* @var String
*/
public $logged_in = '';
/**
* Target URL when logged out.
*
* @var String
*/
public $logged_out = '';
public function name() {
return __( 'URL and login status', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
if ( $no_target_url ) {
return null;
}
return [
'logged_in' => isset( $details['logged_in'] ) ? $this->sanitize_url( $details['logged_in'] ) : '',
'logged_out' => isset( $details['logged_out'] ) ? $this->sanitize_url( $details['logged_out'] ) : '',
];
}
public function is_match( $url ) {
return is_user_logged_in();
}
public function get_target_url( $requested_url, $source_url, Red_Source_Flags $flags, $match ) {
$target = false;
if ( $match && $this->logged_in !== '' ) {
$target = $this->logged_in;
} elseif ( ! $match && $this->logged_out !== '' ) {
$target = $this->logged_out;
}
if ( $flags->is_regex() && $target ) {
$target = $this->get_target_regex_url( $source_url, $target, $requested_url, $flags );
}
return $target;
}
public function get_data() {
return [
'logged_in' => $this->logged_in,
'logged_out' => $this->logged_out,
];
}
/**
* Load the match data into this instance.
*
* @param string $values Match values, as read from the database (plain text or serialized PHP).
* @return void
*/
public function load( $values ) {
$values = unserialize( $values );
$this->logged_in = isset( $values['logged_in'] ) ? $values['logged_in'] : '';
$this->logged_out = isset( $values['logged_out'] ) ? $values['logged_out'] : '';
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* Match the WordPress page type
*/
class Page_Match extends Red_Match {
use FromUrl_Match;
/**
* Page type
*
* @var String
*/
public $page = '404';
public function name() {
return __( 'URL and WordPress page type', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array( 'page' => isset( $details['page'] ) ? $this->sanitize_page( $details['page'] ) : '404' );
return $this->save_data( $details, $no_target_url, $data );
}
private function sanitize_page( $page ) {
return '404';
}
public function is_match( $url ) {
return is_404();
}
public function get_data() {
return array_merge( array(
'page' => $this->page,
), $this->get_from_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
*/
public function load( $values ) {
$values = $this->load_data( $values );
$this->page = isset( $values['page'] ) ? $values['page'] : '404';
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Match the referrer
*/
class Referrer_Match extends Red_Match {
use FromNotFrom_Match;
/**
* Referrer
*
* @var String
*/
public $referrer = '';
/**
* Regex match?
*
* @var boolean
*/
public $regex = false;
public function name() {
return __( 'URL and referrer', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array(
'regex' => isset( $details['regex'] ) && $details['regex'] ? true : false,
'referrer' => isset( $details['referrer'] ) ? $this->sanitize_referrer( $details['referrer'] ) : '',
);
return $this->save_data( $details, $no_target_url, $data );
}
public function sanitize_referrer( $agent ) {
return $this->sanitize_url( $agent );
}
public function is_match( $url ) {
if ( $this->regex ) {
$regex = new Red_Regex( $this->referrer, true );
return $regex->is_match( Redirection_Request::get_referrer() );
}
return Redirection_Request::get_referrer() === $this->referrer;
}
public function get_data() {
return array_merge( array(
'regex' => $this->regex,
'referrer' => $this->referrer,
), $this->get_from_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
*/
public function load( $values ) {
$values = $this->load_data( $values );
$this->regex = isset( $values['regex'] ) ? $values['regex'] : false;
$this->referrer = isset( $values['referrer'] ) ? $values['referrer'] : '';
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Match the server URL. Used to match requests for another domain.
*/
class Server_Match extends Red_Match {
use FromNotFrom_Match;
/**
* Server URL.
*
* @var String
*/
public $server = '';
public function name() {
return __( 'URL and server', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array( 'server' => isset( $details['server'] ) ? $this->sanitize_server( $details['server'] ) : '' );
return $this->save_data( $details, $no_target_url, $data );
}
private function sanitize_server( $server ) {
if ( strpos( $server, 'http' ) === false ) {
$server = ( is_ssl() ? 'https://' : 'http://' ) . $server;
}
$parts = wp_parse_url( $server );
if ( isset( $parts['host'] ) ) {
return $parts['scheme'] . '://' . $parts['host'];
}
return '';
}
public function is_match( $url ) {
$server = wp_parse_url( $this->server, PHP_URL_HOST );
return $server === Redirection_Request::get_server_name();
}
public function get_data() {
return array_merge( array(
'server' => $this->server,
), $this->get_from_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
*/
public function load( $values ) {
$values = $this->load_data( $values );
$this->server = isset( $values['server'] ) ? $values['server'] : '';
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Match the URL only.
*/
class URL_Match extends Red_Match {
/**
* URL
*
* @var String
*/
public $url = '';
public function name() {
return __( 'URL only', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = isset( $details['url'] ) ? $details['url'] : '';
if ( strlen( $data ) === 0 ) {
$data = '/';
}
if ( $no_target_url ) {
return null;
}
return $this->sanitize_url( $data );
}
public function is_match( $url ) {
return true;
}
public function get_target_url( $original_url, $matched_url, Red_Source_Flags $flag, $is_matched ) {
$target = $this->url;
if ( $flag->is_regex() ) {
$target = $this->get_target_regex_url( $matched_url, $target, $original_url, $flag );
}
return $target;
}
public function get_data() {
if ( $this->url ) {
return [
'url' => $this->url,
];
}
return null;
}
public function load( $values ) {
$this->url = $values;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Match the user agent
*/
class Agent_Match extends Red_Match {
use FromNotFrom_Match;
/**
* User agent.
*
* @var String
*/
public $agent = '';
/**
* Is this a regex match?
*
* @var boolean
*/
public $regex = false;
public function name() {
return __( 'URL and user agent', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array(
'regex' => isset( $details['regex'] ) && $details['regex'] ? true : false,
'agent' => isset( $details['agent'] ) ? $this->sanitize_agent( $details['agent'] ) : '',
);
return $this->save_data( $details, $no_target_url, $data );
}
private function sanitize_agent( $agent ) {
return $this->sanitize_url( $agent );
}
public function is_match( $url ) {
if ( $this->regex ) {
$regex = new Red_Regex( $this->agent, true );
return $regex->is_match( Redirection_Request::get_user_agent() );
}
return $this->agent === Redirection_Request::get_user_agent();
}
public function get_data() {
return array_merge( array(
'regex' => $this->regex,
'agent' => $this->agent,
), $this->get_from_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
*/
public function load( $values ) {
$values = $this->load_data( $values );
$this->regex = isset( $values['regex'] ) ? $values['regex'] : false;
$this->agent = isset( $values['agent'] ) ? $values['agent'] : '';
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Match a particular role or capability
*/
class Role_Match extends Red_Match {
use FromNotFrom_Match;
/**
* WordPress role or capability
*
* @var String
*/
public $role = '';
public function name() {
return __( 'URL and role/capability', 'redirection' );
}
public function save( array $details, $no_target_url = false ) {
$data = array( 'role' => isset( $details['role'] ) ? $details['role'] : '' );
return $this->save_data( $details, $no_target_url, $data );
}
public function is_match( $url ) {
return current_user_can( $this->role );
}
public function get_data() {
return array_merge( array(
'role' => $this->role,
), $this->get_from_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
*/
public function load( $values ) {
$values = $this->load_data( $values );
$this->role = isset( $values['role'] ) ? $values['role'] : '';
}
}

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 );
}
}

View File

@@ -0,0 +1,95 @@
<?php
class Apache_Module extends Red_Module {
const MODULE_ID = 2;
private $location = '';
public function get_id() {
return self::MODULE_ID;
}
public function get_name() {
return 'Apache';
}
public function get_location() {
return $this->location;
}
protected function load( $data ) {
$mine = array( 'location' );
foreach ( $mine as $key ) {
if ( isset( $data[ $key ] ) ) {
$this->$key = $data[ $key ];
}
}
}
protected function flush_module() {
include_once dirname( dirname( __FILE__ ) ) . '/models/htaccess.php';
if ( empty( $this->location ) ) {
return false;
}
$items = Red_Item::get_all_for_module( $this->get_id() );
// Produce the .htaccess file
$htaccess = new Red_Htaccess();
if ( is_array( $items ) && count( $items ) > 0 ) {
foreach ( $items as $item ) {
if ( $item->is_enabled() ) {
$htaccess->add( $item );
}
}
}
return $htaccess->save( $this->location );
}
public function can_save( $location ) {
$location = $this->sanitize_location( $location );
if ( @fopen( $location, 'a' ) === false ) {
$error = error_get_last();
return new WP_Error( 'redirect', isset( $error['message'] ) ? $error['message'] : 'Unknown error' );
}
return true;
}
private function sanitize_location( $location ) {
$location = str_replace( '.htaccess', '', $location );
$location = rtrim( $location, '/' ) . '/.htaccess';
return rtrim( dirname( $location ), '/' ) . '/.htaccess';
}
public function update( array $data ) {
include_once dirname( dirname( __FILE__ ) ) . '/models/htaccess.php';
$new_location = isset( $data['location'] ) ? $data['location'] : '';
if ( strlen( $new_location ) > 0 ) {
$new_location = $this->sanitize_location( trim( $data['location'] ) );
}
$save = [
'location' => $new_location,
];
if ( ! empty( $this->location ) && $save['location'] !== $this->location && $save['location'] !== '' ) {
// Location has moved. Remove from old location
$htaccess = new Red_Htaccess();
$htaccess->save( $this->location, '' );
}
$this->load( $save );
if ( $save['location'] !== '' && $this->flush_module() === false ) {
$save['location'] = '';
}
return $save;
}
}

View File

@@ -0,0 +1,32 @@
<?php
class Nginx_Module extends Red_Module {
const MODULE_ID = 3;
private $location = '';
public function get_id() {
return self::MODULE_ID;
}
public function get_name() {
return 'Nginx';
}
protected function load( $data ) {
$mine = array( 'location' );
foreach ( $mine as $key ) {
if ( isset( $data[ $key ] ) ) {
$this->$key = $data[ $key ];
}
}
}
protected function flush_module() {
}
public function update( array $data ) {
return false;
}
}

View File

@@ -0,0 +1,566 @@
<?php
/**
* WordPress redirect module.
*
* Provides PHP controlled redirects and monitoring and is the core of the front-end redirection.
*/
class WordPress_Module extends Red_Module {
/**
* @var integer
*/
const MODULE_ID = 1;
/**
* Can we log?
*
* @var boolean
*/
private $can_log = true;
/**
* The target redirect URL
*
* @var string|false
*/
private $redirect_url = false;
/**
* The target redirect code
*
* @var integer
*/
private $redirect_code = 0;
/**
* Copy of redirects that match the requested URL
*
* @var Red_Item[]
*/
private $redirects = [];
/**
* Matched redirect
*
* @var Red_Item|false
*/
private $matched = false;
/**
* Return the module ID
*
* @return integer
*/
public function get_id() {
return self::MODULE_ID;
}
/**
* Return the module name
*
* @return string
*/
public function get_name() {
return 'WordPress';
}
/**
* Start the module. Hooks any filters and actions
*
* @return void
*/
public function start() {
// Only run redirect rules if we're not disabled
if ( ! red_is_disabled() ) {
// Canonical site settings - https, www, relocate, and aliases
add_action( 'init', [ $this, 'canonical_domain' ] );
// The main redirect loop
add_action( 'init', [ $this, 'init' ] );
// Send site HTTP headers as well as 410 error codes
add_action( 'send_headers', [ $this, 'send_headers' ] );
// Redirect HTTP headers and server-specific overrides
add_filter( 'wp_redirect', [ $this, 'wp_redirect' ], 1, 2 );
// Allow permalinks to be redirected
add_filter( 'pre_handle_404', [ $this, 'pre_handle_404' ], 10, 2 );
// Cache support
add_action( 'redirection_matched', [ $this, 'cache_redirects' ], 10, 3 );
add_action( 'redirection_last', [ $this, 'cache_unmatched_redirects' ], 10, 3 );
}
// Setup the various filters and actions that allow Redirection to happen
add_action( 'redirection_visit', [ $this, 'redirection_visit' ], 10, 3 );
add_action( 'redirection_do_nothing', [ $this, 'redirection_do_nothing' ] );
// Prevent WordPress overriding a canonical redirect
add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ], 10, 2 );
// Log 404s and perform 'URL and WordPress page type' redirects
add_action( 'template_redirect', [ $this, 'template_redirect' ] );
// Back-compat for < database 4.2
add_filter( 'redirection_404_data', [ $this, 'log_back_compat' ] );
add_filter( 'redirection_log_data', [ $this, 'log_back_compat' ] );
// Record the redirect agent
add_filter( 'x_redirect_by', [ $this, 'record_redirect_by' ], 90 );
}
/**
* Called after no redirect is matched. This allows us to cache a negative result/
*
* @param string $url URL.
* @param WordPress_Module $wp This.
* @param array $redirects Array of redirects.
* @return void
*/
public function cache_unmatched_redirects( $url, $wp, $redirects ) {
if ( $this->matched ) {
return;
}
$this->cache_redirects( $url, $this->matched, $redirects );
}
/**
* Called when a redirect is matched. This allows us to cache a positive result.
*
* @param string $url URL.
* @param Red_Item|false $matched_redirect Matched redirect.
* @param array $redirects Array of redirects.
* @return void
*/
public function cache_redirects( $url, $matched_redirect, $redirects ) {
$cache = Redirect_Cache::init();
$cache->set( $url, $matched_redirect, $redirects );
}
/**
* If we have a 404 then check for any permalink migrations
*
* @param boolean $result Return result.
* @param WP_Query $query WP_Query object.
* @return boolean
*/
public function pre_handle_404( $result, WP_Query $query ) {
$options = red_get_options();
if ( count( $options['permalinks'] ) > 0 ) {
include_once dirname( dirname( __FILE__ ) ) . '/models/permalinks.php';
$permalinks = new Red_Permalinks( $options['permalinks'] );
$permalinks->migrate( $query );
}
return $result;
}
/**
* Back-compatability for Redirection databases older than 4.2. Prevents errors from storing data that has no DB column
*
* @param array $insert Data to log.
* @return array
*/
public function log_back_compat( $insert ) {
// Remove columns not supported in older versions
$status = new Red_Database_Status();
if ( ! $status->does_support( '4.2' ) ) {
foreach ( [ 'request_data', 'request_method', 'http_code', 'domain', 'redirect_by' ] as $ignore ) {
unset( $insert[ $ignore ] );
}
}
return $insert;
}
/**
* This ensures that a matched URL is not overriddden by WordPress, if the URL happens to be a WordPress URL of some kind
* For example: /?author=1 will be redirected to /author/name unless this returns false
*
* @param string $redirect_url The redirected URL.
* @param string $requested_url The requested URL.
* @return string|false
*/
public function redirect_canonical( $redirect_url, $requested_url ) {
if ( $this->matched ) {
return false;
}
return $redirect_url;
}
/**
* WordPress 'template_redirect' hook. Used to check for 404s
*
* @return void
*/
public function template_redirect() {
if ( ! is_404() || $this->matched ) {
return;
}
$this->is_url_and_page_type();
$options = red_get_options();
if ( isset( $options['expire_404'] ) && $options['expire_404'] >= 0 && $this->can_log() ) {
$details = [
'agent' => Redirection_Request::get_user_agent(),
'referrer' => Redirection_Request::get_referrer(),
'request_method' => Redirection_Request::get_request_method(),
'http_code' => 404,
];
if ( $options['log_header'] ) {
$details['request_data'] = [
'headers' => Redirection_Request::get_request_headers(),
];
}
Red_404_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
}
}
/**
* Return `true` if any of the matched redirects is a 'url and page type', `false` otherwise
*
* @return boolean
*/
private function is_url_and_page_type() {
$page_types = array_values(
array_filter(
$this->redirects,
function ( Red_Item $redirect ) {
return $redirect->match && $redirect->match->get_type() === 'page';
}
)
);
if ( count( $page_types ) > 0 ) {
$request = new Red_Url_Request( Redirection_Request::get_request_url() );
foreach ( $page_types as $page_type ) {
$action = $page_type->get_match( $request->get_decoded_url(), $request->get_original_url() );
if ( $action ) {
$action->run();
return true;
}
}
return true;
}
return false;
}
/**
* Called by a 'do nothing' action. Return true to stop further processing of the 'do nothing'
*
* @return boolean
*/
public function redirection_do_nothing() {
$this->can_log = false;
return true;
}
/**
* Action fired when a redirect is performed, and used to log the data
*
* @param Red_Item $redirect The redirect.
* @param string $url The source URL.
* @param string $target The target URL.
* @return void
*/
public function redirection_visit( $redirect, $url, $target ) {
$redirect->visit( $url, $target );
}
/**
* Get canonical target
*
* @return string|false
*/
public function get_canonical_target() {
$options = red_get_options();
$canonical = new Redirection_Canonical( $options['https'], $options['preferred_domain'], $options['aliases'], get_home_url() );
// Relocate domain?
if ( $options['relocate'] ) {
return $canonical->relocate_request( $options['relocate'], Redirection_Request::get_server_name(), Redirection_Request::get_request_url() );
}
// Force HTTPS or www
return $canonical->get_redirect( Redirection_Request::get_request_server_name(), Redirection_Request::get_request_url() );
}
/**
* Checks for canonical domain requests
*
* @return void
*/
public function canonical_domain() {
$target = $this->get_canonical_target();
if ( $target ) {
// phpcs:ignore
wp_redirect( $target, 301, 'redirection' );
die();
}
}
/**
* Redirection 'main loop'. Checks the currently requested URL against the database and perform a redirect, if necessary.
*
* @return void
*/
public function init() {
if ( $this->matched ) {
return;
}
$request = new Red_Url_Request( Redirection_Request::get_request_url() );
// Make sure we don't try and redirect something essential
if ( $request->is_valid() && ! $request->is_protected_url() ) {
do_action( 'redirection_first', $request->get_decoded_url(), $this );
// Get all redirects that match the URL
$redirects = Red_Item::get_for_url( $request->get_decoded_url() );
// Redirects will be ordered by position. Run through the list until one fires
foreach ( (array) $redirects as $item ) {
$action = $item->get_match( $request->get_decoded_url(), $request->get_original_url() );
if ( $action ) {
$this->matched = $item;
do_action( 'redirection_matched', $request->get_decoded_url(), $item, $redirects );
$action->run();
break;
}
}
// We will only get here if there is no match (check $this->matched) or the action does not result in redirecting away
do_action( 'redirection_last', $request->get_decoded_url(), $this, $redirects );
if ( ! $this->matched ) {
// Keep them for later
$this->redirects = $redirects;
}
}
}
/**
* Fix for incorrect headers sent when using FastCGI/IIS
*
* @param string $status HTTP status line.
* @return string
*/
public function status_header( $status ) {
if ( substr( php_sapi_name(), 0, 3 ) === 'cgi' ) {
return str_replace( 'HTTP/1.1', 'Status:', $status );
}
return $status;
}
/**
* Add any custom HTTP headers to the response.
*
* @param array $obj Some object.
* @return void
*/
public function send_headers( $obj ) {
if ( ! empty( $this->matched ) && $this->matched->action && $this->matched->action->get_code() === 410 ) {
add_filter( 'status_header', [ $this, 'set_header_410' ] );
}
// Add any custom headers
$options = red_get_options();
$headers = new Red_Http_Headers( $options['headers'] );
$headers->run( $headers->get_site_headers() );
}
/**
* Add support for a 410 response.
*
* @return string
*/
public function set_header_410() {
return 'HTTP/1.1 410 Gone';
}
/**
* IIS fix. Don't know if this is still needed
*
* @param string $url URL.
* @return void
*/
private function iis_fix( $url ) {
global $is_IIS;
if ( $is_IIS ) {
header( "Refresh: 0;url=$url" );
}
}
/**
* Don't know if this is still needed
*
* @param string $url URL.
* @param integer $status HTTP status code.
* @return void
*/
private function cgi_fix( $url, $status ) {
if ( $status === 301 && php_sapi_name() === 'cgi-fcgi' ) {
$servers_to_check = [ 'lighttpd', 'nginx' ];
$server = '';
if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && is_string( $_SERVER['SERVER_SOFTWARE'] ) ) {
$server = sanitize_text_field( $_SERVER['SERVER_SOFTWARE'] );
}
foreach ( $servers_to_check as $name ) {
if ( stripos( $server, $name ) !== false ) {
status_header( $status );
header( "Location: $url" );
exit( 0 );
}
}
}
}
/**
* Get a 'source' for a redirect by digging through the backtrace.
*
* @return string[]
*/
private function get_redirect_source() {
$ignore = [
'WP_Hook',
'template-loader.php',
'wp-blog-header.php',
];
// phpcs:ignore
$source = wp_debug_backtrace_summary( null, 5, false );
return array_filter( $source, function( $item ) use ( $ignore ) {
foreach ( $ignore as $ignore_item ) {
if ( strpos( $item, $ignore_item ) !== false ) {
return false;
}
}
return true;
} );
}
/**
* Record a redirect.
*
* @param string $agent Redirect agent.
* @return string
*/
public function record_redirect_by( $agent ) {
// Have we already redirected with Redirection?
if ( $this->matched || $agent === 'redirection' ) {
return $agent;
}
$options = red_get_options();
if ( ! $options['log_external'] ) {
return $agent;
}
$details = [
'target' => $this->redirect_url,
'agent' => Redirection_Request::get_user_agent(),
'referrer' => Redirection_Request::get_referrer(),
'request_method' => Redirection_Request::get_request_method(),
'redirect_by' => $agent ? $agent : 'wordpress',
'http_code' => $this->redirect_code,
'request_data' => [
'source' => array_values( $this->get_redirect_source() ),
],
];
if ( $options['log_header'] ) {
$details['request_data']['headers'] = Redirection_Request::get_request_headers();
}
Red_Redirect_Log::create( Redirection_Request::get_server(), Redirection_Request::get_request_url(), Redirection_Request::get_ip(), $details );
return $agent;
}
/**
* Perform any pre-redirect processing, such as logging and header fixing.
*
* @param string $url Target URL.
* @param integer $status HTTP status.
* @return string
*/
public function wp_redirect( $url, $status = 302 ) {
global $wp_version;
$this->redirect_url = $url;
$this->redirect_code = $status;
$this->iis_fix( $url );
$this->cgi_fix( $url, $status );
if ( intval( $status, 10 ) === 307 ) {
status_header( $status );
nocache_headers();
return $url;
}
$options = red_get_options();
$headers = new Red_Http_Headers( $options['headers'] );
$headers->run( $headers->get_redirect_headers() );
// Do we need to set the cache header?
if ( ! headers_sent() && isset( $options['redirect_cache'] ) && $options['redirect_cache'] !== 0 && intval( $status, 10 ) === 301 ) {
if ( $options['redirect_cache'] === -1 ) {
// No cache - just use WP function
nocache_headers();
} else {
// Custom cache
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s T', time() + $options['redirect_cache'] * 60 * 60 ) );
header( 'Cache-Control: max-age=' . $options['redirect_cache'] * 60 * 60 );
}
}
status_header( $status );
return $url;
}
/**
* Reset the module. Used for unit tests
*
* @param Red_Item|false $matched Set the `matched` var.
* @return void
*/
public function reset( $matched = false ) {
$this->can_log = true;
$this->matched = $matched;
}
/**
* Can we log a redirect?
*
* @return boolean
*/
public function can_log() {
return apply_filters( 'redirection_log_404', $this->can_log );
}
}

View File

@@ -0,0 +1,895 @@
=== Redirection ===
Contributors: johnny5
Donate link: https://redirection.me/donation/
Tags: redirect, htaccess, 301, 404, apache
Tested up to: 6.8
Stable tag: 5.5.2
License: GPLv3
Manage 301 redirects, track 404 errors, and improve your site. No knowledge of Apache or Nginx required.
== Description ==
Redirection is the most popular redirect manager for WordPress. With it you can easily manage 301 redirections, keep track of 404 errors, and generally tidy up any loose ends your site may have. This can help reduce errors and improve your site ranking.
Redirection is designed to be used on sites with a few redirects to sites with thousands of redirects.
It has been a WordPress plugin for over 10 years and has been recommended countless times. And it's free!
Full documentation can be found at [https://redirection.me](https://redirection.me)
Redirection is compatible with PHP from 7.0 to 8.3.
= Redirect manager =
Create and manage redirects quickly and easily without needing Apache or Nginx knowledge. If your WordPress supports permalinks then you can use Redirection to redirect any URL.
There is full support for regular expressions so you can create redirect patterns to match any number of URLs. You can match query parameters and even pass them through to the target URL.
The plugin can also be configured to monitor when post or page permalinks are changed and automatically create a redirect to the new URL.
= Conditional redirects =
In addition to straightforward URL matching you can redirect based on other conditions:
- Login status - redirect only if the user is logged in or logged out
- WordPress capability - redirect if the user is able to perform a certain capability
- Browser - redirect if the user is using a certain browser
- Referrer - redirect if the user visited the link from another page
- Cookies - redirect if a particular cookie is set
- HTTP headers - redirect based on a HTTP header
- Custom filter - redirect based on your own WordPress filter
- IP address - redirect if the client IP address matches
- Server - redirect another domain if also hosted on this server
- Page type - redirect if the current page is a 404
= Full logging =
A configurable logging option allows to view all redirects occurring on your site, including information about the visitor, the browser used, and the referrer. A 'hit' count is maintained for each redirect so you can see if a URL is being used.
Logs can be exported for external viewing, and can be searched and filtered for more detailed investigation.
Display geographic information about an IP address, as well as a full user agent information, to try and understand who the visitor is.
You are able to disable or reduce IP collection to meet the legal requirements of your geographic region, and can change the amount of information captured from the bare minimum to HTTP headers.
You can also log any redirect happening on your site, including those performed outside of Redirection.
= Add HTTP headers =
HTTP headers can be added to redirects or your entire site that help reduce the impact of redirects or help increase security. You can also add your own custom headers.
= Track 404 errors =
Redirection will keep track of all 404 errors that occur on your site, allowing you to track down and fix problems.
Errors can be grouped to show where you should focus your attention, and can be redirected in bulk.
= Query parameter handling =
You can match query parameters exactly, ignore them, and even pass them through to your target.
= Migrate Permalinks =
Changed your permalink structure? You can migrate old permalinks simply by entering the old permalink structure. Multiple migrations are supported.
= Apache & Nginx support =
By default Redirection will manage all redirects using WordPress. However you can configure it so redirects are automatically saved to a .htaccess file and handled by Apache itself.
If you use Nginx then you can export redirects to an Nginx rewrite rules file.
= Fine-grained permissions =
Fine-grained permissions are available so you can customise the plugin for different users. This makes it particularly suitable for client sites where you may want to prevent certain actions, and remove functionality.
= Import & Export =
The plugin has a fully-featured import and export system and you can:
- Import and export to Apache .htaccess
- Export to Nginx rewrite rules
- Copy redirects between sites using JSON
- Import and export to CSV for viewing in a spreadsheet
- Use WP CLI to automate import and export
You can also import from the following plugins:
- Simple 301 Redirects
- SEO Redirection
- Safe Redirect Manager
- Rank Math
- WordPress old slug redirects
- Quick Post/Pages redirects
= Search Regex compatible =
Redirection is compatible with [Search Regex](https://searchregex.com), allowing you to bulk update your redirects.
= Wait, it's free? =
Yes, it's really free. There's no premium version and no need to pay money to get access to features. This is a dedicated redirect management plugin.
== Support ==
Please submit bugs, patches, and feature requests to:
[https://github.com/johngodley/redirection](https://github.com/johngodley/redirection)
Please submit translations to:
[https://translate.wordpress.org/projects/wp-plugins/redirection](https://translate.wordpress.org/projects/wp-plugins/redirection)
== Installation ==
The plugin is simple to install:
1. Download `redirection.zip`
1. Unzip
1. Upload `redirection` directory to your `/wp-content/plugins` directory
1. Go to the plugin management page and enable the plugin
1. Configure the options from the `Tools/Redirection` page
You can find full details of installing a plugin on the [plugin installation page](https://redirection.me/support/installation/).
Full documentation can be found on the [Redirection](https://redirection.me/support/) site.
== Screenshots ==
1. Redirection management interface
2. Adding a redirection
3. Redirect logs
4. Import/Export
5. Options
6. Support
== Frequently Asked Questions ==
= Why would I want to use this instead of .htaccess? =
Ease of use. Redirections are automatically created when a post URL changes, and it is a lot easier to manually add redirections than to hack around a .htaccess. You also get the added benefit of being able to keep track of 404 errors.
= What is the performance of this plugin? =
The plugin works in a similar manner to how WordPress handles permalinks and should not result in any noticeable slowdown to your site.
== Upgrade Notice ==
= 5.4 =
* You may need to configure the IP header option if using a proxy
= 3.0 =
* Upgrades the database to support IPv6. Please backup your data and visit the Redirection settings to perform the upgrade
* Switches to the WordPress REST API
* Permissions changed from 'administrator' role to 'manage_options' capability
= 3.6.1 =
* Note Redirection will not work with PHP < 5.4 after 3.6 - please upgrade your PHP
= 3.7 =
* Requires minimum PHP 5.4. Do not upgrade if you are still using PHP < 5.4
= 4.0 =
* Alters database to support case insensitivity, trailing slashes, and query params. Please backup your data
= 4.7 =
* Requires minimum PHP 5.6+. Do not upgrade if you are still using PHP < 5.6
= 4.9 =
* Alters database to support enhanced logging. Please backup your data
== Changelog ==
A x.1 version increase introduces new or updated features and can be considered to contain 'breaking' changes. A x.x.1 increase is purely a bug fix and introduces no new features, and can be considered as containing no breaking changes.
= 5.5.2 =
* Fix saving of x-frame-options
* Fix CPT loading
* Fix last access date changing on update
* Remove newsletter option
= 5.5.1 =
* Fix problem with category pages and permalink migration
* Don't report invalid JSON import as successful
* Exclude CPTs without URLs for monitoring
* Update for WP 6.7
= 5.5.0 =
* Multiple 'URL and WP page type' redirects will now work
* Translations now use WP core
= 5.4.2 - 27th January 2024 =
* Remove Geo IP option (it may return)
* Fix crash in agent info
* Add new max-age header
* Remove deprecated ini_set call
* Don't double encode URLs when checking
= 5.4.1 - 5th January 2024 =
* Fix problem with some international URLs not appearing in the 404 log
= 5.4 - 1st January 2024 =
* Don't encode negative lookaheads
* Remove port from server name
* Importing into a disabled group now creates disabled items
* Add option to pick IP header
* Fix save of x-content-type-options: sniff
* Fix save of multiple spaces
= 5.3.10 - 2nd April 2023 =
* Fix associated redirect setting not saving properly
= 5.3.9 - 25th January 2023 =
* Fix incorrect sanitization applied to target URLs
= 5.3.8 - 22nd January 2023 =
* Fix app rendering twice causing problems with upgrades
* Fix CSV header being detected as an error
= 5.3.7 - 8th January 2023 =
* Fix problem with locales in certain directories
* Fix incorrect import of empty CSV lines
* Don't encode regex for Nginx
= 5.3.6 - 12th November 2022 =
* Fix for sites with a version of +OK
* Another fix for CZ locale
= 5.3.5 - 6th November 2022 =
* Fix crash on options page for Czech language
= 5.3.4 - 14th September 2022 =
* Fix query parameter name with a + not matching
= 5.3.3 - 7th September 2022 =
* Fix default HTTP header not being set when first used
* Fix incorrect column heading in CSV
* Fix passing of mixed case parameters
= 5.3.2 - 6th August 2022 =
* Fix missing props error
* Fix missing value for .htaccess location display
= 5.3.1 - 29th July 2022 =
* Fix crash caused by bad translations in locale files
* Fix query match not working when it contained mixed case
* Fix missing flag in .htaccess export
= 5.3.0 - 21st July 2022 =
* Improve installation process
* Improve permalink migration so it works with more permalinks
* Prevent ordering columns by HTTP code
* Better encode URLs in Nginx export
* Allow escaped characters to work in the redirect checker
* Reduce CSV import time
= 5.2.3 - 6th February 2022 =
* Fix error when grouping by URL, adding redirect, and then adding another redirect
* Add a warning for unescaped ? regex
= 5.2.2 - 22nd January 2022 =
* Further improve URL checker response to clarify responsibility
* Fix WordPress and pagetype match preventing the logging of 404s
* Fix title field being inactive
* Fix CSV export having duplicate column
= 5.2.1 - 16th January 2022 =
* Include path with inline URL checker
= 5.2 - 15th January 2022 =
* Improve URL checker and show more details
* Retain query parameter case when passing to target URL
* Remove unnecessary database stage option check
* PHP 8.1 compatibility
= 5.1.3 - 24th July 2021 =
* Fix geo IP on log pages showing an API redirected error
* Fix crash when changing match type in edit dialog
= 5.1.2 - 17th July 2021 =
* Fix random redirect not working
* Fix [userid] shortcode returning 1
= 5.1.1 - 11th April 2021 =
* Revert the permalink migration improvement from 5.1 as it's causing problems on some sites
= 5.1 - 10th April 2021 =
* Add importer for PrettyLinks
* Fix crash converting a 'do nothing' to 'redirect to URL'
* Improve warning messages
* Improve permalink migration when is_404 is not set
* Fix 'delete log entries' returning blank data
* Fix missing .htaccess location
* Fix hits & date not imported with JSON format
= 5.0.1 - 26th Jan 2021 =
* Fix incorrect warning when creating a regular expression with captured data
* Fix JS error when upgrading a database with a broken REST API
* Increase regular expression redirect limit
* PHP8 support
= 5.0 - 16th Jan 2021 =
* Add caching support
* Add support for migrated permalink structures
* Add dynamic URL variables
* Add fully automatic database upgrade option
* Add a new version release information prompt
* Improve performance when many redirects have the same path
* Move bulk all action to a separate button after selecting all
* Fix error in display with restricted capabilities
* Avoid problems with 7G Firewall
* Improve handling of invalid encoded characters
= 4.9.2 - 30th October =
* Fix warning with PHP 5.6
* Improve display of long URLs
= 4.9.1 - 26th October 2020 =
* Restore missing time and referrer URL from log pages
* Restore missing client information from debug reports
* Fix order by count when grouping by URL
* Check for duplicate columns in DB upgrade
= 4.9 - 24th October 2020 =
* Expand log information to capture HTTP headers, domain, HTTP code, and HTTP method
* Allow non-Redirection redirects to be logged - allows tracking of all redirects on a site
* Expand log and 404 pages with greatly improved filters
* Bulk delete logs and 404s by selected filter
* Logging is now optional per redirect rule
* Fix random action on a site with non-root URL
* Fix group and search being reset when searching
* Fix canonical alias not using request server name
= 4.8 - 23rd May 2020 =
* Add importer for Quick Post/Page Redirects plugin
* Add plugin imports to WP CLI
* Fix install wizard using wrong relative API
* Fix sub menu outputting invalid HTML
= 4.7.2 - 8th May 2020 =
* Fix PHP warning decoding an encoded question mark
* Fix site adding an extra period in a domain name
* Fix protocol appearing in .htaccess file server redirect
= 4.7.1 - 14th March 2020 =
* Fix HTTP header over-sanitizing the value
* Fix inability to remove .htaccess location
* Fix 404 group by 'delete all'
* Fix import of empty 'old slugs'
= 4.7 - 15th February 2020 =
* Relocate entire site to another domain, with exceptions
* Site aliases to map another site to current site
* Canonical settings for www/no-www
* Change content-type for API requests to help with mod_security
= 4.6.2 - 6th January 2020 =
* Fix 404 log export button
* Fix HTTPS option not appearing enabled
* Fix another PHP compat issue
= 4.6.1 - 30th December 2019 =
* Back-compatibility fix for old PHP versions
= 4.6 - 27th December 2019 =
* Add fine-grained permissions allowing greater customisation of the plugin, and removal of functionality
* Add an import step to the install wizard
* Remove overriding of default WordPress 'old slugs'
= 4.5.1 - 23rd November 2019 =
* Fix broken canonical redirects
= 4.5 - 23rd November 2019 =
* Add HTTP header feature, with x-robots-tag support
* Move HTTPS setting to new Site page
* Add filter to disable redirect hits
* Add 'Disable Redirection' option to stop Redirection, in case you break your site
* Fill out API documentation
* Fix style with WordPress 5.4
* Fix encoding of # in .htaccess
= 4.4.2 - 29th September 2019 =
* Fix missing options for monitor group
* Fix check redirect not appearing if position column not shown
= 4.4.1 - 28th September 2019 =
* Fix search highlighter causing problems with regex characters
* Fix 'show all' link not working
* Fix 'Request URI Too Long' error when switching pages after creating redirects
= 4.4 - 22nd September 2019 =
* Add 'URL and language' match
* Add page display type for configurable information
* Add 'search by' to search by different information
* Add filter dropdown to filter data
* Add warning about relative absolute URLs
* Add 451, 500, 501, 502, 503, 504 error codes
* Fix multiple 'URL and page type' redirects
* Improve invalid nonce warning
* Encode replaced values in regular expression targets
= 4.3.3 - 8th August 2019 ==
* Add back compatibility fix for URL sanitization
= 4.3.2 - 4th August 2019 ==
* Fix problem with UTF8 characters in a regex URL
* Fix invalid characters causing an error message
* Fix regex not disabled when removed
= 4.3.1 - 8th June 2019 =
* Fix + character being removed from source URL
= 4.3 - 2nd June 2019 =
* Add support for UTF8 URLs without manual encoding
* Add manual database install option
* Add check for pipe character in target URL
* Add warning when problems saving .htaccess file
* Switch from 'x-redirect-agent' to 'x-redirect-by', for WP 5+
* Improve handling of invalid query parameters
* Fix query param name is a number
* Fix redirect with blank target and auto target settings
* Fix monitor trash option applying when deleting a draft
* Fix case insensitivity not applying to query params
* Disable IP grouping when IP option is disabled
* Allow multisite database updates to run when more than 100 sites
= 4.2.3 - 16th Apr 2019 =
* Fix bug with old API routes breaking test
= 4.2.2 - 13th Apr 2019 =
* Improve API checking logic
* Fix '1' being logged for pass-through redirects
= 4.2.1 - 8th Apr 2019 =
* Fix incorrect CSV download link
= 4.2 - 6th Apr 2019 =
* Add auto-complete for target URLs
* Add manual database upgrade
* Add support for semi-colon separated import files
* Add user agent to 404 export
* Add workaround for qTranslate breaking REST API
* Improve API problem detection
* Fix JSON import ignoring group status
= 4.1.1 - 23rd Mar 2019 =
* Remove deprecated PHP
* Fix REST API warning
* Improve WP CLI database output
= 4.1 - 16th Mar 2019 =
* Move 404 export option to import/export page
* Add additional redirect suggestions
* Add import from Rank Math
* Fix 'force https' causing WP to redirect to admin URL when accessing www subdomain
* Fix .htaccess import adding ^ to the source
* Fix handling of double-slashed URLs
* Fix WP CLI on single site
* Add DB upgrade to catch URLs with double-slash URLs
* Remove unnecessary escaped slashes from JSON output
= 4.0.1 - 2nd Mar 2019 =
* Improve styling of query flags
* Match DB upgrade for new match_url to creation script
* Fix upgrade on some hosts where plugin is auto-updated
* Fix pagination button style in WP 5.1
* Fix IP match when action is 'error'
* Fix database upgrade on multisite WP CLI
= 4.0 - 23rd Feb 2019 =
* Add option for case insensitive redirects
* Add option to ignore trailing slashes
* Add option to copy query parameters to target URL
* Add option to ignore query parameters
* Add option to set defaults for case, trailing, and query settings
* Improve upgrade for sites with missing tables
= 3.7.3 - 2nd Feb 2019 =
* Add PHP < 5.4 message on plugins page
* Prevent upgrade message being hidden by other plugins
* Fix warning with regex and no leading slash
* Fix missing display of disabled redirects with a title
* Improve upgrade for sites with a missing IP column
* Improve API detection with plugins that use sessions
* Improve compatibility with ModSecurity
* Improve compatibility with custom API prefix
* Detect site where Redirection was once installed and has settings but no database tables
= 3.7.2 - 16th Jan 2019 =
* Add further partial upgrade detection
* Add fallback for sites with no REST API value
= 3.7.1 - 13th Jan 2019 =
* Clarify database upgrade text
* Fix Firefox problem with multiple URLs
* Fix 3.7 built against wrong dropzone module
* Add DB upgrade detection for people with partial 2.4 sites
= 3.7 - 12th Jan 2019 =
* Add redirect warning for known problem redirects
* Add new database install and upgrade process
* Add database functions to WP CLI
* Add introduction message when first installed
* Drop PHP < 5.4 support. Please use version 3.6.3 if your PHP is too old
* Improve export filename
* Fix IPs appearing for bulk redirect
* Fix disabled redirects appearing in htaccess
= 3.6.3 - 14th November 2018 =
* Remove potential CSRF
= 3.6.2 - 10th November 2018 =
* Add another PHP < 5.4 compat fix
* Fix 'delete all from 404 log' when ungrouped deleting all 404s
* Fix IDs shown in bulk add redirect
= 3.6.1 - 3rd November 2018 =
* Add another PHP < 5.4 fix. Sigh
= 3.6 - 3rd November 2018 =
* Add option to ignore 404s
* Add option to block 404s by IP
* Add grouping of 404s by IP and URL
* Add bulk block or redirect a group of 404s
* Add option to redirect on a 404
* Better page navigation change monitoring
* Add URL & IP match
* Add 303 and 304 redirect codes
* Add 400, 403, and 418 (I'm a teapot!) error codes
* Fix server match not supporting regex properly
* Deprecated file pass through removed
* 'Do nothing' now stops processing further rules
= 3.5 - 23rd September 2018 =
* Add redirect checker on redirects page
* Fix missing translations
* Restore 4.7 backwards compatibility
* Fix unable to delete server name in server match
* Fix error shown when source URL is blank
= 3.4.1 - 9th September 2018 =
* Fix import of WordPress redirects
* Fix incorrect parsing of URLs with 'http' in the path
* Fix 'force ssl' not including path
= 3.4 - 17th July 2018 =
* Add a redirect checker
* Fix incorrect host parsing with server match
* Fix PHP warning with CSV import
* Fix old capability check that was missed from 3.0
= 3.3.1 - 24th June 2018 =
* Add a minimum PHP check for people < 5.4
= 3.3 - 24th June 2018 =
* Add user role/capability match
* Add fix for IP blocking plugins
* Add server match to redirect other domains (beta)
* Add a force http to https option (beta)
* Use users locale setting, not site
* Check for mismatched site/home URLs
* Fix WP CLI not clearing logs
* Fix old capability check
* Detect BOM marker in response
* Improve detection of servers that block content-type json
* Fix incorrect encoding of entities in some locale files
* Fix table navigation parameters not affecting subsequent pages
* Fix .htaccess saving after WordPress redirects
* Fix get_plugin_data error
* Fix canonical redirect problem caused by change in WordPress
* Fix situation that prevented rules cascading
= 3.2 - 11th February 2018 =
* Add cookie match - redirect based on a cookie
* Add HTTP header match - redirect based on an HTTP header
* Add custom filter match - redirect based on a custom WordPress filter
* Add detection of REST API redirect, causing 'fetch error' on some sites
* Update table responsiveness
* Allow redirects for canonical WordPress URLs
* Fix double include error on some sites
* Fix delete action on some sites
* Fix trailing slash redirect of API on some sites
= 3.1.1 - 29th January 2018 =
* Fix problem fetching data on sites without https
= 3.1 - 27th January 2018 =
* Add alternative REST API routes to help servers that block the API
* Move DELETE API calls to POST, to help servers that block DELETE
* Move API nonce to query param, to help servers that don't pass HTTP headers
* Improve error messaging
* Preload support page so it can be used when REST API isn't working
* Fix bug editing Nginx redirects
* Fix import from JSON not setting status
= 3.0.1 - 21st Jan 2018 =
* Don't show warning if per page setting is greater than max
* Don't allow WP REST API to be redirected
= 3.0 - 20th Jan 2018 =
* Add support for IPv6
* Add support for disabling or anonymising IP collection
* Add support for monitoring custom post types
* Add support for monitoring from quick edit mode
* Default to last group used when editing
* Permissions changed from 'administrator' role to 'manage_options' capability
* Swap to WP REST API
* Add new IP map service
* Add new useragent service
* Add 'add new' button to redirect page
* Increase 'title' length
* Fix position not saving on creation
* Fix log pages not remembering table settings
* Fix incorrect column used for HTTP code when importing CSV
* Add support links from inside the plugin
= 2.10.1 - 26th November 2017 =
* Fix incorrect HTTP code reported in errors
* Improve management page hook usage
= 2.10 - 18th November 2017 =
* Add support for WordPress multisite
* Add new Redirection documentation
* Add extra actions when creating redirects
* Fix user agent dropdown not setting agent
= 2.9.2 - 11th November 2017 =
* Fix regex breaking .htaccess export
* Fix error when saving Error or No action
* Restore sortable table headers
= 2.9.1 - 4th November 2017 =
* Fix const issues with PHP 5
= 2.9 - 4th November 2017 =
* Add option to set redirect cache expiry, default 1 hour
* Add a check for unsupported versions of WordPress
* Add check for database tables before starting the plugin
* Improve JSON import memory usage
* Add importers for: Simple 301 Redirects, SEO Redirection, Safe Redirect Manager, and WordPress old post slugs
* Add responsive admin UI
= 2.8.1 - 22nd October 2017 =
* Fix redirect edit not closing after save
* Fix user agent dropdown not auto-selecting regex
* Fix focus to bottom of page on load
* Improve error message when failing to start
* Fix associated redirect appearing at start of URL, not end
= 2.8 - 18th October 2017 =
* Add a fixer to the support page
* Ignore case for imported files
* Fixes for Safari
* Fix WP CLI importing CSV
* Fix monitor not setting HTTP code
* Improve error, random, and pass-through actions
* Fix bug when saving long title
* Add user agent dropdown to user agent match
* Add pages and trashed posts to monitoring
* Add 'associated redirect' option to monitoring, for AMP
* Remove 404 after adding
* Allow search term to apply to deleting logs and 404s
* Deprecate file pass-through, needs to be enabled with REDIRECTION_SUPPORT_PASS_FILE and will be replaced with WP actions
* Further sanitize match data against bad serialization
= 2.7.3 - 26th August 2017 =
* Fix an import regression bug
= 2.7.2 - 25th August 2017 =
* Better IE11 support
* Fix Apache importer
* Show more detailed error messages
* Refactor match code and fix a problem saving referrer & user agent matches
* Fix save button not enabling for certain redirect types
= 2.7.1 - 14th August 2017 =
* Improve display of errors
* Improve handling of CSV
* Reset tables when changing menus
* Change how the page is displayed to reduce change of interference from other plugins
= 2.7 - 6th August 2017 =
* Finish conversion to React
* Add WP CLI support for import/export
* Add a JSON import/export that exports all data
* Edit redirect position
* Apache config moved to options page
* Fix 410 error code
* Fix page limits
* Fix problems with IE/Safari
= 2.6.6 =
* Use React on redirects page
* Use translate.wordpress.org for language files
= 2.6.5 =
* Use React on groups page
= 2.6.4 =
* Add a limit to per page screen options
* Fix warning in referrer match when referrer doesn't exist
* Fix 404 page showing options
* Fix RSS token not regenerating
* 404 and log filters can now avoid logging
* Use React on modules page
= 2.6.3 =
* Use React on log and 404 pages
* Fix log option not saving 'never'
* Additional check for auto-redirect from root
* Fix delete plugin button
* Improve IP detection for Cloudflare
= 2.6.2 =
* Set auto_detect_line_endings when importing CSV
* Replace options page with a fancy React version that looks exactly the same
= 2.6.1 =
* Fix CSV export merging everything into one line
* Fix bug with HTTP codes not being imported from CSV
* Add filters for source and target URLs
* Add filters for log and 404s
* Add filters for request data
* Add filter for monitoring post permalinks
* Fix export of 404 and logs
= 2.6 =
* Show example CSV
* Allow regex and redirect code to be set on import
* Fix a bunch of database installation problems
= 2.5 =
* Fix no group created on install
* Fix missing export key on install
* Add 308 HTTP code, props to radenui
* Fix imported URLs set to regex, props to alpipego
* Fix sorting of URLs, props to JordanReiter
* Don't cache 307s, props to rmarchant
* Abort redirect exit if no redirection happened, props to junc
= 2.4.5 =
* Ensure cleanup code runs even if plugin was updated
* Extra sanitization of Apache & Nginx files, props to Ed Shirey
* Fix regex bug, props to romulodl
* Fix bug in correct group not being shown in dropdown
= 2.4.4 =
* Fix large advanced settings icon
* Add text domain to plugin file, props Bernhard Kau
* Better PHP7 compatibility, props to Ohad Raz
* Better Polylang compatibility, props to imrehg
= 2.4.3 =
* Bump minimum WP to 4.0.0
* Updated German translation, props to Konrad Tadesse
* Additional check when creating redirections in case of bad data
= 2.4.2 =
* Add Gulp task to generate POT file
* Fix a problem with duplicate positions in the redirect table, props to Jon Jensen
* Fix URL monitor not triggering
* Fix CSV export
= 2.4.1 =
* Fix error for people with an unknown module in a group
= 2.4 =
* Reworked modules now no longer stored in database
* Nginx module (experimental)
* View .htaccess/Nginx inline
* Beginnings of some unit tests!
* Fix DB creation on activation, props syntax53
* Updated Japanese locale, props to Naoko
* Remove deprecated like escaping
= 2.3.16 =
* Fix export options not showing for some people
= 2.3.15 =
* Fix error on admin page for WP 4.2
= 2.3.14 =
* Remove error_log statements
* Fix incorrect table name when exporting 404 errors, props to brazenest/synchronos-t
= 2.3.13 =
* Split admin and front-end code out to streamline the loading a bit
* Fix bad groups link when viewing redirects in a group, props to Patrick Fabre
* Improved plugin activation/deactivation and cleanup
* Improved log clearing
= 2.3.12 =
* Persian translation by Danial Hatami
* Fix saving a redirection with login status, referrer, and user agent
* Fix problem where deleting your last group would cause Redirection to only show an error
* Add limits to referrer and destination in the logs
* Redirect title now shows in the main list again. The field is hidden when editing until toggled
* Fix 'bad nonce' error, props to Jonathan Harrell
* Remove old WP code
= 2.3.11 =
* Fix log cleanup options
* More space when editing redirects
* Better detection of regex when importing
* Restore export options
* Fix unnecessary protected
= 2.3.10 =
* Another compatibility fix for PHP < 5.3
* Fix incorrect module ID used when creating a group
* Fix .htaccess duplication, props to Jörg Liwa
= 2.3.9 =
* Compatibility fix for PHP < 5.3
= 2.3.8 =
* Fix plugin activation error
* Fix fatal error in table nav, props to spacedmonkey
= 2.3.7 =
* New redirect page to match WP style
* New module page to match WP style
* Configurable permissions via redirection_role filter, props to RodGer-GR
* Fix saving 2 month log period
* Fix importer
* Fix DB creation to check for existing tables
= 2.3.6 =
* Updated Italian translation, props to Raffaello Tesi
* Updated Romanian translation, props to Flo Bejgu
* Simplify logging options
* Fix log deletion by search term
* Export logs and 404s to CSV
= 2.3.5 =
* Default log settings to 7 days, props to Maura
* Updated Danish translation thanks to Mikael Rieck
* Add per-page screen option for log pages
* Remove all the corners
= 2.3.4 =
* Fix escaping of URL in admin page
= 2.3.3 =
* Fix PHP strict, props to Juliette Folmer
* Fix RSS entry date, props to Juliette
* Fix pagination
= 2.3.2 =
* WP 3.5 compatibility
* Fix export
= 2.3.0 and earlier =
* Remove 404 module and move 404 logs into a separate option
* Clean up log code, using WP_List_Table to power it
* Fix some broken links in admin pages
* Fix order of redirects, thanks to Nicolas Hatier
* Fix XSS in admin menu & referrers log
* Better database compatibility
* Remove warning from VaultPress
* Remove debug from htaccess module
* Fix encoding of JS strings
* Use fgetcsv for CSV importer - better handling
* Allow http as URL parameter
* Props to Ben Noordhuis for a patch
* WordPress 2.9+ only - cleaned up all the old cruft
* Better new-install process
* Upgrades from 1.0 of Redirection no longer supported
* Optimized DB tables
* Change to jQuery
* Nonce protection
* Disable category monitor in 2.7
* Refix log delete
* get_home_path seems not be available for some people
* Update plugin.php to better handle odd directories
* Correct DB install
* Install defaults when no existing redirection setup
* Fix problem with custom post types auto-redirecting (click on 'groups' and then 'modified posts' and clear any entries for '/' from your list)
* Database optimization
* Add patch to disable logs (thanks to Simon Wheatley!)
* Fix for some users with problems deleting redirections
* Fix group edit and log add entry
* Disable category monitoring
* Fix 'you do not permissions' error on some non-English sites
* Fix category change 'quick edit'
* RSS feed token

View File

@@ -0,0 +1,720 @@
<?php
require_once __DIR__ . '/models/group.php';
require_once __DIR__ . '/models/monitor.php';
require_once __DIR__ . '/models/file-io.php';
require_once __DIR__ . '/database/database.php';
require_once __DIR__ . '/redirection-capabilities.php';
define( 'RED_DEFAULT_PER_PAGE', 25 );
define( 'RED_MAX_PER_PAGE', 200 );
class Redirection_Admin {
private static $instance = null;
private $monitor;
private $fixit_failed = false;
public static function init() {
if ( is_null( self::$instance ) ) {
self::$instance = new Redirection_Admin();
}
return self::$instance;
}
public function __construct() {
add_action( 'admin_menu', [ $this, 'admin_menu' ] );
add_action( 'admin_notices', [ $this, 'update_nag' ] );
add_filter( 'plugin_action_links_' . basename( dirname( REDIRECTION_FILE ) ) . '/' . basename( REDIRECTION_FILE ), [ $this, 'plugin_settings' ], 10, 4 );
add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 4 );
add_filter( 'redirection_save_options', [ $this, 'flush_schedule' ] );
add_filter( 'set-screen-option', [ $this, 'set_per_page' ], 10, 3 );
add_filter( 'set_screen_option_redirection_log_per_page', function( $ignore, $option, $value ) {
return $value;
}, 10, 3 );
add_action( 'redirection_redirect_updated', [ $this, 'set_default_group' ], 10, 2 );
add_action( 'redirection_redirect_updated', [ $this, 'clear_cache' ], 10, 2 );
if ( defined( 'REDIRECTION_FLYING_SOLO' ) && REDIRECTION_FLYING_SOLO ) {
add_filter( 'script_loader_src', [ $this, 'flying_solo' ], 10, 2 );
}
register_deactivation_hook( REDIRECTION_FILE, [ 'Redirection_Admin', 'plugin_deactivated' ] );
register_uninstall_hook( REDIRECTION_FILE, [ 'Redirection_Admin', 'plugin_uninstall' ] );
$this->monitor = new Red_Monitor( red_get_options() );
$this->run_hacks();
}
// These are only called on the single standard site, or in the network admin of the multisite - they run across all available sites
public static function plugin_activated() {
Red_Database::apply_to_sites( function() {
Red_Flusher::clear();
red_set_options();
} );
}
// These are only called on the single standard site, or in the network admin of the multisite - they run across all available sites
public static function plugin_deactivated() {
Red_Database::apply_to_sites( function() {
Red_Flusher::clear();
} );
}
// These are only called on the single standard site, or in the network admin of the multisite - they run across all available sites
public static function plugin_uninstall() {
$database = Red_Database::get_latest_database();
Red_Database::apply_to_sites( function() use ( $database ) {
$database->remove();
} );
}
/**
* Show the database upgrade nag
*
* @return void
*/
public function update_nag() {
$options = red_get_options();
// Is the site configured to upgrade automatically?
if ( $options['plugin_update'] === 'admin' ) {
$this->automatic_upgrade();
return;
}
// Can the user perform a manual database upgrade?
if ( ! Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_OPTION_MANAGE ) ) {
return;
}
// Default manual update, with nag
$status = new Red_Database_Status();
$message = false;
if ( $status->needs_installing() ) {
/* translators: 1: URL to plugin page */
$message = sprintf( __( 'Please complete your <a href="%s">Redirection setup</a> to activate the plugin.', 'redirection' ), esc_url( $this->get_plugin_url() ) );
} elseif ( $status->needs_updating() ) {
/* translators: 1: URL to plugin page, 2: current version, 3: target version */
$message = sprintf( __( 'Redirection\'s database needs to be updated - <a href="%1$1s">click to update</a>.', 'redirection' ), esc_url( $this->get_plugin_url() ) );
}
if ( ! $message || strpos( Redirection_Request::get_request_url(), 'page=redirection.php' ) !== false ) {
return;
}
// Known HTML and so isn't escaped
// phpcs:ignore
echo '<div class="update-nag notice notice-warning" style="width: 95%">' . $message . '</div>';
}
/**
* Perform an automatic DB upgrade
*
* @return void
*/
private function automatic_upgrade() {
$loop = 0;
$status = new Red_Database_Status();
$database = new Red_Database();
// Loop until the DB is upgraded, or until a max is exceeded (just in case)
while ( $loop < 20 ) {
if ( ! $status->needs_updating() ) {
break;
}
$database->apply_upgrade( $status );
if ( $status->is_error() ) {
// If an error occurs then switch to 'prompt' mode and let the user deal with it.
red_set_options( [ 'plugin_update' => 'prompt' ] );
return;
}
$loop++;
}
}
// So it finally came to this... some plugins include their JS in all pages, whether they are needed or not. If there is an error
// then this can prevent Redirection running and it's a little sensitive about that. We use the nuclear option here to disable
// all other JS while viewing Redirection
public function flying_solo( $src, $handle ) {
$request = Redirection_Request::get_request_url();
if ( strpos( $request, 'page=redirection.php' ) !== false ) {
if ( substr( $src, 0, 4 ) === 'http' && $handle !== 'redirection' && strpos( $src, 'plugins' ) !== false ) {
if ( $this->ignore_this_plugin( $src ) ) {
return false;
}
}
}
return $src;
}
private function ignore_this_plugin( $src ) {
$ignore = array(
'mootools',
'wp-seo-',
'authenticate',
'wordpress-seo',
'yikes',
);
foreach ( $ignore as $text ) {
if ( strpos( $src, $text ) !== false ) {
return true;
}
}
return false;
}
public function flush_schedule( $options ) {
Red_Flusher::schedule();
return $options;
}
public function set_per_page( $status, $option, $value ) {
if ( $option === 'redirection_log_per_page' ) {
$value = max( 1, min( intval( $value, 10 ), RED_MAX_PER_PAGE ) );
return $value;
}
return $status;
}
public function plugin_settings( $links ) {
$status = new Red_Database_Status();
if ( $status->needs_updating() ) {
array_unshift( $links, '<a style="color: red" href="' . esc_url( $this->get_plugin_url() ) . '&amp;sub=support">' . __( 'Upgrade Database', 'redirection' ) . '</a>' );
}
array_unshift( $links, '<a href="' . esc_url( $this->get_plugin_url() ) . '&amp;sub=options">' . __( 'Settings', 'redirection' ) . '</a>' );
return $links;
}
public function plugin_row_meta( $plugin_meta, $plugin_file, $plugin_data, $status ) {
if ( $plugin_file === basename( dirname( REDIRECTION_FILE ) ) . '/' . basename( REDIRECTION_FILE ) ) {
$plugin_data['Description'] .= '<p>' . __( 'Please upgrade your database', 'redirection' ) . '</p>';
}
return $plugin_meta;
}
private function get_plugin_url() {
return admin_url( 'tools.php?page=' . basename( REDIRECTION_FILE ) );
}
private function get_first_available_page_url() {
$pages = Redirection_Capabilities::get_available_pages();
if ( count( $pages ) > 0 ) {
return $this->get_plugin_url() . ( $pages[0] === 'redirect' ? '' : '&sub=' . rawurlencode( $pages[0] ) );
}
return admin_url();
}
private function get_query( $name ) {
if ( isset( $_GET[ $name ] ) ) {
return sanitize_text_field( $_GET[ $name ] );
}
return null;
}
public function redirection_head() {
global $wp_version;
// Does user have access to this page?
if ( $this->get_current_page() === false ) {
// Redirect to root plugin page
wp_safe_redirect( $this->get_first_available_page_url() );
die();
}
if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['action'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'wp_rest' ) ) {
$action = sanitize_text_field( $_REQUEST['action'] );
if ( $action === 'fixit' ) {
$this->run_fixit();
} elseif ( $action === 'rest_api' ) {
$this->set_rest_api( intval( $_REQUEST['rest_api'], 10 ) );
}
}
$build = REDIRECTION_VERSION . '-' . REDIRECTION_BUILD;
$preload = $this->get_preload_data();
$options = red_get_options();
$versions = array(
'Plugin: ' . REDIRECTION_VERSION . ' ' . REDIRECTION_DB_VERSION,
'WordPress: ' . $wp_version . ' (' . ( is_multisite() ? 'multi' : 'single' ) . ')',
'PHP: ' . phpversion(),
'Browser: ' . Redirection_Request::get_user_agent(),
'JavaScript: ' . plugin_dir_url( REDIRECTION_FILE ) . 'redirection.js?ver=' . $build,
'REST API: ' . red_get_rest_api(),
);
$this->inject();
// Add contextual help to some pages
if ( in_array( $this->get_current_page(), [ 'redirect', 'log', '404s', 'groups' ], true ) ) {
add_screen_option( 'per_page', array(
/* translators: maximum number of log entries */
'label' => sprintf( __( 'Log entries (%d max)', 'redirection' ), RED_MAX_PER_PAGE ),
'default' => RED_DEFAULT_PER_PAGE,
'option' => 'redirection_log_per_page',
) );
}
if ( defined( 'REDIRECTION_DEV_MODE' ) && REDIRECTION_DEV_MODE ) {
wp_enqueue_script( 'redirection', 'http://localhost:3312/redirection.js', array(), $build, true );
} else {
wp_enqueue_script( 'redirection', plugin_dir_url( REDIRECTION_FILE ) . 'redirection.js', array(), $build, true );
}
wp_enqueue_style( 'redirection', plugin_dir_url( REDIRECTION_FILE ) . 'redirection.css', array(), $build );
$is_new = false;
$major_version = implode( '.', array_slice( explode( '.', REDIRECTION_VERSION ), 0, 2 ) );
if ( $this->get_query( 'page' ) === 'redirection.php' && strpos( REDIRECTION_VERSION, '-beta' ) === false ) {
$is_new = version_compare( $options['update_notice'], $major_version ) < 0;
}
$status = new Red_Database_Status();
$status->check_tables_exist();
// Fix some sites having a version set to +OK - not sure why
if ( isset( $options['database'] ) && $options['database'] === '+OK' ) {
red_set_options( [ 'database' => REDIRECTION_DB_VERSION ] );
$status->stop_update();
}
$translations = $this->get_i18n_data();
wp_localize_script( 'redirection', 'Redirectioni10n', array(
'api' => [
'WP_API_root' => esc_url_raw( red_get_rest_api() ),
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
'site_health' => admin_url( 'site-health.php' ),
'current' => $options['rest_api'],
'routes' => [
REDIRECTION_API_JSON => red_get_rest_api( REDIRECTION_API_JSON ),
REDIRECTION_API_JSON_INDEX => red_get_rest_api( REDIRECTION_API_JSON_INDEX ),
REDIRECTION_API_JSON_RELATIVE => red_get_rest_api( REDIRECTION_API_JSON_RELATIVE ),
],
],
'pluginBaseUrl' => plugins_url( '', REDIRECTION_FILE ),
'pluginRoot' => $this->get_plugin_url(),
'per_page' => $this->get_per_page(),
'locale' => implode( '-', array_slice( explode( '-', str_replace( '_', '-', get_locale() ) ), 0, 2 ) ),
'settings' => $options,
'preload' => $preload,
'versions' => implode( "\n", $versions ),
'version' => REDIRECTION_VERSION,
'database' => $status->get_json(),
'caps' => [
'pages' => Redirection_Capabilities::get_available_pages(),
'capabilities' => Redirection_Capabilities::get_all_capabilities(),
],
'update_notice' => $is_new ? $major_version : false,
) );
wp_set_script_translations( 'redirection', 'redirection', plugin_dir_path( __FILE__ ) . 'locale/json/' );
$this->add_help_tab();
}
// Some plugins misbehave, so this attempts to 'fix' them so Redirection can get on with it's work
private function run_hacks() {
add_filter( 'ip-geo-block-admin', array( $this, 'ip_geo_block' ) );
}
/*
* This works around the IP Geo Block plugin being very aggressive and breaking Redirection
*/
public function ip_geo_block( $validate ) {
$url = Redirection_Request::get_request_url();
$override = array(
'tools.php?page=redirection.php',
'action=red_proxy&rest_path=redirection',
);
foreach ( $override as $path ) {
if ( strpos( $url, $path ) !== false ) {
return array(
'result' => 'passed',
'auth' => false,
'asn' => false,
'code' => false,
'ip' => false,
);
}
}
return $validate;
}
private function run_fixit() {
if ( Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_SUPPORT_MANAGE ) ) {
require_once dirname( REDIRECTION_FILE ) . '/models/fixer.php';
$fixer = new Red_Fixer();
$result = $fixer->fix( $fixer->get_status() );
if ( is_wp_error( $result ) ) {
$this->fixit_failed = $result;
}
}
}
private function set_rest_api( $api ) {
if ( $api >= 0 && $api <= REDIRECTION_API_JSON_RELATIVE ) {
red_set_options( array( 'rest_api' => intval( $api, 10 ) ) );
}
}
private function get_preload_data() {
$status = new Red_Database_Status();
if ( $status->needs_installing() ) {
include_once __DIR__ . '/models/importer.php';
return [
'importers' => Red_Plugin_Importer::get_plugins(),
];
}
if ( $this->get_current_page() === 'support' ) {
require_once dirname( REDIRECTION_FILE ) . '/models/fixer.php';
$fixer = new Red_Fixer();
return array(
'pluginStatus' => $fixer->get_json(),
);
}
return [];
}
private function add_help_tab() {
/* translators: URL */
$content = sprintf( __( 'You can find full documentation about using Redirection on the <a href="%s" target="_blank">redirection.me</a> support site.', 'redirection' ), 'https://redirection.me/support/?utm_source=redirection&utm_medium=plugin&utm_campaign=context-help' );
$title = __( 'Redirection Support', 'redirection' );
$current_screen = get_current_screen();
$current_screen->add_help_tab( array(
'id' => 'redirection',
'title' => 'Redirection',
'content' => "<h2>$title</h2><p>$content</p>",
) );
}
private function get_per_page() {
$per_page = intval( get_user_meta( get_current_user_id(), 'redirection_log_per_page', true ), 10 );
return $per_page > 0 ? max( 5, min( $per_page, RED_MAX_PER_PAGE ) ) : RED_DEFAULT_PER_PAGE;
}
private function get_i18n_data() {
$locale = get_locale();
// WP 4.7
if ( function_exists( 'get_user_locale' ) ) {
$locale = get_user_locale();
}
$i18n_json = dirname( REDIRECTION_FILE ) . '/locale/json/redirection-' . $locale . '.json';
if ( is_file( $i18n_json ) && is_readable( $i18n_json ) ) {
// phpcs:ignore
$locale_data = @file_get_contents( $i18n_json );
if ( $locale_data ) {
return json_decode( $locale_data, true );
}
}
// Return empty if we have nothing to return so it doesn't fail when parsed in JS
return array();
}
public function admin_menu() {
$hook = add_management_page( 'Redirection', 'Redirection', Redirection_Capabilities::get_plugin_access(), basename( REDIRECTION_FILE ), [ $this, 'admin_screen' ] );
add_action( 'load-' . $hook, [ $this, 'redirection_head' ] );
}
private function check_minimum_wp() {
$wp_version = get_bloginfo( 'version' );
if ( version_compare( $wp_version, REDIRECTION_MIN_WP, '<' ) ) {
return false;
}
return true;
}
/**
* Update the cache key when updating or creating a redirect
*
* @return void
*/
public function clear_cache() {
$settings = red_get_options();
if ( $settings['cache_key'] > 0 ) {
red_set_options( [ 'cache_key' => time() ] );
}
}
public function set_default_group( $id, $redirect ) {
red_set_options( array( 'last_group_id' => $redirect->get_group_id() ) );
}
public function admin_screen() {
if ( count( Redirection_Capabilities::get_all_capabilities() ) === 0 ) {
die( 'You do not have sufficient permissions to access this page.' );
}
if ( $this->check_minimum_wp() === false ) {
return $this->show_minimum_wordpress();
}
if ( $this->fixit_failed ) {
$this->show_fixit_failed();
}
Red_Flusher::schedule();
$this->show_main();
}
private function show_fixit_failed() {
?>
<div class="notice notice-error">
<h1><?php echo esc_html( $this->fixit_failed->get_error_message() ); ?></h1>
<p><?php echo esc_html( $this->fixit_failed->get_error_data() ); ?></p>
</div>
<?php
}
private function show_minimum_wordpress() {
global $wp_version;
/* translators: 1: Expected WordPress version, 2: Actual WordPress version */
$wp_requirement = sprintf( __( 'Redirection requires WordPress v%1$1s, you are using v%2$2s - please update your WordPress', 'redirection' ), REDIRECTION_MIN_WP, $wp_version );
?>
<div class="react-error">
<h1><?php esc_html_e( 'Unable to load Redirection', 'redirection' ); ?></h1>
<p><?php echo esc_html( $wp_requirement ); ?></p>
</div>
<?php
}
private function show_load_fail() {
?>
<div class="react-error" style="display: none">
<h1><?php esc_html_e( 'Unable to load Redirection ☹️', 'redirection' ); ?> v<?php echo esc_html( REDIRECTION_VERSION ); ?></h1>
<p><?php esc_html_e( "This may be caused by another plugin - look at your browser's error console for more details.", 'redirection' ); ?></p>
<p><?php esc_html_e( 'If you are using a page caching plugin or service (CloudFlare, OVH, etc) then you can also try clearing that cache.', 'redirection' ); ?></p>
<p><?php _e( 'Also check if your browser is able to load <code>redirection.js</code>:', 'redirection' ); ?></p>
<p><code><?php echo esc_html( plugin_dir_url( REDIRECTION_FILE ) . 'redirection.js?ver=' . rawurlencode( REDIRECTION_VERSION ) . '-' . rawurlencode( REDIRECTION_BUILD ) ); ?></code></p>
<p><?php esc_html_e( 'Please note that Redirection requires the WordPress REST API to be enabled. If you have disabled this then you won\'t be able to use Redirection', 'redirection' ); ?></p>
<p><?php _e( 'Please see the <a href="https://redirection.me/support/problems/">list of common problems</a>.', 'redirection' ); ?></p>
<p><?php esc_html_e( 'If you think Redirection is at fault then create an issue.', 'redirection' ); ?></p>
<p class="versions"><?php _e( '<code>Redirectioni10n</code> is not defined. This usually means another plugin is blocking Redirection from loading. Please disable all plugins and try again.', 'redirection' ); ?></p>
<p>
<a class="button-primary" target="_blank" href="https://github.com/johngodley/redirection/issues/new?title=Problem%20starting%20Redirection%20<?php echo esc_attr( REDIRECTION_VERSION ); ?>">
<?php esc_html_e( 'Create Issue', 'redirection' ); ?>
</a>
</p>
</div>
<?php
}
private function show_main() {
?>
<div id="react-modal"></div>
<div id="react-ui">
<div class="react-loading">
<h1><?php esc_html_e( 'Loading, please wait...', 'redirection' ); ?></h1>
<span class="react-loading-spinner"></span>
</div>
<noscript><?php esc_html_e( 'Please enable JavaScript', 'redirection' ); ?></noscript>
<?php $this->show_load_fail(); ?>
</div>
<script>
var prevError = window.onerror;
var errors = [];
var timeout = 0;
var timer = setInterval( function() {
if ( isRedirectionLoaded() ) {
resetAll();
} else if ( errors.length > 0 || timeout++ === 5 ) {
showError();
}
}, 5000 );
function isRedirectionLoaded() {
return typeof redirection !== 'undefined';
}
function showError() {
var errorText = "";
if ( errors.length > 0 ) {
errorText = "```\n" + errors.join( ',' ) + "\n```\n\n";
}
resetAll();
if ( document.querySelector( '.react-loading' ) ) {
document.querySelector( '.react-loading' ).style.display = 'none';
document.querySelector( '.react-error' ).style.display = 'block';
if ( typeof Redirectioni10n !== 'undefined' && Redirectioni10n ) {
document.querySelector( '.versions' ).innerHTML = Redirectioni10n.versions.replace( /\n/g, '<br />' );
document.querySelector( '.react-error .button-primary' ).href += '&body=' + encodeURIComponent( errorText ) + encodeURIComponent( Redirectioni10n.versions );
}
} else {
document.querySelector( '#react-ui' ).innerHTML = '<p>Sorry something went very wrong.</p>';
}
}
function resetAll() {
clearInterval( timer );
window.onerror = prevError;
}
window.onerror = function( error, url, line ) {
console.error( error );
errors.push( error + ' ' + url + ' ' + line );
};
</script>
<?php
}
/**
* Get the current plugin page.
* Uses $_GET['sub'] to determine the current page unless a page is supplied.
*
* @param string $page Current page.
*
* @return string|boolean Current page, or false.
*/
private function get_current_page( $page = false ) {
if ( ! $page ) {
// phpcs:ignore
$page = 'redirect';
if ( $this->get_query( 'sub' ) ) {
$page = $this->get_query( 'sub' );
}
}
// Are we allowed to access this page?
if ( in_array( $page, Redirection_Capabilities::get_available_pages(), true ) ) {
// phpcs:ignore
return $page;
}
return false;
}
private function inject() {
// phpcs:ignore
$page = false;
if ( $this->get_query( 'page' ) ) {
$page = $this->get_query( 'page' );
}
if ( $page && $this->get_current_page() !== 'redirect' && $page === 'redirection.php' ) {
$this->try_export_logs();
$this->try_export_redirects();
$this->try_export_rss();
}
}
public function try_export_rss() {
$token = $this->get_query( 'token' );
$sub = $this->get_query( 'sub' );
if ( $token && $sub === 'rss' && Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_REDIRECT_MANAGE ) ) {
$options = red_get_options();
// phpcs:ignore
if ( $token === $options['token'] && ! empty( $options['token'] ) ) {
$module = $this->get_query( 'module' );
// phpcs:ignore
$items = Red_Item::get_all_for_module( intval( $module, 10 ) );
$exporter = Red_FileIO::create( 'rss' );
$exporter->force_download();
// phpcs:ignore
echo $exporter->get_data( $items, array() );
die();
}
}
}
private function try_export_logs() {
if ( Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE ) && isset( $_POST['export-csv'] ) && check_admin_referer( 'wp_rest' ) ) {
if ( $this->get_current_page() === 'log' ) {
Red_Redirect_Log::export_to_csv();
} elseif ( $this->get_current_page() === '404s' ) {
Red_404_Log::export_to_csv();
}
die();
}
}
private function try_export_redirects() {
// phpcs:ignore
$sub = $this->get_query( 'sub' );
if ( $sub !== 'io' ) {
return;
}
$export = $this->get_query( 'export' );
$exporter = $this->get_query( 'exporter' );
if ( Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE ) && $export && $exporter && check_admin_referer( 'wp_rest' ) ) {
$export = Red_FileIO::export( $export, $exporter );
if ( $export !== false ) {
$export['exporter']->force_download();
// This data is not displayed and will be downloaded to a file
echo str_replace( '&amp;', '&', wp_kses( $export['data'], 'strip' ) );
die();
}
}
}
}
register_activation_hook( REDIRECTION_FILE, array( 'Redirection_Admin', 'plugin_activated' ) );
add_action( 'init', array( 'Redirection_Admin', 'init' ) );
// This is causing a lot of problems with the REST API - disable qTranslate
add_filter( 'qtranslate_language_detect_redirect', function( $lang, $url ) {
$url = Redirection_Request::get_request_url();
if ( strpos( $url, '/wp-json/' ) !== false || strpos( $url, '?rest_route' ) !== false ) {
return false;
}
return $lang;
}, 10, 2 );

View File

@@ -0,0 +1,167 @@
<?php
/**
* Redirection capabilities
*
* Pre 4.6
* =======
* Hook `redirection_role` and return a capability. This gives access to the entire plugin
*
* Post 4.6
* ========
* Hook `redirection_role` and return a capability for access to the plugin menu. For example `edit_pages` will allow an editor
* Hook `redirection_capability_check` and return a different capability for each check that needs specific permissions.
*
* For example, if you want to give editors access to create redirects, but nothing else:
*
* ```php
* add_filter( 'redirection_capability_check', function( $capability, $permission_name ) {
* if ( $permission_name === 'redirection_cap_redirect_manage' || $permission_name === 'redirection_cap_redirect_add' ) {
* return $capability;
* }
*
* return 'manage_options';
* } );
* ```
*
* Always default to restrictive and then grant permissions. Don't default to permissive and remove permissions. This way if a new
* capability is added your users won't automatically be granted access.
*
* Capabilities can be filtered with:
* - `redirection_capability_check( $capability, $permission_name )` - override `$capability` dependant on `$permission_name`
* - `redirection_capability_pages( $pages )` - filters the list of available pages
* - `redirection_role( $cap )` - return the role/capability used for overall access to the plugin
*
* Note some capabilities may give access to data from others. For example, when viewing a page of redirects via `redirection_cap_redirect_manage`
* the client will need to access group data.
*/
class Redirection_Capabilities {
const FILTER_ALL = 'redirection_capability_all';
const FILTER_PAGES = 'redirection_capability_pages';
const FILTER_CAPABILITY = 'redirection_capability_check';
// The default WordPress capability used for all checks
const CAP_DEFAULT = 'manage_options';
// The main capability used to provide access to the plugin
const CAP_PLUGIN = 'redirection_role';
// These capabilities are combined with `redirection_cap_` to form `redirection_cap_redirect_add` etc
const CAP_REDIRECT_MANAGE = 'redirection_cap_redirect_manage';
const CAP_REDIRECT_ADD = 'redirection_cap_redirect_add';
const CAP_REDIRECT_DELETE = 'redirection_cap_redirect_delete';
const CAP_GROUP_MANAGE = 'redirection_cap_group_manage';
const CAP_GROUP_ADD = 'redirection_cap_group_add';
const CAP_GROUP_DELETE = 'redirection_cap_group_delete';
const CAP_404_MANAGE = 'redirection_cap_404_manage';
const CAP_404_DELETE = 'redirection_cap_404_delete';
const CAP_LOG_MANAGE = 'redirection_cap_log_manage';
const CAP_LOG_DELETE = 'redirection_cap_log_delete';
const CAP_IO_MANAGE = 'redirection_cap_io_manage';
const CAP_OPTION_MANAGE = 'redirection_cap_option_manage';
const CAP_SUPPORT_MANAGE = 'redirection_cap_support_manage';
const CAP_SITE_MANAGE = 'redirection_cap_site_manage';
/**
* Determine if the current user has access to a named capability.
*
* @param string $cap_name The capability to check for. See Redirection_Capabilities for constants.
* @return boolean
*/
public static function has_access( $cap_name ) {
// Get the capability using the default plugin access as the base. Old sites overriding `redirection_role` will get access to everything
$cap_to_check = apply_filters( self::FILTER_CAPABILITY, self::get_plugin_access(), $cap_name );
// Check the capability
return current_user_can( $cap_to_check );
}
/**
* Return the role/capability used for displaying the plugin menu. This is also the base capability for all other checks.
*
* @return string Role/capability
*/
public static function get_plugin_access() {
return apply_filters( self::CAP_PLUGIN, self::CAP_DEFAULT );
}
/**
* Return all the pages the user has access to.
*
* @return array Array of pages
*/
public static function get_available_pages() {
$pages = [
self::CAP_REDIRECT_MANAGE => 'redirect',
self::CAP_GROUP_MANAGE => 'groups',
self::CAP_404_MANAGE => '404s',
self::CAP_LOG_MANAGE => 'log',
self::CAP_IO_MANAGE => 'io',
self::CAP_OPTION_MANAGE => 'options',
self::CAP_SUPPORT_MANAGE => 'support',
self::CAP_SITE_MANAGE => 'site',
];
$available = [];
foreach ( $pages as $key => $page ) {
if ( self::has_access( $key ) ) {
$available[] = $page;
}
}
return array_values( apply_filters( self::FILTER_PAGES, $available ) );
}
/**
* Return all the capabilities the current user has
*
* @return array Array of capabilities
*/
public static function get_all_capabilities() {
$caps = self::get_every_capability();
$caps = array_filter( $caps, function( $cap ) {
return self::has_access( $cap );
} );
return array_values( apply_filters( self::FILTER_ALL, $caps ) );
}
/**
* Unfiltered list of all the supported capabilities, without influence from the current user
*
* @return array Array of capabilities
*/
public static function get_every_capability() {
return [
self::CAP_REDIRECT_MANAGE,
self::CAP_REDIRECT_ADD,
self::CAP_REDIRECT_DELETE,
self::CAP_GROUP_MANAGE,
self::CAP_GROUP_ADD,
self::CAP_GROUP_DELETE,
self::CAP_404_MANAGE,
self::CAP_404_DELETE,
self::CAP_LOG_MANAGE,
self::CAP_LOG_DELETE,
self::CAP_IO_MANAGE,
self::CAP_OPTION_MANAGE,
self::CAP_SUPPORT_MANAGE,
self::CAP_SITE_MANAGE,
];
}
}

View File

@@ -0,0 +1,303 @@
<?php
/**
* Implements example command.
*/
class Redirection_Cli extends WP_CLI_Command {
private function get_group( $group_id ) {
if ( $group_id === 0 ) {
$groups = Red_Group::get_filtered( array() );
if ( count( $groups['items'] ) > 0 ) {
return $groups['items'][0]['id'];
}
} else {
$groups = Red_Group::get( $group_id );
if ( $groups ) {
return $group_id;
}
}
return false;
}
/**
* Import from another plugin to Redirection.
*
* Supports:
* - wp-simple-redirect
* - seo-redirection
* - safe-redirect-manager
* - wordpress-old-slugs
* - rank-math
* - quick-redirects
*
* ## OPTIONS
*
* <name>
* : The plugin name to import from (see above)
*
* [--group=<groupid>]
* : The group ID to import into. Defaults to the first available group.
*
* ## EXAMPLES
*
* wp redirection plugin quick-redirects
*/
public function plugin( $args, $extra ) {
include_once __DIR__ . '/models/importer.php';
$name = $args[0];
$group = $this->get_group( isset( $extra['group'] ) ? intval( $extra['group'], 10 ) : 0 );
$importer = Red_Plugin_Importer::get_importer( $name );
if ( $importer ) {
$count = $importer->import_plugin( $group );
WP_CLI::success( sprintf( 'Imported %d redirects from plugin %s', $count, $name ) );
return;
}
WP_CLI::error( 'Invalid plugin name' );
}
/**
* Get or set a Redirection setting
*
* ## OPTIONS
*
* <name>
* : The setting name to get or set
*
* [--set=<value>]
* : The value to set (JSON)
*
* ## EXAMPLES
*
* wp redirection setting name <value>
*/
public function setting( $args, $extra ) {
$name = $args[0];
$set = isset( $extra['set'] ) ? $extra['set'] : false;
$options = red_get_options();
if ( ! isset( $options[ $name ] ) ) {
WP_CLI::error( 'Unsupported setting: ' . $name );
return;
}
$value = $options[ $name ];
if ( $set ) {
$decoded = json_decode( $set, true );
if ( ! $decoded ) {
$decoded = $set;
}
$options = [];
$options[ $name ] = $decoded;
$options = red_set_options( $options );
$value = $options[ $name ];
}
WP_CLI::success( is_array( $value ) ? wp_json_encode( $value ) : $value );
}
/**
* Import redirections from a JSON, CSV, or .htaccess file
*
* ## OPTIONS
*
* <file>
* : The name of the file to import.
*
* [--group=<groupid>]
* : The group ID to import into. Defaults to the first available group. JSON
* contains it's own group
*
* [--format=<importformat>]
* : The import format - csv, apache, or json. Defaults to json
*
* ## EXAMPLES
*
* wp redirection import .htaccess --format=apache
*/
public function import( $args, $extra ) {
$format = isset( $extra['format'] ) ? $extra['format'] : 'json';
$group = $this->get_group( isset( $extra['group'] ) ? intval( $extra['group'], 10 ) : 0 );
if ( ! $group ) {
WP_CLI::error( 'Invalid group' );
return;
}
$importer = Red_FileIO::create( $format );
if ( ! $importer ) {
WP_CLI::error( 'Invalid import format - csv, json, or apache supported' );
return;
}
if ( $format === 'csv' ) {
$file = fopen( $args[0], 'r' );
if ( $file ) {
$count = $importer->load( $group, $args[0], '' );
WP_CLI::success( 'Imported ' . $count . ' as ' . $format );
} else {
WP_CLI::error( 'Invalid import file' );
}
} else {
$data = @file_get_contents( $args[0] );
if ( $data ) {
$count = $importer->load( $group, $args[0], $data );
WP_CLI::success( 'Imported ' . $count . ' redirects as ' . $format );
} else {
WP_CLI::error( 'Invalid import file' );
}
}
}
/**
* Export redirections to a CSV, JSON, .htaccess, or rewrite.rules file
*
* ## OPTIONS
*
* <module>
* : The module to export - wordpress, apache, nginx, or all
*
* <filename>
* : The file to export to, or - for stdout
*
* [--format=<exportformat>]
* : The export format. One of json, csv, apache, or nginx. Defaults to json
*
* ## EXAMPLES
*
* wp redirection export wordpress --format=apache
*/
public function export( $args, $extra ) {
$format = isset( $extra['format'] ) ? $extra['format'] : 'json';
$exporter = Red_FileIO::create( $format );
if ( ! $exporter ) {
WP_CLI::error( 'Invalid export format - json, csv, apache, or nginx supported' );
return;
}
$file = fopen( $args[1] === '-' ? 'php://stdout' : $args[1], 'w' );
if ( $file ) {
$export = Red_FileIO::export( $args[0], $format );
if ( $export === false ) {
// phpcs:ignore
WP_CLI::error( 'Invalid module - must be wordpress, apache, nginx, or all' );
return;
}
fwrite( $file, $export['data'] );
fclose( $file );
WP_CLI::success( 'Exported ' . $export['total'] . ' to ' . $format );
} else {
WP_CLI::error( 'Invalid output file' );
}
}
/**
* Perform Redirection database actions
*
* ## OPTIONS
*
* <action>
* : The database action to perform: install, remove, upgrade
*
* [--skip-errors]
* : Skip errors and keep on upgrading
*
* ## EXAMPLES
*
* wp redirection database install
*/
public function database( $args, $extra ) {
$action = false;
$skip = isset( $extra['skip-errors'] ) ? true : false;
if ( count( $args ) === 0 || ! in_array( $args[0], array( 'install', 'remove', 'upgrade' ), true ) ) {
WP_CLI::error( 'Invalid database action - please use install, remove, or upgrade' );
return;
}
if ( $args[0] === 'install' ) {
Red_Database::apply_to_sites( function() {
$latest = Red_Database::get_latest_database();
$latest->install();
WP_CLI::success( 'Site ' . get_current_blog_id() . ' database is installed' );
} );
WP_CLI::success( 'Database install finished' );
} elseif ( $args[0] === 'upgrade' ) {
global $wpdb;
$wpdb->show_errors( false );
Red_Database::apply_to_sites( function() {
$database = new Red_Database();
$status = new Red_Database_Status();
if ( ! $status->needs_updating() ) {
WP_CLI::success( 'Site ' . get_current_blog_id() . ' database is already the latest version' );
return;
}
$loop = 0;
while ( $loop < 50 ) {
$result = $database->apply_upgrade( $status );
$info = $status->get_json();
if ( ! $info['inProgress'] ) {
break;
}
if ( $info['result'] === 'error' ) {
if ( $skip === false ) {
WP_CLI::error( 'Site ' . get_current_blog_id() . ' database failed to upgrade: ' . $info['reason'] . ' - ' . $info['debug'][0] );
return;
}
WP_CLI::warning( 'Site ' . get_current_blog_id() . ' database failed to upgrade: ' . $info['reason'] . ' - ' . $info['debug'][0] );
$status->set_next_stage();
}
$loop++;
}
WP_CLI::success( 'Site ' . get_current_blog_id() . ' database upgraded' );
} );
WP_CLI::success( 'Database upgrade finished' );
} elseif ( $args[0] === 'remove' ) {
Red_Database::apply_to_sites( function() {
$latest = Red_Database::get_latest_database();
$latest->remove();
} );
WP_CLI::success( 'Database removed' );
}
}
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
// Register "redirection" as top-level command, and all public methods as sub-commands
WP_CLI::add_command( 'redirection', 'Redirection_Cli' );
add_action( Red_Flusher::DELETE_HOOK, function() {
$flusher = new Red_Flusher();
$flusher->flush();
} );
}

View File

@@ -0,0 +1,141 @@
<?php
require_once __DIR__ . '/modules/wordpress.php';
require_once __DIR__ . '/models/canonical.php';
require_once __DIR__ . '/database/database-status.php';
/**
* This powers all of the front-end redirecting
*/
class Redirection {
/**
* Instance variable
*
* @var Redirection|null
*/
private static $instance = null;
/**
* WordPress module
*
* @var WordPress_Module|null
*/
private $module = null;
/**
* Singleton
*
* @return Redirection
*/
public static function init() {
if ( is_null( self::$instance ) ) {
self::$instance = new Redirection();
}
return self::$instance;
}
/**
* Constructor
*/
public function __construct() {
if ( ! $this->can_start() ) {
return;
}
$this->module = Red_Module::get( WordPress_Module::MODULE_ID );
if ( $this->module ) {
$this->module->start();
}
add_action( Red_Flusher::DELETE_HOOK, array( $this, 'clean_redirection_logs' ) );
add_filter( 'redirection_url_target', [ $this, 'transform_url' ] );
$options = red_get_options();
if ( $options['ip_logging'] === 0 ) {
add_filter( 'redirection_request_ip', array( $this, 'no_ip_logging' ) );
} elseif ( $options['ip_logging'] === 2 ) {
add_filter( 'redirection_request_ip', array( $this, 'mask_ip' ) );
}
}
public function transform_url( $url ) {
$transformer = new Red_Url_Transform();
return $transformer->transform( $url );
}
/**
* Check if Redirection can run. We require the database to be installed.
*
* @return boolean
*/
public function can_start() {
$status = new Red_Database_Status();
if ( $status->needs_installing() ) {
return false;
}
return true;
}
/**
* Override the IP with an empty value
*
* @param string $ip IP.
* @return string
*/
public function no_ip_logging( $ip ) {
return '';
}
/**
* Override the IP with a masked IP
*
* @param string $ip IP.
* @return string
*/
public function mask_ip( $ip ) {
$ip = trim( $ip );
if ( strpos( $ip, ':' ) !== false ) {
// phpcs:ignore
$ip = @inet_pton( trim( $ip ) );
// phpcs:ignore
return @inet_ntop( $ip & pack( 'a16', 'ffff:ffff:ffff:ffff::ff00::0000::0000::0000' ) );
}
$parts = [];
if ( strlen( $ip ) > 0 ) {
$parts = explode( '.', $ip );
}
if ( count( $parts ) > 0 ) {
$parts[ count( $parts ) - 1 ] = 0;
}
return implode( '.', $parts );
}
/**
* Hook to flush the logs
*
* @return void
*/
public function clean_redirection_logs() {
$flusher = new Red_Flusher();
$flusher->flush();
}
/**
* Used for unit tests
*
* @return WordPress_Module|null
*/
public function get_module() {
return $this->module;
}
}
add_action( 'plugins_loaded', array( 'Redirection', 'init' ) );

View File

@@ -0,0 +1,426 @@
<?php
define( 'REDIRECTION_OPTION', 'redirection_options' );
define( 'REDIRECTION_API_JSON', 0 );
define( 'REDIRECTION_API_JSON_INDEX', 1 );
define( 'REDIRECTION_API_JSON_RELATIVE', 3 );
function red_get_plugin_data( $plugin ) {
if ( ! function_exists( 'get_plugin_data' ) ) {
/** @psalm-suppress MissingFile */
include_once ABSPATH . '/wp-admin/includes/plugin.php';
}
return get_plugin_data( $plugin );
}
function red_get_post_types( $full = true ) {
$types = get_post_types( [ 'public' => true ], 'objects' );
$types[] = (object) array(
'name' => 'trash',
'label' => __( 'Trash', 'default' ),
);
$post_types = [];
foreach ( $types as $type ) {
if ( $type->name === 'attachment' ) {
continue;
}
if ( $full && strlen( $type->label ) > 0 ) {
$post_types[ $type->name ] = $type->label;
} else {
$post_types[] = $type->name;
}
}
return apply_filters( 'redirection_post_types', $post_types );
}
/**
* Get default options. Contains all valid options
*
* @return array
*/
function red_get_default_options() {
$flags = new Red_Source_Flags();
$defaults = [
'support' => false,
'token' => md5( uniqid() ),
'monitor_post' => 0, // Dont monitor posts by default
'monitor_types' => [],
'associated_redirect' => '',
'auto_target' => '',
'expire_redirect' => 7, // Expire in 7 days
'expire_404' => 7, // Expire in 7 days
'log_external' => false,
'log_header' => false,
'track_hits' => true,
'modules' => [],
'redirect_cache' => 1, // 1 hour
'ip_logging' => 0, // No IP logging
'ip_headers' => [],
'ip_proxy' => [],
'last_group_id' => 0,
'rest_api' => REDIRECTION_API_JSON,
'https' => false,
'headers' => [],
'database' => '',
'relocate' => '',
'preferred_domain' => '',
'aliases' => [],
'permalinks' => [],
'cache_key' => 0,
'plugin_update' => 'prompt',
'update_notice' => 0,
];
$defaults = array_merge( $defaults, $flags->get_json() );
return apply_filters( 'red_default_options', $defaults );
}
/**
* Set options
*
* @param array $settings Partial settings.
* @return array
*/
function red_set_options( array $settings = [] ) {
$options = red_get_options();
$monitor_types = [];
if ( isset( $settings['database'] ) ) {
$options['database'] = sanitize_text_field( $settings['database'] );
}
if ( array_key_exists( 'database_stage', $settings ) ) {
if ( $settings['database_stage'] === false ) {
unset( $options['database_stage'] );
} else {
$options['database_stage'] = $settings['database_stage'];
}
}
if ( isset( $settings['ip_proxy'] ) && is_array( $settings['ip_proxy'] ) ) {
$options['ip_proxy'] = array_map( function( $ip ) {
$ip = new Redirection_IP( $ip );
return $ip->get();
}, $settings['ip_proxy'] );
$options['ip_proxy'] = array_values( array_filter( $options['ip_proxy'] ) );
}
if ( isset( $settings['ip_headers'] ) && is_array( $settings['ip_headers'] ) ) {
$available = Redirection_Request::get_ip_headers();
$options['ip_headers'] = array_filter( $settings['ip_headers'], function( $header ) use ( $available ) {
return in_array( $header, $available, true );
} );
$options['ip_headers'] = array_values( $options['ip_headers'] );
}
if ( isset( $settings['rest_api'] ) && in_array( intval( $settings['rest_api'], 10 ), array( 0, 1, 2, 3, 4 ), true ) ) {
$options['rest_api'] = intval( $settings['rest_api'], 10 );
}
if ( isset( $settings['monitor_types'] ) && is_array( $settings['monitor_types'] ) ) {
$allowed = red_get_post_types( false );
foreach ( $settings['monitor_types'] as $type ) {
if ( in_array( $type, $allowed, true ) ) {
$monitor_types[] = $type;
}
}
$options['monitor_types'] = $monitor_types;
}
if ( isset( $settings['associated_redirect'] ) && is_string( $settings['associated_redirect'] ) ) {
$options['associated_redirect'] = '';
if ( strlen( $settings['associated_redirect'] ) > 0 ) {
$sanitizer = new Red_Item_Sanitize();
$options['associated_redirect'] = trim( $sanitizer->sanitize_url( $settings['associated_redirect'] ) );
}
}
if ( isset( $settings['monitor_types'] ) && count( $monitor_types ) === 0 ) {
$options['monitor_post'] = 0;
$options['associated_redirect'] = '';
} elseif ( isset( $settings['monitor_post'] ) ) {
$options['monitor_post'] = max( 0, intval( $settings['monitor_post'], 10 ) );
if ( ! Red_Group::get( $options['monitor_post'] ) && $options['monitor_post'] !== 0 ) {
$groups = Red_Group::get_all();
if ( count( $groups ) > 0 ) {
$options['monitor_post'] = $groups[0]['id'];
}
}
}
if ( isset( $settings['auto_target'] ) && is_string( $settings['auto_target'] ) ) {
$options['auto_target'] = sanitize_text_field( $settings['auto_target'] );
}
if ( isset( $settings['last_group_id'] ) ) {
$options['last_group_id'] = max( 0, intval( $settings['last_group_id'], 10 ) );
if ( ! Red_Group::get( $options['last_group_id'] ) ) {
$groups = Red_Group::get_all();
$options['last_group_id'] = $groups[0]['id'];
}
}
if ( isset( $settings['token'] ) && is_string( $settings['token'] ) ) {
$options['token'] = sanitize_text_field( $settings['token'] );
}
if ( isset( $settings['token'] ) && trim( $options['token'] ) === '' ) {
$options['token'] = md5( uniqid() );
}
// Boolean settings
foreach ( [ 'support', 'https', 'log_external', 'log_header', 'track_hits' ] as $name ) {
if ( isset( $settings[ $name ] ) ) {
$options[ $name ] = $settings[ $name ] ? true : false;
}
}
if ( isset( $settings['expire_redirect'] ) ) {
$options['expire_redirect'] = max( -1, min( intval( $settings['expire_redirect'], 10 ), 60 ) );
}
if ( isset( $settings['expire_404'] ) ) {
$options['expire_404'] = max( -1, min( intval( $settings['expire_404'], 10 ), 60 ) );
}
if ( isset( $settings['ip_logging'] ) ) {
$options['ip_logging'] = max( 0, min( 2, intval( $settings['ip_logging'], 10 ) ) );
}
if ( isset( $settings['redirect_cache'] ) ) {
$options['redirect_cache'] = intval( $settings['redirect_cache'], 10 );
if ( ! in_array( $options['redirect_cache'], array( -1, 0, 1, 24, 24 * 7 ), true ) ) {
$options['redirect_cache'] = 1;
}
}
if ( isset( $settings['location'] ) && ( ! isset( $options['location'] ) || $options['location'] !== $settings['location'] ) ) {
$module = Red_Module::get( 2 );
if ( $module ) {
$options['modules'][2] = $module->update( $settings );
}
}
if ( ! empty( $options['monitor_post'] ) && count( $options['monitor_types'] ) === 0 ) {
// If we have a monitor_post set, but no types, then blank everything
$options['monitor_post'] = 0;
$options['associated_redirect'] = '';
}
if ( isset( $settings['plugin_update'] ) && in_array( $settings['plugin_update'], [ 'prompt', 'admin' ], true ) ) {
$options['plugin_update'] = sanitize_text_field( $settings['plugin_update'] );
}
$flags = new Red_Source_Flags();
$flags_present = [];
foreach ( array_keys( $flags->get_json() ) as $flag ) {
if ( isset( $settings[ $flag ] ) ) {
$flags_present[ $flag ] = $settings[ $flag ];
}
}
if ( count( $flags_present ) > 0 ) {
$flags->set_flags( $flags_present );
$options = array_merge( $options, $flags->get_json() );
}
if ( isset( $settings['headers'] ) ) {
$headers = new Red_Http_Headers( $settings['headers'] );
$options['headers'] = $headers->get_json();
}
if ( isset( $settings['aliases'] ) && is_array( $settings['aliases'] ) ) {
$options['aliases'] = array_map( 'sanitize_text_field', $settings['aliases'] );
$options['aliases'] = array_values( array_filter( array_map( 'red_parse_domain_only', $settings['aliases'] ) ) );
$options['aliases'] = array_slice( $options['aliases'], 0, 20 ); // Max 20
}
if ( isset( $settings['permalinks'] ) && is_array( $settings['permalinks'] ) ) {
$options['permalinks'] = array_map(
function ( $permalink ) {
return sanitize_option( 'permalink_structure', $permalink );
},
$settings['permalinks']
);
$options['permalinks'] = array_values( array_filter( array_map( 'trim', $options['permalinks'] ) ) );
$options['permalinks'] = array_slice( $options['permalinks'], 0, 10 ); // Max 10
}
if ( isset( $settings['preferred_domain'] ) && in_array( $settings['preferred_domain'], [ '', 'www', 'nowww' ], true ) ) {
$options['preferred_domain'] = sanitize_text_field( $settings['preferred_domain'] );
}
if ( isset( $settings['relocate'] ) && is_string( $settings['relocate'] ) ) {
$options['relocate'] = red_parse_domain_path( sanitize_text_field( $settings['relocate'] ) );
if ( strlen( $options['relocate'] ) > 0 ) {
$options['preferred_domain'] = '';
$options['aliases'] = [];
$options['https'] = false;
}
}
if ( isset( $settings['cache_key'] ) ) {
$key = intval( $settings['cache_key'], 10 );
if ( $settings['cache_key'] === true ) {
$key = time();
} elseif ( $settings['cache_key'] === false ) {
$key = 0;
}
$options['cache_key'] = $key;
}
if ( isset( $settings['update_notice'] ) ) {
$major_version = explode( '-', REDIRECTION_VERSION )[0]; // Remove any beta suffix
$major_version = implode( '.', array_slice( explode( '.', $major_version ), 0, 2 ) );
$options['update_notice'] = $major_version;
}
update_option( REDIRECTION_OPTION, apply_filters( 'redirection_save_options', $options ) );
return $options;
}
function red_parse_url( $url ) {
$domain = filter_var( $url, FILTER_SANITIZE_URL );
if ( substr( $domain, 0, 5 ) !== 'http:' && substr( $domain, 0, 6 ) !== 'https:' ) {
$domain = ( is_ssl() ? 'https://' : 'http://' ) . $domain;
}
return wp_parse_url( $domain );
}
function red_parse_domain_only( $domain ) {
$parsed = red_parse_url( $domain );
if ( $parsed && isset( $parsed['host'] ) ) {
return $parsed['host'];
}
return '';
}
function red_parse_domain_path( $domain ) {
$parsed = red_parse_url( $domain );
if ( $parsed && isset( $parsed['host'] ) ) {
return $parsed['scheme'] . '://' . $parsed['host'] . ( isset( $parsed['path'] ) ? $parsed['path'] : '' );
}
return '';
}
/**
* Have redirects been disabled?
*
* @return boolean
*/
function red_is_disabled() {
return ( defined( 'REDIRECTION_DISABLE' ) && REDIRECTION_DISABLE ) || file_exists( __DIR__ . '/redirection-disable.txt' );
}
/**
* Get Redirection options
*
* @return array
*/
function red_get_options() {
$options = get_option( REDIRECTION_OPTION );
$fresh_install = false;
if ( $options === false ) {
$fresh_install = true;
}
if ( ! is_array( $options ) ) {
$options = [];
}
if ( red_is_disabled() ) {
$options['https'] = false;
}
$defaults = red_get_default_options();
foreach ( $defaults as $key => $value ) {
if ( ! isset( $options[ $key ] ) ) {
$options[ $key ] = $value;
}
}
if ( $fresh_install ) {
// Default flags for new installs - ignore case and trailing slashes
$options['flag_case'] = true;
$options['flag_trailing'] = true;
}
// Back-compat. If monitor_post is set without types then it's from an older Redirection
if ( $options['monitor_post'] > 0 && count( $options['monitor_types'] ) === 0 ) {
$options['monitor_types'] = [ 'post' ];
}
// Remove old options not in red_get_default_options()
foreach ( array_keys( $options ) as $key ) {
if ( ! isset( $defaults[ $key ] ) && $key !== 'database_stage' ) {
unset( $options[ $key ] );
}
}
// Back-compat fix
if ( $options['rest_api'] === false || ! in_array( $options['rest_api'], [ REDIRECTION_API_JSON, REDIRECTION_API_JSON_INDEX, REDIRECTION_API_JSON_RELATIVE ], true ) ) {
$options['rest_api'] = REDIRECTION_API_JSON;
}
if ( isset( $options['modules'] ) && isset( $options['modules']['2'] ) && isset( $options['modules']['2']['location'] ) ) {
$options['location'] = $options['modules']['2']['location'];
}
return $options;
}
/**
* Get the current REST API
*
* @param boolean $type Override with a specific API type.
* @return string
*/
function red_get_rest_api( $type = false ) {
if ( $type === false ) {
$options = red_get_options();
$type = $options['rest_api'];
}
$url = get_rest_url(); // REDIRECTION_API_JSON
if ( $type === REDIRECTION_API_JSON_INDEX ) {
$url = home_url( '/?rest_route=/' );
} elseif ( $type === REDIRECTION_API_JSON_RELATIVE ) {
/** @psalm-suppress TooManyArguments, InvalidCast */
$relative = (string) wp_parse_url( $url, PHP_URL_PATH );
if ( $relative ) {
$url = $relative;
}
if ( $url === '/index.php' ) {
// No permalinks. Default to normal REST API
$url = home_url( '/?rest_route=/' );
}
}
return $url;
}

View File

@@ -0,0 +1,763 @@
<?php
/* THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. */
$redirection_strings = array(
_n( "Are you sure you want to delete this item?", "Are you sure you want to delete the %d selected items?", 1, "redirection" ), // client/lib/store/index.js:11
__( "Are you sure want to delete all %d matching items?", "redirection" ), // client/lib/store/index.js:18
__( "Database problem", "redirection" ), // client/component/database/api-error.js:11
__( "Try again", "redirection" ), // client/component/database/api-error.js:15
__( "Database problem", "redirection" ), // client/component/database/database-error.js:41
__( "Try again", "redirection" ), // client/component/database/database-error.js:45
__( "Skip this stage", "redirection" ), // client/component/database/database-error.js:50
__( "Stop upgrade", "redirection" ), // client/component/database/database-error.js:56
__( "If you want to {{support}}ask for support{{/support}} please include these details:", "redirection" ), // client/component/database/database-error.js:62
__( "Upgrading Redirection", "redirection" ), // client/component/database/index.js:27
__( "Setting up Redirection", "redirection" ), // client/component/database/index.js:30
__( "Leaving before the process has completed may cause problems.", "redirection" ), // client/component/database/index.js:71
__( "Please remain on this page until complete.", "redirection" ), // client/component/database/index.js:77
__( "Progress: %(complete)d\$", "redirection" ), // client/component/database/index.js:81
__( "Custom Display", "redirection" ), // client/component/display-options/index.js:30
__( "Pre-defined", "redirection" ), // client/component/display-options/index.js:64
__( "Custom", "redirection" ), // client/component/display-options/index.js:69
__( "Geo IP Error", "redirection" ), // client/component/geo-map/index.js:29
__( "Something went wrong obtaining this information", "redirection" ), // client/component/geo-map/index.js:30
__( "Geo IP", "redirection" ), // client/component/geo-map/index.js:43
__( "This is an IP from a private network. This means it is located inside a home or business network and no more information can be displayed.", "redirection" ), // client/component/geo-map/index.js:46
__( "Geo IP", "redirection" ), // client/component/geo-map/index.js:57
__( "No details are known for this address.", "redirection" ), // client/component/geo-map/index.js:60
__( "Geo IP", "redirection" ), // client/component/geo-map/index.js:77
__( "City", "redirection" ), // client/component/geo-map/index.js:82
__( "Area", "redirection" ), // client/component/geo-map/index.js:86
__( "Timezone", "redirection" ), // client/component/geo-map/index.js:90
__( "Geo Location", "redirection" ), // client/component/geo-map/index.js:94
__( "An unknown errorm", "redirection" ), // client/component/http-check/details.js:37
__( "Something is wrong with the server. This is not a problem with Redirection and you will need to resolve the error yourself.", "redirection" ), // client/component/http-check/details.js:41
__( "An error page was returned. This is unlikely to be a problem with Redirection. {{support}}What does this mean?{{/support}}.", "redirection" ), // client/component/http-check/details.js:47
__( "Redirected by Redirection.", "redirection" ), // client/component/http-check/details.js:61
__( "Matches your redirect", "redirection" ), // client/component/http-check/details.js:63
__( "Redirected by Redirection.", "redirection" ), // client/component/http-check/details.js:74
__( "Redirected by %1s. {{support}}What does this mean?{{/support}}.", "redirection" ), // client/component/http-check/details.js:77
__( "Redirected by an unknown agent. {{support}}What does this mean?{{/support}}.", "redirection" ), // client/component/http-check/details.js:87
__( "Page was loaded.", "redirection" ), // client/component/http-check/details.js:95
__( "Unable to check that URL. It may not be valid or accessible.", "redirection" ), // client/component/http-check/details.js:188
__( "If this is not expected then this {{support}}support page{{/support}} may help.", "redirection" ), // client/component/http-check/details.js:206
__( "If your browser is behaving differently then you should clear your browser cache.", "redirection" ), // client/component/http-check/details.js:219
__( "View full redirect.li results.", "redirection" ), // client/component/http-check/details.js:223
__( "Error", "redirection" ), // client/component/http-check/response.js:21
__( "Something went wrong obtaining this information. It may work in the future.", "redirection" ), // client/component/http-check/response.js:22
__( "Check redirect for: {{code}}%s{{/code}}", "redirection" ), // client/component/http-check/response.js:50
__( "Filters", "redirection" ), // client/component/log-page/log-filters.js:87
__( "Powered by {{link}}redirect.li{{/link}}", "redirection" ), // client/component/powered-by/index.js:16
__( "Saving...", "redirection" ), // client/component/progress/index.js:23
__( "Saving...", "redirection" ), // client/component/progress/index.js:26
__( "with HTTP code", "redirection" ), // client/component/redirect-edit/action-code.js:42
__( "URL only", "redirection" ), // client/component/redirect-edit/constants.js:30
__( "URL and login status", "redirection" ), // client/component/redirect-edit/constants.js:34
__( "URL and role/capability", "redirection" ), // client/component/redirect-edit/constants.js:38
__( "URL and referrer", "redirection" ), // client/component/redirect-edit/constants.js:42
__( "URL and user agent", "redirection" ), // client/component/redirect-edit/constants.js:46
__( "URL and cookie", "redirection" ), // client/component/redirect-edit/constants.js:50
__( "URL and IP", "redirection" ), // client/component/redirect-edit/constants.js:54
__( "URL and server", "redirection" ), // client/component/redirect-edit/constants.js:58
__( "URL and HTTP header", "redirection" ), // client/component/redirect-edit/constants.js:62
__( "URL and custom filter", "redirection" ), // client/component/redirect-edit/constants.js:66
__( "URL and WordPress page type", "redirection" ), // client/component/redirect-edit/constants.js:70
__( "URL and language", "redirection" ), // client/component/redirect-edit/constants.js:74
__( "Redirect to URL", "redirection" ), // client/component/redirect-edit/constants.js:81
__( "Redirect to random post", "redirection" ), // client/component/redirect-edit/constants.js:85
__( "Pass-through", "redirection" ), // client/component/redirect-edit/constants.js:89
__( "Error (404)", "redirection" ), // client/component/redirect-edit/constants.js:93
__( "Do nothing (ignore)", "redirection" ), // client/component/redirect-edit/constants.js:97
__( "301 - Moved Permanently", "redirection" ), // client/component/redirect-edit/constants.js:104
__( "302 - Found", "redirection" ), // client/component/redirect-edit/constants.js:108
__( "303 - See Other", "redirection" ), // client/component/redirect-edit/constants.js:112
__( "304 - Not Modified", "redirection" ), // client/component/redirect-edit/constants.js:116
__( "307 - Temporary Redirect", "redirection" ), // client/component/redirect-edit/constants.js:120
__( "308 - Permanent Redirect", "redirection" ), // client/component/redirect-edit/constants.js:124
__( "400 - Bad Request", "redirection" ), // client/component/redirect-edit/constants.js:131
__( "401 - Unauthorized", "redirection" ), // client/component/redirect-edit/constants.js:135
__( "403 - Forbidden", "redirection" ), // client/component/redirect-edit/constants.js:139
__( "404 - Not Found", "redirection" ), // client/component/redirect-edit/constants.js:143
__( "410 - Gone", "redirection" ), // client/component/redirect-edit/constants.js:147
__( "418 - I'm a teapot", "redirection" ), // client/component/redirect-edit/constants.js:151
__( "451 - Unavailable For Legal Reasons", "redirection" ), // client/component/redirect-edit/constants.js:155
__( "500 - Internal Server Error", "redirection" ), // client/component/redirect-edit/constants.js:159
__( "501 - Not implemented", "redirection" ), // client/component/redirect-edit/constants.js:163
__( "502 - Bad Gateway", "redirection" ), // client/component/redirect-edit/constants.js:167
__( "503 - Service Unavailable", "redirection" ), // client/component/redirect-edit/constants.js:171
__( "504 - Gateway Timeout", "redirection" ), // client/component/redirect-edit/constants.js:175
__( "Regex", "redirection" ), // client/component/redirect-edit/constants.js:184
__( "Ignore Slash", "redirection" ), // client/component/redirect-edit/constants.js:188
__( "Ignore Case", "redirection" ), // client/component/redirect-edit/constants.js:192
__( "Exact match", "redirection" ), // client/component/redirect-edit/constants.js:203
__( "Exact match in any order", "redirection" ), // client/component/redirect-edit/constants.js:207
__( "Ignore all parameters", "redirection" ), // client/component/redirect-edit/constants.js:211
__( "Ignore & pass parameters to the target", "redirection" ), // client/component/redirect-edit/constants.js:215
__( "Exclude from logs", "redirection" ), // client/component/redirect-edit/index.js:318
__( "When matched", "redirection" ), // client/component/redirect-edit/index.js:360
__( "Group", "redirection" ), // client/component/redirect-edit/index.js:384
__( "Save", "redirection" ), // client/component/redirect-edit/index.js:394
__( "Cancel", "redirection" ), // client/component/redirect-edit/index.js:415
__( "Close", "redirection" ), // client/component/redirect-edit/index.js:420
__( "Show advanced options", "redirection" ), // client/component/redirect-edit/index.js:429
__( "Match", "redirection" ), // client/component/redirect-edit/match-type.js:19
__( "Position", "redirection" ), // client/component/redirect-edit/position.js:12
__( "Query Parameters", "redirection" ), // client/component/redirect-edit/source-query.js:25
__( "Source URL", "redirection" ), // client/component/redirect-edit/source-url.js:25
__( "Source URL", "redirection" ), // client/component/redirect-edit/source-url.js:40
__( "The relative URL you want to redirect from", "redirection" ), // client/component/redirect-edit/source-url.js:48
__( "URL options / Regex", "redirection" ), // client/component/redirect-edit/source-url.js:55
__( "The target URL you want to redirect, or auto-complete on post name or permalink.", "redirection" ), // client/component/redirect-edit/target.js:24
__( "Title", "redirection" ), // client/component/redirect-edit/title.js:23
__( "Describe the purpose of this redirect (optional)", "redirection" ), // client/component/redirect-edit/title.js:29
__( "Anchor values are not sent to the server and cannot be redirected.", "redirection" ), // client/component/redirect-edit/warning.js:57
__( "This will be converted to a server redirect for the domain {{code}}%(server)s{{/code}}.", "redirection" ), // client/component/redirect-edit/warning.js:66
__( "The source URL should probably start with a {{code}}/{{/code}}", "redirection" ), // client/component/redirect-edit/warning.js:87
__( "Remember to enable the \"regex\" option if this is a regular expression.", "redirection" ), // client/component/redirect-edit/warning.js:99
__( "Please add migrated permalinks to the Site page under the \"Permalink Migration\" section.", "redirection" ), // client/component/redirect-edit/warning.js:108
__( "To prevent a greedy regular expression you can use {{code}}^{{/code}} to anchor it to the start of the URL. For example: {{code}}%(example)s{{/code}}", "redirection" ), // client/component/redirect-edit/warning.js:126
__( "The caret {{code}}^{{/code}} should be at the start. For example: {{code}}%(example)s{{/code}}", "redirection" ), // client/component/redirect-edit/warning.js:142
__( "To match {{code}}?{{/code}} you need to escape it with {{code}}\\?{{/code}}", "redirection" ), // client/component/redirect-edit/warning.js:155
__( "Wildcards are not supported. You need to use a {{link}}regular expression{{/link}}.", "redirection" ), // client/component/redirect-edit/warning.js:164
__( "If you want to redirect everything please use a site relocation or alias from the Site page.", "redirection" ), // client/component/redirect-edit/warning.js:174
__( "Your source is the same as a target and this will create a loop. Leave a target blank if you do not want to take action.", "redirection" ), // client/component/redirect-edit/warning.js:184
__( "Your target URL should be an absolute URL like {{code}}https://domain.com/%(url)s{{/code}} or start with a slash {{code}}/%(url)s{{/code}}.", "redirection" ), // client/component/redirect-edit/warning.js:205
__( "Your target URL contains the invalid character {{code}}%(invalid)s{{/code}}", "redirection" ), // client/component/redirect-edit/warning.js:225
__( "Your URL appears to contain a domain inside the path: {{code}}%(relative)s{{/code}}. Did you mean to use {{code}}%(absolute)s{{/code}} instead?", "redirection" ), // client/component/redirect-edit/warning.js:244
__( "Some servers may be configured to serve file resources directly, preventing a redirect occurring.", "redirection" ), // client/component/redirect-edit/warning.js:264
__( "Request Headers", "redirection" ), // client/component/request-data/index.js:21
__( "Redirect Source", "redirection" ), // client/component/request-data/index.js:44
__( "Working!", "redirection" ), // client/component/rest-api-status/api-result-pass.js:15
__( "Show Full", "redirection" ), // client/component/rest-api-status/api-result-raw.js:41
__( "Hide", "redirection" ), // client/component/rest-api-status/api-result-raw.js:42
__( "Good", "redirection" ), // client/component/rest-api-status/index.js:102
__( "Working but some issues", "redirection" ), // client/component/rest-api-status/index.js:104
__( "Unavailable", "redirection" ), // client/component/rest-api-status/index.js:107
__( "There are some problems connecting to your REST API. It is not necessary to fix these problems and the plugin is able to work.", "redirection" ), // client/component/rest-api-status/index.js:122
__( "Your REST API is not working and the plugin will not be able to continue until this is fixed.", "redirection" ), // client/component/rest-api-status/index.js:127
__( "Summary", "redirection" ), // client/component/rest-api-status/index.js:135
__( "Show Problems", "redirection" ), // client/component/rest-api-status/index.js:141
__( "Testing - %s\$", "redirection" ), // client/component/rest-api-status/index.js:170
__( "Check Again", "redirection" ), // client/component/rest-api-status/index.js:179
__( "Bulk Actions", "redirection" ), // client/component/table/bulk-actions.js:45
__( "Apply", "redirection" ), // client/component/table/bulk-actions.js:61
__( "Apply", "redirection" ), // client/component/table/group.js:31
__( "Useragent Error", "redirection" ), // client/component/useragent/index.js:30
__( "Something went wrong obtaining this information", "redirection" ), // client/component/useragent/index.js:31
__( "Unknown Useragent", "redirection" ), // client/component/useragent/index.js:42
__( "Device", "redirection" ), // client/component/useragent/index.js:97
__( "Operating System", "redirection" ), // client/component/useragent/index.js:101
__( "Browser", "redirection" ), // client/component/useragent/index.js:105
__( "Engine", "redirection" ), // client/component/useragent/index.js:109
__( "Useragent", "redirection" ), // client/component/useragent/index.js:114
__( "Agent", "redirection" ), // client/component/useragent/index.js:118
__( "Something went wrong when installing Redirection.", "redirection" ), // client/component/welcome-wizard/index.js:89
__( "Redirection", "redirection" ), // client/component/welcome-wizard/index.js:94
__( "I need support!", "redirection" ), // client/component/welcome-wizard/index.js:107
__( "Manual Install", "redirection" ), // client/component/welcome-wizard/manual-install.js:27
__( "If your site needs special database permissions, or you would rather do it yourself, you can manually run the following SQL.", "redirection" ), // client/component/welcome-wizard/manual-install.js:30
__( "Click \"Finished! 🎉\" when finished.", "redirection" ), // client/component/welcome-wizard/manual-install.js:33
__( "Database problem", "redirection" ), // client/component/welcome-wizard/manual-install.js:47
__( "The Redirection database does not appear to exist. Have you run the above SQL?", "redirection" ), // client/component/welcome-wizard/manual-install.js:48
__( "Finished! 🎉", "redirection" ), // client/component/welcome-wizard/manual-install.js:53
__( "Go back", "redirection" ), // client/component/welcome-wizard/manual-install.js:56
__( "If you do not complete the manual install you will be returned here.", "redirection" ), // client/component/welcome-wizard/manual-install.js:58
__( "REST API", "redirection" ), // client/component/welcome-wizard/step-api.js:38
__( "Redirection uses the {{link}}WordPress REST API{{/link}} to communicate with WordPress. This is enabled and working by default. Sometimes the REST API is blocked by:", "redirection" ), // client/component/welcome-wizard/step-api.js:41
__( "A security plugin (e.g Wordfence)", "redirection" ), // client/component/welcome-wizard/step-api.js:52
__( "A server firewall or other server configuration (e.g OVH)", "redirection" ), // client/component/welcome-wizard/step-api.js:53
__( "Caching software (e.g Cloudflare)", "redirection" ), // client/component/welcome-wizard/step-api.js:54
__( "Some other plugin that blocks the REST API", "redirection" ), // client/component/welcome-wizard/step-api.js:55
__( "If you do experience a problem then please consult your plugin documentation, or try contacting your host support. This is generally {{link}}not a problem caused by Redirection{{/link}}.", "redirection" ), // client/component/welcome-wizard/step-api.js:59
__( "You have different URLs configured on your WordPress Settings > General page, which is usually an indication of a misconfiguration, and it can cause problems with the REST API. Please review your settings.", "redirection" ), // client/component/welcome-wizard/step-api.js:71
__( "You will need at least one working REST API to continue.", "redirection" ), // client/component/welcome-wizard/step-api.js:85
__( "Finish Setup", "redirection" ), // client/component/welcome-wizard/step-api.js:89
__( "Continue", "redirection" ), // client/component/welcome-wizard/step-database.js:30
__( "Installation Complete", "redirection" ), // client/component/welcome-wizard/step-finish.js:42
__( "Redirection is now installed!", "redirection" ), // client/component/welcome-wizard/step-finish.js:44
__( "Please take a moment to consult the {{support}}support site{{/support}} for information about how to use Redirection.", "redirection" ), // client/component/welcome-wizard/step-finish.js:46
__( "Ready to begin! 🎉", "redirection" ), // client/component/welcome-wizard/step-finish.js:53
__( "Import Existing Redirects", "redirection" ), // client/component/welcome-wizard/step-importer.js:34
__( "Importing existing redirects from WordPress or other plugins is a good way to get started with Redirection. Check each set of redirects you wish to import.", "redirection" ), // client/component/welcome-wizard/step-importer.js:37
__( "WordPress automatically creates redirects when you change a post URL. Importing these into Redirection will allow you to manage and monitor them.", "redirection" ), // client/component/welcome-wizard/step-importer.js:45
__( "The following plugins have been detected.", "redirection" ), // client/component/welcome-wizard/step-importer.js:67
__( "Continue", "redirection" ), // client/component/welcome-wizard/step-importer.js:88
__( "Go back", "redirection" ), // client/component/welcome-wizard/step-importer.js:92
__( "Import Existing Redirects", "redirection" ), // client/component/welcome-wizard/step-importing.js:32
__( "Please wait, importing.", "redirection" ), // client/component/welcome-wizard/step-importing.js:36
__( "Import finished.", "redirection" ), // client/component/welcome-wizard/step-importing.js:46
__( "Importing failed.", "redirection" ), // client/component/welcome-wizard/step-importing.js:46
__( "Retry", "redirection" ), // client/component/welcome-wizard/step-importing.js:51
__( "Continue", "redirection" ), // client/component/welcome-wizard/step-importing.js:56
__( "Basic Setup", "redirection" ), // client/component/welcome-wizard/step-options.js:36
__( "These are some options you may want to enable now. They can be changed at any time.", "redirection" ), // client/component/welcome-wizard/step-options.js:38
__( "Monitor permalink changes in WordPress posts and pages", "redirection" ), // client/component/welcome-wizard/step-options.js:49
__( "If you change the permalink in a post or page then Redirection can automatically create a redirect for you.", "redirection" ), // client/component/welcome-wizard/step-options.js:53
__( "{{link}}Read more about this.{{/link}}", "redirection" ), // client/component/welcome-wizard/step-options.js:57
__( "Keep a log of all redirects and 404 errors.", "redirection" ), // client/component/welcome-wizard/step-options.js:69
__( "Storing logs for redirects and 404s will allow you to see what is happening on your site. This will increase your database storage requirements.", "redirection" ), // client/component/welcome-wizard/step-options.js:73
__( "{{link}}Read more about this.{{/link}}", "redirection" ), // client/component/welcome-wizard/step-options.js:77
__( "Store IP information for redirects and 404 errors.", "redirection" ), // client/component/welcome-wizard/step-options.js:89
__( "Storing the IP address allows you to perform additional log actions. Note that you will need to adhere to local laws regarding the collection of data (for example GDPR).", "redirection" ), // client/component/welcome-wizard/step-options.js:93
__( "{{link}}Read more about this.{{/link}}", "redirection" ), // client/component/welcome-wizard/step-options.js:97
__( "Continue", "redirection" ), // client/component/welcome-wizard/step-options.js:107
__( "Go back", "redirection" ), // client/component/welcome-wizard/step-options.js:111
__( "Welcome to Redirection 🚀🎉", "redirection" ), // client/component/welcome-wizard/step-welcome.js:27
__( "Thank you for installing and using Redirection v%(version)s. This plugin will allow you to manage 301 redirections, keep track of 404 errors, and improve your site, with no knowledge of Apache or Nginx needed.", "redirection" ), // client/component/welcome-wizard/step-welcome.js:30
__( "Redirection is designed to be used on sites with a few redirects to sites with thousands of redirects.", "redirection" ), // client/component/welcome-wizard/step-welcome.js:40
__( "How do I use this plugin?", "redirection" ), // client/component/welcome-wizard/step-welcome.js:45
__( "A simple redirect involves setting a {{strong}}source URL{{/strong}} (the old URL) and a {{strong}}target URL{{/strong}} (the new URL). Here's an example:", "redirection" ), // client/component/welcome-wizard/step-welcome.js:47
__( "Source URL", "redirection" ), // client/component/welcome-wizard/step-welcome.js:60
__( "(Example) The source URL is your old or original URL", "redirection" ), // client/component/welcome-wizard/step-welcome.js:66
__( "Target URL", "redirection" ), // client/component/welcome-wizard/step-welcome.js:71
__( "(Example) The target URL is the new URL", "redirection" ), // client/component/welcome-wizard/step-welcome.js:77
__( "That's all there is to it - you are now redirecting! Note that the above is just an example.", "redirection" ), // client/component/welcome-wizard/step-welcome.js:85
__( "Full documentation can be found on the {{link}}Redirection website.{{/link}}", "redirection" ), // client/component/welcome-wizard/step-welcome.js:88
__( "Some features you may find useful are", "redirection" ), // client/component/welcome-wizard/step-welcome.js:95
__( "{{link}}Monitor 404 errors{{/link}}, get detailed information about the visitor, and fix any problems", "redirection" ), // client/component/welcome-wizard/step-welcome.js:99
__( "{{link}}Import{{/link}} from .htaccess, CSV, and a variety of other plugins", "redirection" ), // client/component/welcome-wizard/step-welcome.js:109
__( "More powerful URL matching, including {{regular}}regular expressions{{/regular}}, and {{other}}other conditions{{/other}}", "redirection" ), // client/component/welcome-wizard/step-welcome.js:116
__( "Check a URL is being redirected", "redirection" ), // client/component/welcome-wizard/step-welcome.js:128
__( "What's next?", "redirection" ), // client/component/welcome-wizard/step-welcome.js:131
__( "First you will be asked a few questions, and then Redirection will set up your database.", "redirection" ), // client/component/welcome-wizard/step-welcome.js:132
__( "Start Setup", "redirection" ), // client/component/welcome-wizard/step-welcome.js:136
__( "Manual Setup", "redirection" ), // client/component/welcome-wizard/step-welcome.js:139
__( "Name", "redirection" ), // client/page/groups/constants.js:8
__( "Module", "redirection" ), // client/page/groups/constants.js:9
__( "Status", "redirection" ), // client/page/groups/constants.js:10
__( "Redirects", "redirection" ), // client/page/groups/constants.js:11
__( "Standard Display", "redirection" ), // client/page/groups/constants.js:17
__( "Compact Display", "redirection" ), // client/page/groups/constants.js:22
__( "Display All", "redirection" ), // client/page/groups/constants.js:27
__( "Status", "redirection" ), // client/page/groups/constants.js:34
__( "Enabled", "redirection" ), // client/page/groups/constants.js:38
__( "Disabled", "redirection" ), // client/page/groups/constants.js:42
__( "Module", "redirection" ), // client/page/groups/constants.js:48
__( "Status", "redirection" ), // client/page/groups/constants.js:57
__( "Name", "redirection" ), // client/page/groups/constants.js:62
__( "Redirects", "redirection" ), // client/page/groups/constants.js:67
__( "Module", "redirection" ), // client/page/groups/constants.js:72
__( "Delete", "redirection" ), // client/page/groups/constants.js:80
__( "Enable", "redirection" ), // client/page/groups/constants.js:84
__( "Disable", "redirection" ), // client/page/groups/constants.js:88
__( "Search", "redirection" ), // client/page/groups/constants.js:95
__( "Add Group", "redirection" ), // client/page/groups/create-group.js:29
__( "Use groups to organise your redirects. Groups are assigned to a module, which affects how the redirects in that group work. If you are unsure then stick to the WordPress module.", "redirection" ), // client/page/groups/create-group.js:31
__( "Name", "redirection" ), // client/page/groups/create-group.js:40
__( "Note that you will need to set the Apache module path in your Redirection options.", "redirection" ), // client/page/groups/create-group.js:73
__( "Edit", "redirection" ), // client/page/groups/row-actions.js:30
__( "Delete", "redirection" ), // client/page/groups/row-actions.js:38
__( "View Redirects", "redirection" ), // client/page/groups/row-actions.js:49
__( "Disable", "redirection" ), // client/page/groups/row-actions.js:58
__( "Enable", "redirection" ), // client/page/groups/row-actions.js:64
__( "Cached Redirection detected", "redirection" ), // client/page/home/cache-detect.js:20
__( "Please clear your browser cache and reload this page.", "redirection" ), // client/page/home/cache-detect.js:23
__( "If you are using a caching system such as Cloudflare then please read this: ", "redirection" ), // client/page/home/cache-detect.js:25
__( "clearing your cache.", "redirection" ), // client/page/home/cache-detect.js:27
__( "Redirection is not working. Try clearing your browser cache and reloading this page.", "redirection" ), // client/page/home/crash-handler.js:25
__( "If you are using a page caching plugin or service (CloudFlare, OVH, etc) then you can also try clearing that cache.", "redirection" ), // client/page/home/crash-handler.js:27
__( "If that doesn't help, open your browser's error console and create a {{link}}new issue{{/link}} with the details.", "redirection" ), // client/page/home/crash-handler.js:33
__( "Please check the {{link}}support site{{/link}} before proceeding further.", "redirection" ), // client/page/home/debug.js:19
__( "If that did not help then {{strong}}create an issue{{/strong}} or send it in an {{strong}}email{{/strong}}.", "redirection" ), // client/page/home/debug.js:26
__( "Create An Issue", "redirection" ), // client/page/home/debug.js:37
__( "Email", "redirection" ), // client/page/home/debug.js:40
__( "Include these details in your report along with a description of what you were doing and a screenshot.", "redirection" ), // client/page/home/debug.js:44
__( "What do I do next?", "redirection" ), // client/page/home/error-details.js:21
__( "Take a look at the {{link}}plugin status{{/link}}. It may be able to identify and \"magic fix\" the problem.", "redirection" ), // client/page/home/error-details.js:25
__( "{{link}}Caching software{{/link}}, in particular Cloudflare, can cache the wrong thing. Try clearing all your caches.", "redirection" ), // client/page/home/error-details.js:35
__( "{{link}}Please temporarily disable other plugins!{{/link}} This fixes so many problems.", "redirection" ), // client/page/home/error-details.js:45
__( "If you are using WordPress 5.2 or newer then look at your {{link}}Site Health{{/link}} and resolve any issues.", "redirection" ), // client/page/home/error-details.js:52
__( "Redirections", "redirection" ), // client/page/home/index.js:40
__( "Site", "redirection" ), // client/page/home/index.js:41
__( "Groups", "redirection" ), // client/page/home/index.js:42
__( "Import/Export", "redirection" ), // client/page/home/index.js:43
__( "Logs", "redirection" ), // client/page/home/index.js:44
__( "404 errors", "redirection" ), // client/page/home/index.js:45
__( "Options", "redirection" ), // client/page/home/index.js:46
__( "Support", "redirection" ), // client/page/home/index.js:47
__( "Redirects", "redirection" ), // client/page/home/index.js:53
__( "Groups", "redirection" ), // client/page/home/index.js:57
__( "Site", "redirection" ), // client/page/home/index.js:61
__( "Log", "redirection" ), // client/page/home/index.js:65
__( "404s", "redirection" ), // client/page/home/index.js:69
__( "Import/Export", "redirection" ), // client/page/home/index.js:73
__( "Options", "redirection" ), // client/page/home/index.js:77
__( "Support", "redirection" ), // client/page/home/index.js:81
__( "Add New", "redirection" ), // client/page/home/index.js:157
__( "Version %s installed! Please read the {{url}}release notes{{/url}} for details.", "redirection" ), // client/page/home/update-notice.js:32
__( "OK", "redirection" ), // client/page/home/update-notice.js:43
__( "total = ", "redirection" ), // client/page/io/importer.js:17
__( "Import from %s", "redirection" ), // client/page/io/importer.js:20
__( "Import to group", "redirection" ), // client/page/io/index.js:101
__( "Import a CSV, .htaccess, or JSON file.", "redirection" ), // client/page/io/index.js:109
__( "Click 'Add File' or drag and drop here.", "redirection" ), // client/page/io/index.js:110
__( "Add File", "redirection" ), // client/page/io/index.js:112
__( "File selected", "redirection" ), // client/page/io/index.js:123
__( "Upload", "redirection" ), // client/page/io/index.js:129
__( "Cancel", "redirection" ), // client/page/io/index.js:130
__( "Importing", "redirection" ), // client/page/io/index.js:140
__( "Finished importing", "redirection" ), // client/page/io/index.js:156
__( "Total redirects imported:", "redirection" ), // client/page/io/index.js:158
__( "Double-check the file is the correct format!", "redirection" ), // client/page/io/index.js:159
__( "OK", "redirection" ), // client/page/io/index.js:161
__( "Close", "redirection" ), // client/page/io/index.js:210
__( "Are you sure you want to import from %s?", "redirection" ), // client/page/io/index.js:224
__( "Plugin Importers", "redirection" ), // client/page/io/index.js:232
__( "The following redirect plugins were detected on your site and can be imported from.", "redirection" ), // client/page/io/index.js:234
__( "Import", "redirection" ), // client/page/io/index.js:246
__( "All imports will be appended to the current database - nothing is merged.", "redirection" ), // client/page/io/index.js:257
__( "{{strong}}CSV file format{{/strong}}: {{code}}source URL, target URL{{/code}} - and can be optionally followed with {{code}}regex, http code{{/code}} ({{code}}regex{{/code}} - 0 for no, 1 for yes).", "redirection" ), // client/page/io/index.js:260
__( "CSV does not include all information, and everything is imported/exported as \"URL only\" matches. Use the JSON format for a full set of data.", "redirection" ), // client/page/io/index.js:267
__( "Export", "redirection" ), // client/page/io/index.js:270
__( "Export to CSV, Apache .htaccess, Nginx, or Redirection JSON. The JSON format contains full information, and other formats contain partial information appropriate to the format.", "redirection" ), // client/page/io/index.js:271
__( "Everything", "redirection" ), // client/page/io/index.js:275
__( "WordPress redirects", "redirection" ), // client/page/io/index.js:276
__( "Apache redirects", "redirection" ), // client/page/io/index.js:277
__( "Nginx redirects", "redirection" ), // client/page/io/index.js:278
__( "Complete data (JSON)", "redirection" ), // client/page/io/index.js:282
__( "CSV", "redirection" ), // client/page/io/index.js:283
__( "Apache .htaccess", "redirection" ), // client/page/io/index.js:284
__( "Nginx rewrite rules", "redirection" ), // client/page/io/index.js:285
__( "View", "redirection" ), // client/page/io/index.js:288
__( "Download", "redirection" ), // client/page/io/index.js:289
__( "Export redirect", "redirection" ), // client/page/io/index.js:296
__( "Export 404", "redirection" ), // client/page/io/index.js:297
__( "Source URL", "redirection" ), // client/page/logs/constants.js:12
__( "Count", "redirection" ), // client/page/logs/constants.js:18
__( "IP", "redirection" ), // client/page/logs/constants.js:26
__( "Count", "redirection" ), // client/page/logs/constants.js:32
__( "User Agent", "redirection" ), // client/page/logs/constants.js:40
__( "Count", "redirection" ), // client/page/logs/constants.js:46
__( "Date", "redirection" ), // client/page/logs/constants.js:54
__( "Method", "redirection" ), // client/page/logs/constants.js:58
__( "Domain", "redirection" ), // client/page/logs/constants.js:63
__( "Source URL", "redirection" ), // client/page/logs/constants.js:68
__( "Target URL", "redirection" ), // client/page/logs/constants.js:73
__( "Redirect By", "redirection" ), // client/page/logs/constants.js:78
__( "HTTP code", "redirection" ), // client/page/logs/constants.js:83
__( "Referrer", "redirection" ), // client/page/logs/constants.js:88
__( "User Agent", "redirection" ), // client/page/logs/constants.js:93
__( "IP", "redirection" ), // client/page/logs/constants.js:98
__( "Delete", "redirection" ), // client/page/logs/constants.js:107
__( "Group", "redirection" ), // client/page/logs/constants.js:113
__( "Standard Display", "redirection" ), // client/page/logs/constants.js:118
__( "Compact Display", "redirection" ), // client/page/logs/constants.js:123
__( "Display All", "redirection" ), // client/page/logs/constants.js:128
__( "URL", "redirection" ), // client/page/logs/constants.js:136
__( "Count", "redirection" ), // client/page/logs/constants.js:136
__( "User Agent", "redirection" ), // client/page/logs/constants.js:140
__( "Count", "redirection" ), // client/page/logs/constants.js:140
__( "IP", "redirection" ), // client/page/logs/constants.js:144
__( "Count", "redirection" ), // client/page/logs/constants.js:144
__( "Date", "redirection" ), // client/page/logs/constants.js:148
__( "Method", "redirection" ), // client/page/logs/constants.js:149
__( "Domain", "redirection" ), // client/page/logs/constants.js:150
__( "URL", "redirection" ), // client/page/logs/constants.js:151
__( "Redirect By", "redirection" ), // client/page/logs/constants.js:152
__( "HTTP code", "redirection" ), // client/page/logs/constants.js:153
__( "Referrer", "redirection" ), // client/page/logs/constants.js:154
__( "User Agent", "redirection" ), // client/page/logs/constants.js:155
__( "Target", "redirection" ), // client/page/logs/constants.js:156
__( "IP", "redirection" ), // client/page/logs/constants.js:157
__( "Search URL", "redirection" ), // client/page/logs/constants.js:164
__( "Search exact URL", "redirection" ), // client/page/logs/constants.js:168
__( "Search referrer", "redirection" ), // client/page/logs/constants.js:172
__( "Search user agent", "redirection" ), // client/page/logs/constants.js:176
__( "Search IP", "redirection" ), // client/page/logs/constants.js:180
__( "Search target URL", "redirection" ), // client/page/logs/constants.js:184
__( "Search domain", "redirection" ), // client/page/logs/constants.js:188
__( "No grouping", "redirection" ), // client/page/logs/constants.js:196
__( "Group by URL", "redirection" ), // client/page/logs/constants.js:200
__( "Group by user agent", "redirection" ), // client/page/logs/constants.js:204
__( "Group by IP", "redirection" ), // client/page/logs/constants.js:211
__( "Method", "redirection" ), // client/page/logs/constants.js:220
__( "Redirect By", "redirection" ), // client/page/logs/constants.js:238
__( "WordPress", "redirection" ), // client/page/logs/constants.js:242
__( "Redirection", "redirection" ), // client/page/logs/constants.js:246
__( "RSS", "redirection" ), // client/page/logs/index.js:112
__( "Delete", "redirection" ), // client/page/logs/row-actions.js:30
__( "View Redirect", "redirection" ), // client/page/logs/row-actions.js:52
__( "Source URL", "redirection" ), // client/page/logs404/constants.js:13
__( "Count", "redirection" ), // client/page/logs404/constants.js:19
__( "User Agent", "redirection" ), // client/page/logs404/constants.js:29
__( "Count", "redirection" ), // client/page/logs404/constants.js:35
__( "IP", "redirection" ), // client/page/logs404/constants.js:45
__( "Count", "redirection" ), // client/page/logs404/constants.js:51
__( "Date", "redirection" ), // client/page/logs404/constants.js:60
__( "Method", "redirection" ), // client/page/logs404/constants.js:64
__( "Domain", "redirection" ), // client/page/logs404/constants.js:68
__( "Source URL", "redirection" ), // client/page/logs404/constants.js:72
__( "HTTP code", "redirection" ), // client/page/logs404/constants.js:77
__( "Referrer", "redirection" ), // client/page/logs404/constants.js:82
__( "User Agent", "redirection" ), // client/page/logs404/constants.js:87
__( "IP", "redirection" ), // client/page/logs404/constants.js:92
__( "Delete", "redirection" ), // client/page/logs404/constants.js:108
__( "Redirect All", "redirection" ), // client/page/logs404/constants.js:116
__( "Block IP", "redirection" ), // client/page/logs404/constants.js:120
__( "Redirect All", "redirection" ), // client/page/logs404/constants.js:132
__( "Ignore URL", "redirection" ), // client/page/logs404/constants.js:137
__( "No grouping", "redirection" ), // client/page/logs404/constants.js:147
__( "Group by URL", "redirection" ), // client/page/logs404/constants.js:151
__( "Group by user agent", "redirection" ), // client/page/logs404/constants.js:155
__( "Group by IP", "redirection" ), // client/page/logs404/constants.js:162
__( "Group", "redirection" ), // client/page/logs404/constants.js:171
__( "Standard Display", "redirection" ), // client/page/logs404/constants.js:177
__( "Compact Display", "redirection" ), // client/page/logs404/constants.js:182
__( "Display All", "redirection" ), // client/page/logs404/constants.js:187
__( "URL", "redirection" ), // client/page/logs404/constants.js:199
__( "Count", "redirection" ), // client/page/logs404/constants.js:199
__( "User Agent", "redirection" ), // client/page/logs404/constants.js:203
__( "Count", "redirection" ), // client/page/logs404/constants.js:203
__( "IP", "redirection" ), // client/page/logs404/constants.js:207
__( "Count", "redirection" ), // client/page/logs404/constants.js:207
__( "Date", "redirection" ), // client/page/logs404/constants.js:211
__( "Method", "redirection" ), // client/page/logs404/constants.js:212
__( "Domain", "redirection" ), // client/page/logs404/constants.js:213
__( "URL", "redirection" ), // client/page/logs404/constants.js:214
__( "HTTP code", "redirection" ), // client/page/logs404/constants.js:215
__( "Referrer", "redirection" ), // client/page/logs404/constants.js:216
__( "User Agent", "redirection" ), // client/page/logs404/constants.js:217
__( "IP", "redirection" ), // client/page/logs404/constants.js:218
__( "Method", "redirection" ), // client/page/logs404/constants.js:224
__( "HTTP Status Code", "redirection" ), // client/page/logs404/constants.js:242
__( "Search URL", "redirection" ), // client/page/logs404/constants.js:251
__( "Search exact URL", "redirection" ), // client/page/logs404/constants.js:255
__( "Search referrer", "redirection" ), // client/page/logs404/constants.js:259
__( "Search user agent", "redirection" ), // client/page/logs404/constants.js:263
__( "Search IP", "redirection" ), // client/page/logs404/constants.js:267
__( "Search domain", "redirection" ), // client/page/logs404/constants.js:271
__( "Add Redirect", "redirection" ), // client/page/logs404/create-redirect.js:53
__( "Are you sure you want to delete the selected items?", "redirection" ), // client/page/logs404/create-redirect.js:62
__( "Are you sure you want to delete this item?", "redirection" ), // client/page/logs404/create-redirect.js:63
__( "Delete Log Entries", "redirection" ), // client/page/logs404/create-redirect.js:71
__( "Delete logs for this entry", "redirection" ), // client/page/logs404/create-redirect.js:81
__( "Delete logs for these entries", "redirection" ), // client/page/logs404/create-redirect.js:82
__( "Delete", "redirection" ), // client/page/logs404/row-actions.js:42
__( "Add Redirect", "redirection" ), // client/page/logs404/row-actions.js:52
__( "Show All", "redirection" ), // client/page/logs404/row-actions.js:70
__( "Block IP", "redirection" ), // client/page/logs404/row-actions.js:77
__( "Ignore URL", "redirection" ), // client/page/logs404/row-actions.js:87
__( "Delete the plugin - are you sure?", "redirection" ), // client/page/options/delete-plugin.js:37
__( "Deleting the plugin will remove all your redirections, logs, and settings. Do this if you want to remove the plugin for good, or if you want to reset the plugin.", "redirection" ), // client/page/options/delete-plugin.js:38
__( "Once deleted your redirections will stop working. If they appear to continue working then please clear your browser cache.", "redirection" ), // client/page/options/delete-plugin.js:39
__( "Yes! Delete the plugin", "redirection" ), // client/page/options/delete-plugin.js:41
__( "No! Don't delete the plugin", "redirection" ), // client/page/options/delete-plugin.js:41
__( "Delete Redirection", "redirection" ), // client/page/options/delete-plugin.js:52
__( "Selecting this option will delete all redirections, all logs, and any options associated with the Redirection plugin. Make sure this is what you want to do.", "redirection" ), // client/page/options/delete-plugin.js:54
__( "Delete", "redirection" ), // client/page/options/delete-plugin.js:55
__( "You've supported this plugin - thank you!", "redirection" ), // client/page/options/donation.js:82
__( "I'd like to support some more.", "redirection" ), // client/page/options/donation.js:83
__( "Redirection is free to use - life is wonderful and lovely! It has required a great deal of time and effort to develop and you can help support this development by {{strong}}making a small donation{{/strong}}.", "redirection" ), // client/page/options/donation.js:99
__( "You get useful software and I get to carry on making it better.", "redirection" ), // client/page/options/donation.js:104
__( "Support 💰", "redirection" ), // client/page/options/donation.js:127
__( "Plugin Support", "redirection" ), // client/page/options/donation.js:139
__( "Newsletter", "redirection" ), // client/page/options/newsletter.js:23
__( "Thanks for subscribing! {{a}}Click here{{/a}} if you need to return to your subscription.", "redirection" ), // client/page/options/newsletter.js:25
__( "Newsletter", "redirection" ), // client/page/options/newsletter.js:36
__( "Want to keep up to date with changes to Redirection?", "redirection" ), // client/page/options/newsletter.js:38
__( "Sign up for the tiny Redirection newsletter - a low volume newsletter about new features and changes to the plugin. Ideal if you want to test beta changes before release.", "redirection" ), // client/page/options/newsletter.js:39
__( "Your email address:", "redirection" ), // client/page/options/newsletter.js:43
__( "Status", "redirection" ), // client/page/redirects/constants.js:11
__( "URL", "redirection" ), // client/page/redirects/constants.js:16
__( "Match Type", "redirection" ), // client/page/redirects/constants.js:21
__( "Action Type", "redirection" ), // client/page/redirects/constants.js:26
__( "Code", "redirection" ), // client/page/redirects/constants.js:31
__( "Group", "redirection" ), // client/page/redirects/constants.js:36
__( "Pos", "redirection" ), // client/page/redirects/constants.js:41
__( "Hits", "redirection" ), // client/page/redirects/constants.js:45
__( "Last Access", "redirection" ), // client/page/redirects/constants.js:49
__( "Delete", "redirection" ), // client/page/redirects/constants.js:56
__( "Enable", "redirection" ), // client/page/redirects/constants.js:60
__( "Disable", "redirection" ), // client/page/redirects/constants.js:64
__( "Reset hits", "redirection" ), // client/page/redirects/constants.js:68
__( "Source", "redirection" ), // client/page/redirects/constants.js:73
__( "URL options", "redirection" ), // client/page/redirects/constants.js:74
__( "Query Parameters", "redirection" ), // client/page/redirects/constants.js:75
__( "Title", "redirection" ), // client/page/redirects/constants.js:76
__( "Target", "redirection" ), // client/page/redirects/constants.js:77
__( "HTTP code", "redirection" ), // client/page/redirects/constants.js:78
__( "Match Type", "redirection" ), // client/page/redirects/constants.js:79
__( "Position", "redirection" ), // client/page/redirects/constants.js:80
__( "Hits", "redirection" ), // client/page/redirects/constants.js:81
__( "Last Access", "redirection" ), // client/page/redirects/constants.js:82
__( "Status", "redirection" ), // client/page/redirects/constants.js:83
__( "Action Type", "redirection" ), // client/page/redirects/constants.js:84
__( "Group", "redirection" ), // client/page/redirects/constants.js:85
__( "Standard Display", "redirection" ), // client/page/redirects/constants.js:91
__( "Compact Display", "redirection" ), // client/page/redirects/constants.js:96
__( "Display All", "redirection" ), // client/page/redirects/constants.js:101
__( "Status", "redirection" ), // client/page/redirects/constants.js:108
__( "Enabled", "redirection" ), // client/page/redirects/constants.js:112
__( "Disabled", "redirection" ), // client/page/redirects/constants.js:116
__( "URL match", "redirection" ), // client/page/redirects/constants.js:122
__( "Regular Expression", "redirection" ), // client/page/redirects/constants.js:126
__( "Plain", "redirection" ), // client/page/redirects/constants.js:130
__( "Match Type", "redirection" ), // client/page/redirects/constants.js:136
__( "Action Type", "redirection" ), // client/page/redirects/constants.js:141
__( "HTTP Status Code", "redirection" ), // client/page/redirects/constants.js:147
__( "Last Accessed", "redirection" ), // client/page/redirects/constants.js:152
__( "Never accessed", "redirection" ), // client/page/redirects/constants.js:156
__( "Not accessed in last month", "redirection" ), // client/page/redirects/constants.js:160
__( "Not accessed in last year", "redirection" ), // client/page/redirects/constants.js:164
__( "Search URL", "redirection" ), // client/page/redirects/constants.js:174
__( "Search target URL", "redirection" ), // client/page/redirects/constants.js:178
__( "Search title", "redirection" ), // client/page/redirects/constants.js:182
__( "Add new redirection", "redirection" ), // client/page/redirects/create.js:27
__( "Add Redirect", "redirection" ), // client/page/redirects/create.js:32
__( "All groups", "redirection" ), // client/page/redirects/index.js:62
__( "Edit", "redirection" ), // client/page/redirects/row-actions.js:28
__( "Delete", "redirection" ), // client/page/redirects/row-actions.js:32
__( "Disable", "redirection" ), // client/page/redirects/row-actions.js:37
__( "Enable", "redirection" ), // client/page/redirects/row-actions.js:39
__( "Check Redirect", "redirection" ), // client/page/redirects/row-actions.js:44
__( "Options on this page can cause problems if used incorrectly. You can {{link}}temporarily disable them{{/link}} to make changes.", "redirection" ), // client/page/site/index.js:72
__( "Update", "redirection" ), // client/page/site/index.js:107
__( "Database version", "redirection" ), // client/page/support/debug.js:68
__( "Do not change unless advised to do so!", "redirection" ), // client/page/support/debug.js:79
__( "Save", "redirection" ), // client/page/support/debug.js:81
__( "IP Headers", "redirection" ), // client/page/support/debug.js:88
__( "Need help?", "redirection" ), // client/page/support/help.js:14
__( "Full documentation for Redirection can be found at {{site}}https://redirection.me{{/site}}. If you have a problem please check the {{faq}}FAQ{{/faq}} first.", "redirection" ), // client/page/support/help.js:16
__( "If you want to report a bug please read the {{report}}Reporting Bugs{{/report}} guide.", "redirection" ), // client/page/support/help.js:28
__( "Please note that any support is provide on as-time-is-available basis and is not guaranteed. I do not provide paid support.", "redirection" ), // client/page/support/help.js:50
__( "If you want to submit information that you don't want in a public repository then send it directly via {{email}}email{{/email}} - include as much information as you can!", "redirection" ), // client/page/support/help.js:55
__( "Need to search and replace?", "redirection" ), // client/page/support/help.js:71
__( "The companion plugin Search Regex allows you to search and replace data on your site. It also supports Redirection, and is handy if you want to bulk update a lot of redirects.", "redirection" ), // client/page/support/help.js:73
__( "Redirect Tester", "redirection" ), // client/page/support/http-tester.js:41
__( "Sometimes your browser can cache a URL, making it hard to know if it's working as expected. Use this service from {{link}}redirect.li{{/link}} to get accurate results.", "redirection" ), // client/page/support/http-tester.js:44
__( "URL", "redirection" ), // client/page/support/http-tester.js:50
__( "Enter full URL, including http:// or https://", "redirection" ), // client/page/support/http-tester.js:58
__( "Check", "redirection" ), // client/page/support/http-tester.js:64
__( "If the magic button doesn't work then you should read the error and see if you can fix it manually, otherwise follow the 'Need help' section below.", "redirection" ), // client/page/support/plugin-status.js:22
__( "⚡️ Magic fix ⚡️", "redirection" ), // client/page/support/plugin-status.js:27
__( "Good", "redirection" ), // client/page/support/plugin-status.js:39
__( "Problem", "redirection" ), // client/page/support/plugin-status.js:39
__( "WordPress REST API", "redirection" ), // client/page/support/status.js:29
__( "Redirection communicates with WordPress through the WordPress REST API. This is a standard part of WordPress, and you will experience problems if you cannot use it.", "redirection" ), // client/page/support/status.js:30
__( "Plugin Status", "redirection" ), // client/page/support/status.js:33
__( "Plugin Debug", "redirection" ), // client/page/support/status.js:38
__( "This information is provided for debugging purposes. Be careful making any changes.", "redirection" ), // client/page/support/status.js:39
__( "Redirection saved", "redirection" ), // client/state/message/reducer.js:49
__( "Log deleted", "redirection" ), // client/state/message/reducer.js:50
__( "Settings saved", "redirection" ), // client/state/message/reducer.js:51
__( "Group saved", "redirection" ), // client/state/message/reducer.js:52
__( "404 deleted", "redirection" ), // client/state/message/reducer.js:53
__( "View notice", "redirection" ), // client/wp-plugin-components/snackbar/index.js:75
__( "Add File", "redirection" ), // client/wp-plugin-components/uploader/content.js:45
__( "Upload", "redirection" ), // client/wp-plugin-components/uploader/content.js:54
__( "Cancel", "redirection" ), // client/wp-plugin-components/uploader/content.js:57
__( "View Data", "redirection" ), // client/component/log-page/log-actions/extra-data.js:28
__( "Geo Info", "redirection" ), // client/component/log-page/log-actions/geo-map.js:32
__( "Agent Info", "redirection" ), // client/component/log-page/log-actions/user-agent.js:32
__( "Filter by IP", "redirection" ), // client/component/log-page/log-columns/column-ip.js:60
__( "Redirection", "redirection" ), // client/component/log-page/log-columns/index.js:77
__( "Logged In", "redirection" ), // client/component/redirect-edit/action/login.js:20
__( "Target URL when matched (empty to ignore)", "redirection" ), // client/component/redirect-edit/action/login.js:21
__( "Logged Out", "redirection" ), // client/component/redirect-edit/action/login.js:23
__( "Target URL when not matched (empty to ignore)", "redirection" ), // client/component/redirect-edit/action/login.js:24
__( "Matched Target", "redirection" ), // client/component/redirect-edit/action/url-from.js:20
__( "Target URL when matched (empty to ignore)", "redirection" ), // client/component/redirect-edit/action/url-from.js:21
__( "Unmatched Target", "redirection" ), // client/component/redirect-edit/action/url-from.js:23
__( "Target URL when not matched (empty to ignore)", "redirection" ), // client/component/redirect-edit/action/url-from.js:24
__( "Target URL", "redirection" ), // client/component/redirect-edit/action/url.js:20
__( "User Agent", "redirection" ), // client/component/redirect-edit/match/agent.js:51
__( "Match against this browser user agent", "redirection" ), // client/component/redirect-edit/match/agent.js:52
__( "Custom", "redirection" ), // client/component/redirect-edit/match/agent.js:55
__( "Mobile", "redirection" ), // client/component/redirect-edit/match/agent.js:56
__( "Feed Readers", "redirection" ), // client/component/redirect-edit/match/agent.js:57
__( "Libraries", "redirection" ), // client/component/redirect-edit/match/agent.js:58
__( "Regex", "redirection" ), // client/component/redirect-edit/match/agent.js:62
__( "Cookie", "redirection" ), // client/component/redirect-edit/match/cookie.js:20
__( "Cookie name", "redirection" ), // client/component/redirect-edit/match/cookie.js:21
__( "Cookie value", "redirection" ), // client/component/redirect-edit/match/cookie.js:22
__( "Regex", "redirection" ), // client/component/redirect-edit/match/cookie.js:25
__( "Filter Name", "redirection" ), // client/component/redirect-edit/match/custom.js:18
__( "WordPress filter name", "redirection" ), // client/component/redirect-edit/match/custom.js:19
__( "HTTP Header", "redirection" ), // client/component/redirect-edit/match/header.js:50
__( "Header name", "redirection" ), // client/component/redirect-edit/match/header.js:51
__( "Header value", "redirection" ), // client/component/redirect-edit/match/header.js:52
__( "Custom", "redirection" ), // client/component/redirect-edit/match/header.js:55
__( "Accept Language", "redirection" ), // client/component/redirect-edit/match/header.js:56
__( "Regex", "redirection" ), // client/component/redirect-edit/match/header.js:60
__( "Note it is your responsibility to pass HTTP headers to PHP. Please contact your hosting provider for support about this.", "redirection" ), // client/component/redirect-edit/match/header.js:67
__( "IP", "redirection" ), // client/component/redirect-edit/match/ip.js:22
__( "Enter IP addresses (one per line)", "redirection" ), // client/component/redirect-edit/match/ip.js:23
__( "Language", "redirection" ), // client/component/redirect-edit/match/language.js:18
__( "Comma separated list of languages to match against (i.e. da, en-GB)", "redirection" ), // client/component/redirect-edit/match/language.js:19
__( "Page Type", "redirection" ), // client/component/redirect-edit/match/page.js:17
__( "Only the 404 page type is currently supported.", "redirection" ), // client/component/redirect-edit/match/page.js:19
__( "Please do not try and redirect all your 404s - this is not a good thing to do.", "redirection" ), // client/component/redirect-edit/match/page.js:20
__( "Referrer", "redirection" ), // client/component/redirect-edit/match/referrer.js:19
__( "Match against this browser referrer text", "redirection" ), // client/component/redirect-edit/match/referrer.js:20
__( "Regex", "redirection" ), // client/component/redirect-edit/match/referrer.js:23
__( "Role", "redirection" ), // client/component/redirect-edit/match/role.js:18
__( "Enter role or capability value", "redirection" ), // client/component/redirect-edit/match/role.js:19
__( "Server", "redirection" ), // client/component/redirect-edit/match/server.js:18
__( "Enter server URL to match against", "redirection" ), // client/component/redirect-edit/match/server.js:19
__( "Select All", "redirection" ), // client/component/table/header/check-column.js:23
_n( "%s item", "%s items", 1, "redirection" ), // client/component/table/navigation/navigation-pages.js:33
__( "%1d of %1d selected. {{all}}Select All.{{/all}}", "redirection" ), // client/component/table/navigation/navigation-pages.js:36
__( "%1d of %1d selected. {{all}}Clear All.{{/all}}", "redirection" ), // client/component/table/navigation/navigation-pages.js:43
__( "First page", "redirection" ), // client/component/table/navigation/pagination-links.js:39
__( "Prev page", "redirection" ), // client/component/table/navigation/pagination-links.js:47
__( "Current Page", "redirection" ), // client/component/table/navigation/pagination-links.js:55
__( "of %(page)s", "redirection" ), // client/component/table/navigation/pagination-links.js:72
__( "Next page", "redirection" ), // client/component/table/navigation/pagination-links.js:84
__( "Last page", "redirection" ), // client/component/table/navigation/pagination-links.js:92
__( "Nothing to display.", "redirection" ), // client/component/table/row/empty-row.js:13
__( "Sorry, something went wrong loading the data - please try again", "redirection" ), // client/component/table/row/failed-row.js:14
__( "Name", "redirection" ), // client/page/groups/columns/edit.js:41
__( "Module", "redirection" ), // client/page/groups/columns/edit.js:53
__( "Save", "redirection" ), // client/page/groups/columns/edit.js:71
__( "Cancel", "redirection" ), // client/page/groups/columns/edit.js:78
__( "Note that you will need to set the Apache module path in your Redirection options.", "redirection" ), // client/page/groups/columns/edit.js:86
__( "Filter on: %(type)s", "redirection" ), // client/page/groups/columns/module.js:24
__( "I'm a nice person and I have helped support the author of this plugin", "redirection" ), // client/page/options/options-form/index.js:57
__( "Update", "redirection" ), // client/page/options/options-form/index.js:75
__( "No logs", "redirection" ), // client/page/options/options-form/log-options.js:15
__( "A day", "redirection" ), // client/page/options/options-form/log-options.js:16
__( "A week", "redirection" ), // client/page/options/options-form/log-options.js:17
__( "A month", "redirection" ), // client/page/options/options-form/log-options.js:18
__( "Two months", "redirection" ), // client/page/options/options-form/log-options.js:19
__( "Forever", "redirection" ), // client/page/options/options-form/log-options.js:20
__( "No IP logging", "redirection" ), // client/page/options/options-form/log-options.js:23
__( "Full IP logging", "redirection" ), // client/page/options/options-form/log-options.js:24
__( "Anonymize IP (mask last part)", "redirection" ), // client/page/options/options-form/log-options.js:25
__( "Logs", "redirection" ), // client/page/options/options-form/log-options.js:36
__( "Redirect Logs", "redirection" ), // client/page/options/options-form/log-options.js:39
__( "(time to keep logs for)", "redirection" ), // client/page/options/options-form/log-options.js:46
__( "404 Logs", "redirection" ), // client/page/options/options-form/log-options.js:48
__( "(time to keep logs for)", "redirection" ), // client/page/options/options-form/log-options.js:55
__( "IP Logging", "redirection" ), // client/page/options/options-form/log-options.js:57
__( "(IP logging level)", "redirection" ), // client/page/options/options-form/log-options.js:64
__( "Logging", "redirection" ), // client/page/options/options-form/log-options.js:66
__( "Log \"external\" redirects - those not from Redirection. This can increase your log size and contains no user information.", "redirection" ), // client/page/options/options-form/log-options.js:76
__( "Track redirect hits and date of last access. Contains no user information.", "redirection" ), // client/page/options/options-form/log-options.js:84
__( "Capture HTTP header information with logs (except cookies). It may include user information, and could increase your log size.", "redirection" ), // client/page/options/options-form/log-options.js:90
__( "Redirection stores no user identifiable information other than what is configured above. It is your responsibility to ensure your site meets any applicable {{link}}privacy requirements{{/link}}.", "redirection" ), // client/page/options/options-form/log-options.js:99
__( "Default REST API", "redirection" ), // client/page/options/options-form/other-options.js:15
__( "Raw REST API", "redirection" ), // client/page/options/options-form/other-options.js:16
__( "Relative REST API", "redirection" ), // client/page/options/options-form/other-options.js:17
__( "Upgrade manually when prompted", "redirection" ), // client/page/options/options-form/other-options.js:24
__( "Automatically upgrade on admin pages", "redirection" ), // client/page/options/options-form/other-options.js:28
__( "Advanced", "redirection" ), // client/page/options/options-form/other-options.js:41
__( "RSS Token", "redirection" ), // client/page/options/options-form/other-options.js:44
__( "A unique token allowing feed readers access to Redirection log RSS (leave blank to auto-generate)", "redirection" ), // client/page/options/options-form/other-options.js:48
__( "Apache .htaccess", "redirection" ), // client/page/options/options-form/other-options.js:53
__( "Redirects added to an Apache group can be saved to an {{code}}.htaccess{{/code}} file by adding the full path here. For reference, your WordPress is installed to {{code}}%(installed)s{{/code}}.", "redirection" ), // client/page/options/options-form/other-options.js:66
__( "Unable to save .htaccess file", "redirection" ), // client/page/options/options-form/other-options.js:81
__( "REST API", "redirection" ), // client/page/options/options-form/other-options.js:86
__( "How Redirection uses the REST API - don't change unless necessary", "redirection" ), // client/page/options/options-form/other-options.js:90
__( "Data Upgrade", "redirection" ), // client/page/options/options-form/other-options.js:93
__( "Decide how Redirection updates itself, if needed.", "redirection" ), // client/page/options/options-form/other-options.js:102
__( "Monitor changes to %(type)s", "redirection" ), // client/page/options/options-form/url-monitor.js:42
__( "URL Monitor", "redirection" ), // client/page/options/options-form/url-monitor.js:98
__( "URL Monitor Changes", "redirection" ), // client/page/options/options-form/url-monitor.js:103
__( "Save changes to this group", "redirection" ), // client/page/options/options-form/url-monitor.js:106
__( "For example \"/amp\"", "redirection" ), // client/page/options/options-form/url-monitor.js:113
__( "Create associated redirect (added to end of URL)", "redirection" ), // client/page/options/options-form/url-monitor.js:116
__( "Exact match in any order", "redirection" ), // client/page/options/options-form/url-options.js:16
__( "Ignore all query parameters", "redirection" ), // client/page/options/options-form/url-options.js:17
__( "Ignore and pass all query parameters", "redirection" ), // client/page/options/options-form/url-options.js:18
__( "Never cache", "redirection" ), // client/page/options/options-form/url-options.js:21
__( "An hour", "redirection" ), // client/page/options/options-form/url-options.js:22
__( "A day", "redirection" ), // client/page/options/options-form/url-options.js:23
__( "A week", "redirection" ), // client/page/options/options-form/url-options.js:24
__( "Forever", "redirection" ), // client/page/options/options-form/url-options.js:25
__( "URL", "redirection" ), // client/page/options/options-form/url-options.js:36
__( "Default URL settings", "redirection" ), // client/page/options/options-form/url-options.js:46
__( "Applies to all redirections unless you configure them otherwise.", "redirection" ), // client/page/options/options-form/url-options.js:47
__( "Case insensitive matches (i.e. {{code}}/Exciting-Post{{/code}} will match {{code}}/exciting-post{{/code}})", "redirection" ), // client/page/options/options-form/url-options.js:51
__( "Ignore trailing slashes (i.e. {{code}}/exciting-post/{{/code}} will match {{code}}/exciting-post{{/code}})", "redirection" ), // client/page/options/options-form/url-options.js:70
__( "Default query matching", "redirection" ), // client/page/options/options-form/url-options.js:81
__( "Applies to all redirections unless you configure them otherwise.", "redirection" ), // client/page/options/options-form/url-options.js:82
__( "Exact - matches the query parameters exactly defined in your source, in any order", "redirection" ), // client/page/options/options-form/url-options.js:88
__( "Ignore - as exact, but ignores any query parameters not in your source", "redirection" ), // client/page/options/options-form/url-options.js:92
__( "Pass - as ignore, but also copies the query parameters to the target", "redirection" ), // client/page/options/options-form/url-options.js:93
__( "Auto-generate URL", "redirection" ), // client/page/options/options-form/url-options.js:96
__( "Used to auto-generate a URL if no URL is given. Use the special tags {{code}}\$dec\${{/code}} or {{code}}\$hex\${{/code}} to insert a unique ID instead", "redirection" ), // client/page/options/options-form/url-options.js:106
__( "HTTP Cache Header", "redirection" ), // client/page/options/options-form/url-options.js:117
__( "How long to cache redirected 301 URLs (via \"Expires\" HTTP header)", "redirection" ), // client/page/options/options-form/url-options.js:126
__( "Redirect Caching", "redirection" ), // client/page/options/options-form/url-options.js:129
__( "(beta) Enable caching of redirects via WordPress object cache. Can improve performance. Requires an object cache.", "redirection" ), // client/page/options/options-form/url-options.js:134
__( "pass", "redirection" ), // client/page/redirects/columns/code.js:16
__( "Exact Query", "redirection" ), // client/page/redirects/columns/source-query.js:21
__( "Ignore Query", "redirection" ), // client/page/redirects/columns/source-query.js:24
__( "Ignore & Pass Query", "redirection" ), // client/page/redirects/columns/source-query.js:26
__( "Site Aliases", "redirection" ), // client/page/site/aliases/index.js:39
__( "A site alias is another domain that you want to be redirected to this site. For example, an old domain, or a subdomain. This will redirect all URLs, including WordPress login and admin.", "redirection" ), // client/page/site/aliases/index.js:41
__( "You will need to configure your system (DNS and server) to pass requests for these domains to this WordPress install.", "redirection" ), // client/page/site/aliases/index.js:42
__( "Aliased Domain", "redirection" ), // client/page/site/aliases/index.js:47
__( "Alias", "redirection" ), // client/page/site/aliases/index.js:48
__( "No aliases", "redirection" ), // client/page/site/aliases/index.js:64
__( "Add Alias", "redirection" ), // client/page/site/aliases/index.js:68
__( "Don't set a preferred domain - {{code}}%(site)s{{/code}}", "redirection" ), // client/page/site/canonical/index.js:10
__( "Remove www from domain - {{code}}%(siteWWW)s{{/code}} ⇒ {{code}}%(site)s{{/code}}", "redirection" ), // client/page/site/canonical/index.js:21
__( "Add www to domain - {{code}}%(site)s{{/code}} ⇒ {{code}}%(siteWWW)s{{/code}}", "redirection" ), // client/page/site/canonical/index.js:33
__( "Canonical Settings", "redirection" ), // client/page/site/canonical/index.js:85
__( "Force a redirect from HTTP to HTTPS - {{code}}%(site)s{{/code}} ⇒ {{code}}%(siteHTTPS)s{{/code}}", "redirection" ), // client/page/site/canonical/index.js:89
__( "{{strong}}Warning{{/strong}}: ensure your HTTPS is working before forcing a redirect.", "redirection" ), // client/page/site/canonical/index.js:102
__( "Preferred domain", "redirection" ), // client/page/site/canonical/index.js:109
__( "You should update your site URL to match your canonical settings: {{code}}%(current)s{{/code}} ⇒ {{code}}%(site)s{{/code}}", "redirection" ), // client/page/site/canonical/index.js:119
__( "Site", "redirection" ), // client/page/site/headers/header.js:24
__( "Redirect", "redirection" ), // client/page/site/headers/header.js:28
__( "General", "redirection" ), // client/page/site/headers/header.js:225
__( "Custom Header", "redirection" ), // client/page/site/headers/header.js:258
__( "Add Header", "redirection" ), // client/page/site/headers/index.js:17
__( "Add Security Presets", "redirection" ), // client/page/site/headers/index.js:21
__( "Add CORS Presets", "redirection" ), // client/page/site/headers/index.js:25
__( "HTTP Headers", "redirection" ), // client/page/site/headers/index.js:84
__( "Site headers are added across your site, including redirects. Redirect headers are only added to redirects.", "redirection" ), // client/page/site/headers/index.js:85
__( "Location", "redirection" ), // client/page/site/headers/index.js:90
__( "Header", "redirection" ), // client/page/site/headers/index.js:91
__( "No headers", "redirection" ), // client/page/site/headers/index.js:106
__( "Note that some HTTP headers are set by your server and cannot be changed.", "redirection" ), // client/page/site/headers/index.js:117
__( "Permalink Migration", "redirection" ), // client/page/site/permalink/index.js:39
__( "Enter old permalinks structures to automatically migrate them to your current one.", "redirection" ), // client/page/site/permalink/index.js:40
__( "Note: this is in beta and will only migrate posts. Certain permalinks will not work. If yours does not work then you will need to wait until it is out of beta.", "redirection" ), // client/page/site/permalink/index.js:41
__( "Permalinks", "redirection" ), // client/page/site/permalink/index.js:46
__( "No migrated permalinks", "redirection" ), // client/page/site/permalink/index.js:62
__( "Add Permalink", "redirection" ), // client/page/site/permalink/index.js:70
__( "Relocate Site", "redirection" ), // client/page/site/relocate/index.js:31
__( "Want to redirect the entire site? Enter a domain to redirect everything, except WordPress login and admin. Enabling this option will disable any site aliases or canonical settings.", "redirection" ), // client/page/site/relocate/index.js:32
__( "Relocate to domain", "redirection" ), // client/page/site/relocate/index.js:34
__( "Show debug", "redirection" ), // client/wp-plugin-components/error/debug/index.js:70
__( "Debug Information", "redirection" ), // client/wp-plugin-components/error/debug/index.js:80
__( "WordPress did not return a response. This could mean an error occurred or that the request was blocked. Please check your server error_log.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:76
__( "Your REST API is probably being blocked by a security plugin. Please disable this, or configure it to allow REST API requests.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:88
__( "Read this REST API guide for more information.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:94
__( "Your WordPress REST API is returning a 404 page. This is almost certainly an external plugin or server configuration issue.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:105
__( "You will will need to fix this on your site. Redirection is not causing the error.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:111
__( "Can you access your {{api}}REST API{{/api}} without it redirecting?.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:116
__( "Check your {{link}}Site Health{{/link}} and fix any issues.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:123
__( "Your server configuration is blocking access to the REST API.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:129
__( "A security plugin or firewall is blocking access. You will need to whitelist the REST API.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:131
__( "Read this REST API guide for more information.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:138
__( "Your REST API is being redirected. Please remove the redirection for the API.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:146
__( "Your server has rejected the request for being too big. You will need to reconfigure it to continue.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:152
__( "An unknown error occurred.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:160
__( "Your REST API is showing a deprecated PHP error. Please fix this error.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:167
__( "This could be a security plugin, or your server is out of memory or has an external error. Please check your server error log", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:177
__( "Read this REST API guide for more information.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:183
__( "Your WordPress REST API has been disabled. You will need to enable it to continue.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:191
__( "WordPress returned an unexpected message. This could be a PHP error from another plugin, or data inserted by your theme.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:201
__( "Possible cause", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:207
__( "Unable to make request due to browser security. This is typically because your WordPress and Site URL settings are inconsistent, or the request was blocked by your site CORS policy.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:219
__( "Read this REST API guide for more information.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:225
__( "Your REST API appears to be cached and this will cause problems. Please exclude your REST API from your caching system.", "redirection" ), // client/wp-plugin-components/error/decode-error/index.js:237
__( "Bad data", "redirection" ), // client/wp-plugin-components/error/display/error-api.js:17
__( "There was a problem making a request to your site. This could indicate you provided data that did not match requirements, or that the plugin sent a bad request.", "redirection" ), // client/wp-plugin-components/error/display/error-api.js:19
__( "Please review your data and try again.", "redirection" ), // client/wp-plugin-components/error/display/error-api.js:20
__( "REST API 404", "redirection" ), // client/wp-plugin-components/error/display/error-default.js:18
__( "Something went wrong 🙁", "redirection" ), // client/wp-plugin-components/error/display/error-default.js:21
__( "Something went wrong 🙁", "redirection" ), // client/wp-plugin-components/error/display/error-fixed.js:19
__( "Something went wrong 🙁", "redirection" ), // client/wp-plugin-components/error/display/error-known.js:26
__( "You are using an old or cached session", "redirection" ), // client/wp-plugin-components/error/display/error-nonce.js:17
__( "This is usually fixed by doing one of the following:", "redirection" ), // client/wp-plugin-components/error/display/error-nonce.js:19
__( "Reload the page - your current session is old.", "redirection" ), // client/wp-plugin-components/error/display/error-nonce.js:21
__( "Log out, clear your browser cache, and log in again - your browser has cached an old session.", "redirection" ), // client/wp-plugin-components/error/display/error-nonce.js:23
__( "Your admin pages are being cached. Clear this cache and try again. There may be multiple caches involved.", "redirection" ), // client/wp-plugin-components/error/display/error-nonce.js:28
__( "All", "redirection" ), // client/page/site/headers/types/multi-choice.js:26
__( "Values", "redirection" ), // client/page/site/headers/types/multi-choice.js:29
__( "Value", "redirection" ), // client/page/site/headers/types/plain-value.js:10
);
/* THIS IS THE END OF THE GENERATED FILE */

View File

@@ -0,0 +1,5 @@
<?php
define( 'REDIRECTION_VERSION', '5.5.2' );
define( 'REDIRECTION_MIN_WP', '6.4' );
define( 'REDIRECTION_BUILD', '80472d8a5661430170ee21ed7eeb5882' );

View File

@@ -0,0 +1,68 @@
.react-error h1, .react-loading h1 {
text-align: center;
color: #999;
margin-top: 150px;
}
.react-loading {
position: absolute;
width: 100%;
height: 100%;
}
.react-loading-spinner {
position: absolute;
width: 120px;
height: 120px;
left: 50%;
margin-left: -65px;
background-color: #333;
border-radius: 100%;
-webkit-animation: sk-scaleout-loading 1.0s infinite ease-in-out;
animation: sk-scaleout-loading 1.0s infinite ease-in-out;
}
.react-error p {
text-align: center;
line-height: 1;
}
.react-error pre {
border: 1px solid #aaa;
background-color: white;
padding: 10px;
margin: 0 auto;
width: 600px;
}
p.versions {
text-align: left;
width: 600px;
margin: 0 auto;
line-height: 1.6;
color: #666;
font-size: 12px;
background-color: white;
padding: 10px;
border: 1px solid #ddd;
}
@-webkit-keyframes sk-scaleout-loading {
0% { -webkit-transform: scale(0) }
100% {
-webkit-transform: scale(1.0);
opacity: 0;
}
}
@keyframes sk-scaleout-loading {
0% {
-webkit-transform: scale(0);
transform: scale(0);
} 100% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
opacity: 0;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,100 @@
<?php
/*
Plugin Name: Redirection
Plugin URI: https://redirection.me/
Description: Manage all your 301 redirects and monitor 404 errors
Version: 5.5.2
Author: John Godley
Text Domain: redirection
Requires PHP: 7.0
Requires at least: 6.4
============================================================================================================
This software is provided "as is" and any express or implied warranties, including, but not limited to, the
implied warranties of merchantibility and fitness for a particular purpose are disclaimed. In no event shall
the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or
consequential damages(including, but not limited to, procurement of substitute goods or services; loss of
use, data, or profits; or business interruption) however caused and on any theory of liability, whether in
contract, strict liability, or tort(including negligence or otherwise) arising in any way out of the use of
this software, even if advised of the possibility of such damage.
For full license details see license.txt
============================================================================================================
*/
define( 'REDIRECTION_DB_VERSION', '4.2' ); // DB schema version. Only change if DB needs changing
define( 'REDIRECTION_FILE', __FILE__ );
define( 'REDIRECTION_DEV_MODE', false );
if ( ! defined( 'REDIRECTION_FLYING_SOLO' ) ) {
define( 'REDIRECTION_FLYING_SOLO', apply_filters( 'redirection_flying_solo', true ) );
}
// This file must support PHP < 5.6 so as not to crash
if ( version_compare( phpversion(), '5.6' ) < 0 ) {
add_action( 'plugin_action_links_' . basename( dirname( REDIRECTION_FILE ) ) . '/' . basename( REDIRECTION_FILE ), 'red_deprecated_php', 10, 4 );
function red_deprecated_php( $links ) {
/* translators: 1: server PHP version. 2: required PHP version. */
array_unshift( $links, '<a href="https://redirection.me/support/problems/php-version/" style="color: red; text-decoration: underline">' . sprintf( __( 'Disabled! Detected PHP %1$s, need PHP %2$s+', 'redirection' ), phpversion(), '5.6' ) . '</a>' );
return $links;
}
return;
}
require_once __DIR__ . '/redirection-version.php';
require_once __DIR__ . '/redirection-settings.php';
require_once __DIR__ . '/models/redirect/redirect.php';
require_once __DIR__ . '/models/url/url.php';
require_once __DIR__ . '/models/regex.php';
require_once __DIR__ . '/models/module.php';
require_once __DIR__ . '/models/log/log.php';
require_once __DIR__ . '/models/flusher.php';
require_once __DIR__ . '/models/match.php';
require_once __DIR__ . '/models/action.php';
require_once __DIR__ . '/models/request.php';
require_once __DIR__ . '/models/header.php';
function red_is_wpcli() {
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return true;
}
return false;
}
function red_is_admin() {
if ( is_admin() ) {
return true;
}
return false;
}
function red_start_rest() {
require_once __DIR__ . '/redirection-admin.php';
require_once __DIR__ . '/api/api.php';
Redirection_Api::init();
Redirection_Admin::init();
remove_action( 'rest_api_init', 'red_start_rest' );
}
function redirection_locale() {
load_plugin_textdomain( 'redirection', false, dirname( plugin_basename( REDIRECTION_FILE ) ) . '/locale/' );
}
if ( red_is_admin() || red_is_wpcli() ) {
require_once __DIR__ . '/redirection-admin.php';
require_once __DIR__ . '/api/api.php';
} else {
require_once __DIR__ . '/redirection-front.php';
}
if ( red_is_wpcli() ) {
require_once __DIR__ . '/redirection-cli.php';
}
add_action( 'rest_api_init', 'red_start_rest' );
add_action( 'init', 'redirection_locale' );