<?php
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 2.2.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Routing;
use Cake\Core\InstanceConfigTrait;
use Cake\Event\Event;
use Cake\Event\EventListenerInterface;
use InvalidArgumentException;
/**
* This abstract class represents a filter to be applied to a dispatcher cycle. It acts as an
* event listener with the ability to alter the request or response as needed before it is handled
* by a controller or after the response body has already been built.
*
* Subclasses of this class use a class naming convention having a `Filter` suffix.
*
* ### Limiting filters to specific paths
*
* By using the `for` option you can limit with request paths a filter is applied to.
* Both the before and after event will have the same conditions applied to them. For
* example, if you only wanted a filter applied to blog requests you could do:
*
* ```
* $filter = new BlogFilter(['for' => '/blog']);
* ```
*
* When the above filter is connected to a dispatcher it will only fire
* its `beforeDispatch` and `afterDispatch` methods on requests that start with `/blog`.
*
* The for condition can also be a regular expression by using the `preg:` prefix:
*
* ```
* $filter = new BlogFilter(['for' => 'preg:#^/blog/\d+$#']);
* ```
*
* ### Limiting filters based on conditions
*
* In addition to simple path based matching you can use a closure to match on arbitrary request
* or response conditions. For example:
*
* ```
* $cookieMonster = new CookieFilter([
* 'when' => function ($req, $res) {
* // Custom code goes here.
* }
* ]);
* ```
*
* If your when condition returns `true` the before/after methods will be called.
*
* When using the `for` or `when` matchers, conditions will be re-checked on the before and after
* callback as the conditions could change during the dispatch cycle.
*/
class DispatcherFilter implements EventListenerInterface
{
use InstanceConfigTrait;
/**
* Default priority for all methods in this filter
*
* @var int
*/
protected $_priority = 10;
/**
* Default config
*
* These are merged with user-provided config when the class is used.
* The when and for options allow you to define conditions that are checked before
* your filter is called.
*
* @var array
*/
protected $_defaultConfig = [
'when' => null,
'for' => null,
'priority' => null,
];
/**
* Constructor.
*
* @param array $config Settings for the filter.
* @throws \InvalidArgumentException When 'when' conditions are not callable.
*/
public function __construct($config = [])
{
if (!isset($config['priority'])) {
$config['priority'] = $this->_priority;
}
$this->setConfig($config);
if (isset($config['when']) && !is_callable($config['when'])) {
throw new InvalidArgumentException('"when" conditions must be a callable.');
}
}
/**
* Returns the list of events this filter listens to.
* Dispatcher notifies 2 different events `Dispatcher.before` and `Dispatcher.after`.
* By default this class will attach `preDispatch` and `postDispatch` method respectively.
*
* Override this method at will to only listen to the events you are interested in.
*
* @return array
*/
public function implementedEvents()
{
return [
'Dispatcher.beforeDispatch' => [
'callable' => 'handle',
'priority' => $this->_config['priority'],
],
'Dispatcher.afterDispatch' => [
'callable' => 'handle',
'priority' => $this->_config['priority'],
],
];
}
/**
* Handler method that applies conditions and resolves the correct method to call.
*
* @param \Cake\Event\Event $event The event instance.
* @return mixed
*/
public function handle(Event $event)
{
$name = $event->getName();
list(, $method) = explode('.', $name);
if (empty($this->_config['for']) && empty($this->_config['when'])) {
return $this->{$method}($event);
}
if ($this->matches($event)) {
return $this->{$method}($event);
}
}
/**
* Check to see if the incoming request matches this filter's criteria.
*
* @param \Cake\Event\Event $event The event to match.
* @return bool
*/
public function matches(Event $event)
{
/** @var \Cake\Http\ServerRequest $request */
$request = $event->getData('request');
$pass = true;
if (!empty($this->_config['for'])) {
$len = strlen('preg:');
$for = $this->_config['for'];
$url = $request->getRequestTarget();
if (substr($for, 0, $len) === 'preg:') {
$pass = (bool)preg_match(substr($for, $len), $url);
} else {
$pass = strpos($url, $for) === 0;
}
}
if ($pass && !empty($this->_config['when'])) {
$response = $event->getData('response');
$pass = $this->_config['when']($request, $response);
}
return $pass;
}
/**
* Method called before the controller is instantiated and called to serve a request.
* If used with default priority, it will be called after the Router has parsed the
* URL and set the routing params into the request object.
*
* If a Cake\Http\Response object instance is returned, it will be served at the end of the
* event cycle, not calling any controller as a result. This will also have the effect of
* not calling the after event in the dispatcher.
*
* If false is returned, the event will be stopped and no more listeners will be notified.
* Alternatively you can call `$event->stopPropagation()` to achieve the same result.
*
* @param \Cake\Event\Event $event container object having the `request`, `response` and `additionalParams`
* keys in the data property.
* @return void
*/
public function beforeDispatch(Event $event)
{
}
/**
* Method called after the controller served a request and generated a response.
* It is possible to alter the response object at this point as it is not sent to the
* client yet.
*
* If false is returned, the event will be stopped and no more listeners will be notified.
* Alternatively you can call `$event->stopPropagation()` to achieve the same result.
*
* @param \Cake\Event\Event $event container object having the `request` and `response`
* keys in the data property.
* @return void
*/
public function afterDispatch(Event $event)
{
}
}