Passed
Push — master ( 45ef5d...645e1c )
by Morris
11:54 queued 10s
created

RouteParser::buildControllerName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 *
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Robin McCorkell <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 *
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\AppFramework\Routing;
33
34
use OC\Route\Route;
35
use Symfony\Component\Routing\RouteCollection;
36
37
class RouteParser {
38
	/** @var string[] */
39
	private $controllerNameCache = [];
40
41
	private const rootUrlApps = [
42
		'cloud_federation_api',
43
		'core',
44
		'files_sharing',
45
		'files',
46
		'settings',
47
		'spreed',
48
	];
49
50
	public function parseDefaultRoutes(array $routes, string $appName): RouteCollection {
51
		$collection = $this->processIndexRoutes($routes, $appName);
52
		$collection->addCollection($this->processIndexResources($routes, $appName));
53
54
		return $collection;
55
	}
56
57
	public function parseOCSRoutes(array $routes, string $appName): RouteCollection {
58
		$collection = $this->processOCS($routes, $appName);
59
		$collection->addCollection($this->processOCSResources($routes, $appName));
60
61
		return $collection;
62
	}
63
64
	private function processOCS(array $routes, string $appName): RouteCollection {
65
		$collection = new RouteCollection();
66
		$ocsRoutes = $routes['ocs'] ?? [];
67
		foreach ($ocsRoutes as $ocsRoute) {
68
			$result = $this->processRoute($ocsRoute, $appName, 'ocs.');
69
70
			$collection->add($result[0], $result[1]);
71
		}
72
73
		return $collection;
74
	}
75
76
	/**
77
	 * Creates one route base on the give configuration
78
	 * @param array $routes
79
	 * @throws \UnexpectedValueException
80
	 */
81
	private function processIndexRoutes(array $routes, string $appName): RouteCollection {
82
		$collection = new RouteCollection();
83
		$simpleRoutes = $routes['routes'] ?? [];
84
		foreach ($simpleRoutes as $simpleRoute) {
85
			$result = $this->processRoute($simpleRoute, $appName);
86
87
			$collection->add($result[0], $result[1]);
88
		}
89
90
		return $collection;
91
	}
92
93
	private function processRoute(array $route, string $appName, string $routeNamePrefix = ''): array {
94
		$name = $route['name'];
95
		$postfix = $route['postfix'] ?? '';
96
		$root = $this->buildRootPrefix($route, $appName, $routeNamePrefix);
97
98
		$url = $root . '/' . ltrim($route['url'], '/');
99
		$verb = strtoupper($route['verb'] ?? 'GET');
100
101
		$split = explode('#', $name, 2);
102
		if (count($split) !== 2) {
103
			throw new \UnexpectedValueException('Invalid route name');
104
		}
105
		list($controller, $action) = $split;
106
107
		$controllerName = $this->buildControllerName($controller);
108
		$actionName = $this->buildActionName($action);
109
110
		$routeName = $routeNamePrefix . $appName . '.' . $controller . '.' . $action . $postfix;
111
112
		$routeObject = new Route($url);
113
		$routeObject->method($verb);
114
115
		// optionally register requirements for route. This is used to
116
		// tell the route parser how url parameters should be matched
117
		if (array_key_exists('requirements', $route)) {
118
			$routeObject->requirements($route['requirements']);
119
		}
120
121
		// optionally register defaults for route. This is used to
122
		// tell the route parser how url parameters should be default valued
123
		$defaults = [];
124
		if (array_key_exists('defaults', $route)) {
125
			$defaults = $route['defaults'];
126
		}
127
128
		$defaults['caller'] = [$appName, $controllerName, $actionName];
129
		$routeObject->defaults($defaults);
130
131
		return [$routeName, $routeObject];
132
	}
133
134
	/**
135
	 * For a given name and url restful OCS routes are created:
136
	 *  - index
137
	 *  - show
138
	 *  - create
139
	 *  - update
140
	 *  - destroy
141
	 *
142
	 * @param array $routes
143
	 */
144
	private function processOCSResources(array $routes, string $appName): RouteCollection {
145
		return $this->processResources($routes['ocs-resources'] ?? [], $appName, 'ocs.');
146
	}
147
148
	/**
149
	 * For a given name and url restful routes are created:
150
	 *  - index
151
	 *  - show
152
	 *  - create
153
	 *  - update
154
	 *  - destroy
155
	 *
156
	 * @param array $routes
157
	 */
158
	private function processIndexResources(array $routes, string $appName): RouteCollection {
159
		return $this->processResources($routes['resources'] ?? [], $appName);
160
	}
161
162
	/**
163
	 * For a given name and url restful routes are created:
164
	 *  - index
165
	 *  - show
166
	 *  - create
167
	 *  - update
168
	 *  - destroy
169
	 *
170
	 * @param array $resources
171
	 * @param string $routeNamePrefix
172
	 */
173
	private function processResources(array $resources, string $appName, string $routeNamePrefix = ''): RouteCollection {
174
		// declaration of all restful actions
175
		$actions = [
176
			['name' => 'index', 'verb' => 'GET', 'on-collection' => true],
177
			['name' => 'show', 'verb' => 'GET'],
178
			['name' => 'create', 'verb' => 'POST', 'on-collection' => true],
179
			['name' => 'update', 'verb' => 'PUT'],
180
			['name' => 'destroy', 'verb' => 'DELETE'],
181
		];
182
183
		$collection = new RouteCollection();
184
		foreach ($resources as $resource => $config) {
185
			$root = $this->buildRootPrefix($config, $appName, $routeNamePrefix);
186
187
			// the url parameter used as id to the resource
188
			foreach ($actions as $action) {
189
				$url = $root . '/' . ltrim($config['url'], '/');
190
				$method = $action['name'];
191
192
				$verb = strtoupper($action['verb'] ?? 'GET');
193
				$collectionAction = $action['on-collection'] ?? false;
194
				if (!$collectionAction) {
195
					$url .= '/{id}';
196
				}
197
198
				$controller = $resource;
199
200
				$controllerName = $this->buildControllerName($controller);
201
				$actionName = $this->buildActionName($method);
202
203
				$routeName = $routeNamePrefix . $appName . '.' . strtolower($resource) . '.' . $method;
204
205
				$route = new Route($url);
206
				$route->method($verb);
207
208
				$route->defaults(['caller' => [$appName, $controllerName, $actionName]]);
209
210
				$collection->add($routeName, $route);
211
			}
212
		}
213
214
		return $collection;
215
	}
216
217
	private function buildRootPrefix(array $route, string $appName, string $routeNamePrefix): string {
218
		$defaultRoot = $appName === 'core' ? '' : '/apps/' . $appName;
219
		$root = $route['root'] ?? $defaultRoot;
220
221
		if ($routeNamePrefix !== '') {
222
			// In OCS all apps are whitelisted
223
			return $root;
224
		}
225
226
		if (!\in_array($appName, self::rootUrlApps, true)) {
227
			// Only allow root URLS for some apps
228
			return  $defaultRoot;
229
		}
230
231
		return $root;
232
	}
233
234
	/**
235
	 * Based on a given route name the controller name is generated
236
	 * @param string $controller
237
	 * @return string
238
	 */
239
	private function buildControllerName(string $controller): string {
240
		if (!isset($this->controllerNameCache[$controller])) {
241
			$this->controllerNameCache[$controller] = $this->underScoreToCamelCase(ucfirst($controller)) . 'Controller';
242
		}
243
		return $this->controllerNameCache[$controller];
244
	}
245
246
	/**
247
	 * Based on the action part of the route name the controller method name is generated
248
	 * @param string $action
249
	 * @return string
250
	 */
251
	private function buildActionName(string $action): string {
252
		return $this->underScoreToCamelCase($action);
253
	}
254
255
	/**
256
	 * Underscored strings are converted to camel case strings
257
	 * @param string $str
258
	 * @return string
259
	 */
260
	private function underScoreToCamelCase(string $str): string {
261
		$pattern = '/_[a-z]?/';
262
		return preg_replace_callback(
263
			$pattern,
264
			function ($matches) {
265
				return strtoupper(ltrim($matches[0], '_'));
266
			},
267
			$str);
268
	}
269
}
270