Passed
Push — master ( 0f1cc7...3c334b )
by Roeland
13:14 queued 10s
created

Router::findMatchingRoute()   B

Complexity

Conditions 10
Paths 24

Size

Total Lines 47
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 31
nc 24
nop 1
dl 0
loc 47
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bernhard Posselt <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Jörn Friedrich Dreyer <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Robin McCorkell <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 * @author Vincent Petry <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program. If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC\Route;
35
36
use OC\AppFramework\Routing\RouteParser;
37
use OCP\AppFramework\App;
38
use OCP\ILogger;
39
use OCP\Route\IRouter;
40
use OCP\Util;
41
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
42
use Symfony\Component\Routing\Exception\RouteNotFoundException;
43
use Symfony\Component\Routing\Generator\UrlGenerator;
44
use Symfony\Component\Routing\Matcher\UrlMatcher;
45
use Symfony\Component\Routing\RequestContext;
46
use Symfony\Component\Routing\RouteCollection;
47
48
class Router implements IRouter {
0 ignored issues
show
Deprecated Code introduced by
The interface OCP\Route\IRouter has been deprecated: 9.0.0 ( Ignorable by Annotation )

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

48
class Router implements /** @scrutinizer ignore-deprecated */ IRouter {

This interface has been deprecated. The supplier of the interface has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.

Loading history...
49
	/** @var RouteCollection[] */
50
	protected $collections = [];
51
	/** @var null|RouteCollection */
52
	protected $collection = null;
53
	/** @var null|string */
54
	protected $collectionName = null;
55
	/** @var null|RouteCollection */
56
	protected $root = null;
57
	/** @var null|UrlGenerator */
58
	protected $generator = null;
59
	/** @var string[] */
60
	protected $routingFiles;
61
	/** @var bool */
62
	protected $loaded = false;
63
	/** @var array */
64
	protected $loadedApps = [];
65
	/** @var ILogger */
66
	protected $logger;
67
	/** @var RequestContext */
68
	protected $context;
69
70
	/**
71
	 * @param ILogger $logger
72
	 */
73
	public function __construct(ILogger $logger) {
74
		$this->logger = $logger;
75
		$baseUrl = \OC::$WEBROOT;
76
		if (!(\OC::$server->getConfig()->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true')) {
77
			$baseUrl .= '/index.php';
78
		}
79
		if (!\OC::$CLI && isset($_SERVER['REQUEST_METHOD'])) {
80
			$method = $_SERVER['REQUEST_METHOD'];
81
		} else {
82
			$method = 'GET';
83
		}
84
		$request = \OC::$server->getRequest();
85
		$host = $request->getServerHost();
86
		$schema = $request->getServerProtocol();
87
		$this->context = new RequestContext($baseUrl, $method, $host, $schema);
88
		// TODO cache
89
		$this->root = $this->getCollection('root');
90
	}
91
92
	/**
93
	 * Get the files to load the routes from
94
	 *
95
	 * @return string[]
96
	 */
97
	public function getRoutingFiles() {
98
		if (!isset($this->routingFiles)) {
99
			$this->routingFiles = [];
100
			foreach (\OC_APP::getEnabledApps() as $app) {
101
				$appPath = \OC_App::getAppPath($app);
102
				if ($appPath !== false) {
103
					$file = $appPath . '/appinfo/routes.php';
104
					if (file_exists($file)) {
105
						$this->routingFiles[$app] = $file;
106
					}
107
				}
108
			}
109
		}
110
		return $this->routingFiles;
111
	}
112
113
	/**
114
	 * Loads the routes
115
	 *
116
	 * @param null|string $app
117
	 */
118
	public function loadRoutes($app = null) {
119
		if (is_string($app)) {
120
			$app = \OC_App::cleanAppId($app);
121
		}
122
123
		$requestedApp = $app;
124
		if ($this->loaded) {
125
			return;
126
		}
127
		if (is_null($app)) {
128
			$this->loaded = true;
129
			$routingFiles = $this->getRoutingFiles();
130
		} else {
131
			if (isset($this->loadedApps[$app])) {
132
				return;
133
			}
134
			$file = \OC_App::getAppPath($app) . '/appinfo/routes.php';
0 ignored issues
show
Bug introduced by
Are you sure OC_App::getAppPath($app) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

134
			$file = /** @scrutinizer ignore-type */ \OC_App::getAppPath($app) . '/appinfo/routes.php';
Loading history...
135
			if ($file !== false && file_exists($file)) {
136
				$routingFiles = [$app => $file];
137
			} else {
138
				$routingFiles = [];
139
			}
140
		}
141
		\OC::$server->getEventLogger()->start('loadroutes' . $requestedApp, 'Loading Routes');
142
		foreach ($routingFiles as $app => $file) {
143
			if (!isset($this->loadedApps[$app])) {
144
				if (!\OC_App::isAppLoaded($app)) {
145
					// app MUST be loaded before app routes
146
					// try again next time loadRoutes() is called
147
					$this->loaded = false;
148
					continue;
149
				}
150
				$this->loadedApps[$app] = true;
151
				$this->useCollection($app);
152
				$this->requireRouteFile($file, $app);
153
				$collection = $this->getCollection($app);
154
				$this->root->addCollection($collection);
0 ignored issues
show
Bug introduced by
The method addCollection() 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

154
				$this->root->/** @scrutinizer ignore-call */ 
155
                 addCollection($collection);

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...
155
156
				// Also add the OCS collection
157
				$collection = $this->getCollection($app.'.ocs');
158
				$collection->addPrefix('/ocsapp');
159
				$this->root->addCollection($collection);
160
			}
161
		}
162
		if (!isset($this->loadedApps['core'])) {
163
			$this->loadedApps['core'] = true;
164
			$this->useCollection('root');
165
			require_once __DIR__ . '/../../../core/routes.php';
166
167
			// Also add the OCS collection
168
			$collection = $this->getCollection('root.ocs');
169
			$collection->addPrefix('/ocsapp');
170
			$this->root->addCollection($collection);
171
		}
172
		if ($this->loaded) {
173
			$collection = $this->getCollection('ocs');
174
			$collection->addPrefix('/ocs');
175
			$this->root->addCollection($collection);
176
		}
177
		\OC::$server->getEventLogger()->end('loadroutes' . $requestedApp);
178
	}
179
180
	/**
181
	 * @param string $name
182
	 * @return \Symfony\Component\Routing\RouteCollection
183
	 */
184
	protected function getCollection($name) {
185
		if (!isset($this->collections[$name])) {
186
			$this->collections[$name] = new RouteCollection();
187
		}
188
		return $this->collections[$name];
189
	}
190
191
	/**
192
	 * Sets the collection to use for adding routes
193
	 *
194
	 * @param string $name Name of the collection to use.
195
	 * @return void
196
	 */
197
	public function useCollection($name) {
198
		$this->collection = $this->getCollection($name);
199
		$this->collectionName = $name;
200
	}
201
202
	/**
203
	 * returns the current collection name in use for adding routes
204
	 *
205
	 * @return string the collection name
206
	 */
207
	public function getCurrentCollection() {
208
		return $this->collectionName;
209
	}
210
211
212
	/**
213
	 * Create a \OC\Route\Route.
214
	 *
215
	 * @param string $name Name of the route to create.
216
	 * @param string $pattern The pattern to match
217
	 * @param array $defaults An array of default parameter values
218
	 * @param array $requirements An array of requirements for parameters (regexes)
219
	 * @return \OC\Route\Route
220
	 */
221
	public function create($name,
222
						   $pattern,
223
						   array $defaults = [],
224
						   array $requirements = []) {
225
		$route = new Route($pattern, $defaults, $requirements);
226
		$this->collection->add($name, $route);
0 ignored issues
show
Bug introduced by
The method add() 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

226
		$this->collection->/** @scrutinizer ignore-call */ 
227
                     add($name, $route);

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...
227
		return $route;
228
	}
229
230
	/**
231
	 * Find the route matching $url
232
	 *
233
	 * @param string $url The url to find
234
	 * @throws \Exception
235
	 * @return array
236
	 */
237
	public function findMatchingRoute(string $url): array {
238
		if (substr($url, 0, 6) === '/apps/') {
239
			// empty string / 'apps' / $app / rest of the route
240
			list(, , $app,) = explode('/', $url, 4);
241
242
			$app = \OC_App::cleanAppId($app);
243
			\OC::$REQUESTEDAPP = $app;
244
			$this->loadRoutes($app);
245
		} elseif (substr($url, 0, 13) === '/ocsapp/apps/') {
246
			// empty string / 'ocsapp' / 'apps' / $app / rest of the route
247
			list(, , , $app,) = explode('/', $url, 5);
248
249
			$app = \OC_App::cleanAppId($app);
250
			\OC::$REQUESTEDAPP = $app;
251
			$this->loadRoutes($app);
252
		} elseif (substr($url, 0, 10) === '/settings/') {
253
			$this->loadRoutes('settings');
254
		} elseif (substr($url, 0, 6) === '/core/') {
255
			\OC::$REQUESTEDAPP = $url;
256
			if (!\OC::$server->getConfig()->getSystemValueBool('maintenance') && !Util::needUpgrade()) {
257
				\OC_App::loadApps();
258
			}
259
			$this->loadRoutes('core');
260
		} else {
261
			$this->loadRoutes();
262
		}
263
264
		$matcher = new UrlMatcher($this->root, $this->context);
265
		try {
266
			$parameters = $matcher->match($url);
267
		} catch (ResourceNotFoundException $e) {
268
			if (substr($url, -1) !== '/') {
269
				// We allow links to apps/files? for backwards compatibility reasons
270
				// However, since Symfony does not allow empty route names, the route
271
				// we need to match is '/', so we need to append the '/' here.
272
				try {
273
					$parameters = $matcher->match($url . '/');
274
				} catch (ResourceNotFoundException $newException) {
275
					// If we still didn't match a route, we throw the original exception
276
					throw $e;
277
				}
278
			} else {
279
				throw $e;
280
			}
281
		}
282
283
		return $parameters;
284
	}
285
286
	/**
287
	 * Find and execute the route matching $url
288
	 *
289
	 * @param string $url The url to find
290
	 * @throws \Exception
291
	 * @return void
292
	 */
293
	public function match($url) {
294
		$parameters = $this->findMatchingRoute($url);
295
296
		\OC::$server->getEventLogger()->start('run_route', 'Run route');
297
		if (isset($parameters['caller'])) {
298
			$caller = $parameters['caller'];
299
			unset($parameters['caller']);
300
			$application = $this->getApplicationClass($caller[0]);
301
			\OC\AppFramework\App::main($caller[1], $caller[2], $application->getContainer(), $parameters);
302
		} elseif (isset($parameters['action'])) {
303
			$action = $parameters['action'];
304
			if (!is_callable($action)) {
305
				throw new \Exception('not a callable action');
306
			}
307
			unset($parameters['action']);
308
			call_user_func($action, $parameters);
309
		} elseif (isset($parameters['file'])) {
310
			include $parameters['file'];
311
		} else {
312
			throw new \Exception('no action available');
313
		}
314
		\OC::$server->getEventLogger()->end('run_route');
315
	}
316
317
	/**
318
	 * Get the url generator
319
	 *
320
	 * @return \Symfony\Component\Routing\Generator\UrlGenerator
321
	 *
322
	 */
323
	public function getGenerator() {
324
		if (null !== $this->generator) {
325
			return $this->generator;
326
		}
327
328
		return $this->generator = new UrlGenerator($this->root, $this->context);
329
	}
330
331
	/**
332
	 * Generate url based on $name and $parameters
333
	 *
334
	 * @param string $name Name of the route to use.
335
	 * @param array $parameters Parameters for the route
336
	 * @param bool $absolute
337
	 * @return string
338
	 */
339
	public function generate($name,
340
							 $parameters = [],
341
							 $absolute = false) {
342
		$referenceType = UrlGenerator::ABSOLUTE_URL;
343
		if ($absolute === false) {
344
			$referenceType = UrlGenerator::ABSOLUTE_PATH;
345
		}
346
		$name = $this->fixLegacyRootName($name);
347
		if (strpos($name, '.') !== false) {
348
			list($appName, $other) = explode('.', $name, 3);
349
			// OCS routes are prefixed with "ocs."
350
			if ($appName === 'ocs') {
351
				$appName = $other;
352
			}
353
			$this->loadRoutes($appName);
354
			try {
355
				return $this->getGenerator()->generate($name, $parameters, $referenceType);
356
			} catch (RouteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
357
			}
358
		}
359
360
		// Fallback load all routes
361
		$this->loadRoutes();
362
		try {
363
			return $this->getGenerator()->generate($name, $parameters, $referenceType);
364
		} catch (RouteNotFoundException $e) {
365
			$this->logger->logException($e, ['level' => ILogger::INFO]);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::INFO has been deprecated: 20.0.0 ( Ignorable by Annotation )

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

365
			$this->logger->logException($e, ['level' => /** @scrutinizer ignore-deprecated */ ILogger::INFO]);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
366
			return '';
367
		}
368
	}
369
370
	protected function fixLegacyRootName(string $routeName): string {
371
		if ($routeName === 'files.viewcontroller.showFile') {
372
			return 'files.View.showFile';
373
		}
374
		if ($routeName === 'files_sharing.sharecontroller.showShare') {
375
			return 'files_sharing.Share.showShare';
376
		}
377
		if ($routeName === 'files_sharing.sharecontroller.showAuthenticate') {
378
			return 'files_sharing.Share.showAuthenticate';
379
		}
380
		if ($routeName === 'files_sharing.sharecontroller.authenticate') {
381
			return 'files_sharing.Share.authenticate';
382
		}
383
		if ($routeName === 'files_sharing.sharecontroller.downloadShare') {
384
			return 'files_sharing.Share.downloadShare';
385
		}
386
		if ($routeName === 'files_sharing.publicpreview.directLink') {
387
			return 'files_sharing.PublicPreview.directLink';
388
		}
389
		if ($routeName === 'cloud_federation_api.requesthandlercontroller.addShare') {
390
			return 'cloud_federation_api.RequestHandler.addShare';
391
		}
392
		if ($routeName === 'cloud_federation_api.requesthandlercontroller.receiveNotification') {
393
			return 'cloud_federation_api.RequestHandler.receiveNotification';
394
		}
395
		return $routeName;
396
	}
397
398
	/**
399
	 * To isolate the variable scope used inside the $file it is required in it's own method
400
	 *
401
	 * @param string $file the route file location to include
402
	 * @param string $appName
403
	 */
404
	private function requireRouteFile($file, $appName) {
405
		$this->setupRoutes(include_once $file, $appName);
406
	}
407
408
409
	/**
410
	 * If a routes.php file returns an array, try to set up the application and
411
	 * register the routes for the app. The application class will be chosen by
412
	 * camelcasing the appname, e.g.: my_app will be turned into
413
	 * \OCA\MyApp\AppInfo\Application. If that class does not exist, a default
414
	 * App will be intialized. This makes it optional to ship an
415
	 * appinfo/application.php by using the built in query resolver
416
	 *
417
	 * @param array $routes the application routes
418
	 * @param string $appName the name of the app.
419
	 */
420
	private function setupRoutes($routes, $appName) {
421
		if (is_array($routes)) {
0 ignored issues
show
introduced by
The condition is_array($routes) is always true.
Loading history...
422
			$routeParser = new RouteParser();
423
424
			$defaultRoutes = $routeParser->parseDefaultRoutes($routes, $appName);
425
			$ocsRoutes = $routeParser->parseOCSRoutes($routes, $appName);
426
427
			$this->root->addCollection($defaultRoutes);
428
			$ocsRoutes->addPrefix('/ocsapp');
429
			$this->root->addCollection($ocsRoutes);
430
		}
431
	}
432
433
	private function getApplicationClass(string $appName) {
434
		$appNameSpace = App::buildAppNamespace($appName);
435
436
		$applicationClassName = $appNameSpace . '\\AppInfo\\Application';
437
438
		if (class_exists($applicationClassName)) {
439
			$application = \OC::$server->query($applicationClassName);
440
		} else {
441
			$application = new App($appName);
442
		}
443
444
		return $application;
445
	}
446
}
447