Completed
Pull Request — master (#30885)
by Thomas
13:02
created

Router::loadRoutes()   D

Complexity

Conditions 15
Paths 196

Size

Total Lines 81
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 58
nc 196
nop 1
dl 0
loc 81
rs 4.668
c 0
b 0
f 0

How to fix   Long Method    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
 * @author Bart Visscher <[email protected]>
4
 * @author Bernhard Posselt <[email protected]>
5
 * @author Joas Schilling <[email protected]>
6
 * @author Jörn Friedrich Dreyer <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Robin McCorkell <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Vincent Petry <[email protected]>
14
 *
15
 * @copyright Copyright (c) 2018, ownCloud GmbH
16
 * @license AGPL-3.0
17
 *
18
 * This code is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License, version 3,
20
 * as published by the Free Software Foundation.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License, version 3,
28
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
29
 *
30
 */
31
32
namespace OC\Route;
33
34
use OCP\AppFramework\Http\TemplateResponse;
35
use OCP\ILogger;
36
use OCP\Route\IRouter;
37
use OCP\AppFramework\App;
38
use OCP\Util;
39
use Symfony\Component\Routing\Exception\RouteNotFoundException;
40
use Symfony\Component\Routing\Matcher\UrlMatcher;
41
use Symfony\Component\Routing\Generator\UrlGenerator;
42
use Symfony\Component\Routing\RequestContext;
43
use Symfony\Component\Routing\RouteCollection;
44
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
45
46
class Router implements IRouter {
47
	/** @var RouteCollection[] */
48
	protected $collections = [];
49
	/** @var null|RouteCollection */
50
	protected $collection = null;
51
	/** @var null|string */
52
	protected $collectionName = null;
53
	/** @var null|RouteCollection */
54
	protected $root = null;
55
	/** @var null|UrlGenerator */
56
	protected $generator = null;
57
	/** @var string[] */
58
	protected $routingFiles;
59
	/** @var bool */
60
	protected $loaded = false;
61
	/** @var array */
62
	protected $loadedApps = [];
63
	/** @var ILogger */
64
	protected $logger;
65
	/** @var RequestContext */
66
	protected $context;
67
68
	/**
69
	 * @param ILogger $logger
70
	 */
71
	public function __construct(ILogger $logger, $baseUrl = null) {
72
		$this->logger = $logger;
73
		if (is_null($baseUrl)) {
74
			$baseUrl = \OC::$WEBROOT;
75
		}
76
		if(!(getenv('front_controller_active') === 'true')) {
77
			$baseUrl = rtrim($baseUrl, '/') . '/index.php';
78
		}
79
		if (!\OC::$CLI) {
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
					} else {
107
						$info = \OC_App::getAppInfo($app);
108
						if (isset($info['navigation'], $info['frontend'])) {
109
							$this->routingFiles[$app] = $info;
110
						}
111
					}
112
				}
113
			}
114
		}
115
		return $this->routingFiles;
116
	}
117
118
	/**
119
	 * Loads the routes
120
	 *
121
	 * @param null|string $app
122
	 */
123
	public function loadRoutes($app = null) {
124
		if(is_string($app)) {
125
			$app = \OC_App::cleanAppId($app);
126
		}
127
128
		$requestedApp = $app;
129
		if ($this->loaded) {
130
			return;
131
		}
132
		if ($app === null) {
133
			$this->loaded = true;
134
			$routingFiles = $this->getRoutingFiles();
135
		} else {
136
			if (isset($this->loadedApps[$app])) {
137
				return;
138
			}
139
			$file = \OC_App::getAppPath($app) . '/appinfo/routes.php';
140
			if ($file !== false && file_exists($file)) {
141
				$routingFiles = [$app => $file];
142
			} else {
143
				$routingFiles = [];
144
				$info = \OC_App::getAppInfo($app);
145
				if (isset($info['navigation'], $info['frontend'])) {
146
					$routingFiles = [$app => $info];
147
				}
148
			}
149
		}
150
		\OC::$server->getEventLogger()->start('loadroutes' . $requestedApp, 'Loading Routes');
151
		foreach ($routingFiles as $app => $file) {
152
			if (!isset($this->loadedApps[$app])) {
153
				if (!\OC_App::isAppLoaded($app)) {
154
					// app MUST be loaded before app routes
155
					// try again next time loadRoutes() is called
156
					$this->loaded = false;
157
					continue;
158
				}
159
				$this->loadedApps[$app] = true;
160
				$this->useCollection($app);
161
				if (is_string($file)) {
162
					$this->requireRouteFile($file, $app);
163
				} else {
164
					if (isset($file['navigation'], $file['frontend'])) {
165
						$file['navigation']['route'] = "$app.view.index";
166
						$this->create($file['navigation']['route'], '/')
167
							->method('GET')
168
							->action(function() use ($app) {
169
								$template = new TemplateResponse($app, '');
170
								echo $template->render();
171
							});
172
					}
173
				}
174
				$collection = $this->getCollection($app);
175
				$collection->addPrefix('/apps/' . $app);
176
				$this->root->addCollection($collection);
0 ignored issues
show
Documentation introduced by
$collection is of type object<Symfony\Component\Routing\RouteCollection>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
177
178
				// Also add the OCS collection
179
				$collection = $this->getCollection($app.'.ocs');
180
				$collection->addPrefix('/ocsapp/apps/' . $app);
181
				$this->root->addCollection($collection);
0 ignored issues
show
Documentation introduced by
$collection is of type object<Symfony\Component\Routing\RouteCollection>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
182
			}
183
		}
184
		if (!isset($this->loadedApps['core'])) {
185
			$this->loadedApps['core'] = true;
186
			$this->useCollection('root');
187
			require_once __DIR__ . '/../../../settings/routes.php';
188
			require_once __DIR__ . '/../../../core/routes.php';
189
190
			// Also add the OCS collection
191
			$collection = $this->getCollection('root.ocs');
192
			$collection->addPrefix('/ocsapp');
193
			$this->root->addCollection($collection);
0 ignored issues
show
Documentation introduced by
$collection is of type object<Symfony\Component\Routing\RouteCollection>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
194
		}
195
		if ($this->loaded) {
196
			// include ocs routes, must be loaded last for /ocs prefix
197
			require_once __DIR__ . '/../../../ocs/routes.php';
198
			$collection = $this->getCollection('ocs');
199
			$collection->addPrefix('/ocs');
200
			$this->root->addCollection($collection);
0 ignored issues
show
Documentation introduced by
$collection is of type object<Symfony\Component\Routing\RouteCollection>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
201
		}
202
		\OC::$server->getEventLogger()->end('loadroutes' . $requestedApp);
203
	}
204
205
	/**
206
	 * @return string
207
	 * @deprecated
208
	 */
209
	public function getCacheKey() {
210
		return '';
211
	}
212
213
	/**
214
	 * @param string $name
215
	 * @return \Symfony\Component\Routing\RouteCollection
216
	 */
217
	protected function getCollection($name) {
218
		if (!isset($this->collections[$name])) {
219
			$this->collections[$name] = new RouteCollection();
220
		}
221
		return $this->collections[$name];
222
	}
223
224
	/**
225
	 * Sets the collection to use for adding routes
226
	 *
227
	 * @param string $name Name of the collection to use.
228
	 * @return void
229
	 */
230
	public function useCollection($name) {
231
		$this->collection = $this->getCollection($name);
232
		$this->collectionName = $name;
233
	}
234
235
	/**
236
	 * returns the current collection name in use for adding routes
237
	 *
238
	 * @return string the collection name
239
	 */
240
	public function getCurrentCollection() {
241
		return $this->collectionName;
242
	}
243
244
	/**
245
	 * returns the current collections
246
	 *
247
	 * @return RouteCollection[] collections
248
	 */
249
	public function getCollections() {
250
		return $this->collections;
251
	}
252
253
	/**
254
	 * Create a \OC\Route\Route.
255
	 *
256
	 * @param string $name Name of the route to create.
257
	 * @param string $pattern The pattern to match
258
	 * @param array $defaults An array of default parameter values
259
	 * @param array $requirements An array of requirements for parameters (regexes)
260
	 * @return \OC\Route\Route
261
	 */
262
	public function create($name,
263
						   $pattern,
264
						   array $defaults = [],
265
						   array $requirements = []) {
266
		$route = new Route($pattern, $defaults, $requirements);
267
		$this->collection->add($name, $route);
268
		return $route;
269
	}
270
271
	/**
272
	 * Find the route matching $url
273
	 *
274
	 * @param string $url The url to find
275
	 * @throws \Exception
276
	 * @return void
277
	 */
278
	public function match($url) {
279
		if (substr($url, 0, 6) === '/apps/') {
280
			// empty string / 'apps' / $app / rest of the route
281
			list(, , $app,) = explode('/', $url, 4);
282
283
			$app = \OC_App::cleanAppId($app);
284
			\OC::$REQUESTEDAPP = $app;
285
			$this->loadRoutes($app);
286
		} else if (substr($url, 0, 13) === '/ocsapp/apps/') {
287
			// empty string / 'ocsapp' / 'apps' / $app / rest of the route
288
			list(, , , $app,) = explode('/', $url, 5);
289
290
			$app = \OC_App::cleanAppId($app);
291
			\OC::$REQUESTEDAPP = $app;
292
			$this->loadRoutes($app);
293
		} else if (substr($url, 0, 6) === '/core/' or substr($url, 0, 10) === '/settings/') {
294
			\OC::$REQUESTEDAPP = $url;
295
			if (!\OC::$server->getConfig()->getSystemValue('maintenance', false) && !Util::needUpgrade()) {
296
				\OC_App::loadApps();
297
			}
298
			$this->loadRoutes('core');
299
		} else {
300
			$this->loadRoutes();
301
		}
302
303
		$matcher = new UrlMatcher($this->root, $this->context);
0 ignored issues
show
Bug introduced by
It seems like $this->root can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
304
305
		if (\OC::$server->getRequest()->getMethod() === "OPTIONS") {
306
			try {
307
				// Checking whether the actual request (one which OPTIONS is pre-flight for)
308
				// Is actually valid
309
				$requestingMethod = \OC::$server->getRequest()->getHeader('Access-Control-Request-Method');
310
				$tempContext = $this->context;
311
				$tempContext->setMethod($requestingMethod);
312
				$tempMatcher = new UrlMatcher($this->root, $tempContext);
0 ignored issues
show
Bug introduced by
It seems like $this->root can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
313
				$parameters = $tempMatcher->match($url);
314
315
				// Reach here if it's valid
316
				$response = new \OC\OCS\Result(null, 100, 'OPTIONS request successful');
317
				$response = \OC_Response::setOptionsRequestHeaders($response);
0 ignored issues
show
Documentation introduced by
$response is of type object<OC\OCS\Result>, but the function expects a object<Sabre\HTTP\ResponseInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
318
				\OC_API::respond($response, \OC_API::requestedFormat());
0 ignored issues
show
Documentation introduced by
$response is of type object<Sabre\HTTP\ResponseInterface>, but the function expects a object<OC\OCS\Result>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
319
320
				// Return since no more processing for an OPTIONS request is required
321
				return;
322
			} catch (ResourceNotFoundException $e) {
323 View Code Duplication
				if (substr($url, -1) !== '/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
324
					// We allow links to apps/files? for backwards compatibility reasons
325
					// However, since Symfony does not allow empty route names, the route
326
					// we need to match is '/', so we need to append the '/' here.
327
					try {
328
						$parameters = $matcher->match($url . '/');
329
					} catch (ResourceNotFoundException $newException) {
330
						// If we still didn't match a route, we throw the original exception
331
						throw $e;
332
					}
333
				} else {
334
					throw $e;
335
				}
336
			}
337
		}
338
339
		try {
340
			$parameters = $matcher->match($url);
341
		} catch (ResourceNotFoundException $e) {
342 View Code Duplication
			if (substr($url, -1) !== '/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
343
				// We allow links to apps/files? for backwards compatibility reasons
344
				// However, since Symfony does not allow empty route names, the route
345
				// we need to match is '/', so we need to append the '/' here.
346
				try {
347
					$parameters = $matcher->match($url . '/');
348
				} catch (ResourceNotFoundException $newException) {
349
					// If we still didn't match a route, we throw the original exception
350
					throw $e;
351
				}
352
			} else {
353
				throw $e;
354
			}
355
		}
356
357
		\OC::$server->getEventLogger()->start('run_route', 'Run route');
358
		if (isset($parameters['action'])) {
359
			$action = $parameters['action'];
360
			if (!is_callable($action)) {
361
				throw new \Exception('not a callable action');
362
			}
363
			unset($parameters['action']);
364
			call_user_func($action, $parameters);
365
		} elseif (isset($parameters['file'])) {
366
			include $parameters['file'];
367
		} else {
368
			throw new \Exception('no action available');
369
		}
370
		\OC::$server->getEventLogger()->end('run_route');
371
	}
372
373
	/**
374
	 * Get the url generator
375
	 *
376
	 * @return \Symfony\Component\Routing\Generator\UrlGenerator
377
	 *
378
	 */
379
	public function getGenerator() {
380
		if (null !== $this->generator) {
381
			return $this->generator;
382
		}
383
384
		return $this->generator = new UrlGenerator($this->root, $this->context);
0 ignored issues
show
Bug introduced by
It seems like $this->root can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
385
	}
386
387
	/**
388
	 * Generate url based on $name and $parameters
389
	 *
390
	 * @param string $name Name of the route to use.
391
	 * @param array $parameters Parameters for the route
392
	 * @param bool $absolute
393
	 * @return string
394
	 */
395
	public function generate($name,
396
							 $parameters = [],
397
							 $absolute = false) {
398
		$this->loadRoutes();
399
		try {
400
			$referenceType = UrlGenerator::ABSOLUTE_URL;
401
			if ($absolute === false) {
402
				$referenceType = UrlGenerator::ABSOLUTE_PATH;
403
			}
404
			return $this->getGenerator()->generate($name, $parameters, $referenceType);
405
		} catch (RouteNotFoundException $e) {
406
			$this->logger->logException($e);
407
			return '';
408
		}
409
	}
410
411
	/**
412
	 * To isolate the variable scope used inside the $file it is required in it's own method
413
	 *
414
	 * @param string $file the route file location to include
415
	 * @param string $appName
416
	 */
417
	private function requireRouteFile($file, $appName) {
418
		$this->setupRoutes(include_once $file, $appName);
419
	}
420
421
422
	/**
423
	 * If a routes.php file returns an array, try to set up the application and
424
	 * register the routes for the app. The application class will be chosen by
425
	 * camelcasing the appname, e.g.: my_app will be turned into
426
	 * \OCA\MyApp\AppInfo\Application. If that class does not exist, a default
427
	 * App will be intialized. This makes it optional to ship an
428
	 * appinfo/application.php by using the built in query resolver
429
	 *
430
	 * @param array $routes the application routes
431
	 * @param string $appName the name of the app.
432
	 */
433
	private function setupRoutes($routes, $appName) {
434
		if (is_array($routes)) {
435
			$appNameSpace = App::buildAppNamespace($appName);
436
437
			$applicationClassName = $appNameSpace . '\\AppInfo\\Application';
438
439
			if (class_exists($applicationClassName)) {
440
				$application = new $applicationClassName();
441
			} else {
442
				$application = new App($appName);
443
			}
444
445
			$application->registerRoutes($this, $routes);
446
		}
447
	}
448
}
449