|
<?php |
|
|
|
class WPRoutes { |
|
|
|
/** |
|
* @var $this[] |
|
*/ |
|
private static $instances = []; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
private static $routes = []; |
|
|
|
/** |
|
* @var bool |
|
*/ |
|
private static $initialized = false; |
|
|
|
/** |
|
* @var string |
|
*/ |
|
private $namespace; |
|
|
|
/** |
|
* @var string|null |
|
*/ |
|
private static $rawInput = null; |
|
|
|
/** |
|
* Constructor |
|
* |
|
* @param string $namespace |
|
*/ |
|
private function __construct( $namespace ) { |
|
$this->namespace = trim( $namespace, '/' ); |
|
|
|
if ( !self::$initialized ) { |
|
add_action( 'init', [self::class, 'register_rewrite_rules'] ); |
|
add_filter( 'query_vars', [self::class, 'query_vars'] ); |
|
add_action( 'parse_request', [self::class, 'handle_request'] ); |
|
self::$initialized = true; |
|
} |
|
} |
|
|
|
/** |
|
* Get or create a WPRoutes instance for a specific namespace |
|
* |
|
* @param string $namespace |
|
* @return $this |
|
*/ |
|
public static function namespace( $namespace ) { |
|
if ( !isset( self::$instances[$namespace] ) ) { |
|
self::$instances[$namespace] = new self( $namespace ); |
|
} |
|
|
|
return self::$instances[$namespace]; |
|
} |
|
|
|
/** |
|
* Define a GET route |
|
* |
|
* @param string $path |
|
* @param callable $callback |
|
* @return $this |
|
*/ |
|
public function get( $path, $callback ) { |
|
return $this->add_route( 'GET', $path, $callback ); |
|
} |
|
|
|
/** |
|
* Define a POST route |
|
* |
|
* @param string $path |
|
* @param callable $callback |
|
* @return $this |
|
*/ |
|
public function post( $path, $callback ) { |
|
return $this->add_route( 'POST', $path, $callback ); |
|
} |
|
|
|
/** |
|
* Define a PUT route |
|
* |
|
* @param string $path |
|
* @param callable $callback |
|
* @return $this |
|
*/ |
|
public function put( $path, $callback ) { |
|
return $this->add_route( 'PUT', $path, $callback ); |
|
} |
|
|
|
/** |
|
* Define a DELETE route |
|
* |
|
* @param string $path |
|
* @param callable $callback |
|
* @return $this |
|
*/ |
|
public function delete( $path, $callback ) { |
|
return $this->add_route( 'DELETE', $path, $callback ); |
|
} |
|
|
|
/** |
|
* Add a route to the routing table |
|
* |
|
* @param string $method |
|
* @param string $path |
|
* @param callable $callback |
|
* @return $this |
|
*/ |
|
private function add_route( $method, $path, $callback ) { |
|
$path = '/' . trim( $path, '/' ); |
|
|
|
$pattern = '#^' . preg_replace( '/\{([^}]+)\}/', '([^\/]+)', $path ) . '$#'; |
|
|
|
self::$routes[$method][] = [ |
|
'namespace' => $this->namespace, |
|
'callback' => $callback, |
|
'pattern' => $pattern, |
|
'params' => $this->extract_param_names( $path ), |
|
]; |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Extract parameter names from a route path |
|
* |
|
* @param string $path |
|
* @return array |
|
*/ |
|
private function extract_param_names( $path ) { |
|
preg_match_all( '/\{([^}]+)\}/', $path, $matches ); |
|
|
|
return $matches[1] ?? []; |
|
} |
|
|
|
/** |
|
* Register rewrite rules for all defined routes |
|
*/ |
|
public static function register_rewrite_rules() { |
|
if ( empty( self::$routes ) ) { |
|
return; |
|
} |
|
|
|
$namespaces = []; |
|
foreach ( self::$routes as $method => $routes ) { |
|
foreach ( $routes as $route ) { |
|
$namespaces[] = $route['namespace']; |
|
} |
|
} |
|
$namespaces = array_unique( $namespaces ); |
|
|
|
foreach ( $namespaces as $ns ) { |
|
add_rewrite_tag( "%{$ns}_route%", '(.+)' ); |
|
add_rewrite_rule( "^{$ns}/(.+)?", "index.php?{$ns}_route=\$matches[1]", 'top' ); |
|
add_rewrite_rule( "^{$ns}/?$", "index.php?{$ns}_route=", 'top' ); |
|
} |
|
} |
|
|
|
/** |
|
* Add query vars to WordPress |
|
* |
|
* @param array $vars |
|
* @return array |
|
*/ |
|
public static function query_vars( $vars ) { |
|
if ( empty( self::$routes ) ) { |
|
return $vars; |
|
} |
|
|
|
$namespaces = []; |
|
foreach ( self::$routes as $method => $routes ) { |
|
foreach ( $routes as $route ) { |
|
$namespaces[] = $route['namespace']; |
|
} |
|
} |
|
$namespaces = array_unique( $namespaces ); |
|
|
|
foreach ( $namespaces as $ns ) { |
|
$vars[] = "{$ns}_route"; |
|
} |
|
|
|
return $vars; |
|
} |
|
|
|
/** |
|
* Handle incoming requests and dispatch to the appropriate route callback |
|
* |
|
* @param WP $wp |
|
*/ |
|
public static function handle_request( $wp ) { |
|
$method = self::get_method(); |
|
|
|
if ( !isset( self::$routes[$method] ) ) { |
|
return; |
|
} |
|
|
|
foreach ( self::$routes[$method] as $route ) { |
|
$ns_key = $route['namespace'] . '_route'; |
|
|
|
if ( !isset( $wp->query_vars[$ns_key] ) ) { |
|
continue; |
|
} |
|
|
|
$requestPath = '/' . trim( $wp->query_vars[$ns_key], '/' ); |
|
|
|
if ( preg_match( $route['pattern'], $requestPath, $matches ) ) { |
|
array_shift( $matches ); |
|
|
|
$params = array_combine( $route['params'], $matches ); |
|
$request = self::build_request( $params, $method ); |
|
|
|
$response = call_user_func( $route['callback'], $request ); |
|
self::send_response( $response ); |
|
exit; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Get the HTTP method, considering method spoofing |
|
* |
|
* @return string |
|
*/ |
|
private static function get_method() { |
|
$method = strtoupper( $_SERVER['REQUEST_METHOD'] ?? 'GET' ); |
|
|
|
if ( $method === 'POST' ) { |
|
// Check header first |
|
$spoofed = strtoupper( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? '' ); |
|
|
|
if ( in_array( $spoofed, ['PUT', 'DELETE', 'PATCH'], true ) ) { |
|
return $spoofed; |
|
} |
|
|
|
// Check raw input for _method parameter |
|
$rawInput = self::get_raw_input(); |
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? ''; |
|
|
|
if ( stripos( $contentType, 'application/json' ) !== false ) { |
|
$decoded = json_decode( $rawInput, true ); |
|
if ( isset( $decoded['_method'] ) ) { |
|
$spoofed = strtoupper( $decoded['_method'] ); |
|
if ( in_array( $spoofed, ['PUT', 'DELETE', 'PATCH'], true ) ) { |
|
return $spoofed; |
|
} |
|
} |
|
} else { |
|
// Check $_POST or parse raw input |
|
if ( isset( $_POST['_method'] ) ) { |
|
$spoofed = strtoupper( $_POST['_method'] ); |
|
} else { |
|
parse_str( $rawInput, $parsed ); |
|
$spoofed = strtoupper( $parsed['_method'] ?? '' ); |
|
} |
|
|
|
if ( in_array( $spoofed, ['PUT', 'DELETE', 'PATCH'], true ) ) { |
|
return $spoofed; |
|
} |
|
} |
|
} |
|
|
|
return $method; |
|
} |
|
|
|
/** |
|
* Get and cache raw input from php://input |
|
* |
|
* @return string |
|
*/ |
|
private static function get_raw_input() { |
|
if ( self::$rawInput === null ) { |
|
self::$rawInput = file_get_contents( 'php://input' ); |
|
} |
|
|
|
return self::$rawInput; |
|
} |
|
|
|
/** |
|
* Build a request array |
|
* |
|
* @param array $params |
|
* @param string $method |
|
* @return array |
|
*/ |
|
private static function build_request( $params, $method ) { |
|
$body = []; |
|
|
|
if ( in_array( $method, ['POST', 'PUT', 'PATCH', 'DELETE'] ) ) { |
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? ''; |
|
$rawInput = self::get_raw_input(); |
|
|
|
if ( stripos( $contentType, 'application/json' ) !== false ) { |
|
$decoded = json_decode( $rawInput, true ); |
|
$body = is_array( $decoded ) ? $decoded : []; |
|
// Remove _method from body if present |
|
unset( $body['_method'] ); |
|
} elseif ( stripos( $contentType, 'multipart/form-data' ) !== false ) { |
|
$body = array_merge( $_POST, $_FILES ); |
|
unset( $body['_method'] ); |
|
} elseif ( stripos( $contentType, 'application/x-www-form-urlencoded' ) !== false || !empty( $_POST ) ) { |
|
$body = $_POST; |
|
unset( $body['_method'] ); |
|
} else { |
|
// Fallback: try to parse raw input as query string |
|
parse_str( $rawInput, $parsed ); |
|
$body = is_array( $parsed ) ? $parsed : []; |
|
unset( $body['_method'] ); |
|
} |
|
} |
|
|
|
return [ |
|
'params' => array_map( 'sanitize_text_field', $params ), |
|
'query' => array_map( 'sanitize_text_field', $_GET ), |
|
'body' => $body, |
|
'method' => $method, |
|
]; |
|
} |
|
|
|
/** |
|
* Send the response to the client |
|
* |
|
* @param mixed $response |
|
*/ |
|
private static function send_response( $response ) { |
|
if ( is_array( $response ) || is_object( $response ) ) { |
|
wp_send_json( $response ); |
|
} else { |
|
echo $response; |
|
} |
|
} |
|
|
|
/** |
|
* Flush rewrite rules |
|
*/ |
|
public static function flush() { |
|
flush_rewrite_rules(); |
|
} |
|
|
|
} |