initial
This commit is contained in:
@@ -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' );
|
||||
}
|
||||
}
|
||||
@@ -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' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/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' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/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' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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 ) {
|
||||
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' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?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
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @phpstan-type Log404Response array{
|
||||
* items: list<array<string, mixed>|object>,
|
||||
* total: int
|
||||
* }
|
||||
*
|
||||
* 404 API endpoint
|
||||
*/
|
||||
class Redirection_Api_404 extends Redirection_Api_Filter_Route {
|
||||
/**
|
||||
* 404 API endpoint constructor
|
||||
*
|
||||
* @param non-falsy-string $api_namespace Namespace.
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
$orders = [ 'url', 'ip', 'total', 'count', '' ];
|
||||
$filters = [ 'ip', 'url-exact', 'referrer', 'agent', 'url', 'domain', 'method', 'http' ];
|
||||
|
||||
// GET /404 - List 404 logs
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/404',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_404' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
'args' => $this->get_filter_args( $orders, $filters ),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /bulk/404/:bulk - Bulk delete 404 logs
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/bulk/404/(?P<bulk>delete)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_bulk' ],
|
||||
'permission_callback' => [ $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<array<string, mixed>> $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<array<string, mixed>> $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<array<string, mixed>> $request The request.
|
||||
* @return Log404Response
|
||||
*/
|
||||
public function route_404( WP_REST_Request $request ) {
|
||||
return $this->get_404( $request->get_params() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform action on 404s
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request The request.
|
||||
* @return Log404Response
|
||||
*/
|
||||
public function route_bulk( WP_REST_Request $request ) {
|
||||
$params = $request->get_params();
|
||||
|
||||
if ( isset( $params['items'] ) && is_array( $params['items'] ) && count( $params['items'] ) > 0 ) {
|
||||
$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'] !== false ) {
|
||||
Red_404_Log::delete_all( $params );
|
||||
}
|
||||
|
||||
return $this->get_404( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 404 log
|
||||
*
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @return Log404Response
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
class Redirection_Api {
|
||||
/**
|
||||
* @var Redirection_Api|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* @var array<int, Redirection_Api_Route>
|
||||
* @phpstan-ignore property.onlyWritten
|
||||
*/
|
||||
private $routes = array();
|
||||
|
||||
/**
|
||||
* @return Redirection_Api
|
||||
*/
|
||||
public static function init() {
|
||||
if ( is_null( self::$instance ) ) {
|
||||
self::$instance = new Redirection_Api();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?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 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"
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @phpstan-type ExportResponse array{
|
||||
* data: string,
|
||||
* total: int
|
||||
* }
|
||||
*/
|
||||
class Redirection_Api_Export extends Redirection_Api_Route {
|
||||
/**
|
||||
* Export API endpoint constructor
|
||||
*
|
||||
* @param non-falsy-string $api_namespace Namespace.
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
// GET /export/:module/:format - Export redirects to specified format
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/export/(?P<module>1|2|3|all)/(?P<format>csv|apache|nginx|json)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_export' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has permission to manage import/export
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return bool
|
||||
*/
|
||||
public function permission_callback_manage( WP_REST_Request $request ) {
|
||||
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Export redirects to a specified format
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return ExportResponse|WP_Error
|
||||
*/
|
||||
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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
class Redirection_Api_Filter_Route extends Redirection_Api_Route {
|
||||
/**
|
||||
* Validate filter param against allowed fields
|
||||
*
|
||||
* @param mixed $value Provided filter value.
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @param string $param Param name.
|
||||
*
|
||||
* @phpstan-param mixed $value
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @phpstan-param string $param
|
||||
* @phpstan-return true|WP_Error
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function validate_filter( $value, $request, $param ) {
|
||||
$attrs = $request->get_attributes();
|
||||
/** @var array<string, mixed> $attrs */
|
||||
$fields = [];
|
||||
if ( isset( $attrs['args'] ) && is_array( $attrs['args'] ) ) {
|
||||
$args = $attrs['args'];
|
||||
if ( isset( $args['filterBy'] ) && is_array( $args['filterBy'] ) ) {
|
||||
$filter_by = $args['filterBy'];
|
||||
if ( isset( $filter_by['filter_fields'] ) && is_array( $filter_by['filter_fields'] ) ) {
|
||||
/** @var list<string> $fields */
|
||||
$fields = array_values( array_map( 'strval', $filter_by['filter_fields'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An empty string means no filter was set (client sent filterBy= with nothing) — normalize to an empty array
|
||||
if ( $value === '' ) {
|
||||
$request->set_param( $param, [] );
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! is_array( $value ) ) {
|
||||
return new WP_Error( 'rest_invalid_param', 'Filter is not an array', array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $value */
|
||||
|
||||
if ( count( $fields ) > 0 ) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build shared filter args
|
||||
*
|
||||
* @param list<string> $order_fields Fields allowed for orderby.
|
||||
* @param list<string> $filters Fields allowed to filter by.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function get_filter_args( $order_fields, $filters = [] ) {
|
||||
// Safety check: ensure Red_Item class is loaded (prevents fatal errors during incomplete plugin updates)
|
||||
// Use a fallback value if the class doesn't exist
|
||||
$max_per_page = 200; // Default fallback value
|
||||
if ( class_exists( 'Red_Item' ) ) {
|
||||
$max_per_page = Red_Item::MAX_PER_PAGE;
|
||||
}
|
||||
|
||||
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' => $max_per_page,
|
||||
],
|
||||
'page' => [
|
||||
'description' => 'Page offset',
|
||||
'type' => 'integer',
|
||||
'minimum' => 0,
|
||||
'default' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a bulk action endpoint for a resource
|
||||
*
|
||||
* @param non-falsy-string $namespace REST namespace.
|
||||
* @param non-falsy-string $route REST route.
|
||||
* @param list<string> $orders Allowed order fields.
|
||||
* @param list<string> $filters Allowed filter fields.
|
||||
* @param string $callback Handler method on this class.
|
||||
* @param callable|array{0:self,1:string}|false $permissions Permission callback or false for default.
|
||||
* @return void
|
||||
*/
|
||||
public function register_bulk( $namespace, $route, $orders, $filters, $callback, $permissions = false ) {
|
||||
register_rest_route(
|
||||
$namespace,
|
||||
$route,
|
||||
array_merge(
|
||||
$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',
|
||||
],
|
||||
],
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
<?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
|
||||
*
|
||||
* @phpstan-type GroupListResponse array{
|
||||
* items: list<array<string, mixed>>,
|
||||
* total: int
|
||||
* }
|
||||
*/
|
||||
class Redirection_Api_Group extends Redirection_Api_Filter_Route {
|
||||
/**
|
||||
* 404 API endpoint constructor
|
||||
*
|
||||
* @param non-falsy-string $api_namespace Namespace.
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
$orders = [ 'name', 'id', '' ];
|
||||
$filters = [ 'status', 'module', 'name' ];
|
||||
|
||||
// GET /group - List groups
|
||||
// POST /group - Create group
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/group',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_list' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
'args' => $this->get_filter_args( $orders, $filters ),
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_create' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_add' ],
|
||||
'args' => array_merge(
|
||||
$this->get_filter_args( $orders, $filters ),
|
||||
$this->get_group_args()
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /group/:id - Update group
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/group/(?P<id>[\d]+)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_update' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_add' ],
|
||||
'args' => $this->get_group_args(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /bulk/group/:bulk - Bulk actions on groups
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/bulk/group/(?P<bulk>delete|enable|disable)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_bulk' ],
|
||||
'permission_callback' => [ $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<array<string, mixed>> $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<array<string, mixed>> $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<array<string, mixed>> $request Request.
|
||||
* @return bool
|
||||
*/
|
||||
public function permission_callback_add( WP_REST_Request $request ) {
|
||||
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_GROUP_ADD );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
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<array<string, mixed>> $request The request.
|
||||
* @return GroupListResponse
|
||||
*/
|
||||
public function route_list( WP_REST_Request $request ) {
|
||||
return Red_Group::get_filtered( $request->get_params() ); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request The request.
|
||||
* @return GroupListResponse|WP_Error
|
||||
*/
|
||||
public function route_create( WP_REST_Request $request ) {
|
||||
$params = $request->get_params();
|
||||
$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 !== false ) {
|
||||
return Red_Group::get_filtered( $params ); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
return $this->add_error_details( new WP_Error( 'redirect_group_invalid', 'Invalid group or parameters' ), __LINE__ );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a group
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request The request.
|
||||
* @return array{item: array<string, mixed>}|WP_Error
|
||||
*/
|
||||
public function route_update( WP_REST_Request $request ) {
|
||||
$params = $request->get_params();
|
||||
$group = Red_Group::get( intval( $request['id'], 10 ) );
|
||||
|
||||
if ( $group !== false ) {
|
||||
$result = $group->update( $params );
|
||||
|
||||
if ( $result !== false ) {
|
||||
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<array<string, mixed>> $request The request.
|
||||
* @return GroupListResponse|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'] !== false ) {
|
||||
// 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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?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"
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @phpstan-type ImportPluginPayload array{
|
||||
* plugin?: string|list<string>
|
||||
* }
|
||||
* @phpstan-type ImportFileParams array{
|
||||
* file?: array{
|
||||
* tmp_name: string,
|
||||
* name: string,
|
||||
* size: int,
|
||||
* type: string,
|
||||
* error: int
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
class Redirection_Api_Import extends Redirection_Api_Route {
|
||||
/**
|
||||
* @param non-falsy-string $api_namespace REST namespace.
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
// POST /import/file/:group_id - Import from file upload
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/import/file/(?P<group_id>\d+)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_import_file' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// GET/POST /import/plugin - List or import from plugins
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/import/plugin',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_plugin_import_list' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_plugin_import' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback used for import routes.
|
||||
*
|
||||
* @param WP_REST_Request $_request Request (unused).
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $_request
|
||||
* @return bool
|
||||
*/
|
||||
public function permission_callback_manage( WP_REST_Request $_request ) {
|
||||
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE );
|
||||
}
|
||||
|
||||
/**
|
||||
* List available plugin importers.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @phpstan-return array{importers: array<int, mixed>}
|
||||
* @return array{importers: array}
|
||||
*/
|
||||
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() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Import redirects using selected plugin importers.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @phpstan-return array{imported: int}|WP_Error
|
||||
* @return array{imported: int}|WP_Error
|
||||
*/
|
||||
public function route_plugin_import( WP_REST_Request $request ) {
|
||||
include_once dirname( __DIR__ ) . '/models/importer.php';
|
||||
|
||||
$params = $request->get_params();
|
||||
/** @var ImportPluginPayload $params */
|
||||
$groups = Red_Group::get_all();
|
||||
/** @var array<array{id: int, name: string, redirects: int, module_id: int, moduleName: string, enabled: bool, default?: bool}> $groups */
|
||||
$plugin_param = $params['plugin'] ?? $request->get_param( 'plugin' );
|
||||
if ( is_array( $plugin_param ) ) {
|
||||
$plugins = array_map( 'strval', $plugin_param );
|
||||
} elseif ( $plugin_param === null ) {
|
||||
$plugins = [];
|
||||
} else {
|
||||
$plugins = [ (string) $plugin_param ];
|
||||
}
|
||||
/** @var list<string> $plugins */
|
||||
$plugins = array_map( 'sanitize_text_field', $plugins );
|
||||
$total = 0;
|
||||
|
||||
if ( count( $groups ) === 0 ) {
|
||||
return $this->add_error_details(
|
||||
new WP_Error( 'redirect_import_invalid_group', 'No groups are available for import' ),
|
||||
__LINE__
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $plugins as $plugin ) {
|
||||
$total += Red_Plugin_Importer::import( $plugin, $groups[0]['id'] );
|
||||
}
|
||||
|
||||
return [ 'imported' => $total ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Import redirects from an uploaded file.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @phpstan-return array{imported: int}|WP_Error
|
||||
* @return array{imported: int}|WP_Error
|
||||
*/
|
||||
public function route_import_file( WP_REST_Request $request ) {
|
||||
$file_params = $request->get_file_params();
|
||||
/** @var ImportFileParams $file_params */
|
||||
$group_id = intval( $request['group_id'], 10 );
|
||||
|
||||
if ( ! isset( $file_params['file'] ) || ! is_uploaded_file( $file_params['file']['tmp_name'] ) ) {
|
||||
return $this->add_error_details( new WP_Error( 'redirect_import_invalid_file', 'Invalid file upload' ), __LINE__ );
|
||||
}
|
||||
|
||||
$upload = $file_params['file'];
|
||||
$parts = pathinfo( $upload['name'] );
|
||||
$extension = isset( $parts['extension'] ) ? strtolower( $parts['extension'] ) : '';
|
||||
|
||||
// JSON imports don't need a group, but all other formats do
|
||||
if ( $extension !== 'json' ) {
|
||||
$group = Red_Group::get( $group_id );
|
||||
if ( $group === false ) {
|
||||
return $this->add_error_details( new WP_Error( 'redirect_import_invalid_group', 'Invalid group' ), __LINE__ );
|
||||
}
|
||||
}
|
||||
|
||||
$count = Red_FileIO::import( $group_id, $upload );
|
||||
|
||||
// Import failure returns 0, but 0 can also mean no valid redirects in file
|
||||
// For JSON files, pre-validate to distinguish between invalid JSON and empty/no-redirects
|
||||
if ( $count === 0 && $extension === 'json' ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file read
|
||||
$content = file_get_contents( $upload['tmp_name'] );
|
||||
if ( $content !== false ) {
|
||||
json_decode( $content, true );
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return $this->add_error_details(
|
||||
new WP_Error( 'redirect_import_invalid_json', 'Invalid JSON file: ' . json_last_error_msg() ),
|
||||
__LINE__
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'imported' => $count,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?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
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @phpstan-type RedirectLogResponse array{
|
||||
* items: list<array<string, mixed>|object>,
|
||||
* total: int
|
||||
* }
|
||||
*
|
||||
* Log API endpoint
|
||||
*/
|
||||
class Redirection_Api_Log extends Redirection_Api_Filter_Route {
|
||||
/**
|
||||
* Log API endpoint constructor
|
||||
*
|
||||
* @param non-falsy-string $api_namespace Namespace.
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
$orders = [ 'url', 'ip', 'total', 'count', '' ];
|
||||
$filters = [ 'ip', 'url-exact', 'referrer', 'agent', 'url', 'target', 'domain', 'method', 'http', 'redirect_by' ];
|
||||
|
||||
// GET /log - Get redirect logs
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/log',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_log' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
'args' => $this->get_filter_args( $orders, $filters ),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /bulk/log/:bulk - Bulk delete logs
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/bulk/log/(?P<bulk>delete)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_bulk' ],
|
||||
'permission_callback' => [ $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<array<string, mixed>> $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<array<string, mixed>> $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<array<string, mixed>> $request The request.
|
||||
* @return RedirectLogResponse
|
||||
*/
|
||||
public function route_log( WP_REST_Request $request ) {
|
||||
return $this->get_logs( $request->get_params() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform bulk action on logs
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request The request.
|
||||
* @return RedirectLogResponse
|
||||
*/
|
||||
public function route_bulk( WP_REST_Request $request ) {
|
||||
$params = $request->get_params();
|
||||
|
||||
if ( isset( $params['items'] ) && is_array( $params['items'] ) && count( $params['items'] ) > 0 ) {
|
||||
$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 ] ] ); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
} elseif ( isset( $params['global'] ) && $params['global'] !== false ) {
|
||||
Red_Redirect_Log::delete_all( $params );
|
||||
}
|
||||
|
||||
return $this->route_log( $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirect logs
|
||||
*
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @return RedirectLogResponse
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 'Plugin' functions for Redirection
|
||||
*
|
||||
* @phpstan-import-type DatabaseStatus from Red_Database_Status
|
||||
* @phpstan-import-type FixerJson from Red_Fixer
|
||||
*/
|
||||
class Redirection_Api_Plugin extends Redirection_Api_Route {
|
||||
/**
|
||||
* Register REST routes for plugin actions
|
||||
*
|
||||
* @param non-falsy-string $api_namespace REST namespace.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
// GET/POST /plugin - Get plugin status or run fixer
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/plugin',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_status' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_fixit' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
'args' => [
|
||||
'name' => [
|
||||
'description' => 'Name',
|
||||
'type' => 'string',
|
||||
],
|
||||
'value' => [
|
||||
'description' => 'Value',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /plugin/delete - Delete plugin data
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/plugin/delete',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_delete' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// ALL /plugin/test - Test endpoint
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/plugin/test',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::ALLMETHODS,
|
||||
'callback' => [ $this, 'route_test' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_setup' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /plugin/data - Database upgrade/status
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/plugin/data',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_database' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_setup' ],
|
||||
'args' => [
|
||||
'upgrade' => [
|
||||
'description' => 'Upgrade parameter',
|
||||
'type' => 'string',
|
||||
'enum' => [
|
||||
'stop',
|
||||
'skip',
|
||||
'retry',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /plugin/finish - Finish setup wizard
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/plugin/finish',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_finish' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_setup' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /plugin/fix - Fix database status
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/plugin/fix',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_fix_status' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_setup' ],
|
||||
'args' => [
|
||||
'reason' => [
|
||||
'description' => 'Reason for fix',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
'current' => [
|
||||
'description' => 'Current version',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback for manage-level actions
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return bool
|
||||
*/
|
||||
public function permission_callback_manage( WP_REST_Request $request ) {
|
||||
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_SUPPORT_MANAGE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback for setup and database upgrade actions
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return bool
|
||||
*/
|
||||
public function permission_callback_setup( WP_REST_Request $request ) {
|
||||
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_OPTION_MANAGE ) ||
|
||||
$this->permission_callback_manage( $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fixer status/debug details
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return FixerJson
|
||||
*/
|
||||
public function route_status( WP_REST_Request $request ) {
|
||||
include_once dirname( REDIRECTION_FILE ) . '/models/fixer.php';
|
||||
|
||||
$fixer = new Red_Fixer();
|
||||
return $fixer->get_json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run fixer or save a specific debug setting
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return FixerJson|WP_Error
|
||||
*/
|
||||
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 ) {
|
||||
$group = Red_Group::create( __( 'Redirections', 'redirection' ), 1 );
|
||||
|
||||
if ( $group === false ) {
|
||||
return $this->add_error_details( new WP_Error( 'redirect_group_create_failed', 'Unable to create group' ), __LINE__ );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result = $fixer->fix( $fixer->get_status() );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $this->add_error_details( $result, __LINE__ );
|
||||
}
|
||||
}
|
||||
|
||||
return $fixer->get_json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete plugin (single-site only) and redirect back to plugins page
|
||||
*
|
||||
* @return array{location: string}|WP_Error
|
||||
*/
|
||||
public function route_delete() {
|
||||
if ( is_multisite() ) {
|
||||
return new WP_Error( 'redirect_delete_multi', 'Multisite installations must delete the plugin from the network admin' );
|
||||
}
|
||||
|
||||
Redirection_Admin::plugin_uninstall();
|
||||
|
||||
$current = get_option( 'active_plugins' );
|
||||
$plugin_position = array_search( basename( dirname( REDIRECTION_FILE ) ) . '/' . basename( REDIRECTION_FILE ), $current, true );
|
||||
if ( $plugin_position !== false ) {
|
||||
array_splice( $current, (int) $plugin_position, 1 );
|
||||
update_option( 'active_plugins', $current );
|
||||
}
|
||||
|
||||
return array( 'location' => admin_url() . 'plugins.php' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple plugin test endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return array{success: true}
|
||||
*/
|
||||
public function route_test( WP_REST_Request $request ) {
|
||||
return array(
|
||||
'success' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Database status/upgrade orchestration
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return DatabaseStatus
|
||||
*/
|
||||
public function route_database( WP_REST_Request $request ) {
|
||||
$params = $request->get_params();
|
||||
$status = new Red_Database_Status();
|
||||
/** @var string|false $upgrade */
|
||||
$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();
|
||||
}
|
||||
|
||||
$should_upgrade = $upgrade === false || $status->get_current_stage() !== false;
|
||||
if ( $should_upgrade ) {
|
||||
$database = new Red_Database();
|
||||
$database->apply_upgrade( $status );
|
||||
}
|
||||
|
||||
return $status->get_json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish setup wizard
|
||||
*
|
||||
* @return array{success: true}
|
||||
*/
|
||||
public function route_finish() {
|
||||
$status = new Red_Database_Status();
|
||||
$status->finish();
|
||||
|
||||
return array( 'success' => true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix database status after manual upgrade
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return array{success: true, database: DatabaseStatus}|WP_Error
|
||||
*/
|
||||
public function route_fix_status( WP_REST_Request $request ) {
|
||||
global $wpdb;
|
||||
|
||||
$params = $request->get_params();
|
||||
$reason = isset( $params['reason'] ) ? sanitize_text_field( $params['reason'] ) : '';
|
||||
$current = isset( $params['current'] ) ? sanitize_text_field( $params['current'] ) : '';
|
||||
|
||||
// Validate required parameters
|
||||
if ( $reason === '' ) {
|
||||
return $this->add_error_details(
|
||||
new WP_Error( 'redirection_invalid_reason', 'Missing or invalid reason parameter' ),
|
||||
__LINE__
|
||||
);
|
||||
}
|
||||
|
||||
if ( $current === '' ) {
|
||||
return $this->add_error_details(
|
||||
new WP_Error( 'redirection_invalid_version', 'Missing or invalid current version parameter' ),
|
||||
__LINE__
|
||||
);
|
||||
}
|
||||
|
||||
$status = new Red_Database_Status();
|
||||
|
||||
if ( $reason === 'database' ) {
|
||||
$status->save_db_version( $current );
|
||||
|
||||
// After manual database install, ensure default groups are created
|
||||
$latest = Red_Database::get_latest_database();
|
||||
$latest->create_groups( $wpdb, true );
|
||||
|
||||
$status->finish();
|
||||
} else {
|
||||
return $this->add_error_details(
|
||||
new WP_Error( 'redirection_unsupported_reason', "Unsupported reason: $reason" ),
|
||||
__LINE__
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'database' => $status->get_json(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @phpstan-type RedirectListResponse array{
|
||||
* items: list<array<string, mixed>>,
|
||||
* total: int
|
||||
* }
|
||||
*
|
||||
* Redirect API endpoint
|
||||
*/
|
||||
class Redirection_Api_Redirect extends Redirection_Api_Filter_Route {
|
||||
/**
|
||||
* Redirect API endpoint constructor
|
||||
*
|
||||
* @param non-falsy-string $api_namespace Namespace.
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
$orders = [ 'source', 'last_count', 'last_access', 'position', 'id', '' ];
|
||||
$filters = [ 'status', 'url-match', 'match', 'action', 'http', 'access', 'url', 'target', 'title', 'group', 'id' ];
|
||||
|
||||
// GET /redirect - List redirects
|
||||
// POST /redirect - Create redirect
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/redirect',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_list' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
'args' => $this->get_filter_args( $orders, $filters ),
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_create' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_add' ],
|
||||
'args' => $this->get_filter_args( $orders, $filters ),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /redirect/:id - Update redirect
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/redirect/(?P<id>[\d]+)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_update' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_add' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// GET /redirect/post - Search for posts
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/redirect/post',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_match_post' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
'args' => [
|
||||
'text' => [
|
||||
'description' => 'Text to match',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// POST /bulk/redirect/:bulk - Bulk actions on redirects
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/bulk/redirect/(?P<bulk>delete|enable|disable|reset)',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_bulk' ],
|
||||
'permission_callback' => [ $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<array<string, mixed>> $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<array<string, mixed>> $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<array<string, mixed>> $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<array<string, mixed>> $request The request.
|
||||
* @return RedirectListResponse
|
||||
*/
|
||||
public function route_list( WP_REST_Request $request ) {
|
||||
return Red_Item::get_filtered( $request->get_params() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new redirect(s)
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request The request.
|
||||
* @return RedirectListResponse|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<array<string, mixed>> $request The request.
|
||||
* @return array{item: array<string, mixed>}|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 !== false ) {
|
||||
$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<array<string, mixed>> $request The request.
|
||||
* @return RedirectListResponse|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'] ) && count( $params['items'] ) > 0 ) {
|
||||
$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'] !== false ) {
|
||||
// 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<array<string, mixed>> $request The request.
|
||||
* @return list<array{title: string, value: string|false}>
|
||||
*/
|
||||
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 ) !== false ) {
|
||||
$title = $post->post_title;
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'title' => $title,
|
||||
'value' => get_permalink( $post->ID ),
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
class Redirection_Api_Route {
|
||||
/**
|
||||
* Add DB error details to a WP_Error
|
||||
*
|
||||
* @param WP_Error $error Error instance.
|
||||
* @param string|int $line Error code/identifier.
|
||||
* @param int $code HTTP status code.
|
||||
* @return WP_Error
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default permission callback for API routes
|
||||
*
|
||||
* @param WP_REST_Request $request REST request.
|
||||
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return bool
|
||||
*/
|
||||
public function permission_callback( WP_REST_Request $request ) {
|
||||
return Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_PLUGIN );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a REST route config
|
||||
*
|
||||
* @param mixed $method Allowed methods (WP_REST_Server constants).
|
||||
* @param string $callback Method name on this class.
|
||||
* @param callable|array{0:self,1:string}|false $permissions Permission callback or false for default.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get_route( $method, $callback, $permissions = false ) {
|
||||
return [
|
||||
'methods' => $method,
|
||||
'callback' => [ $this, $callback ],
|
||||
'permission_callback' => $permissions ? $permissions : [ $this, 'permission_callback' ],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?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"
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @phpstan-import-type RedirectionOptions from Red_Options
|
||||
*
|
||||
* @phpstan-type SettingsResponse array{
|
||||
* settings: RedirectionOptions,
|
||||
* groups: array<int, object>,
|
||||
* installed: string,
|
||||
* canDelete: bool,
|
||||
* post_types: array<int|string>
|
||||
* }
|
||||
* @phpstan-type SettingsResponseWithWarning array{
|
||||
* settings: RedirectionOptions,
|
||||
* groups: array<int, object>,
|
||||
* installed: string,
|
||||
* canDelete: bool,
|
||||
* post_types: array<int|string>,
|
||||
* warning?: string
|
||||
* }
|
||||
*/
|
||||
class Redirection_Api_Settings extends Redirection_Api_Route {
|
||||
/**
|
||||
* Settings API endpoint constructor
|
||||
*
|
||||
* @param non-falsy-string $api_namespace Namespace.
|
||||
*/
|
||||
public function __construct( $api_namespace ) {
|
||||
// GET /setting - Get settings
|
||||
// POST /setting - Update settings
|
||||
register_rest_route(
|
||||
$api_namespace,
|
||||
'/setting',
|
||||
[
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'route_settings' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [ $this, 'route_save_settings' ],
|
||||
'permission_callback' => [ $this, 'permission_callback_manage' ],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings for Redirection
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return SettingsResponse
|
||||
*/
|
||||
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_Options::get(),
|
||||
'groups' => $this->groups_to_json( Red_Group::get_for_select() ),
|
||||
'installed' => get_home_path(),
|
||||
'canDelete' => ! is_multisite(),
|
||||
'post_types' => red_get_post_types(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has permission to manage settings
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return bool
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings for Redirection
|
||||
*
|
||||
* @param WP_REST_Request<array<string, mixed>> $request
|
||||
* @return SettingsResponseWithWarning
|
||||
*/
|
||||
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 );
|
||||
if ( $module !== false && $module instanceof Apache_Module ) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert groups array to JSON format
|
||||
*
|
||||
* @param array<string|int, mixed> $groups Groups array from Red_Group::get_for_select()
|
||||
* @param int $depth Current recursion depth
|
||||
* @return array<int, object>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/api-route.php';
|
||||
require_once __DIR__ . '/api-filter-route.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';
|
||||
require_once __DIR__ . '/api-core.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"
|
||||
* }
|
||||
*/
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
define( 'REDIRECTION_VERSION', '5.8.0' );
|
||||
define( 'REDIRECTION_BUILD', 'ccd61875d87555ffa26d4caa63395f50' );
|
||||
define( 'REDIRECTION_MIN_WP', '6.6' );
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-element', 'wp-i18n'), 'version' => '1e46303089c718887d2a');
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
||||
/*!
|
||||
Copyright (c) 2018 Jed Watson.
|
||||
Licensed under the MIT License (MIT), see
|
||||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* use-sync-external-store-with-selector.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
@@ -0,0 +1,573 @@
|
||||
= 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
|
||||
@@ -0,0 +1,569 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type DatabaseStatus array{
|
||||
* status: string|false,
|
||||
* inProgress: bool,
|
||||
* current?: string,
|
||||
* next?: string,
|
||||
* time?: float,
|
||||
* manual?: array<int, string>,
|
||||
* result?: 'ok'|'error',
|
||||
* reason?: string|false,
|
||||
* debug?: array<int, string>,
|
||||
* complete?: int|float
|
||||
* }
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* Current upgrade stage
|
||||
*
|
||||
* @var string|false
|
||||
*/
|
||||
private $stage = false;
|
||||
|
||||
/**
|
||||
* List of all upgrade stages
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private $stages = [];
|
||||
|
||||
/**
|
||||
* Current database status
|
||||
*
|
||||
* @var string|false
|
||||
*/
|
||||
private $status = false;
|
||||
|
||||
/**
|
||||
* Result of last operation
|
||||
*
|
||||
* @var string|false
|
||||
*/
|
||||
private $result = false;
|
||||
|
||||
/**
|
||||
* Reason for current status
|
||||
*
|
||||
* @var string|false
|
||||
*/
|
||||
private $reason = false;
|
||||
|
||||
/**
|
||||
* Debug information
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current upgrade stage from options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function load_stage(): void {
|
||||
$settings = Red_Options::get();
|
||||
|
||||
if ( isset( $settings[ self::DB_UPGRADE_STAGE ] ) ) {
|
||||
$stage_data = $settings[ self::DB_UPGRADE_STAGE ];
|
||||
|
||||
// Database stage can be set to false to clear it - only process if it's an array
|
||||
// phpcs:ignore function.alreadyNarrowedType
|
||||
// @phpstan-ignore function.alreadyNarrowedType
|
||||
if ( ! is_array( $stage_data ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->stage = isset( $stage_data['stage'] ) ? $stage_data['stage'] : false;
|
||||
$this->stages = isset( $stage_data['stages'] ) ? $stage_data['stages'] : [];
|
||||
|
||||
// Only override status if we have a saved upgrade in progress
|
||||
if ( isset( $stage_data['status'] ) && $stage_data['status'] !== false ) {
|
||||
$this->status = $stage_data['status'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the database need install
|
||||
*
|
||||
* @return bool true if needs installing, false otherwise
|
||||
*/
|
||||
public function needs_installing(): bool {
|
||||
$settings = Red_Options::get();
|
||||
|
||||
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(): bool {
|
||||
// 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() !== false && $this->status !== self::STATUS_NEED_INSTALL ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current database version
|
||||
*
|
||||
* @return string Current database version
|
||||
*/
|
||||
public function get_current_version(): string {
|
||||
$settings = Red_Options::get();
|
||||
|
||||
if ( $settings['database'] !== '' ) {
|
||||
if ( $settings['database'] === '+OK' ) {
|
||||
return REDIRECTION_DB_VERSION;
|
||||
}
|
||||
|
||||
return $settings['database'];
|
||||
}
|
||||
|
||||
$version = $this->get_old_version();
|
||||
if ( $version !== false && $version !== '' && $version !== '0' && $version !== 0 ) {
|
||||
// Upgrade the old value
|
||||
$version = (string) $version;
|
||||
red_set_options( array( 'database' => $version ) );
|
||||
delete_option( self::OLD_DB_VERSION );
|
||||
$this->clear_cache();
|
||||
return $version;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old database version from legacy option
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function get_old_version() {
|
||||
return get_option( self::OLD_DB_VERSION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if required database tables exist
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check_tables_exist(): void {
|
||||
$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( self::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( string $version ): bool {
|
||||
return version_compare( $this->get_current_version(), $version, 'ge' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if last operation resulted in error
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_error(): bool {
|
||||
return $this->result === self::RESULT_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set error status
|
||||
*
|
||||
* @param string $error Error message.
|
||||
* @return void
|
||||
*/
|
||||
public function set_error( string $error ): void {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set success status
|
||||
*
|
||||
* @param string $reason Success message.
|
||||
* @return void
|
||||
*/
|
||||
public function set_ok( string $reason ): void {
|
||||
$this->reason = $reason;
|
||||
$this->result = self::RESULT_OK;
|
||||
$this->debug = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop current upgrade
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function stop_update(): void {
|
||||
$this->stage = false;
|
||||
$this->stages = [];
|
||||
$this->debug = [];
|
||||
|
||||
red_set_options( [ self::DB_UPGRADE_STAGE => false ] );
|
||||
$this->clear_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish upgrade process
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finish(): void {
|
||||
$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|false Current stage name, or false if not upgrading
|
||||
*/
|
||||
public function get_current_stage() {
|
||||
return $this->stage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move current stage on to the next
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_next_stage(): void {
|
||||
$this->debug = [];
|
||||
$stage = $this->get_current_stage();
|
||||
|
||||
if ( $stage !== false ) {
|
||||
$stage = $this->get_next_stage( $stage );
|
||||
|
||||
// Save next position
|
||||
if ( $stage !== false ) {
|
||||
$this->set_stage( $stage );
|
||||
} else {
|
||||
$this->finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current upgrade status
|
||||
*
|
||||
* @return DatabaseStatus 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;
|
||||
} elseif ( $this->status === self::STATUS_NEED_INSTALL || $this->status === self::STATUS_NEED_UPDATING ) {
|
||||
// For fresh install/update that hasn't started yet, set initial progress state
|
||||
$result['complete'] = 0;
|
||||
$result['result'] = self::RESULT_OK;
|
||||
$result['reason'] = false;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error status information
|
||||
*
|
||||
* @phpstan-return array{reason: string|false, result: 'error', debug: array<int, string>}
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function get_error_status(): array {
|
||||
return [
|
||||
'reason' => $this->reason,
|
||||
'result' => self::RESULT_ERROR,
|
||||
'debug' => $this->debug,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress status information
|
||||
*
|
||||
* @return array{complete: int|float, result: 'ok', reason: string|false}
|
||||
*/
|
||||
private function get_progress_status() {
|
||||
$complete = 0;
|
||||
|
||||
if ( $this->stage !== false ) {
|
||||
$total = count( $this->stages );
|
||||
$pos = array_search( $this->stage, $this->stages, true );
|
||||
|
||||
if ( $pos !== false && $total > 0 ) {
|
||||
$complete = round( ( $pos / $total ) * 100, 1 );
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'complete' => $complete,
|
||||
'result' => self::RESULT_OK,
|
||||
'reason' => $this->reason,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version upgrade information
|
||||
*
|
||||
* @phpstan-return array{current: string, next: string, time: float}
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function get_version_upgrade(): array {
|
||||
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
|
||||
*
|
||||
* @param Red_Database_Upgrade[] $upgrades List of upgrade versions.
|
||||
* @return void
|
||||
*/
|
||||
public function start_install( array $upgrades ) {
|
||||
$this->set_stages( $upgrades );
|
||||
$this->status = self::STATUS_NEED_INSTALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start database upgrade process
|
||||
*
|
||||
* @param Red_Database_Upgrade[] $upgrades List of upgrade versions.
|
||||
* @return void
|
||||
*/
|
||||
public function start_upgrade( array $upgrades ) {
|
||||
$this->set_stages( $upgrades );
|
||||
$this->status = self::STATUS_NEED_UPDATING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set upgrade stages
|
||||
*
|
||||
* @param Red_Database_Upgrade[] $upgrades List of upgrade versions.
|
||||
* @return void
|
||||
*/
|
||||
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] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|false $stage
|
||||
* @return void
|
||||
*/
|
||||
public function set_stage( $stage ): void {
|
||||
$this->stage = $stage;
|
||||
$this->save_details();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function save_details(): void {
|
||||
$stages = [
|
||||
self::DB_UPGRADE_STAGE => [
|
||||
'stage' => $this->stage,
|
||||
'stages' => $this->stages,
|
||||
'status' => $this->status,
|
||||
],
|
||||
];
|
||||
|
||||
red_set_options( $stages );
|
||||
|
||||
$this->clear_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function get_manual_upgrade(): array {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stage
|
||||
* @return string|false
|
||||
*/
|
||||
private function get_next_stage( string $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 );
|
||||
}
|
||||
|
||||
if ( count( $upgraders ) === 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$upgrader = Red_Database_Upgrader::get( $upgraders[0] );
|
||||
|
||||
// Where are we in this?
|
||||
$pos = array_search( $stage, $this->stages, true );
|
||||
|
||||
if ( $pos === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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() );
|
||||
|
||||
$current_position = array_search( $stage, $current_stages, true );
|
||||
|
||||
if ( $current_position !== false && $current_position === count( $current_stages ) - 1 && isset( $upgraders[1] ) ) {
|
||||
$this->save_db_version( $upgraders[1]->get_version() );
|
||||
}
|
||||
|
||||
// Move on to next in current version
|
||||
return $this->stages[ $pos + 1 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @return void
|
||||
*/
|
||||
public function save_db_version( string $version ): void {
|
||||
red_set_options( array( 'database' => $version ) );
|
||||
delete_option( self::OLD_DB_VERSION );
|
||||
|
||||
$this->clear_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function clear_cache(): void {
|
||||
// Clear Red_Options in-memory cache
|
||||
Red_Options::reset();
|
||||
|
||||
// Clear WordPress object cache if available
|
||||
if ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) && function_exists( 'wp_cache_flush' ) ) {
|
||||
wp_cache_flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
class Red_Database_Upgrade {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $version;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $file;
|
||||
|
||||
/**
|
||||
* @var class-string<Red_Database_Upgrader>
|
||||
*/
|
||||
private $class;
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @param string $file
|
||||
* @param class-string<Red_Database_Upgrader> $class
|
||||
*/
|
||||
public function __construct( string $version, string $file, string $class ) {
|
||||
$this->version = $version;
|
||||
$this->file = $file;
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
public function get_version(): string {
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function get_file(): string {
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Red_Database_Upgrader>
|
||||
*/
|
||||
public function get_class(): string {
|
||||
return $this->class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/database-upgrade.php';
|
||||
|
||||
abstract class Red_Database_Upgrader {
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $queries = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $live = true;
|
||||
|
||||
/**
|
||||
* Return an array of all the stages for an upgrade
|
||||
*
|
||||
* @return array<string, string> stage name => reason
|
||||
*/
|
||||
abstract public function get_stages();
|
||||
|
||||
/**
|
||||
* @param string $stage
|
||||
* @return string
|
||||
*/
|
||||
public function get_reason( string $stage ): string {
|
||||
$stages = $this->get_stages();
|
||||
|
||||
if ( isset( $stages[ $stage ] ) ) {
|
||||
return $stages[ $stage ];
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a particular stage on the current upgrader
|
||||
*
|
||||
* @param Red_Database_Status $status
|
||||
* @return void
|
||||
*/
|
||||
public function perform_stage( Red_Database_Status $status ): void {
|
||||
global $wpdb;
|
||||
|
||||
$stage = $status->get_current_stage();
|
||||
if ( is_string( $stage ) && $this->has_stage( $stage ) && method_exists( $this, $stage ) ) {
|
||||
try {
|
||||
$this->invoke_stage( $stage, $wpdb, true );
|
||||
$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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stage
|
||||
* @return list<string>
|
||||
*/
|
||||
public function get_queries_for_stage( string $stage ): array {
|
||||
global $wpdb;
|
||||
|
||||
$this->queries = [];
|
||||
$this->live = false;
|
||||
$this->invoke_stage( $stage, $wpdb, false );
|
||||
$this->live = true;
|
||||
|
||||
return $this->queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current database charset
|
||||
*
|
||||
* @return string Database charset
|
||||
*/
|
||||
public function get_charset(): string {
|
||||
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
|
||||
*
|
||||
* @param wpdb $wpdb WordPress database instance.
|
||||
* @param string $sql SQL query.
|
||||
* @return bool true if query is performed ok, otherwise an exception is thrown
|
||||
*/
|
||||
protected function do_query( wpdb $wpdb, string $sql ): bool {
|
||||
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 ) ); // phpcs:ignore
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a database upgrader class
|
||||
*
|
||||
* @param Red_Database_Upgrade $version
|
||||
* @return Red_Database_Upgrader Database upgrader
|
||||
*/
|
||||
public static function get( Red_Database_Upgrade $version ): Red_Database_Upgrader {
|
||||
include_once __DIR__ . '/schema/' . str_replace( [ '..', '/' ], '', $version->get_file() );
|
||||
|
||||
$class = $version->get_class();
|
||||
|
||||
return new $class();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stage
|
||||
* @return bool
|
||||
*/
|
||||
private function has_stage( string $stage ): bool {
|
||||
return in_array( $stage, array_keys( $this->get_stages() ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stage
|
||||
* @param wpdb $wpdb
|
||||
* @param bool $live
|
||||
* @return void
|
||||
*/
|
||||
private function invoke_stage( string $stage, wpdb $wpdb, bool $live ): void {
|
||||
if ( ! method_exists( $this, $stage ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Methods that accept both $wpdb and $live parameters
|
||||
// Most stage methods only accept $wpdb, but some (like create_groups) accept both
|
||||
$two_param_methods = [ 'create_groups' ];
|
||||
|
||||
if ( in_array( $stage, $two_param_methods, true ) ) {
|
||||
/** @var callable(wpdb, bool): void $callable */
|
||||
$callable = [ $this, $stage ];
|
||||
call_user_func( $callable, $wpdb, $live );
|
||||
} else {
|
||||
/** @var callable(wpdb): void $callable */
|
||||
$callable = [ $this, $stage ];
|
||||
call_user_func( $callable, $wpdb );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/database-status.php';
|
||||
require_once __DIR__ . '/database-upgrade.php';
|
||||
require_once __DIR__ . '/database-upgrader.php';
|
||||
|
||||
class Red_Database {
|
||||
/**
|
||||
* Get all upgrades for a database version
|
||||
*
|
||||
* @param string $current_version
|
||||
* @param string|false $current_stage
|
||||
* @return list<Red_Database_Upgrade> Array of versions from self::get_upgrades()
|
||||
*/
|
||||
public function get_upgrades_for_version( $current_version, $current_stage ) {
|
||||
if ( empty( $current_version ) ) {
|
||||
return [
|
||||
new Red_Database_Upgrade( REDIRECTION_DB_VERSION, 'latest.php', 'Red_Latest_Database' ),
|
||||
];
|
||||
}
|
||||
|
||||
$upgraders = [];
|
||||
$found = false;
|
||||
|
||||
foreach ( $this->get_upgrades() as $upgrade ) {
|
||||
if ( ! $found ) {
|
||||
$upgrader = Red_Database_Upgrader::get( $upgrade );
|
||||
|
||||
$stage_present = is_string( $current_stage ) && in_array( $current_stage, array_keys( $upgrader->get_stages() ), true );
|
||||
$same_version = $current_stage === false && version_compare( $upgrade->get_version(), $current_version, 'gt' );
|
||||
|
||||
if ( $stage_present || $same_version ) {
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $found ) {
|
||||
$upgraders[] = $upgrade;
|
||||
}
|
||||
}
|
||||
|
||||
return $upgraders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a particular upgrade stage
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a callback to all sites in a multisite network, or to the current site if not multisite.
|
||||
*
|
||||
* @param callable $callback Callback function to apply to each site.
|
||||
* @return void
|
||||
*/
|
||||
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( (int) $site->blog_id );
|
||||
|
||||
$callback();
|
||||
|
||||
restore_current_blog();
|
||||
},
|
||||
get_sites( [ 'number' => $per_page, 'offset' => $offset ] )
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest database installer
|
||||
*
|
||||
* @return Red_Latest_Database Red_Latest_Database
|
||||
*/
|
||||
public static function get_latest_database() {
|
||||
include_once __DIR__ . '/schema/latest.php';
|
||||
|
||||
return new Red_Latest_Database();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all upgrades and their associated file
|
||||
*
|
||||
* @return list<Red_Database_Upgrade> Database upgrade array
|
||||
*/
|
||||
public function get_upgrades() {
|
||||
return [
|
||||
new Red_Database_Upgrade( '2.0.1', '201.php', 'Red_Database_201' ),
|
||||
new Red_Database_Upgrade( '2.1.16', '216.php', 'Red_Database_216' ),
|
||||
new Red_Database_Upgrade( '2.2', '220.php', 'Red_Database_220' ),
|
||||
new Red_Database_Upgrade( '2.3.1', '231.php', 'Red_Database_231' ),
|
||||
new Red_Database_Upgrade( '2.3.2', '232.php', 'Red_Database_232' ),
|
||||
new Red_Database_Upgrade( '2.3.3', '233.php', 'Red_Database_233' ),
|
||||
new Red_Database_Upgrade( '2.4', '240.php', 'Red_Database_240' ),
|
||||
new Red_Database_Upgrade( '4.0', '400.php', 'Red_Database_400' ),
|
||||
new Red_Database_Upgrade( '4.1', '410.php', 'Red_Database_410' ),
|
||||
new Red_Database_Upgrade( '4.2', '420.php', 'Red_Database_420' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
// Note: not localised as the messages aren't important enough
|
||||
class Red_Database_201 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_stages() {
|
||||
return [
|
||||
'add_title_201' => 'Add titles to redirects',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
protected function add_title_201( $wpdb ) {
|
||||
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` ADD `title` varchar(50) NULL" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
// Note: not localised as the messages aren't important enough
|
||||
class Red_Database_216 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_stages() {
|
||||
return [
|
||||
'add_group_indices_216' => 'Add indices to groups',
|
||||
'add_redirect_indices_216' => 'Add indices to redirects',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
// Note: not localised as the messages aren't important enough
|
||||
class Red_Database_220 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_stages() {
|
||||
return [
|
||||
'add_group_indices_220' => 'Add group indices to redirects',
|
||||
'add_log_indices_220' => 'Add indices to logs',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
// Note: not localised as the messages aren't important enough
|
||||
class Red_Database_231 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_stages() {
|
||||
return [
|
||||
'remove_404_module_231' => 'Remove 404 module',
|
||||
'create_404_table_231' => 'Create 404 table',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
protected function create_404_table_231( $wpdb ) {
|
||||
$this->do_query( $wpdb, $this->get_404_table( $wpdb ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return string
|
||||
*/
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
// Note: not localised as the messages aren't important enough
|
||||
class Red_Database_232 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_stages() {
|
||||
return [
|
||||
'remove_modules_232' => 'Remove module table',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
protected function remove_modules_232( $wpdb ) {
|
||||
$this->do_query( $wpdb, "DROP TABLE IF EXISTS {$wpdb->prefix}redirection_modules" );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
// Note: not localised as the messages aren't important enough
|
||||
class Red_Database_233 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_stages() {
|
||||
return [
|
||||
'fix_invalid_groups_233' => 'Migrate any groups with invalid module ID',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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 {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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`)" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
protected function convert_title_to_text_240( $wpdb ) {
|
||||
return $this->do_query( $wpdb, "ALTER TABLE `{$wpdb->prefix}redirection_items` CHANGE `title` `title` text" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
class Red_Database_400 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @param string $column
|
||||
* @return bool
|
||||
*/
|
||||
private function has_column( $wpdb, $column ) {
|
||||
$wpdb->hide_errors();
|
||||
$wpdb->suppress_errors( true );
|
||||
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_items`", ARRAY_N );
|
||||
$wpdb->show_errors();
|
||||
$wpdb->suppress_errors( false );
|
||||
|
||||
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), strtolower( $column ) ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
private function has_match_index( $wpdb ) {
|
||||
$wpdb->hide_errors();
|
||||
$wpdb->suppress_errors( true );
|
||||
$existing = $wpdb->get_row( "SHOW CREATE TABLE `{$wpdb->prefix}redirection_items`", ARRAY_N );
|
||||
$wpdb->show_errors();
|
||||
$wpdb->suppress_errors( false );
|
||||
|
||||
if ( isset( $existing[1] ) && strpos( strtolower( $existing[1] ), 'key `match_url' ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool|void
|
||||
*/
|
||||
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))" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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
|
||||
|
||||
$table_name = $wpdb->prefix . 'redirection_items';
|
||||
// phpstan requires literal-string but table prefix must be dynamic for WordPress multisite
|
||||
/** @phpstan-ignore argument.type */
|
||||
$prepared = $wpdb->prepare( "UPDATE `{$table_name}` SET match_data=%s WHERE regex=0 AND match_url LIKE %s", '{"source":{"flag_query":"exactorder"}}', '%?%' ); // phpcs:ignore
|
||||
if ( $prepared !== null ) {
|
||||
$this->do_query( $wpdb, $prepared );
|
||||
}
|
||||
|
||||
// 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=''" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
class Red_Database_410 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_stages() {
|
||||
return [
|
||||
'handle_double_slash' => 'Support double-slash URLs',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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=''" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
class Red_Database_420 extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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`" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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`" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
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" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Latest database schema
|
||||
*/
|
||||
class Red_Latest_Database extends Red_Database_Upgrader {
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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
|
||||
*/
|
||||
/**
|
||||
* @return bool|\WP_Error
|
||||
*/
|
||||
public function install() {
|
||||
global $wpdb;
|
||||
|
||||
foreach ( $this->get_stages() as $stage => $info ) {
|
||||
// @phpstan-ignore method.dynamicName
|
||||
$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)
|
||||
*/
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
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
|
||||
*/
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
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
|
||||
*/
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function get_table_schema() {
|
||||
global $wpdb;
|
||||
|
||||
$tables = array_keys( $this->get_all_tables() );
|
||||
$show = array();
|
||||
|
||||
foreach ( $tables as $table ) {
|
||||
// Suppress errors when checking for table existence
|
||||
$wpdb->hide_errors();
|
||||
$wpdb->suppress_errors( true );
|
||||
// These are known queries without user input
|
||||
// phpcs:ignore
|
||||
$row = $wpdb->get_row( 'SHOW CREATE TABLE ' . $table, ARRAY_N );
|
||||
$wpdb->show_errors();
|
||||
$wpdb->suppress_errors( false );
|
||||
|
||||
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
|
||||
*/
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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
|
||||
*/
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @param bool $is_live
|
||||
* @return bool
|
||||
*/
|
||||
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 !== null ) {
|
||||
red_set_options( array( 'last_group_id' => $group->id ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all the tables
|
||||
*/
|
||||
/**
|
||||
* @param \wpdb $wpdb
|
||||
* @return bool
|
||||
*/
|
||||
public function create_tables( $wpdb ) {
|
||||
global $wpdb;
|
||||
|
||||
foreach ( $this->get_all_tables() as $table => $sql ) {
|
||||
$sql = (string) preg_replace( '/[ \t]{2,}/', '', $sql );
|
||||
$this->do_query( $wpdb, $sql );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prefix
|
||||
* @param string $charset_collate
|
||||
* @return string
|
||||
*/
|
||||
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 DEFAULT NULL,
|
||||
`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 DEFAULT NULL,
|
||||
`match_type` VARCHAR(20) NOT NULL,
|
||||
`title` TEXT DEFAULT NULL,
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prefix
|
||||
* @param string $charset_collate
|
||||
* @return string
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prefix
|
||||
* @param string $charset_collate
|
||||
* @return string
|
||||
*/
|
||||
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 DEFAULT NULL,
|
||||
`agent` MEDIUMTEXT DEFAULT NULL,
|
||||
`referrer` MEDIUMTEXT DEFAULT NULL,
|
||||
`http_code` INT(11) unsigned NOT NULL DEFAULT 0,
|
||||
`request_method` VARCHAR(10) DEFAULT NULL,
|
||||
`request_data` MEDIUMTEXT DEFAULT NULL,
|
||||
`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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prefix
|
||||
* @param string $charset_collate
|
||||
* @return string
|
||||
*/
|
||||
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 DEFAULT NULL,
|
||||
`ip` VARCHAR(45) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `created` (`created`),
|
||||
KEY `referrer` (`referrer`(191)),
|
||||
KEY `ip` (`ip`)
|
||||
) $charset_collate";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type GroupJson from Red_Group
|
||||
*/
|
||||
|
||||
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' ) . '"' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Red_Item> $items
|
||||
* @param array<GroupJson> $groups
|
||||
* @return string
|
||||
*/
|
||||
public function get_data( array $items, array $groups ) {
|
||||
include_once dirname( __DIR__ ) . '/models/htaccess.php';
|
||||
|
||||
$htaccess = new Red_Htaccess();
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
$htaccess->add( $item );
|
||||
}
|
||||
|
||||
return $htaccess->get() . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $group Group ID to import into.
|
||||
* @param string $filename Path to the file to import.
|
||||
* @param string|false $data File contents (or false if not pre-loaded).
|
||||
* @return int
|
||||
*/
|
||||
public function load( $group, $filename, $data ) {
|
||||
if ( $data === false ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remove any comments
|
||||
$data = str_replace( "\n", "\r", $data );
|
||||
|
||||
// Split it into lines
|
||||
$lines = array_filter( explode( "\r", $data ) );
|
||||
$count = 0;
|
||||
|
||||
foreach ( $lines as $line ) {
|
||||
$item = $this->get_as_item( $line );
|
||||
|
||||
if ( $item !== false ) {
|
||||
$item['group_id'] = $group;
|
||||
$redirect = Red_Item::create( $item );
|
||||
|
||||
if ( ! is_wp_error( $redirect ) ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $line
|
||||
* @return array<string, mixed>|false
|
||||
*/
|
||||
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( isset( $matches[3] ) ? $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 !== false ) {
|
||||
$item['action_type'] = 'url';
|
||||
$item['match_type'] = 'url';
|
||||
|
||||
if ( $item['action_code'] === 0 ) {
|
||||
$item['action_type'] = 'pass';
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
private function decode_url( $url ) {
|
||||
$url = rawurldecode( $url );
|
||||
|
||||
// Replace quoted slashes
|
||||
$url = (string) preg_replace( '@\\\/@', '/', $url );
|
||||
|
||||
// Ensure escaped '.' is still escaped
|
||||
$url = (string) preg_replace( '@\\\\.@', '\\\\.', $url );
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
private function is_str_regex( $url ) {
|
||||
$regex = '()[]$^?+.';
|
||||
$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 ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @return int
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CSV import/export handler
|
||||
*
|
||||
* @phpstan-type CsvItem array{
|
||||
* url: string,
|
||||
* action_data: array{url: string},
|
||||
* regex: bool,
|
||||
* group_id: int,
|
||||
* match_type: 'url',
|
||||
* action_type: 'url'|'error',
|
||||
* action_code: int,
|
||||
* status?: 'enabled'|'disabled'
|
||||
* }
|
||||
* @phpstan-import-type GroupJson from Red_Group
|
||||
*/
|
||||
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' ) . '"' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Red_Item> $items
|
||||
* @param array<GroupJson> $groups
|
||||
* @return string
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Red_Item $item
|
||||
* @return string
|
||||
*/
|
||||
public function item_as_csv( $item ) {
|
||||
$data = [];
|
||||
|
||||
if ( $item->match !== null ) {
|
||||
$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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int|float $item
|
||||
* @return string|int|float
|
||||
*/
|
||||
public function escape_csv( $item ) {
|
||||
if ( is_numeric( $item ) ) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
return '"' . str_replace( '"', '""', $item ) . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $group Group ID to import into.
|
||||
* @param string $filename Path to the file to import.
|
||||
* @param string|false $data File contents (or false if not pre-loaded).
|
||||
* @return int
|
||||
*/
|
||||
public function load( $group, $filename, $data ) {
|
||||
$file = fopen( $filename, 'r' );
|
||||
|
||||
if ( $file !== false ) {
|
||||
$separators = [
|
||||
',',
|
||||
';',
|
||||
'|',
|
||||
];
|
||||
|
||||
foreach ( $separators as $separator ) {
|
||||
fseek( $file, 0 );
|
||||
$count = $this->load_from_file( $group, $file, $separator );
|
||||
|
||||
if ( $count > 0 ) {
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $group_id
|
||||
* @param resource $file
|
||||
* @param string $separator
|
||||
* @return int
|
||||
*/
|
||||
public function load_from_file( $group_id, $file, $separator ) {
|
||||
global $wpdb;
|
||||
|
||||
$count = 0;
|
||||
$group = Red_Group::get( $group_id );
|
||||
if ( $group === false ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @var Red_Group $group */
|
||||
|
||||
while ( ( $csv = fgetcsv( $file, 5000, $separator ) ) !== false ) {
|
||||
if ( $csv === null ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<int, string> $csv */
|
||||
$csv = array_map(
|
||||
function ( $v ) {
|
||||
return (string) $v;
|
||||
},
|
||||
$csv
|
||||
);
|
||||
$item = $this->csv_as_item( $csv, $group );
|
||||
|
||||
if ( $item !== false && $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CsvItem $csv
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $code
|
||||
* @return int
|
||||
*/
|
||||
private function get_valid_code( $code ) {
|
||||
if ( get_status_header_desc( $code ) !== '' ) {
|
||||
return intval( $code, 10 );
|
||||
}
|
||||
|
||||
return 301;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
* @return 'url'|'error'
|
||||
*/
|
||||
private function get_action_type( $code ) {
|
||||
if ( $code > 400 && $code < 500 ) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'url';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $csv
|
||||
* @param Red_Group $group
|
||||
* @return CsvItem|false
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int $value
|
||||
* @return bool
|
||||
*/
|
||||
private function parse_regex( $value ) {
|
||||
return intval( $value, 10 ) === 1 ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
private function is_regex( $url ) {
|
||||
$regex = '()[]$^*';
|
||||
|
||||
if ( strpbrk( $url, $regex ) === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type GroupJson from Red_Group
|
||||
*/
|
||||
|
||||
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' ) . '"' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Red_Item> $items
|
||||
* @param array<GroupJson> $groups
|
||||
* @return string
|
||||
*/
|
||||
public function get_data( array $items, array $groups ) {
|
||||
$version = red_get_plugin_data( dirname( __DIR__ ) . '/redirection.php' );
|
||||
|
||||
$items = array(
|
||||
'plugin' => array(
|
||||
'version' => trim( $version['Version'] ),
|
||||
'date' => gmdate( '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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $group Group ID to import into.
|
||||
* @param string $filename Path to the file to import.
|
||||
* @param string|false $data File contents (or false if not pre-loaded).
|
||||
* @return int
|
||||
*/
|
||||
public function load( $group, $filename, $data ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( $data === false ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
/** @var array<string, mixed>|false $json */
|
||||
$json = @json_decode( $data, true );
|
||||
if ( $json === false ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Import groups
|
||||
$group_map = array();
|
||||
|
||||
if ( isset( $json['groups'] ) ) {
|
||||
foreach ( $json['groups'] as $json_group ) {
|
||||
$old_group_id = $json_group['id'];
|
||||
unset( $json_group['id'] );
|
||||
|
||||
$json_group = Red_Group::create( $json_group['name'], $json_group['module_id'], $json_group['enabled'] ? true : false );
|
||||
if ( $json_group !== false ) {
|
||||
$group_map[ $old_group_id ] = $json_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 );
|
||||
if ( $new_group !== false ) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type GroupJson from Red_Group
|
||||
* @phpstan-import-type RedirectMatchData from Red_Item
|
||||
*/
|
||||
|
||||
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' ) . '"' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Red_Item> $items
|
||||
* @param array<GroupJson> $groups
|
||||
* @return string
|
||||
*/
|
||||
public function get_data( array $items, array $groups ) {
|
||||
$lines = array();
|
||||
$version = red_get_plugin_data( dirname( __DIR__ ) . '/redirection.php' );
|
||||
|
||||
$lines[] = '# Created by Redirection';
|
||||
$lines[] = '# ' . gmdate( '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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 'permanent'|'redirect'
|
||||
*/
|
||||
private function get_redirect_code( Red_Item $item ) {
|
||||
if ( $item->get_action_code() === 301 ) {
|
||||
return 'permanent';
|
||||
}
|
||||
return 'redirect';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $group Group ID to import into.
|
||||
* @param string $filename Path to the file to import.
|
||||
* @param string|false $data File contents (or false if not pre-loaded).
|
||||
* @return int
|
||||
*/
|
||||
public function load( $group, $filename, $data ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|false
|
||||
*/
|
||||
private function get_nginx_item( Red_Item $item ) {
|
||||
$target = 'add_' . $item->get_match_type();
|
||||
|
||||
if ( method_exists( $this, $target ) ) {
|
||||
$match_data = $item->get_match_data();
|
||||
$match_data = is_array( $match_data ) ? $match_data : array();
|
||||
// @phpstan-ignore method.dynamicName
|
||||
return ' ' . $this->$target( $item, $match_data );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RedirectMatchData $match_data
|
||||
* @return string
|
||||
*/
|
||||
private function add_url( Red_Item $item, array $match_data ) {
|
||||
// @phpstan-ignore booleanAnd.rightAlwaysTrue
|
||||
$source = isset( $match_data['source'] ) && is_array( $match_data['source'] ) ? $match_data['source'] : null;
|
||||
$regex = $item->source_flags !== null && $item->source_flags->is_regex();
|
||||
|
||||
return $this->get_redirect( $item->get_url(), $item->get_action_data(), $this->get_redirect_code( $item ), $source, $regex );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RedirectMatchData $match_data
|
||||
* @return string
|
||||
*/
|
||||
private function add_agent( Red_Item $item, array $match_data ) {
|
||||
$lines = array();
|
||||
|
||||
// @phpstan-ignore booleanAnd.rightAlwaysTrue
|
||||
$source = isset( $match_data['source'] ) && is_array( $match_data['source'] ) ? $match_data['source'] : null;
|
||||
|
||||
// Help PHPStan: ensure we operate on an Agent_Match
|
||||
$match = $item->match;
|
||||
if ( ! ( $match instanceof Agent_Match ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( $match->url_from !== '' ) {
|
||||
$lines[] = 'if ( $http_user_agent ~* ^' . $match->agent . '$ ) {';
|
||||
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $match->url_from, $this->get_redirect_code( $item ), $source );
|
||||
$lines[] = ' }';
|
||||
}
|
||||
|
||||
if ( $match->url_notfrom !== '' ) {
|
||||
$lines[] = 'if ( $http_user_agent !~* ^' . $match->agent . '$ ) {';
|
||||
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $match->url_notfrom, $this->get_redirect_code( $item ), $source );
|
||||
$lines[] = ' }';
|
||||
}
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RedirectMatchData $match_data
|
||||
* @return string
|
||||
*/
|
||||
private function add_referrer( Red_Item $item, array $match_data ) {
|
||||
$lines = array();
|
||||
// @phpstan-ignore booleanAnd.rightAlwaysTrue
|
||||
$source = isset( $match_data['source'] ) && is_array( $match_data['source'] ) ? $match_data['source'] : null;
|
||||
|
||||
// Help PHPStan: ensure we operate on a Referrer_Match
|
||||
$match = $item->match;
|
||||
if ( ! ( $match instanceof Referrer_Match ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( $match->url_from !== '' ) {
|
||||
$lines[] = 'if ( $http_referer ~* ^' . $match->referrer . '$ ) {';
|
||||
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $match->url_from, $this->get_redirect_code( $item ), $source );
|
||||
$lines[] = ' }';
|
||||
}
|
||||
|
||||
if ( $match->url_notfrom !== '' ) {
|
||||
$lines[] = 'if ( $http_referer !~* ^' . $match->referrer . '$ ) {';
|
||||
$lines[] = ' ' . $this->get_redirect( $item->get_url(), $match->url_notfrom, $this->get_redirect_code( $item ), $source );
|
||||
$lines[] = ' }';
|
||||
}
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $line
|
||||
* @param string $target
|
||||
* @param 'permanent'|'redirect' $code
|
||||
* @param array{
|
||||
* flag_query?: 'ignore'|'exact'|'pass'|'exactorder',
|
||||
* flag_case?: bool,
|
||||
* flag_trailing?: bool,
|
||||
* flag_regex?: bool
|
||||
* }|null $source
|
||||
* @param bool $regex
|
||||
* @return string
|
||||
*/
|
||||
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 . ';';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type GroupJson from Red_Group
|
||||
*/
|
||||
|
||||
class Red_Rss_File extends Red_FileIO {
|
||||
public function force_download() {
|
||||
header( 'Content-type: text/xml; charset=' . get_option( 'blog_charset' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Red_Item> $items
|
||||
* @param array<GroupJson> $groups
|
||||
* @return string
|
||||
*/
|
||||
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>
|
||||
<description><?php esc_html( bloginfo_rss( 'description' ) ); ?></description>
|
||||
<pubDate><?php echo esc_html( (string) 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( gmdate( 'D, d M Y H:i:s +0000', intval( $log->get_last_hit(), 10 ) ) ); ?></pubDate>
|
||||
<guid isPermaLink="false"><?php echo esc_html( (string) $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $group Group ID to import into.
|
||||
* @param string $filename Path to the file to import.
|
||||
* @param string|false $data File contents (or false if not pre-loaded).
|
||||
* @return int
|
||||
*/
|
||||
public function load( $group, $filename, $data ) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -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>.
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/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 );
|
||||
$cookie = Redirection_Request::get_cookie( $this->name );
|
||||
if ( $cookie === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $regex->is_match( $cookie );
|
||||
}
|
||||
|
||||
return Redirection_Request::get_cookie( $this->name ) === $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type CustomFilterMap array{
|
||||
* filter?: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type CustomFilterResult array{
|
||||
* filter: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type CustomFilterData array{
|
||||
* filter: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-extends Red_Match<CustomFilterMap, CustomFilterResult>
|
||||
*
|
||||
* 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 = '';
|
||||
|
||||
/**
|
||||
* Name of this match used in UI.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name() {
|
||||
return __( 'URL and custom filter', 'redirection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data to an array, ready for serializing.
|
||||
*
|
||||
* @param CustomFilterMap $details New match data.
|
||||
* @param boolean $no_target_url Does the action have a target URL.
|
||||
* @return CustomFilterResult
|
||||
*/
|
||||
public function save( array $details, $no_target_url = false ) {
|
||||
$data = [
|
||||
'filter' => isset( $details['filter'] ) ? $this->sanitize_filter( $details['filter'] ) : '',
|
||||
];
|
||||
|
||||
/** @var CustomFilterResult $result */
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize filter name to allow only alphanumeric, dash and underscore.
|
||||
*
|
||||
* @param string $name Filter name.
|
||||
* @return string
|
||||
*/
|
||||
public function sanitize_filter( $name ) {
|
||||
$name = (string) preg_replace( '/[^A-Za-z0-9\-_]/', '', sanitize_text_field( $name ) );
|
||||
|
||||
return trim( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current request matches using the custom filter.
|
||||
*
|
||||
* @param string $url Requested URL.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_match( $url ) {
|
||||
if ( $this->filter === '' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return apply_filters( $this->filter, false, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the match data for persistence.
|
||||
*
|
||||
* @return CustomFilterData
|
||||
*/
|
||||
public function get_data() {
|
||||
return array_merge(
|
||||
[
|
||||
'filter' => $this->filter,
|
||||
],
|
||||
$this->get_from_data()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|CustomFilterMap $values Match values from database (serialized PHP or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$values = $this->load_data( $values );
|
||||
/** @var CustomFilterMap $values */
|
||||
$this->filter = isset( $values['filter'] ) ? $values['filter'] : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type FromNotFromMap (array{
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* } & array<string, mixed>)
|
||||
* @phpstan-type FromNotFromData array{
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @phpstan-template TData of array<string, mixed>
|
||||
* @param FromNotFromMap $details New match data.
|
||||
* @param bool $no_target_url Does the action have a target URL.
|
||||
* @phpstan-param TData $data Existing match data.
|
||||
* @param array<string, mixed> $data Existing match data.
|
||||
* @phpstan-return TData&FromNotFromMap
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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 !== false ) {
|
||||
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.
|
||||
*
|
||||
* @phpstan-template TValues of array<string, mixed>
|
||||
* @param string|TValues $values Serialized PHP data or parsed array.
|
||||
* @phpstan-return TValues&FromNotFromMap
|
||||
* @return array<string, mixed>&FromNotFromMap
|
||||
*/
|
||||
private function load_data( $values ) {
|
||||
if ( is_string( $values ) ) {
|
||||
$values = @unserialize( $values ); // phpcs:ignore
|
||||
}
|
||||
|
||||
if ( isset( $values['url_from'] ) ) {
|
||||
$this->url_from = $values['url_from'];
|
||||
}
|
||||
|
||||
if ( isset( $values['url_notfrom'] ) ) {
|
||||
$this->url_notfrom = $values['url_notfrom'];
|
||||
}
|
||||
|
||||
return is_array( $values ) ? $values : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the match data
|
||||
*
|
||||
* @return FromNotFromData
|
||||
*/
|
||||
private function get_from_data(): array {
|
||||
return [
|
||||
'url_from' => $this->url_from,
|
||||
'url_notfrom' => $this->url_notfrom,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type FromUrlMap (array{
|
||||
* url?: string
|
||||
* } & array<string, mixed>)
|
||||
* @phpstan-type FromUrlData array{
|
||||
* url: string
|
||||
* }
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @phpstan-template TData of array<string, mixed>
|
||||
* @param FromUrlMap $details New match data.
|
||||
* @param bool $no_target_url Does the action have a target URL.
|
||||
* @phpstan-param TData $data Existing match data.
|
||||
* @param array<string, mixed> $data Existing match data.
|
||||
* @phpstan-return TData&FromUrlMap
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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 !== false ) {
|
||||
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.
|
||||
*
|
||||
* @phpstan-template TValues of array<string, mixed>
|
||||
* @param string|TValues $values Serialized PHP or parsed array.
|
||||
* @phpstan-return TValues&FromUrlMap
|
||||
* @return array<string, mixed>&FromUrlMap
|
||||
*/
|
||||
private function load_data( $values ) {
|
||||
if ( is_string( $values ) ) {
|
||||
$values = unserialize( $values ); // phpcs:ignore
|
||||
}
|
||||
|
||||
if ( isset( $values['url'] ) ) {
|
||||
$this->url = $values['url'];
|
||||
}
|
||||
|
||||
return is_array( $values ) ? $values : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the loaded data as an array.
|
||||
*
|
||||
* @return FromUrlData
|
||||
*/
|
||||
private function get_from_data(): array {
|
||||
return [
|
||||
'url' => $this->url,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type HeaderMap array{
|
||||
* name?: string,
|
||||
* value?: string,
|
||||
* regex?: bool,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type HeaderResult array{
|
||||
* name: string,
|
||||
* value: string,
|
||||
* regex: bool,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type HeaderData array{
|
||||
* name: string,
|
||||
* value: string,
|
||||
* regex: bool,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* Check a HTTP request header
|
||||
*
|
||||
* @phpstan-extends Red_Match<HeaderMap, HeaderResult>
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function name() {
|
||||
return __( 'URL and HTTP header', 'redirection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HeaderMap $details
|
||||
* @param bool $no_target_url
|
||||
* @return HeaderResult
|
||||
*/
|
||||
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'] ) : '',
|
||||
);
|
||||
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public function sanitize_name( $name ) {
|
||||
$name = $this->sanitize_url( sanitize_text_field( $name ) );
|
||||
$name = str_replace( ' ', '', $name );
|
||||
$name = (string) preg_replace( '/[^A-Za-z0-9\-_]/', '', $name );
|
||||
|
||||
return trim( trim( $name, ':' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public function sanitize_value( $value ) {
|
||||
return $this->sanitize_url( sanitize_text_field( $value ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_match( $url ) {
|
||||
if ( $this->regex ) {
|
||||
$regex = new Red_Regex( $this->value, true );
|
||||
$header = Redirection_Request::get_header( $this->name );
|
||||
|
||||
if ( ! is_string( $header ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $regex->is_match( $header );
|
||||
}
|
||||
|
||||
return Redirection_Request::get_header( $this->name ) === $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HeaderData
|
||||
*/
|
||||
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|array<string, mixed> $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$data = $this->load_data( $values );
|
||||
$this->regex = isset( $data['regex'] ) ? (bool) $data['regex'] : false; // @phpstan-ignore-line
|
||||
$this->name = isset( $data['name'] ) ? (string) $data['name'] : ''; // @phpstan-ignore-line
|
||||
$this->value = isset( $data['value'] ) ? (string) $data['value'] : ''; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type IpMap array{
|
||||
* ip?: string[],
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type IpResult array{
|
||||
* ip: string[],
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type IpData array{
|
||||
* ip: string[],
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* Check the request IP
|
||||
*
|
||||
* @phpstan-extends Red_Match<IpMap, IpResult>
|
||||
*/
|
||||
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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IpMap $details
|
||||
* @return IpResult
|
||||
*/
|
||||
public function save( array $details, $no_target_url = false ) {
|
||||
$data = array( 'ip' => isset( $details['ip'] ) && is_array( $details['ip'] ) ? $this->sanitize_ips( $details['ip'] ) : [] ); // @phpstan-ignore-line
|
||||
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpData
|
||||
*/
|
||||
public function get_data() {
|
||||
return array_merge(
|
||||
array(
|
||||
'ip' => $this->ip,
|
||||
),
|
||||
$this->get_from_data()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|IpMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$data = $this->load_data( $values );
|
||||
$this->ip = isset( $data['ip'] ) ? $data['ip'] : []; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type LanguageMap array{
|
||||
* language?: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type LanguageResult array{
|
||||
* language: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type LanguageData array{
|
||||
* language: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* Check the client language
|
||||
*
|
||||
* @phpstan-extends Red_Match<LanguageMap, LanguageResult>
|
||||
*/
|
||||
class Language_Match extends Red_Match {
|
||||
use FromNotFrom_Match;
|
||||
|
||||
/**
|
||||
* Language to check.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $language = '';
|
||||
|
||||
public function name() {
|
||||
return __( 'URL and language', 'redirection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LanguageMap $details
|
||||
* @return LanguageResult
|
||||
*/
|
||||
public function save( array $details, $no_target_url = false ) {
|
||||
$data = array( 'language' => isset( $details['language'] ) ? $this->sanitize_language( $details['language'] ) : '' );
|
||||
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageData
|
||||
*/
|
||||
public function get_data() {
|
||||
return array_merge(
|
||||
array(
|
||||
'language' => $this->language,
|
||||
),
|
||||
$this->get_from_data()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|LanguageMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$data = $this->load_data( $values );
|
||||
$this->language = isset( $data['language'] ) ? $data['language'] : ''; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type LoginMap array{
|
||||
* logged_in?: string,
|
||||
* logged_out?: string
|
||||
* }
|
||||
* @phpstan-type LoginResult array{
|
||||
* logged_in: string,
|
||||
* logged_out: string
|
||||
* }
|
||||
* @phpstan-type LoginData array{
|
||||
* logged_in: string,
|
||||
* logged_out: string
|
||||
* }
|
||||
*
|
||||
* Check whether the user is logged in or out
|
||||
*
|
||||
* @phpstan-extends Red_Match<LoginMap, LoginResult>
|
||||
*/
|
||||
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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LoginMap $details
|
||||
* @return LoginResult|null
|
||||
*/
|
||||
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 !== false ) {
|
||||
$target = $this->get_target_regex_url( $source_url, $target, $requested_url, $flags );
|
||||
}
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoginData
|
||||
*/
|
||||
public function get_data() {
|
||||
return [
|
||||
'logged_in' => $this->logged_in,
|
||||
'logged_out' => $this->logged_out,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|LoginMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
if ( is_string( $values ) ) {
|
||||
$values = @unserialize( $values );
|
||||
}
|
||||
|
||||
if ( is_array( $values ) ) {
|
||||
$this->logged_in = isset( $values['logged_in'] ) ? $values['logged_in'] : '';
|
||||
$this->logged_out = isset( $values['logged_out'] ) ? $values['logged_out'] : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type PageMap array{
|
||||
* page?: string,
|
||||
* url?: string
|
||||
* }
|
||||
* @phpstan-type PageResult array{
|
||||
* page: string,
|
||||
* url?: string
|
||||
* }
|
||||
* @phpstan-type PageData array{
|
||||
* page: string,
|
||||
* url: string
|
||||
* }
|
||||
*
|
||||
* Match the WordPress page type
|
||||
*
|
||||
* @phpstan-extends Red_Match<PageMap, PageResult>
|
||||
*/
|
||||
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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data to an array, ready for serializing.
|
||||
*
|
||||
* @param PageMap $details New match data.
|
||||
* @param bool $no_target_url Does the action have a target URL.
|
||||
* @return PageResult
|
||||
*/
|
||||
public function save( array $details, $no_target_url = false ) {
|
||||
$data = array( 'page' => isset( $details['page'] ) ? $this->sanitize_page( $details['page'] ) : '404' );
|
||||
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $page
|
||||
* @return string
|
||||
*/
|
||||
private function sanitize_page( $page ) {
|
||||
return '404';
|
||||
}
|
||||
|
||||
public function is_match( $url ) {
|
||||
return is_404();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the match data for persistence.
|
||||
*
|
||||
* @return PageData
|
||||
*/
|
||||
public function get_data() {
|
||||
return array_merge(
|
||||
array(
|
||||
'page' => $this->page,
|
||||
),
|
||||
$this->get_from_data()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|PageMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$data = $this->load_data( $values );
|
||||
$this->page = isset( $data['page'] ) ? $data['page'] : '404'; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type ReferrerMap array{
|
||||
* regex?: bool,
|
||||
* referrer?: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type ReferrerResult array{
|
||||
* regex: bool,
|
||||
* referrer: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type ReferrerData array{
|
||||
* regex: bool,
|
||||
* referrer: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* Match the referrer
|
||||
*
|
||||
* @phpstan-extends Red_Match<ReferrerMap, ReferrerResult>
|
||||
*/
|
||||
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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ReferrerMap $details
|
||||
* @return ReferrerResult
|
||||
*/
|
||||
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'] ) : '',
|
||||
);
|
||||
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $agent
|
||||
* @return string
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReferrerData
|
||||
*/
|
||||
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|ReferrerMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$data = $this->load_data( $values );
|
||||
$this->regex = isset( $data['regex'] ) ? $data['regex'] : false; // @phpstan-ignore-line
|
||||
$this->referrer = isset( $data['referrer'] ) ? $data['referrer'] : ''; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type ServerMap array{
|
||||
* server?: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type ServerResult array{
|
||||
* server: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type ServerData array{
|
||||
* server: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* Match the server URL. Used to match requests for another domain.
|
||||
*
|
||||
* @phpstan-extends Red_Match<ServerMap, ServerResult>
|
||||
*/
|
||||
class Server_Match extends Red_Match {
|
||||
use FromNotFrom_Match;
|
||||
|
||||
/**
|
||||
* Server URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $server = '';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function name() {
|
||||
return __( 'URL and server', 'redirection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerMap $details
|
||||
* @param bool $no_target_url
|
||||
* @return ServerResult
|
||||
*/
|
||||
public function save( array $details, $no_target_url = false ) {
|
||||
$data = array( 'server' => isset( $details['server'] ) ? $this->sanitize_server( $details['server'] ) : '' );
|
||||
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $server
|
||||
* @return string
|
||||
*/
|
||||
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'] ) && isset( $parts['scheme'] ) ) {
|
||||
return $parts['scheme'] . '://' . $parts['host'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_match( $url ) {
|
||||
$server = wp_parse_url( $this->server, PHP_URL_HOST );
|
||||
|
||||
return $server === Redirection_Request::get_server_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ServerData
|
||||
*/
|
||||
public function get_data() {
|
||||
return array_merge(
|
||||
array(
|
||||
'server' => $this->server,
|
||||
),
|
||||
$this->get_from_data()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|ServerMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$data = $this->load_data( $values );
|
||||
$this->server = isset( $data['server'] ) ? $data['server'] : ''; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type UrlMap array{
|
||||
* url?: string
|
||||
* }
|
||||
* @phpstan-type UrlData array{
|
||||
* url: string
|
||||
* }
|
||||
*
|
||||
* Match the URL only.
|
||||
*
|
||||
* @phpstan-extends Red_Match<UrlMap, string>
|
||||
*/
|
||||
class URL_Match extends Red_Match {
|
||||
/**
|
||||
* URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $url = '';
|
||||
|
||||
public function name() {
|
||||
return __( 'URL only', 'redirection' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UrlMap $details
|
||||
* @return string|null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UrlData|null
|
||||
*/
|
||||
public function get_data() {
|
||||
if ( $this->url !== '' ) {
|
||||
return [
|
||||
'url' => $this->url,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|UrlMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
if ( is_array( $values ) ) {
|
||||
$this->url = isset( $values['url'] ) ? $values['url'] : '';
|
||||
return;
|
||||
}
|
||||
|
||||
$this->url = $values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type AgentMap array{
|
||||
* regex?: bool,
|
||||
* agent?: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type AgentResult array{
|
||||
* regex: bool,
|
||||
* agent: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type AgentData array{
|
||||
* regex: bool,
|
||||
* agent: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* Match the user agent
|
||||
*
|
||||
* @phpstan-extends Red_Match<AgentMap, AgentResult>
|
||||
*/
|
||||
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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AgentMap $details
|
||||
* @return AgentResult
|
||||
*/
|
||||
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'] ) : '',
|
||||
);
|
||||
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $agent User agent string.
|
||||
* @return string
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AgentData
|
||||
*/
|
||||
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|AgentMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$data = $this->load_data( $values );
|
||||
$this->regex = isset( $data['regex'] ) ? $data['regex'] : false; // @phpstan-ignore-line
|
||||
$this->agent = isset( $data['agent'] ) ? $data['agent'] : ''; // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type RoleMap array{
|
||||
* role?: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type RoleResult array{
|
||||
* role: string,
|
||||
* url_from?: string,
|
||||
* url_notfrom?: string
|
||||
* }
|
||||
* @phpstan-type RoleData array{
|
||||
* role: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
*
|
||||
* Match a particular role or capability
|
||||
*
|
||||
* @phpstan-extends Red_Match<RoleMap, RoleResult>
|
||||
*/
|
||||
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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RoleMap $details
|
||||
* @return RoleResult
|
||||
*/
|
||||
public function save( array $details, $no_target_url = false ) {
|
||||
$data = array( 'role' => isset( $details['role'] ) ? $details['role'] : '' );
|
||||
|
||||
/** @var RoleResult $result */
|
||||
$result = $this->save_data( $details, $no_target_url, $data );
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function is_match( $url ) {
|
||||
return current_user_can( $this->role );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RoleData
|
||||
*/
|
||||
public function get_data() {
|
||||
return array_merge(
|
||||
array(
|
||||
'role' => $this->role,
|
||||
),
|
||||
$this->get_from_data()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param string|RoleMap $values Match values, as read from the database (plain text, serialized PHP, or parsed array).
|
||||
* @return void
|
||||
*/
|
||||
public function load( $values ) {
|
||||
$values = $this->load_data( $values );
|
||||
/** @var RoleMap $values */
|
||||
$this->role = isset( $values['role'] ) ? $values['role'] : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A redirect action - what happens after a URL is matched.
|
||||
*
|
||||
* @phpstan-type UrlActionData array{
|
||||
* code: int,
|
||||
* target: string,
|
||||
* type: string
|
||||
* }
|
||||
* @phpstan-type ErrorActionData array{
|
||||
* code: int,
|
||||
* type: string
|
||||
* }
|
||||
* @phpstan-type NothingActionData array{
|
||||
* code: int,
|
||||
* type: string
|
||||
* }
|
||||
* @phpstan-type RandomActionData array{
|
||||
* code: int,
|
||||
* type: string
|
||||
* }
|
||||
* @phpstan-type PassActionData array{
|
||||
* code: int,
|
||||
* target: string,
|
||||
* type: string
|
||||
* }
|
||||
* @phpstan-type RedActionData UrlActionData|ErrorActionData|NothingActionData|RandomActionData|PassActionData
|
||||
*/
|
||||
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 RedActionData|array{} $values Values.
|
||||
*/
|
||||
public function __construct( $values = [] ) {
|
||||
if ( isset( $values['code'] ) ) {
|
||||
$this->code = $values['code'];
|
||||
}
|
||||
|
||||
if ( isset( $values['target'] ) ) {
|
||||
$this->target = $values['target'];
|
||||
}
|
||||
|
||||
if ( isset( $values['type'] ) ) {
|
||||
$this->type = $values['type'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
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 __DIR__ . '/../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<string, array{string, string}>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?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 === false ) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Base class for file import/export operations
|
||||
*
|
||||
* @phpstan-import-type GroupJson from Red_Group
|
||||
* @phpstan-type UploadedFile array{
|
||||
* name: string,
|
||||
* type: string,
|
||||
* tmp_name: string,
|
||||
* error: int,
|
||||
* size: int
|
||||
* }
|
||||
* @phpstan-type ExportResult array{
|
||||
* data: string,
|
||||
* total: int,
|
||||
* exporter: Red_FileIO
|
||||
* }
|
||||
*/
|
||||
abstract class Red_FileIO {
|
||||
/**
|
||||
* Create a file IO handler for the specified type
|
||||
*
|
||||
* @param string $type File format type (rss, csv, apache, nginx, json).
|
||||
* @return Red_FileIO|false
|
||||
*/
|
||||
public static function create( $type ) {
|
||||
$exporter = false;
|
||||
|
||||
if ( $type === 'rss' ) {
|
||||
include_once dirname( __DIR__ ) . '/fileio/rss.php';
|
||||
$exporter = new Red_Rss_File();
|
||||
} elseif ( $type === 'csv' ) {
|
||||
include_once dirname( __DIR__ ) . '/fileio/csv.php';
|
||||
$exporter = new Red_Csv_File();
|
||||
} elseif ( $type === 'apache' ) {
|
||||
include_once dirname( __DIR__ ) . '/fileio/apache.php';
|
||||
$exporter = new Red_Apache_File();
|
||||
} elseif ( $type === 'nginx' ) {
|
||||
include_once dirname( __DIR__ ) . '/fileio/nginx.php';
|
||||
$exporter = new Red_Nginx_File();
|
||||
} elseif ( $type === 'json' ) {
|
||||
include_once dirname( __DIR__ ) . '/fileio/json.php';
|
||||
$exporter = new Red_Json_File();
|
||||
}
|
||||
|
||||
return $exporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import redirects from an uploaded file
|
||||
*
|
||||
* @param int $group_id Group ID to import into.
|
||||
* @param UploadedFile $file Uploaded file data from $_FILES.
|
||||
* @return int
|
||||
*/
|
||||
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( __DIR__ ) . '/fileio/csv.php';
|
||||
$importer = new Red_Csv_File();
|
||||
$data = '';
|
||||
} elseif ( $extension === 'json' ) {
|
||||
include_once dirname( __DIR__ ) . '/fileio/json.php';
|
||||
$importer = new Red_Json_File();
|
||||
$data = @file_get_contents( $file['tmp_name'] );
|
||||
} else {
|
||||
include_once dirname( __DIR__ ) . '/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 === false ) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $importer->load( $group_id, $file['tmp_name'], $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set headers to force file download
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function force_download() {
|
||||
header( 'Cache-Control: no-cache, must-revalidate' );
|
||||
header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate export filename
|
||||
*
|
||||
* @param string $extension File extension (without dot).
|
||||
* @return string Generated filename.
|
||||
*/
|
||||
protected function export_filename( $extension ) {
|
||||
$name = wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
if ( $name === false || $name === null || $name === '' ) {
|
||||
$name = 'export';
|
||||
}
|
||||
|
||||
$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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Export redirects to a file format
|
||||
*
|
||||
* @param string|int $module_name_or_id Module name, ID, or 'all' for all modules.
|
||||
* @param string $format Export format (rss, csv, apache, nginx, json).
|
||||
* @return ExportResult|false Export data or false on failure.
|
||||
*/
|
||||
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 !== false ) {
|
||||
$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 !== false && $items !== false && $groups !== false ) {
|
||||
return [
|
||||
'data' => $exporter->get_data( $items, $groups ),
|
||||
'total' => count( $items ),
|
||||
'exporter' => $exporter,
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get export data for items and groups
|
||||
*
|
||||
* @param array<Red_Item> $items Redirect items to export.
|
||||
* @param array<GroupJson> $groups Groups to export.
|
||||
* @return string Formatted export data.
|
||||
*/
|
||||
abstract public function get_data( array $items, array $groups );
|
||||
|
||||
/**
|
||||
* Load and import data from a file
|
||||
*
|
||||
* @param int $group Group ID to import into.
|
||||
* @param string $filename Path to the file to import.
|
||||
* @param string|false $data File contents (or false if not pre-loaded).
|
||||
* @return int
|
||||
*/
|
||||
abstract public function load( $group, $filename, $data );
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
require_once dirname( REDIRECTION_FILE ) . '/database/database.php';
|
||||
|
||||
/**
|
||||
* Diagnostic and repair tool for Redirection plugin
|
||||
*
|
||||
* @phpstan-type StatusItem array{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* message: string,
|
||||
* status: string
|
||||
* }
|
||||
* @phpstan-type DebugInfo array{
|
||||
* database: array{
|
||||
* current: string,
|
||||
* latest: string
|
||||
* },
|
||||
* ip_header: array<string, string|false>
|
||||
* }
|
||||
* @phpstan-type FixerJson array{
|
||||
* status: array<StatusItem>,
|
||||
* debug: DebugInfo
|
||||
* }
|
||||
*/
|
||||
class Red_Fixer {
|
||||
const REGEX_LIMIT = 200;
|
||||
|
||||
/**
|
||||
* Get JSON representation of fixer status and debug info
|
||||
*
|
||||
* @return FixerJson
|
||||
*/
|
||||
public function get_json() {
|
||||
return [
|
||||
'status' => $this->get_status(),
|
||||
'debug' => $this->get_debug(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*
|
||||
* @return DebugInfo
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save debug setting
|
||||
*
|
||||
* @param string $name Setting name.
|
||||
* @param string $value Setting value.
|
||||
* @return void
|
||||
*/
|
||||
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->get_version() ) {
|
||||
$status->finish();
|
||||
$status->save_db_version( $value );
|
||||
|
||||
// Switch to prompt mode
|
||||
red_set_options( [ 'plugin_update' => 'prompt' ] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of all diagnostic checks
|
||||
*
|
||||
* @return array<StatusItem>
|
||||
*/
|
||||
public function get_status() {
|
||||
global $wpdb;
|
||||
|
||||
$options = Red_Options::get();
|
||||
|
||||
$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 ) !== false || $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database table status
|
||||
*
|
||||
* @param Red_Latest_Database $database Database instance.
|
||||
* @return array{status: string, message: string}
|
||||
*/
|
||||
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 ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP settings status
|
||||
*
|
||||
* @return StatusItem
|
||||
*/
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix all issues found in status
|
||||
*
|
||||
* @param array<StatusItem> $status Status items to fix.
|
||||
* @return array<StatusItem>|WP_Error Updated status or error.
|
||||
*/
|
||||
public function fix( $status ) {
|
||||
foreach ( $status as $item ) {
|
||||
if ( $item['status'] !== 'good' ) {
|
||||
$fixer = 'fix_' . $item['id'];
|
||||
|
||||
$result = true;
|
||||
if ( method_exists( $this, $fixer ) ) {
|
||||
// @phpstan-ignore method.dynamicName
|
||||
$result = $this->$fixer();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->get_status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirects with missing groups
|
||||
*
|
||||
* @return list<object{id: string}>
|
||||
*/
|
||||
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" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix database tables
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function fix_db() {
|
||||
$database = Red_Database::get_latest_database();
|
||||
return $database->install();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix missing groups
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function fix_groups() {
|
||||
if ( Red_Group::create( __( 'Redirections', 'redirection' ), 1 ) === false ) {
|
||||
return new WP_Error( 'redirect_group_create_failed', 'Unable to create group' );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix redirects with invalid groups
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function fix_redirect_groups() {
|
||||
global $wpdb;
|
||||
|
||||
$missing = $this->get_missing();
|
||||
$group_id = $this->get_valid_group();
|
||||
|
||||
if ( is_wp_error( $group_id ) ) {
|
||||
return $group_id;
|
||||
}
|
||||
|
||||
foreach ( $missing as $row ) {
|
||||
$wpdb->update( $wpdb->prefix . 'redirection_items', array( 'group_id' => $group_id ), array( 'id' => $row->id ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix invalid monitor group setting
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function fix_monitor() {
|
||||
$group_id = $this->get_valid_group();
|
||||
|
||||
if ( is_wp_error( $group_id ) ) {
|
||||
return $group_id;
|
||||
}
|
||||
|
||||
red_set_options( array( 'monitor_post' => $group_id ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid group ID
|
||||
*
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
private function get_valid_group() {
|
||||
$groups = Red_Group::get_all();
|
||||
|
||||
if ( count( $groups ) === 0 ) {
|
||||
$group = Red_Group::create( __( 'Redirections', 'redirection' ), 1 );
|
||||
|
||||
if ( $group !== false ) {
|
||||
return $group->get_id();
|
||||
}
|
||||
|
||||
return new WP_Error( 'redirect_group_create_failed', 'Unable to create group' );
|
||||
}
|
||||
|
||||
return $groups[0]['id'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Handles automatic expiration and optimization of redirect and 404 logs
|
||||
*/
|
||||
class Red_Flusher {
|
||||
const DELETE_HOOK = 'redirection_log_delete';
|
||||
const DELETE_FREQ = 'daily';
|
||||
const DELETE_MAX = 20000;
|
||||
const DELETE_AGGRESSIVE = 50000; // Batch size for large backlogs (reduced from 100k for better replication)
|
||||
const DELETE_KEEP_ON = 10; // 10 minutes
|
||||
const DELETE_FAST = 3; // 3 minutes for aggressive mode (increased to reduce replication pressure)
|
||||
const AGGRESSIVE_THRESHOLD = 100000; // Switch to aggressive mode if more than 100k logs need deletion
|
||||
|
||||
/**
|
||||
* Flush expired logs and optimize tables
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flush() {
|
||||
$options = Red_Options::get();
|
||||
|
||||
// Start with normal batch size
|
||||
$batch_size = self::DELETE_MAX;
|
||||
|
||||
// Check if we're in an ongoing aggressive deletion cycle
|
||||
$aggressive_mode = get_transient( 'redirection_aggressive_delete' );
|
||||
$is_aggressive = $aggressive_mode !== false;
|
||||
if ( $is_aggressive ) {
|
||||
$batch_size = self::DELETE_AGGRESSIVE;
|
||||
}
|
||||
|
||||
$total = $this->expire_logs( 'redirection_logs', $options['expire_redirect'], $batch_size );
|
||||
$total += $this->expire_logs( 'redirection_404', $options['expire_404'], $batch_size );
|
||||
|
||||
// If we deleted the full batch, there are likely more logs to delete
|
||||
if ( $total >= $batch_size ) {
|
||||
// Check if we should switch to aggressive mode (only if not already in it)
|
||||
if ( ! $is_aggressive ) {
|
||||
// Sample check: if we hit the normal limit, check if there's a large backlog
|
||||
$remaining = $this->estimate_remaining_logs( 'redirection_logs', $options['expire_redirect'] );
|
||||
$remaining += $this->estimate_remaining_logs( 'redirection_404', $options['expire_404'] );
|
||||
|
||||
if ( $remaining >= self::AGGRESSIVE_THRESHOLD ) {
|
||||
// Enable aggressive mode for 1 hour (will auto-expire if deletion completes)
|
||||
set_transient( 'redirection_aggressive_delete', true, HOUR_IN_SECONDS );
|
||||
$is_aggressive = true;
|
||||
$batch_size = self::DELETE_AGGRESSIVE;
|
||||
}
|
||||
}
|
||||
|
||||
$delay_minutes = $is_aggressive ? self::DELETE_FAST : self::DELETE_KEEP_ON;
|
||||
$next = time() + ( $delay_minutes * 60 );
|
||||
|
||||
// Schedule next deletion if it's before the next normal event
|
||||
$next_scheduled = wp_next_scheduled( self::DELETE_HOOK );
|
||||
if ( $next_scheduled === false || $next < $next_scheduled ) {
|
||||
wp_schedule_single_event( $next, self::DELETE_HOOK );
|
||||
}
|
||||
} else {
|
||||
// Deletion is complete or slowing down, clear aggressive mode
|
||||
delete_transient( 'redirection_aggressive_delete' );
|
||||
}
|
||||
|
||||
$this->optimize_logs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomly optimize log tables to improve performance
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate remaining expired logs using a fast sampling method
|
||||
* Uses COUNT(*) over a limited subquery to avoid full table scans
|
||||
*
|
||||
* @param string $table Table name (without prefix).
|
||||
* @param int $expiry_time Number of days to keep logs.
|
||||
* @return int Estimated number of expired logs.
|
||||
*/
|
||||
private function estimate_remaining_logs( $table, $expiry_time ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( $expiry_time <= 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sample approach: Check up to AGGRESSIVE_THRESHOLD + 1 expired logs
|
||||
// Use COUNT(*) over a limited subquery so only a single scalar is returned
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM ( SELECT 1 FROM {$wpdb->prefix}{$table} WHERE created < DATE_SUB(NOW(), INTERVAL %d DAY) LIMIT %d ) AS t", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$expiry_time,
|
||||
self::AGGRESSIVE_THRESHOLD + 1
|
||||
)
|
||||
);
|
||||
|
||||
// If we reached AGGRESSIVE_THRESHOLD rows, there's definitely a large backlog
|
||||
return $count ? (int) $count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete expired logs from a table
|
||||
*
|
||||
* @param string $table Table name (without prefix).
|
||||
* @param int $expiry_time Number of days to keep logs.
|
||||
* @param int $batch_size Maximum number of logs to delete in this batch.
|
||||
* @return int Number of logs deleted.
|
||||
*/
|
||||
private function expire_logs( $table, $expiry_time, $batch_size = self::DELETE_MAX ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( $expiry_time <= 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Use DELETE with LIMIT - more efficient than counting first
|
||||
// The affected rows tell us how many were deleted
|
||||
$deleted = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->prefix}{$table} WHERE created < DATE_SUB(NOW(), INTERVAL %d DAY) LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$expiry_time,
|
||||
$batch_size
|
||||
)
|
||||
);
|
||||
|
||||
return $deleted ? (int) $deleted : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the automatic log deletion cron job
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function schedule() {
|
||||
$options = Red_Options::get();
|
||||
|
||||
if ( $options['expire_redirect'] > 0 || $options['expire_404'] > 0 ) {
|
||||
if ( wp_next_scheduled( self::DELETE_HOOK ) === false ) {
|
||||
wp_schedule_event( time(), self::DELETE_FREQ, self::DELETE_HOOK );
|
||||
}
|
||||
} else {
|
||||
self::clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the scheduled log deletion cron job
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clear() {
|
||||
wp_clear_scheduled_hook( self::DELETE_HOOK );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Filter builder for group queries
|
||||
*/
|
||||
class Red_Group_Filters {
|
||||
/**
|
||||
* SQL filter conditions
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $filters = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array<string, string> $filter_params Filter parameters.
|
||||
*/
|
||||
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' ) {
|
||||
$this->filters[] = $filter === 'enabled' ? "status='enabled'" : "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 ) ) . '%' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filters as SQL WHERE clause
|
||||
*
|
||||
* @return string SQL WHERE clause or empty string.
|
||||
*/
|
||||
public function get_as_sql() {
|
||||
if ( count( $this->filters ) > 0 ) {
|
||||
return ' WHERE ' . implode( ' AND ', $this->filters );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/group-filter.php';
|
||||
|
||||
/**
|
||||
* A group of redirects
|
||||
*
|
||||
* @phpstan-type GroupData object{
|
||||
* id: int,
|
||||
* name: string,
|
||||
* module_id?: int,
|
||||
* status?: string,
|
||||
* position?: int
|
||||
* }
|
||||
* @phpstan-type GroupJson array{
|
||||
* id: int,
|
||||
* name: string,
|
||||
* redirects: int,
|
||||
* module_id: int,
|
||||
* moduleName: string,
|
||||
* enabled: bool,
|
||||
* default?: bool
|
||||
* }
|
||||
* @phpstan-type GroupFilteredResult array{
|
||||
* items: array<GroupJson>,
|
||||
* total: int
|
||||
* }
|
||||
* @phpstan-type GroupSelectData array<string, array<int, string>>
|
||||
*/
|
||||
class Red_Group {
|
||||
const DEFAULT_PER_PAGE = 25;
|
||||
const MAX_PER_PAGE = 200;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param GroupData|string $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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|false
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param array<string, mixed> $params Optional filter parameters.
|
||||
* @return array<GroupJson>
|
||||
*/
|
||||
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 = [];
|
||||
// phpcs:ignore
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all groups for a specific module
|
||||
*
|
||||
* @param int $module_id Module ID.
|
||||
* @return array<GroupJson>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups formatted for select dropdown
|
||||
*
|
||||
* @return GroupSelectData
|
||||
*/
|
||||
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 !== false ) {
|
||||
$data[ $module->get_name() ][ intval( $row->id, 10 ) ] = $row->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new group
|
||||
*
|
||||
* @param string $name Group name.
|
||||
* @param int $module_id Module ID.
|
||||
* @param bool $enabled Whether the group is enabled.
|
||||
* @return Red_Group|false
|
||||
*/
|
||||
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 self::get( $wpdb->insert_id );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update group details
|
||||
*
|
||||
* @param array<string, mixed> $data Update data.
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this group and all its redirects
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of redirects in this group
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable this group and all its redirects
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable this group and all its redirects
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module ID for this group
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_module_id() {
|
||||
return $this->module_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered groups with pagination
|
||||
*
|
||||
* @param array<string, mixed> $params Filter and pagination parameters.
|
||||
* @return GroupFilteredResult
|
||||
*/
|
||||
public static function get_filtered( array $params ) {
|
||||
global $wpdb;
|
||||
|
||||
$orderby = 'name';
|
||||
$direction = 'DESC';
|
||||
$limit = self::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( self::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(
|
||||
// phpcs:ignore
|
||||
"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_Options::get();
|
||||
|
||||
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 ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert group to JSON representation
|
||||
*
|
||||
* @return GroupJson
|
||||
*/
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all groups matching filters
|
||||
*
|
||||
* @param array<string, mixed> $params Filter parameters.
|
||||
* @return void
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set status for all groups matching filters
|
||||
*
|
||||
* @param string $action Action to perform ('enable' or 'disable').
|
||||
* @param array<string, mixed> $params Filter parameters.
|
||||
* @return void
|
||||
*/
|
||||
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();
|
||||
|
||||
// phpcs:ignore
|
||||
$sql = $wpdb->prepare( "UPDATE {$wpdb->prefix}redirection_groups SET status=%s {$query}", $action === 'enable' ? 'enable' : 'disable' );
|
||||
|
||||
// phpcs:ignore
|
||||
$wpdb->query( $sql );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Manages HTTP headers for site-wide and redirect-specific use
|
||||
*
|
||||
* @phpstan-type HeaderSettingChoice array{
|
||||
* label: string,
|
||||
* value: string
|
||||
* }
|
||||
* @phpstan-type HeaderSettings array<string, string|array<HeaderSettingChoice>>
|
||||
* @phpstan-type HeaderData array{
|
||||
* type: string,
|
||||
* headerName: string,
|
||||
* headerValue: string,
|
||||
* location: string,
|
||||
* headerSettings: HeaderSettings
|
||||
* }
|
||||
* @phpstan-type RawHeaderInput array{
|
||||
* headerName?: string,
|
||||
* type?: string,
|
||||
* headerValue?: string,
|
||||
* location?: string,
|
||||
* headerSettings?: array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
class Red_Http_Headers {
|
||||
/**
|
||||
* Normalized headers
|
||||
*
|
||||
* @var array<HeaderData>
|
||||
*/
|
||||
private $headers = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array<RawHeaderInput> $options Raw header configuration.
|
||||
*/
|
||||
public function __construct( $options = [] ) {
|
||||
// @phpstan-ignore function.alreadyNarrowedType
|
||||
if ( is_array( $options ) ) {
|
||||
$this->headers = array_filter( array_map( [ $this, 'normalize' ], $options ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a raw header input
|
||||
*
|
||||
* @param RawHeaderInput $header Raw header data.
|
||||
* @return HeaderData|null Normalized header or null if invalid.
|
||||
*/
|
||||
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 = [];
|
||||
|
||||
// @phpstan-ignore booleanAnd.rightAlwaysTrue
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers as JSON
|
||||
*
|
||||
* @return array<HeaderData>
|
||||
*/
|
||||
public function get_json() {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to Dash-Case format
|
||||
*
|
||||
* @param string $name Input string.
|
||||
* @return string Dash-Case formatted string.
|
||||
*/
|
||||
private function dash_case( $name ) {
|
||||
$name = (string) preg_replace( '/[^A-Za-z0-9]/', ' ', $name );
|
||||
$name = (string) preg_replace( '/\s{2,}/', ' ', $name );
|
||||
$name = trim( $name, ' ' );
|
||||
$name = ucwords( $name );
|
||||
$name = str_replace( ' ', '-', $name );
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate headers by name
|
||||
*
|
||||
* @param array<HeaderData> $headers Headers to deduplicate.
|
||||
* @return array<HeaderData>
|
||||
*/
|
||||
private function remove_dupes( $headers ) {
|
||||
$new_headers = [];
|
||||
|
||||
foreach ( $headers as $header ) {
|
||||
$new_headers[ $header['headerName'] ] = $header;
|
||||
}
|
||||
|
||||
return array_values( $new_headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers for site-wide application
|
||||
*
|
||||
* @return array<HeaderData>
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers for redirects (combines site and redirect-specific headers)
|
||||
*
|
||||
* @return array<HeaderData>
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if header is a site header
|
||||
*
|
||||
* @param HeaderData $header Header to check.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_site_header( $header ) {
|
||||
return $header['location'] === 'site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if header is a redirect header
|
||||
*
|
||||
* @param HeaderData $header Header to check.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_redirect_header( $header ) {
|
||||
return $header['location'] === 'redirect';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply headers to the current request
|
||||
*
|
||||
* @param array<HeaderData> $headers Headers to apply.
|
||||
* @return void
|
||||
*/
|
||||
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 a string for use in headers
|
||||
*
|
||||
* @param string|array<mixed> $text Text to sanitize.
|
||||
* @return string Sanitized text.
|
||||
*/
|
||||
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
|
||||
// @phpstan-ignore function.alreadyNarrowedType
|
||||
if ( function_exists( 'iconv' ) && is_string( $text ) ) {
|
||||
$converted = @iconv( 'UTF-8', 'UTF-8//IGNORE', $text );
|
||||
if ( $converted !== false ) {
|
||||
$text = $converted;
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
<?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<string, string> $allowed Allowed encodings.
|
||||
* @return string
|
||||
*/
|
||||
private function replace_encoding( $str, array $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' => '?',
|
||||
'+' => '\\s',
|
||||
'.' => '\\.',
|
||||
'%20' => '\\s',
|
||||
];
|
||||
|
||||
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 = (string) preg_replace( "/[\r\n\t].*?$/s", '', $url );
|
||||
|
||||
// Remove invalid characters
|
||||
$url = (string) preg_replace( '/[^\PC\s]/u', '', $url );
|
||||
|
||||
// Make sure spaces are escaped as \s for regex matching
|
||||
$url = str_replace( ' ', '\\s', $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 = (string) preg_replace( '@^\^/@', '^', $url );
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a referrer redirect
|
||||
*
|
||||
* @param Red_Item $item Redirect item.
|
||||
* @param Referrer_Match $match_object Redirect match.
|
||||
* @return void
|
||||
*/
|
||||
private function add_referrer( $item, $match_object ) {
|
||||
$from = $this->encode_from( ltrim( $item->get_url(), '/' ), $item->source_flags !== null && $item->source_flags->is_ignore_trailing() );
|
||||
if ( $item->is_regex() ) {
|
||||
$from = $this->encode_regex( ltrim( $item->get_url(), '/' ) );
|
||||
}
|
||||
|
||||
if ( ( $match_object->url_from !== '' || $match_object->url_notfrom !== '' ) && $match_object->referrer !== '' ) {
|
||||
$referrer = $match_object->regex ? $this->encode_regex( $match_object->referrer ) : $this->encode_from( $match_object->referrer, false );
|
||||
$to = false;
|
||||
$match_data = $item->get_match_data();
|
||||
|
||||
if ( $match_object->url_from !== '' && $match_data !== null ) {
|
||||
$to = $this->target( $item->get_action_type(), $match_object->url_from, $item->get_action_code(), $match_data );
|
||||
}
|
||||
|
||||
if ( $match_object->url_notfrom !== '' && $match_data !== null ) {
|
||||
$to = $this->target( $item->get_action_type(), $match_object->url_notfrom, $item->get_action_code(), $match_data );
|
||||
}
|
||||
|
||||
$this->items[] = sprintf( 'RewriteCond %%{HTTP_REFERER} %s [NC]', $referrer );
|
||||
if ( $to !== false ) {
|
||||
$this->items[] = sprintf( 'RewriteRule %s %s', $from, $to );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a useragent redirect
|
||||
*
|
||||
* @param Red_Item $item Redirect item.
|
||||
* @param Agent_Match $match_object Redirect match.
|
||||
* @return void
|
||||
*/
|
||||
private function add_agent( $item, $match_object ) {
|
||||
$from = $this->encode( ltrim( $item->get_url(), '/' ) );
|
||||
if ( $item->is_regex() ) {
|
||||
$from = $this->encode_regex( ltrim( $item->get_url(), '/' ) );
|
||||
}
|
||||
|
||||
if ( ( $match_object->url_from !== '' || $match_object->url_notfrom !== '' ) && $match_object->agent !== '' ) {
|
||||
$agent = ( $match_object->regex ? $this->encode_regex( $match_object->agent ) : $this->encode2nd( $match_object->agent ) );
|
||||
$to = false;
|
||||
$match_data = $item->get_match_data();
|
||||
|
||||
if ( $match_object->url_from !== '' && $match_data !== null ) {
|
||||
$to = $this->target( $item->get_action_type(), $match_object->url_from, $item->get_action_code(), $match_data );
|
||||
}
|
||||
|
||||
if ( $match_object->url_notfrom !== '' && $match_data !== null ) {
|
||||
$to = $this->target( $item->get_action_type(), $match_object->url_notfrom, $item->get_action_code(), $match_data );
|
||||
}
|
||||
|
||||
$this->items[] = sprintf( 'RewriteCond %%{HTTP_USER_AGENT} %s [NC]', $agent );
|
||||
if ( $to !== false ) {
|
||||
$this->items[] = sprintf( 'RewriteRule %s %s', $from, $to );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a server redirect
|
||||
*
|
||||
* @param Red_Item $item Redirect item.
|
||||
* @param Server_Match $match_object Redirect match.
|
||||
* @return void
|
||||
*/
|
||||
private function add_server( $item, $match_object ) {
|
||||
// Temporarily set url property for add_url method
|
||||
$host = wp_parse_url( $match_object->server, PHP_URL_HOST );
|
||||
if ( is_string( $host ) ) {
|
||||
$this->items[] = sprintf( 'RewriteCond %%{HTTP_HOST} ^%s$ [NC]', preg_quote( $host, '/' ) );
|
||||
}
|
||||
$this->add_url( $item, $match_object->url_from );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a redirect
|
||||
*
|
||||
* @param Red_Item $item Redirect item.
|
||||
* @param string $target_url Target URL.
|
||||
* @return void
|
||||
*/
|
||||
private function add_url( $item, $target_url ) {
|
||||
$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 = '';
|
||||
$match_data = $item->get_match_data();
|
||||
if ( $match_data !== null && $target_url !== '' ) {
|
||||
$to = $this->target( $item->get_action_type(), $target_url, $item->get_action_code(), $match_data );
|
||||
}
|
||||
|
||||
$from = $this->encode_from( $url, $item->source_flags !== null && $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
|
||||
*
|
||||
* @param string $current Current redirect rule.
|
||||
* @param array<string> $flags Flags to add.
|
||||
* @return string
|
||||
*/
|
||||
private function add_flags( string $current, array $flags ) {
|
||||
return $current . ' [' . implode( ',', $flags ) . ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source flags
|
||||
*
|
||||
* @param array<string> $existing Existing flags.
|
||||
* @param array<string, mixed> $source Source flags.
|
||||
* @param string $url URL.
|
||||
* @return array<string>
|
||||
*/
|
||||
private function get_source_flags( array $existing, array $source, string $url ) {
|
||||
$flags = [];
|
||||
|
||||
if ( isset( $source['flag_case'] ) && $source['flag_case'] !== false ) {
|
||||
$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 string $data Target URL data.
|
||||
* @param int $code HTTP status code.
|
||||
* @param array<string, mixed> $match_data Match data including source flags.
|
||||
* @return string
|
||||
*/
|
||||
private function action_random( string $data, int $code, array $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" );
|
||||
$permalink = get_permalink( $post );
|
||||
if ( $permalink === false ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$url = wp_parse_url( $permalink );
|
||||
if ( $url === false || ! isset( $url['path'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$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 string $data Target URL data.
|
||||
* @param int $code HTTP status code.
|
||||
* @param array<string, mixed> $match_data Match data including source flags.
|
||||
* @return string
|
||||
*/
|
||||
private function action_pass( string $data, int $code, array $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 string $data Target URL data.
|
||||
* @param int $code HTTP status code.
|
||||
* @param array<string, mixed> $match_data Match data including source flags.
|
||||
* @return string
|
||||
*/
|
||||
private function action_error( string $data, int $code, array $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 string $data Target URL data.
|
||||
* @param int $code HTTP status code.
|
||||
* @param array<string, mixed> $match_data Match data including source flags.
|
||||
* @return string
|
||||
*/
|
||||
private function action_url( string $data, int $code, array $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 string $action Action type.
|
||||
* @param string $data Target URL data.
|
||||
* @param int $code HTTP status code.
|
||||
* @param array<string, mixed> $match_data Match data including source flags.
|
||||
* @return string
|
||||
*/
|
||||
private function target( string $action, string $data, int $code, array $match_data ) {
|
||||
$target = 'action_' . $action;
|
||||
|
||||
if ( method_exists( $this, $target ) ) {
|
||||
return $this->$target( $data, $code, $match_data ); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the .htaccess file in memory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generate() {
|
||||
$version = red_get_plugin_data( dirname( __DIR__ ) . '/redirection.php' );
|
||||
|
||||
if ( count( $this->items ) === 0 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = [
|
||||
'# Created by Redirection',
|
||||
'# ' . gmdate( 'r' ),
|
||||
'# Redirection ' . trim( $version['Version'] ) . ' - https://redirection.me',
|
||||
'',
|
||||
'<IfModule mod_rewrite.c>',
|
||||
];
|
||||
|
||||
// Add http => https option
|
||||
$options = Red_Options::get();
|
||||
if ( $options['https'] !== false ) {
|
||||
$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() ) {
|
||||
// For URL matches, extract target URL from match object
|
||||
if ( $target === 'add_url' && $item->match instanceof URL_Match ) {
|
||||
$this->add_url( $item, $item->match->url );
|
||||
} else {
|
||||
$this->$target( $item, $item->match ); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the .htaccess file
|
||||
*
|
||||
* @param string|false $existing Existing .htaccess data.
|
||||
* @return string
|
||||
*/
|
||||
public function get( $existing = false ) {
|
||||
$text = $this->generate();
|
||||
|
||||
if ( $existing !== false ) {
|
||||
if ( preg_match( self::INSERT_REGEX, $existing ) > 0 ) {
|
||||
$text = (string) 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 = (string) 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 (unused parameter).
|
||||
* @return bool
|
||||
*/
|
||||
public function save( $filename, $content_to_save = false ) {
|
||||
$existing = false;
|
||||
$filename = $this->sanitize_filename( $filename );
|
||||
|
||||
// Initialize WP_Filesystem
|
||||
global $wp_filesystem;
|
||||
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
// Initialize the filesystem with direct method
|
||||
if ( WP_Filesystem() === null ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read existing file contents if file exists
|
||||
if ( $wp_filesystem->exists( $filename ) ) {
|
||||
$file_contents = $wp_filesystem->get_contents( $filename );
|
||||
if ( $file_contents !== false ) {
|
||||
$existing = $file_contents;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the file
|
||||
return $wp_filesystem->put_contents( $filename, $this->get( $existing ), FS_CHMOD_FILE );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
// Loader for importer classes
|
||||
require_once __DIR__ . '/importer/base.php';
|
||||
require_once __DIR__ . '/importer/pretty-links.php';
|
||||
require_once __DIR__ . '/importer/quick-redirects.php';
|
||||
require_once __DIR__ . '/importer/rank-math.php';
|
||||
require_once __DIR__ . '/importer/simple301.php';
|
||||
require_once __DIR__ . '/importer/wordpress-old-slugs.php';
|
||||
require_once __DIR__ . '/importer/seo-redirection.php';
|
||||
require_once __DIR__ . '/importer/safe-redirect-manager.php';
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type ImporterInfo array{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* total: int
|
||||
* }
|
||||
*/
|
||||
abstract class Red_Plugin_Importer {
|
||||
/**
|
||||
* @return list<array{id: string, name: string, total: int}>
|
||||
*/
|
||||
public static function get_plugins(): array {
|
||||
$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_id ) {
|
||||
$importer = self::get_importer( $importer_id );
|
||||
if ( ! $importer instanceof Red_Plugin_Importer ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $importer->get_data();
|
||||
if ( $data === false || $data['total'] === 0 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[] = $data;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an importer instance by ID.
|
||||
*
|
||||
* @param string $id Importer identifier.
|
||||
* @return Red_Plugin_Importer|false
|
||||
*/
|
||||
public static function get_importer( string $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import all redirects for a plugin ID into a target group.
|
||||
*
|
||||
* @param string $plugin Importer identifier.
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
public static function import( $plugin, $group_id ) {
|
||||
$importer = self::get_importer( $plugin );
|
||||
if ( $importer !== false ) {
|
||||
return $importer->import_plugin( $group_id );
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import using a specific importer instance.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
abstract public function import_plugin( $group_id );
|
||||
|
||||
/**
|
||||
* Get importer summary data used by UI/CLI.
|
||||
*
|
||||
* @return ImporterInfo|false
|
||||
*/
|
||||
abstract public function get_data();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ImporterInfo from Red_Plugin_Importer
|
||||
*/
|
||||
class Red_PrettyLinks_Importer extends Red_Plugin_Importer {
|
||||
/**
|
||||
* Import redirects from Pretty Links.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
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 instanceof Red_Item ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redirection item for a Pretty Links row.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @param stdClass $link Row from prli_links.
|
||||
* @return Red_Item|WP_Error Created redirect or error.
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer summary for Pretty Links.
|
||||
*
|
||||
* @return ImporterInfo|false
|
||||
*/
|
||||
public function get_data() {
|
||||
$data = get_option( 'prli_db_version' );
|
||||
|
||||
if ( $data !== false ) {
|
||||
global $wpdb;
|
||||
|
||||
return [
|
||||
'id' => 'pretty-links',
|
||||
'name' => 'PrettyLinks',
|
||||
'total' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}prli_links" ),
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ImporterInfo from Red_Plugin_Importer
|
||||
*/
|
||||
class Red_QuickRedirect_Importer extends Red_Plugin_Importer {
|
||||
/**
|
||||
* Import redirects from Quick Page/Post Redirects.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
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 instanceof Red_Item ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redirection item for a given source/target pair.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @param string $source Source URL.
|
||||
* @param string $target Target URL.
|
||||
* @return Red_Item|WP_Error Created redirect or error.
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer summary for Quick Page/Post Redirects.
|
||||
*
|
||||
* @return ImporterInfo|false
|
||||
*/
|
||||
public function get_data() {
|
||||
$data = get_option( 'quickppr_redirects' );
|
||||
|
||||
if ( $data !== false ) {
|
||||
return array(
|
||||
'id' => 'quick-redirects',
|
||||
'name' => 'Quick Page/Post Redirects',
|
||||
'total' => count( $data ),
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
class Red_RankMath_Importer extends Red_Plugin_Importer {
|
||||
/**
|
||||
* Import redirects from RankMath.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one or more Redirection items for a RankMath row.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @param stdClass $redirect Row from rank_math_redirections.
|
||||
* @return int Number of created redirects for this row.
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer summary for RankMath.
|
||||
*
|
||||
* @return array{id: string, name: string, total: int}|false
|
||||
*/
|
||||
public function get_data() {
|
||||
global $wpdb;
|
||||
|
||||
if ( defined( 'REDIRECTION_TESTS' ) && REDIRECTION_TESTS ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ImporterInfo from Red_Plugin_Importer
|
||||
*/
|
||||
class Red_SafeRedirectManager_Importer extends Red_Plugin_Importer {
|
||||
/**
|
||||
* Import redirects from Safe Redirect Manager.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
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 instanceof Red_Item ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redirection item from a collected SRM post meta map.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @param array<string,string> $post Map of SRM fields for a single link.
|
||||
* @return Red_Item|WP_Error Created redirect or error.
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer summary for Safe Redirect Manager.
|
||||
*
|
||||
* @return ImporterInfo|false
|
||||
*/
|
||||
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 !== null ) {
|
||||
return array(
|
||||
'id' => 'safe-redirect-manager',
|
||||
'name' => 'Safe Redirect Manager',
|
||||
'total' => intval( $total, 10 ),
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ImporterInfo from Red_Plugin_Importer
|
||||
*/
|
||||
class Red_SeoRedirection_Importer extends Red_Plugin_Importer {
|
||||
/**
|
||||
* Import redirects from SEO Redirection.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
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 instanceof Red_Item ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redirection item for an SEO Redirection row.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @param stdClass $seo Row from WP_SEO_Redirection.
|
||||
* @return Red_Item|WP_Error|false Created redirect, error, or false if disabled.
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer summary for SEO Redirection.
|
||||
*
|
||||
* @return ImporterInfo|false
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ImporterInfo from Red_Plugin_Importer
|
||||
*/
|
||||
class Red_Simple301_Importer extends Red_Plugin_Importer {
|
||||
/**
|
||||
* Import redirects from Simple 301 Redirects.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
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 instanceof Red_Item ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redirection item for a given source/target pair.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @param string $source Source pattern from Simple 301.
|
||||
* @param string $target Target URL from Simple 301.
|
||||
* @return Red_Item|WP_Error Created redirect or error.
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer summary for Simple 301 Redirects.
|
||||
*
|
||||
* @return ImporterInfo|false
|
||||
*/
|
||||
public function get_data() {
|
||||
$data = get_option( '301_redirects' );
|
||||
|
||||
if ( $data !== false ) {
|
||||
return array(
|
||||
'id' => 'wp-simple-redirect',
|
||||
'name' => 'Simple 301 Redirects',
|
||||
'total' => count( $data ),
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ImporterInfo from Red_Plugin_Importer
|
||||
*/
|
||||
class Red_WordPressOldSlug_Importer extends Red_Plugin_Importer {
|
||||
/**
|
||||
* Import redirects for WordPress old slugs.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @return int Number of imported redirects.
|
||||
*/
|
||||
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 instanceof Red_Item ) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redirection item for a WordPress old slug row.
|
||||
*
|
||||
* @param int $group_id Target group ID.
|
||||
* @param stdClass $redirect Row from postmeta/posts join.
|
||||
* @return Red_Item|WP_Error|false Created redirect, error, or false on permalink error.
|
||||
*/
|
||||
private function create_for_item( $group_id, $redirect ) {
|
||||
$new = get_permalink( $redirect->post_id );
|
||||
if ( $new === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$new_path = wp_parse_url( $new, PHP_URL_PATH );
|
||||
if ( $new_path === false || $new_path === null ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer summary for WordPress old slugs.
|
||||
*
|
||||
* @return ImporterInfo|false
|
||||
*/
|
||||
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 !== null && intval( $total, 10 ) > 0 ) {
|
||||
return array(
|
||||
'id' => 'wordpress-old-slugs',
|
||||
'name' => __( 'Default WordPress "old slugs"', 'redirection' ),
|
||||
'total' => intval( $total, 10 ),
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* IP address handler for validating and normalizing IP addresses
|
||||
*/
|
||||
class Redirection_IP {
|
||||
/**
|
||||
* Validated and normalized IP address
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $ip = '';
|
||||
|
||||
/**
|
||||
* Constructor. Validates and normalizes an IP address
|
||||
*
|
||||
* @param string $ip IP address to validate (may be comma-separated list, first value will be used).
|
||||
*/
|
||||
public function __construct( $ip = '' ) {
|
||||
$ip = sanitize_text_field( $ip );
|
||||
$ip = explode( ',', $ip );
|
||||
$ip = array_shift( $ip );
|
||||
$ip = filter_var( $ip, FILTER_VALIDATE_IP );
|
||||
if ( $ip === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to binary
|
||||
// phpcs:ignore
|
||||
$ip = @inet_pton( trim( $ip ) );
|
||||
if ( $ip !== false ) {
|
||||
// phpcs:ignore
|
||||
$ip = @inet_ntop( $ip ); // Convert back to string;
|
||||
if ( $ip === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ip = $ip;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validated IP address
|
||||
*
|
||||
* @return string Validated IP address, or empty string if invalid.
|
||||
*/
|
||||
public function get() {
|
||||
return $this->ip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-type Log404Row object{
|
||||
* id: int,
|
||||
* created: string,
|
||||
* url: string,
|
||||
* agent: string,
|
||||
* referrer: string,
|
||||
* ip: string,
|
||||
* domain?: string
|
||||
* }
|
||||
*
|
||||
* 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 \wpdb $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<string, mixed> $details Other log details.
|
||||
* @return int|false Log ID, or false
|
||||
*/
|
||||
public static function create( $domain, $url, $ip, array $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<int, string>
|
||||
*/
|
||||
public static function get_csv_header() {
|
||||
return [ 'date', 'source', 'ip', 'referrer', 'useragent' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSV row for this log object
|
||||
*
|
||||
* @param object $row Log row.
|
||||
* @return array<int, string|int>
|
||||
*/
|
||||
public static function get_csv_row( $row ) {
|
||||
/** @var Log404Row $row */
|
||||
return [
|
||||
$row->created,
|
||||
$row->url,
|
||||
$row->ip,
|
||||
$row->referrer,
|
||||
$row->agent,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type LogJson from Red_Log
|
||||
* @phpstan-import-type LogFilterParams from Red_Log
|
||||
* @phpstan-import-type LogGetParams from Red_Log
|
||||
* @phpstan-type RedirectCsvRow object{
|
||||
* created: string,
|
||||
* url: string,
|
||||
* sent_to: string,
|
||||
* ip: string,
|
||||
* referrer: string,
|
||||
* agent: string
|
||||
* }
|
||||
* @phpstan-type RedirectLogJson array{
|
||||
* id: int,
|
||||
* created: string,
|
||||
* created_time: string,
|
||||
* url: string,
|
||||
* agent: string,
|
||||
* referrer: string,
|
||||
* domain: string,
|
||||
* ip: string,
|
||||
* http_code: int,
|
||||
* request_method: string,
|
||||
* request_data: mixed,
|
||||
* sent_to: string,
|
||||
* redirection_id: int,
|
||||
* redirect_by_slug: string,
|
||||
* redirect_by: string
|
||||
* }
|
||||
*
|
||||
* 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 \wpdb $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<string, mixed> $details Other log details.
|
||||
* @return int|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
|
||||
*
|
||||
* @phpstan-param LogFilterParams & array{target?: string, redirect_by?: string} $filter
|
||||
* @phpstan-return list<string>
|
||||
* @return array
|
||||
*/
|
||||
protected static function get_query_filter( array $filter ) {
|
||||
global $wpdb;
|
||||
|
||||
$where = parent::get_query_filter( $filter );
|
||||
|
||||
/** @var array{target?: string, redirect_by?: string} $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<int, string>
|
||||
*/
|
||||
public static function get_csv_header() {
|
||||
return [ 'date', 'source', 'target', 'ip', 'referrer', 'agent' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSV row for this log object
|
||||
*
|
||||
* @param object $row Log row.
|
||||
* @phpstan-param object $row
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function get_csv_row( $row ) {
|
||||
/** @var RedirectCsvRow $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
|
||||
*
|
||||
* @phpstan-return RedirectLogJson
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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 === null ? '' : $this->redirect_by ),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/log-404.php';
|
||||
require_once __DIR__ . '/log-redirect.php';
|
||||
|
||||
/**
|
||||
* Base log class
|
||||
*
|
||||
* @phpstan-type LogJson array{
|
||||
* id: int,
|
||||
* created: string,
|
||||
* created_time: string,
|
||||
* url: string,
|
||||
* agent: string,
|
||||
* referrer: string,
|
||||
* domain: string,
|
||||
* ip: string,
|
||||
* http_code: int,
|
||||
* request_method: string,
|
||||
* request_data: mixed
|
||||
* }
|
||||
* @phpstan-type LogQuery array{
|
||||
* orderby: string,
|
||||
* direction: string,
|
||||
* limit: int,
|
||||
* offset: int,
|
||||
* where: string
|
||||
* }
|
||||
* @phpstan-type LogFilterParams array{
|
||||
* ip?: string,
|
||||
* domain?: string,
|
||||
* 'url-exact'?: string,
|
||||
* url?: string,
|
||||
* referrer?: string,
|
||||
* agent?: string,
|
||||
* http?: int,
|
||||
* method?: string
|
||||
* }
|
||||
* @phpstan-type LogGetParams array{
|
||||
* orderby?: 'ip'|'url',
|
||||
* direction?: 'ASC'|'DESC',
|
||||
* per_page?: int,
|
||||
* page?: int,
|
||||
* filterBy?: LogFilterParams
|
||||
* }
|
||||
*/
|
||||
abstract class Red_Log {
|
||||
const MAX_IP_LENGTH = 45;
|
||||
const MAX_DOMAIN_LENGTH = 255;
|
||||
const MAX_URL_LENGTH = 2000;
|
||||
const DEFAULT_PER_PAGE = 25;
|
||||
const MAX_PER_PAGE = 200;
|
||||
const MAX_AGENT_LENGTH = 255;
|
||||
const MAX_REFERRER_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Supported HTTP methods
|
||||
*
|
||||
* @phpstan-var list<string>
|
||||
* @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 LogJson $values Array of log values.
|
||||
*/
|
||||
final public function __construct( $values ) {
|
||||
foreach ( $values as $key => $value ) {
|
||||
// @phpstan-ignore property.notFound, assign.propertyType
|
||||
$this->$key = $value;
|
||||
}
|
||||
|
||||
// @phpstan-ignore isset.offset
|
||||
if ( isset( $values['created'] ) ) {
|
||||
$converted = mysql2date( 'U', $values['created'] );
|
||||
|
||||
if ( $converted !== false ) {
|
||||
$this->created = intval( $converted, 10 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's the table name for this log object
|
||||
*
|
||||
* @param \wpdb $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
|
||||
*
|
||||
* @phpstan-param LogGetParams $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
|
||||
*
|
||||
* @phpstan-return LogJson
|
||||
* @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<string, mixed> $params Filters.
|
||||
* @phpstan-return array{items: list<LogJson>, total: int}
|
||||
* @return array
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/** @var list<LogJson> $items */
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => intval( $total_items, 10 ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grouped log entries
|
||||
*
|
||||
* @param string $group Group type ('ip'|'url'|'agent').
|
||||
* @param array<string, mixed> $params Filter params.
|
||||
* @phpstan-return array{items: list<object>, total: int}
|
||||
* @return array
|
||||
*/
|
||||
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 );
|
||||
$row->id = isset( $row->{ $group } ) ? $row->{ $group } : '';
|
||||
}
|
||||
|
||||
/** @var list<object> $rows */
|
||||
return array(
|
||||
'items' => $rows,
|
||||
'total' => intval( $total_items, 10 ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a set of filters to a SQL query.
|
||||
*
|
||||
* @param array<string, mixed> $params Filters.
|
||||
* @phpstan-return LogQuery
|
||||
* @return array
|
||||
*/
|
||||
public static function get_query( array $params ) {
|
||||
$query = [
|
||||
'orderby' => 'id',
|
||||
'direction' => 'DESC',
|
||||
'limit' => self::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 <= self::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
|
||||
*
|
||||
* @phpstan-param LogFilterParams $filter Array of filter params.
|
||||
* @phpstan-return list<string>
|
||||
* @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<string, mixed> $details Extra log details.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<int, string>
|
||||
*/
|
||||
public static function get_csv_header() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSV row for this log object
|
||||
*
|
||||
* @param object $row Log row.
|
||||
* @return array<int, string|int>
|
||||
*/
|
||||
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 === false ) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
require_once dirname( __DIR__ ) . '/matches/from-notfrom.php';
|
||||
require_once dirname( __DIR__ ) . '/matches/from-url.php';
|
||||
|
||||
/**
|
||||
* Matches a URL and some other condition
|
||||
*
|
||||
* @phpstan-template TSaveDetails of array<string, mixed>
|
||||
* @phpstan-template-covariant TSaveResult of (array<string, mixed>|string)
|
||||
*
|
||||
* @phpstan-type RedMatchUrlData array{
|
||||
* url: string
|
||||
* }
|
||||
* @phpstan-type RedMatchAgentData array{
|
||||
* agent: string,
|
||||
* regex: bool,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchReferrerData array{
|
||||
* referrer: string,
|
||||
* regex: bool,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchHeaderData array{
|
||||
* name: string,
|
||||
* value: string,
|
||||
* regex: bool,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchCookieData array{
|
||||
* name: string,
|
||||
* value: string,
|
||||
* regex: bool,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchCustomData array{
|
||||
* filter: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchRoleData array{
|
||||
* role: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchServerData array{
|
||||
* server: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchIpData array{
|
||||
* ip: string[],
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchPageData array{
|
||||
* page: string,
|
||||
* url: string
|
||||
* }
|
||||
* @phpstan-type RedMatchLanguageData array{
|
||||
* language: string,
|
||||
* url_from: string,
|
||||
* url_notfrom: string
|
||||
* }
|
||||
* @phpstan-type RedMatchLoginData array{
|
||||
* logged_in: string,
|
||||
* logged_out: string
|
||||
* }
|
||||
* @phpstan-type RedMatchData RedMatchUrlData|RedMatchAgentData|RedMatchReferrerData|RedMatchHeaderData|RedMatchCookieData|RedMatchCustomData|RedMatchRoleData|RedMatchServerData|RedMatchIpData|RedMatchPageData|RedMatchLanguageData|RedMatchLoginData
|
||||
*/
|
||||
abstract class Red_Match {
|
||||
/**
|
||||
* Match type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param RedMatchData|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
|
||||
*
|
||||
* @phpstan-param TSaveDetails $details Details to save.
|
||||
* @param array<string, mixed> $details Details to save.
|
||||
* @param boolean $no_target_url The URL when no target.
|
||||
* @phpstan-return TSaveResult|null
|
||||
* @return array<string, mixed>|string|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 RedMatchData|null
|
||||
*/
|
||||
abstract public function get_data();
|
||||
|
||||
/**
|
||||
* Load the match data into this instance.
|
||||
*
|
||||
* @param RedMatchData|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 = (string) preg_replace( "/[\r\n\t].*?$/s", '', $url );
|
||||
|
||||
// Clean control codes
|
||||
$url = (string) 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 RedMatchData|string $data Match data.
|
||||
* @return Red_Match<array<string, mixed>, (array<string, mixed>|string)>|null
|
||||
*/
|
||||
public static function create( $name, $data = '' ) {
|
||||
$avail = self::available();
|
||||
if ( isset( $avail[ strtolower( $name ) ] ) ) {
|
||||
$classname = $name . '_match';
|
||||
|
||||
/** @var class-string<Red_Match<array<string, mixed>, (array<string, mixed>|string)>> $classname */
|
||||
if ( ! class_exists( strtolower( $classname ) ) ) {
|
||||
include __DIR__ . '/../matches/' . $avail[ strtolower( $name ) ];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Red_Match<array<string, mixed>, (array<string, mixed>|string)>
|
||||
*/
|
||||
$class = new $classname( $data );
|
||||
$class->type = $name;
|
||||
return $class;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Red_Match objects
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function all() {
|
||||
$data = [];
|
||||
|
||||
$avail = self::available();
|
||||
foreach ( array_keys( $avail ) as $name ) {
|
||||
/** @var Red_Match<array<string, mixed>, (array<string, mixed>|string)>|null $obj */
|
||||
$obj = self::create( $name );
|
||||
if ( $obj === null ) {
|
||||
continue;
|
||||
}
|
||||
$data[ $name ] = $obj->name();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available matches
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
require_once dirname( __DIR__ ) . '/modules/wordpress.php';
|
||||
require_once dirname( __DIR__ ) . '/modules/apache.php';
|
||||
require_once dirname( __DIR__ ) . '/modules/nginx.php';
|
||||
|
||||
/**
|
||||
* Base class for redirect module.
|
||||
*
|
||||
* @phpstan-type WordPressModuleOptions array{}
|
||||
* @phpstan-type ApacheModuleOptions array{
|
||||
* location: string
|
||||
* }
|
||||
* @phpstan-type NginxModuleOptions array{
|
||||
* location: string
|
||||
* }
|
||||
* @phpstan-type RedModuleOptions WordPressModuleOptions|ApacheModuleOptions|NginxModuleOptions
|
||||
*/
|
||||
abstract class Red_Module {
|
||||
/**
|
||||
* Constructor. Loads options
|
||||
*
|
||||
* @param RedModuleOptions $options Any module options.
|
||||
*/
|
||||
public function __construct( array $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_Options::get();
|
||||
|
||||
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 !== false ) {
|
||||
$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 !== false ) {
|
||||
$module->flush_module();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module ID
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
abstract public function get_id();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_name();
|
||||
|
||||
/**
|
||||
* Update
|
||||
*
|
||||
* @param RedModuleOptions $data Data.
|
||||
* @return array<string, string>|false
|
||||
*/
|
||||
public function update( array $data ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load
|
||||
*
|
||||
* @param RedModuleOptions $options Options.
|
||||
* @return void
|
||||
*/
|
||||
protected function load( array $options ) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function flush_module() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
class Red_Monitor {
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $monitor_group_id = 0;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private $updated_posts = array();
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $monitor_types = array();
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $associated = '';
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct( array $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' ) !== false ) {
|
||||
add_action( 'pre_post_update', array( $this, 'pre_post_update' ), 10, 2 );
|
||||
add_action( 'post_updated', array( $this, 'post_updated' ), 11, 3 );
|
||||
add_action( '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, true ) ) {
|
||||
add_action( 'wp_trash_post', array( $this, 'post_trashed' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return void
|
||||
*/
|
||||
public function remove_existing_redirect( string $url ): void {
|
||||
Red_Item::disable_where_matches( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WP_Post|null $post
|
||||
* @param WP_Post|null $post_before
|
||||
* @return bool
|
||||
*/
|
||||
public function can_monitor_post( ?WP_Post $post, ?WP_Post $post_before ): bool {
|
||||
// Defensive check: ensure we have valid post objects
|
||||
if ( $post === null || $post_before === null ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check this is for the expected post
|
||||
// @phpstan-ignore isset.property
|
||||
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, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a post has been updated - check if the slug has changed
|
||||
*
|
||||
* @param int $post_id
|
||||
* @param WP_Post|null $post
|
||||
* @param WP_Post|null $post_before
|
||||
* @return void
|
||||
*/
|
||||
public function post_updated( int $post_id, ?WP_Post $post, ?WP_Post $post_before ): void {
|
||||
// WordPress may pass null during trash/delete operations - handle gracefully
|
||||
if ( $post === null || $post_before === null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* @param int $post_id
|
||||
* @param array<string, mixed> $data
|
||||
* @return void
|
||||
*/
|
||||
public function pre_post_update( int $post_id, array $data ): void {
|
||||
$permalink = get_permalink( $post_id );
|
||||
if ( $permalink !== false ) {
|
||||
$this->updated_posts[ $post_id ] = $permalink;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @return void
|
||||
*/
|
||||
public function post_trashed( int $post_id ): void {
|
||||
// Only create redirects for post types that are being monitored
|
||||
$post_type = get_post_type( $post_id );
|
||||
if ( $post_type === false || ! in_array( $post_type, $this->monitor_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$permalink = get_permalink( $post_id );
|
||||
if ( $permalink === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'url' => wp_parse_url( $permalink, PHP_URL_PATH ),
|
||||
'action_data' => array( 'url' => '/' ),
|
||||
'match_type' => 'url',
|
||||
'action_type' => 'url',
|
||||
'action_code' => 301,
|
||||
'group_id' => $this->monitor_group_id,
|
||||
'status' => 'disabled',
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the redirect data before creating a redirect for a trashed post.
|
||||
*
|
||||
* @param array $data The redirect data to be created.
|
||||
* @param int $post_id The ID of the trashed post.
|
||||
*/
|
||||
$data = apply_filters( 'redirection_monitor_trashed_data', $data, $post_id );
|
||||
|
||||
// Create a new redirect for this post, but only if not draft
|
||||
if ( $data['url'] !== null && $data['url'] !== false && $data['url'] !== '/' ) {
|
||||
$new_item = Red_Item::create( $data );
|
||||
|
||||
if ( ! is_wp_error( $new_item ) ) {
|
||||
do_action( 'redirection_monitor_created', $new_item, $data['url'], $post_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changed if permalinks are different and the before wasn't the site url (we don't want to redirect the site URL)
|
||||
*
|
||||
* @param bool $result
|
||||
* @param string|false $before
|
||||
* @param string|false $after
|
||||
* @return bool
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function get_site_path(): string {
|
||||
$path = wp_parse_url( get_site_url(), PHP_URL_PATH );
|
||||
|
||||
if ( is_string( $path ) ) {
|
||||
return rtrim( $path, '/' ) . '/';
|
||||
}
|
||||
|
||||
return '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param string $before
|
||||
* @return bool
|
||||
*/
|
||||
public function check_for_modified_slug( int $post_id, string $before ): bool {
|
||||
$permalink = get_permalink( $post_id );
|
||||
if ( $permalink === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$after = wp_parse_url( $permalink, PHP_URL_PATH );
|
||||
$before = wp_parse_url( esc_url( $before ), PHP_URL_PATH );
|
||||
|
||||
if ( is_string( $before ) && is_string( $after ) && 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Redirection options manager - pure static utility class.
|
||||
*
|
||||
* Usage: Red_Options::get() returns a fully typed array.
|
||||
*
|
||||
* @phpstan-type RedirectionOptions array{
|
||||
* support: bool,
|
||||
* token: string,
|
||||
* monitor_post: int,
|
||||
* monitor_types: array<string>,
|
||||
* associated_redirect: string,
|
||||
* auto_target: string,
|
||||
* expire_redirect: int,
|
||||
* expire_404: int,
|
||||
* log_external: bool,
|
||||
* log_header: bool,
|
||||
* track_hits: bool,
|
||||
* modules: array<int, mixed>,
|
||||
* redirect_cache: int,
|
||||
* ip_logging: int,
|
||||
* ip_headers: array<string>,
|
||||
* ip_proxy: array<string>,
|
||||
* last_group_id: int,
|
||||
* rest_api: int,
|
||||
* https: bool,
|
||||
* headers: array<mixed>,
|
||||
* database: string,
|
||||
* relocate: string,
|
||||
* preferred_domain: string,
|
||||
* aliases: array<string>,
|
||||
* permalinks: array<mixed>,
|
||||
* cache_key: int,
|
||||
* plugin_update: string,
|
||||
* update_notice: int,
|
||||
* flag_query: 'ignore'|'exact'|'pass'|'exactorder',
|
||||
* flag_case: bool,
|
||||
* flag_trailing: bool,
|
||||
* flag_regex: bool,
|
||||
* database_stage?: array{stage?: string|false, stages?: array<mixed>, status?: string|false},
|
||||
* location?: string
|
||||
* }
|
||||
*/
|
||||
class Red_Options {
|
||||
/**
|
||||
* Options DB key. Previously defined by REDIRECTION_OPTION.
|
||||
*/
|
||||
public const OPTION_KEY = 'redirection_options';
|
||||
|
||||
/**
|
||||
* REST API location constants. Previously REDIRECTION_API_JSON*, now centralized here.
|
||||
*/
|
||||
public const API_JSON = 0;
|
||||
public const API_JSON_INDEX = 1;
|
||||
public const API_JSON_RELATIVE = 3;
|
||||
|
||||
/**
|
||||
* In-memory cache for build_options result.
|
||||
*
|
||||
* @var RedirectionOptions|null
|
||||
*/
|
||||
private static $options_cache = null;
|
||||
|
||||
/**
|
||||
* Prevent instantiation - this is a static utility class.
|
||||
*/
|
||||
private function __construct() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current options and return as a typed array.
|
||||
*
|
||||
* @phpstan-return RedirectionOptions
|
||||
* @return array
|
||||
*/
|
||||
public static function get(): array {
|
||||
return self::build_options();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the options cache.
|
||||
* @return void
|
||||
*/
|
||||
public static function reset() {
|
||||
self::$options_cache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Redirection options (array form).
|
||||
* Mirrors legacy red_get_options() behavior.
|
||||
*
|
||||
* @phpstan-return RedirectionOptions
|
||||
* @return array
|
||||
*/
|
||||
private static function build_options(): array {
|
||||
// Return cached result if available
|
||||
if ( self::$options_cache !== null ) {
|
||||
return self::$options_cache;
|
||||
}
|
||||
|
||||
$options = get_option( self::OPTION_KEY );
|
||||
$fresh_install = false;
|
||||
|
||||
if ( $options === false ) {
|
||||
$fresh_install = true;
|
||||
}
|
||||
|
||||
if ( ! is_array( $options ) ) {
|
||||
$options = [];
|
||||
}
|
||||
|
||||
if ( red_is_disabled() ) {
|
||||
$options['https'] = false;
|
||||
}
|
||||
|
||||
$defaults = self::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 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'], [ self::API_JSON, self::API_JSON_INDEX, self::API_JSON_RELATIVE ], true ) ) {
|
||||
$options['rest_api'] = self::API_JSON;
|
||||
}
|
||||
|
||||
if ( isset( $options['modules'] ) && isset( $options['modules']['2'] ) && isset( $options['modules']['2']['location'] ) ) {
|
||||
$options['location'] = $options['modules']['2']['location'];
|
||||
}
|
||||
|
||||
/** @var RedirectionOptions $options */
|
||||
// Cache the result before returning
|
||||
self::$options_cache = $options;
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default options. Contains all valid options.
|
||||
*
|
||||
* @phpstan-return RedirectionOptions
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function get_default_options() {
|
||||
$flags = new Red_Source_Flags();
|
||||
$defaults = [
|
||||
'support' => false,
|
||||
'token' => md5( uniqid() ),
|
||||
'monitor_post' => 0,
|
||||
'monitor_types' => [],
|
||||
'associated_redirect' => '',
|
||||
'auto_target' => '',
|
||||
'expire_redirect' => 7,
|
||||
'expire_404' => 7,
|
||||
'log_external' => false,
|
||||
'log_header' => false,
|
||||
'track_hits' => true,
|
||||
'modules' => [],
|
||||
'redirect_cache' => 1,
|
||||
'ip_logging' => 0,
|
||||
'ip_headers' => [],
|
||||
'ip_proxy' => [],
|
||||
'last_group_id' => 0,
|
||||
'rest_api' => self::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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist option values using the same sanitization/merge rules as legacy red_set_options.
|
||||
* This is a static method that saves options and returns the updated array.
|
||||
*
|
||||
* @param array<string, mixed> $settings Partial settings to apply.
|
||||
* @phpstan-return RedirectionOptions
|
||||
* @return array
|
||||
*/
|
||||
public static function save( array $settings ): array {
|
||||
// Clear the cache before applying settings
|
||||
self::$options_cache = null;
|
||||
|
||||
$options = self::apply_settings( $settings );
|
||||
update_option( self::OPTION_KEY, apply_filters( 'redirection_save_options', $options ) );
|
||||
|
||||
// Clear the cache after saving to ensure fresh data on next get()
|
||||
self::$options_cache = null;
|
||||
|
||||
/** @var RedirectionOptions $options */
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply settings and return the updated options array without saving.
|
||||
*
|
||||
* @param array<string, mixed> $settings Partial settings to apply.
|
||||
* @phpstan-return RedirectionOptions
|
||||
* @return array
|
||||
*/
|
||||
private static function apply_settings( array $settings ): array {
|
||||
$options = self::build_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'] ) === false && $options['monitor_post'] !== 0 ) {
|
||||
$groups = Red_Group::get_all();
|
||||
|
||||
if ( count( $groups ) > 0 ) {
|
||||
$options['monitor_post'] = $groups[0]['id'];
|
||||
} else {
|
||||
$options['monitor_post'] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'] ) === false ) {
|
||||
$groups = Red_Group::get_all();
|
||||
$options['last_group_id'] = count( $groups ) > 0 ? $groups[0]['id'] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
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 !== false ) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/** @var RedirectionOptions $options */
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Query variables for permalink matching
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $query_vars = [];
|
||||
|
||||
/**
|
||||
* 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 && $query->posts[0] instanceof WP_Post ) {
|
||||
// Restore permalinks
|
||||
$this->release_permalinks();
|
||||
|
||||
// Get real URL from the post ID
|
||||
$url = get_permalink( $query->posts[0]->ID );
|
||||
if ( $url !== false ) {
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO: query_vars doesnt appear to be used
|
||||
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( (int) $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() !== null )
|
||||
// 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<string, string> $rules Current rules.
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function get_old_rewrite_rules( $rules ) {
|
||||
global $wp_rewrite;
|
||||
|
||||
if ( $this->current_permalink !== null ) {
|
||||
$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 !== null ) {
|
||||
return $this->current_permalink;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?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.
|
||||
*
|
||||
* @phpstan-type RedirectSqlRow array{
|
||||
* id: int,
|
||||
* url: string,
|
||||
* match_url: string,
|
||||
* match_data: mixed,
|
||||
* action_code: int,
|
||||
* action_type: string,
|
||||
* action_data: string|null,
|
||||
* match_type: string,
|
||||
* title: string,
|
||||
* hits: int,
|
||||
* regex: bool,
|
||||
* group_id: int,
|
||||
* position: int,
|
||||
* last_access: string,
|
||||
* status: string,
|
||||
* enabled: bool
|
||||
* }
|
||||
*/
|
||||
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<string,bool>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reset() {
|
||||
$settings = Red_Options::get();
|
||||
$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 !== false ) {
|
||||
$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
|
||||
* Returns the JSON representation of redirects (without 'status' field).
|
||||
*
|
||||
* @param Red_Item[] $rows Redirects.
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Filter the redirects
|
||||
*/
|
||||
class Red_Item_Filters {
|
||||
/**
|
||||
* List of filters
|
||||
*
|
||||
* @phpstan-var list<string>
|
||||
* @var array
|
||||
*/
|
||||
private $filters = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array<string, string> $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 '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Options for a redirect source URL
|
||||
*
|
||||
* @phpstan-type SourceOptionsJson array{
|
||||
* log_exclude?: bool
|
||||
* }
|
||||
*/
|
||||
class Red_Source_Options {
|
||||
/**
|
||||
* Exclude this from logging.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $log_exclude = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param SourceOptionsJson|null $options Options.
|
||||
*/
|
||||
public function __construct( $options = null ) {
|
||||
if ( $options !== null ) {
|
||||
$this->set_options( $options );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set options
|
||||
*
|
||||
* @param SourceOptionsJson $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_Options::get();
|
||||
|
||||
if ( $options['expire_redirect'] !== -1 ) {
|
||||
return ! $this->log_exclude;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options as JSON
|
||||
*
|
||||
* @return SourceOptionsJson
|
||||
*/
|
||||
public function get_json() {
|
||||
return array_filter(
|
||||
[
|
||||
'log_exclude' => $this->log_exclude,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @phpstan-import-type RedirectCreateDetails from Red_Item
|
||||
*/
|
||||
|
||||
class Red_Item_Sanitize {
|
||||
/**
|
||||
* Trim strings recursively in an array
|
||||
*
|
||||
* @param array<string, mixed> $array_to_clean
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function clean_array( $array_to_clean ) {
|
||||
foreach ( $array_to_clean as $name => $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
$array_to_clean[ $name ] = $this->clean_array( $value );
|
||||
} elseif ( is_string( $value ) ) {
|
||||
$value = trim( $value );
|
||||
$array_to_clean[ $name ] = $value;
|
||||
} else {
|
||||
$array_to_clean[ $name ] = $value;
|
||||
}
|
||||
};
|
||||
|
||||
return $array_to_clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert absolute URL into server match where appropriate
|
||||
*
|
||||
* @param string $url
|
||||
* @param array<string, mixed> $details
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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 ( is_string( $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 ( $url === null || $url === false ) {
|
||||
$url = '/';
|
||||
}
|
||||
}
|
||||
|
||||
$return['url'] = $url;
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and normalize redirect details
|
||||
*
|
||||
* @param array<string, mixed>|RedirectCreateDetails $details
|
||||
* @return array<string, mixed>|\WP_Error
|
||||
*/
|
||||
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_Options::get();
|
||||
|
||||
// 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
|
||||
// @phpstan-ignore isset.offset
|
||||
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'] === 1 ? true : false );
|
||||
|
||||
if ( isset( $details['hits'] ) ) {
|
||||
$data['last_count'] = intval( $details['hits'], 10 );
|
||||
}
|
||||
|
||||
if ( isset( $details['last_access'] ) ) {
|
||||
$last_access = strtotime( sanitize_text_field( $details['last_access'] ) );
|
||||
if ( $last_access !== false ) {
|
||||
$data['last_access'] = gmdate( 'Y-m-d H:i:s', $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'] === 1 ) {
|
||||
$data['match_url'] = 'regex';
|
||||
}
|
||||
|
||||
if ( is_string( $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'] ) : '' );
|
||||
if ( $matcher === null ) {
|
||||
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'] ) : '', $action_code );
|
||||
if ( $action === null ) {
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $details
|
||||
* @return int
|
||||
*/
|
||||
protected function get_position( $details ) {
|
||||
if ( isset( $details['position'] ) ) {
|
||||
return max( 0, intval( $details['position'], 10 ) );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_url_type( $type ) {
|
||||
if ( $type === 'url' || $type === 'pass' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
* @return bool
|
||||
*/
|
||||
public function is_valid_redirect_code( $code ) {
|
||||
return in_array( $code, array( 301, 302, 303, 304, 307, 308 ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
* @return bool
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action_type
|
||||
* @param int $code
|
||||
* @return int
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $group_id
|
||||
* @return int|\WP_Error
|
||||
*/
|
||||
protected function get_group( $group_id ) {
|
||||
$group_id = intval( $group_id, 10 );
|
||||
|
||||
if ( Red_Group::get( $group_id ) === false ) {
|
||||
return new WP_Error( 'redirect', 'Invalid group when creating redirect' );
|
||||
}
|
||||
|
||||
return $group_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param bool $regex
|
||||
* @return string|\WP_Error
|
||||
*/
|
||||
protected function get_url( $url, $regex ) {
|
||||
$url = self::sanitize_url( $url, $regex );
|
||||
|
||||
if ( $url === '' ) {
|
||||
return new WP_Error( 'redirect', 'Invalid source URL' );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function auto_generate() {
|
||||
$options = Red_Options::get();
|
||||
$url = '';
|
||||
|
||||
if ( strlen( $options['auto_target'] ) > 0 ) {
|
||||
$id = time();
|
||||
$url = str_replace( '$dec$', (string) $id, $options['auto_target'] );
|
||||
$url = str_replace( '$hex$', sprintf( '%x', $id ), $url );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param bool $regex
|
||||
* @return string
|
||||
*/
|
||||
public function sanitize_url( $url, $regex = false ) {
|
||||
$url = wp_kses( $url, 'strip' );
|
||||
$url = str_replace( '&', '&', $url );
|
||||
|
||||
// Make sure that the old URL is relative
|
||||
$url = (string) preg_replace( '@^https?://(.*?)/@', '/', $url );
|
||||
$url = (string) preg_replace( '@^https?://(.*?)$@', '/', $url );
|
||||
|
||||
// No new lines
|
||||
$url = (string) preg_replace( "/[\r\n\t].*?$/s", '', $url );
|
||||
|
||||
// Clean control codes
|
||||
$url = (string) preg_replace( '/[^\PC\s]/u', '', $url );
|
||||
|
||||
// Ensure a slash at start
|
||||
if ( substr( $url, 0, 1 ) !== '/' && $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( '?<!', '?<!', $decoded );
|
||||
}
|
||||
|
||||
// Return the URL
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any bad encoding, where possible
|
||||
*
|
||||
* @param string $text Text.
|
||||
* @return string|false
|
||||
*/
|
||||
/**
|
||||
* @param string $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
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Regular expression helper
|
||||
*/
|
||||
class Red_Regex {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $pattern;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $case;
|
||||
|
||||
/**
|
||||
* @param string $pattern
|
||||
* @param bool $case_insensitive
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
private function encode_path( $path ) {
|
||||
return str_replace( ' ', '%20', $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function get_regex() {
|
||||
$at_escaped = str_replace( '@', '\\@', $this->pattern );
|
||||
$case = '';
|
||||
|
||||
if ( $this->is_ignore_case() ) {
|
||||
$case = 'i';
|
||||
}
|
||||
|
||||
return '@' . $at_escaped . '@s' . $case;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function is_ignore_case() {
|
||||
return $this->case;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
<?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 = (string) preg_replace( "/[\r\n\t].*?$/s", '', $url );
|
||||
|
||||
// Clean control codes
|
||||
$url = (string) preg_replace( '/[^\PC\s]/u', '', $url );
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP headers
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
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_Options::get();
|
||||
$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 = (string) preg_replace( '/;.*$/', '', sanitize_text_field( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) );
|
||||
$languages = str_replace( ' ', '', $languages );
|
||||
|
||||
return apply_filters( 'redirection_request_accept_language', explode( ',', $languages ) );
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* URL encoding handler for source and target URLs
|
||||
*
|
||||
* @phpstan-type EncodingMap array<string, string>
|
||||
*/
|
||||
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 = (string) preg_replace( "/[\r\n\t].*?$/s", '', $url );
|
||||
|
||||
// Remove invalid characters
|
||||
$url = (string) 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 EncodingMap $allowed Map of encoded characters to their replacements.
|
||||
* @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 = (string) preg_replace( '@^\^/@', '^', $url );
|
||||
|
||||
$url = $this->replace_encoding(
|
||||
$url,
|
||||
[
|
||||
'%2A' => '*',
|
||||
'%3F' => '?',
|
||||
'%28' => '(',
|
||||
'%29' => ')',
|
||||
'%5B' => '[',
|
||||
'%5C' => ']',
|
||||
'%24' => '$',
|
||||
'%2B' => '+',
|
||||
'%7C' => '|',
|
||||
'\\.' => '.',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represent URL source flags.
|
||||
*
|
||||
* @phpstan-type FlagName 'flag_query'|'flag_case'|'flag_trailing'|'flag_regex'
|
||||
* @phpstan-type QueryType 'ignore'|'exact'|'pass'|'exactorder'
|
||||
* @phpstan-type FlagsJson array{
|
||||
* flag_query: QueryType,
|
||||
* flag_case: bool,
|
||||
* flag_trailing: bool,
|
||||
* flag_regex: bool
|
||||
* }
|
||||
* @phpstan-import-type RedirectionOptions from Red_Options
|
||||
*/
|
||||
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 (tracks which flag keys were provided)
|
||||
*
|
||||
* @var array<FlagName>
|
||||
*/
|
||||
private $values_set = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array<string, mixed>|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 array<QueryType>
|
||||
*/
|
||||
private function get_allowed_query() {
|
||||
return [
|
||||
self::QUERY_IGNORE,
|
||||
self::QUERY_EXACT,
|
||||
self::QUERY_PASS,
|
||||
self::QUERY_EXACT_ORDER,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse flag data.
|
||||
*
|
||||
* @param array<string, mixed> $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
|
||||
/** @var array<FlagName> $intersected */
|
||||
$intersected = array_values( array_intersect( array_keys( $json ), array_keys( $this->get_json() ) ) );
|
||||
$this->values_set = $intersected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 FlagsJson
|
||||
*/
|
||||
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 RedirectionOptions $defaults Defaults to remove.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get_json_without_defaults( $defaults ) {
|
||||
$json = $this->get_json();
|
||||
|
||||
// @phpstan-ignore greater.alwaysTrue
|
||||
if ( count( $defaults ) > 0 ) {
|
||||
foreach ( $json as $key => $value ) {
|
||||
// @phpstan-ignore isset.offset
|
||||
if ( isset( $defaults[ $key ] ) && $value === $defaults[ $key ] ) {
|
||||
unset( $json[ $key ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return flag data, with defaults filling in any gaps not set.
|
||||
*
|
||||
* @return FlagsJson
|
||||
*/
|
||||
public function get_json_with_defaults() {
|
||||
$settings = Red_Options::get();
|
||||
$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 ) {
|
||||
// @phpstan-ignore isset.offset
|
||||
if ( ! isset( $json[ $key ] ) ) {
|
||||
$json[ $key ] = $defaults[ $key ];
|
||||
}
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
@@ -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() );
|
||||
}
|
||||
}
|
||||
@@ -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 (string) 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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Query parameter matching
|
||||
*
|
||||
* @phpstan-type QueryParams array<string, string|null|array<string, mixed>>
|
||||
* @phpstan-type ParsedParam array{
|
||||
* name: string,
|
||||
* value: string|null,
|
||||
* parse_str: string
|
||||
* }
|
||||
*/
|
||||
class Red_Url_Query {
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
const RECURSION_LIMIT = 10;
|
||||
|
||||
/**
|
||||
* Original query parameters (used when passing)
|
||||
*
|
||||
* @var QueryParams
|
||||
*/
|
||||
private $original_query = [];
|
||||
|
||||
/**
|
||||
* Match query parameters (used only for matching, and may be lowercased)
|
||||
*
|
||||
* @var QueryParams
|
||||
*/
|
||||
private $match_query = [];
|
||||
|
||||
/**
|
||||
* Is this an exact match?
|
||||
*
|
||||
* @var false|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 QueryParams $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 = (string) 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 ( is_string( $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 QueryParams
|
||||
*/
|
||||
public function get() {
|
||||
return $this->original_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if query parameters match exactly
|
||||
*
|
||||
* @param string $url URL.
|
||||
* @param QueryParams $params Query parameters.
|
||||
* @return bool
|
||||
*/
|
||||
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 QueryParams
|
||||
*/
|
||||
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[ (string) $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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @var QueryParams $params */
|
||||
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 array<ParsedParam>
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lowercase key mapping for case-insensitive matching
|
||||
*
|
||||
* @param QueryParams $query Query parameters.
|
||||
* @return array<string, string> Map of lowercase keys to original keys.
|
||||
*/
|
||||
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 QueryParams $source_query Source query params.
|
||||
* @param QueryParams $target_query Target query params.
|
||||
* @param bool $is_ignore_case Ignore case.
|
||||
* @param int $depth Current recursion depth.
|
||||
* @return QueryParams
|
||||
*/
|
||||
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 QueryParams $source_query Source query params.
|
||||
* @param QueryParams $target_query Target query params.
|
||||
* @param int $depth Current recursion depth.
|
||||
* @return QueryParams
|
||||
*/
|
||||
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 ] ) ) {
|
||||
/** @var QueryParams $child_source */
|
||||
$child_source = $source_query[ $key ];
|
||||
/** @var QueryParams $child_target */
|
||||
$child_target = $target_query[ $key ];
|
||||
$add = $this->get_query_diff( $child_source, $child_target, $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value is a simple query value (string or null)
|
||||
*
|
||||
* @param mixed $value Value to check.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_value( $value ) {
|
||||
return is_string( $value ) || $value === null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user