From 3a07312cf9b2774baf5bc0d60b6736c3c78dda7a Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Fri, 15 May 2020 03:28:48 +0200 Subject: [PATCH 01/15] #343 - Move redirect handling to new redirector event subscriber This allows pages to be to redirect any url, anywhere, even when not in a page context. --- .../dispatcher/behavior/redirectable.php | 28 ----------- .../com_pages/event/subscriber/redirector.php | 50 +++++++++++++++++++ .../resources/config/bootstrapper.php | 3 +- 3 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 code/site/components/com_pages/event/subscriber/redirector.php diff --git a/code/site/components/com_pages/dispatcher/behavior/redirectable.php b/code/site/components/com_pages/dispatcher/behavior/redirectable.php index ac04a56a2..f30f9d292 100644 --- a/code/site/components/com_pages/dispatcher/behavior/redirectable.php +++ b/code/site/components/com_pages/dispatcher/behavior/redirectable.php @@ -18,34 +18,6 @@ protected function _initialize(KObjectConfig $config) parent::_initialize($config); } - protected function _beforeDispatch(KDispatcherContextInterface $context) - { - $router = $this->getObject('com://site/pages.dispatcher.router.redirect', ['request' => $context->request]); - - if(false !== $route = $router->resolve()) - { - if($route->toString(KHttpUrl::AUTHORITY)) - { - //External redierct: 301 permanent - $status = KHttpResponse::MOVED_PERMANENTLY; - } - else - { - //Internal redirect: 307 temporary - $status = KHttpResponse::TEMPORARY_REDIRECT; - } - - //Qualify the route - $url = $router->qualify($route); - - //Set the location header - $context->getResponse()->getHeaders()->set('Location', $url); - $context->getResponse()->setStatus($status); - - $context->getSubject()->send(); - } - } - protected function _beforeSend(KDispatcherContextInterface $context) { $response = $context->response; diff --git a/code/site/components/com_pages/event/subscriber/redirector.php b/code/site/components/com_pages/event/subscriber/redirector.php new file mode 100644 index 000000000..5381580f8 --- /dev/null +++ b/code/site/components/com_pages/event/subscriber/redirector.php @@ -0,0 +1,50 @@ + + * @link https://github.com/joomlatools/joomlatools-pages for the canonical source repository + */ + +class ComPagesEventSubscriberRedirector extends ComPagesEventSubscriberAbstract +{ + protected function _initialize(KObjectConfig $config) + { + $config->append(array( + 'priority' => KEvent::PRIORITY_HIGH, + )); + + parent::_initialize($config); + } + + public function onAfterApplicationRoute(KEventInterface $event) + { + $request = $this->getObject('request'); + $router = $this->getObject('com://site/pages.dispatcher.router.redirect', ['request' => $request]); + + if(false !== $route = $router->resolve()) + { + if($route->toString(KHttpUrl::AUTHORITY)) + { + //External redierct: 301 permanent + $status = KHttpResponse::MOVED_PERMANENTLY; + } + else + { + //Internal redirect: 307 temporary + $status = KHttpResponse::TEMPORARY_REDIRECT; + } + + //Qualify the route + $url = $router->qualify($route); + + //Set the location header + $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); + $dispatcher->getResponse()->getHeaders()->set('Location', $url); + $dispatcher->getResponse()->setStatus($status); + + $dispatcher->send(); + } + } +} \ No newline at end of file diff --git a/code/site/components/com_pages/resources/config/bootstrapper.php b/code/site/components/com_pages/resources/config/bootstrapper.php index 0eab3b46b..638709125 100644 --- a/code/site/components/com_pages/resources/config/bootstrapper.php +++ b/code/site/components/com_pages/resources/config/bootstrapper.php @@ -63,6 +63,7 @@ 'event.subscriber.factory' => [ 'subscribers' => [ 'com://site/pages.event.subscriber.bootstrapper', + 'com://site/pages.event.subscriber.redirector', 'com://site/pages.event.subscriber.dispatcher', 'com://site/pages.event.subscriber.pagedecorator', 'com://site/pages.event.subscriber.errorhandler', @@ -82,7 +83,7 @@ } ], 'com://site/pages.dispatcher.router.site' => [ - 'routes' => isset($config['sites']) ? array_flip($config['sites']) : array(JPATH_ROOT.'/joomlatools-pages' => '[*]'), + 'routes' => isset($config['sites']) ? $config['sites'] : array('[*]' => JPATH_ROOT.'/joomlatools-pages'), ], ] ]; \ No newline at end of file From b7548b8c3717aa7923ca9575f16dd4cd43efcc6b Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Fri, 15 May 2020 03:31:13 +0200 Subject: [PATCH 02/15] #343 - Also pass in in the request query when resolving redirects --- .../com_pages/dispatcher/router/redirect.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/code/site/components/com_pages/dispatcher/router/redirect.php b/code/site/components/com_pages/dispatcher/router/redirect.php index 51a752bca..0e1e36428 100644 --- a/code/site/components/com_pages/dispatcher/router/redirect.php +++ b/code/site/components/com_pages/dispatcher/router/redirect.php @@ -24,14 +24,21 @@ protected function _initialize(KObjectConfig $config) public function resolve($route = null, array $parameters = array()) { - if(!$route) + $result = false; + if(count($this->getConfig()->routes)) { - $base = $this->getRequest()->getBasePath(); - $url = urldecode( $this->getRequest()->getUrl()->getPath()); + if(!$route) + { + $base = $this->getRequest()->getBasePath(); + $url = urldecode( $this->getRequest()->getUrl()->getPath()); + $parameters = $this->getRequest()->getUrl()->getQuery(true); - $route = trim(str_replace(array($base, '/index.php'), '', $url), '/'); + $route = trim(str_replace(array($base, '/index.php'), '', $url), '/'); + } + + $result = parent::resolve($route, $parameters); } - return parent::resolve($route, $parameters); + return $result; } } \ No newline at end of file From 93a1074b46d896807a191af16b60294485e0461f Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Fri, 15 May 2020 03:47:24 +0200 Subject: [PATCH 03/15] #343 - Add a new 'routers' config option to allow to register routers identifiers by a custom scheme and rename 'pages' scheme to 'page' --- .../controller/behavior/breadcrumbable.php | 2 +- .../components/com_pages/dispatcher/http.php | 2 +- .../com_pages/dispatcher/router/pages.php | 2 +- .../com_pages/dispatcher/router/router.php | 91 +++++++++++++++---- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/code/site/components/com_pages/controller/behavior/breadcrumbable.php b/code/site/components/com_pages/controller/behavior/breadcrumbable.php index e42b50186..c52011a32 100644 --- a/code/site/components/com_pages/controller/behavior/breadcrumbable.php +++ b/code/site/components/com_pages/controller/behavior/breadcrumbable.php @@ -27,7 +27,7 @@ protected function _beforeRender(KControllerContextInterface $context) { $segments[] = $segment; - if($route = $router->generate('pages:'.implode('/', $segments))) + if($route = $router->generate('page:'.implode('/', $segments))) { $page = $route->getPage(); diff --git a/code/site/components/com_pages/dispatcher/http.php b/code/site/components/com_pages/dispatcher/http.php index c14cfe4d3..5f2b9ee36 100644 --- a/code/site/components/com_pages/dispatcher/http.php +++ b/code/site/components/com_pages/dispatcher/http.php @@ -72,7 +72,7 @@ public function getRoute() $url = urldecode($this->getRequest()->getUrl()->getPath()); $path = trim(str_replace(array($base, '/index.php'), '', $url), '/'); - $this->__route = $this->getRouter()->resolve('pages:'.$path, $this->getRequest()->query->toArray()); + $this->__route = $this->getRouter()->resolve('page:'.$path, $this->getRequest()->query->toArray()); } if(is_object($this->__route)) { diff --git a/code/site/components/com_pages/dispatcher/router/pages.php b/code/site/components/com_pages/dispatcher/router/pages.php index e4f038bb5..88f0d32be 100644 --- a/code/site/components/com_pages/dispatcher/router/pages.php +++ b/code/site/components/com_pages/dispatcher/router/pages.php @@ -27,7 +27,7 @@ protected function _initialize(KObjectConfig $config) public function getRoute($route, array $parameters = array()) { if($route instanceof ComPagesModelEntityPage) { - $route = 'pages:'.$route->path; + $route = 'page:'.$route->path; } return parent::getRoute($route, $parameters); diff --git a/code/site/components/com_pages/dispatcher/router/router.php b/code/site/components/com_pages/dispatcher/router/router.php index 1eabf46a9..96c0df12a 100644 --- a/code/site/components/com_pages/dispatcher/router/router.php +++ b/code/site/components/com_pages/dispatcher/router/router.php @@ -30,6 +30,29 @@ public function __construct(KObjectConfig $config) //Add a global object alias $this->getObject('manager')->registerAlias($this->getIdentifier(), 'router'); + + $this->__routers = KObjectConfig::unbox($config->routers); + } + + /** + * Initializes the options for the object + * + * Called from {@link __construct()} as a first step of object instantiation. + * + * @param KObjectConfig $config An optional ObjectConfig object with configuration options. + * @return void + */ + protected function _initialize(KObjectConfig $config) + { + $config->append(array( + 'routers' => [ + 'page' => 'com://site/pages.dispatcher.router.pages', + 'site' => 'com://site/pages.dispatcher.router.site', + 'redirect' => 'com://site/pages.dispatcher.router.redirect', + ], + )); + + parent::_initialize($config); } /** @@ -43,24 +66,38 @@ public function __construct(KObjectConfig $config) */ public function resolve($route, array $parameters = array()) { - $result = false; + $identifier = null; - //Find router package + //Find router identifier if($route instanceof KObjectInterface) { - if($route instanceof ComPagesDispatcherRouterRouteInterface) { + if($route instanceof ComPagesDispatcherRouterRouteInterface) + { $package = $route->getScheme(); - } else { - $package = $route->getIdentifier()->getPackage(); + + if(isset($this->__routers[$package])) { + $identifier = $this->__routers[$package]; + } + } + else $package = $route->getIdentifier()->getPackage(); + } + else + { + $package = parse_url($route, PHP_URL_SCHEME); + + if(isset($this->__routers[$package])) { + $identifier = $this->__routers[$package]; } } - else $package = parse_url($route, PHP_URL_SCHEME); + + //Identifier Fallback + if(!$identifier) { + $identifier = 'com://site/' . $package . '.dispatcher.router.' . $package; + } //Get router instance - if(!isset($this->__routers[$package])) + if(is_string($identifier)) { - $identifier = 'com://site/'.$package.'.dispatcher.router.'.$package; - $config = [ 'request' => $this->getRequest(), 'resolvers' => $this->getResolvers() @@ -70,7 +107,7 @@ public function resolve($route, array $parameters = array()) $this->__routers[$package] = $router; } - else $router = $this->__routers[$package]; + else $router = $identifier; return $router->resolve($route, $parameters); } @@ -86,24 +123,38 @@ public function resolve($route, array $parameters = array()) */ public function generate($route, array $parameters = array()) { - $result = false; + $identifier = null; - //Find router package + //Find router identifier if($route instanceof KObjectInterface) { - if($route instanceof ComPagesDispatcherRouterRouteInterface) { + if($route instanceof ComPagesDispatcherRouterRouteInterface) + { $package = $route->getScheme(); - } else { - $package = $route->getIdentifier()->getPackage(); + + if(isset($this->__routers[$package])) { + $identifier = $this->__routers[$package]; + } + } + else $package = $route->getIdentifier()->getPackage(); + } + else + { + $package = parse_url($route, PHP_URL_SCHEME); + + if(isset($this->__routers[$package])) { + $identifier = $this->__routers[$package]; } } - else $package = parse_url($route, PHP_URL_SCHEME); + + //Identifier Fallback + if(!$identifier) { + $identifier = 'com://site/' . $package . '.dispatcher.router.' . $package; + } //Get router instance - if(!isset($this->__routers[$package])) + if(is_string($identifier)) { - $identifier = 'com://site/'.$package.'.dispatcher.router.'.$package; - $config = [ 'request' => $this->getRequest(), 'resolvers' => $this->getResolvers() @@ -113,7 +164,7 @@ public function generate($route, array $parameters = array()) $this->__routers[$package] = $router; } - else $router = $this->__routers[$package]; + else $router = $identifier; return $router->generate($route, $parameters); } From 13e4f5f9d16ff385513d1162c4a1b29151878ed2 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Fri, 15 May 2020 03:55:47 +0200 Subject: [PATCH 04/15] #343 - Add support for callback route targets The callback is defined is as follows: 'function($route, $generate = false)' - $route: a ComPagesDispatcherRouteRouteInterface object - $generate: are we generating a url or resolving (default false) Callbacks are both supported for static and dynamic routes, in case of a dynamic route the callback is called only if the route could be succesfully resolved. --- .../dispatcher/router/resolver/regex.php | 70 ++++++++++++------- .../components/com_pages/page/registry.php | 29 ++++++-- .../com_pages/resources/config/site.php | 2 +- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/code/site/components/com_pages/dispatcher/router/resolver/regex.php b/code/site/components/com_pages/dispatcher/router/resolver/regex.php index 0fb7060b0..21609777e 100644 --- a/code/site/components/com_pages/dispatcher/router/resolver/regex.php +++ b/code/site/components/com_pages/dispatcher/router/resolver/regex.php @@ -90,18 +90,21 @@ protected function _initialize(KObjectConfig $config) * Add a route for matching * * @param string $regex The route regex You can use multiple pre-set regex filters, like [digit:id] - * @param string $path The path this route should point to. + * @param string|callable $target The target this route points to * @return ComPagesDispatcherRouterResolverInterface */ - public function addRoute($regex, $path) + public function addRoute($regex, $target) { $regex = trim($regex, '/'); - $path = rtrim($path, '/'); + + if(is_string($target)) { + $path = rtrim($target, '/'); + } if(strpos($regex, '[') !== false) { - $this->__dynamic_routes[$regex] = $path; + $this->__dynamic_routes[$regex] = $target; } else { - $this->__static_routes[$regex] = $path; + $this->__static_routes[$regex] = $target; } return $this; @@ -115,16 +118,8 @@ public function addRoute($regex, $path) */ public function addRoutes($routes) { - foreach((array)KObjectConfig::unbox($routes) as $path => $routes) - { - foreach((array) $routes as $regex) - { - if (is_numeric($path)) { - $this->addRoute($regex, $regex); - } else { - $this->addRoute($regex, $path); - } - } + foreach((array)KObjectConfig::unbox($routes) as $regex => $target) { + $this->addRoute($regex, $target); } return $this; @@ -173,8 +168,13 @@ public function resolve(ComPagesDispatcherRouterRouteInterface $route) $this->__static_routes = array($path => $result) + $this->__static_routes; } - if($result !== false) { - $this->_buildRoute($result, $route); + if($result !== false) + { + if(is_callable($result)) { + $result = (bool) call_user_func($result, $route, false); + } else { + $result = $this->_buildRoute($result, $route); + } } return $result !== false ? parent::resolve($route) : false; @@ -194,12 +194,20 @@ public function generate(ComPagesDispatcherRouterRouteInterface $route) $path = ltrim($route->getPath(), '/'); //Dynamic routes - if($routes = array_keys($this->__dynamic_routes, $path)) + $routes = $this->__dynamic_routes; + + foreach($routes as $regex => $target) { - foreach($routes as $regex) + if(is_callable($target)) + { + //Parse the route to match it + if($this->_parseRoute($regex, $route) && (bool) call_user_func($target, $route, false) == true) { + $generated = true; break; + } + } + else { - //Generate the dynamic route - if($this->_buildRoute($regex, $route)) { + if($target == $path && $this->_buildRoute($regex, $route)) { $generated = true; break; } } @@ -208,12 +216,22 @@ public function generate(ComPagesDispatcherRouterRouteInterface $route) //Static routes if(!$generated) { - $routes = array_flip(array_reverse($this->__static_routes, true)); + $routes = array_reverse($this->__static_routes, true); - if(isset($routes[$path])) + foreach($routes as $regex => $target) { - if($this->_buildRoute($routes[$path], $route)) { - $generated = true; + if(is_callable($target)) + { + //Compare the path to match it + if($regex == $path && (bool) call_user_func($target, $route, false) == true) { + $generated = true; break; + } + } + else + { + if($target == $path && $this->_buildRoute($regex, $route)) { + $generated = true; break; + } } } } @@ -338,7 +356,7 @@ protected function _buildRoute($regex, ComPagesDispatcherRouterRouteInterface $r if($optional) { $regex = str_replace($pre . $block, '', $regex); } else { - $result = false; break; + $result = false; break; } } } diff --git a/code/site/components/com_pages/page/registry.php b/code/site/components/com_pages/page/registry.php index 16414784f..2a1f00cc1 100755 --- a/code/site/components/com_pages/page/registry.php +++ b/code/site/components/com_pages/page/registry.php @@ -239,10 +239,29 @@ public function getPageContent($path, $render = false) public function getRoutes($path = null) { - if(!is_null($path)) { - $result = $this->__data['routes'][$path]; - } else { - $result = $this->__data['routes']; + $result = array(); + if(is_null($path)) + { + foreach( $this->__data['routes'] as $path => $routes) + { + foreach((array) $routes as $regex) + { + if (is_numeric($path)) { + $result[$regex] = $regex; + } else { + $result[$regex] = $path; + } + } + } + } + else + { + if(isset($this->__data['routes'][$path])) + { + foreach((array) $this->__data['routes'][$path] as $regex) { + $result[$regex] = $path; + } + } } return $result; @@ -458,7 +477,7 @@ public function loadCache($basedir, $refresh = true) $result['pages'] = $pages; $result['routes'] = $routes; $result['collections'] = $collections; - $result['redirects'] = array_flip($redirects); + $result['redirects'] = $redirects; //Generate a checksum $result['hash'] = hash('crc32b', serialize($result)); diff --git a/code/site/components/com_pages/resources/config/site.php b/code/site/components/com_pages/resources/config/site.php index 27375e7be..35117ef72 100644 --- a/code/site/components/com_pages/resources/config/site.php +++ b/code/site/components/com_pages/resources/config/site.php @@ -19,7 +19,7 @@ 'cache_path' => $config['page_cache_path'], 'cache_validation' => $config['page_cache_validation'], 'collections' => $config['collections'], - 'redirects' => array_flip($config['redirects']), + 'redirects' => $config['redirects'], 'properties' => $config['page'], ], 'data.registry' => [ From e637b59393eddc64778651c66e1b1651da6cf8b2 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sat, 16 May 2020 02:23:13 +0200 Subject: [PATCH 05/15] #343 - Implement separate named callbacks for 'resolve' and 'generate' Example: '/path/to/page' => [ 'generate' => function($route) { return true; }, 'resolve' => function($route) { return true; } ], --- .../dispatcher/router/resolver/regex.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/code/site/components/com_pages/dispatcher/router/resolver/regex.php b/code/site/components/com_pages/dispatcher/router/resolver/regex.php index 21609777e..f0d7a335d 100644 --- a/code/site/components/com_pages/dispatcher/router/resolver/regex.php +++ b/code/site/components/com_pages/dispatcher/router/resolver/regex.php @@ -170,8 +170,8 @@ public function resolve(ComPagesDispatcherRouterRouteInterface $route) if($result !== false) { - if(is_callable($result)) { - $result = (bool) call_user_func($result, $route, false); + if(isset($result['resolve']) && is_callable($result['resolve'])) { + $result = (bool) call_user_func($result['resolve'], $route); } else { $result = $this->_buildRoute($result, $route); } @@ -198,16 +198,17 @@ public function generate(ComPagesDispatcherRouterRouteInterface $route) foreach($routes as $regex => $target) { - if(is_callable($target)) + if(isset($target['generate']) && is_callable($target['generate'])) { //Parse the route to match it - if($this->_parseRoute($regex, $route) && (bool) call_user_func($target, $route, false) == true) { + if($this->_parseRoute($regex, $route) && (bool) call_user_func($target['generate'], $route) == true) { $generated = true; break; } } else { - if($target == $path && $this->_buildRoute($regex, $route)) { + //Parse the route to match it + if($this->_parseRoute($regex, $route) && $this->_buildRoute($target, $route)) { $generated = true; break; } } @@ -220,15 +221,16 @@ public function generate(ComPagesDispatcherRouterRouteInterface $route) foreach($routes as $regex => $target) { - if(is_callable($target)) + if(isset($target['generate']) && is_callable($target['generate'])) { //Compare the path to match it - if($regex == $path && (bool) call_user_func($target, $route, false) == true) { + if($regex == $path && (bool) call_user_func($target['generate'], $route) == true) { $generated = true; break; } } else { + //Compare the path to match it if($target == $path && $this->_buildRoute($regex, $route)) { $generated = true; break; } From 08482931882e1ad419425515952d123635eda3f1 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Tue, 19 May 2020 01:00:20 +0200 Subject: [PATCH 06/15] #347 - Add support for route target cache parameter and use dispatcher redirect method --- .../com_pages/event/subscriber/redirector.php | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/code/site/components/com_pages/event/subscriber/redirector.php b/code/site/components/com_pages/event/subscriber/redirector.php index 5381580f8..20a2c5298 100644 --- a/code/site/components/com_pages/event/subscriber/redirector.php +++ b/code/site/components/com_pages/event/subscriber/redirector.php @@ -25,26 +25,32 @@ public function onAfterApplicationRoute(KEventInterface $event) if(false !== $route = $router->resolve()) { - if($route->toString(KHttpUrl::AUTHORITY)) - { + $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); + $response = $dispatcher->getResponse(); + + //Set the location header + if($route->toString(KHttpUrl::AUTHORITY)) { //External redierct: 301 permanent $status = KHttpResponse::MOVED_PERMANENTLY; - } - else - { + } else { //Internal redirect: 307 temporary $status = KHttpResponse::TEMPORARY_REDIRECT; } + //Set the redirect status + $response->setStatus($status); + + //Set the cache time + if(isset($route->query['cache'])) + { + $response->setMaxAge($route->query['cache']); + unset($route->query['cache']); + } + //Qualify the route $url = $router->qualify($route); - //Set the location header - $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); - $dispatcher->getResponse()->getHeaders()->set('Location', $url); - $dispatcher->getResponse()->setStatus($status); - - $dispatcher->send(); + $dispatcher->redirect($url); } } } \ No newline at end of file From 3598b6e3dde316ca433e621f95b4b0e3821ad3d9 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Wed, 17 Jun 2020 02:24:41 +0200 Subject: [PATCH 07/15] #372 - Route segment filtering - Add 'slug' expression constraint - If the target contains a contraint the target segment will be filtered acoordingly --- .../dispatcher/router/resolver/regex.php | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/code/site/components/com_pages/dispatcher/router/resolver/regex.php b/code/site/components/com_pages/dispatcher/router/resolver/regex.php index f0d7a335d..d0e6dcc3b 100644 --- a/code/site/components/com_pages/dispatcher/router/resolver/regex.php +++ b/code/site/components/com_pages/dispatcher/router/resolver/regex.php @@ -68,18 +68,19 @@ protected function _initialize(KObjectConfig $config) $config->append(array( 'routes' => array(), 'types' => [ - 'email' => '\S+@\S+', - 'month' => '(0?[1-9]|1[012])', - 'year' => '(19|20)\d{2}', - 'digit' => '[0-9]++', + 'email' => '\S+@\S+', + 'month' => '(0?[1-9]|1[012])', + 'year' => '(19|20)\d{2}', + 'digit' => '[0-9]++', '*digit' => '[0-9]+(,[0-9]+)*', - 'alnum' => '[0-9A-Za-z]++', + 'alnum' => '[0-9A-Za-z]++', '*alnum' => '[0-9A-Za-z]+(,[0-9A-Za-z]+)*', - 'alpha' => '[A-Za-z]++', + 'alpha' => '[A-Za-z]++', '*alpha' => '[A-Za-z]+(,[A-Za-z]+)*', - '*' => '.+?', - '**' => '.++', - '' => '[^/\.]++', + 'slug' => '[0-9]++[-]++([0-9A-Za-z-]++)', + '*' => '.+?', + '**' => '.++', + '' => '[^/\.]++', ], )); @@ -341,11 +342,19 @@ protected function _buildRoute($regex, ComPagesDispatcherRouterRouteInterface $r if(isset($route->query[$param])) { if(is_array($route->query[$param])) { - $value= implode(',', $route->query[$param]); + $value = implode(',', $route->query[$param]); } else { $value = $route->query[$param]; } + if ($type && isset($this->_match_types[$type])) + { + $type = $this->_match_types[$type]; + + preg_match('/'.$type.'/', $value, $matches); + $value = $matches[1] ?? $value; + } + //Part is found, replace for param value $regex = str_replace($block, $value, $regex); From f873bcf5b6d8d41ea6b870f69c5ba640e0970cbc Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Wed, 17 Jun 2020 02:25:21 +0200 Subject: [PATCH 08/15] #372 - Add __debugInfo() support to route object for easier debugging --- .../dispatcher/router/route/abstract.php | 17 +++++++++++++++++ .../com_pages/dispatcher/router/route/page.php | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/code/site/components/com_pages/dispatcher/router/route/abstract.php b/code/site/components/com_pages/dispatcher/router/route/abstract.php index 385f16829..eb2e2bb24 100644 --- a/code/site/components/com_pages/dispatcher/router/route/abstract.php +++ b/code/site/components/com_pages/dispatcher/router/route/abstract.php @@ -125,4 +125,21 @@ public function isAbsolute() { return (bool) ($this->scheme && $this->host); } + + /** + * Generate debug info + * + * @return array + */ + public function __debugInfo() + { + $result = [ + 'route' => $this->toString(), + 'state' => $this->getState(), + 'format' => $this->getFormat(), + 'status' => $this->isResolved() ? 'resolved' : $this->isGenerated() ? 'generated' : '' + ]; + + return $result; + } } \ No newline at end of file diff --git a/code/site/components/com_pages/dispatcher/router/route/page.php b/code/site/components/com_pages/dispatcher/router/route/page.php index 4419a4f06..58b722cd3 100644 --- a/code/site/components/com_pages/dispatcher/router/route/page.php +++ b/code/site/components/com_pages/dispatcher/router/route/page.php @@ -55,4 +55,17 @@ public function setGenerated() return $this; } + + /** + * Generate debug info + * + * @return array + */ + public function __debugInfo() + { + $result = parent::__debugInfo(); + $result['page'] = $this->_page_path; + + return $result; + } } \ No newline at end of file From ccffcc62a13ec3b97fc0f77ba7837795e6f9de3e Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Wed, 17 Jun 2020 02:25:48 +0200 Subject: [PATCH 09/15] #372 - Add url router --- .../com_pages/dispatcher/router/router.php | 1 + .../com_pages/dispatcher/router/url.php | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 code/site/components/com_pages/dispatcher/router/url.php diff --git a/code/site/components/com_pages/dispatcher/router/router.php b/code/site/components/com_pages/dispatcher/router/router.php index 96c0df12a..c26a1f3a8 100644 --- a/code/site/components/com_pages/dispatcher/router/router.php +++ b/code/site/components/com_pages/dispatcher/router/router.php @@ -49,6 +49,7 @@ protected function _initialize(KObjectConfig $config) 'page' => 'com://site/pages.dispatcher.router.pages', 'site' => 'com://site/pages.dispatcher.router.site', 'redirect' => 'com://site/pages.dispatcher.router.redirect', + 'url' => 'com://site/pages.dispatcher.router.url', ], )); diff --git a/code/site/components/com_pages/dispatcher/router/url.php b/code/site/components/com_pages/dispatcher/router/url.php new file mode 100644 index 000000000..092ce8ac4 --- /dev/null +++ b/code/site/components/com_pages/dispatcher/router/url.php @@ -0,0 +1,24 @@ + + * @link https://github.com/joomlatools/joomlatools-pages for the canonical source repository + */ + +class ComPagesDispatcherRouterUrl extends ComPagesDispatcherRouterAbstract +{ + protected function _initialize(KObjectConfig $config) + { + $config->append([ + 'routes' => [] + ])->append([ + 'resolvers' => [ + 'regex' => ['routes' => $config->routes], + ] + ]); + + parent::_initialize($config); + } +} \ No newline at end of file From 4ecb89842b71a7fe09e2bff06fab39e2dcf53f82 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Wed, 17 Jun 2020 02:26:41 +0200 Subject: [PATCH 10/15] #372 - Implement Joomla url rewriter --- .../event/subscriber/urlrewriter.php | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 code/site/components/com_pages/event/subscriber/urlrewriter.php diff --git a/code/site/components/com_pages/event/subscriber/urlrewriter.php b/code/site/components/com_pages/event/subscriber/urlrewriter.php new file mode 100644 index 000000000..4b8e66e3d --- /dev/null +++ b/code/site/components/com_pages/event/subscriber/urlrewriter.php @@ -0,0 +1,112 @@ + + * @link https://github.com/joomlatools/joomlatools-pages for the canonical source repository + */ + +class ComPagesEventSubscriberUrlrewriter extends ComPagesEventSubscriberAbstract +{ + private $__routes = array(); + + protected function _initialize(KObjectConfig $config) + { + $config->append(array( + 'priority' => KEvent::PRIORITY_HIGH, + 'cache_path' => $this->getObject('com://site/pages.config')->getSitePath('cache'), + )); + + parent::_initialize($config); + } + + public function onAfterApplicationInitialise(KEventInterface $event) + { + //Load the routes + $path = $this->getConfig()->cache_path; + $file = $path.'/rewrites.php'; + + if(file_exists($file)) { + $this->__routes = require($file); + } + + //Attach build rule + JFactory::getApplication()->getRouter()->attachBuildRule(function($router, $url) + { + $path = trim(str_replace(array('index.php'), '', $url->getPath()), '/'); + $query = $url->getQuery(true); + + $request = $this->getObject('request'); + $router = $this->getObject('com://site/pages.dispatcher.router.url', ['request' => $request]); + + //Internal rewrite from old to new url + if($route = $router->resolve($path, $query)) + { + $target = trim($route->getPath(), '/'); + + if(strpos($router->getRequest()->getUrl()->getPath(), 'index.php') !== false) { + $url->setPath('index.php/' . $target); + } else { + $url->setPath($target); + } + + $url->setQuery($route->getQuery(true)); + + //Cache the route resolution + $this->__routes[$path] = $target; + } + + }, JRouter::PROCESS_AFTER); + + //Attach parse rule + JFactory::getApplication()->getRouter()->attachParseRule(function($router, $url) + { + $path = trim(str_replace(array('index.php'), '', $url->getPath()), '/'); + $query = $url->getQuery(true); + + $request = $this->getObject('request'); + $router = $this->getObject('com://site/pages.dispatcher.router.url', ['request' => $request]); + + if(isset($this->__routes[$path])) + { + //Redirect OLD to NEW + if($route = $router->resolve($path, $query)) + { + $route = $router->getRoute(trim($route->getPath(), '/')); + $url = $router->qualify($route); + + $this->getObject('com://site/pages.dispatcher.http')->redirect($url); + } + } + + if($old = array_search($path, $this->__routes)) + { + //Redirect NEW to OLD + if(!$router->resolve($old, $query)) + { + $route = $router->getRoute($old); + $url = $router->qualify($route); + + $this->getObject('com://site/pages.dispatcher.http')->redirect($url); + } + else $url->setPath($old); + } + + }, JRouter::PROCESS_BEFORE); + } + + public function onBeforeApplicationTerminate(KEventInterface $event) + { + //Store the routes + $path = $this->getConfig()->cache_path; + $file = $path.'/rewrites.php'; + + $result = '__routes, true).';'; + + if(@file_put_contents($file, $result) === false) { + throw new RuntimeException(sprintf('The routes cannot be cached in "%s"', $file)); + } + } +} \ No newline at end of file From 50a41c6835eb8083495e880cdf0564d617e7ed31 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Wed, 17 Jun 2020 02:27:30 +0200 Subject: [PATCH 11/15] #372 - Setup config for url router --- code/site/components/com_pages/config.php | 1 + code/site/components/com_pages/resources/config/site.php | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/code/site/components/com_pages/config.php b/code/site/components/com_pages/config.php index 75fe5e5b4..9ef78cdc8 100644 --- a/code/site/components/com_pages/config.php +++ b/code/site/components/com_pages/config.php @@ -53,6 +53,7 @@ protected function _initialize(KObjectConfig $config) 'collections' => array(), 'redirects' => array(), + 'rewrites' => array(), 'page' => array(), 'sites' => array('[*]' => JPATH_ROOT.'/joomlatools-pages'), 'headers' => array(), diff --git a/code/site/components/com_pages/resources/config/site.php b/code/site/components/com_pages/resources/config/site.php index 35117ef72..f4a09306a 100644 --- a/code/site/components/com_pages/resources/config/site.php +++ b/code/site/components/com_pages/resources/config/site.php @@ -50,7 +50,10 @@ ], 'com://site/pages.model.cache' => [ 'cache_path' => $config['http_cache_path'], - ] + ], + 'com://site/pages.dispatcher.router.url' => [ + 'routes' => $config['rewrites'], + ], ], 'extensions' => $config['extensions'] ?? array(), ]; \ No newline at end of file From 01b7f8d06e794cef97f692fde8bd78535a321e23 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Thu, 18 Jun 2020 03:50:58 +0200 Subject: [PATCH 12/15] #372 - Regex improvements - Add 'id' type - Do not use a match group for 'slug' type --- .../com_pages/dispatcher/router/resolver/regex.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/code/site/components/com_pages/dispatcher/router/resolver/regex.php b/code/site/components/com_pages/dispatcher/router/resolver/regex.php index d0e6dcc3b..2f3380585 100644 --- a/code/site/components/com_pages/dispatcher/router/resolver/regex.php +++ b/code/site/components/com_pages/dispatcher/router/resolver/regex.php @@ -77,7 +77,8 @@ protected function _initialize(KObjectConfig $config) '*alnum' => '[0-9A-Za-z]+(,[0-9A-Za-z]+)*', 'alpha' => '[A-Za-z]++', '*alpha' => '[A-Za-z]+(,[A-Za-z]+)*', - 'slug' => '[0-9]++[-]++([0-9A-Za-z-]++)', + 'id' => '[0-9]++[-]++[0-9A-Za-z-]++', + 'slug' => '[^0-9-][a-z-0-9]++', '*' => '.+?', '**' => '.++', '' => '[^/\.]++', @@ -351,8 +352,10 @@ protected function _buildRoute($regex, ComPagesDispatcherRouterRouteInterface $r { $type = $this->_match_types[$type]; - preg_match('/'.$type.'/', $value, $matches); - $value = $matches[1] ?? $value; + //Get first capturing group if it exists, if not use full match + if(preg_match('/'.$type.'/', $value, $matches)) { + $value = $matches[0] ?? $value; + } } //Part is found, replace for param value From c79c0e55f83b1f15d19e27b74044799bea3b5f00 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Thu, 18 Jun 2020 03:51:56 +0200 Subject: [PATCH 13/15] #372 - Do not cache route if the resolved target is the same as the path --- .../components/com_pages/event/subscriber/urlrewriter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/site/components/com_pages/event/subscriber/urlrewriter.php b/code/site/components/com_pages/event/subscriber/urlrewriter.php index 4b8e66e3d..b89a7c06b 100644 --- a/code/site/components/com_pages/event/subscriber/urlrewriter.php +++ b/code/site/components/com_pages/event/subscriber/urlrewriter.php @@ -54,7 +54,9 @@ public function onAfterApplicationInitialise(KEventInterface $event) $url->setQuery($route->getQuery(true)); //Cache the route resolution - $this->__routes[$path] = $target; + if($path != $target) { + $this->__routes[$path] = $target; + } } }, JRouter::PROCESS_AFTER); From f1fab39b3b19763aecd662c2752a9ba44f75c614 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Fri, 19 Jun 2020 05:36:35 +0200 Subject: [PATCH 14/15] #372 - Allow a slug to to start with a number --- .../components/com_pages/dispatcher/router/resolver/regex.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/site/components/com_pages/dispatcher/router/resolver/regex.php b/code/site/components/com_pages/dispatcher/router/resolver/regex.php index 2f3380585..1f054793e 100644 --- a/code/site/components/com_pages/dispatcher/router/resolver/regex.php +++ b/code/site/components/com_pages/dispatcher/router/resolver/regex.php @@ -78,7 +78,7 @@ protected function _initialize(KObjectConfig $config) 'alpha' => '[A-Za-z]++', '*alpha' => '[A-Za-z]+(,[A-Za-z]+)*', 'id' => '[0-9]++[-]++[0-9A-Za-z-]++', - 'slug' => '[^0-9-][a-z-0-9]++', + 'slug' => '(?![0-9]++-)[^-][0-9A-Za-z-]++', '*' => '.+?', '**' => '.++', '' => '[^/\.]++', From 63d55e886a837653a6cbfc7d7aa5778c1e8e9a39 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sat, 20 Jun 2020 04:42:17 +0200 Subject: [PATCH 15/15] #372 - Accept any characters none white-space characters --- .../components/com_pages/dispatcher/router/resolver/regex.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/site/components/com_pages/dispatcher/router/resolver/regex.php b/code/site/components/com_pages/dispatcher/router/resolver/regex.php index 1f054793e..ab22e7b91 100644 --- a/code/site/components/com_pages/dispatcher/router/resolver/regex.php +++ b/code/site/components/com_pages/dispatcher/router/resolver/regex.php @@ -77,8 +77,8 @@ protected function _initialize(KObjectConfig $config) '*alnum' => '[0-9A-Za-z]+(,[0-9A-Za-z]+)*', 'alpha' => '[A-Za-z]++', '*alpha' => '[A-Za-z]+(,[A-Za-z]+)*', - 'id' => '[0-9]++[-]++[0-9A-Za-z-]++', - 'slug' => '(?![0-9]++-)[^-][0-9A-Za-z-]++', + 'id' => '[0-9]++[-]++[\S]++', + 'slug' => '(?![0-9]++-)[^-][\S]++', '*' => '.+?', '**' => '.++', '' => '[^/\.]++',