Completed
Push — master ( f3421d...e15370 )
by Jörn Friedrich
11:43
created

ListRoutes::buildController()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 20
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 30
rs 8.439
1
<?php
2
/**
3
 * @author Jörn Friedrich Dreyer <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2017, ownCloud GmbH
6
 * @license AGPL-3.0
7
 *
8
 * This code is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License, version 3,
10
 * as published by the Free Software Foundation.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License, version 3,
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19
 *
20
 */
21
22
namespace OC\Core\Command\Security;
23
24
use OC\AppFramework\App;
25
use OC\Core\Command\Base;
26
use OC\Route\Route;
27
use OC\Route\Router;
28
use OCP\Route\IRouter;
29
use Symfony\Component\Console\Helper\Table;
30
use Symfony\Component\Console\Input\InputInterface;
31
use Symfony\Component\Console\Input\InputOption;
32
use Symfony\Component\Console\Output\OutputInterface;
33
34
class ListRoutes extends Base {
35
36
	/**
37
	 * @var Router
38
	 */
39
	protected $router;
40
41
	public function __construct(IRouter $router) {
42
		$this->router = $router;
0 ignored issues
show
Documentation Bug introduced by
$router is of type object<OCP\Route\IRouter>, but the property $router was declared to be of type object<OC\Route\Router>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
43
		parent::__construct();
44
	}
45
46
	protected function configure() {
47
		$this
48
			->setName('security:routes')
49
			->setDescription('list used routes')
50
			->addOption('with-details', 'd', InputOption::VALUE_NONE);
51
		parent::configure();
52
	}
53
54
	protected function execute(InputInterface $input, OutputInterface $output) {
55
		$outputType = $input->getOption('output');
56
57
		\OC_App::loadApps();
58
		$this->router->loadRoutes();
59
60
		$rows = [];
61
62
		if ($input->getOption('with-details')) {
63
			$headers = [
64
				'Path',
65
				'Methods',
66
				'Controller',
67
				'Annotations',
68
			];
69
			/** @var Route[] $collections */
70
			$collections = [];
71
			foreach ($this->router->getCollections() as $c) {
72
				$new = $c->all();
73
				$collections = $collections + $new;
74
			}
75
76
			foreach ($collections as $name => $route) {
77
				$c = $this->buildController($name);
78
				$rows[] = array_merge([
79
					'path' => $route->getPath(),
80
					'methods' => $route->getMethods()
81
				], $c);
82
			}
83
		} else {
84
			$headers = [
85
				'Path',
86
				'Methods'
87
			];
88
			foreach ($this->router->getCollections() as $routeCollection) {
89
				foreach ($routeCollection as $route) {
90
					$path = $route->getPath();
91
					if (isset($rows[$path])) {
92
						$rows[$path]['methods'] = array_unique(array_merge($rows[$path]['methods'], $route->getMethods()));
93
					} else {
94
						$rows[$path] = [
95
							'path' => $path,
96
							'methods' => $route->getMethods()
97
						];
98
					}
99
					sort ($rows[$path]['methods']);
100
				}
101
			}
102
		}
103
		usort($rows, function ($a, $b) {
104
			return strcmp($a['path'], $b['path']);
105
		});
106
		$rows = array_map(function($row) {
107
			$row['methods'] = implode(',', $row['methods']);
108
			return $row;
109
		}, $rows);
110
111
		if ($outputType === self::OUTPUT_FORMAT_JSON ) {
112
			$output->write(json_encode($rows));
113
		} else if ($outputType === self::OUTPUT_FORMAT_JSON_PRETTY) {
114
			$output->writeln(json_encode($rows, JSON_PRETTY_PRINT));
115
		} else {
116
			$table = new Table($output);
117
			$table->setHeaders($headers);
118
119
			$table->addRows($rows);
120
			$table->render();
121
		}
122
	}
123
124
	private function buildController($name) {
125
		$parts = explode('.', $name);
126
		if (count($parts) === 4 && $parts[0] === 'ocs') {
127
			array_shift($parts);
128
		}
129
		if (count($parts) !== 3) {
130
			return [
131
				'controllerClass' => '*** not controller based ***'
132
			];
133
		}
134
		$appName = $parts[0];
135
		$controllerName = $parts[1];
136
		$method = $parts[2];
137
		$reflection = $this->buildReflection($appName, $controllerName, $method);
138
		if ($reflection === null) {
139
			return [
140
				'controllerClass' => '*** controller not resolvable ***'
141
			];
142
		}
143
		$docs = $reflection->getDocComment();
144
145
		// extract everything prefixed by @ and first letter uppercase
146
		preg_match_all('/@([A-Z]\w+)/', $docs, $matches);
147
		$annotations = $matches[1];
148
149
		return [
150
			'controllerClass' => $reflection->getDeclaringClass()->getName() . '::' . $reflection->getName(),
151
			'annotations' => implode(',', $annotations),
152
		];
153
	}
154
155
	/**
156
	 * @param string $appName
157
	 * @param string $controllerName
158
	 * @param string $method
159
	 * @return null|\ReflectionMethod
160
	 */
161
	private function buildReflection($appName, $controllerName, $method) {
162
		foreach ($this->listControllerNames($appName, $controllerName) as $controllerName) {
163
			foreach ($this->listMethodNames($method) as $m) {
164
				try {
165
					$reflection = new \ReflectionMethod($controllerName, $m);
166
					return $reflection;
167
				} catch (\ReflectionException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
168
				}
169
			}
170
		}
171
		return null;
172
	}
173
174
	/**
175
	 * @param string $appName
176
	 * @param string $controllerName
177
	 * @return \Generator | string[]
178
	 */
179
	private function listControllerNames($appName, $controllerName) {
180
		foreach ([App::buildAppNamespace($appName), App::buildAppNamespace($appName, 'OC\\')] as $appNameSpace) {
181
			foreach (['\\Controller\\', '\\Controllers\\'] as $namespace) {
182
				yield $appNameSpace . $namespace . $controllerName;
183
				yield $appNameSpace . $namespace . ucfirst(strtolower($controllerName));
184
				yield $appNameSpace . $namespace . $controllerName . 'Controller';
185
				yield $appNameSpace . $namespace . ucfirst(strtolower($controllerName)) . 'Controller';
186
				$controllerName = implode('', array_map(function ($word) {
187
					return ucfirst($word);
188
				}, explode('_', $controllerName)));
189
				yield $appNameSpace . $namespace . $controllerName;
190
				yield $appNameSpace . $namespace . ucfirst(strtolower($controllerName));
191
				yield $appNameSpace . $namespace . $controllerName . 'Controller';
192
				yield $appNameSpace . $namespace . ucfirst(strtolower($controllerName)) . 'Controller';
193
			}
194
		}
195
	}
196
197
	/**
198
	 * @param string $method
199
	 * @return \Generator | string[]
200
	 */
201
	private function listMethodNames($method) {
202
		yield $method;
203
		yield implode('', explode('_', $method));
204
		foreach (['post', 'put'] as $verb) {
205
			if (substr( $method, -strlen($verb)) == $verb) {
206
				yield substr($method, 0, -strlen($verb));
207
				yield implode('', explode('_', substr($method, 0, -strlen($verb))));
208
			}
209
		}
210
	}
211
212
}
213