1 | <?php |
||
2 | |||
3 | namespace Elgg; |
||
4 | |||
5 | use Elgg\Http\Request; |
||
6 | use Elgg\Http\ResponseBuilder; |
||
7 | use Elgg\Router\Route; |
||
8 | use Elgg\Router\RouteCollection; |
||
9 | use Elgg\Router\UrlGenerator; |
||
10 | use Elgg\Router\UrlMatcher; |
||
11 | use InvalidParameterException; |
||
12 | use RuntimeException; |
||
13 | use SecurityException; |
||
14 | use Symfony\Component\Routing\Exception\MethodNotAllowedException; |
||
15 | use Symfony\Component\Routing\Exception\ResourceNotFoundException; |
||
16 | use Symfony\Component\Routing\Exception\RouteNotFoundException; |
||
17 | |||
18 | /** |
||
19 | * Delegates requests to controllers based on the registered configuration. |
||
20 | * |
||
21 | * Plugin devs should use these wrapper functions: |
||
22 | * * elgg_register_page_handler |
||
23 | * * elgg_unregister_page_handler |
||
24 | * |
||
25 | * @package Elgg.Core |
||
26 | * @subpackage Router |
||
27 | * @since 1.9.0 |
||
28 | * @access private |
||
29 | */ |
||
30 | class Router { |
||
31 | |||
32 | use Profilable; |
||
33 | |||
34 | /** |
||
35 | * @var PluginHooksService |
||
36 | */ |
||
37 | protected $hooks; |
||
38 | |||
39 | /** |
||
40 | * @var RouteCollection |
||
41 | */ |
||
42 | protected $routes; |
||
43 | |||
44 | /** |
||
45 | * @var UrlMatcher |
||
46 | */ |
||
47 | protected $matcher; |
||
48 | |||
49 | /** |
||
50 | * @var UrlGenerator |
||
51 | */ |
||
52 | protected $generator; |
||
53 | |||
54 | /** |
||
55 | * @var Route |
||
56 | */ |
||
57 | protected $current_route; |
||
58 | |||
59 | /** |
||
60 | * Constructor |
||
61 | * |
||
62 | * @param PluginHooksService $hooks Hook service |
||
63 | * @param RouteCollection $routes Route collection |
||
64 | * @param UrlMatcher $matcher URL Matcher |
||
65 | * @param UrlGenerator $generator URL Generator |
||
66 | */ |
||
67 | 214 | public function __construct(PluginHooksService $hooks, RouteCollection $routes, UrlMatcher $matcher, UrlGenerator $generator) { |
|
68 | 214 | $this->hooks = $hooks; |
|
69 | 214 | $this->routes = $routes; |
|
70 | 214 | $this->matcher = $matcher; |
|
71 | 214 | $this->generator = $generator; |
|
72 | 214 | } |
|
73 | |||
74 | /** |
||
75 | * Routes the request to a registered page handler |
||
76 | * |
||
77 | * This function triggers a plugin hook `'route', $identifier` so that plugins can |
||
78 | * modify the routing or handle a request. |
||
79 | * |
||
80 | * @param Request $request The request to handle. |
||
81 | * |
||
82 | * @return boolean Whether the request was routed successfully. |
||
83 | * @throws InvalidParameterException |
||
84 | * @throws SecurityException |
||
85 | * @access private |
||
86 | */ |
||
87 | 97 | public function route(Request $request) { |
|
88 | 97 | $segments = $request->getUrlSegments(); |
|
89 | 97 | if ($segments) { |
|
90 | 97 | $identifier = array_shift($segments); |
|
91 | } else { |
||
92 | $identifier = ''; |
||
93 | $segments = []; |
||
94 | } |
||
95 | |||
96 | 97 | $is_walled_garden = _elgg_config()->walled_garden; |
|
97 | 97 | $is_logged_in = _elgg_services()->session->isLoggedIn(); |
|
98 | 97 | $url = elgg_normalize_url($identifier . '/' . implode('/', $segments)); |
|
99 | |||
100 | 97 | if ($is_walled_garden && !$is_logged_in && !$this->isPublicPage($url)) { |
|
101 | 1 | if (!elgg_is_xhr()) { |
|
102 | 1 | _elgg_services()->session->set('last_forward_from', current_page_url()); |
|
103 | } |
||
104 | 1 | register_error(_elgg_services()->translator->translate('loggedinrequired')); |
|
105 | 1 | _elgg_services()->responseFactory->redirect('', 'walled_garden'); |
|
106 | |||
107 | 1 | return false; |
|
108 | } |
||
109 | |||
110 | $old = [ |
||
111 | 96 | 'identifier' => $identifier, |
|
112 | 96 | 'handler' => $identifier, // backward compatibility |
|
113 | 96 | 'segments' => $segments, |
|
114 | ]; |
||
115 | |||
116 | 96 | if ($this->timer) { |
|
117 | $this->timer->begin(['build page']); |
||
118 | } |
||
119 | |||
120 | 96 | ob_start(); |
|
121 | 96 | $result = $this->hooks->trigger('route', $identifier, $old, $old); |
|
122 | |||
123 | // false: request was handled, stop processing. |
||
124 | // array: compare to old params. |
||
125 | |||
126 | 96 | if ($result === false) { |
|
127 | 9 | $output = ob_get_clean(); |
|
128 | 9 | $response = elgg_ok_response($output); |
|
129 | } else { |
||
130 | 87 | $response = false; |
|
131 | |||
132 | 87 | if ($result !== $old) { |
|
133 | 2 | _elgg_services()->logger->warn('Use the route:rewrite hook to modify routes.'); |
|
134 | } |
||
135 | |||
136 | 87 | if ($identifier != $result['identifier']) { |
|
137 | 1 | $identifier = $result['identifier']; |
|
138 | 86 | } else if ($identifier != $result['handler']) { |
|
139 | 1 | $identifier = $result['handler']; |
|
140 | } |
||
141 | |||
142 | 87 | $segments = $result['segments']; |
|
143 | |||
144 | 87 | $path = '/'; |
|
145 | 87 | if ($identifier) { |
|
146 | 87 | $path .= $identifier; |
|
147 | 87 | if (!empty($segments)) { |
|
148 | 69 | $path .= '/' . implode('/', $segments); |
|
149 | } |
||
150 | } |
||
151 | |||
152 | try { |
||
153 | 87 | $parameters = $this->matcher->match($path); |
|
154 | |||
155 | 81 | $this->current_route = $this->routes->get($parameters['_route']); |
|
156 | |||
157 | 81 | $resource = elgg_extract('_resource', $parameters); |
|
158 | 81 | unset($parameters['_resource']); |
|
159 | |||
160 | 81 | $handler = elgg_extract('_handler', $parameters); |
|
161 | 81 | unset($parameters['_handler']); |
|
162 | |||
163 | 81 | if ($handler) { |
|
164 | 80 | if (is_callable($handler)) { |
|
165 | 80 | $response = call_user_func($handler, $segments, $identifier); |
|
166 | } |
||
167 | } else { |
||
168 | 1 | $output = elgg_view_resource($resource, $parameters); |
|
169 | 81 | $response = elgg_ok_response($output); |
|
170 | } |
||
171 | 6 | } catch (ResourceNotFoundException $ex) { |
|
172 | // continue with the legacy logic |
||
173 | } catch (MethodNotAllowedException $ex) { |
||
174 | $response = elgg_error_response($ex->getMessage(), REFERRER, ELGG_HTTP_METHOD_NOT_ALLOWED); |
||
175 | } |
||
176 | |||
177 | 87 | $output = ob_get_clean(); |
|
178 | |||
179 | 87 | if ($response === false) { |
|
180 | 6 | return headers_sent(); |
|
181 | } |
||
182 | |||
183 | 81 | if (!$response instanceof ResponseBuilder) { |
|
184 | 19 | $response = elgg_ok_response($output); |
|
185 | } |
||
186 | } |
||
187 | |||
188 | 90 | if (_elgg_services()->responseFactory->getSentResponse()) { |
|
189 | 15 | return true; |
|
190 | } |
||
191 | |||
192 | 76 | _elgg_services()->responseFactory->respond($response); |
|
193 | |||
194 | 75 | return headers_sent(); |
|
195 | } |
||
196 | |||
197 | /** |
||
198 | * Register a function that gets called when the first part of a URL is |
||
199 | * equal to the identifier. |
||
200 | * |
||
201 | * @param string $identifier The page type to handle |
||
202 | * @param string $function Your function name |
||
203 | * |
||
204 | * @return bool Depending on success |
||
205 | * @deprecated 3.0 |
||
206 | */ |
||
207 | 159 | public function registerPageHandler($identifier, $function) { |
|
208 | 159 | if (!is_callable($function, true)) { |
|
209 | 1 | return false; |
|
210 | } |
||
211 | |||
212 | 158 | $this->registerRoute($identifier, [ |
|
213 | 158 | 'path' => "/$identifier/{segments}", |
|
214 | 158 | 'handler' => $function, |
|
215 | 'defaults' => [ |
||
216 | 'segments' => '', |
||
217 | ], |
||
218 | 'requirements' => [ |
||
219 | 'segments' => '.+', |
||
220 | ], |
||
221 | ]); |
||
222 | |||
223 | 158 | return true; |
|
224 | } |
||
225 | |||
226 | /** |
||
227 | * Register a new route |
||
228 | * |
||
229 | * Route paths can contain wildcard segments, i.e. /blog/owner/{username} |
||
230 | * To make a certain wildcard segment optional, add ? to its name, |
||
231 | * i.e. /blog/owner/{username?} |
||
232 | * |
||
233 | * Wildcard requirements for common named variables such as 'guid' and 'username' |
||
234 | * will be set automatically. |
||
235 | * |
||
236 | * @param string $name Unique route name |
||
237 | * This name can later be used to generate route URLs |
||
238 | * @param array $params Route parameters |
||
239 | * - path : path of the route |
||
240 | * - resource : name of the resource view |
||
241 | * - defaults : default values of wildcard segments |
||
242 | * - requirements : regex patterns for wildcard segment requirements |
||
243 | * - methods : HTTP methods |
||
244 | * |
||
245 | * @return Route |
||
246 | * @throws InvalidParameterException |
||
247 | */ |
||
248 | 226 | public function registerRoute($name, array $params = []) { |
|
249 | |||
250 | 226 | $path = elgg_extract('path', $params); |
|
251 | 226 | $resource = elgg_extract('resource', $params); |
|
252 | 226 | $handler = elgg_extract('handler', $params); |
|
253 | |||
254 | 226 | if (!$path || (!$resource && !$handler)) { |
|
255 | throw new InvalidParameterException(__METHOD__ . ' requires "path" and "resource" parameters to be set'); |
||
256 | } |
||
257 | |||
258 | 226 | $defaults = elgg_extract('defaults', $params, []); |
|
259 | 226 | $requirements = elgg_extract('requirements', $params, []); |
|
260 | 226 | $methods = elgg_extract('methods', $params, []); |
|
261 | |||
262 | $patterns = [ |
||
263 | 226 | 'guid' => '\d+', |
|
264 | 'group_guid' => '\d+', |
||
265 | 'container_guid' => '\d+', |
||
266 | 'owner_guid' => '\d+', |
||
267 | 'username' => '[\p{L}\p{Nd}._-]+', |
||
268 | ]; |
||
269 | |||
270 | 226 | $path = trim($path, '/'); |
|
271 | 226 | $segments = explode('/', $path); |
|
272 | 226 | foreach ($segments as &$segment) { |
|
273 | // look for segments that are defined as optional with added ? |
||
274 | // e.g. /blog/owner/{username?} |
||
275 | |||
276 | 226 | if (!preg_match('/\{(\w*)(\?)?\}/i', $segment, $matches)) { |
|
277 | 226 | continue; |
|
278 | } |
||
279 | |||
280 | 226 | $wildcard = $matches[1]; |
|
281 | 226 | if (!isset($defaults[$wildcard]) && isset($matches[2])) { |
|
282 | 36 | $defaults[$wildcard] = ''; // make it optional |
|
283 | } |
||
284 | |||
285 | 226 | if (array_key_exists($wildcard, $patterns) && !isset($requirements[$wildcard])) { |
|
286 | 28 | $requirements[$wildcard] = $patterns[$wildcard]; |
|
287 | } |
||
288 | |||
289 | 226 | $segment = '{' . $wildcard . '}'; |
|
290 | } |
||
291 | |||
292 | 226 | $path = '/' . implode('/', $segments); |
|
293 | |||
294 | 226 | $defaults['_resource'] = $resource; |
|
295 | 226 | $defaults['_handler'] = $handler; |
|
296 | |||
297 | 226 | $route = new Route($path, $defaults, $requirements, [], '', [], $methods); |
|
298 | |||
299 | 226 | $this->routes->add($name, $route); |
|
300 | |||
301 | 226 | return $route; |
|
302 | } |
||
303 | |||
304 | /** |
||
305 | * Unregister a route by its name |
||
306 | * |
||
307 | * @param string $name Name of the route |
||
308 | * |
||
309 | * @return void |
||
310 | */ |
||
311 | 51 | public function unregisterRoute($name) { |
|
312 | 51 | $this->routes->remove($name); |
|
313 | 51 | } |
|
314 | |||
315 | /** |
||
316 | * Generate a relative URL for a named route |
||
317 | * |
||
318 | * @param string $name Route name |
||
319 | * @param array $parameters Query parameters |
||
320 | * |
||
321 | * @return string |
||
322 | */ |
||
323 | 32 | public function generateUrl($name, array $parameters = []) { |
|
324 | try { |
||
325 | 32 | return $this->generator->generate($name, $parameters, UrlGenerator::ABSOLUTE_URL); |
|
326 | 19 | } catch (RouteNotFoundException $exception) { |
|
327 | 19 | elgg_log($exception->getMessage(), 'ERROR'); |
|
328 | 19 | return ''; |
|
329 | } |
||
330 | } |
||
331 | |||
332 | /** |
||
333 | * Unregister a page handler for an identifier |
||
334 | * |
||
335 | * @param string $identifier The page type identifier |
||
336 | * |
||
337 | * @return void |
||
338 | * @deprecated 3.0 |
||
339 | */ |
||
340 | 33 | public function unregisterPageHandler($identifier) { |
|
341 | 33 | $this->unregisterRoute($identifier); |
|
342 | 33 | } |
|
343 | |||
344 | /** |
||
345 | * Filter a request through the route:rewrite hook |
||
346 | * |
||
347 | * @param Request $request Elgg request |
||
348 | * |
||
349 | * @return Request |
||
350 | * @access private |
||
351 | */ |
||
352 | 19 | public function allowRewrite(Request $request) { |
|
353 | 19 | $segments = $request->getUrlSegments(); |
|
354 | 19 | if ($segments) { |
|
0 ignored issues
–
show
|
|||
355 | 3 | $identifier = array_shift($segments); |
|
356 | } else { |
||
357 | 16 | $identifier = ''; |
|
358 | } |
||
359 | |||
360 | $old = [ |
||
361 | 19 | 'identifier' => $identifier, |
|
362 | 19 | 'segments' => $segments, |
|
363 | ]; |
||
364 | 19 | $new = _elgg_services()->hooks->trigger('route:rewrite', $identifier, $old, $old); |
|
365 | 19 | if ($new === $old) { |
|
366 | 18 | return $request; |
|
367 | } |
||
368 | |||
369 | 1 | if (!isset($new['identifier']) || !isset($new['segments']) || !is_string($new['identifier']) || !is_array($new['segments']) |
|
370 | ) { |
||
371 | throw new RuntimeException('rewrite_path handler returned invalid route data.'); |
||
372 | } |
||
373 | |||
374 | // rewrite request |
||
375 | 1 | $segments = $new['segments']; |
|
376 | 1 | array_unshift($segments, $new['identifier']); |
|
377 | |||
378 | 1 | return $request->setUrlSegments($segments); |
|
379 | } |
||
380 | |||
381 | /** |
||
382 | * Checks if the page should be allowed to be served in a walled garden mode |
||
383 | * |
||
384 | * Pages are registered to be public by {@elgg_plugin_hook public_pages walled_garden}. |
||
385 | * |
||
386 | * @param string $url Defaults to the current URL |
||
387 | * |
||
388 | * @return bool |
||
389 | * @since 3.0 |
||
390 | */ |
||
391 | 22 | public function isPublicPage($url = '') { |
|
392 | 22 | if (empty($url)) { |
|
393 | $url = current_page_url(); |
||
394 | } |
||
395 | |||
396 | 22 | $parts = parse_url($url); |
|
397 | 22 | unset($parts['query']); |
|
398 | 22 | unset($parts['fragment']); |
|
399 | 22 | $url = elgg_http_build_url($parts); |
|
400 | 22 | $url = rtrim($url, '/') . '/'; |
|
401 | |||
402 | 22 | $site_url = _elgg_config()->wwwroot; |
|
403 | |||
404 | 22 | if ($url == $site_url) { |
|
405 | // always allow index page |
||
406 | return true; |
||
407 | } |
||
408 | |||
409 | // default public pages |
||
410 | $defaults = [ |
||
411 | 22 | 'walled_garden/.*', |
|
412 | 'action/.*', |
||
413 | 'login', |
||
414 | 'register', |
||
415 | 'forgotpassword', |
||
416 | 'changepassword', |
||
417 | 'refresh_token', |
||
418 | 'ajax/view/languages.js', |
||
419 | 'upgrade\.php', |
||
420 | 'css/.*', |
||
421 | 'js/.*', |
||
422 | 'cache/[0-9]+/\w+/.*', |
||
423 | 'cron/.*', |
||
424 | 'services/.*', |
||
425 | 'serve-file/.*', |
||
426 | 'robots.txt', |
||
427 | 'favicon.ico', |
||
428 | ]; |
||
429 | |||
430 | $params = [ |
||
431 | 22 | 'url' => $url, |
|
432 | ]; |
||
433 | |||
434 | 22 | $public_routes = _elgg_services()->hooks->trigger('public_pages', 'walled_garden', $params, $defaults); |
|
435 | |||
436 | 22 | $site_url = preg_quote($site_url); |
|
437 | 22 | foreach ($public_routes as $public_route) { |
|
438 | 22 | $pattern = "`^{$site_url}{$public_route}/*$`i"; |
|
439 | 22 | if (preg_match($pattern, $url)) { |
|
440 | 22 | return true; |
|
441 | } |
||
442 | } |
||
443 | |||
444 | // non-public page |
||
445 | 3 | return false; |
|
446 | } |
||
447 | |||
448 | } |
||
449 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.