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 - 2018, 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 - 2018, 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 */ 38defined('BASEPATH') OR exit('No direct script access allowed'); 39 40/** 41 * Router Class 42 * 43 * Parses URIs and determines routing 44 * 45 * @package CodeIgniter 46 * @subpackage Libraries 47 * @category Libraries 48 * @author EllisLab Dev Team 49 * @link https://codeigniter.com/user_guide/general/routing.html 50 */ 51class CI_Router { 52 53 /** 54 * CI_Config class object 55 * 56 * @var object 57 */ 58 public $config; 59 60 /** 61 * List of routes 62 * 63 * @var array 64 */ 65 public $routes = array(); 66 67 /** 68 * Current class name 69 * 70 * @var string 71 */ 72 public $class = ''; 73 74 /** 75 * Current method name 76 * 77 * @var string 78 */ 79 public $method = 'index'; 80 81 /** 82 * Sub-directory that contains the requested controller class 83 * 84 * @var string 85 */ 86 public $directory; 87 88 /** 89 * Default controller (and method if specific) 90 * 91 * @var string 92 */ 93 public $default_controller; 94 95 /** 96 * Translate URI dashes 97 * 98 * Determines whether dashes in controller & method segments 99 * should be automatically replaced by underscores. 100 * 101 * @var bool 102 */ 103 public $translate_uri_dashes = FALSE; 104 105 /** 106 * Enable query strings flag 107 * 108 * Determines whether to use GET parameters or segment URIs 109 * 110 * @var bool 111 */ 112 public $enable_query_strings = FALSE; 113 114 // -------------------------------------------------------------------- 115 116 /** 117 * Class constructor 118 * 119 * Runs the route mapping function. 120 * 121 * @param array $routing 122 * @return void 123 */ 124 public function __construct($routing = NULL) 125 { 126 $this->config =& load_class('Config', 'core'); 127 $this->uri =& load_class('URI', 'core'); 128 129 $this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE); 130 131 // If a directory override is configured, it has to be set before any dynamic routing logic 132 is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']); 133 $this->_set_routing(); 134 135 // Set any routing overrides that may exist in the main index file 136 if (is_array($routing)) 137 { 138 empty($routing['controller']) OR $this->set_class($routing['controller']); 139 empty($routing['function']) OR $this->set_method($routing['function']); 140 } 141 142 log_message('info', 'Router Class Initialized'); 143 } 144 145 // -------------------------------------------------------------------- 146 147 /** 148 * Set route mapping 149 * 150 * Determines what should be served based on the URI request, 151 * as well as any "routes" that have been set in the routing config file. 152 * 153 * @return void 154 */ 155 protected function _set_routing() 156 { 157 // Load the routes.php file. It would be great if we could 158 // skip this for enable_query_strings = TRUE, but then 159 // default_controller would be empty ... 160 if (file_exists(APPPATH.'config/routes.php')) 161 { 162 include(APPPATH.'config/routes.php'); 163 } 164 165 if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/routes.php')) 166 { 167 include(APPPATH.'config/'.ENVIRONMENT.'/routes.php'); 168 } 169 170 // Validate & get reserved routes 171 if (isset($route) && is_array($route)) 172 { 173 isset($route['default_controller']) && $this->default_controller = $route['default_controller']; 174 isset($route['translate_uri_dashes']) && $this->translate_uri_dashes = $route['translate_uri_dashes']; 175 unset($route['default_controller'], $route['translate_uri_dashes']); 176 $this->routes = $route; 177 } 178 179 // Are query strings enabled in the config file? Normally CI doesn't utilize query strings 180 // since URI segments are more search-engine friendly, but they can optionally be used. 181 // If this feature is enabled, we will gather the directory/class/method a little differently 182 if ($this->enable_query_strings) 183 { 184 // If the directory is set at this time, it means an override exists, so skip the checks 185 if ( ! isset($this->directory)) 186 { 187 $_d = $this->config->item('directory_trigger'); 188 $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : ''; 189 190 if ($_d !== '') 191 { 192 $this->uri->filter_uri($_d); 193 $this->set_directory($_d); 194 } 195 } 196 197 $_c = trim($this->config->item('controller_trigger')); 198 if ( ! empty($_GET[$_c])) 199 { 200 $this->uri->filter_uri($_GET[$_c]); 201 $this->set_class($_GET[$_c]); 202 203 $_f = trim($this->config->item('function_trigger')); 204 if ( ! empty($_GET[$_f])) 205 { 206 $this->uri->filter_uri($_GET[$_f]); 207 $this->set_method($_GET[$_f]); 208 } 209 210 $this->uri->rsegments = array( 211 1 => $this->class, 212 2 => $this->method 213 ); 214 } 215 else 216 { 217 $this->_set_default_controller(); 218 } 219 220 // Routing rules don't apply to query strings and we don't need to detect 221 // directories, so we're done here 222 return; 223 } 224 225 // Is there anything to parse? 226 if ($this->uri->uri_string !== '') 227 { 228 $this->_parse_routes(); 229 } 230 else 231 { 232 $this->_set_default_controller(); 233 } 234 } 235 236 // -------------------------------------------------------------------- 237 238 /** 239 * Set request route 240 * 241 * Takes an array of URI segments as input and sets the class/method 242 * to be called. 243 * 244 * @used-by CI_Router::_parse_routes() 245 * @param array $segments URI segments 246 * @return void 247 */ 248 protected function _set_request($segments = array()) 249 { 250 $segments = $this->_validate_request($segments); 251 // If we don't have any segments left - try the default controller; 252 // WARNING: Directories get shifted out of the segments array! 253 if (empty($segments)) 254 { 255 $this->_set_default_controller(); 256 return; 257 } 258 259 if ($this->translate_uri_dashes === TRUE) 260 { 261 $segments[0] = str_replace('-', '_', $segments[0]); 262 if (isset($segments[1])) 263 { 264 $segments[1] = str_replace('-', '_', $segments[1]); 265 } 266 } 267 268 $this->set_class($segments[0]); 269 if (isset($segments[1])) 270 { 271 $this->set_method($segments[1]); 272 } 273 else 274 { 275 $segments[1] = 'index'; 276 } 277 278 array_unshift($segments, NULL); 279 unset($segments[0]); 280 $this->uri->rsegments = $segments; 281 } 282 283 // -------------------------------------------------------------------- 284 285 /** 286 * Set default controller 287 * 288 * @return void 289 */ 290 protected function _set_default_controller() 291 { 292 if (empty($this->default_controller)) 293 { 294 show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.'); 295 } 296 297 // Is the method being specified? 298 if (sscanf($this->default_controller, '%[^/]/%s', $class, $method) !== 2) 299 { 300 $method = 'index'; 301 } 302 303 if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php')) 304 { 305 // This will trigger 404 later 306 return; 307 } 308 309 $this->set_class($class); 310 $this->set_method($method); 311 312 // Assign routed segments, index starting from 1 313 $this->uri->rsegments = array( 314 1 => $class, 315 2 => $method 316 ); 317 318 log_message('debug', 'No URI present. Default controller set.'); 319 } 320 321 // -------------------------------------------------------------------- 322 323 /** 324 * Validate request 325 * 326 * Attempts validate the URI request and determine the controller path. 327 * 328 * @used-by CI_Router::_set_request() 329 * @param array $segments URI segments 330 * @return mixed URI segments 331 */ 332 protected function _validate_request($segments) 333 { 334 $c = count($segments); 335 $directory_override = isset($this->directory); 336 337 // Loop through our segments and return as soon as a controller 338 // is found or when such a directory doesn't exist 339 while ($c-- > 0) 340 { 341 $test = $this->directory 342 .ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]); 343 344 if ( ! file_exists(APPPATH.'controllers/'.$test.'.php') 345 && $directory_override === FALSE 346 && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0]) 347 ) 348 { 349 $this->set_directory(array_shift($segments), TRUE); 350 continue; 351 } 352 353 return $segments; 354 } 355 356 // This means that all segments were actually directories 357 return $segments; 358 } 359 360 // -------------------------------------------------------------------- 361 362 /** 363 * Parse Routes 364 * 365 * Matches any routes that may exist in the config/routes.php file 366 * against the URI to determine if the class/method need to be remapped. 367 * 368 * @return void 369 */ 370 protected function _parse_routes() 371 { 372 // Turn the segment array into a URI string 373 $uri = implode('/', $this->uri->segments); 374 375 // Get HTTP verb 376 $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli'; 377 378 // Loop through the route array looking for wildcards 379 foreach ($this->routes as $key => $val) 380 { 381 // Check if route format is using HTTP verbs 382 if (is_array($val)) 383 { 384 $val = array_change_key_case($val, CASE_LOWER); 385 if (isset($val[$http_verb])) 386 { 387 $val = $val[$http_verb]; 388 } 389 else 390 { 391 continue; 392 } 393 } 394 395 // Convert wildcards to RegEx 396 $key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key); 397 398 // Does the RegEx match? 399 if (preg_match('#^'.$key.'$#', $uri, $matches)) 400 { 401 // Are we using callbacks to process back-references? 402 if ( ! is_string($val) && is_callable($val)) 403 { 404 // Remove the original string from the matches array. 405 array_shift($matches); 406 407 // Execute the callback using the values in matches as its parameters. 408 $val = call_user_func_array($val, $matches); 409 } 410 // Are we using the default routing method for back-references? 411 elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE) 412 { 413 $val = preg_replace('#^'.$key.'$#', $val, $uri); 414 } 415 416 $this->_set_request(explode('/', $val)); 417 return; 418 } 419 } 420 421 // If we got this far it means we didn't encounter a 422 // matching route so we'll set the site default route 423 $this->_set_request(array_values($this->uri->segments)); 424 } 425 426 // -------------------------------------------------------------------- 427 428 /** 429 * Set class name 430 * 431 * @param string $class Class name 432 * @return void 433 */ 434 public function set_class($class) 435 { 436 $this->class = str_replace(array('/', '.'), '', $class); 437 } 438 439 // -------------------------------------------------------------------- 440 441 /** 442 * Fetch the current class 443 * 444 * @deprecated 3.0.0 Read the 'class' property instead 445 * @return string 446 */ 447 public function fetch_class() 448 { 449 return $this->class; 450 } 451 452 // -------------------------------------------------------------------- 453 454 /** 455 * Set method name 456 * 457 * @param string $method Method name 458 * @return void 459 */ 460 public function set_method($method) 461 { 462 $this->method = $method; 463 } 464 465 // -------------------------------------------------------------------- 466 467 /** 468 * Fetch the current method 469 * 470 * @deprecated 3.0.0 Read the 'method' property instead 471 * @return string 472 */ 473 public function fetch_method() 474 { 475 return $this->method; 476 } 477 478 // -------------------------------------------------------------------- 479 480 /** 481 * Set directory name 482 * 483 * @param string $dir Directory name 484 * @param bool $append Whether we're appending rather than setting the full value 485 * @return void 486 */ 487 public function set_directory($dir, $append = FALSE) 488 { 489 if ($append !== TRUE OR empty($this->directory)) 490 { 491 $this->directory = str_replace('.', '', trim($dir, '/')).'/'; 492 } 493 else 494 { 495 $this->directory .= str_replace('.', '', trim($dir, '/')).'/'; 496 } 497 } 498 499 // -------------------------------------------------------------------- 500 501 /** 502 * Fetch directory 503 * 504 * Feches the sub-directory (if any) that contains the requested 505 * controller class. 506 * 507 * @deprecated 3.0.0 Read the 'directory' property instead 508 * @return string 509 */ 510 public function fetch_directory() 511 { 512 return $this->directory; 513 } 514 515} 516