You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

515 lines
13 KiB

7 years ago
  1. <?php
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP
  6. *
  7. * This content is released under the MIT License (MIT)
  8. *
  9. * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining a copy
  12. * of this software and associated documentation files (the "Software"), to deal
  13. * in the Software without restriction, including without limitation the rights
  14. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. * copies of the Software, and to permit persons to whom the Software is
  16. * furnished to do so, subject to the following conditions:
  17. *
  18. * The above copyright notice and this permission notice shall be included in
  19. * all copies or substantial portions of the Software.
  20. *
  21. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. * THE SOFTWARE.
  28. *
  29. * @package CodeIgniter
  30. * @author EllisLab Dev Team
  31. * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
  32. * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
  33. * @license http://opensource.org/licenses/MIT MIT License
  34. * @link https://codeigniter.com
  35. * @since Version 1.0.0
  36. * @filesource
  37. */
  38. defined('BASEPATH') OR exit('No direct script access allowed');
  39. /**
  40. * Router Class
  41. *
  42. * Parses URIs and determines routing
  43. *
  44. * @package CodeIgniter
  45. * @subpackage Libraries
  46. * @category Libraries
  47. * @author EllisLab Dev Team
  48. * @link https://codeigniter.com/user_guide/general/routing.html
  49. */
  50. class CI_Router {
  51. /**
  52. * CI_Config class object
  53. *
  54. * @var object
  55. */
  56. public $config;
  57. /**
  58. * List of routes
  59. *
  60. * @var array
  61. */
  62. public $routes = array();
  63. /**
  64. * Current class name
  65. *
  66. * @var string
  67. */
  68. public $class = '';
  69. /**
  70. * Current method name
  71. *
  72. * @var string
  73. */
  74. public $method = 'index';
  75. /**
  76. * Sub-directory that contains the requested controller class
  77. *
  78. * @var string
  79. */
  80. public $directory;
  81. /**
  82. * Default controller (and method if specific)
  83. *
  84. * @var string
  85. */
  86. public $default_controller;
  87. /**
  88. * Translate URI dashes
  89. *
  90. * Determines whether dashes in controller & method segments
  91. * should be automatically replaced by underscores.
  92. *
  93. * @var bool
  94. */
  95. public $translate_uri_dashes = FALSE;
  96. /**
  97. * Enable query strings flag
  98. *
  99. * Determines whether to use GET parameters or segment URIs
  100. *
  101. * @var bool
  102. */
  103. public $enable_query_strings = FALSE;
  104. // --------------------------------------------------------------------
  105. /**
  106. * Class constructor
  107. *
  108. * Runs the route mapping function.
  109. *
  110. * @param array $routing
  111. * @return void
  112. */
  113. public function __construct($routing = NULL)
  114. {
  115. $this->config =& load_class('Config', 'core');
  116. $this->uri =& load_class('URI', 'core');
  117. $this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE);
  118. // If a directory override is configured, it has to be set before any dynamic routing logic
  119. is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']);
  120. $this->_set_routing();
  121. // Set any routing overrides that may exist in the main index file
  122. if (is_array($routing))
  123. {
  124. empty($routing['controller']) OR $this->set_class($routing['controller']);
  125. empty($routing['function']) OR $this->set_method($routing['function']);
  126. }
  127. log_message('info', 'Router Class Initialized');
  128. }
  129. // --------------------------------------------------------------------
  130. /**
  131. * Set route mapping
  132. *
  133. * Determines what should be served based on the URI request,
  134. * as well as any "routes" that have been set in the routing config file.
  135. *
  136. * @return void
  137. */
  138. protected function _set_routing()
  139. {
  140. // Load the routes.php file. It would be great if we could
  141. // skip this for enable_query_strings = TRUE, but then
  142. // default_controller would be empty ...
  143. if (file_exists(APPPATH.'config/routes.php'))
  144. {
  145. include(APPPATH.'config/routes.php');
  146. }
  147. if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/routes.php'))
  148. {
  149. include(APPPATH.'config/'.ENVIRONMENT.'/routes.php');
  150. }
  151. // Validate & get reserved routes
  152. if (isset($route) && is_array($route))
  153. {
  154. isset($route['default_controller']) && $this->default_controller = $route['default_controller'];
  155. isset($route['translate_uri_dashes']) && $this->translate_uri_dashes = $route['translate_uri_dashes'];
  156. unset($route['default_controller'], $route['translate_uri_dashes']);
  157. $this->routes = $route;
  158. }
  159. // Are query strings enabled in the config file? Normally CI doesn't utilize query strings
  160. // since URI segments are more search-engine friendly, but they can optionally be used.
  161. // If this feature is enabled, we will gather the directory/class/method a little differently
  162. if ($this->enable_query_strings)
  163. {
  164. // If the directory is set at this time, it means an override exists, so skip the checks
  165. if ( ! isset($this->directory))
  166. {
  167. $_d = $this->config->item('directory_trigger');
  168. $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';
  169. if ($_d !== '')
  170. {
  171. $this->uri->filter_uri($_d);
  172. $this->set_directory($_d);
  173. }
  174. }
  175. $_c = trim($this->config->item('controller_trigger'));
  176. if ( ! empty($_GET[$_c]))
  177. {
  178. $this->uri->filter_uri($_GET[$_c]);
  179. $this->set_class($_GET[$_c]);
  180. $_f = trim($this->config->item('function_trigger'));
  181. if ( ! empty($_GET[$_f]))
  182. {
  183. $this->uri->filter_uri($_GET[$_f]);
  184. $this->set_method($_GET[$_f]);
  185. }
  186. $this->uri->rsegments = array(
  187. 1 => $this->class,
  188. 2 => $this->method
  189. );
  190. }
  191. else
  192. {
  193. $this->_set_default_controller();
  194. }
  195. // Routing rules don't apply to query strings and we don't need to detect
  196. // directories, so we're done here
  197. return;
  198. }
  199. // Is there anything to parse?
  200. if ($this->uri->uri_string !== '')
  201. {
  202. $this->_parse_routes();
  203. }
  204. else
  205. {
  206. $this->_set_default_controller();
  207. }
  208. }
  209. // --------------------------------------------------------------------
  210. /**
  211. * Set request route
  212. *
  213. * Takes an array of URI segments as input and sets the class/method
  214. * to be called.
  215. *
  216. * @used-by CI_Router::_parse_routes()
  217. * @param array $segments URI segments
  218. * @return void
  219. */
  220. protected function _set_request($segments = array())
  221. {
  222. $segments = $this->_validate_request($segments);
  223. // If we don't have any segments left - try the default controller;
  224. // WARNING: Directories get shifted out of the segments array!
  225. if (empty($segments))
  226. {
  227. $this->_set_default_controller();
  228. return;
  229. }
  230. if ($this->translate_uri_dashes === TRUE)
  231. {
  232. $segments[0] = str_replace('-', '_', $segments[0]);
  233. if (isset($segments[1]))
  234. {
  235. $segments[1] = str_replace('-', '_', $segments[1]);
  236. }
  237. }
  238. $this->set_class($segments[0]);
  239. if (isset($segments[1]))
  240. {
  241. $this->set_method($segments[1]);
  242. }
  243. else
  244. {
  245. $segments[1] = 'index';
  246. }
  247. array_unshift($segments, NULL);
  248. unset($segments[0]);
  249. $this->uri->rsegments = $segments;
  250. }
  251. // --------------------------------------------------------------------
  252. /**
  253. * Set default controller
  254. *
  255. * @return void
  256. */
  257. protected function _set_default_controller()
  258. {
  259. if (empty($this->default_controller))
  260. {
  261. show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.');
  262. }
  263. // Is the method being specified?
  264. if (sscanf($this->default_controller, '%[^/]/%s', $class, $method) !== 2)
  265. {
  266. $method = 'index';
  267. }
  268. if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php'))
  269. {
  270. // This will trigger 404 later
  271. return;
  272. }
  273. $this->set_class($class);
  274. $this->set_method($method);
  275. // Assign routed segments, index starting from 1
  276. $this->uri->rsegments = array(
  277. 1 => $class,
  278. 2 => $method
  279. );
  280. log_message('debug', 'No URI present. Default controller set.');
  281. }
  282. // --------------------------------------------------------------------
  283. /**
  284. * Validate request
  285. *
  286. * Attempts validate the URI request and determine the controller path.
  287. *
  288. * @used-by CI_Router::_set_request()
  289. * @param array $segments URI segments
  290. * @return mixed URI segments
  291. */
  292. protected function _validate_request($segments)
  293. {
  294. $c = count($segments);
  295. $directory_override = isset($this->directory);
  296. // Loop through our segments and return as soon as a controller
  297. // is found or when such a directory doesn't exist
  298. while ($c-- > 0)
  299. {
  300. $test = $this->directory
  301. .ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
  302. if ( ! file_exists(APPPATH.'controllers/'.$test.'.php')
  303. && $directory_override === FALSE
  304. && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0])
  305. )
  306. {
  307. $this->set_directory(array_shift($segments), TRUE);
  308. continue;
  309. }
  310. return $segments;
  311. }
  312. // This means that all segments were actually directories
  313. return $segments;
  314. }
  315. // --------------------------------------------------------------------
  316. /**
  317. * Parse Routes
  318. *
  319. * Matches any routes that may exist in the config/routes.php file
  320. * against the URI to determine if the class/method need to be remapped.
  321. *
  322. * @return void
  323. */
  324. protected function _parse_routes()
  325. {
  326. // Turn the segment array into a URI string
  327. $uri = implode('/', $this->uri->segments);
  328. // Get HTTP verb
  329. $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';
  330. // Loop through the route array looking for wildcards
  331. foreach ($this->routes as $key => $val)
  332. {
  333. // Check if route format is using HTTP verbs
  334. if (is_array($val))
  335. {
  336. $val = array_change_key_case($val, CASE_LOWER);
  337. if (isset($val[$http_verb]))
  338. {
  339. $val = $val[$http_verb];
  340. }
  341. else
  342. {
  343. continue;
  344. }
  345. }
  346. // Convert wildcards to RegEx
  347. $key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);
  348. // Does the RegEx match?
  349. if (preg_match('#^'.$key.'$#', $uri, $matches))
  350. {
  351. // Are we using callbacks to process back-references?
  352. if ( ! is_string($val) && is_callable($val))
  353. {
  354. // Remove the original string from the matches array.
  355. array_shift($matches);
  356. // Execute the callback using the values in matches as its parameters.
  357. $val = call_user_func_array($val, $matches);
  358. }
  359. // Are we using the default routing method for back-references?
  360. elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE)
  361. {
  362. $val = preg_replace('#^'.$key.'$#', $val, $uri);
  363. }
  364. $this->_set_request(explode('/', $val));
  365. return;
  366. }
  367. }
  368. // If we got this far it means we didn't encounter a
  369. // matching route so we'll set the site default route
  370. $this->_set_request(array_values($this->uri->segments));
  371. }
  372. // --------------------------------------------------------------------
  373. /**
  374. * Set class name
  375. *
  376. * @param string $class Class name
  377. * @return void
  378. */
  379. public function set_class($class)
  380. {
  381. $this->class = str_replace(array('/', '.'), '', $class);
  382. }
  383. // --------------------------------------------------------------------
  384. /**
  385. * Fetch the current class
  386. *
  387. * @deprecated 3.0.0 Read the 'class' property instead
  388. * @return string
  389. */
  390. public function fetch_class()
  391. {
  392. return $this->class;
  393. }
  394. // --------------------------------------------------------------------
  395. /**
  396. * Set method name
  397. *
  398. * @param string $method Method name
  399. * @return void
  400. */
  401. public function set_method($method)
  402. {
  403. $this->method = $method;
  404. }
  405. // --------------------------------------------------------------------
  406. /**
  407. * Fetch the current method
  408. *
  409. * @deprecated 3.0.0 Read the 'method' property instead
  410. * @return string
  411. */
  412. public function fetch_method()
  413. {
  414. return $this->method;
  415. }
  416. // --------------------------------------------------------------------
  417. /**
  418. * Set directory name
  419. *
  420. * @param string $dir Directory name
  421. * @param bool $append Whether we're appending rather than setting the full value
  422. * @return void
  423. */
  424. public function set_directory($dir, $append = FALSE)
  425. {
  426. if ($append !== TRUE OR empty($this->directory))
  427. {
  428. $this->directory = str_replace('.', '', trim($dir, '/')).'/';
  429. }
  430. else
  431. {
  432. $this->directory .= str_replace('.', '', trim($dir, '/')).'/';
  433. }
  434. }
  435. // --------------------------------------------------------------------
  436. /**
  437. * Fetch directory
  438. *
  439. * Feches the sub-directory (if any) that contains the requested
  440. * controller class.
  441. *
  442. * @deprecated 3.0.0 Read the 'directory' property instead
  443. * @return string
  444. */
  445. public function fetch_directory()
  446. {
  447. return $this->directory;
  448. }
  449. }