These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Kotori.php |
||
4 | * |
||
5 | * A Tiny Model-View-Controller PHP Framework |
||
6 | * |
||
7 | * This content is released under the Apache 2 License |
||
8 | * |
||
9 | * Copyright (c) 2015-2017 Kotori Technology. All rights reserved. |
||
10 | * |
||
11 | * Licensed under the Apache License, Version 2.0 (the "License"); |
||
12 | * you may not use this file except in compliance with the License. |
||
13 | * You may obtain a copy of the License at |
||
14 | * |
||
15 | * http://www.apache.org/licenses/LICENSE-2.0 |
||
16 | * |
||
17 | * Unless required by applicable law or agreed to in writing, software |
||
18 | * distributed under the License is distributed on an "AS IS" BASIS, |
||
19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||
20 | * See the License for the specific language governing permissions and |
||
21 | * limitations under the License. |
||
22 | */ |
||
23 | |||
24 | /** |
||
25 | * Route class |
||
26 | * |
||
27 | * Parses URIs and determines routing |
||
28 | * |
||
29 | * @package Kotori |
||
30 | * @subpackage Http |
||
31 | * @author Kokororin |
||
32 | * @link https://kotori.love |
||
33 | */ |
||
34 | namespace Kotori\Http; |
||
35 | |||
36 | use Kotori\Core\Container; |
||
37 | use Kotori\Core\Helper; |
||
38 | use Kotori\Core\Middleware; |
||
39 | use Kotori\Debug\Hook; |
||
40 | use Kotori\Exception\ConfigException; |
||
41 | use Kotori\Exception\NotFoundException; |
||
42 | use Kotori\Exception\RouteNotFoundException; |
||
43 | |||
44 | class Route |
||
45 | { |
||
46 | /** |
||
47 | * Controllers Array |
||
48 | * |
||
49 | * @var array |
||
50 | */ |
||
51 | protected $controllers = []; |
||
52 | |||
53 | /** |
||
54 | * Current controller |
||
55 | * |
||
56 | * @var string |
||
57 | */ |
||
58 | protected $controller; |
||
59 | |||
60 | /** |
||
61 | * Current action |
||
62 | * |
||
63 | * @var string |
||
64 | */ |
||
65 | protected $action; |
||
66 | |||
67 | /** |
||
68 | * Current URI string |
||
69 | * |
||
70 | * @var mixed |
||
71 | */ |
||
72 | protected $uri = ''; |
||
73 | |||
74 | /** |
||
75 | * Parsed URI Array |
||
76 | * |
||
77 | * @var array |
||
78 | */ |
||
79 | protected $uris = []; |
||
80 | |||
81 | /** |
||
82 | * Parsed params |
||
83 | * |
||
84 | * @var array |
||
85 | */ |
||
86 | protected $params = []; |
||
87 | |||
88 | /** |
||
89 | * Class constructor |
||
90 | * |
||
91 | * Initialize route class. |
||
92 | * |
||
93 | * @return void |
||
94 | */ |
||
95 | public function __construct() |
||
96 | { |
||
97 | if (Container::get('request')->isCli()) { |
||
98 | $this->uri = $this->parseArgv(); |
||
99 | } else { |
||
100 | if (isset($_GET['_i'])) { |
||
101 | $_SERVER['PATH_INFO'] = $_GET['_i']; |
||
102 | } |
||
103 | |||
104 | $_SERVER['PATH_INFO'] = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] |
||
105 | : (isset($_SERVER['ORIG_PATH_INFO']) ? $_SERVER['ORIG_PATH_INFO'] |
||
106 | : (isset($_SERVER['REDIRECT_PATH_INFO']) ? $_SERVER['REDIRECT_PATH_INFO'] : '')); |
||
107 | |||
108 | $this->uri = $_SERVER['PATH_INFO']; |
||
109 | } |
||
110 | |||
111 | if (substr($this->uri, 0, 1) == '/') { |
||
112 | $this->uri = ltrim($this->uri, '/'); |
||
113 | } |
||
114 | |||
115 | if (trim($this->uri, '/') == '') { |
||
116 | $this->uri = '/'; |
||
117 | } |
||
118 | |||
119 | Hook::listen(__CLASS__); |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * Map URL to controller and action |
||
124 | * |
||
125 | * @return void |
||
126 | * |
||
127 | * @throws \Kotori\Exception\RouteNotFoundException |
||
128 | * @throws \Kotori\Exception\NotFoundException |
||
129 | */ |
||
130 | public function dispatch() |
||
131 | { |
||
132 | if (strtolower(Container::get('config')->get('url_mode')) == 'query_string') { |
||
133 | $this->uri = explode('?', $this->uri, 2); |
||
134 | $_SERVER['QUERY_STRING'] = isset($this->uri[1]) ? $this->uri[1] : ''; |
||
135 | $this->uri = $this->uri[0]; |
||
136 | parse_str($_SERVER['QUERY_STRING'], $_GET); |
||
137 | } |
||
138 | |||
139 | if ($this->uri == 'favicon.ico') { |
||
140 | return Container::get('response')->setStatus(404); |
||
141 | } |
||
142 | |||
143 | Middleware::register('before_route'); |
||
144 | $parsedRoute = $this->parseRoutes($this->uri); |
||
145 | Middleware::register('after_route'); |
||
146 | |||
147 | if ($parsedRoute) { |
||
148 | $this->uri = $parsedRoute; |
||
149 | } else { |
||
150 | if (Container::get('request')->isOptions()) { |
||
151 | Container::get('response')->setStatus(204); |
||
152 | exit; |
||
0 ignored issues
–
show
|
|||
153 | } |
||
154 | |||
155 | throw new RouteNotFoundException('Request URI ' . $this->uri . ' is not Matched by any route.'); |
||
156 | } |
||
157 | |||
158 | $this->uris = ($this->uri != '') ? explode('/', trim($this->uri, '/')) : []; |
||
159 | |||
160 | // Clean uris |
||
161 | foreach ($this->uris as $key => $value) { |
||
162 | if ($value == '') { |
||
163 | unset($this->uris[$key]); |
||
164 | } |
||
165 | } |
||
166 | |||
167 | $this->uris = array_merge($this->uris); |
||
168 | |||
169 | $this->controller = $this->getController(); |
||
170 | $this->action = $this->getAction(); |
||
171 | |||
172 | // If is already initialized |
||
173 | $prefix = Container::get('config')->get('namespace_prefix'); |
||
174 | |||
175 | $controllerClassName = $prefix . 'controllers\\' . $this->controller; |
||
176 | |||
177 | Middleware::register('before_controller'); |
||
178 | |||
179 | if (isset($this->controllers[$this->controller])) { |
||
180 | $class = $this->controllers[$this->controller]; |
||
181 | } else { |
||
182 | $class = new $controllerClassName(); |
||
183 | $this->controllers[$this->controller] = $class; |
||
184 | } |
||
185 | |||
186 | Middleware::register('after_controller'); |
||
187 | |||
188 | if (!class_exists($controllerClassName)) { |
||
189 | throw new NotFoundException('Request Controller ' . $this->controller . ' is not Found.'); |
||
190 | } |
||
191 | |||
192 | if (!method_exists($class, $this->action)) { |
||
193 | throw new NotFoundException('Request Action ' . $this->action . ' is not Found.'); |
||
194 | } |
||
195 | |||
196 | $callback = [$class, $this->action]; |
||
197 | if (!is_callable($callback)) { |
||
198 | throw new NotFoundException($controllerClassName . '::' . $this->action . '() is not callable'); |
||
199 | } |
||
200 | |||
201 | // Parse params from uri |
||
202 | $this->params = $this->getParams(); |
||
203 | |||
204 | // Do some final cleaning of the params |
||
205 | $_GET = array_merge($this->params, $_GET); |
||
206 | $_REQUEST = array_merge($_POST, $_GET, $_COOKIE); |
||
207 | |||
208 | if (Container::get('config')->get('app_debug')) { |
||
209 | Container::get('response')->setHeader('X-Kotori-Hash', call_user_func(function () { |
||
210 | $lockFile = Helper::getComposerVendorPath() . '/../composer.lock'; |
||
211 | if (!Helper::isFile($lockFile)) { |
||
212 | return 'unknown'; |
||
213 | } else { |
||
214 | $lockData = file_get_contents($lockFile); |
||
215 | $lockData = json_decode($lockData, true); |
||
216 | foreach ($lockData['packages'] as $package) { |
||
217 | if ($package['name'] == 'kokororin/kotori-php') { |
||
218 | return substr($package['source']['reference'], 0, 6); |
||
219 | } |
||
220 | } |
||
221 | } |
||
222 | |||
223 | return 'unknown'; |
||
224 | })); |
||
225 | } |
||
226 | |||
227 | Middleware::register('before_action'); |
||
228 | // Call the requested method |
||
229 | call_user_func_array($callback, $this->params); |
||
230 | Middleware::register('after_action'); |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Returns the controller name |
||
235 | * |
||
236 | * @return string |
||
237 | * |
||
238 | * @throws \Kotori\Exception\NotFoundException |
||
239 | */ |
||
240 | public function getController() |
||
241 | { |
||
242 | if (isset($this->uris[0]) && '' !== $this->uris[0]) { |
||
243 | $_controller = $this->uris[0]; |
||
244 | } else { |
||
245 | throw new NotFoundException('Cannot dispatch controller name.'); |
||
246 | } |
||
247 | |||
248 | return strip_tags($_controller); |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * Returns the action name |
||
253 | * |
||
254 | * @return string |
||
255 | * |
||
256 | * @throws \Kotori\Exception\NotFoundException |
||
257 | */ |
||
258 | public function getAction() |
||
259 | { |
||
260 | if (isset($this->uris[1])) { |
||
261 | $_action = $this->uris[1]; |
||
262 | } else { |
||
263 | throw new NotFoundException('Cannot dispatch action name.'); |
||
264 | } |
||
265 | |||
266 | return strip_tags($_action); |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * Returns the request params |
||
271 | * |
||
272 | * @return array |
||
273 | */ |
||
274 | public function getParams() |
||
275 | { |
||
276 | $params = $this->uris; |
||
277 | unset($params[0], $params[1]); |
||
278 | return array_merge($params); |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * Returns the URI |
||
283 | * |
||
284 | * @return string |
||
285 | */ |
||
286 | public function getUri() |
||
287 | { |
||
288 | return $this->uri; |
||
289 | } |
||
290 | |||
291 | /** |
||
292 | * Parse Routes |
||
293 | * |
||
294 | * Matches any routes that may exist in URL_ROUTE array |
||
295 | * against the URI to determine if the class/method need to be remapped. |
||
296 | * |
||
297 | * @param string $uri |
||
298 | * @return string |
||
299 | */ |
||
300 | protected function parseRoutes($uri) |
||
301 | { |
||
302 | $routes = Container::get('config')->get('url_route'); |
||
303 | |||
304 | $hostName = Container::get('request')->getHostName(); |
||
305 | |||
306 | if (isset($routes[$hostName])) { |
||
307 | $routes = $routes[$hostName]; |
||
308 | } |
||
309 | |||
310 | // Get HTTP verb |
||
311 | $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli'; |
||
312 | |||
313 | if (null != $routes) { |
||
314 | foreach ($routes as $key => $val) { |
||
315 | // Check if route format is using HTTP verbs |
||
316 | if (is_array($val)) { |
||
317 | $val = array_change_key_case($val, CASE_LOWER); |
||
318 | if (isset($val[$http_verb])) { |
||
319 | $val = $val[$http_verb]; |
||
320 | } else { |
||
321 | continue; |
||
322 | } |
||
323 | } |
||
324 | |||
325 | // Does the RegEx match? |
||
326 | if (preg_match('#^' . $key . '$#', $uri, $matches)) { |
||
327 | // Are we using callbacks to process back-references? |
||
328 | if (!is_string($val) && is_callable($val)) { |
||
329 | // Remove the original string from the matches array. |
||
330 | array_shift($matches); |
||
331 | |||
332 | // Execute the callback using the values in matches as its parameters. |
||
333 | $val = call_user_func_array($val, $matches); |
||
334 | } |
||
335 | // Are we using the default routing method for back-references? |
||
336 | elseif (strpos($val, '$') !== false && strpos($key, '(') !== false) { |
||
337 | $val = preg_replace('#^' . $key . '$#', $val, $uri); |
||
338 | } |
||
339 | |||
340 | return $val; |
||
341 | } |
||
342 | } |
||
343 | } |
||
344 | |||
345 | } |
||
346 | |||
347 | /** |
||
348 | * Parse CLI arguments |
||
349 | * |
||
350 | * Take each command line argument and assume it is a URI segment. |
||
351 | * |
||
352 | * @return string |
||
353 | */ |
||
354 | protected function parseArgv() |
||
355 | { |
||
356 | $args = array_slice($_SERVER['argv'], 1); |
||
357 | return $args ? implode('/', $args) : ''; |
||
358 | } |
||
359 | |||
360 | /** |
||
361 | * Build Full URL |
||
362 | * |
||
363 | * @param string $uri |
||
364 | * @param string $module |
||
365 | * @return string |
||
366 | * |
||
367 | * @throws \Kotori\Exception\ConfigException |
||
368 | */ |
||
369 | public function url($uri = '', $module = null) |
||
370 | { |
||
371 | if ($module != null) { |
||
0 ignored issues
–
show
|
|||
372 | $appNames = Container::get('config')->get('app_name'); |
||
373 | if (is_array($appNames)) { |
||
374 | foreach ($appNames as &$appName) { |
||
375 | $appName = str_replace('./', '', $appName); |
||
376 | } |
||
377 | |||
378 | $appNames = array_flip($appNames); |
||
379 | $baseUrl = $appNames[$module]; |
||
380 | $baseUrl = '//' . $baseUrl . '/'; |
||
381 | } |
||
382 | } else { |
||
383 | $baseUrl = Container::get('request')->getBaseUrl(); |
||
384 | } |
||
385 | |||
386 | $uri = is_array($uri) ? implode('/', $uri) : trim($uri, '/'); |
||
387 | $prefix = $baseUrl . 'index.php?_i='; |
||
0 ignored issues
–
show
The variable
$baseUrl does not seem to be defined for all execution paths leading up to this point.
If you define a variable conditionally, it can happen that it is not defined for all execution paths. Let’s take a look at an example: function myFunction($a) {
switch ($a) {
case 'foo':
$x = 1;
break;
case 'bar':
$x = 2;
break;
}
// $x is potentially undefined here.
echo $x;
}
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined. Available Fixes
Loading history...
|
|||
388 | |||
389 | switch (strtolower(Container::get('config')->get('url_mode'))) { |
||
390 | case 'path_info': |
||
391 | return $uri == '' ? rtrim($baseUrl, '/') : $baseUrl . $uri; |
||
392 | case 'query_string': |
||
393 | return $uri == '' ? rtrim($baseUrl, '/') : $prefix . $uri; |
||
394 | default: |
||
395 | throw new ConfigException('`url_mode` Config ERROR'); |
||
396 | } |
||
397 | |||
398 | } |
||
399 | |||
400 | } |
||
401 |
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exit
expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.