Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/Elgg/Router.php (2 issues)

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) {
0 ignored issues
show
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...
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
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...
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