1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 2.2.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Routing;
16:
17: use Cake\Core\InstanceConfigTrait;
18: use Cake\Event\Event;
19: use Cake\Event\EventListenerInterface;
20: use InvalidArgumentException;
21:
22: /**
23: * This abstract class represents a filter to be applied to a dispatcher cycle. It acts as an
24: * event listener with the ability to alter the request or response as needed before it is handled
25: * by a controller or after the response body has already been built.
26: *
27: * Subclasses of this class use a class naming convention having a `Filter` suffix.
28: *
29: * ### Limiting filters to specific paths
30: *
31: * By using the `for` option you can limit with request paths a filter is applied to.
32: * Both the before and after event will have the same conditions applied to them. For
33: * example, if you only wanted a filter applied to blog requests you could do:
34: *
35: * ```
36: * $filter = new BlogFilter(['for' => '/blog']);
37: * ```
38: *
39: * When the above filter is connected to a dispatcher it will only fire
40: * its `beforeDispatch` and `afterDispatch` methods on requests that start with `/blog`.
41: *
42: * The for condition can also be a regular expression by using the `preg:` prefix:
43: *
44: * ```
45: * $filter = new BlogFilter(['for' => 'preg:#^/blog/\d+$#']);
46: * ```
47: *
48: * ### Limiting filters based on conditions
49: *
50: * In addition to simple path based matching you can use a closure to match on arbitrary request
51: * or response conditions. For example:
52: *
53: * ```
54: * $cookieMonster = new CookieFilter([
55: * 'when' => function ($req, $res) {
56: * // Custom code goes here.
57: * }
58: * ]);
59: * ```
60: *
61: * If your when condition returns `true` the before/after methods will be called.
62: *
63: * When using the `for` or `when` matchers, conditions will be re-checked on the before and after
64: * callback as the conditions could change during the dispatch cycle.
65: */
66: class DispatcherFilter implements EventListenerInterface
67: {
68: use InstanceConfigTrait;
69:
70: /**
71: * Default priority for all methods in this filter
72: *
73: * @var int
74: */
75: protected $_priority = 10;
76:
77: /**
78: * Default config
79: *
80: * These are merged with user-provided config when the class is used.
81: * The when and for options allow you to define conditions that are checked before
82: * your filter is called.
83: *
84: * @var array
85: */
86: protected $_defaultConfig = [
87: 'when' => null,
88: 'for' => null,
89: 'priority' => null,
90: ];
91:
92: /**
93: * Constructor.
94: *
95: * @param array $config Settings for the filter.
96: * @throws \InvalidArgumentException When 'when' conditions are not callable.
97: */
98: public function __construct($config = [])
99: {
100: if (!isset($config['priority'])) {
101: $config['priority'] = $this->_priority;
102: }
103: $this->setConfig($config);
104: if (isset($config['when']) && !is_callable($config['when'])) {
105: throw new InvalidArgumentException('"when" conditions must be a callable.');
106: }
107: }
108:
109: /**
110: * Returns the list of events this filter listens to.
111: * Dispatcher notifies 2 different events `Dispatcher.before` and `Dispatcher.after`.
112: * By default this class will attach `preDispatch` and `postDispatch` method respectively.
113: *
114: * Override this method at will to only listen to the events you are interested in.
115: *
116: * @return array
117: */
118: public function implementedEvents()
119: {
120: return [
121: 'Dispatcher.beforeDispatch' => [
122: 'callable' => 'handle',
123: 'priority' => $this->_config['priority']
124: ],
125: 'Dispatcher.afterDispatch' => [
126: 'callable' => 'handle',
127: 'priority' => $this->_config['priority']
128: ],
129: ];
130: }
131:
132: /**
133: * Handler method that applies conditions and resolves the correct method to call.
134: *
135: * @param \Cake\Event\Event $event The event instance.
136: * @return mixed
137: */
138: public function handle(Event $event)
139: {
140: $name = $event->getName();
141: list(, $method) = explode('.', $name);
142: if (empty($this->_config['for']) && empty($this->_config['when'])) {
143: return $this->{$method}($event);
144: }
145: if ($this->matches($event)) {
146: return $this->{$method}($event);
147: }
148: }
149:
150: /**
151: * Check to see if the incoming request matches this filter's criteria.
152: *
153: * @param \Cake\Event\Event $event The event to match.
154: * @return bool
155: */
156: public function matches(Event $event)
157: {
158: /* @var \Cake\Http\ServerRequest $request */
159: $request = $event->getData('request');
160: $pass = true;
161: if (!empty($this->_config['for'])) {
162: $len = strlen('preg:');
163: $for = $this->_config['for'];
164: $url = $request->getRequestTarget();
165: if (substr($for, 0, $len) === 'preg:') {
166: $pass = (bool)preg_match(substr($for, $len), $url);
167: } else {
168: $pass = strpos($url, $for) === 0;
169: }
170: }
171: if ($pass && !empty($this->_config['when'])) {
172: $response = $event->getData('response');
173: $pass = $this->_config['when']($request, $response);
174: }
175:
176: return $pass;
177: }
178:
179: /**
180: * Method called before the controller is instantiated and called to serve a request.
181: * If used with default priority, it will be called after the Router has parsed the
182: * URL and set the routing params into the request object.
183: *
184: * If a Cake\Http\Response object instance is returned, it will be served at the end of the
185: * event cycle, not calling any controller as a result. This will also have the effect of
186: * not calling the after event in the dispatcher.
187: *
188: * If false is returned, the event will be stopped and no more listeners will be notified.
189: * Alternatively you can call `$event->stopPropagation()` to achieve the same result.
190: *
191: * @param \Cake\Event\Event $event container object having the `request`, `response` and `additionalParams`
192: * keys in the data property.
193: * @return void
194: */
195: public function beforeDispatch(Event $event)
196: {
197: }
198:
199: /**
200: * Method called after the controller served a request and generated a response.
201: * It is possible to alter the response object at this point as it is not sent to the
202: * client yet.
203: *
204: * If false is returned, the event will be stopped and no more listeners will be notified.
205: * Alternatively you can call `$event->stopPropagation()` to achieve the same result.
206: *
207: * @param \Cake\Event\Event $event container object having the `request` and `response`
208: * keys in the data property.
209: * @return void
210: */
211: public function afterDispatch(Event $event)
212: {
213: }
214: }
215: