Completed
Push — master ( 214a55...83dd26 )
by Nazar
03:59
created

Router::check_and_normalize_route_internal()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
c 2
b 1
f 1
dl 0
loc 20
rs 7.7777
cc 8
eloc 9
nc 9
nop 3
1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2015-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\App;
9
use
10
	cli,
11
	cs\Config,
12
	cs\Config\Module_Properties,
13
	cs\ExitException,
14
	cs\Page,
15
	cs\Request,
16
	cs\Response;
17
18
/**
19
 * @property string[] $controller_path Path that will be used by controller to render page
20
 */
21
trait Router {
22
	/**
23
	 * Path that will be used by controller to render page
24
	 *
25
	 * @var string[]
26
	 */
27
	protected $controller_path;
28
	/**
29
	 * Execute router
30
	 *
31
	 * Depending on module, files-based or controller-based router might be used
32
	 *
33
	 * @throws ExitException
34
	 */
35
	protected function execute_router () {
36
		$Request = Request::instance();
37
		$this->check_and_normalize_route($Request);
38
		if (file_exists("$this->working_directory/Controller.php")) {
39
			$this->controller_router($Request);
40
		} else {
41
			$this->files_router($Request);
42
		}
43
	}
44
	protected function print_cli_structure ($path) {
45
		$Config = Config::instance();
46
		$result = [];
47
		foreach ($Config->components['modules'] as $module_name => $data) {
48
			if ($data['active'] == Module_Properties::ENABLED) {
49
				$working_dir = MODULES."/$module_name/cli";
50
				$structure   = file_exists("$working_dir/index.json") ? file_get_json("$working_dir/index.json") : [];
51
				$this->print_cli_structure_internal(
52
					$working_dir,
53
					$module_name,
54
					'',
55
					$structure,
56
					$result[$module_name]
57
				);
58
			}
59
		}
60
		$result = $this->print_cli_structure_normalize_result($result);
61
		$Page   = Page::instance();
62
		// Cut `/cli/` prefix
63
		$path = substr($path, 5);
64
		if ($path) {
65
			$Page->content("%WPaths and methods for \"$path\":%n\n");
66
			$result = array_filter(
67
				$result,
68
				function ($item) use ($path) {
69
					return strpos($item[0], $path) === 0;
70
				}
71
			);
72
		} else {
73
			$Page->content("%WAll paths and methods:%n\n");
74
		}
75
		$Page->content(
76
			implode("\n", (new cli\Table(['Path', 'Methods available'], $result))->getDisplayLines())."\n"
77
		);
78
	}
79
	/**
80
	 * @param string $dir
81
	 * @param string $module_name
82
	 * @param string $basename
83
	 * @param array  $structure
84
	 * @param array  $result
85
	 */
86
	protected function print_cli_structure_internal ($dir, $module_name, $basename, $structure, &$result) {
87
		/** @noinspection NestedTernaryOperatorInspection */
88
		foreach ($structure ?: (!$basename ? ['index'] : []) as $path => $nested_structure) {
89
			if (!is_array($nested_structure)) {
90
				$path             = $nested_structure;
91
				$nested_structure = [];
92
			}
93
			$key = $path == '_' ? 0 : $path;
94
			if (file_exists("$dir/Controller.php")) {
95
				$result[$key] = $this->controller_router_available_methods(
96
					$dir,
97
					"\\cs\\modules\\$module_name\\cli\\Controller",
98
					$basename ? $basename.'_'.$path : $path
99
				);
100
				$new_dir      = $dir;
101
				$new_basename = $basename ? $basename.'_'.$path : $path;
102
			} else {
103
				$result[$key] = $this->files_router_available_methods($dir, $path);
104
				$new_dir      = "$dir/$path";
105
				$new_basename = $basename;
106
			}
107
			if ($structure && $nested_structure) {
108
				$this->print_cli_structure_internal($new_dir, $module_name, $new_basename, $nested_structure, $result[$key]);
109
			}
110
		}
111
	}
112
	/**
113
	 * @param array  $result
114
	 * @param string $prefix
115
	 *
116
	 * @return string[]
117
	 */
118
	protected function print_cli_structure_normalize_result ($result, $prefix = '') {
119
		$normalized = [];
120
		foreach ($result as $key => $value) {
121
			if (is_array_assoc($value)) {
122
				if (!$prefix && isset($value['index'])) {
123
					$value[0] = $value['index'];
124
					unset($value['index']);
125
				}
126
				if (is_array(@$value[0]) && $value[0]) {
127
					$normalized[] = [$prefix.$key, strtolower(implode(', ', $value[0]))];
128
				}
129
				unset($value[0]);
130
				/** @noinspection SlowArrayOperationsInLoopInspection */
131
				$normalized = array_merge($normalized, $this->print_cli_structure_normalize_result($value, $prefix.$key.'/'));
132
			} elseif (is_array($value) && $value) {
133
				$normalized[] = [$prefix.$key, strtolower(implode(', ', $value))];
134
			}
135
		}
136
		return $normalized;
137
	}
138
	/**
139
	 * Normalize `cs\Request::$route_path` and fill `cs\App::$controller_path`
140
	 *
141
	 * @param Request $Request
142
	 *
143
	 * @throws ExitException
144
	 */
145
	protected function check_and_normalize_route ($Request) {
146
		if (!file_exists("$this->working_directory/index.json")) {
147
			return;
148
		}
149
		$structure = file_get_json("$this->working_directory/index.json");
150
		if (!$structure) {
151
			return;
152
		}
153
		for ($nesting_level = 0; $structure; ++$nesting_level) {
154
			/**
155
			 * Next level of routing path
156
			 */
157
			$path = @$Request->route_path[$nesting_level];
158
			/**
159
			 * If path not specified - take first from structure
160
			 */
161
			$this->check_and_normalize_route_internal($path, $structure, $Request->cli_path || $Request->api_path);
162
			$Request->route_path[$nesting_level] = $path;
163
			/**
164
			 * Fill paths array intended for controller's usage
165
			 */
166
			$this->controller_path[] = $path;
167
			/**
168
			 * If nested structure is not available - we'll not go into next iteration of this cycle
169
			 */
170
			$structure = @$structure[$path];
171
		}
172
	}
173
	/**
174
	 * @param string $path
175
	 * @param array  $structure
176
	 * @param bool   $cli_or_api_path
177
	 *
178
	 * @throws ExitException
179
	 */
180
	protected function check_and_normalize_route_internal (&$path, $structure, $cli_or_api_path) {
181
		/**
182
		 * If path not specified - take first from structure
183
		 */
184
		if (!$path) {
185
			$path = isset($structure[0]) ? $structure[0] : array_keys($structure)[0];
186
			/**
187
			 * We need exact paths for CLI and API request (or `_` ending if available) and less strict mode for other cases that allows go deeper automatically
188
			 */
189
			if ($path !== '_' && $cli_or_api_path) {
190
				throw new ExitException(404);
191
			}
192
		} elseif (!isset($structure[$path]) && !in_array($path, $structure)) {
193
			throw new ExitException(404);
194
		}
195
		/** @noinspection PhpUndefinedMethodInspection */
196
		if (!$this->check_permission($path)) {
197
			throw new ExitException(403);
198
		}
199
	}
200
	/**
201
	 * Include files necessary for module page rendering
202
	 *
203
	 * @param Request $Request
204
	 *
205
	 * @throws ExitException
206
	 */
207
	protected function files_router ($Request) {
208
		foreach ($this->controller_path as $index => $path) {
209
			/**
210
			 * Starting from index 2 we need to maintain slash-separated string that includes all paths from index 1 and till current
211
			 */
212
			if ($index > 1) {
213
				$path = implode('/', array_slice($this->controller_path, 1, $index));
214
			}
215
			$next_exists = isset($this->controller_path[$index + 1]);
216
			$this->files_router_handler($Request, $this->working_directory, $path, !$next_exists);
217
		}
218
	}
219
	/**
220
	 * Include files that corresponds for specific paths in URL
221
	 *
222
	 * @param Request $Request
223
	 * @param string  $dir
224
	 * @param string  $basename
225
	 * @param bool    $required
226
	 *
227
	 * @throws ExitException
228
	 */
229
	protected function files_router_handler ($Request, $dir, $basename, $required = true) {
230
		$this->files_router_handler_internal($Request, $dir, $basename, $required);
231
	}
232
	/**
233
	 * @param Request $Request
234
	 * @param string  $dir
235
	 * @param string  $basename
236
	 * @param bool    $required
237
	 *
238
	 * @throws ExitException
239
	 */
240
	protected function files_router_handler_internal ($Request, $dir, $basename, $required) {
241
		$included = _include("$dir/$basename.php", false, false) !== false;
242
		if (!$Request->cli_path && !$Request->api_path) {
243
			return;
244
		}
245
		$request_method = strtolower($Request->method);
246
		$included       = _include("$dir/$basename.$request_method.php", false, false) !== false || $included;
247
		if ($included || !$required) {
248
			return;
249
		}
250
		$this->handler_not_found(
251
			$this->files_router_available_methods($dir, $basename),
252
			$request_method,
253
			$Request
254
		);
255
	}
256
	/**
257
	 * @param string $dir
258
	 * @param string $basename
259
	 *
260
	 * @return string[]
261
	 */
262
	protected function files_router_available_methods ($dir, $basename) {
263
		$methods = get_files_list($dir, "/^$basename\\.[a-z]+\\.php$/");
264
		$methods = _strtoupper(_substr($methods, strlen($basename) + 1, -4));
265
		natcasesort($methods);
266
		return array_values($methods);
267
	}
268
	/**
269
	 * If HTTP method handler not found we generate either `501 Not Implemented` if other methods are supported or `404 Not Found` if handlers for others
270
	 * methods also doesn't exist
271
	 *
272
	 * @param string[] $available_methods
273
	 * @param string   $request_method
274
	 * @param Request  $Request
275
	 *
276
	 * @throws ExitException
277
	 */
278
	protected function handler_not_found ($available_methods, $request_method, $Request) {
279
		if ($available_methods) {
280
			if ($Request->cli_path) {
281
				$this->print_cli_structure($Request->path);
282
				if ($request_method !== 'cli') {
283
					throw new ExitException(501);
284
				}
285
			} else {
286
				Response::instance()->header('Allow', implode(', ', $available_methods));
287
				if ($request_method !== 'options') {
288
					throw new ExitException(501);
289
				}
290
			}
291
		} else {
292
			throw new ExitException(404);
293
		}
294
	}
295
	/**
296
	 * Call methods necessary for module page rendering
297
	 *
298
	 * @param Request $Request
299
	 *
300
	 * @throws ExitException
301
	 */
302
	protected function controller_router ($Request) {
303
		$suffix = '';
304
		if ($Request->cli_path) {
305
			$suffix = '\\cli';
306
		} elseif ($Request->admin_path) {
307
			$suffix = '\\admin';
308
		} elseif ($Request->api_path) {
309
			$suffix = '\\api';
310
		}
311
		$controller_class = "cs\\modules\\$Request->current_module$suffix\\Controller";
312
		foreach ($this->controller_path as $index => $path) {
313
			/**
314
			 * Starting from index 2 we need to maintain underscore-separated string that includes all paths from index 1 and till current
315
			 */
316
			if ($index > 1) {
317
				$path = implode('_', array_slice($this->controller_path, 1, $index));
318
			}
319
			$next_exists = isset($this->controller_path[$index + 1]);
320
			$this->controller_router_handler($Request, $controller_class, $path, !$next_exists);
321
		}
322
	}
323
	/**
324
	 * Call methods that corresponds for specific paths in URL
325
	 *
326
	 * @param Request $Request
327
	 * @param string  $controller_class
328
	 * @param string  $method_name
329
	 * @param bool    $required
330
	 *
331
	 * @throws ExitException
332
	 */
333
	protected function controller_router_handler ($Request, $controller_class, $method_name, $required = true) {
334
		$method_name = str_replace('.', '_', $method_name);
335
		$this->controller_router_handler_internal($Request, $controller_class, $method_name, $required);
336
	}
337
	/**
338
	 * @param Request $Request
339
	 * @param string  $controller_class
340
	 * @param string  $method_name
341
	 * @param bool    $required
342
	 *
343
	 * @throws ExitException
344
	 */
345
	protected function controller_router_handler_internal ($Request, $controller_class, $method_name, $required) {
346
		$Response = Response::instance();
347
		$found    = $this->controller_router_handler_internal_execute($controller_class, $method_name, $Request, $Response);
348
		if (!$Request->cli_path && !$Request->api_path) {
349
			return;
350
		}
351
		$request_method = strtolower($Request->method);
352
		$found          = $this->controller_router_handler_internal_execute($controller_class, $method_name.'_'.$request_method, $Request, $Response) || $found;
353
		if ($found || !$required) {
354
			return;
355
		}
356
		$this->handler_not_found(
357
			$this->controller_router_available_methods($this->working_directory, $controller_class, $method_name),
358
			$request_method,
359
			$Request
360
		);
361
	}
362
	/**
363
	 * @param string $working_directory
364
	 * @param string $controller_class
365
	 * @param string $method_name
366
	 *
367
	 * @return string[]
368
	 */
369
	protected function controller_router_available_methods ($working_directory, $controller_class, $method_name) {
370
		$structure = file_exists("$working_directory/index.json") ? file_get_json("$working_directory/index.json") : ['index'];
371
		$structure = $this->controller_router_available_methods_to_flat_structure($structure);
372
		$methods   = array_filter(
373
			get_class_methods($controller_class),
374
			function ($found_method) use ($method_name, $structure) {
375
				if (!preg_match("/^{$method_name}_[a-z_]+$/", $found_method)) {
376
					return false;
377
				}
378
				foreach ($structure as $structure_method) {
379
					if (strpos($found_method, $structure_method) === 0 && strpos($method_name, $structure_method) !== 0) {
380
						return false;
381
					}
382
				}
383
				return true;
384
			}
385
		);
386
		if (method_exists($controller_class, $method_name)) {
387
			$methods[] = $method_name;
388
		}
389
		$methods = _strtoupper(_substr($methods, strlen($method_name) + 1));
390
		natcasesort($methods);
391
		return array_values($methods);
392
	}
393
	/**
394
	 * @param array  $structure
395
	 * @param string $prefix
396
	 *
397
	 * @return string[]
398
	 */
399
	protected function controller_router_available_methods_to_flat_structure ($structure, $prefix = '') {
400
		$flat_structure = [];
401
		foreach ($structure as $path => $nested_structure) {
402
			if (!is_array($nested_structure)) {
403
				$path             = $nested_structure;
404
				$nested_structure = [];
405
			}
406
			$flat_structure[] = $prefix.$path;
407
			/** @noinspection SlowArrayOperationsInLoopInspection */
408
			$flat_structure = array_merge(
409
				$flat_structure,
410
				$this->controller_router_available_methods_to_flat_structure($nested_structure, $prefix.$path.'_')
411
			);
412
		}
413
		return $flat_structure;
414
	}
415
	/**
416
	 * @param string   $controller_class
417
	 * @param string   $method_name
418
	 * @param Request  $Request
419
	 * @param Response $Response
420
	 *
421
	 * @return bool
422
	 */
423
	protected function controller_router_handler_internal_execute ($controller_class, $method_name, $Request, $Response) {
424
		if (!method_exists($controller_class, $method_name)) {
425
			return false;
426
		}
427
		$result = $controller_class::$method_name($Request, $Response);
428
		if ($result !== null) {
429
			Page::instance()->{$Request->api_path ? 'json' : 'content'}($result);
430
		}
431
		return true;
432
	}
433
}
434