Completed
Push — master ( 561c31...ac170c )
by Nazar
04:13
created

Router::files_router()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 12
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
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
	cs\ExitException,
11
	cs\Request,
12
	cs\Response;
13
14
/**
15
 * @property string[] $controller_path Path that will be used by controller to render page
16
 */
17
trait Router {
18
	/**
19
	 * Path that will be used by controller to render page
20
	 *
21
	 * @var string[]
22
	 */
23
	protected $controller_path;
24
	/**
25
	 * Execute router
26
	 *
27
	 * Depending on module, files-based or controller-based router might be used
28
	 *
29
	 * @throws ExitException
30
	 */
31
	protected function execute_router () {
32
		$Request = Request::instance();
33
		$this->check_and_normalize_route($Request);
34
		if (file_exists("$this->working_directory/Controller.php")) {
35
			$this->controller_router($Request);
36
		} else {
37
			$this->files_router($Request);
38
		}
39
	}
40
	/**
41
	 * Normalize `cs\Request::$route_path` and fill `cs\App::$controller_path`
42
	 *
43
	 * @param Request $Request
44
	 *
45
	 * @throws ExitException
46
	 */
47
	protected function check_and_normalize_route ($Request) {
48
		if (!file_exists("$this->working_directory/index.json")) {
49
			return;
50
		}
51
		$structure = file_get_json("$this->working_directory/index.json");
52
		if (!$structure) {
53
			return;
54
		}
55
		for ($nesting_level = 0; $structure; ++$nesting_level) {
56
			/**
57
			 * Next level of routing path
58
			 */
59
			$path = @$Request->route_path[$nesting_level];
60
			/**
61
			 * If path not specified - take first from structure
62
			 */
63
			$this->check_and_normalize_route_internal($path, $structure, $Request->api_path);
64
			$Request->route_path[$nesting_level] = $path;
65
			/**
66
			 * Fill paths array intended for controller's usage
67
			 */
68
			$this->controller_path[] = $path;
69
			/**
70
			 * If nested structure is not available - we'll not go into next iteration of this cycle
71
			 */
72
			$structure = @$structure[$path];
73
		}
74
	}
75
	/**
76
	 * @param string $path
77
	 * @param array  $structure
78
	 * @param bool   $api_path
79
	 *
80
	 * @throws ExitException
81
	 */
82
	protected function check_and_normalize_route_internal (&$path, $structure, $api_path) {
83
		/**
84
		 * If path not specified - take first from structure
85
		 */
86
		if (!$path) {
87
			$path = isset($structure[0]) ? $structure[0] : array_keys($structure)[0];
88
			/**
89
			 * We need exact paths for API request (or `_` ending if available) and less strict mode for other cases that allows go deeper automatically
90
			 */
91
			if ($path !== '_' && $api_path) {
92
				throw new ExitException(404);
93
			}
94
		} elseif (!isset($structure[$path]) && !in_array($path, $structure)) {
95
			throw new ExitException(404);
96
		}
97
		/** @noinspection PhpUndefinedMethodInspection */
98
		if (!$this->check_permission($path)) {
99
			throw new ExitException(403);
100
		}
101
	}
102
	/**
103
	 * Include files necessary for module page rendering
104
	 *
105
	 * @param Request $Request
106
	 *
107
	 * @throws ExitException
108
	 */
109
	protected function files_router ($Request) {
110
		foreach ($this->controller_path as $index => $path) {
111
			/**
112
			 * Starting from index 2 we need to maintain slash-separated string that includes all paths from index 1 and till current
113
			 */
114
			if ($index > 1) {
115
				$path = implode('/', array_slice($this->controller_path, 1, $index));
116
			}
117
			$next_exists = isset($this->controller_path[$index + 1]);
118
			$this->files_router_handler($Request, $this->working_directory, $path, !$next_exists);
119
		}
120
	}
121
	/**
122
	 * Include files that corresponds for specific paths in URL
123
	 *
124
	 * @param Request $Request
125
	 * @param string  $dir
126
	 * @param string  $basename
127
	 * @param bool    $required
128
	 *
129
	 * @throws ExitException
130
	 */
131
	protected function files_router_handler ($Request, $dir, $basename, $required = true) {
132
		$this->files_router_handler_internal($Request, $dir, $basename, $required);
133
	}
134
	/**
135
	 * @param Request $Request
136
	 * @param string  $dir
137
	 * @param string  $basename
138
	 * @param bool    $required
139
	 *
140
	 * @throws ExitException
141
	 */
142
	protected function files_router_handler_internal ($Request, $dir, $basename, $required) {
143
		$included = _include("$dir/$basename.php", false, false) !== false;
144
		if (!$Request->api_path) {
145
			return;
146
		}
147
		$request_method = strtolower($Request->method);
148
		$included       = _include("$dir/$basename.$request_method.php", false, false) !== false || $included;
149
		if ($included || !$required) {
150
			return;
151
		}
152
		$methods = get_files_list($dir, "/^$basename\\.[a-z]+\\.php$/");
153
		$methods = _strtoupper(_substr($methods, strlen($basename) + 1, -4));
154
		$this->handler_not_found($methods, $request_method);
155
	}
156
	/**
157
	 * 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
158
	 * methods also doesn't exist
159
	 *
160
	 * @param string[] $available_methods
161
	 * @param string   $request_method
162
	 *
163
	 * @throws ExitException
164
	 */
165
	protected function handler_not_found ($available_methods, $request_method) {
166
		if ($available_methods) {
167
			Response::instance()->header('Allow', implode(', ', $available_methods));
168
			if ($request_method !== 'options') {
169
				throw new ExitException(501);
170
			}
171
		} else {
172
			throw new ExitException(404);
173
		}
174
	}
175
	/**
176
	 * Call methods necessary for module page rendering
177
	 *
178
	 * @param Request $Request
179
	 *
180
	 * @throws ExitException
181
	 */
182
	protected function controller_router ($Request) {
183
		$suffix = '';
184
		if ($Request->admin_path) {
185
			$suffix = '\\admin';
186
		} elseif ($Request->api_path) {
187
			$suffix = '\\api';
188
		}
189
		$controller_class = "cs\\modules\\$Request->current_module$suffix\\Controller";
190
		foreach ($this->controller_path as $index => $path) {
191
			/**
192
			 * Starting from index 2 we need to maintain underscore-separated string that includes all paths from index 1 and till current
193
			 */
194
			if ($index > 1) {
195
				$path = implode('_', array_slice($this->controller_path, 1, $index));
196
			}
197
			$next_exists = isset($this->controller_path[$index + 1]);
198
			$this->controller_router_handler($Request, $controller_class, $path, !$next_exists);
199
		}
200
	}
201
	/**
202
	 * Call methods that corresponds for specific paths in URL
203
	 *
204
	 * @param Request $Request
205
	 * @param string  $controller_class
206
	 * @param string  $method_name
207
	 * @param bool    $required
208
	 *
209
	 * @throws ExitException
210
	 */
211
	protected function controller_router_handler ($Request, $controller_class, $method_name, $required = true) {
212
		$method_name = str_replace('.', '_', $method_name);
213
		$this->controller_router_handler_internal($Request, $controller_class, $method_name, $required);
214
	}
215
	/**
216
	 * @param Request $Request
217
	 * @param string  $controller_class
218
	 * @param string  $method_name
219
	 * @param bool    $required
220
	 *
221
	 * @throws ExitException
222
	 */
223
	protected function controller_router_handler_internal ($Request, $controller_class, $method_name, $required) {
224
		$included =
225
			method_exists($controller_class, $method_name) &&
226
			$controller_class::$method_name($Request->route_ids, $Request->route_path) !== false;
227
		if (!$Request->api_path) {
228
			return;
229
		}
230
		$request_method = strtolower($Request->method);
231
		$included       =
232
			method_exists($controller_class, $method_name.'_'.$request_method) &&
233
			$controller_class::{$method_name.'_'.$request_method}($Request->route_ids, $Request->route_path) !== false ||
234
			$included;
235
		if ($included || !$required) {
236
			return;
237
		}
238
		$methods = array_filter(
239
			get_class_methods($controller_class),
240
			function ($method) use ($method_name) {
241
				return preg_match("/^{$method_name}_[a-z]+$/", $method);
242
			}
243
		);
244
		$methods = _strtoupper(_substr($methods, strlen($method_name) + 1));
245
		$this->handler_not_found($methods, $request_method);
246
	}
247
}
248