diff --git a/code/site/components/com_pages/config.php b/code/site/components/com_pages/config.php index 4bbf8ac75..fb2a50be0 100644 --- a/code/site/components/com_pages/config.php +++ b/code/site/components/com_pages/config.php @@ -54,6 +54,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/controller/behavior/breadcrumbable.php b/code/site/components/com_pages/controller/behavior/breadcrumbable.php index 17a772ba1..4b24e9063 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/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/dispatcher/http.php b/code/site/components/com_pages/dispatcher/http.php index 5f6dd1f67..638b21b9c 100644 --- a/code/site/components/com_pages/dispatcher/http.php +++ b/code/site/components/com_pages/dispatcher/http.php @@ -91,7 +91,7 @@ public function getRoute() $path = trim(str_replace(array($base, '/index.php'), '', $url), '/'); $query = $this->getRequest()->getUrl()->getQuery(true); - $this->__route = $this->getRouter()->resolve('pages:'.$path, $query); + $this->__route = $this->getRouter()->resolve('page:'.$path, $query); } 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/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 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..ab22e7b91 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,20 @@ 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]+)*', - '*' => '.+?', - '**' => '.++', - '' => '[^/\.]++', + 'id' => '[0-9]++[-]++[\S]++', + 'slug' => '(?![0-9]++-)[^-][\S]++', + '*' => '.+?', + '**' => '.++', + '' => '[^/\.]++', ], )); @@ -90,18 +92,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 +120,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 +170,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(isset($result['resolve']) && is_callable($result['resolve'])) { + $result = (bool) call_user_func($result['resolve'], $route); + } else { + $result = $this->_buildRoute($result, $route); + } } return $result !== false ? parent::resolve($route) : false; @@ -194,12 +196,21 @@ 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(isset($target['generate']) && is_callable($target['generate'])) { - //Generate the dynamic route - if($this->_buildRoute($regex, $route)) { + //Parse the route to match it + if($this->_parseRoute($regex, $route) && (bool) call_user_func($target['generate'], $route) == true) { + $generated = true; break; + } + } + else + { + //Parse the route to match it + if($this->_parseRoute($regex, $route) && $this->_buildRoute($target, $route)) { $generated = true; break; } } @@ -208,12 +219,23 @@ 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(isset($target['generate']) && is_callable($target['generate'])) + { + //Compare the path to match it + 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; + } } } } @@ -321,11 +343,21 @@ 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]; + + //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 $regex = str_replace($block, $value, $regex); @@ -338,7 +370,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/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 a9746b337..6f8bac177 100644 --- a/code/site/components/com_pages/dispatcher/router/route/page.php +++ b/code/site/components/com_pages/dispatcher/router/route/page.php @@ -59,4 +59,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 diff --git a/code/site/components/com_pages/dispatcher/router/router.php b/code/site/components/com_pages/dispatcher/router/router.php index 1eabf46a9..c26a1f3a8 100644 --- a/code/site/components/com_pages/dispatcher/router/router.php +++ b/code/site/components/com_pages/dispatcher/router/router.php @@ -30,6 +30,30 @@ 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', + 'url' => 'com://site/pages.dispatcher.router.url', + ], + )); + + parent::_initialize($config); } /** @@ -43,24 +67,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 +108,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 +124,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 +165,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); } 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 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..20a2c5298 --- /dev/null +++ b/code/site/components/com_pages/event/subscriber/redirector.php @@ -0,0 +1,56 @@ + + * @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()) + { + $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 { + //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); + + $dispatcher->redirect($url); + } + } +} \ No newline at end of file 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..b89a7c06b --- /dev/null +++ b/code/site/components/com_pages/event/subscriber/urlrewriter.php @@ -0,0 +1,114 @@ + + * @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 + if($path != $target) { + $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 diff --git a/code/site/components/com_pages/page/registry.php b/code/site/components/com_pages/page/registry.php index fc2150534..ae0e5b5d6 100755 --- a/code/site/components/com_pages/page/registry.php +++ b/code/site/components/com_pages/page/registry.php @@ -256,10 +256,29 @@ public function getPageEntity($path) 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; @@ -480,7 +499,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/bootstrapper.php b/code/site/components/com_pages/resources/config/bootstrapper.php index 2e8f08651..3d049175d 100644 --- a/code/site/components/com_pages/resources/config/bootstrapper.php +++ b/code/site/components/com_pages/resources/config/bootstrapper.php @@ -54,13 +54,14 @@ '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', ] ], '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 diff --git a/code/site/components/com_pages/resources/config/site.php b/code/site/components/com_pages/resources/config/site.php index 25bcb4a3f..e1af5b9c3 100644 --- a/code/site/components/com_pages/resources/config/site.php +++ b/code/site/components/com_pages/resources/config/site.php @@ -62,7 +62,10 @@ //See: https://github.com/scrivo/highlight.php return (new \Highlight\Highlighter())->highlight($language, $source, false)->value; } - ] + ], + 'com://site/pages.dispatcher.router.url' => [ + 'routes' => $config['rewrites'], + ], ], 'extensions' => $config['extensions'] ?? array(), ]; \ No newline at end of file