Skip to content

Commit

Permalink
feature(routing): allow more reliable URL path rewriting
Browse files Browse the repository at this point in the history
Adds an early called `route:rewrite` hook expressly for URL rewriting.
Changes there update the request object and affect the default context,
and functions like current_page_url().

Removes legacy magic quotes-related code.

Fixes #9388
  • Loading branch information
mrclay committed Feb 16, 2016
1 parent f67a67b commit 853fc0e
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 184 deletions.
8 changes: 5 additions & 3 deletions docs/guides/hooks-list.rst
Expand Up @@ -18,7 +18,6 @@ System hooks
* headers * headers
* params * params



**page_owner, system** **page_owner, system**
Filter the page_owner for the current page. No options are passed. Filter the page_owner for the current page. No options are passed.


Expand Down Expand Up @@ -296,8 +295,11 @@ Routing
======= =======


**route, <identifier>** **route, <identifier>**
Allows altering the parameters used to route requests. ``identifier`` is the first URL segment, Allows applying logic or returning a response before the page handler is called. See :doc:`routing`
registered with ``elgg_register_page_handler()``. for details.

**route:rewrite, <identifier>**
Allows altering the site-relative URL path. See :doc:`routing` for details.


**ajax_response, path:<path>** **ajax_response, path:<path>**
Filters ajax responses before they're sent back to the ``elgg/Ajax`` module. This hook type will Filters ajax responses before they're sent back to the ``elgg/Ajax`` module. This hook type will
Expand Down
53 changes: 32 additions & 21 deletions docs/guides/routing.rst
Expand Up @@ -56,25 +56,12 @@ the request to a resource view.
The ``route`` Plugin Hook The ``route`` Plugin Hook
========================= =========================


The ``route`` plugin hook is triggered earlier, before page handlers are called. The URL The ``route`` plugin hook is triggered before page handlers are called. The URL
identifier is given as the type of the hook. This hook can be used to modify the identifier identifier is given as the type of the hook. This hook can be used to add some logic before the
or segments, to take over page rendering completely, or just to add some logic before the request is handled elsewhere, or take over page rendering completely.
request is handled elsewhere.


Generally devs should use a page handler unless they need to affect a single page or a wider variety of URLs. Generally devs should instead use a page handler unless they need to affect a single page or a wider

variety of URLs.
The following code intercepts requests to the page handler for ``customblog`` and internally redirects them
to the ``blog`` page handler.

.. code:: php
function myplugin_customblog_route_handler($hook, $type, $returnvalue, $params) {
// direct Elgg to use the page handler for 'blog'
$returnvalue['identifier'] = 'blog';
return $returnvalue;
}
elgg_register_plugin_hook_handler('route', 'customblog', 'myplugin_customblog_route_handler');


The following code results in ``/blog/all`` requests being completely handled by the plugin hook handler. The following code results in ``/blog/all`` requests being completely handled by the plugin hook handler.
For these requests the ``blog`` page handler is never called. For these requests the ``blog`` page handler is never called.
Expand All @@ -99,16 +86,40 @@ For these requests the ``blog`` page handler is never called.
elgg_register_plugin_hook_handler('route', 'blog', 'myplugin_blog_all_handler'); elgg_register_plugin_hook_handler('route', 'blog', 'myplugin_blog_all_handler');
.. note:: As of 2.1, route modification should be done in the ``route:rewrite`` hook.

The ``route:rewrite`` Plugin Hook
=================================

For URL rewriting, the ``route:rewrite`` hook (with similar arguments as ``route``) is triggered very early,
and allows modifying the request URL path (relative to the Elgg site).

Here we rewrite requests for ``news/*`` to ``blog/*``:

.. code:: php
function myplugin_rewrite_handler($hook, $type, $value, $params) {
$value['identifier'] = 'blog';
return $value;
}
elgg_register_plugin_hook_handler('route:rewrite', 'news', 'myplugin_rewrite_handler');
.. warning::

The hook must be registered directly in your plugin ``start.php`` (the ``[init, system]`` event
is too late).


Routing overview Routing overview
================ ================


For regular pages, Elgg's program flow is something like this: For regular pages, Elgg's program flow is something like this:


#. A user requests ``http://example.com/blog/owner/jane``. #. A user requests ``http://example.com/news/owner/jane``.
#. Plugins are initialized. #. Plugins are initialized.
#. Elgg parses the URL to identifier ``blog`` and segments ``['owner', 'jane']``. #. Elgg parses the URL to identifier ``news`` and segments ``['owner', 'jane']``.
#. Elgg triggers the plugin hook ``route, blog`` (see above). #. Elgg triggers the plugin hook ``route:rewrite, news`` (see above).
#. Elgg triggers the plugin hook ``route, blog`` (was rewritten in the rewrite hook).
#. Elgg finds a registered page handler (see above) for ``blog``, and calls the function, passing in #. Elgg finds a registered page handler (see above) for ``blog``, and calls the function, passing in
the segments. the segments.
#. The page handler function determines it needs to render a single user's blog. It calls #. The page handler function determines it needs to render a single user's blog. It calls
Expand Down
18 changes: 18 additions & 0 deletions engine/classes/Elgg/Application.php
Expand Up @@ -297,6 +297,8 @@ public function bootCore() {
elgg_set_viewtype('default'); elgg_set_viewtype('default');
} }


$this->allowPathRewrite();

// @todo deprecate as plugins can use 'init', 'system' event // @todo deprecate as plugins can use 'init', 'system' event
$events->trigger('plugins_boot', 'system'); $events->trigger('plugins_boot', 'system');


Expand Down Expand Up @@ -577,4 +579,20 @@ private function setupPath() {


return $_GET[self::GET_PATH_KEY]; return $_GET[self::GET_PATH_KEY];
} }

/**
* Allow plugins to rewrite the path.
*
* @return void
*/
private function allowPathRewrite() {
$request = $this->services->request;
$new = $this->services->router->allowRewrite($request);
if ($new === $request) {
return;
}

$this->services->setValue('request', $new);
_elgg_set_initial_context($new);
}
} }
93 changes: 17 additions & 76 deletions engine/classes/Elgg/Http/Request.php
Expand Up @@ -2,77 +2,19 @@
namespace Elgg\Http; namespace Elgg\Http;


use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\FileBag;
use Symfony\Component\HttpFoundation\ServerBag;
use Symfony\Component\HttpFoundation\HeaderBag;
use Elgg\Application; use Elgg\Application;


/** /**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY. * Elgg HTTP request.
* *
* Represents an HTTP request.
*
* Some methods were pulled from Symfony. They are
* Copyright (c) 2004-2013 Fabien Potencier
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @package Elgg.Core
* @subpackage Http
* @since 1.9.0
* @access private * @access private
*/ */
class Request extends SymfonyRequest { class Request extends SymfonyRequest {


/** /**
* {@inheritDoc} * Get the Elgg URL segments
*/
public function initialize(array $query = array(), array $request = array(), array $attributes = array(),
array $cookies = array(), array $files = array(), array $server = array(), $content = null) {
$this->request = new ParameterBag($this->stripSlashesIfMagicQuotes($request));
$this->query = new ParameterBag($this->stripSlashesIfMagicQuotes($query));
$this->attributes = new ParameterBag($attributes);
$this->cookies = new ParameterBag($this->stripSlashesIfMagicQuotes($cookies));
$this->files = new FileBag($files);
$this->server = new ServerBag($server);
$this->headers = new HeaderBag($this->server->getHeaders());

$this->content = $content;
$this->languages = null;
$this->charsets = null;
$this->encodings = null;
$this->acceptableContentTypes = null;
$this->pathInfo = null;
$this->requestUri = null;
$this->baseUrl = null;
$this->basePath = null;
$this->method = null;
$this->format = null;
}

/**
* Get URL segments from the path info
* *
* @see \Elgg\Http\Request::getPathInfo() * @return string[]
*
* @return array
*/ */
public function getUrlSegments() { public function getUrlSegments() {
$path = trim($this->query->get(Application::GET_PATH_KEY), '/'); $path = trim($this->query->get(Application::GET_PATH_KEY), '/');
Expand All @@ -84,7 +26,20 @@ public function getUrlSegments() {
} }


/** /**
* Get first URL segment from the path info * Get a cloned request with new Elgg URL segments
*
* @param string[] $segments URL segments
*
* @return Request
*/
public function setUrlSegments(array $segments) {
$query = $this->query->all();
$query[Application::GET_PATH_KEY] = '/' . implode('/', $segments);
return $this->duplicate($query);
}

/**
* Get first Elgg URL segment
* *
* @see \Elgg\Http\Request::getUrlSegments() * @see \Elgg\Http\Request::getUrlSegments()
* *
Expand Down Expand Up @@ -115,18 +70,4 @@ public function getClientIp() {


return $ip; return $ip;
} }

/**
* Strip slashes if magic quotes is on
*
* @param mixed $data Data to strip slashes from
* @return mixed
*/
protected function stripSlashesIfMagicQuotes($data) {
if (get_magic_quotes_gpc()) {
return _elgg_stripslashes_deep($data);
} else {
return $data;
}
}
} }
49 changes: 47 additions & 2 deletions engine/classes/Elgg/Router.php
@@ -1,6 +1,8 @@
<?php <?php
namespace Elgg; namespace Elgg;


use Elgg\Http\Request;

/** /**
* Delegates requests to controllers based on the registered configuration. * Delegates requests to controllers based on the registered configuration.
* *
Expand Down Expand Up @@ -58,7 +60,7 @@ public function route(\Elgg\Http\Request $request) {


// return false to stop processing the request (because you handled it) // return false to stop processing the request (because you handled it)
// return a new $result array if you want to route the request differently // return a new $result array if you want to route the request differently
$result = array( $old = array(
'identifier' => $identifier, 'identifier' => $identifier,
'handler' => $identifier, // backward compatibility 'handler' => $identifier, // backward compatibility
'segments' => $segments, 'segments' => $segments,
Expand All @@ -68,11 +70,15 @@ public function route(\Elgg\Http\Request $request) {
$this->timer->begin(['build page']); $this->timer->begin(['build page']);
} }


$result = $this->hooks->trigger('route', $identifier, $result, $result); $result = $this->hooks->trigger('route', $identifier, $old, $old);
if ($result === false) { if ($result === false) {
return true; return true;
} }


if ($result !== $old) {
_elgg_services()->logger->warn('Use the route:rewrite hook to modify routes.');
}

if ($identifier != $result['identifier']) { if ($identifier != $result['identifier']) {
$identifier = $result['identifier']; $identifier = $result['identifier'];
} else if ($identifier != $result['handler']) { } else if ($identifier != $result['handler']) {
Expand Down Expand Up @@ -139,5 +145,44 @@ public function unregisterPageHandler($identifier) {
public function getPageHandlers() { public function getPageHandlers() {
return $this->handlers; return $this->handlers;
} }

/**
* Filter a request through the route:rewrite hook
*
* @param Request $request Elgg request
*
* @return Request
* @access private
*/
public function allowRewrite(Request $request) {
$segments = $request->getUrlSegments();
if ($segments) {
$identifier = array_shift($segments);
} else {
$identifier = '';
}

$old = array(
'identifier' => $identifier,
'segments' => $segments,
);
$new = _elgg_services()->hooks->trigger('route:rewrite', $identifier, $old, $old);
if ($new === $old) {
return $request;
}

if (!isset($new['identifier'])
|| !isset($new['segments'])
|| !is_string($new['identifier'])
|| !is_array($new['segments'])
) {
throw new \RuntimeException('rewrite_path handler returned invalid route data.');
}

// rewrite request
$segments = $new['segments'];
array_unshift($segments, $new['identifier']);
return $request->setUrlSegments($segments);
}
} }


0 comments on commit 853fc0e

Please sign in to comment.