1: <?php
2: /**
3: * This file is part of the PHPLucidFrame library.
4: * Core utility for system routing
5: *
6: * @package PHPLucidFrame\Core
7: * @since PHPLucidFrame v 1.0.0
8: * @copyright Copyright (c), PHPLucidFrame.
9: * @link http://phplucidframe.com
10: * @license http://www.opensource.org/licenses/mit-license.php MIT License
11: * This source file is subject to the MIT license that is bundled
12: * with this source code in the file LICENSE
13: */
14:
15: use LucidFrame\Core\Router;
16:
17: /**
18: *
19: * @internal
20: * @ignore
21: * Route to a page according to request
22: * internally called by /app/index.php
23: * @return string|\Closure
24: */
25: function router()
26: {
27: if (PHP_SAPI === 'cli') {
28: return '';
29: }
30:
31: # Get a route from the defined custom routes (if any)
32: $_page = Router::match();
33:
34: if ($_page instanceof \Closure) {
35: return $_page;
36: }
37:
38: if ($_page) {
39: $pathToPage = Router::getAbsolutePathToRoot($_page);
40: if (is_file($pathToPage) && file_exists($pathToPage)) {
41: return $pathToPage;
42: }
43: }
44:
45: $q = route_path();
46:
47: # if it is still empty, set it to the system default
48: if (empty($q)) {
49: $q = 'home';
50: }
51:
52: # Get the complete path to root
53: $_page = Router::getAbsolutePathToRoot($q);
54: if (!empty($_page) && is_file($_page) && file_exists($_page)) {
55: return $_page;
56: }
57:
58: if (preg_match('/(.*)(401|403|404) {1}$/', $_page, $matches)) {
59: return _i('inc/tpl/' . $matches[2] . '.php');
60: }
61:
62: # Search the physical directory according to the routing path
63: $_page = route_search();
64: if ($_page && is_file($_page) && file_exists($_page)) {
65: return $_page;
66: }
67:
68: if (in_array(_arg(0), array('401', '403'))) {
69: _header(_arg(0));
70: return _i('inc/tpl/' . _arg(0) . '.php');
71: } else {
72: _header(404);
73: return _i('inc/tpl/404.php');
74: }
75: }
76:
77: /**
78: * Search the physical directory according to the routing path
79: *
80: * @return mixed The path if found; otherwise return false
81: */
82: function route_search()
83: {
84: $q = route_path();
85: $seg = explode('/', $q);
86: $count = sizeof($seg);
87: $sites = _cfg('sites');
88:
89: if ($seg[0] == LC_NAMESPACE && is_array($sites) && array_key_exists(LC_NAMESPACE, $sites)) {
90: $seg[0] = $sites[LC_NAMESPACE];
91: }
92:
93: $path = implode('/', $seg);
94: if (is_file($path) && file_exists($path)) {
95: if (count($seg) > 1) {
96: _cfg('cleanRoute', implode('/', array_slice($seg, 0, count($seg) - 1)));
97: } else {
98: _cfg('cleanRoute', '');
99: }
100: return $path;
101: }
102:
103: $append = array('/index.php', '/view.php', '.php');
104: for ($i = $count; $i > 0; $i--) {
105: # try to look for
106: # ~/path/to/the-given-name/index.php
107: # ~/path/to/the-given-name.php
108: foreach ($append as $a) {
109: $cleanRoute = implode('/', array_slice($seg, 0, $i));
110: $path = $cleanRoute . $a;
111: if (is_file($path) && file_exists($path)) {
112: _cfg('cleanRoute', rtrim($cleanRoute, '/'));
113:
114: $definedRoutes = Router::getRoutes();
115: // Find matching routes for this clean route
116: $routes = array_filter($definedRoutes, function ($route) use ($cleanRoute) {
117: return is_string($route['to']) && ltrim($route['to'], '/') == ltrim($cleanRoute, '/');
118: });
119:
120: foreach ($routes as $key => $value) {
121: if (!in_array($_SERVER['REQUEST_METHOD'], $value['method'])) {
122: if ($key == Router::getMatchedName()) {
123: _header(405);
124: throw new \RuntimeException(sprintf('The Router does not allow the method "%s" for "%s".', $_SERVER['REQUEST_METHOD'], $key));
125: } else {
126: _header(404);
127: throw new \RuntimeException(sprintf('The Router is not found for "%s".', Router::getMatchedName()));
128: }
129: }
130: }
131:
132: return $path;
133: }
134: }
135: }
136:
137: return false;
138: }
139:
140: /**
141: * Get the routing path
142: * Alias `_r()`
143: *
144: * @return string
145: */
146: function route_path()
147: {
148: $path = '';
149:
150: if (isset($_GET[ROUTE])) {
151: $path = $_GET[ROUTE] instanceof \Closure ? $_GET[ROUTE . '_path'] : urldecode($_GET[ROUTE]);
152: }
153:
154: return $path;
155: }
156:
157: /**
158: * Return the absolute URL path appended the query string if necessary
159: * Alias `_url()`
160: *
161: * @param string $path Routing path such as "foo/bar"; Named route such as "fool_bar"; NULL for the current path
162: * @param array $queryStr Query string as
163: * array(
164: * $value1, // no key here
165: * 'key1' => $value2,
166: * 'key3' => $value3 or array($value3, $value4)
167: * )
168: * @param string $lang Language code to be prepended to $path such as "en/foo/bar".
169: * It will be useful for site language switch redirect
170: * @return string
171: */
172: function route_url($path = null, $queryStr = array(), $lang = '')
173: {
174: global $lc_cleanURL;
175: global $lc_translationEnabled;
176: global $lc_sites;
177: global $lc_langInURI;
178:
179: $forceExcludeLangInURL = $lang === false;
180:
181: if ($path && stripos($path, 'http') === 0) {
182: return $path;
183: }
184:
185: $customRoute = Router::getPathByName($path);
186: if ($customRoute !== null) {
187: $path = $customRoute ?: 'home';
188: if ($queryStr && is_array($queryStr) && count($queryStr)) {
189: foreach ($queryStr as $key => $value) {
190: $path = str_replace('{' . $key . '}', urlencode($value), $path);
191: }
192: }
193:
194: $queryStr = array(); // clean query strings to not be processed later
195: }
196:
197: if ($path && is_string($path)) {
198: $path = rtrim($path, '/');
199: } else {
200: $r = (_isRewriteRule()) ? REQUEST_URI : route_path();
201: $path = route_updateQueryStr($r, $queryStr);
202: }
203:
204: $q = '';
205: if ($queryStr && is_array($queryStr) && count($queryStr)) {
206: foreach ($queryStr as $key => $value) {
207: if (is_array($value)) {
208: $v = array_map('urlencode', $value);
209: $value = implode('/', $v);
210: } else {
211: $value = urlencode($value);
212: }
213: if (is_numeric($key)) {
214: if ($lc_cleanURL) {
215: $q .= '/' . $value;
216: } else {
217: $q .= '&' . $value;
218: }
219: } else {
220: if ($lc_cleanURL) {
221: $q .= '/-' . $key . '/' . $value;
222: } else {
223: $q .= '&' . $key . '=' . $value;
224: }
225: }
226: }
227: }
228:
229: if (is_array($lc_sites) && array_key_exists(LC_NAMESPACE, $lc_sites)) {
230: $regex = str_replace('/', '\/', $lc_sites[LC_NAMESPACE]);
231: $regex = '/\b^(' . $regex . ') {1}\b/i';
232: $path = preg_replace($regex, LC_NAMESPACE, $path);
233: }
234:
235: # If URI contains the language code, force to include it in the URI
236: if (is_null($lc_langInURI)) {
237: $lc_langInURI = _getLangInURI();
238: }
239:
240: if (empty($lang) && $lc_langInURI) {
241: $lang = $lc_langInURI;
242: }
243:
244: $url = WEB_ROOT;
245: if ($lang && $lc_translationEnabled && !$forceExcludeLangInURL) {
246: if ($lc_cleanURL) {
247: $url .= $lang . '/';
248: } else {
249: $q .= '&lang=' . $lang;
250: }
251: }
252:
253: if (strtolower($path) == 'home') {
254: $path = '';
255: $q = ltrim($q, '/');
256: }
257:
258: if ($lc_cleanURL) {
259: $url .= $path . $q;
260: } else {
261: $url .= $path . '?' . ltrim($q, '&');
262: $url = trim($url, '?');
263: }
264:
265: $url = preg_replace('/(\s) {1,}/', '+', $url); # replace the space with "+"
266: $url = preg_replace('/\?&/', '?', $url);
267: $url = preg_replace('/&&/', '&', $url);
268:
269: return rtrim($url, '/');
270: }
271:
272: /**
273: * Update the route path with the given query string
274: *
275: * @param string $path The route path which may contain the query string
276: * @param array $queryStr Query string as
277: * array(
278: * $value1, // no key here
279: * 'key1' => $value2,
280: * 'key3' => $value3 or array($value3, $value4)
281: * )
282: * @return string The updated route path
283: */
284: function route_updateQueryStr($path, &$queryStr = array())
285: {
286: global $lc_cleanURL;
287:
288: if (is_array($queryStr) && count($queryStr)) {
289: if ($lc_cleanURL) {
290: # For clean URLs like /path/query/str/-key/value
291: foreach ($queryStr as $key => $value) {
292: $route = _arg($key, $path);
293: if ($route) {
294: if (is_string($key)) {
295: $regex = '/(\-' . $key . '\/)';
296: if (is_array($route)) {
297: $regex .= '(' . implode('\/', $route) . '+)';
298: } else {
299: $regex .= '(' . $route . '+)';
300: }
301: $regex .= '/i';
302: } elseif (is_numeric($key)) {
303: $regex = '/\b(' . $route . '){1}\b/i';
304: } else {
305: continue;
306: }
307: } else {
308: # if the key could not be retrieved from URI, skip it
309: continue;
310: }
311: if (preg_match($regex, $path)) {
312: # find the key in URI
313: if (is_array($value)) {
314: $v = array_map('urlencode', $value);
315: $value = implode('/', $v);
316: } else {
317: $value = urlencode($value);
318: }
319: if (is_numeric($key)) {
320: $path = preg_replace($regex, $value, $path); # no key
321: } else {
322: $path = preg_replace($regex, '-' . $key . '/' . $value, $path);
323: }
324: unset($queryStr[$key]); # removed the replaced query string from the array
325: }
326: }
327: } else {
328: # For unclean URLs like /path/query/str?key=value
329: parse_str($_SERVER['QUERY_STRING'], $serverQueryStr);
330: $queryStr = array_merge($serverQueryStr, $queryStr);
331: }
332: }
333:
334: return $path;
335: }
336:
337: /**
338: * Initialize a route to define
339: *
340: * @param string $name The route name that is unique to the mapped path
341: * @return object Router
342: */
343: function route($name)
344: {
345: return new Router($name);
346: }
347:
348: /**
349: * Define route group
350: *
351: * @param string $prefix A prefix for the group of the routes
352: * @param callable $callback The callback function that defines each route in the group
353: */
354: function route_group($prefix, $callback)
355: {
356: Router::group($prefix, $callback);
357: }
358:
359: /**
360: * Get the current route name
361: *
362: * @return string The route name defined in route.config.php
363: */
364: function route_name()
365: {
366: return Router::getMatchedName();
367: }
368:
369: /**
370: * Check if the current route is equal to the given uri or route name
371: *
372: * @param string $uri URI string or the route name defined in route.config.php
373: * @return boolean true if it is matched, otherwise false
374: */
375: function route_equal($uri)
376: {
377: $uri = trim($uri, '/');
378:
379: return $uri == _rr() || $uri == route_name();
380: }
381:
382: /**
383: * Check if the current route uri is started with the given uri
384: *
385: * @param string $uri URI string
386: * @param array $except Array of URI string to be excluded in check
387: * @return boolean true/false
388: */
389: function route_start($uri, array $except = array())
390: {
391: if (call_user_func_array('route_except', $except) === false) {
392: return false;
393: }
394:
395: if ($uri) {
396: $uri = trim($uri, '/');
397: }
398:
399: return $uri && stripos(_rr(), $uri) === 0;
400: }
401:
402: /**
403: * Check if the current route uri contains the given URI or list of URIs
404: *
405: * @param array|string $uri URI string or array of URI strings
406: * @param array $except Array of URI string to be excluded in check
407: * @return boolean true/false
408: */
409: function route_contain($uri, array $except = array())
410: {
411: if (call_user_func_array('route_except', $except) === false) {
412: return false;
413: }
414:
415: $args = is_array($uri) ? $uri : array($uri);
416: foreach ($args as $uri) {
417: if ($uri) {
418: $uri = trim($uri, '/');
419: }
420:
421: if (stristr(_rr(), $uri)) {
422: return true;
423: }
424: }
425:
426: return false;
427: }
428:
429: /**
430: * Check if the current route uri is in th exception list
431: *
432: * @param string $args Variable list of URI strings
433: * @return boolean true/false
434: * @since PHPLucidFrame v 3.0.0
435: */
436: function route_except()
437: {
438: $except = func_get_args();
439: if (count($except)) {
440: foreach ($except as $string) {
441: if ($string) {
442: $string = trim($string, '/');
443: }
444:
445: if (stripos(_rr(), $string) === 0 || route_name() == $string) {
446: return false;
447: }
448: }
449: }
450:
451: return true;
452: }
453: