Passed
Push — master ( ba823b...756562 )
by Jeroen
06:16
created

Router::resolveRouteParameters()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 37
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.0291

Importance

Changes 0
Metric Value
cc 8
eloc 23
nc 8
nop 3
dl 0
loc 37
ccs 12
cts 13
cp 0.9231
crap 8.0291
rs 5.3846
c 0
b 0
f 0
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 ElggEntity;
12
use InvalidParameterException;
13
use RuntimeException;
14
use SecurityException;
15
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
16
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
17
use Symfony\Component\Routing\Exception\RouteNotFoundException;
18
19
/**
20
 * Delegates requests to controllers based on the registered configuration.
21
 *
22
 * Plugin devs should use these wrapper functions:
23
 *  * elgg_register_page_handler
24
 *  * elgg_unregister_page_handler
25
 *
26
 * @package    Elgg.Core
27
 * @subpackage Router
28
 * @since      1.9.0
29
 * @access     private
30
 */
31
class Router {
32
33
	use Profilable;
34
35
	/**
36
	 * @var PluginHooksService
37
	 */
38
	protected $hooks;
39
40
	/**
41
	 * @var RouteCollection
42
	 */
43
	protected $routes;
44
45
	/**
46
	 * @var UrlMatcher
47
	 */
48
	protected $matcher;
49
50
	/**
51
	 * @var UrlGenerator
52
	 */
53
	protected $generator;
54
55
	/**
56
	 * @var Route
57
	 */
58
	protected $current_route;
59
60
	/**
61
	 * Constructor
62
	 *
63
	 * @param PluginHooksService $hooks     Hook service
64
	 * @param RouteCollection    $routes    Route collection
65
	 * @param UrlMatcher         $matcher   URL Matcher
66
	 * @param UrlGenerator       $generator URL Generator
67 214
	 */
68 214
	public function __construct(PluginHooksService $hooks, RouteCollection $routes, UrlMatcher $matcher, UrlGenerator $generator) {
69 214
		$this->hooks = $hooks;
70 214
		$this->routes = $routes;
71 214
		$this->matcher = $matcher;
72 214
		$this->generator = $generator;
73
	}
74
75
	/**
76
	 * Routes the request to a registered page handler
77
	 *
78
	 * This function triggers a plugin hook `'route', $identifier` so that plugins can
79
	 * modify the routing or handle a request.
80
	 *
81
	 * @param Request $request The request to handle.
82
	 *
83
	 * @return boolean Whether the request was routed successfully.
84
	 * @throws InvalidParameterException
85
	 * @throws SecurityException
86
	 * @access private
87 97
	 */
88 97
	public function route(Request $request) {
89 97
		$segments = $request->getUrlSegments();
90 97
		if ($segments) {
0 ignored issues
show
introduced by Cash Costello
The condition $segments can never be true.
Loading history...
Bug Best Practice introduced by Cash Costello
The expression $segments of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
91
			$identifier = array_shift($segments);
92
		} else {
93
			$identifier = '';
94
			$segments = [];
95
		}
96 97
97 97
		$is_walled_garden = _elgg_config()->walled_garden;
98 97
		$is_logged_in = _elgg_services()->session->isLoggedIn();
99
		$url = elgg_normalize_url($identifier . '/' . implode('/', $segments));
100 97
101 1
		if ($is_walled_garden && !$is_logged_in && !$this->isPublicPage($url)) {
102 1
			if (!elgg_is_xhr()) {
103
				_elgg_services()->session->set('last_forward_from', current_page_url());
104 1
			}
105 1
			register_error(_elgg_services()->translator->translate('loggedinrequired'));
106
			_elgg_services()->responseFactory->redirect('', 'walled_garden');
107 1
108
			return false;
109
		}
110
111 96
		$old = [
112 96
			'identifier' => $identifier,
113 96
			'handler' => $identifier, // backward compatibility
114
			'segments' => $segments,
115
		];
116 96
117
		if ($this->timer) {
118
			$this->timer->begin(['build page']);
119
		}
120 96
121 96
		ob_start();
122
		$result = $this->hooks->trigger('route', $identifier, $old, $old);
123
124
		// false: request was handled, stop processing.
125
		// array: compare to old params.
126 96
127 9
		if ($result === false) {
0 ignored issues
show
introduced by Evan Winslow
The condition $result === false can never be true.
Loading history...
128 9
			$output = ob_get_clean();
129
			$response = elgg_ok_response($output);
130 87
		} else {
131
			$response = false;
132 87
133 2
			if ($result !== $old) {
134
				_elgg_services()->logger->warn('Use the route:rewrite hook to modify routes.');
135
			}
136 87
137 1
			if ($identifier != $result['identifier']) {
138 86
				$identifier = $result['identifier'];
139 1
			} else if ($identifier != $result['handler']) {
140
				$identifier = $result['handler'];
141
			}
142 87
143
			$segments = $result['segments'];
144 87
145 87
			$path = '/';
146 87
			if ($identifier) {
147 87
				$path .= $identifier;
148 69
				if (!empty($segments)) {
149
					$path .= '/' . implode('/', $segments);
150
				}
151
			}
152
153 87
			try {
154
				$parameters = $this->matcher->match($path);
155 81
156
				$resource = elgg_extract('_resource', $parameters);
157 81
				unset($parameters['_resource']);
158 81
159
				$handler = elgg_extract('_handler', $parameters);
160 81
				unset($parameters['_handler']);
161 81
162
				$this->current_route = $this->routes->get($parameters['_route']);
163 81
				$this->current_route->setMatchedParameters($parameters);
0 ignored issues
show
Bug introduced by Ismayil Khayredinov
The method setMatchedParameters() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

163
				$this->current_route->/** @scrutinizer ignore-call */ 
164
                          setMatchedParameters($parameters);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
introduced by Ismayil Khayredinov
The method setMatchedParameters() does not exist on Symfony\Component\Routing\Route. Are you sure you never get this type here, but always one of the subclasses? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

163
				$this->current_route->/** @scrutinizer ignore-call */ 
164
                          setMatchedParameters($parameters);
Loading history...
164 80
165 80
				if ($handler) {
166
					if (is_callable($handler)) {
167
						$response = call_user_func($handler, $segments, $identifier);
168 1
					}
169 81
				} else {
170
					$output = elgg_view_resource($resource, $parameters);
171 6
					$response = elgg_ok_response($output);
172
				}
173
			} catch (ResourceNotFoundException $ex) {
174
				// continue with the legacy logic
175
			} catch (MethodNotAllowedException $ex) {
176
				$response = elgg_error_response($ex->getMessage(), REFERRER, ELGG_HTTP_METHOD_NOT_ALLOWED);
177 87
			}
178
179 87
			$output = ob_get_clean();
180 6
181
			if ($response === false) {
182
				return headers_sent();
183 81
			}
184 19
185
			if (!$response instanceof ResponseBuilder) {
186
				$response = elgg_ok_response($output);
187
			}
188 90
		}
189 15
190
		if (_elgg_services()->responseFactory->getSentResponse()) {
191
			return true;
192 76
		}
193
194 75
		_elgg_services()->responseFactory->respond($response);
0 ignored issues
show
Bug introduced by Ismayil Khayredinov
It seems like $response can also be of type false; however, parameter $response of Elgg\Http\ResponseFactory::respond() does only seem to accept Elgg\Http\ResponseBuilder, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

194
		_elgg_services()->responseFactory->respond(/** @scrutinizer ignore-type */ $response);
Loading history...
195
196
		return headers_sent();
197
	}
198
199
	/**
200
	 * Returns current route
201
	 * @return Route
202
	 */
203
	public function getCurrentRoute() {
204
		return $this->current_route;
205
	}
206
207 159
	/**
208 159
	 * Register a function that gets called when the first part of a URL is
209 1
	 * equal to the identifier.
210
	 *
211
	 * @param string $identifier The page type to handle
212 158
	 * @param string $function   Your function name
213 158
	 *
214 158
	 * @return bool Depending on success
215
	 * @deprecated 3.0
216
	 */
217
	public function registerPageHandler($identifier, $function) {
218
		if (!is_callable($function, true)) {
219
			return false;
220
		}
221
222
		$this->registerRoute($identifier, [
223 158
			'path' => "/$identifier/{segments}",
224
			'handler' => $function,
225
			'defaults' => [
226
				'segments' => '',
227
			],
228
			'requirements' => [
229
				'segments' => '.+',
230
			],
231
		]);
232
233
		return true;
234
	}
235
236
	/**
237
	 * Register a new route
238
	 *
239
	 * Route paths can contain wildcard segments, i.e. /blog/owner/{username}
240
	 * To make a certain wildcard segment optional, add ? to its name,
241
	 * i.e. /blog/owner/{username?}
242
	 *
243
	 * Wildcard requirements for common named variables such as 'guid' and 'username'
244
	 * will be set automatically.
245
	 *
246
	 * @param string $name   Unique route name
247
	 *                       This name can later be used to generate route URLs
248 226
	 * @param array  $params Route parameters
249
	 *                       - path : path of the route
250 226
	 *                       - resource : name of the resource view
251 226
	 *                       - defaults : default values of wildcard segments
252 226
	 *                       - requirements : regex patterns for wildcard segment requirements
253
	 *                       - methods : HTTP methods
254 226
	 *
255
	 * @return Route
256
	 * @throws InvalidParameterException
257
	 */
258 226
	public function registerRoute($name, array $params = []) {
259 226
260 226
		$path = elgg_extract('path', $params);
261
		$resource = elgg_extract('resource', $params);
262
		$handler = elgg_extract('handler', $params);
263 226
264
		if (!$path || (!$resource && !$handler)) {
265
			throw new InvalidParameterException(__METHOD__ . ' requires "path" and "resource" parameters to be set');
266
		}
267
268
		$defaults = elgg_extract('defaults', $params, []);
269
		$requirements = elgg_extract('requirements', $params, []);
270 226
		$methods = elgg_extract('methods', $params, []);
271 226
272 226
		$patterns = [
273
			'guid' => '\d+',
274
			'group_guid' => '\d+',
275
			'container_guid' => '\d+',
276 226
			'owner_guid' => '\d+',
277 226
			'username' => '[\p{L}\p{Nd}._-]+',
278
		];
279
280 226
		$path = trim($path, '/');
281 226
		$segments = explode('/', $path);
282 36
		foreach ($segments as &$segment) {
283
			// look for segments that are defined as optional with added ?
284
			// e.g. /blog/owner/{username?}
285 226
286 28
			if (!preg_match('/\{(\w*)(\?)?\}/i', $segment, $matches)) {
287
				continue;
288
			}
289 226
290
			$wildcard = $matches[1];
291
			if (!isset($defaults[$wildcard]) && isset($matches[2])) {
292 226
				$defaults[$wildcard] = ''; // make it optional
293
			}
294 226
295 226
			if (!isset($requirements[$wildcard])) {
296
				if (array_key_exists($wildcard, $patterns)) {
297 226
					$requirements[$wildcard] = $patterns[$wildcard];
298
				} else {
299 226
					$requirements[$wildcard] = '.+?';
300
				}
301 226
			}
302
303
			$segment = '{' . $wildcard . '}';
304
		}
305
306
		$path = '/' . implode('/', $segments);
307
308
		$defaults['_resource'] = $resource;
309
		$defaults['_handler'] = $handler;
310
311 51
		$route = new Route($path, $defaults, $requirements, [], '', [], $methods);
312 51
313 51
		$this->routes->add($name, $route);
314
315
		return $route;
316
	}
317
318
	/**
319
	 * Unregister a route by its name
320
	 *
321
	 * @param string $name Name of the route
322
	 *
323 32
	 * @return void
324
	 */
325 32
	public function unregisterRoute($name) {
326 19
		$this->routes->remove($name);
327 19
	}
328 19
329
	/**
330
	 * Generate a relative URL for a named route
331
	 *
332
	 * @param string $name       Route name
333
	 * @param array  $parameters Query parameters
334
	 *
335
	 * @return string
336
	 */
337
	public function generateUrl($name, array $parameters = []) {
338
		try {
339
			return $this->generator->generate($name, $parameters, UrlGenerator::ABSOLUTE_URL);
340 33
		} catch (RouteNotFoundException $exception) {
341 33
			elgg_log($exception->getMessage(), 'ERROR');
342 33
			return '';
343
		}
344
	}
345
346
	/**
347
	 * Populates route parameters from entity properties
348
	 *
349
	 * @param string          $name       Route name
350
	 * @param ElggEntity|null $entity     Entity
351
	 * @param array 		  $parameters Preset parameters
352 19
	 *
353 19
	 * @return array|false
354 19
	 */
355 3
	public function resolveRouteParameters($name, ElggEntity $entity = null, array $parameters = []) {
356
		$route = $this->routes->get($name);
357 16
		if (!$route) {
358
			return false;
359
		}
360
361 19
		$requirements = $route->getRequirements();
362 19
		$defaults = $route->getDefaults();
363
		$props = array_merge(array_keys($requirements), array_keys($defaults));
364 19
365 19
		foreach ($props as $prop) {
366 18
			if (substr($prop, 0, 1) === '_') {
367
				continue;
368
			}
369 1
370
			if (isset($parameters[$prop])) {
371
				continue;
372
			}
373
374
			if (!$entity) {
375 1
				$parameters[$prop] = '';
376 1
				continue;
377
			}
378 1
379
			switch ($prop) {
380
				case 'title' :
381
				case 'name' :
382
					$parameters[$prop] = elgg_get_friendly_title($entity->getDisplayName());
383
					break;
384
385
				default :
386
					$parameters[$prop] = $entity->$prop;
387
					break;
388
			}
389
		}
390
391 22
		return $parameters;
392 22
	}
393
394
	/**
395
	 * Unregister a page handler for an identifier
396 22
	 *
397 22
	 * @param string $identifier The page type identifier
398 22
	 *
399 22
	 * @return void
400 22
	 * @deprecated 3.0
401
	 */
402 22
	public function unregisterPageHandler($identifier) {
403
		$this->unregisterRoute($identifier);
404 22
	}
405
406
	/**
407
	 * Filter a request through the route:rewrite hook
408
	 *
409
	 * @param Request $request Elgg request
410
	 *
411 22
	 * @return Request
412
	 * @access private
413
	 */
414
	public function allowRewrite(Request $request) {
415
		$segments = $request->getUrlSegments();
416
		if ($segments) {
0 ignored issues
show
introduced by Steve Clay
The condition $segments can never be true.
Loading history...
Bug Best Practice introduced by Steve Clay
The expression $segments of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
417
			$identifier = array_shift($segments);
418
		} else {
419
			$identifier = '';
420
		}
421
422
		$old = [
423
			'identifier' => $identifier,
424
			'segments' => $segments,
425
		];
426
		$new = _elgg_services()->hooks->trigger('route:rewrite', $identifier, $old, $old);
427
		if ($new === $old) {
428
			return $request;
429
		}
430
431 22
		if (!isset($new['identifier']) || !isset($new['segments']) || !is_string($new['identifier']) || !is_array($new['segments'])
432
		) {
433
			throw new RuntimeException('rewrite_path handler returned invalid route data.');
434 22
		}
435
436 22
		// rewrite request
437 22
		$segments = $new['segments'];
438 22
		array_unshift($segments, $new['identifier']);
439 22
440 22
		return $request->setUrlSegments($segments);
441
	}
442
443
	/**
444
	 * Checks if the page should be allowed to be served in a walled garden mode
445 3
	 *
446
	 * Pages are registered to be public by {@elgg_plugin_hook public_pages walled_garden}.
447
	 *
448
	 * @param string $url Defaults to the current URL
449
	 *
450
	 * @return bool
451
	 * @since 3.0
452
	 */
453
	public function isPublicPage($url = '') {
454
		if (empty($url)) {
455
			$url = current_page_url();
456
		}
457
458
		$parts = parse_url($url);
459
		unset($parts['query']);
460
		unset($parts['fragment']);
461
		$url = elgg_http_build_url($parts);
462
		$url = rtrim($url, '/') . '/';
463
464
		$site_url = _elgg_config()->wwwroot;
465
466
		if ($url == $site_url) {
467
			// always allow index page
468
			return true;
469
		}
470
471
		// default public pages
472
		$defaults = [
473
			'walled_garden/.*',
474
			'action/.*',
475
			'login',
476
			'register',
477
			'forgotpassword',
478
			'changepassword',
479
			'refresh_token',
480
			'ajax/view/languages.js',
481
			'upgrade\.php',
482
			'css/.*',
483
			'js/.*',
484
			'cache/[0-9]+/\w+/.*',
485
			'cron/.*',
486
			'services/.*',
487
			'serve-file/.*',
488
			'robots.txt',
489
			'favicon.ico',
490
		];
491
492
		$params = [
493
			'url' => $url,
494
		];
495
496
		$public_routes = _elgg_services()->hooks->trigger('public_pages', 'walled_garden', $params, $defaults);
497
498
		$site_url = preg_quote($site_url);
499
		foreach ($public_routes as $public_route) {
500
			$pattern = "`^{$site_url}{$public_route}/*$`i";
501
			if (preg_match($pattern, $url)) {
502
				return true;
503
			}
504
		}
505
506
		// non-public page
507
		return false;
508
	}
509
510
}
511