├── .github └── workflows │ └── php-qa.yml ├── LICENSE ├── composer.json ├── phpunit.xml.dist └── src ├── Cortex.php ├── Cortex ├── Controller │ ├── ControllerInterface.php │ ├── QueryVarsController.php │ └── RedirectController.php ├── Group │ ├── Group.php │ ├── GroupCollection.php │ ├── GroupCollectionInterface.php │ └── GroupInterface.php ├── Route │ ├── ActionRoute.php │ ├── DerivativeRouteTrait.php │ ├── PriorityRouteCollection.php │ ├── QueryRoute.php │ ├── RedirectRoute.php │ ├── Route.php │ ├── RouteCollectionInterface.php │ └── RouteInterface.php ├── Router │ ├── MatchingResult.php │ ├── ResultHandler.php │ ├── ResultHandlerInterface.php │ ├── RouteFilterIterator.php │ ├── Router.php │ └── RouterInterface.php └── Uri │ ├── PsrUri.php │ ├── UriInterface.php │ └── WordPressUri.php └── Routes.php /.github/workflows/php-qa.yml: -------------------------------------------------------------------------------- 1 | name: PHP Quality Assurance 2 | on: 3 | push: 4 | # Allow manually triggering the workflow. 5 | workflow_dispatch: 6 | 7 | # Cancels all previous workflow runs for the same branch that have not yet completed. 8 | concurrency: 9 | # The concurrency group contains the workflow name and the branch name. 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | qa: 15 | runs-on: ubuntu-latest 16 | if: "!contains(github.event.head_commit.message, 'ci skip')" 17 | strategy: 18 | fail-fast: true 19 | matrix: 20 | php-versions: ['5.5', '5.6'] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php-versions }} 29 | ini-values: zend.assertions=1, error_reporting=-1, display_errors=On 30 | coverage: none 31 | tools: parallel-lint 32 | env: 33 | fail-fast: true 34 | 35 | - name: Check syntax error in sources 36 | run: parallel-lint ./src/ ./tests/ 37 | 38 | - name: Install dependencies 39 | uses: "ramsey/composer-install@v2" 40 | with: 41 | # Bust the cache at least once a month - output format: YYYY-MM. 42 | custom-cache-suffix: $(date -u "+%Y-%m") 43 | 44 | - name: Check cross-version PHP compatibility 45 | if: ${{ matrix.php-versions == '5.6' }} 46 | run: composer phpcompat 47 | 48 | - name: Run unit tests 49 | run: ./vendor/bin/phpunit 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Giuseppe Mazzapica 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brain/cortex", 3 | "description": "Cortex is a package that implements a routing system in WordPress.", 4 | "keywords": [ 5 | "wordpress", 6 | "routing", 7 | "fast route", 8 | "router", 9 | "rewrite rules", 10 | "pretty permalink" 11 | ], 12 | "homepage": "https://github.com/Brain-WP/Cortex", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Giuseppe Mazzapica", 17 | "email": "giuseppe.mazzapica@gmail.com", 18 | "homepage": "http://gm.zoomlab.it", 19 | "role": "Developer" 20 | } 21 | ], 22 | "support": { 23 | "issues": "https://github.com/Brain-WP/Cortex/issues", 24 | "source": "https://github.com/Brain-WP/Cortex" 25 | }, 26 | "require": { 27 | "php": ">=5.5", 28 | "nikic/fast-route": "~0.7.0", 29 | "psr/http-message": "<1.1" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "4.8.*", 33 | "mockery/mockery": "0.9.3", 34 | "brain/monkey": "~1.2.0", 35 | "gmazzap/andrew": "~1.0.0", 36 | "squizlabs/php_codesniffer": "^3.7", 37 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 38 | "phpcompatibility/php-compatibility": "^9.3" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Brain\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Brain\\Cortex\\Tests\\": "tests/src/" 48 | } 49 | }, 50 | "minimum-stability": "stable", 51 | "config": { 52 | "optimize-autoloader": true, 53 | "allow-plugins": { 54 | "dealerdirect/phpcodesniffer-composer-installer": true 55 | } 56 | }, 57 | "scripts" : { 58 | "phpcompat": [ 59 | "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs -ps . --standard=PHPCompatibility --exclude=PHPCompatibility.Attributes.NewAttributes --ignore=*/vendor/* --extensions=php --runtime-set testVersion 7.0-" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | ./.build 17 | ./tests 18 | ./vendor 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ./tests/src/Unit 28 | 29 | 30 | ./tests/src/Functional 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Cortex.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain; 12 | 13 | use Brain\Cortex\Group\GroupCollection; 14 | use Brain\Cortex\Group\GroupCollectionInterface; 15 | use Brain\Cortex\Route\PriorityRouteCollection; 16 | use Brain\Cortex\Route\RouteCollectionInterface; 17 | use Brain\Cortex\Router\ResultHandler; 18 | use Brain\Cortex\Router\ResultHandlerInterface; 19 | use Brain\Cortex\Router\Router; 20 | use Brain\Cortex\Router\RouterInterface; 21 | use Brain\Cortex\Uri\PsrUri; 22 | use Brain\Cortex\Uri\WordPressUri; 23 | use Brain\Cortex\Uri\UriInterface; 24 | use Psr\Http\Message\RequestInterface; 25 | 26 | /** 27 | * @author Giuseppe Mazzapica 28 | * @license http://opensource.org/licenses/MIT MIT 29 | * @package Cortex 30 | */ 31 | class Cortex 32 | { 33 | /** 34 | * @var bool 35 | */ 36 | private static $booted = false; 37 | 38 | /** 39 | * @var bool 40 | */ 41 | private static $late = false; 42 | 43 | /** 44 | * @param \Psr\Http\Message\RequestInterface $request 45 | * @return bool 46 | * @throws \Exception 47 | */ 48 | public static function boot(RequestInterface $request = null) 49 | { 50 | try { 51 | if (self::$booted) { 52 | return false; 53 | } 54 | 55 | if (did_action('parse_request')) { 56 | throw new \BadMethodCallException( 57 | sprintf('%s must be called before "do_parse_request".', __METHOD__) 58 | ); 59 | } 60 | 61 | self::$booted = add_filter('do_parse_request', function ($do, \WP $wp) use ($request) { 62 | self::$late = true; 63 | try { 64 | $instance = new static(); 65 | $do = $instance->doBoot($wp, $do, $request); 66 | unset($instance); 67 | 68 | if ( ! $do ) { 69 | global $wp_version; 70 | 71 | if ( $wp_version && version_compare( $wp_version, '6', '>=' ) ) { 72 | $wp->query_posts(); 73 | $wp->register_globals(); 74 | } 75 | } 76 | 77 | return $do; 78 | } catch (\Exception $e) { 79 | if (defined('WP_DEBUG') && WP_DEBUG) { 80 | throw $e; 81 | } 82 | 83 | do_action('cortex.fail', $e); 84 | 85 | return $do; 86 | } 87 | }, 100, 2); 88 | } catch (\Exception $e) { 89 | if (defined('WP_DEBUG') && WP_DEBUG) { 90 | throw $e; 91 | } 92 | 93 | do_action('cortex.fail', $e); 94 | } 95 | 96 | return true; 97 | } 98 | 99 | /** 100 | * @return bool 101 | */ 102 | public static function late() 103 | { 104 | return self::$late; 105 | } 106 | 107 | /** 108 | * @param \WP $wp 109 | * @param bool $do 110 | * @param \Psr\Http\Message\RequestInterface|null $request 111 | * @return bool 112 | */ 113 | private function doBoot(\WP $wp, $do, RequestInterface $request = null) 114 | { 115 | $uri = $this->factoryUri($request); 116 | $method = $this->getMethod($request); 117 | $routes = $this->factoryRoutes($uri, $method); 118 | $groups = $this->factoryGroups(); 119 | $router = $this->factoryRouter($routes, $groups); 120 | $handler = $this->factoryHandler(); 121 | add_filter('cortex.match.done', function ($result) { 122 | remove_all_filters('cortex.routes'); 123 | remove_all_filters('cortex.groups'); 124 | 125 | return $result; 126 | }); 127 | $do = $handler->handle($router->match($uri, $method), $wp, $do); 128 | is_bool($do) or $do = true; 129 | 130 | return $do; 131 | } 132 | 133 | /** 134 | * @param string $name 135 | * @param string|null $abstract 136 | * @param callable|null $default 137 | * @return object 138 | */ 139 | private function factoryByHook($name, $abstract = null, callable $default = null) 140 | { 141 | $thing = apply_filters("cortex.{$name}.instance", null); 142 | if ( 143 | is_string($abstract) 144 | && (class_exists($abstract) || interface_exists($abstract)) 145 | && (! is_object($thing) || ! is_subclass_of($thing, $abstract, true)) 146 | ) { 147 | $thing = is_callable($default) ? $default() : null; 148 | } 149 | 150 | if (! is_object($thing)) { 151 | throw new \RuntimeException(sprintf('Impossible to factory "%s".', $name)); 152 | } 153 | 154 | return $thing; 155 | } 156 | 157 | /** 158 | * @param \Psr\Http\Message\RequestInterface $request 159 | * @return \Brain\Cortex\Uri\UriInterface 160 | */ 161 | private function factoryUri(RequestInterface $request = null) 162 | { 163 | $psrUri = is_null($request) ? null : $request->getUri(); 164 | 165 | /** @var UriInterface $uri */ 166 | $uri = $this->factoryByHook( 167 | 'uri', 168 | UriInterface::class, 169 | function () use ($psrUri) { 170 | is_null($psrUri) and $psrUri = new PsrUri(); 171 | 172 | return new WordPressUri($psrUri); 173 | } 174 | ); 175 | 176 | return $uri; 177 | } 178 | 179 | /** 180 | * @param \Psr\Http\Message\RequestInterface|null $request 181 | * @return string 182 | */ 183 | private function getMethod(RequestInterface $request = null) 184 | { 185 | if ($request) { 186 | return $request->getMethod(); 187 | } 188 | 189 | return empty($_SERVER['REQUEST_METHOD']) ? 'GET' : strtoupper($_SERVER['REQUEST_METHOD']); 190 | } 191 | 192 | /** 193 | * @return \Brain\Cortex\Group\GroupCollectionInterface 194 | */ 195 | private function factoryGroups() 196 | { 197 | /** @var \Brain\Cortex\Group\GroupCollectionInterface $groups */ 198 | $groups = $this->factoryByHook( 199 | 'group-collection', 200 | GroupCollectionInterface::class, 201 | function () { 202 | return new GroupCollection(); 203 | } 204 | ); 205 | 206 | do_action('cortex.groups', $groups); 207 | 208 | return $groups; 209 | } 210 | 211 | /** 212 | * @param \Brain\Cortex\Uri\UriInterface $uri 213 | * @param string $method 214 | * @return \Brain\Cortex\Route\RouteCollectionInterface 215 | */ 216 | private function factoryRoutes(UriInterface $uri, $method) 217 | { 218 | /** @var \Brain\Cortex\Route\RouteCollectionInterface $routes */ 219 | $routes = $this->factoryByHook( 220 | 'route-collection', 221 | RouteCollectionInterface::class, 222 | function () { 223 | return new PriorityRouteCollection(); 224 | } 225 | ); 226 | 227 | do_action('cortex.routes', $routes, $uri, $method); 228 | 229 | return $routes; 230 | } 231 | 232 | /** 233 | * @param \Brain\Cortex\Route\RouteCollectionInterface $routes 234 | * @param \Brain\Cortex\Group\GroupCollectionInterface $groups 235 | * @return \Brain\Cortex\Router\RouterInterface 236 | */ 237 | private function factoryRouter( 238 | RouteCollectionInterface $routes, 239 | GroupCollectionInterface $groups 240 | ) { 241 | /** @var \Brain\Cortex\Router\RouterInterface $router */ 242 | $router = $this->factoryByHook( 243 | 'router', 244 | RouterInterface::class, 245 | function () use ($routes, $groups) { 246 | return new Router($routes, $groups); 247 | } 248 | ); 249 | 250 | return $router; 251 | } 252 | 253 | /** 254 | * @return \Brain\Cortex\Router\ResultHandlerInterface 255 | */ 256 | private function factoryHandler() 257 | { 258 | /** @var ResultHandlerInterface $handler */ 259 | $handler = $this->factoryByHook( 260 | 'result-handler', 261 | ResultHandlerInterface::class, 262 | function () { 263 | return new ResultHandler(); 264 | } 265 | ); 266 | 267 | return $handler; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/Cortex/Controller/ControllerInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Controller; 12 | 13 | /** 14 | * Interface for controllers. 15 | * 16 | * Controllers runs when a route match and can be defined per route. 17 | * 18 | * @author Giuseppe Mazzapica 19 | * @license http://opensource.org/licenses/MIT MIT 20 | * @package Cortex 21 | */ 22 | interface ControllerInterface 23 | { 24 | /** 25 | * @param array $vars 26 | * @param \WP $wp 27 | * @param string $template 28 | * @return bool 29 | */ 30 | public function run(array $vars, \WP $wp, $template = ''); 31 | } 32 | -------------------------------------------------------------------------------- /src/Cortex/Controller/QueryVarsController.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Controller; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | final class QueryVarsController implements ControllerInterface 19 | { 20 | /** 21 | * @inheritdoc 22 | */ 23 | public function run(array $vars, \WP $wp, $template = '') 24 | { 25 | $wp->query_vars = $vars; 26 | 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Cortex/Controller/RedirectController.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Controller; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | final class RedirectController implements ControllerInterface 19 | { 20 | /** 21 | * @codeCoverageIgnore 22 | */ 23 | public static function doExit() 24 | { 25 | exit(); 26 | } 27 | 28 | /** 29 | * @inheritdoc 30 | */ 31 | public function run(array $vars, \WP $wp, $template = '') 32 | { 33 | $to = empty($vars['redirect_to']) ? home_url() : $vars['redirect_to']; 34 | 35 | if (filter_var($to, FILTER_VALIDATE_URL)) { 36 | $status = empty($vars['redirect_status']) ? 301 : $vars['redirect_status']; 37 | in_array((int)$status, range(300, 308), true) or $status = 301; 38 | $external = empty($vars['redirect_external']) ? false : $vars['redirect_external']; 39 | /** @var callable $cb */ 40 | $cb = filter_var($external, FILTER_VALIDATE_BOOLEAN) 41 | ? 'wp_redirect' 42 | : 'wp_safe_redirect'; 43 | $cb($to, $status); 44 | 45 | add_action('cortex.exit.redirect', [__CLASS__, 'doExit'], 100); 46 | do_action('cortex.exit.redirect'); 47 | } 48 | 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Cortex/Group/Group.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Group; 12 | 13 | use Brain\Cortex\Route\Route; 14 | 15 | /** 16 | * @author Giuseppe Mazzapica 17 | * @license http://opensource.org/licenses/MIT MIT 18 | * @package Cortex 19 | */ 20 | final class Group implements GroupInterface 21 | { 22 | /** 23 | * @var \Brain\Cortex\Route\Route 24 | */ 25 | private $route; 26 | 27 | /** 28 | * Group constructor. 29 | * 30 | * @param array $data 31 | */ 32 | public function __construct(array $data) 33 | { 34 | array_change_key_case($data, CASE_LOWER); 35 | if (isset($data['group'])) { 36 | unset($data['group']); 37 | } 38 | 39 | $data['id'] = ! empty($data['id']) && is_string($data['id']) 40 | ? $data['id'] 41 | : 'group_'.spl_object_hash($this); 42 | 43 | $this->route = new Route($data); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function id() 50 | { 51 | return $this->route->id(); 52 | } 53 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | public function toArray() 58 | { 59 | return $this->route->toArray(); 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | #[\ReturnTypeWillChange] 66 | public function offsetExists($offset) 67 | { 68 | return $this->route->offsetExists($offset); 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | #[\ReturnTypeWillChange] 75 | public function offsetGet($offset) 76 | { 77 | return $this->route->offsetGet($offset); 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | #[\ReturnTypeWillChange] 84 | public function offsetSet($offset, $value) 85 | { 86 | $this->route->offsetSet($offset, $value); 87 | } 88 | 89 | /** 90 | * @inheritdoc 91 | */ 92 | #[\ReturnTypeWillChange] 93 | public function offsetUnset($offset) 94 | { 95 | $this->route->offsetUnset($offset); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Cortex/Group/GroupCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Group; 12 | 13 | use Brain\Cortex\Route\RouteInterface; 14 | 15 | /** 16 | * @author Giuseppe Mazzapica 17 | * @license http://opensource.org/licenses/MIT MIT 18 | * @package Cortex 19 | */ 20 | final class GroupCollection implements GroupCollectionInterface 21 | { 22 | /** 23 | * @var array 24 | */ 25 | private $groups = []; 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function addGroup(GroupInterface $group) 31 | { 32 | $this->groups[$group->id()] = $group; 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function mergeGroup(RouteInterface $route) 41 | { 42 | $groups = $route->offsetGet('group'); 43 | if (empty($groups)) { 44 | return $route; 45 | } 46 | 47 | $data = array_reduce((array)$groups, function (array $data, $group) { 48 | if (is_string($group) && array_key_exists($group, $this->groups)) { 49 | /** @var \Brain\Cortex\Group\GroupInterface $groupObj */ 50 | $groupObj = $this->groups[$group]; 51 | $data = array_merge($data, $groupObj->toArray()); 52 | unset($data['id']); 53 | } 54 | 55 | return $data; 56 | }, []); 57 | 58 | $clone = clone $route; 59 | array_walk($data, function ($value, $key) use (&$clone) { 60 | $skip = 61 | in_array($key, ['id', 'group'], true) 62 | || ($clone->offsetExists($key) && ! is_null($clone->offsetGet($key))); 63 | $skip or $clone->offsetSet($key, $value); 64 | }); 65 | 66 | return $clone; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Cortex/Group/GroupCollectionInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Group; 12 | 13 | use Brain\Cortex\Route\RouteInterface; 14 | 15 | /** 16 | * Interface for GroupCollection objects. 17 | * Groups objects are the way to share common settings among different routes. 18 | * 19 | * @author Giuseppe Mazzapica 20 | * @license http://opensource.org/licenses/MIT MIT 21 | * @package Cortex 22 | */ 23 | interface GroupCollectionInterface 24 | { 25 | /** 26 | * @param \Brain\Cortex\Group\GroupInterface $group 27 | * @return \Brain\Cortex\Group\GroupCollectionInterface 28 | */ 29 | public function addGroup(GroupInterface $group); 30 | 31 | /** 32 | * Merge group settings into a given route 33 | * 34 | * @param \Brain\Cortex\Route\RouteInterface $route 35 | * @return \Brain\Cortex\Route\RouteInterface Edited route 36 | */ 37 | public function mergeGroup(RouteInterface $route); 38 | } 39 | -------------------------------------------------------------------------------- /src/Cortex/Group/GroupInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Group; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | interface GroupInterface extends \ArrayAccess 19 | { 20 | /** 21 | * @return string 22 | */ 23 | public function id(); 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function toArray(); 29 | } 30 | -------------------------------------------------------------------------------- /src/Cortex/Route/ActionRoute.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | final class ActionRoute implements RouteInterface 19 | { 20 | use DerivativeRouteTrait; 21 | 22 | /** 23 | * @var array 24 | */ 25 | private $route; 26 | 27 | /** 28 | * QueryRoute constructor. 29 | * 30 | * @param string $path 31 | * @param callable $action 32 | * @param array $options 33 | */ 34 | public function __construct($path, callable $action, array $options = []) 35 | { 36 | $options['path'] = $path; 37 | $options['handler'] = $action; 38 | 39 | $this->route = new Route($options); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Cortex/Route/DerivativeRouteTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | * 18 | * @property RouteInterface $route 19 | */ 20 | trait DerivativeRouteTrait 21 | { 22 | /** 23 | * @see RouteInterface::id() 24 | */ 25 | public function id() 26 | { 27 | return $this->route->id(); 28 | } 29 | 30 | /** 31 | * @see RouteInterface::toArray() 32 | */ 33 | public function toArray() 34 | { 35 | return $this->route->toArray(); 36 | } 37 | 38 | /** 39 | * @see RouteInterface::offsetExists() 40 | * @param string $offset 41 | * @return bool 42 | */ 43 | #[\ReturnTypeWillChange] 44 | public function offsetExists($offset) 45 | { 46 | return $this->route->offsetExists($offset); 47 | } 48 | 49 | /** 50 | * @see RouteInterface::offsetGet() 51 | * @param string $offset 52 | * @return mixed 53 | */ 54 | #[\ReturnTypeWillChange] 55 | public function offsetGet($offset) 56 | { 57 | return $this->route->offsetGet($offset); 58 | } 59 | 60 | /** 61 | * @see RouteInterface::offsetSet() 62 | * @param string $offset 63 | * @param mixed $value 64 | */ 65 | #[\ReturnTypeWillChange] 66 | public function offsetSet($offset, $value) 67 | { 68 | $this->route->offsetSet($offset, $value); 69 | } 70 | 71 | /** 72 | * @see RouteInterface::offsetUnset() 73 | * @param string $offset 74 | */ 75 | #[\ReturnTypeWillChange] 76 | public function offsetUnset($offset) 77 | { 78 | $this->route->offsetUnset($offset); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Cortex/Route/PriorityRouteCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | final class PriorityRouteCollection implements RouteCollectionInterface 19 | { 20 | private static $pagedFlags = [RouteInterface::PAGED_ARCHIVE, RouteInterface::PAGED_SINGLE]; 21 | 22 | /** 23 | * @var \SplPriorityQueue 24 | */ 25 | private $queue; 26 | 27 | /** 28 | * @var array 29 | */ 30 | private $priorities = []; 31 | 32 | /** 33 | * PriorityRouteCollection constructor. 34 | */ 35 | public function __construct() 36 | { 37 | $queue = new \SplPriorityQueue(); 38 | $queue->setExtractFlags(\SplPriorityQueue::EXTR_DATA); 39 | $this->queue = $queue; 40 | } 41 | 42 | /** 43 | * @param \Brain\Cortex\Route\RouteInterface $route 44 | * @return \Brain\Cortex\Route\RouteCollectionInterface 45 | */ 46 | public function addRoute(RouteInterface $route) 47 | { 48 | if (! $route->offsetExists('priority') || ! is_numeric($route->offsetGet('priority'))) { 49 | $next = $this->priorities ? max($this->priorities) + 1 : 10; 50 | $route->offsetSet('priority', $next); 51 | } 52 | 53 | $priority = $route->offsetGet('priority'); 54 | 55 | $paged = $this->maybeBuildPaged($route); 56 | if ( 57 | $paged instanceof RouteInterface 58 | && ! in_array($paged['paged'], self::$pagedFlags, true) // ensure avoid infinite loops 59 | ) { 60 | $paged->offsetSet('priority', $priority); 61 | $priority++; 62 | $this->addRoute($paged); 63 | } 64 | 65 | in_array($priority, $this->priorities, true) or $this->priorities[] = $priority; 66 | $this->queue->insert($route, ((-1) * $priority)); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | #[\ReturnTypeWillChange] 75 | public function current() 76 | { 77 | return $this->queue->current(); 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | #[\ReturnTypeWillChange] 84 | public function next() 85 | { 86 | $this->queue->next(); 87 | } 88 | 89 | /** 90 | * @inheritdoc 91 | */ 92 | #[\ReturnTypeWillChange] 93 | public function key() 94 | { 95 | return $this->queue->key(); 96 | } 97 | 98 | /** 99 | * @inheritdoc 100 | */ 101 | #[\ReturnTypeWillChange] 102 | public function valid() 103 | { 104 | return $this->queue->valid(); 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | */ 110 | #[\ReturnTypeWillChange] 111 | public function rewind() 112 | { 113 | $this->queue->rewind(); 114 | } 115 | 116 | /** 117 | * @inheritdoc 118 | */ 119 | #[\ReturnTypeWillChange] 120 | public function count() 121 | { 122 | return $this->queue->count(); 123 | } 124 | 125 | /** 126 | * @param \Brain\Cortex\Route\RouteInterface $route 127 | * @return \Brain\Cortex\Route\RouteInterface|null 128 | */ 129 | private function maybeBuildPaged(RouteInterface $route) 130 | { 131 | $built = null; 132 | $pagedArg = $route->offsetExists('paged') ? $route->offsetGet('paged') : ''; 133 | $path = $route->offsetExists('path') ? $route->offsetGet('path') : ''; 134 | if (in_array($pagedArg, self::$pagedFlags, true) && $path && is_string($path)) { 135 | $base = 'page'; 136 | /** @var \WP_Rewrite $wp_rewrite */ 137 | global $wp_rewrite; 138 | $wp_rewrite instanceof \WP_Rewrite and $base = $wp_rewrite->pagination_base; 139 | $array = $route->toArray(); 140 | $array['id'] = $route->id().'_paged'; 141 | $array['paged'] = RouteInterface::PAGED_UNPAGED; 142 | $array['path'] = $pagedArg === RouteInterface::PAGED_ARCHIVE 143 | ? $path.'/'.$base.'/{paged:\d+}' 144 | : $path.'/{page:\d+}'; 145 | 146 | $routeVars = $route->offsetExists('vars') ? $route->offsetGet('vars') : []; 147 | if (is_callable($routeVars)) { 148 | $array['vars'] = $this->buildPagedVars($routeVars, $pagedArg); 149 | } 150 | 151 | $built = apply_filters('cortex.paged-route', new Route($array), $route); 152 | } 153 | 154 | return $built; 155 | } 156 | 157 | /** 158 | * When route `vars` property is a callback and the route is paged, we ensure that the `vars` 159 | * callable receives the proper `paged` (or `page`) query argument and also that it returned 160 | * array includes `paged` (or `page`) query argument. 161 | * 162 | * @param callable $routeVars 163 | * @param string $pagedArg 164 | * @return \Closure 165 | */ 166 | private function buildPagedVars(callable $routeVars, $pagedArg) 167 | { 168 | $key = $pagedArg === RouteInterface::PAGED_SINGLE ? 'page' : 'paged'; 169 | 170 | return function (array $vars) use ($routeVars, $key) { 171 | (isset($vars[$key]) && is_numeric($vars[$key])) or $vars[$key] = 1; 172 | $vars[$key] = (int)$vars[$key]; 173 | $result = $routeVars($vars); 174 | if (is_array($result) && ! (isset($result[$key]) && is_numeric($result[$key]))) { 175 | $result[$key] = $vars[$key]; 176 | } 177 | $result[$key] = (int)$result[$key]; 178 | 179 | return $result; 180 | }; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Cortex/Route/QueryRoute.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | use Brain\Cortex\Controller\ControllerInterface; 14 | use Brain\Cortex\Controller\QueryVarsController; 15 | 16 | /** 17 | * @author Giuseppe Mazzapica 18 | * @license http://opensource.org/licenses/MIT MIT 19 | * @package Cortex 20 | */ 21 | final class QueryRoute implements RouteInterface 22 | { 23 | use DerivativeRouteTrait; 24 | 25 | /** 26 | * @var array 27 | */ 28 | private $route; 29 | 30 | /** 31 | * QueryRoute constructor. 32 | * 33 | * @param string $path 34 | * @param callable|array $queryBuilder 35 | * @param array $options 36 | */ 37 | public function __construct($path, $queryBuilder, array $options = []) 38 | { 39 | $options['path'] = $path; 40 | $options['vars'] = $queryBuilder; 41 | $handler = isset($options['handler']) && $options['handler'] instanceof ControllerInterface 42 | ? $options['handler'] 43 | : new QueryVarsController(); 44 | $options['handler'] = $handler; 45 | $default = isset($options['default_vars']) && is_array($options['default_vars']) 46 | ? $options['default_vars'] 47 | : []; 48 | if (isset($options['default']) && is_array($options['default'])) { 49 | $options['default_vars'] = array_merge($options['default'], $default); 50 | } 51 | 52 | $this->route = new Route($options); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Cortex/Route/RedirectRoute.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | use Brain\Cortex\Controller\ControllerInterface; 14 | use Brain\Cortex\Controller\RedirectController; 15 | 16 | /** 17 | * @author Giuseppe Mazzapica 18 | * @license http://opensource.org/licenses/MIT MIT 19 | * @package Cortex 20 | */ 21 | final class RedirectRoute implements RouteInterface 22 | { 23 | use DerivativeRouteTrait; 24 | 25 | /** 26 | * @var \Brain\Cortex\Route\Route 27 | */ 28 | private $route; 29 | 30 | /** 31 | * RedirectRoute constructor. 32 | * 33 | * @param string $from 34 | * @param string $to 35 | * @param array $options 36 | */ 37 | public function __construct($from, $to, array $options = []) 38 | { 39 | list($vars, $options) = $this->parseOptions($options); 40 | 41 | $options['vars'] = is_callable($to) 42 | ? $this->redirectToFromCallback($to, $vars) 43 | : array_merge($vars, ['redirect_to' => $this->redirectToFromString($to, $vars)]); 44 | 45 | $options['path'] = $from; 46 | $handler = isset($options['handler']) && $options['handler'] instanceof ControllerInterface 47 | ? $options['handler'] 48 | : new RedirectController(); 49 | $options['handler'] = $handler; 50 | 51 | $this->route = new Route($options); 52 | } 53 | 54 | /** 55 | * @param array $options 56 | * @return array 57 | */ 58 | private function parseOptions(array $options) 59 | { 60 | $status = empty($options['redirect_status']) ? 301 : $options['redirect_status']; 61 | in_array((int)$status, range(300, 308), true) or $status = 301; 62 | 63 | $external = empty($options['redirect_external']) ? false : $options['redirect_external']; 64 | 65 | $vars = [ 66 | 'redirect_status' => $status, 67 | 'redirect_external' => filter_var($external, FILTER_VALIDATE_BOOLEAN), 68 | 'redirect_to' => null, 69 | ]; 70 | 71 | return [$vars, array_diff_key($options, $vars)]; 72 | } 73 | 74 | /** 75 | * @param callable $to 76 | * @param array $vars 77 | * @return \Closure 78 | */ 79 | private function redirectToFromCallback(callable $to, array $vars) 80 | { 81 | return function (array $args) use ($to, $vars) { 82 | $vars['redirect_to'] = $this->redirectToFromString($to($args), $vars); 83 | 84 | return $vars; 85 | }; 86 | } 87 | 88 | /** 89 | * @param string $url 90 | * @param array $vars 91 | * @return string 92 | */ 93 | private function redirectToFromString($url, array $vars) 94 | { 95 | if (! is_string($url)) { 96 | return ''; 97 | } 98 | 99 | $url = filter_var($url, FILTER_SANITIZE_URL); 100 | if (empty($url)) { 101 | return ''; 102 | } 103 | 104 | $valid = filter_var($url, FILTER_VALIDATE_URL); 105 | 106 | if ($vars['redirect_external']) { 107 | return $valid ? $url : ''; 108 | } 109 | 110 | return $valid ? $url : home_url($url); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Cortex/Route/Route.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | final class Route implements RouteInterface 19 | { 20 | /** 21 | * @var array 22 | */ 23 | private static $defaults = [ 24 | 'vars' => null, 25 | 'default_vars' => null, 26 | 'host' => null, 27 | 'priority' => null, 28 | 'group' => null, 29 | 'merge_query_string' => null, 30 | 'paged' => null, 31 | 'method' => null, 32 | 'scheme' => null, 33 | 'before' => null, 34 | 'after' => null, 35 | 'template' => null, 36 | 'no_template' => null, 37 | 'path' => null, 38 | 'handler' => null, 39 | ]; 40 | 41 | /** 42 | * @var array 43 | */ 44 | private $storage = []; 45 | 46 | /** 47 | * Route constructor. 48 | * 49 | * @param array $data 50 | */ 51 | public function __construct(array $data) 52 | { 53 | array_change_key_case($data, CASE_LOWER); 54 | $id = ! empty($data['id']) && is_string($data['id']) 55 | ? $data['id'] 56 | : 'route_'.spl_object_hash($this); 57 | $storage = array_merge(self::$defaults, $data); 58 | $storage['id'] = $id; 59 | $this->storage = $storage; 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function id() 66 | { 67 | return $this->storage['id']; 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function toArray() 74 | { 75 | $result = []; 76 | foreach ($this->storage as $key => $value) { 77 | (is_string($key) && $key && ! is_null($value)) and $result[$key] = $value; 78 | } 79 | 80 | return $result; 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | #[\ReturnTypeWillChange] 87 | public function offsetExists($offset) 88 | { 89 | return array_key_exists($offset, $this->storage); 90 | } 91 | 92 | /** 93 | * @inheritdoc 94 | */ 95 | #[\ReturnTypeWillChange] 96 | public function offsetGet($offset) 97 | { 98 | return $this->offsetExists($offset) ? $this->storage[$offset] : null; 99 | } 100 | 101 | /** 102 | * @inheritdoc 103 | */ 104 | #[\ReturnTypeWillChange] 105 | public function offsetSet($offset, $value) 106 | { 107 | $this->storage[$offset] = $value; 108 | } 109 | 110 | /** 111 | * @inheritdoc 112 | */ 113 | #[\ReturnTypeWillChange] 114 | public function offsetUnset($offset) 115 | { 116 | if ($this->offsetExists($offset) && $offset !== 'id') { 117 | unset($this->storage[$offset]); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Cortex/Route/RouteCollectionInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | interface RouteCollectionInterface extends \Iterator, \Countable 19 | { 20 | /** 21 | * @param \Brain\Cortex\Route\RouteInterface $route 22 | * @return \Brain\Cortex\Route\RouteCollectionInterface 23 | */ 24 | public function addRoute(RouteInterface $route); 25 | } 26 | -------------------------------------------------------------------------------- /src/Cortex/Route/RouteInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Route; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | interface RouteInterface extends \ArrayAccess 19 | { 20 | const PAGED_SINGLE = 'paged_single'; 21 | const PAGED_ARCHIVE = 'paged_archive'; 22 | const PAGED_UNPAGED = ''; 23 | const PAGED_SEARCH = 'paged_archive'; 24 | const PAGED_FRONT_PAGE = 'paged_archive'; 25 | const NOT_PAGED = 'not_paged'; 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function id(); 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function toArray(); 36 | } 37 | -------------------------------------------------------------------------------- /src/Cortex/Router/MatchingResult.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Router; 12 | 13 | use Brain\Cortex\Controller\ControllerInterface as Controller; 14 | 15 | /** 16 | * @author Giuseppe Mazzapica 17 | * @license http://opensource.org/licenses/MIT MIT 18 | * @package Cortex 19 | */ 20 | class MatchingResult 21 | { 22 | /** 23 | * @var array 24 | */ 25 | private $data; 26 | 27 | /** 28 | * MatchingResult constructor. 29 | * 30 | * @param array $data 31 | */ 32 | public function __construct(array $data) 33 | { 34 | $defaults = [ 35 | 'route' => null, 36 | 'path' => null, 37 | 'vars' => null, 38 | 'matches' => null, 39 | 'handler' => null, 40 | 'before' => null, 41 | 'after' => null, 42 | 'template' => null, 43 | ]; 44 | 45 | $this->data = array_merge($defaults, array_change_key_case($data, CASE_LOWER)); 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function route() 52 | { 53 | return is_string($this->data['route']) ? $this->data['route'] : ''; 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function matchedPath() 60 | { 61 | return is_string($this->data['path']) ? $this->data['path'] : ''; 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | public function vars() 68 | { 69 | return is_array($this->data['vars']) ? $this->data['vars'] : []; 70 | } 71 | 72 | /** 73 | * @return array 74 | */ 75 | public function matches() 76 | { 77 | return is_array($this->data['matches']) ? $this->data['matches'] : []; 78 | } 79 | 80 | /** 81 | * @return bool 82 | */ 83 | public function matched() 84 | { 85 | return $this->route() && $this->matchedPath(); 86 | } 87 | 88 | /** 89 | * @return string|bool|callable 90 | */ 91 | public function template() 92 | { 93 | $template = $this->data['template']; 94 | 95 | return (is_string($template) || $template === false || is_callable($template)) 96 | ? $template 97 | : ''; 98 | } 99 | 100 | /** 101 | * @return callable|\Brain\Cortex\Controller\ControllerInterface|null 102 | */ 103 | public function handler() 104 | { 105 | return is_callable($this->data['handler']) || $this->data['handler'] instanceof Controller 106 | ? $this->data['handler'] 107 | : null; 108 | } 109 | 110 | /** 111 | * @return callable|\Brain\Cortex\Controller\ControllerInterface|null 112 | */ 113 | public function beforeHandler() 114 | { 115 | return is_callable($this->data['before']) || $this->data['before'] instanceof Controller 116 | ? $this->data['before'] 117 | : null; 118 | } 119 | 120 | /** 121 | * @return callable|\Brain\Cortex\Controller\ControllerInterface|null 122 | */ 123 | public function afterHandler() 124 | { 125 | return is_callable($this->data['after']) || $this->data['after'] instanceof Controller 126 | ? $this->data['after'] 127 | : null; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Cortex/Router/ResultHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Router; 12 | 13 | use Brain\Cortex\Controller\ControllerInterface; 14 | 15 | /** 16 | * @author Giuseppe Mazzapica 17 | * @license http://opensource.org/licenses/MIT MIT 18 | * @package Cortex 19 | */ 20 | final class ResultHandler implements ResultHandlerInterface 21 | { 22 | /** 23 | * @inheritdoc 24 | */ 25 | public function handle(MatchingResult $result, \WP $wp, $doParseRequest) 26 | { 27 | /** @var \Brain\Cortex\Router\MatchingResult $result */ 28 | $result = apply_filters('cortex.match.done', $result, $wp, $doParseRequest); 29 | $handlerResult = $doParseRequest; 30 | 31 | if (!$result instanceof MatchingResult) { 32 | return $result; 33 | } 34 | 35 | /** @var \Brain\Cortex\Router\MatchingResult $result */ 36 | if ($result->matched()) { 37 | $doParseRequest = false; 38 | $origHandler = $result->handler(); 39 | $handler = $this->buildCallback($origHandler); 40 | $before = $this->buildCallback($result->beforeHandler()); 41 | $after = $this->buildCallback($result->afterHandler()); 42 | $template = $result->template(); 43 | $vars = $result->vars(); 44 | $matches = $result->matches(); 45 | 46 | if (is_callable($template)) { 47 | $template = $template($vars, $wp, $matches); 48 | } 49 | 50 | (is_string($template) || $template === 'false') or $template = ''; 51 | 52 | do_action('cortex.matched', $result, $wp); 53 | 54 | is_callable($before) and $before($vars, $wp, $template, $matches); 55 | is_callable($handler) and $handlerResult = $handler($vars, $wp, $template, $matches); 56 | is_callable($after) and $after($vars, $wp, $template, $matches); 57 | $this->setTemplate($template); 58 | 59 | do_action('cortex.matched-after', $result, $wp, $handlerResult); 60 | 61 | is_bool($handlerResult) and $doParseRequest = $handlerResult; 62 | $doParseRequest = apply_filters('cortex.do-parse-request', $doParseRequest); 63 | 64 | if (!$doParseRequest) { 65 | remove_filter('template_redirect', 'redirect_canonical'); 66 | 67 | return false; 68 | } 69 | } 70 | 71 | do_action('cortex.result.done', $result, $wp, $handlerResult); 72 | 73 | return $doParseRequest; 74 | } 75 | 76 | /** 77 | * @param mixed $handler 78 | * @return callable|null 79 | */ 80 | private function buildCallback($handler) 81 | { 82 | $built = null; 83 | if (is_callable($handler)) { 84 | $built = $handler; 85 | } 86 | 87 | if (!$built && $handler instanceof ControllerInterface) { 88 | $built = function (array $vars, \WP $wp, $template) use ($handler) { 89 | return $handler->run($vars, $wp, $template); 90 | }; 91 | } 92 | 93 | return $built; 94 | } 95 | 96 | /** 97 | * @param $template 98 | */ 99 | private function setTemplate($template) 100 | { 101 | if (is_string($template) && $template) { 102 | $ext = apply_filters('cortex.default-template-extension', 'php'); 103 | pathinfo($template, PATHINFO_EXTENSION) or $template .= '.' . ltrim($ext, '.'); 104 | $template = is_file($template) ? $template : locate_template([$template], false); 105 | $template or $template = ''; 106 | } 107 | 108 | if ($template === '' || !(is_string($template) || $template === false)) { 109 | return; 110 | } 111 | 112 | $types = [ 113 | '404', 114 | 'search', 115 | 'front_page', 116 | 'home', 117 | 'archive', 118 | 'taxonomy', 119 | 'attachment', 120 | 'single', 121 | 'page', 122 | 'singular', 123 | 'category', 124 | 'tag', 125 | 'author', 126 | 'date', 127 | 'paged', 128 | 'index', 129 | ]; 130 | 131 | $returnTemplate = function () use ($template) { 132 | current_filter() === 'template_include' and remove_all_filters('template_include'); 133 | 134 | return $template; 135 | }; 136 | 137 | /* 138 | * If template is `false`, we return `true` on `"{$type}_template"` to speed up 139 | * `template-loader.php`, in fact, if template is `false` we are going to return `false` at 140 | * 'template_include' anyway, so no need to waste time looping all template types, even 141 | * because every type check for file(s) in filesystem, so it's a slow operation. 142 | */ 143 | $template_setter = $template !== false ? $returnTemplate : '__return_true'; 144 | 145 | array_walk($types, function ($type) use ($template_setter) { 146 | add_filter("{$type}_template", $template_setter); 147 | }); 148 | 149 | add_filter('template_include', $returnTemplate, -1); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Cortex/Router/ResultHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Router; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | interface ResultHandlerInterface 19 | { 20 | /** 21 | * @param \Brain\Cortex\Router\MatchingResult $result 22 | * @param \WP $wp 23 | * @param bool $doParseRequest 24 | * @return bool 25 | */ 26 | public function handle(MatchingResult $result, \WP $wp, $doParseRequest); 27 | } 28 | -------------------------------------------------------------------------------- /src/Cortex/Router/RouteFilterIterator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Router; 12 | 13 | use Brain\Cortex\Route\RouteCollectionInterface; 14 | use Brain\Cortex\Route\RouteInterface; 15 | use Brain\Cortex\Uri\UriInterface; 16 | 17 | /** 18 | * @author Giuseppe Mazzapica 19 | * @license http://opensource.org/licenses/MIT MIT 20 | * @package Cortex 21 | */ 22 | final class RouteFilterIterator extends \FilterIterator 23 | { 24 | /** 25 | * @var \Brain\Cortex\Uri\WordPressUri 26 | */ 27 | private $uri; 28 | 29 | /** 30 | * @param string $routeHost 31 | * @param string $serverHost 32 | * @return bool 33 | */ 34 | private static function checkHost($routeHost, $serverHost) 35 | { 36 | if (strpos($routeHost, '*') === false) { 37 | return $routeHost === $serverHost; 38 | } 39 | 40 | if (strpos($routeHost, '*.') === 0) { 41 | return fnmatch($routeHost, $serverHost) || fnmatch(substr($routeHost, 2), $serverHost); 42 | } 43 | 44 | return fnmatch($routeHost, $serverHost); 45 | } 46 | 47 | /** 48 | * RouteFilterIterator constructor. 49 | * 50 | * @param \Brain\Cortex\Route\RouteCollectionInterface $routes 51 | * @param \Brain\Cortex\Uri\UriInterface $uri 52 | */ 53 | public function __construct(RouteCollectionInterface $routes, UriInterface $uri) 54 | { 55 | parent::__construct($routes); 56 | $this->uri = $uri; 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | #[\ReturnTypeWillChange] 63 | public function accept() 64 | { 65 | /** @var RouteInterface $route */ 66 | $route = $this->getInnerIterator()->current(); 67 | if (! $route instanceof RouteInterface) { 68 | return false; 69 | } 70 | 71 | $scheme = strtolower((string)$route['scheme']); 72 | if (! empty($scheme) && $scheme !== $this->uri->scheme()) { 73 | return false; 74 | } 75 | 76 | $host = filter_var(strtolower((string)$route['host']), FILTER_SANITIZE_URL); 77 | if (! empty($host) && ! self::checkHost($route['host'], $this->uri->host())) { 78 | return false; 79 | } 80 | 81 | return true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Cortex/Router/Router.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Router; 12 | 13 | use Brain\Cortex\Controller\ControllerInterface; 14 | use Brain\Cortex\Group\GroupCollectionInterface; 15 | use Brain\Cortex\Route\RouteCollectionInterface; 16 | use Brain\Cortex\Route\RouteInterface; 17 | use Brain\Cortex\Uri\UriInterface; 18 | use FastRoute\DataGenerator\GroupCountBased as DefDataGenerator; 19 | use FastRoute\Dispatcher; 20 | use FastRoute\Dispatcher\GroupCountBased as DefDispatcher; 21 | use FastRoute\RouteCollector; 22 | use FastRoute\RouteParser\Std; 23 | 24 | /** 25 | * @author Giuseppe Mazzapica 26 | * @license http://opensource.org/licenses/MIT MIT 27 | * @package Cortex 28 | */ 29 | final class Router implements RouterInterface 30 | { 31 | /** 32 | * @var \Brain\Cortex\Group\GroupCollectionInterface 33 | */ 34 | private $groups; 35 | 36 | /** 37 | * @var \Brain\Cortex\Route\RouteCollectionInterface 38 | */ 39 | private $routes; 40 | 41 | /** 42 | * @var \FastRoute\RouteCollector 43 | */ 44 | private $collector; 45 | 46 | /** 47 | * @var callable 48 | */ 49 | private $dispatcherFactory; 50 | 51 | /* 52 | * @var array 53 | */ 54 | private $parsedRoutes = []; 55 | 56 | /** 57 | * @var null 58 | */ 59 | private $results = null; 60 | 61 | /** 62 | * Router constructor. 63 | * 64 | * @param \Brain\Cortex\Route\RouteCollectionInterface $routes 65 | * @param \Brain\Cortex\Group\GroupCollectionInterface $groups 66 | * @param \FastRoute\RouteCollector $collector 67 | * @param callable $dispatcherFactory 68 | */ 69 | public function __construct( 70 | RouteCollectionInterface $routes, 71 | GroupCollectionInterface $groups, 72 | RouteCollector $collector = null, 73 | callable $dispatcherFactory = null 74 | ) { 75 | $this->groups = $groups; 76 | $this->routes = $routes; 77 | $this->collector = $collector ? : new RouteCollector(new Std(), new DefDataGenerator()); 78 | $this->dispatcherFactory = $dispatcherFactory; 79 | } 80 | 81 | /** 82 | * @inheritdoc 83 | */ 84 | public function match(UriInterface $uri, $httpMethod) 85 | { 86 | if ($this->results instanceof MatchingResult) { 87 | return $this->results; 88 | } 89 | 90 | if (! count($this->routes) || ! $this->parseRoutes($uri, $httpMethod)) { 91 | $this->results = new MatchingResult(['route' => null]); 92 | 93 | return $this->results; 94 | } 95 | 96 | // in case of exact match, no need to go further 97 | if ($this->results instanceof MatchingResult) { 98 | return $this->results; 99 | } 100 | 101 | $dispatcher = $this->buildDispatcher($this->collector->getData()); 102 | unset($this->collector); 103 | 104 | $uriPath = '/'.trim($uri->path(), '/'); 105 | $routeInfo = $dispatcher->dispatch($httpMethod, $uriPath ? : '/'); 106 | if ($routeInfo[0] === Dispatcher::FOUND) { 107 | $route = $this->parsedRoutes[$routeInfo[1]]; 108 | $vars = $routeInfo[2]; 109 | 110 | $this->results = $this->finalizeRoute($route, $vars, $uri); 111 | } 112 | 113 | $this->results or $this->results = new MatchingResult(['route' => null]); 114 | 115 | unset($this->parsedRoutes); 116 | 117 | return $this->results; 118 | } 119 | 120 | /** 121 | * @param \Brain\Cortex\Uri\UriInterface $uri 122 | * @param string $httpMethod 123 | * @return int 124 | */ 125 | private function parseRoutes(UriInterface $uri, $httpMethod) 126 | { 127 | $iterator = new RouteFilterIterator($this->routes, $uri); 128 | $parsed = 0; 129 | /** @var \Brain\Cortex\Route\RouteInterface $route */ 130 | foreach ($iterator as $route) { 131 | $route = $this->sanitizeRouteMethod($this->groups->mergeGroup($route), $httpMethod); 132 | if (! $this->validateRoute($route, $httpMethod)) { 133 | continue; 134 | } 135 | 136 | $parsed++; 137 | $id = $route->id(); 138 | $this->parsedRoutes[$id] = $route; 139 | $path = '/'.trim((string)$route['path'], '/'); 140 | // exact match 141 | if ($path === '/'.trim($uri->path(), '/')) { 142 | $this->results = $this->finalizeRoute($route, [], $uri); 143 | unset($this->parsedRoutes, $this->collector); 144 | break; 145 | } 146 | 147 | $this->collector->addRoute(strtoupper($route['method']), $path, $id); 148 | } 149 | 150 | unset($this->routes, $this->groups); 151 | 152 | return $parsed; 153 | } 154 | 155 | /** 156 | * @param \Brain\Cortex\Route\RouteInterface $route 157 | * @param string $httpMethod 158 | * @return \Brain\Cortex\Route\RouteInterface 159 | */ 160 | private function sanitizeRouteMethod(RouteInterface $route, $httpMethod) 161 | { 162 | if (empty($route['method']) || ! (is_string($route['method']) || is_array($route['method']))) { 163 | $route['method'] = $httpMethod; 164 | } 165 | 166 | if (is_array($route['method'])) { 167 | $route['method'] = array_map('strtoupper', array_filter($route['method'], 'is_string')); 168 | 169 | return $route; 170 | } 171 | 172 | (strtolower($route['method']) === 'any') and $route['method'] = $httpMethod; 173 | 174 | $route['method'] = strtoupper($route['method']); 175 | 176 | return $route; 177 | } 178 | 179 | /** 180 | * @param \Brain\Cortex\Route\RouteInterface $route 181 | * @param $httpMethod 182 | * @return bool 183 | */ 184 | private function validateRoute(RouteInterface $route, $httpMethod) 185 | { 186 | $id = $route->id(); 187 | $path = trim((string)$route['path'], '/'); 188 | $handler = $route['handler']; 189 | 190 | return 191 | is_string($id) 192 | && $id 193 | && filter_var($path, FILTER_SANITIZE_URL) === $path 194 | && in_array($httpMethod, (array) $route['method'], true) 195 | && (is_callable($handler) || $handler instanceof ControllerInterface); 196 | } 197 | 198 | /** 199 | * @param array $data 200 | * @return \FastRoute\Dispatcher 201 | */ 202 | private function buildDispatcher(array $data) 203 | { 204 | $dispatcher = null; 205 | if (is_callable($this->dispatcherFactory)) { 206 | $factory = $this->dispatcherFactory; 207 | $dispatcher = $factory($data); 208 | } 209 | 210 | $dispatcher instanceof Dispatcher or $dispatcher = new DefDispatcher($data); 211 | 212 | return $dispatcher; 213 | } 214 | 215 | /** 216 | * @param \Brain\Cortex\Route\RouteInterface $route 217 | * @param array $vars 218 | * @param \Brain\Cortex\Uri\UriInterface $uri 219 | * @return \Brain\Cortex\Router\MatchingResult 220 | */ 221 | private function finalizeRoute(RouteInterface $route, array $vars, UriInterface $uri) 222 | { 223 | is_null($route['merge_query_string']) and $route['merge_query_string'] = true; 224 | $merge = filter_var($route['merge_query_string'], FILTER_VALIDATE_BOOLEAN); 225 | $uriVars = $uri->vars(); 226 | $merge and $vars = array_merge($vars, $uriVars); 227 | // vars is going to be modified if route vars is a callback, lets save this as a backup 228 | $varsOriginal = $vars; 229 | $result = null; 230 | switch (true) { 231 | case (is_callable($route['vars'])) : 232 | /** @var callable $cb */ 233 | $cb = $route['vars']; 234 | $routeVars = $cb($vars, $uri); 235 | is_array($routeVars) and $vars = $routeVars; 236 | $routeVars instanceof MatchingResult and $result = $routeVars; 237 | break; 238 | case (is_array($route['vars'])) : 239 | $vars = array_merge($route['vars'], $vars); 240 | break; 241 | case ($route['vars'] instanceof MatchingResult) : 242 | $result = $route['vars']; 243 | break; 244 | } 245 | 246 | if ($result instanceof MatchingResult) { 247 | return $result; 248 | } 249 | 250 | if (! empty($route['default_vars']) && is_array($route['default_vars'])) { 251 | $vars = array_merge($route['default_vars'], $vars); 252 | } 253 | 254 | $vars = $this->ensurePreviewVars($vars, $uriVars); 255 | $vars = apply_filters('cortex.matched-vars', $vars, $route, $uri); 256 | $noTemplate = filter_var($route['no_template'], FILTER_VALIDATE_BOOLEAN); 257 | 258 | return new MatchingResult([ 259 | 'vars' => (array)$vars, 260 | 'matches' => (array)$varsOriginal, 261 | 'route' => $route->id(), 262 | 'path' => $route['path'], 263 | 'handler' => $route['handler'], 264 | 'before' => $route['before'], 265 | 'after' => $route['after'], 266 | 'template' => $noTemplate ? false : $route['template'], 267 | ]); 268 | } 269 | 270 | /** 271 | * To ensure preview works, we need to merge preview-related query string 272 | * to query arguments. 273 | * 274 | * @param array $vars 275 | * @param array $uriVars 276 | * @return array 277 | */ 278 | private function ensurePreviewVars(array $vars, array $uriVars) 279 | { 280 | if (! is_user_logged_in()) { 281 | return $vars; 282 | } 283 | 284 | foreach (['preview', 'preview_id', 'preview_nonce'] as $var) { 285 | if (! isset($vars[$var]) && isset($uriVars[$var])) { 286 | $vars[$var] = $uriVars[$var]; 287 | } 288 | } 289 | 290 | return $vars; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Cortex/Router/RouterInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Router; 12 | 13 | use Brain\Cortex\Uri\UriInterface; 14 | 15 | /** 16 | * @author Giuseppe Mazzapica 17 | * @license http://opensource.org/licenses/MIT MIT 18 | * @package Cortex 19 | */ 20 | interface RouterInterface 21 | { 22 | /** 23 | * @param \Brain\Cortex\Uri\UriInterface $uri 24 | * @param string $httpMethod 25 | * @return \Brain\Cortex\Router\MatchingResult 26 | */ 27 | public function match(UriInterface $uri, $httpMethod); 28 | } 29 | -------------------------------------------------------------------------------- /src/Cortex/Uri/PsrUri.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Uri; 12 | 13 | use Psr\Http\Message\UriInterface as PsrUriInterface; 14 | 15 | /** 16 | * @author Giuseppe Mazzapica 17 | * @license http://opensource.org/licenses/MIT MIT 18 | * @package Cortex 19 | */ 20 | final class PsrUri implements PsrUriInterface 21 | { 22 | /** 23 | * @var array 24 | */ 25 | private $storage = [ 26 | 'scheme' => 'http', 27 | 'host' => '', 28 | 'path' => '/', 29 | 'query_string' => '', 30 | ]; 31 | 32 | /** 33 | * @var array 34 | */ 35 | private $server; 36 | 37 | /** 38 | * @var bool 39 | */ 40 | private $parsed = false; 41 | 42 | /** 43 | * WordPressUri constructor. 44 | * 45 | * @param array $server 46 | */ 47 | public function __construct(array $server = []) 48 | { 49 | $this->server = $server ? : $_SERVER; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function getScheme() 56 | { 57 | $this->parsed or $this->marshallFromServer(); 58 | 59 | return $this->storage['scheme']; 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function getHost() 66 | { 67 | $this->parsed or $this->marshallFromServer(); 68 | 69 | return $this->storage['host']; 70 | } 71 | 72 | /** 73 | * Not really implemented, because Cortex does not need it. 74 | * 75 | * @inheritdoc 76 | * @codeCoverageIgnore 77 | */ 78 | public function getPort() 79 | { 80 | return; 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public function getPath() 87 | { 88 | $this->parsed or $this->marshallFromServer(); 89 | 90 | return $this->storage['path']; 91 | } 92 | 93 | /** 94 | * @inheritdoc 95 | */ 96 | public function getQuery() 97 | { 98 | $this->parsed or $this->marshallFromServer(); 99 | 100 | return $this->storage['query_string']; 101 | } 102 | 103 | /** 104 | * Not really implemented, because Cortex does not need it. 105 | * 106 | * @inheritdoc 107 | * @codeCoverageIgnore 108 | */ 109 | public function getAuthority() 110 | { 111 | return ''; 112 | } 113 | 114 | /** 115 | * Not really implemented, because Cortex does not need it. 116 | * 117 | * @inheritdoc 118 | * @codeCoverageIgnore 119 | */ 120 | public function getUserInfo() 121 | { 122 | return ''; 123 | } 124 | 125 | /** 126 | * Not really implemented, because Cortex does not need it. 127 | * 128 | * @inheritdoc 129 | * @codeCoverageIgnore 130 | */ 131 | public function getFragment() 132 | { 133 | return ''; 134 | } 135 | 136 | /** 137 | * Disabled. 138 | * 139 | * @inheritdoc 140 | */ 141 | public function withScheme($scheme) 142 | { 143 | throw new \BadMethodCallException(sprintf('%s is read-only.', __CLASS__)); 144 | } 145 | 146 | /** 147 | * Disabled. 148 | * 149 | * @inheritdoc 150 | */ 151 | public function withUserInfo($user, $password = null) 152 | { 153 | throw new \BadMethodCallException(sprintf('%s is read-only.', __CLASS__)); 154 | } 155 | 156 | /** 157 | * Disabled. 158 | * 159 | * @inheritdoc 160 | */ 161 | public function withHost($host) 162 | { 163 | throw new \BadMethodCallException(sprintf('%s is read-only.', __CLASS__)); 164 | } 165 | 166 | /** 167 | * Disabled. 168 | * 169 | * @inheritdoc 170 | */ 171 | public function withPort($port) 172 | { 173 | throw new \BadMethodCallException(sprintf('%s is read-only.', __CLASS__)); 174 | } 175 | 176 | /** 177 | * Disabled. 178 | * 179 | * @inheritdoc 180 | */ 181 | public function withPath($path) 182 | { 183 | throw new \BadMethodCallException(sprintf('%s is read-only.', __CLASS__)); 184 | } 185 | 186 | /** 187 | * Disabled. 188 | * 189 | * @inheritdoc 190 | */ 191 | public function withQuery($query) 192 | { 193 | throw new \BadMethodCallException(sprintf('%s is read-only.', __CLASS__)); 194 | } 195 | 196 | /** 197 | * Disabled. 198 | * 199 | * @inheritdoc 200 | */ 201 | public function withFragment($fragment) 202 | { 203 | throw new \BadMethodCallException(sprintf('%s is read-only.', __CLASS__)); 204 | } 205 | 206 | /** 207 | * @inheritdoc 208 | */ 209 | public function __toString() 210 | { 211 | $this->parsed or $this->marshallFromServer(); 212 | 213 | $url = sprintf('%s:://%s/%s', $this->getScheme(), $this->getHost(), $this->getPath()); 214 | $query = $this->getQuery(); 215 | 216 | return $query ? "{$url}?{$query}" : $url; 217 | } 218 | 219 | /** 220 | * Parse server array to find url components. 221 | */ 222 | private function marshallFromServer() 223 | { 224 | $scheme = is_ssl() ? 'https' : 'http'; 225 | 226 | $host = $this->marshallHostFromServer() ? : parse_url(home_url(), PHP_URL_HOST); 227 | $host = trim((string)$host, '/'); 228 | 229 | $pathArray = explode('?', $this->marshallPathFromServer(), 2); 230 | $path = trim($pathArray[0], '/'); 231 | 232 | empty($path) and $path = '/'; 233 | 234 | $query_string = ''; 235 | if (isset($this->server['QUERY_STRING'])) { 236 | $query_string = ltrim($this->server['QUERY_STRING'], '?'); 237 | } 238 | 239 | $this->storage = compact('scheme', 'host', 'path', 'query_string'); 240 | $this->parsed = true; 241 | } 242 | 243 | /** 244 | * Parse server array to find url host. 245 | * 246 | * Contains code from Zend\Diactoros\ServerRequestFactory 247 | * 248 | * @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com) 249 | * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD 250 | * License 251 | * 252 | * @return string 253 | */ 254 | private function marshallHostFromServer() 255 | { 256 | $host = isset($this->server['HTTP_HOST']) ? $this->server['HTTP_HOST'] : ''; 257 | if (empty($host)) { 258 | return isset($this->server['SERVER_NAME']) ? $this->server['SERVER_NAME'] : ''; 259 | } 260 | 261 | if (is_string($host) && preg_match('|\:(\d+)$|', $host, $matches)) { 262 | $host = substr($host, 0, -1 * (strlen($matches[1]) + 1)); 263 | } 264 | 265 | return $host; 266 | } 267 | 268 | /** 269 | * Parse server array to find url path. 270 | * 271 | * Contains code from Zend\Diactoros\ServerRequestFactory 272 | * 273 | * @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com) 274 | * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD 275 | * License 276 | * 277 | * @return string 278 | */ 279 | private function marshallPathFromServer() 280 | { 281 | $get = function ($key, array $values, $default = null) { 282 | return array_key_exists($key, $values) ? $values[$key] : $default; 283 | }; 284 | 285 | // IIS7 with URL Rewrite: make sure we get the unencoded url 286 | // (double slash problem). 287 | $iisUrlRewritten = $get('IIS_WasUrlRewritten', $this->server); 288 | $unencodedUrl = $get('UNENCODED_URL', $this->server, ''); 289 | if ('1' == $iisUrlRewritten && ! empty($unencodedUrl)) { 290 | return $unencodedUrl; 291 | } 292 | 293 | $requestUri = $get('REQUEST_URI', $this->server); 294 | 295 | // Check this first so IIS will catch. 296 | $httpXRewriteUrl = $get('HTTP_X_REWRITE_URL', $this->server); 297 | if ($httpXRewriteUrl !== null) { 298 | $requestUri = $httpXRewriteUrl; 299 | } 300 | 301 | // Check for IIS 7.0 or later with ISAPI_Rewrite 302 | $httpXOriginalUrl = $get('HTTP_X_ORIGINAL_URL', $this->server); 303 | if ($httpXOriginalUrl !== null) { 304 | $requestUri = $httpXOriginalUrl; 305 | } 306 | 307 | if ($requestUri !== null) { 308 | return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri); 309 | } 310 | 311 | $origPathInfo = $get('ORIG_PATH_INFO', $this->server); 312 | 313 | return empty($origPathInfo) ? '/' : $origPathInfo; 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/Cortex/Uri/UriInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Uri; 12 | 13 | /** 14 | * @author Giuseppe Mazzapica 15 | * @license http://opensource.org/licenses/MIT MIT 16 | * @package Cortex 17 | */ 18 | interface UriInterface 19 | { 20 | /** 21 | * @return string 22 | */ 23 | public function scheme(); 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function host(); 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function path(); 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function chunks(); 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function vars(); 44 | } 45 | -------------------------------------------------------------------------------- /src/Cortex/Uri/WordPressUri.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain\Cortex\Uri; 12 | 13 | use Psr\Http\Message\UriInterface as PsrUriInterface; 14 | 15 | /** 16 | * @author Giuseppe Mazzapica 17 | * @license http://opensource.org/licenses/MIT MIT 18 | * @package Cortex 19 | */ 20 | final class WordPressUri implements UriInterface 21 | { 22 | /** 23 | * @var \Psr\Http\Message\UriInterface 24 | */ 25 | private $uri; 26 | 27 | /** 28 | * WordPressUri constructor. 29 | * 30 | * @param \Psr\Http\Message\UriInterface $uri 31 | */ 32 | public function __construct(PsrUriInterface $uri) 33 | { 34 | $this->uri = $uri; 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function scheme() 41 | { 42 | return $this->uri->getScheme(); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function host() 49 | { 50 | return $this->uri->getHost(); 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function chunks() 57 | { 58 | $path = $this->path(); 59 | 60 | return $path === '/' ? [] : explode('/', $path); 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function path() 67 | { 68 | /* 69 | * If WordPress is installed in a subfolder and home url is something like 70 | * `example.com/subfolder` we need to strip down `/subfolder` from path and build a path 71 | * for route matching that is relative to home url. 72 | */ 73 | $homePath = trim((string)parse_url(home_url(), PHP_URL_PATH), '/'); 74 | $path = trim($this->uri->getPath(), '/'); 75 | if ($homePath && strpos($path, $homePath) === 0) { 76 | $path = trim(substr($path, strlen($homePath)), '/'); 77 | } 78 | 79 | return $path ? : '/'; 80 | } 81 | 82 | /** 83 | * @inheritdoc 84 | */ 85 | public function vars() 86 | { 87 | $vars = []; 88 | parse_str($this->uri->getQuery(), $vars); 89 | 90 | return $vars; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Routes.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Brain; 12 | 13 | use Brain\Cortex\Group\Group; 14 | use Brain\Cortex\Group\GroupCollectionInterface; 15 | use Brain\Cortex\Route\QueryRoute; 16 | use Brain\Cortex\Route\RedirectRoute; 17 | use Brain\Cortex\Route\RouteCollectionInterface; 18 | 19 | /** 20 | * @author Giuseppe Mazzapica 21 | * @license http://opensource.org/licenses/MIT MIT 22 | * @package Cortex 23 | */ 24 | class Routes 25 | { 26 | /** 27 | * @param string $method 28 | */ 29 | private static function checkTiming($method) 30 | { 31 | if (! Cortex::late() && ! did_action('parse_request')) { 32 | return; 33 | } 34 | 35 | $exception = new \BadMethodCallException( 36 | sprintf('%s must be called before "do_parse_request".', $method) 37 | ); 38 | 39 | if (defined('WP_DEBUG') && WP_DEBUG) { 40 | throw $exception; 41 | } 42 | 43 | do_action('cortex.fail', $exception); 44 | } 45 | 46 | /** 47 | * @param string $path 48 | * @param callable|array $query 49 | * @param array $options 50 | * @return \Brain\Cortex\Route\RouteInterface 51 | */ 52 | public static function add($path, $query, array $options = []) 53 | { 54 | self::checkTiming(__METHOD__); 55 | 56 | $routeObj = new QueryRoute($path, $query, $options); 57 | 58 | add_action( 59 | 'cortex.routes', 60 | function (RouteCollectionInterface $collection) use ($routeObj) { 61 | $collection->addRoute($routeObj); 62 | } 63 | ); 64 | 65 | return $routeObj; 66 | } 67 | 68 | /** 69 | * @param string $path 70 | * @param string|callable $to 71 | * @param int $status 72 | * @param bool $external 73 | * @return \Brain\Cortex\Route\RedirectRoute 74 | */ 75 | public static function redirect($path, $to, $status = 301, $external = false) 76 | { 77 | self::checkTiming(__METHOD__); 78 | 79 | $routeObj = new RedirectRoute($path, $to, [ 80 | 'redirect_status' => $status, 81 | 'redirect_external' => $external, 82 | ]); 83 | 84 | add_action( 85 | 'cortex.routes', 86 | function (RouteCollectionInterface $collection) use ($routeObj) { 87 | $collection->addRoute($routeObj); 88 | } 89 | ); 90 | 91 | return $routeObj; 92 | } 93 | 94 | /** 95 | * @param string $id 96 | * @param array $group 97 | * @return \Brain\Cortex\Group\GroupInterface 98 | */ 99 | public static function group($id, array $group) 100 | { 101 | self::checkTiming(__METHOD__); 102 | 103 | $group['id'] = $id; 104 | $groupObj = new Group($group); 105 | 106 | add_action( 107 | 'cortex.groups', 108 | function (GroupCollectionInterface $collection) use ($groupObj) { 109 | $collection->addGroup($groupObj); 110 | } 111 | ); 112 | 113 | return $groupObj; 114 | } 115 | } 116 | --------------------------------------------------------------------------------