From 3a07312cf9b2774baf5bc0d60b6736c3c78dda7a Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Fri, 15 May 2020 03:28:48 +0200 Subject: [PATCH 01/14] #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/14] #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/14] #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/14] #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/14] #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 9cfc9eb09fdb601236d4f53c1448f35035921dbf Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sat, 16 May 2020 04:03:58 +0200 Subject: [PATCH 06/14] #345 - Implement static file router --- .../com_pages/dispatcher/router/file.php | 60 +++++++++++++++++++ .../com_pages/resources/config/site.php | 6 +- 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 code/site/components/com_pages/dispatcher/router/file.php diff --git a/code/site/components/com_pages/dispatcher/router/file.php b/code/site/components/com_pages/dispatcher/router/file.php new file mode 100644 index 000000000..c71e767d3 --- /dev/null +++ b/code/site/components/com_pages/dispatcher/router/file.php @@ -0,0 +1,60 @@ + + * @link https://github.com/joomlatools/joomlatools-pages for the canonical source repository + */ + +class ComPagesDispatcherRouterFile extends ComPagesDispatcherRouterAbstract +{ + protected function _initialize(KObjectConfig $config) + { + $config->append([ + 'routes' => [] + ])->append([ + 'resolvers' => [ + 'regex' => ['routes' => $config->routes] + ] + ]); + + parent::_initialize($config); + } + + public function resolve($route = null, array $parameters = array()) + { + $result = false; + if(count($this->getConfig()->routes)) + { + 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), '/'); + } + + $result = parent::resolve($route, $parameters); + } + + return $result; + } + + public function qualify(ComPagesDispatcherRouterRouteInterface $route, $replace = false) + { + $url = clone $route; + + $path = $url->getPath(); + if(!is_file($path)) + { + //Qualify the path + $path = trim($path, '/'); + $base = $this->getRequest()->getBasePath(true); + $url->setPath($base.'/'.$path); + } + + return $url; + } +} \ 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 35117ef72..2e8b4f510 100644 --- a/code/site/components/com_pages/resources/config/site.php +++ b/code/site/components/com_pages/resources/config/site.php @@ -48,9 +48,9 @@ 'cache_force' => $config['http_resource_cache_force'], 'debug' => $config['http_resource_cache_debug'], ], - 'com://site/pages.model.cache' => [ - 'cache_path' => $config['http_cache_path'], - ] + 'com://site/pages.dispatcher.router.file' => [ + 'routes' => isset($config['files']) ? $config['files'] : array(), + ], ], 'extensions' => $config['extensions'] ?? array(), ]; \ No newline at end of file From 2aecc3216a0392c084f94f268e9b533b273371f6 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sat, 16 May 2020 04:05:30 +0200 Subject: [PATCH 07/14] #345 - Implement static file downloader --- .../com_pages/event/subscriber/downloader.php | 46 +++++++++++++++++++ .../resources/config/bootstrapper.php | 1 + 2 files changed, 47 insertions(+) create mode 100644 code/site/components/com_pages/event/subscriber/downloader.php diff --git a/code/site/components/com_pages/event/subscriber/downloader.php b/code/site/components/com_pages/event/subscriber/downloader.php new file mode 100644 index 000000000..27e175721 --- /dev/null +++ b/code/site/components/com_pages/event/subscriber/downloader.php @@ -0,0 +1,46 @@ + + * @link https://github.com/joomlatools/joomlatools-pages for the canonical source repository + */ + +class ComPagesEventSubscriberDownloader 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.file', ['request' => $request]); + + if(false !== $route = $router->resolve()) + { + //Qualify the route + $path = (string) $router->qualify($route, true); + + //Set the location header + $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); + + try + { + $dispatcher->getResponse() + ->setContent((string) $path, @mime_content_type($path) ?? 'application/octet-stream'); + } + catch (InvalidArgumentException $e) { + throw new KControllerExceptionResourceNotFound('File not found'); + } + + $dispatcher->send(); + } + } +} diff --git a/code/site/components/com_pages/resources/config/bootstrapper.php b/code/site/components/com_pages/resources/config/bootstrapper.php index 638709125..4db6d82a0 100644 --- a/code/site/components/com_pages/resources/config/bootstrapper.php +++ b/code/site/components/com_pages/resources/config/bootstrapper.php @@ -64,6 +64,7 @@ 'subscribers' => [ 'com://site/pages.event.subscriber.bootstrapper', 'com://site/pages.event.subscriber.redirector', + 'com://site/pages.event.subscriber.downloader', 'com://site/pages.event.subscriber.dispatcher', 'com://site/pages.event.subscriber.pagedecorator', 'com://site/pages.event.subscriber.errorhandler', From 690e7e32586cde377a96f3bba7503780092912da Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sat, 16 May 2020 23:40:49 +0200 Subject: [PATCH 08/14] #345 - Always use setUrl() to support a regex containing a query --- .../com_pages/dispatcher/router/resolver/regex.php | 6 +++--- 1 file changed, 3 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 f0d7a335d..2e0575865 100644 --- a/code/site/components/com_pages/dispatcher/router/resolver/regex.php +++ b/code/site/components/com_pages/dispatcher/router/resolver/regex.php @@ -373,10 +373,10 @@ protected function _buildRoute($regex, ComPagesDispatcherRouterRouteInterface $r } if(strpos($regex, '://') === false) { - $route->setPath('/'.ltrim($regex, '/')); - } else { - $route->setUrl($regex); + $regex = '/' . ltrim($regex, '/'); } + + $route->setUrl($regex); } return $result; From 8b0d487b2f3ff594a88860e9b7be788decdf8c96 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sat, 16 May 2020 23:42:48 +0200 Subject: [PATCH 09/14] #345 - Implement specialised file route A file route is absolute if the path of the route is a fully qualified file path --- .../com_pages/dispatcher/router/file.php | 24 +++++++++---------- .../dispatcher/router/route/file.php | 22 +++++++++++++++++ 2 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 code/site/components/com_pages/dispatcher/router/route/file.php diff --git a/code/site/components/com_pages/dispatcher/router/file.php b/code/site/components/com_pages/dispatcher/router/file.php index c71e767d3..e254905f4 100644 --- a/code/site/components/com_pages/dispatcher/router/file.php +++ b/code/site/components/com_pages/dispatcher/router/file.php @@ -12,6 +12,7 @@ class ComPagesDispatcherRouterFile extends ComPagesDispatcherRouterAbstract protected function _initialize(KObjectConfig $config) { $config->append([ + 'route' => 'file', 'routes' => [] ])->append([ 'resolvers' => [ @@ -25,15 +26,15 @@ protected function _initialize(KObjectConfig $config) public function resolve($route = null, array $parameters = array()) { $result = false; - if(count($this->getConfig()->routes)) + if (count($this->getConfig()->routes)) { - if(!$route) + if (!$route) { - $base = $this->getRequest()->getBasePath(); - $url = urldecode( $this->getRequest()->getUrl()->getPath()); + $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); @@ -42,17 +43,16 @@ public function resolve($route = null, array $parameters = array()) return $result; } - public function qualify(ComPagesDispatcherRouterRouteInterface $route, $replace = false) + public function qualify(ComPagesDispatcherRouterRouteInterface $route, $replace = false) { $url = clone $route; - $path = $url->getPath(); - if(!is_file($path)) + if(!$url->isAbsolute()) { - //Qualify the path - $path = trim($path, '/'); - $base = $this->getRequest()->getBasePath(true); - $url->setPath($base.'/'.$path); + $base = $this->getRequest()->getBasePath(true); + $path = trim($url->getPath(), '/'); + + $url->setPath($base . '/' . $path); } return $url; diff --git a/code/site/components/com_pages/dispatcher/router/route/file.php b/code/site/components/com_pages/dispatcher/router/route/file.php new file mode 100644 index 000000000..a7ecb8cf5 --- /dev/null +++ b/code/site/components/com_pages/dispatcher/router/route/file.php @@ -0,0 +1,22 @@ + + * @link https://github.com/joomlatools/joomlatools-pages for the canonical source repository + */ + +/** + * Router File Route + * + * @author Johan Janssens + * @package Koowa\Library\Dispatcher\Router\Route + */ +class ComPagesDispatcherRouterRouteFile extends ComPagesDispatcherRouterRouteAbstract +{ + public function isAbsolute() + { + return file_exists($this->getPath()); + } +} \ No newline at end of file From 10514abecb1bdc3126468f277cbb0139fabe21a5 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sun, 17 May 2020 00:00:04 +0200 Subject: [PATCH 10/14] #345 - Add support for force downloading files Add support for the 'force-download' query parameter. If specified the file will be downloaded instead of allowing the browser to preview the file if it can do so. For example: 'files/[:name]' => '/files/documents/[:name].pdf?force-download' --- .../com_pages/event/subscriber/downloader.php | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/code/site/components/com_pages/event/subscriber/downloader.php b/code/site/components/com_pages/event/subscriber/downloader.php index 27e175721..8c57e5ffd 100644 --- a/code/site/components/com_pages/event/subscriber/downloader.php +++ b/code/site/components/com_pages/event/subscriber/downloader.php @@ -9,38 +9,42 @@ class ComPagesEventSubscriberDownloader 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.file', ['request' => $request]); - - if(false !== $route = $router->resolve()) - { - //Qualify the route - $path = (string) $router->qualify($route, true); - - //Set the location header - $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); - - try - { - $dispatcher->getResponse() - ->setContent((string) $path, @mime_content_type($path) ?? 'application/octet-stream'); - } - catch (InvalidArgumentException $e) { - throw new KControllerExceptionResourceNotFound('File not found'); - } - - $dispatcher->send(); - } - } -} + 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.file', ['request' => $request]); + + if(false !== $route = $router->resolve()) + { + //Qualify the route + $route = $router->qualify($route); + + //Get the file path + $path = $route->getPath(); + + if(isset($route->query['force-download'])) { + $request->query->set('force-download', true); + } + + //Set the location header + $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); + + try { + $dispatcher->getResponse()->setContent($path, @mime_content_type($path) ?? 'application/octet-stream'); + } catch (InvalidArgumentException $e) { + throw new KControllerExceptionResourceNotFound('File not found'); + } + + $dispatcher->send(); + } + } +} \ No newline at end of file From e64cdfd64ceaaaadef93165803b775e2c0fc1a31 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sun, 17 May 2020 02:22:28 +0200 Subject: [PATCH 11/14] #345 - Add a transport query parameter The `transport`query parameter allows to define which transport. At the moment two additional transports are supported: `stream` and `sendfile --- .../com_pages/event/subscriber/downloader.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/code/site/components/com_pages/event/subscriber/downloader.php b/code/site/components/com_pages/event/subscriber/downloader.php index 8c57e5ffd..0494525c8 100644 --- a/code/site/components/com_pages/event/subscriber/downloader.php +++ b/code/site/components/com_pages/event/subscriber/downloader.php @@ -38,8 +38,17 @@ public function onAfterApplicationRoute(KEventInterface $event) //Set the location header $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); - try { - $dispatcher->getResponse()->setContent($path, @mime_content_type($path) ?? 'application/octet-stream'); + try + { + $response = $dispatcher->getResponse(); + + //Attach a different transport [stream or sendfile] + if(isset($route->query['transport'])) { + $response->attachTransport($route->query['transport']); + } + + $response->setContent($path, @mime_content_type($path) ?? 'application/octet-stream'); + } catch (InvalidArgumentException $e) { throw new KControllerExceptionResourceNotFound('File not found'); } From 7e6d33e68f8e7ced59103c877cce7d399e870f6c Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Sun, 17 May 2020 02:37:43 +0200 Subject: [PATCH 12/14] #345 - Enable sendfile transport by setting X-Sendfile header --- .../com_pages/event/subscriber/downloader.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/code/site/components/com_pages/event/subscriber/downloader.php b/code/site/components/com_pages/event/subscriber/downloader.php index 0494525c8..6dce7c016 100644 --- a/code/site/components/com_pages/event/subscriber/downloader.php +++ b/code/site/components/com_pages/event/subscriber/downloader.php @@ -43,8 +43,16 @@ public function onAfterApplicationRoute(KEventInterface $event) $response = $dispatcher->getResponse(); //Attach a different transport [stream or sendfile] - if(isset($route->query['transport'])) { - $response->attachTransport($route->query['transport']); + if(isset($route->query['transport'])) + { + $transport = $route->query['transport']; + + //Enable using the header + if($transport == 'sendfile') { + $response->getHeaders()->set('X-Sendfile', 1); + } + + $response->attachTransport($transport); } $response->setContent($path, @mime_content_type($path) ?? 'application/octet-stream'); From 9fbf3541ee66930786b8ec239b1083c8135b1299 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Mon, 18 May 2020 21:06:43 +0200 Subject: [PATCH 13/14] #345 - Rename downloader to filedownloader and add support for cache query parameter The cache query parameter allows to define the max-age in seconds. --- .../event/subscriber/filedownloader.php | 76 +++++++++++++++++++ .../resources/config/bootstrapper.php | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 code/site/components/com_pages/event/subscriber/filedownloader.php diff --git a/code/site/components/com_pages/event/subscriber/filedownloader.php b/code/site/components/com_pages/event/subscriber/filedownloader.php new file mode 100644 index 000000000..abb15f425 --- /dev/null +++ b/code/site/components/com_pages/event/subscriber/filedownloader.php @@ -0,0 +1,76 @@ + + * @link https://github.com/joomlatools/joomlatools-pages for the canonical source repository + */ + +class ComPagesEventSubscriberFiledownloader 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.file', ['request' => $request]); + + if(false !== $route = $router->resolve()) + { + //Set the location header + $dispatcher = $this->getObject('com://site/pages.dispatcher.http'); + $response = $dispatcher->getResponse(); + + //Force download the file + if(isset($route->query['force-download'])) + { + $request->query->set('force-download', true); + unset($route->query['force-download']); + } + + //Attach a different transport [stream or sendfile] + if(isset($route->query['transport'])) + { + $transport = $route->query['transport']; + + //Enable using the header + if($transport == 'sendfile') { + $response->getHeaders()->set('X-Sendfile', 1); + } + + $response->attachTransport($transport); + + unset($route->query['transport']); + } + + //Set the cache time + if(isset($route->query['cache']) && ctype_alnum($route->query['cache'])) + { + $response->setMaxAge($route->query['cache']); + unset($route->query['cache']); + } + + //Qualify the route + $route = $router->qualify($route); + + //Get the file path + $path = $route->getPath(); + + try { + $response->setContent($path, @mime_content_type($path) ?? 'application/octet-stream'); + } catch (InvalidArgumentException $e) { + throw new KControllerExceptionResourceNotFound('File not found'); + } + + $dispatcher->send(); + } + } +} diff --git a/code/site/components/com_pages/resources/config/bootstrapper.php b/code/site/components/com_pages/resources/config/bootstrapper.php index 4db6d82a0..d705030a5 100644 --- a/code/site/components/com_pages/resources/config/bootstrapper.php +++ b/code/site/components/com_pages/resources/config/bootstrapper.php @@ -64,7 +64,7 @@ 'subscribers' => [ 'com://site/pages.event.subscriber.bootstrapper', 'com://site/pages.event.subscriber.redirector', - 'com://site/pages.event.subscriber.downloader', + 'com://site/pages.event.subscriber.filedownloader', 'com://site/pages.event.subscriber.dispatcher', 'com://site/pages.event.subscriber.pagedecorator', 'com://site/pages.event.subscriber.errorhandler', From 471b82af5fb5c57aaf5eba2ab73c64fb9e1aa0f7 Mon Sep 17 00:00:00 2001 From: Johan Janssens Date: Tue, 19 May 2020 00:22:19 +0200 Subject: [PATCH 14/14] #345 - Support relative datetime formats to set max age --- .../components/com_pages/event/subscriber/filedownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/site/components/com_pages/event/subscriber/filedownloader.php b/code/site/components/com_pages/event/subscriber/filedownloader.php index abb15f425..51d68f07d 100644 --- a/code/site/components/com_pages/event/subscriber/filedownloader.php +++ b/code/site/components/com_pages/event/subscriber/filedownloader.php @@ -52,7 +52,7 @@ public function onAfterApplicationRoute(KEventInterface $event) } //Set the cache time - if(isset($route->query['cache']) && ctype_alnum($route->query['cache'])) + if(isset($route->query['cache'])) { $response->setMaxAge($route->query['cache']); unset($route->query['cache']);