SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/core/Kohana.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. use karmabunny\pdb\Exceptions\QueryException;
  15. use karmabunny\pdb\Exceptions\RowMissingException;
  16. use Sprout\Controllers\BaseController;
  17. use Sprout\Helpers\Enc;
  18. use Sprout\Helpers\Inflector;
  19. use Sprout\Helpers\Pdb;
  20. use Sprout\Helpers\Register;
  21. use Sprout\Helpers\Router;
  22. use Sprout\Helpers\Sprout;
  23. use Sprout\Helpers\SubsiteSelector;
  24. use Sprout\Helpers\View;
  25.  
  26.  
  27. /**
  28.  * Provides Kohana-specific helper functions. This is where the magic happens!
  29.  *
  30.  * $Id: Kohana.php 4372 2009-05-28 17:00:34Z ixmatus $
  31.  *
  32.  * @package Core
  33.  * @author Kohana Team
  34.  * @copyright (c) 2007-2008 Kohana Team
  35.  * @license http://kohanaphp.com/license.html
  36.  */
  37. final class Kohana {
  38.  
  39. // The singleton instance of the controller
  40. public static $instance;
  41.  
  42. // Output buffering level
  43. private static $buffer_level;
  44.  
  45. // Will be set to TRUE when an exception is caught
  46. public static $has_error = FALSE;
  47.  
  48. // The final output that will displayed by Kohana
  49. public static $output = '';
  50.  
  51. // The current user agent
  52. public static $user_agent;
  53.  
  54. // The current locale
  55. public static $locale;
  56.  
  57. // Configuration
  58. private static $configuration;
  59.  
  60. // Include paths
  61. private static $include_paths;
  62.  
  63. // Cache lifetime
  64. private static $cache_lifetime;
  65.  
  66. // Internal caches and write status
  67. private static $internal_cache = array();
  68. private static $write_cache;
  69. private static $internal_cache_path;
  70.  
  71. /**
  72.   * Sets up the PHP environment. Adds error/exception handling, output
  73.   * buffering, and adds an auto-loading method for loading classes.
  74.   *
  75.   * For security, this function also destroys the $_REQUEST global variable.
  76.   * Using the proper global (GET, POST, COOKIE, etc) is inherently more secure.
  77.   * @see http://www.php.net/globals
  78.   *
  79.   * @return void
  80.   */
  81. public static function setup()
  82. {
  83. static $run;
  84.  
  85. // This function can only be run once
  86. if ($run === TRUE)
  87. return;
  88.  
  89. // Define Kohana error constant
  90. define('E_KOHANA', 42);
  91.  
  92. // Define 404 error constant
  93. define('E_PAGE_NOT_FOUND', 43);
  94.  
  95. // Define database error constant
  96. define('E_DATABASE_ERROR', 44);
  97.  
  98. // Set the directory to be used for the internal cache
  99. self::$internal_cache_path = APPPATH.'cache/';
  100.  
  101. // How long to save the internal cache, in seconds
  102. self::$cache_lifetime = 60;
  103.  
  104. // Load cached configuration and file paths
  105. //self::$internal_cache['configuration'] = self::cache('configuration');
  106. self::$internal_cache['find_file_paths'] = self::cache('find_file_paths');
  107.  
  108. // Enable cache saving
  109. Event::add('system.shutdown', array(__CLASS__, 'internalCacheSave'));
  110.  
  111. // Set the user agent
  112. self::$user_agent = trim(@$_SERVER['HTTP_USER_AGENT']);
  113.  
  114. // Start output buffering
  115. ob_start(array(__CLASS__, 'outputBuffer'));
  116.  
  117. // Save buffering level
  118. self::$buffer_level = ob_get_level();
  119.  
  120. // Auto-convert errors into exceptions
  121. set_error_handler(array('Kohana', 'errorHandler'));
  122.  
  123. // Set exception handler
  124. set_exception_handler(array('Kohana', 'exceptionHandler'));
  125.  
  126. // Send default text/html UTF-8 header
  127. header('Content-Type: text/html; charset=UTF-8');
  128.  
  129. // Check the CLI domain has been set
  130. if (! Kohana::config('config.cli_domain'))
  131. {
  132. throw new Exception('Sprout config parameter "config.cli_domain" has not been set. See the sprout development documentation for more info.');
  133. }
  134.  
  135. // Set HTTP_HOST for CLI scripts
  136. if (! isset($_SERVER['HTTP_HOST']))
  137. {
  138. if (!empty($_SERVER['PHP_S_HTTP_HOST']))
  139. {
  140. $_SERVER['HTTP_HOST'] = $_SERVER['PHP_S_HTTP_HOST'];
  141. }
  142. else
  143. {
  144. $_SERVER['HTTP_HOST'] = Kohana::config('config.cli_domain');
  145. }
  146. }
  147.  
  148. // Set SERVER_NAME if it's not set
  149. if (! isset($_SERVER['SERVER_NAME']))
  150. {
  151. $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST'];
  152. }
  153.  
  154. // Load locales
  155. $locales = self::config('locale.language');
  156.  
  157. // Make first locale UTF-8
  158. $locales[0] .= '.UTF-8';
  159.  
  160. // Set locale information
  161. self::$locale = setlocale(LC_ALL, $locales);
  162.  
  163. // Enable Kohana 404 pages
  164. Event::add('system.404', array('Kohana', 'show404'));
  165.  
  166. // Enable Kohana output handling
  167. Event::add('system.shutdown', array('Kohana', 'shutdown'));
  168.  
  169. Event::add('system.display', array('Sprout\\Helpers\\Needs', 'replacePlaceholders'));
  170. Event::add('system.display', array('Sprout\\Helpers\\SessionStats', 'trackPageView'));
  171.  
  172. // Setup is complete, prevent it from being run again
  173. $run = TRUE;
  174. }
  175.  
  176. /**
  177.   * Loads the controller and initializes it. Runs the pre_controller,
  178.   * post_controller_constructor, and post_controller events. Triggers
  179.   * a system.404 event when the route cannot be mapped to a controller.
  180.   *
  181.   * @return object instance of controller
  182.   */
  183. public static function & instance()
  184. {
  185. if (self::$instance === NULL)
  186. {
  187. try {
  188. // Start validation of the controller
  189. $class = new ReflectionClass(Router::$controller);
  190. } catch (ReflectionException $e) {
  191. // Controller does not exist
  192. Event::run('system.404');
  193. }
  194.  
  195. if ($class->isAbstract() OR (IN_PRODUCTION AND $class->getConstant('ALLOW_PRODUCTION') == FALSE))
  196. {
  197. // Controller is not allowed to run in production
  198. Event::run('system.404');
  199. }
  200.  
  201. // Initialise Sprout modules, if required
  202. $modules = Register::getModuleDirs();
  203. foreach ($modules as $mod) {
  204. if (is_readable($mod . '/sprout_load.php')) {
  205. require $mod . '/sprout_load.php';
  206. }
  207. }
  208.  
  209. // Initialise any custom non-module code
  210. if (is_readable(DOCROOT . '/skin/sprout_load.php')) {
  211. require DOCROOT . '/skin/sprout_load.php';
  212. }
  213.  
  214. // Run system.pre_controller
  215. Event::run('system.pre_controller');
  216.  
  217. // Create a new controller instance
  218. $controller = $class->newInstance();
  219.  
  220. if (!($controller instanceof BaseController)) {
  221. throw new Exception("Class doesn't extend BaseController: " . get_class($controller));
  222. }
  223.  
  224. // Controller constructor has been executed
  225. Event::run('system.post_controller_constructor');
  226.  
  227. try
  228. {
  229. // Load the controller method
  230. $method = $class->getMethod(Router::$method);
  231.  
  232. // Method exists
  233. if (Router::$method[0] === '_')
  234. {
  235. // Do not allow access to hidden methods
  236. Event::run('system.404');
  237. }
  238.  
  239. if ($method->isProtected() or $method->isPrivate())
  240. {
  241. // Do not attempt to invoke protected methods
  242. throw new ReflectionException('protected controller method');
  243. }
  244. }
  245. catch (ReflectionException $e)
  246. {
  247. }
  248.  
  249. $controller->_run(Router::$method, Router::$arguments);
  250.  
  251. // Controller method has been executed
  252. Event::run('system.post_controller');
  253. }
  254.  
  255. return self::$instance;
  256. }
  257.  
  258. /**
  259.   * Get all include paths. APPPATH is the first path, followed by module
  260.   * paths in the order they are configured.
  261.   *
  262.   * @param boolean re-process the include paths
  263.   * @return array
  264.   */
  265. public static function includePaths($process = FALSE)
  266. {
  267. if ($process === TRUE)
  268. {
  269. self::$include_paths = array();
  270.  
  271. // Sprout modules first
  272. foreach (Register::getModuleDirs() as $path)
  273. {
  274. if ($path = str_replace('\\', '/', realpath($path)))
  275. {
  276. // Add a valid path
  277. self::$include_paths[] = $path.'/';
  278. }
  279. }
  280.  
  281. // Add Sprout core next
  282. self::$include_paths[] = APPPATH;
  283. }
  284.  
  285. return self::$include_paths;
  286. }
  287.  
  288. /**
  289.   * Get a config item or group.
  290.   *
  291.   * @param string item name
  292.   * @param boolean force a forward slash (/) at the end of the item
  293.   * @param boolean is the item required?
  294.   * @return mixed
  295.   */
  296. public static function config($key, $slash = FALSE, $required = TRUE)
  297. {
  298. if (self::$configuration === NULL)
  299. {
  300. // Load core configuration
  301. self::$configuration['core'] = self::configLoad('core');
  302.  
  303. // Re-parse the include paths
  304. self::includePaths(TRUE);
  305. }
  306.  
  307. // Get the group name from the key
  308. $group = explode('.', $key, 2);
  309. $group = $group[0];
  310.  
  311. if ( ! isset(self::$configuration[$group]))
  312. {
  313. // Load the configuration group
  314. self::$configuration[$group] = self::configLoad($group, $required);
  315. }
  316.  
  317. // Get the value of the key string
  318. $value = self::keyString(self::$configuration, $key);
  319.  
  320. if ($slash === TRUE AND is_string($value) AND $value !== '')
  321. {
  322. // Force the value to end with "/"
  323. $value = rtrim($value, '/').'/';
  324. }
  325.  
  326. return $value;
  327. }
  328.  
  329. /**
  330.   * Sets a configuration item, if allowed.
  331.   *
  332.   * @param string config key string
  333.   * @param string config value
  334.   * @return boolean
  335.   */
  336. public static function configSet($key, $value)
  337. {
  338. // Do this to make sure that the config array is already loaded
  339. self::config($key);
  340.  
  341. if (substr($key, 0, 7) === 'routes.')
  342. {
  343. // Routes cannot contain sub keys due to possible dots in regex
  344. $keys = explode('.', $key, 2);
  345. }
  346. else
  347. {
  348. // Convert dot-noted key string to an array
  349. $keys = explode('.', $key);
  350. }
  351.  
  352. // Used for recursion
  353. $conf =& self::$configuration;
  354. $last = count($keys) - 1;
  355.  
  356. foreach ($keys as $i => $k)
  357. {
  358. if ($i === $last)
  359. {
  360. $conf[$k] = $value;
  361. }
  362. else
  363. {
  364. $conf =& $conf[$k];
  365. }
  366. }
  367.  
  368. return TRUE;
  369. }
  370.  
  371. /**
  372.   * Load a config file.
  373.   *
  374.   * @param string config filename, without extension
  375.   * @param boolean is the file required?
  376.   * @return array
  377.   */
  378. public static function configLoad($name, $required = TRUE)
  379. {
  380. if ($name === 'core')
  381. {
  382. // Load the application configuration file
  383. require APPPATH.'config/config.php';
  384.  
  385. if ( ! isset($config['site_domain']))
  386. {
  387. // Invalid config file
  388. die('Your Kohana application configuration file is not valid.');
  389. }
  390.  
  391. return $config;
  392. }
  393.  
  394. if (isset(self::$internal_cache['configuration'][$name]))
  395. return self::$internal_cache['configuration'][$name];
  396.  
  397. // Load matching configs
  398. $configuration = array();
  399.  
  400. if ($files = self::findFile('config', $name, $required))
  401. {
  402. foreach ($files as $file)
  403. {
  404. require $file;
  405.  
  406. if (isset($config) AND is_array($config))
  407. {
  408. // Merge in configuration
  409. $configuration = array_merge($configuration, $config);
  410. }
  411. }
  412. }
  413.  
  414. if ( ! isset(self::$write_cache['configuration']))
  415. {
  416. // Cache has changed
  417. self::$write_cache['configuration'] = TRUE;
  418. }
  419.  
  420. return self::$internal_cache['configuration'][$name] = $configuration;
  421. }
  422.  
  423. /**
  424.   * Clears a config group from the cached configuration.
  425.   *
  426.   * @param string config group
  427.   * @return void
  428.   */
  429. public static function configClear($group)
  430. {
  431. // Remove the group from config
  432. unset(self::$configuration[$group], self::$internal_cache['configuration'][$group]);
  433.  
  434. if ( ! isset(self::$write_cache['configuration']))
  435. {
  436. // Cache has changed
  437. self::$write_cache['configuration'] = TRUE;
  438. }
  439. }
  440.  
  441. /**
  442.   * Deprecated.
  443.   *
  444.   * This function has been removed, and it's signature has
  445.   * been left for compatibilty only.
  446.   */
  447. public static function log($type, $message)
  448. {
  449. }
  450.  
  451. /**
  452.   * Disable the find-files and configuration caches
  453.   * This may be required if the caching is causing problems
  454.   */
  455. public static function disableCache()
  456. {
  457. self::$cache_lifetime = 0;
  458. self::$internal_cache = [];
  459. }
  460.  
  461. /**
  462.   * Load data from a simple cache file. This should only be used internally,
  463.   * and is NOT a replacement for the Cache library.
  464.   *
  465.   * @param string unique name of cache
  466.   * @return mixed
  467.   */
  468. public static function cache($name)
  469. {
  470. if (self::$cache_lifetime > 0)
  471. {
  472. $path = self::$internal_cache_path.'kohana_'.$name;
  473.  
  474. if (is_file($path))
  475. {
  476. // Check the file modification time
  477. if ((time() - filemtime($path)) < self::$cache_lifetime)
  478. {
  479. return json_decode(file_get_contents($path), true);
  480. }
  481. else
  482. {
  483. // Cache is invalid, delete it
  484. unlink($path);
  485. }
  486. }
  487. }
  488.  
  489. // No cache found
  490. return NULL;
  491. }
  492.  
  493. /**
  494.   * Save data to a simple cache file. This should only be used internally, and
  495.   * is NOT a replacement for the Cache library.
  496.   *
  497.   * @param string cache name
  498.   * @param mixed data to cache
  499.   * @param integer expiration in seconds
  500.   * @return boolean
  501.   */
  502. public static function cacheSave($name, $data)
  503. {
  504. $path = self::$internal_cache_path.'kohana_'.$name;
  505.  
  506. if ($data === NULL)
  507. {
  508. // Delete cache
  509. return (is_file($path) and unlink($path));
  510. }
  511. else
  512. {
  513. return (bool) @file_put_contents($path, json_encode($data));
  514. }
  515. }
  516.  
  517. /**
  518.   * Kohana output handler. Called during ob_clean, ob_flush, and their variants.
  519.   *
  520.   * @param string current output buffer
  521.   * @return string
  522.   */
  523. public static function outputBuffer($output)
  524. {
  525. // Could be flushing, so send headers first
  526. if ( ! Event::hasRun('system.send_headers'))
  527. {
  528. // Run the send_headers event
  529. Event::run('system.send_headers');
  530. }
  531.  
  532. self::$output = $output;
  533.  
  534. // Set and return the final output
  535. return self::$output;
  536. }
  537.  
  538. /**
  539.   * Closes all open output buffers, either by flushing or cleaning, and stores the Kohana
  540.   * output buffer for display during shutdown.
  541.   *
  542.   * @param boolean disable to clear buffers, rather than flushing
  543.   * @return void
  544.   */
  545. public static function closeBuffers($flush = TRUE)
  546. {
  547. if (ob_get_level() >= self::$buffer_level)
  548. {
  549. // Set the close function
  550. $close = ($flush === TRUE) ? 'ob_end_flush' : 'Kohana::_obEndClean';
  551.  
  552. while (ob_get_level() > self::$buffer_level)
  553. {
  554. // Flush or clean the buffer
  555. $close();
  556. }
  557.  
  558. // Store the Kohana output buffer
  559. Kohana::_obEndClean();
  560. }
  561. }
  562.  
  563. /**
  564.   * Triggers the shutdown of Kohana by closing the output buffer, runs the system.display event.
  565.   *
  566.   * @return void
  567.   */
  568. public static function shutdown()
  569. {
  570. // Close output buffers
  571. self::closeBuffers(TRUE);
  572.  
  573. // Run the output event
  574. Event::run('system.display', self::$output);
  575.  
  576. // Render the final output
  577. self::render(self::$output);
  578. }
  579.  
  580. /**
  581.   * Inserts global Kohana variables into the generated output and prints it.
  582.   *
  583.   * @param string final output that will displayed
  584.   * @return void
  585.   */
  586. public static function render($output)
  587. {
  588. if ($level = self::config('core.output_compression') AND ini_get('output_handler') !== 'ob_gzhandler' AND (int) ini_get('zlib.output_compression') === 0)
  589. {
  590. if ($level < 1 OR $level > 9)
  591. {
  592. // Normalize the level to be an integer between 1 and 9. This
  593. // step must be done to prevent gzencode from triggering an error
  594. $level = max(1, min($level, 9));
  595. }
  596.  
  597. if (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  598. {
  599. $compress = 'gzip';
  600. }
  601. elseif (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== FALSE)
  602. {
  603. $compress = 'deflate';
  604. }
  605. }
  606.  
  607. if (isset($compress) AND $level > 0)
  608. {
  609. switch ($compress)
  610. {
  611. case 'gzip':
  612. // Compress output using gzip
  613. $output = gzencode($output, $level);
  614. break;
  615. case 'deflate':
  616. // Compress output using zlib (HTTP deflate)
  617. $output = gzdeflate($output, $level);
  618. break;
  619. }
  620.  
  621. // This header must be sent with compressed content to prevent
  622. // browser caches from breaking
  623. header('Vary: Accept-Encoding');
  624.  
  625. // Send the content encoding header
  626. header('Content-Encoding: '.$compress);
  627.  
  628. // Sending Content-Length in CGI can result in unexpected behavior
  629. if (stripos(PHP_SAPI, 'cgi') === FALSE)
  630. {
  631. header('Content-Length: '.strlen($output));
  632. }
  633. }
  634.  
  635. echo $output;
  636. }
  637.  
  638. /**
  639.   * Displays a 404 page.
  640.   *
  641.   * @throws Kohana_404_Exception
  642.   * @param string URI of page
  643.   * @param string custom template
  644.   * @return void
  645.   */
  646. public static function show404($page = FALSE)
  647. {
  648. throw new Kohana_404_Exception($page);
  649. }
  650.  
  651. /**
  652.   * Converts PHP errors into {@see ErrorException}s
  653.   *
  654.   * @param int $errno Error code
  655.   * @param string $errmsg Error message
  656.   * @param string $file
  657.   * @param int $line
  658.   * @throws ErrorException
  659.   * @return void
  660.   */
  661. public static function errorHandler($errno, $errmsg, $file, $line)
  662. {
  663. // Ignore statments prepended by @
  664. if ((error_reporting() & $errno) === 0) return;
  665.  
  666. throw new ErrorException($errmsg, 0, $errno, $file, $line);
  667. }
  668.  
  669. /**
  670.   * Log exceptions in the database
  671.   *
  672.   * @param (\Exception|\Throwable) Exception or error to log
  673.   * @return int Record ID
  674.   */
  675. public static function logException($exception)
  676. {
  677. static $insert; // PDOStatement
  678. static $delete; // PDOStatement
  679.  
  680. $conn = Pdb::getConnection();
  681.  
  682. if (!$insert) {
  683. // This table is MyISAM so this shouldn't affect transactions
  684. $table = Pdb::prefix() . 'exception_log';
  685.  
  686. $insert_q = "INSERT INTO {$table}
  687. (date_generated, class_name, message, exception_object, exception_trace, server, get_data, session)
  688. VALUES
  689. (:date, :class, :message, :exception, :trace, :server, :get, :session)";
  690. $insert = $conn->prepare($insert_q);
  691.  
  692. $delete_q = "DELETE FROM {$table} WHERE date_generated < DATE_SUB(?, INTERVAL 10 DAY)";
  693. $delete = $conn->prepare($delete_q);
  694. }
  695.  
  696. // Extract private attributes from Exception which serialize() would provide
  697. $reflect = new ReflectionClass($exception);
  698. $ex_data = [];
  699. $ignore_props = ['message', 'trace', 'string', 'previous'];
  700. $props = $reflect->getProperties();
  701. foreach ($props as $prop) {
  702. $prop_name = $prop->name;
  703. if (in_array($prop_name, $ignore_props)) continue;
  704.  
  705. $prop->setAccessible(true);
  706. $ex_data[$prop_name] = $prop->getValue($exception);
  707. }
  708.  
  709. $insert->execute([
  710. 'date' => Pdb::now(),
  711. 'class' => get_class($exception),
  712. 'message' => $exception->getMessage(),
  713. 'exception' => json_encode($ex_data),
  714. 'trace' => json_encode($exception->getTraceAsString()),
  715. 'server' => json_encode($_SERVER),
  716. 'get' => json_encode($_GET),
  717. 'session' => json_encode($_SESSION),
  718. ]);
  719. $log_id = $conn->lastInsertId();
  720. $insert->closeCursor();
  721.  
  722. $delete->execute([Pdb::now()]);
  723. $delete->closeCursor();
  724.  
  725. return $log_id;
  726. }
  727.  
  728. /**
  729.   * Exception handler.
  730.   *
  731.   * @param object exception object
  732.   * @return void
  733.   */
  734. public static function exceptionHandler($exception)
  735. {
  736. if (!$exception instanceof \Exception and !$exception instanceof \Throwable) {
  737. throw new Exception('PHP7 - Exception handler was invoked with an invalid exception object type: ' . get_class($exception));
  738. }
  739.  
  740. try {
  741. $log_id = self::logException($exception);
  742. } catch (Exception $junk) {
  743. $log_id = 0;
  744. }
  745.  
  746. try
  747. {
  748. // This is useful for hooks to determine if a page has an error
  749. self::$has_error = TRUE;
  750.  
  751. $code = $exception->getCode();
  752. $type = get_class($exception);
  753. $message = $exception->getMessage();
  754. $file = $exception->getFile();
  755. $line = $exception->getLine();
  756.  
  757. if (is_numeric($code))
  758. {
  759. $codes = self::lang('errors');
  760.  
  761. if ( ! empty($codes[$code]))
  762. {
  763. list($level, $error, $description) = $codes[$code];
  764. }
  765. else
  766. {
  767. $level = 1;
  768. $error = get_class($exception);
  769. $description = '';
  770. }
  771. }
  772. else
  773. {
  774. // Custom error message, this will never be logged
  775. $level = 5;
  776. $error = $code;
  777. $description = '';
  778. }
  779.  
  780. // For PHP errors, look up name and description from the i18n system
  781. if ($exception instanceof ErrorException) {
  782. $severify_info = self::lang('errors.' . $exception->getSeverity());
  783. if (is_array($severify_info)) {
  784. $error = $severify_info[1];
  785. $description = $severify_info[2];
  786. }
  787. }
  788.  
  789. // Remove the DOCROOT from the path, as a security precaution
  790. $file = str_replace('\\', '/', realpath($file));
  791. if (IN_PRODUCTION or @$_SERVER['SERVER_ADDR'] != @$_SERVER['REMOTE_ADDR']) {
  792. $file = preg_replace('|^'.preg_quote(DOCROOT).'|', '', $file);
  793. }
  794.  
  795. if (method_exists($exception, 'sendHeaders') AND ! headers_sent())
  796. {
  797. // Send the headers if they have not already been sent
  798. $exception->sendHeaders();
  799. }
  800.  
  801. // Scrub details for database errors on production sites.
  802. if (IN_PRODUCTION and $exception instanceof QueryException) {
  803. $line = $file = $message = '';
  804. $description = 'A database error occurred while performing the requested procedure.';
  805. }
  806.  
  807. // Close all output buffers except for Kohana
  808. while (ob_get_level() > self::$buffer_level)
  809. {
  810. }
  811.  
  812. // 404 errors are a special case - we load content from the database and put into a nice skin
  813. if ($exception instanceof Kohana_404_Exception
  814. or
  815. ($exception instanceof RowMissingException and IN_PRODUCTION)
  816. ) {
  817. header("HTTP/1.0 404 File Not Found");
  818.  
  819. if ($exception instanceof RowMissingException) {
  820. $message = 'One of the database records for the page you requested could not be found.';
  821. }
  822.  
  823. $page = new View('sprout/404_error');
  824. $page->message = $message;
  825. $page = $page->render();
  826.  
  827. $view = new View('skin/inner');
  828. $view->page_title = '404 File Not Found';
  829. $view->main_content = $page;
  830. $view->controller = '404-error';
  831. echo $view->render();
  832. }
  833. else
  834. {
  835. if (PHP_SAPI != 'cli') {
  836. header("HTTP/1.0 500 Internal Server Error");
  837. }
  838.  
  839. if ( ! IN_PRODUCTION )
  840. {
  841. $trace = $exception->getTrace();
  842. $trace = Sprout::simpleBacktrace($trace);
  843. $trace = self::backtrace($trace);
  844. }
  845.  
  846. // Load the error
  847. if (PHP_SAPI == 'cli') {
  848. require APPPATH . 'views/system/error_cli.php';
  849. } elseif (!IN_PRODUCTION) {
  850. require APPPATH . 'views/system/error_development.php';
  851. } else {
  852. // No skin defined yet? Use the default one
  853. if (! SubsiteSelector::$subsite_code) {
  854. SubsiteSelector::$subsite_code = 'default';
  855. }
  856.  
  857. // Use the skin template or fall back to a sensible default
  858. $file = DOCROOT . 'skin/' . SubsiteSelector::$subsite_code . '/exception.php';
  859. if (! file_exists($file)) {
  860. $file = APPPATH . 'views/system/error_production.php';
  861. }
  862.  
  863. require $file;
  864. }
  865. }
  866.  
  867. if ( ! Event::hasRun('system.shutdown'))
  868. {
  869. // Run the shutdown even to ensure a clean exit
  870. Event::run('system.shutdown');
  871. }
  872.  
  873. // Turn off error reporting
  874. }
  875. catch (Exception $e)
  876. {
  877. if (IN_PRODUCTION) {
  878. die('Fatal Error');
  879. } else {
  880. echo "<pre>";
  881. echo "<b>Failed to handle ", get_class($e), "</b> in ";
  882. echo $e->getFile(), ' on line ', $e->getLine(), ":\n";
  883. echo $e->getMessage(), "\n\n";
  884.  
  885. echo "<b>Route:</b> ", Router::$controller, '::';
  886. echo Router::$method;
  887. if (!empty(Router::$arguments)) {
  888. echo '(', implode(', ', Router::$arguments), ')';
  889. }
  890. echo "\n\n";
  891.  
  892. echo "<b>Trace:</b>\n";
  893. $trace = print_r($e->getTrace(), true);
  894. echo preg_replace('/\n{2,}/', "\n", $trace);
  895. }
  896. }
  897. }
  898.  
  899.  
  900. /**
  901.   * Find a resource file in a given directory. Files will be located according
  902.   * to the order of the include paths. Configuration and i18n files will be
  903.   * returned in reverse order.
  904.   *
  905.   * @throws Kohana_Exception if file is required and not found
  906.   * @param string directory to search in
  907.   * @param string filename to look for (without extension)
  908.   * @param boolean file required
  909.   * @param string file extension
  910.   * @return array if the type is config, i18n or l10n
  911.   * @return string if the file is found
  912.   * @return FALSE if the file is not found
  913.   */
  914. public static function findFile($directory, $filename, $required = FALSE, $ext = FALSE)
  915. {
  916. // NOTE: This test MUST be not be a strict comparison (===), or empty
  917. // extensions will be allowed!
  918. if ($ext == '')
  919. {
  920. // Use the default extension
  921. $ext = '.php';
  922. }
  923. else
  924. {
  925. // Add a period before the extension
  926. $ext = '.'.$ext;
  927. }
  928.  
  929. // Search path
  930. $search = $directory.'/'.$filename.$ext;
  931.  
  932. if (isset(self::$internal_cache['find_file_paths'][$search]))
  933. return self::$internal_cache['find_file_paths'][$search];
  934.  
  935. // Load include paths
  936. $paths = self::$include_paths;
  937.  
  938. // Nothing found, yet
  939. $found = NULL;
  940.  
  941. if ($directory === 'config')
  942. {
  943. array_unshift($paths, DOCROOT);
  944. array_unshift($paths, DOCROOT . 'skin/' . SubsiteSelector::$subsite_code . '/');
  945. }
  946. else if ($directory === 'views')
  947. {
  948. array_unshift($paths, DOCROOT . 'skin/' . SubsiteSelector::$subsite_code . '/');
  949. }
  950.  
  951. if ($directory === 'config' OR $directory === 'i18n')
  952. {
  953. // Search in reverse, for merging
  954. $paths = array_reverse($paths);
  955.  
  956. foreach ($paths as $path)
  957. {
  958. if (is_file($path.$search))
  959. {
  960. // A matching file has been found
  961. $found[] = $path.$search;
  962. }
  963. }
  964. }
  965. else
  966. {
  967. foreach ($paths as $path)
  968. {
  969. if (is_file($path.$search))
  970. {
  971. // A matching file has been found
  972. $found = $path.$search;
  973.  
  974. // Stop searching
  975. break;
  976. }
  977. }
  978. }
  979.  
  980. if ($found === NULL)
  981. {
  982. if ($required === TRUE)
  983. {
  984. // Directory i18n key
  985. $directory = 'core.'.Inflector::singular($directory);
  986.  
  987. // If the file is required, throw an exception
  988. throw new Kohana_Exception('core.resource_not_found', self::lang($directory), $filename);
  989. }
  990. else
  991. {
  992. // Nothing was found, return FALSE
  993. $found = FALSE;
  994. }
  995. }
  996.  
  997. if ( ! isset(self::$write_cache['find_file_paths']))
  998. {
  999. // Write cache at shutdown
  1000. self::$write_cache['find_file_paths'] = TRUE;
  1001. }
  1002.  
  1003. return self::$internal_cache['find_file_paths'][$search] = $found;
  1004. }
  1005.  
  1006. /**
  1007.   * Lists all files and directories in a resource path.
  1008.   *
  1009.   * @param string directory to search
  1010.   * @param boolean list all files to the maximum depth?
  1011.   * @param string full path to search (used for recursion, *never* set this manually)
  1012.   * @return array filenames and directories
  1013.   */
  1014. public static function listFiles($directory, $recursive = FALSE, $path = FALSE)
  1015. {
  1016. $files = array();
  1017.  
  1018. if ($path === FALSE)
  1019. {
  1020. $paths = array_reverse(self::includePaths());
  1021.  
  1022. foreach ($paths as $path)
  1023. {
  1024. // Recursively get and merge all files
  1025. $files = array_merge($files, self::listFiles($directory, $recursive, $path.$directory));
  1026. }
  1027. }
  1028. else
  1029. {
  1030. $path = rtrim($path, '/').'/';
  1031.  
  1032. if (is_readable($path))
  1033. {
  1034. $items = (array) glob($path.'*');
  1035.  
  1036. if ( ! empty($items))
  1037. {
  1038. foreach ($items as $index => $item)
  1039. {
  1040. $files[] = $item = str_replace('\\', '/', $item);
  1041.  
  1042. // Handle recursion
  1043. if (is_dir($item) AND $recursive == TRUE)
  1044. {
  1045. // Filename should only be the basename
  1046. $item = pathinfo($item, PATHINFO_BASENAME);
  1047.  
  1048. // Append sub-directory search
  1049. $files = array_merge($files, self::listFiles($directory, TRUE, $path.$item));
  1050. }
  1051. }
  1052. }
  1053. }
  1054. }
  1055.  
  1056. return $files;
  1057. }
  1058.  
  1059. /**
  1060.   * Fetch an i18n language item.
  1061.   *
  1062.   * @param string language key to fetch
  1063.   * @param array additional information to insert into the line
  1064.   * @return string i18n language string, or the requested key if the i18n item is not found
  1065.   */
  1066. public static function lang($key, $args = array())
  1067. {
  1068. // Extract the main group from the key
  1069. $group = explode('.', $key, 2);
  1070. $group = $group[0];
  1071.  
  1072. // Get locale name
  1073. $locale = self::config('locale.language.0');
  1074.  
  1075. if ( ! isset(self::$internal_cache['language'][$locale][$group]))
  1076. {
  1077. // Messages for this group
  1078. $messages = array();
  1079.  
  1080. include APPPATH . "i18n/{$locale}/{$group}.php";
  1081.  
  1082. // Merge in configuration
  1083. if (!empty($lang) AND is_array($lang)) {
  1084. foreach ($lang as $k => $v) {
  1085. $messages[$k] = $v;
  1086. }
  1087. }
  1088.  
  1089. if ( ! isset(self::$write_cache['language']))
  1090. {
  1091. // Write language cache
  1092. self::$write_cache['language'] = TRUE;
  1093. }
  1094.  
  1095. self::$internal_cache['language'][$locale][$group] = $messages;
  1096. }
  1097.  
  1098. // Get the line from cache
  1099. $line = self::keyString(self::$internal_cache['language'][$locale], $key);
  1100.  
  1101. if ($line === NULL)
  1102. {
  1103. // Return the key string as fallback
  1104. return $key;
  1105. }
  1106.  
  1107. if (is_string($line) AND func_num_args() > 1)
  1108. {
  1109. $args = array_slice(func_get_args(), 1);
  1110.  
  1111. // Add the arguments into the line
  1112. $line = vsprintf($line, is_array($args[0]) ? $args[0] : $args);
  1113. }
  1114.  
  1115. return $line;
  1116. }
  1117.  
  1118. /**
  1119.   * Returns the value of a key, defined by a 'dot-noted' string, from an array.
  1120.   *
  1121.   * @param array array to search
  1122.   * @param string dot-noted string: foo.bar.baz
  1123.   * @return string if the key is found
  1124.   * @return void if the key is not found
  1125.   */
  1126. public static function keyString($array, $keys)
  1127. {
  1128. if (empty($array))
  1129. return NULL;
  1130.  
  1131. // Prepare for loop
  1132. $keys = explode('.', $keys);
  1133.  
  1134. if (count($keys) == 2)
  1135. {
  1136. return @$array[$keys[0]][$keys[1]];
  1137. }
  1138.  
  1139. do
  1140. {
  1141. // Get the next key
  1142. $key = array_shift($keys);
  1143.  
  1144. if (isset($array[$key]))
  1145. {
  1146. if (is_array($array[$key]) AND ! empty($keys))
  1147. {
  1148. // Dig down to prepare the next loop
  1149. $array = $array[$key];
  1150. }
  1151. else
  1152. {
  1153. // Requested key was found
  1154. return $array[$key];
  1155. }
  1156. }
  1157. else
  1158. {
  1159. // Requested key is not set
  1160. break;
  1161. }
  1162. }
  1163. while ( ! empty($keys));
  1164.  
  1165. return NULL;
  1166. }
  1167.  
  1168. /**
  1169.   * Sets values in an array by using a 'dot-noted' string.
  1170.   *
  1171.   * @param array array to set keys in (reference)
  1172.   * @param string dot-noted string: foo.bar.baz
  1173.   * @return mixed fill value for the key
  1174.   * @return void
  1175.   */
  1176. public static function keyStringSet( & $array, $keys, $fill = NULL)
  1177. {
  1178. if (is_object($array) AND ($array instanceof ArrayObject))
  1179. {
  1180. // Copy the array
  1181. $array_copy = $array->getArrayCopy();
  1182.  
  1183. // Is an object
  1184. $array_object = TRUE;
  1185. }
  1186. else
  1187. {
  1188. if ( ! is_array($array))
  1189. {
  1190. // Must always be an array
  1191. $array = (array) $array;
  1192. }
  1193.  
  1194. // Copy is a reference to the array
  1195. $array_copy =& $array;
  1196. }
  1197.  
  1198. if (empty($keys))
  1199. return $array;
  1200.  
  1201. // Create keys
  1202. $keys = explode('.', $keys);
  1203.  
  1204. // Create reference to the array
  1205. $row =& $array_copy;
  1206.  
  1207. for ($i = 0, $end = count($keys) - 1; $i <= $end; $i++)
  1208. {
  1209. // Get the current key
  1210. $key = $keys[$i];
  1211.  
  1212. if ( ! isset($row[$key]))
  1213. {
  1214. if (isset($keys[$i + 1]))
  1215. {
  1216. // Make the value an array
  1217. $row[$key] = array();
  1218. }
  1219. else
  1220. {
  1221. // Add the fill key
  1222. $row[$key] = $fill;
  1223. }
  1224. }
  1225. elseif (isset($keys[$i + 1]))
  1226. {
  1227. // Make the value an array
  1228. $row[$key] = (array) $row[$key];
  1229. }
  1230.  
  1231. // Go down a level, creating a new row reference
  1232. $row =& $row[$key];
  1233. }
  1234.  
  1235. if (isset($array_object))
  1236. {
  1237. // Swap the array back in
  1238. $array->exchangeArray($array_copy);
  1239. }
  1240. }
  1241.  
  1242. /**
  1243.   * Retrieves current user agent information:
  1244.   * keys: browser, version, platform, mobile, robot, referrer, languages, charsets
  1245.   * tests: is_browser, is_mobile, is_robot, accept_lang, accept_charset
  1246.   *
  1247.   * @param string key or test name
  1248.   * @param string used with "accept" tests: userAgent(accept_lang, en)
  1249.   * @return array languages and charsets
  1250.   * @return string all other keys
  1251.   * @return boolean all tests
  1252.   */
  1253. public static function userAgent($key = 'agent', $compare = NULL)
  1254. {
  1255. static $info;
  1256.  
  1257. // Return the raw string
  1258. if ($key === 'agent')
  1259. return self::$user_agent;
  1260.  
  1261. if ($info === NULL)
  1262. {
  1263. // Parse the user agent and extract basic information
  1264. $agents = self::config('user_agents');
  1265.  
  1266. foreach ($agents as $type => $data)
  1267. {
  1268. foreach ($data as $agent => $name)
  1269. {
  1270. if (stripos(self::$user_agent, $agent) !== FALSE)
  1271. {
  1272. if ($type === 'browser' AND preg_match('|'.preg_quote($agent).'[^0-9.]*+([0-9.][0-9.a-z]*)|i', self::$user_agent, $match))
  1273. {
  1274. // Set the browser version
  1275. $info['version'] = $match[1];
  1276. }
  1277.  
  1278. // Set the agent name
  1279. $info[$type] = $name;
  1280. break;
  1281. }
  1282. }
  1283. }
  1284. }
  1285.  
  1286. if (empty($info[$key]))
  1287. {
  1288. switch ($key)
  1289. {
  1290. case 'is_robot':
  1291. case 'is_browser':
  1292. case 'is_mobile':
  1293. // A boolean result
  1294. $return = ! empty($info[substr($key, 3)]);
  1295. break;
  1296. case 'languages':
  1297. $return = array();
  1298. if ( ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
  1299. {
  1300. if (preg_match_all('/[-a-z]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])), $matches))
  1301. {
  1302. // Found a result
  1303. $return = $matches[0];
  1304. }
  1305. }
  1306. break;
  1307. case 'charsets':
  1308. $return = array();
  1309. if ( ! empty($_SERVER['HTTP_ACCEPT_CHARSET']))
  1310. {
  1311. if (preg_match_all('/[-a-z0-9]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_CHARSET'])), $matches))
  1312. {
  1313. // Found a result
  1314. $return = $matches[0];
  1315. }
  1316. }
  1317. break;
  1318. case 'referrer':
  1319. if ( ! empty($_SERVER['HTTP_REFERER']))
  1320. {
  1321. // Found a result
  1322. $return = trim($_SERVER['HTTP_REFERER']);
  1323. }
  1324. break;
  1325. }
  1326.  
  1327. // Cache the return value
  1328. isset($return) and $info[$key] = $return;
  1329. }
  1330.  
  1331. if ( ! empty($compare))
  1332. {
  1333. // The comparison must always be lowercase
  1334. $compare = strtolower($compare);
  1335.  
  1336. switch ($key)
  1337. {
  1338. case 'accept_lang':
  1339. // Check if the lange is accepted
  1340. return in_array($compare, self::userAgent('languages'));
  1341. break;
  1342. case 'accept_charset':
  1343. // Check if the charset is accepted
  1344. return in_array($compare, self::userAgent('charsets'));
  1345. break;
  1346. default:
  1347. // Invalid comparison
  1348. return FALSE;
  1349. break;
  1350. }
  1351. }
  1352.  
  1353. // Return the key, if set
  1354. return isset($info[$key]) ? $info[$key] : NULL;
  1355. }
  1356.  
  1357. /**
  1358.   * Quick debugging of any variable. Any number of parameters can be set.
  1359.   *
  1360.   * @return string
  1361.   */
  1362. public static function debug()
  1363. {
  1364. if (func_num_args() === 0)
  1365. return;
  1366.  
  1367. // Get params
  1368. $params = func_get_args();
  1369. $output = array();
  1370.  
  1371. foreach ($params as $var)
  1372. {
  1373. $output[] = '<pre>('.gettype($var).') '.Enc::html(print_r($var, TRUE)).'</pre>';
  1374. }
  1375.  
  1376. return implode("\n", $output);
  1377. }
  1378.  
  1379.  
  1380. /**
  1381.   * Displays nice backtrace information.
  1382.   * @see http://php.net/debug_backtrace
  1383.   *
  1384.   * @param array backtrace generated by an exception or debug_backtrace
  1385.   * @return string
  1386.   */
  1387. public static function backtrace($trace)
  1388. {
  1389. if ( ! is_array($trace))
  1390. return;
  1391.  
  1392. // Final output
  1393. $output = array();
  1394.  
  1395. foreach ($trace as $entry)
  1396. {
  1397. $temp = '<li>';
  1398.  
  1399. if (isset($entry['file'])) {
  1400. if (IN_PRODUCTION or @$_SERVER['SERVER_ADDR'] != @$_SERVER['REMOTE_ADDR']) {
  1401. $entry['file'] = preg_replace('!^' . preg_quote(DOCROOT) . '!', '', $entry['file']);
  1402. }
  1403.  
  1404. $temp .= self::lang('core.error_file_line', $entry['file'], $entry['line']);
  1405. }
  1406.  
  1407. $temp .= '<pre>';
  1408.  
  1409. if (isset($entry['class']))
  1410. {
  1411. // Add class and call type
  1412. $temp .= $entry['class'].$entry['type'];
  1413. }
  1414.  
  1415. // Add function
  1416. $temp .= $entry['function'].'( ';
  1417.  
  1418. // Add function args
  1419. if (isset($entry['args']) AND is_array($entry['args']))
  1420. {
  1421. // Separator starts as nothing
  1422. $sep = '';
  1423.  
  1424. while ($arg = array_shift($entry['args']))
  1425. {
  1426. if (is_string($arg))
  1427. {
  1428. $arg = Enc::cleanfunky($arg);
  1429.  
  1430. // Remove docroot from filename
  1431. if (is_file($arg))
  1432. {
  1433. $arg = preg_replace('!^'.preg_quote(DOCROOT).'!', '', $arg);
  1434. }
  1435. }
  1436.  
  1437. $temp .= $sep.'<span>'.Enc::html(print_r($arg, TRUE)).'</span>';
  1438.  
  1439. // Change separator to a comma
  1440. $sep = ', ';
  1441. }
  1442. }
  1443.  
  1444. $temp .= ' )</pre></li>';
  1445.  
  1446. $output[] = $temp;
  1447. }
  1448.  
  1449. return '<ul class="backtrace">'.implode("\n", $output).'</ul>';
  1450. }
  1451.  
  1452. /**
  1453.   * Saves the internal caches: configuration, include paths, etc.
  1454.   *
  1455.   * @return boolean
  1456.   */
  1457. public static function internalCacheSave()
  1458. {
  1459. if ( ! is_array(self::$write_cache))
  1460. return FALSE;
  1461.  
  1462. // Get internal cache names
  1463. $caches = array_keys(self::$write_cache);
  1464.  
  1465. // Nothing written
  1466. $written = FALSE;
  1467.  
  1468. // The 'sprout' config is read from different skins based on the subsite
  1469. unset(self::$internal_cache['find_file_paths']['config/sprout.php']);
  1470.  
  1471. foreach ($caches as $cache)
  1472. {
  1473. if (isset(self::$internal_cache[$cache]))
  1474. {
  1475. // Write the cache file
  1476. self::cacheSave($cache, self::$internal_cache[$cache]);
  1477.  
  1478. // A cache has been written
  1479. $written = TRUE;
  1480. }
  1481. }
  1482.  
  1483. return $written;
  1484. }
  1485.  
  1486. /**
  1487.   * Ends the current output buffer with callback in mind
  1488.   * PHP doesn't pass the output to the callback defined in ob_start() since 5.4
  1489.   *
  1490.   * @param callback $callback
  1491.   * @return boolean
  1492.   */
  1493. protected static function _obEndClean($callback = NULL)
  1494. {
  1495. // Pre-5.4 ob_end_clean() will pass the buffer to the callback anyways
  1496. if (version_compare(PHP_VERSION, '5.4', '<'))
  1497. return ob_end_clean();
  1498.  
  1499. $output = ob_get_contents();
  1500.  
  1501. if ($callback === NULL)
  1502. {
  1503. $hdlrs = ob_list_handlers();
  1504. $callback = $hdlrs[ob_get_level() - 1];
  1505. }
  1506.  
  1507. return is_callable($callback)
  1508. ? ob_end_clean() AND call_user_func($callback, $output)
  1509. }
  1510.  
  1511. } // End Kohana
  1512.  
  1513. /**
  1514.  * Creates a generic i18n exception.
  1515.  */
  1516. class Kohana_Exception extends Exception
  1517. {
  1518.  
  1519. // Header
  1520. protected $header = FALSE;
  1521.  
  1522. // Error code
  1523. protected $code = E_KOHANA;
  1524.  
  1525. /**
  1526.   * Set exception message.
  1527.   *
  1528.   * @param string i18n language key for the message
  1529.   * @param array addition line parameters
  1530.   */
  1531. public function __construct($error)
  1532. {
  1533. $args = array_slice(func_get_args(), 1);
  1534.  
  1535. // Fetch the error message
  1536. $message = Kohana::lang($error, $args);
  1537.  
  1538. if ($message === $error OR empty($message))
  1539. {
  1540. // Unable to locate the message for the error
  1541. $message = 'Unknown Exception: '.$error;
  1542. }
  1543.  
  1544. // Sets $this->message the proper way
  1545. parent::__construct($message);
  1546. }
  1547.  
  1548. /**
  1549.   * Magic method for converting an object to a string.
  1550.   *
  1551.   * @return string i18n message
  1552.   */
  1553. public function __toString()
  1554. {
  1555. return (string) $this->message;
  1556. }
  1557.  
  1558. /**
  1559.   * Fetch the template name.
  1560.   *
  1561.   * @return string
  1562.   */
  1563. public function getTemplate()
  1564. {
  1565. return $this->template;
  1566. }
  1567.  
  1568. /**
  1569.   * Sends an Internal Server Error header.
  1570.   *
  1571.   * @return void
  1572.   */
  1573. public function sendHeaders()
  1574. {
  1575. // Send the 500 header
  1576. header('HTTP/1.1 500 Internal Server Error');
  1577. }
  1578.  
  1579. } // End Kohana Exception
  1580.  
  1581. /**
  1582.  * Creates a custom exception.
  1583.  */
  1584. class Kohana_User_Exception extends Kohana_Exception
  1585. {
  1586.  
  1587. /**
  1588.   * Set exception title and message.
  1589.   *
  1590.   * @param string exception title string
  1591.   * @param string exception message string
  1592.   * @param string custom error template
  1593.   */
  1594. public function __construct($title, $message)
  1595. {
  1596. Exception::__construct($message);
  1597.  
  1598. $this->code = $title;
  1599. }
  1600.  
  1601. } // End Kohana PHP Exception
  1602.  
  1603. /**
  1604.  * Creates a Page Not Found exception.
  1605.  */
  1606. class Kohana_404_Exception extends Kohana_Exception
  1607. {
  1608.  
  1609. protected $code = E_PAGE_NOT_FOUND;
  1610.  
  1611. /**
  1612.   * Set internal properties.
  1613.   *
  1614.   * @param string URL of page
  1615.   * @param string custom error template
  1616.   */
  1617. public function __construct($page = FALSE)
  1618. {
  1619. if ($page === FALSE)
  1620. {
  1621. // Construct the page URI using Router properties
  1622. $page = Router::$current_uri.Router::$url_suffix.Router::$query_string;
  1623. }
  1624.  
  1625. Exception::__construct(Kohana::lang('core.page_not_found', $page));
  1626. }
  1627.  
  1628. /**
  1629.   * Sends "File Not Found" headers, to emulate server behavior.
  1630.   *
  1631.   * @return void
  1632.   */
  1633. public function sendHeaders()
  1634. {
  1635. // Send the 404 header
  1636. header('HTTP/1.1 404 File Not Found');
  1637. }
  1638.  
  1639. } // End Kohana 404 Exception
  1640.  
  1641.  
  1642. /**
  1643. * Recursive version of array_map function
  1644. * I would imagine would be much slower than the original. Only use if necessary
  1645. *
  1646. * @param mixed $callback Callback function, can be string, array or in PHP 5.3, a function
  1647. * @param array $array The array to process
  1648. **/
  1649. function arrayMapRecursive($callback, $array) {
  1650. $keys = array_keys($array);
  1651. foreach ($keys as $k)
  1652. {
  1653. $array[$k] = is_array($array[$k]) ? arrayMapRecursive($callback, $array[$k]) : call_user_func($callback, $array[$k]);
  1654. }
  1655. return $array;
  1656. }
  1657.