PageRenderTime 29ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/system/core/Kohana.php

https://github.com/yamamoto123/Ushahidi_Web
PHP | 1736 lines | 1180 code | 193 blank | 363 comment | 79 complexity | 84349dadd6f0427f58378227f9a01069 MD5 | raw file
  1. <?php defined('SYSPATH') OR die('No direct access allowed.');
  2. /**
  3. * Provides Kohana-specific helper functions. This is where the magic happens!
  4. *
  5. * $Id: Kohana.php 3917 2009-01-21 03:06:22Z zombor $
  6. *
  7. * @package Core
  8. * @author Kohana Team
  9. * @copyright (c) 2007-2008 Kohana Team
  10. * @license http://kohanaphp.com/license.html
  11. */
  12. final class Kohana {
  13. // The singleton instance of the controller
  14. public static $instance;
  15. // Output buffering level
  16. private static $buffer_level;
  17. // Will be set to TRUE when an exception is caught
  18. public static $has_error = FALSE;
  19. // The final output that will displayed by Kohana
  20. public static $output = '';
  21. // The current user agent
  22. public static $user_agent;
  23. // The current locale
  24. public static $locale;
  25. // Configuration
  26. private static $configuration;
  27. // Include paths
  28. private static $include_paths;
  29. // Logged messages
  30. private static $log;
  31. // Cache lifetime
  32. private static $cache_lifetime;
  33. // Log levels
  34. private static $log_levels = array
  35. (
  36. 'error' => 1,
  37. 'alert' => 2,
  38. 'info' => 3,
  39. 'debug' => 4,
  40. );
  41. // Internal caches and write status
  42. private static $internal_cache = array();
  43. private static $write_cache;
  44. /**
  45. * Sets up the PHP environment. Adds error/exception handling, output
  46. * buffering, and adds an auto-loading method for loading classes.
  47. *
  48. * This method is run immediately when this file is loaded, and is
  49. * benchmarked as environment_setup.
  50. *
  51. * For security, this function also destroys the $_REQUEST global variable.
  52. * Using the proper global (GET, POST, COOKIE, etc) is inherently more secure.
  53. * The recommended way to fetch a global variable is using the Input library.
  54. * @see http://www.php.net/globals
  55. *
  56. * @return void
  57. */
  58. public static function setup()
  59. {
  60. static $run;
  61. // This function can only be run once
  62. if ($run === TRUE)
  63. return;
  64. // Start the environment setup benchmark
  65. Benchmark::start(SYSTEM_BENCHMARK.'_environment_setup');
  66. // Define Kohana error constant
  67. define('E_KOHANA', 42);
  68. // Define 404 error constant
  69. define('E_PAGE_NOT_FOUND', 43);
  70. // Define database error constant
  71. define('E_DATABASE_ERROR', 44);
  72. if (self::$cache_lifetime = self::config('core.internal_cache'))
  73. {
  74. // Load cached configuration and language files
  75. self::$internal_cache['configuration'] = self::cache('configuration', self::$cache_lifetime);
  76. self::$internal_cache['language'] = self::cache('language', self::$cache_lifetime);
  77. // Load cached file paths
  78. self::$internal_cache['find_file_paths'] = self::cache('find_file_paths', self::$cache_lifetime);
  79. // Enable cache saving
  80. Event::add('system.shutdown', array(__CLASS__, 'internal_cache_save'));
  81. }
  82. // Disable notices and "strict" errors
  83. $ER = error_reporting(~E_NOTICE & ~E_STRICT);
  84. // Set the user agent
  85. self::$user_agent = trim($_SERVER['HTTP_USER_AGENT']);
  86. if (function_exists('date_default_timezone_set'))
  87. {
  88. $timezone = self::config('locale.timezone');
  89. // Set default timezone, due to increased validation of date settings
  90. // which cause massive amounts of E_NOTICEs to be generated in PHP 5.2+
  91. date_default_timezone_set(empty($timezone) ? date_default_timezone_get() : $timezone);
  92. }
  93. // Restore error reporting
  94. error_reporting($ER);
  95. // Start output buffering
  96. ob_start(array(__CLASS__, 'output_buffer'));
  97. // Save buffering level
  98. self::$buffer_level = ob_get_level();
  99. // Set autoloader
  100. spl_autoload_register(array('Kohana', 'auto_load'));
  101. // Set error handler
  102. set_error_handler(array('Kohana', 'exception_handler'));
  103. // Set exception handler
  104. set_exception_handler(array('Kohana', 'exception_handler'));
  105. // Send default text/html UTF-8 header
  106. header('Content-Type: text/html; charset=UTF-8');
  107. // Load locales
  108. $locales = self::config('locale.language');
  109. // Make first locale UTF-8
  110. $locales[0] .= '.UTF-8';
  111. // Set locale information
  112. self::$locale = setlocale(LC_ALL, $locales);
  113. if (self::$configuration['core']['log_threshold'] > 0)
  114. {
  115. // Set the log directory
  116. self::log_directory(self::$configuration['core']['log_directory']);
  117. // Enable log writing at shutdown
  118. register_shutdown_function(array(__CLASS__, 'log_save'));
  119. }
  120. // Enable Kohana routing
  121. Event::add('system.routing', array('Router', 'find_uri'));
  122. Event::add('system.routing', array('Router', 'setup'));
  123. // Enable Kohana controller initialization
  124. Event::add('system.execute', array('Kohana', 'instance'));
  125. // Enable Kohana 404 pages
  126. Event::add('system.404', array('Kohana', 'show_404'));
  127. // Enable Kohana output handling
  128. Event::add('system.shutdown', array('Kohana', 'shutdown'));
  129. if (self::config('core.enable_hooks') === TRUE)
  130. {
  131. // Find all the hook files
  132. $hooks = self::list_files('hooks', TRUE);
  133. foreach ($hooks as $file)
  134. {
  135. // Load the hook
  136. include $file;
  137. }
  138. }
  139. // Setup is complete, prevent it from being run again
  140. $run = TRUE;
  141. // Stop the environment setup routine
  142. Benchmark::stop(SYSTEM_BENCHMARK.'_environment_setup');
  143. }
  144. /**
  145. * Loads the controller and initializes it. Runs the pre_controller,
  146. * post_controller_constructor, and post_controller events. Triggers
  147. * a system.404 event when the route cannot be mapped to a controller.
  148. *
  149. * This method is benchmarked as controller_setup and controller_execution.
  150. *
  151. * @return object instance of controller
  152. */
  153. public static function & instance()
  154. {
  155. if (self::$instance === NULL)
  156. {
  157. Benchmark::start(SYSTEM_BENCHMARK.'_controller_setup');
  158. if (Router::$method[0] === '_')
  159. {
  160. // Do not allow access to hidden methods
  161. Event::run('system.404');
  162. }
  163. // Include the Controller file
  164. require Router::$controller_path;
  165. try
  166. {
  167. // Start validation of the controller
  168. $class = new ReflectionClass(ucfirst(Router::$controller).'_Controller');
  169. }
  170. catch (ReflectionException $e)
  171. {
  172. // Controller does not exist
  173. Event::run('system.404');
  174. }
  175. if ($class->isAbstract() OR (IN_PRODUCTION AND $class->getConstant('ALLOW_PRODUCTION') == FALSE))
  176. {
  177. // Controller is not allowed to run in production
  178. Event::run('system.404');
  179. }
  180. // Run system.pre_controller
  181. Event::run('system.pre_controller');
  182. // Begin benchmark for the controller
  183. Benchmark::f_start(ucfirst(Router::$controller).'_Controller');
  184. // Create a new controller instance
  185. $controller = $class->newInstance();
  186. // End benchmark for the controller
  187. Benchmark::f_stop(ucfirst(Router::$controller).'_Controller');
  188. // Controller constructor has been executed
  189. Event::run('system.post_controller_constructor');
  190. try
  191. {
  192. // Load the controller method
  193. $method = $class->getMethod(Router::$method);
  194. if ($method->isProtected() or $method->isPrivate())
  195. {
  196. // Do not attempt to invoke protected methods
  197. throw new ReflectionException('protected controller method');
  198. }
  199. // Default arguments
  200. $arguments = Router::$arguments;
  201. }
  202. catch (ReflectionException $e)
  203. {
  204. // Use __call instead
  205. $method = $class->getMethod('__call');
  206. // Use arguments in __call format
  207. $arguments = array(Router::$method, Router::$arguments);
  208. }
  209. // Stop the controller setup benchmark
  210. Benchmark::stop(SYSTEM_BENCHMARK.'_controller_setup');
  211. // Start the controller execution benchmark
  212. Benchmark::start(SYSTEM_BENCHMARK.'_controller_execution');
  213. // Execute the controller method
  214. $method->invokeArgs($controller, $arguments);
  215. // Controller method has been executed
  216. Event::run('system.post_controller');
  217. // Stop the controller execution benchmark
  218. Benchmark::stop(SYSTEM_BENCHMARK.'_controller_execution');
  219. }
  220. return self::$instance;
  221. }
  222. /**
  223. * Get all include paths. APPPATH is the first path, followed by module
  224. * paths in the order they are configured, follow by the SYSPATH.
  225. *
  226. * @param boolean re-process the include paths
  227. * @return array
  228. */
  229. public static function include_paths($process = FALSE)
  230. {
  231. if ($process === TRUE)
  232. {
  233. // Add APPPATH as the first path
  234. self::$include_paths = array(APPPATH);
  235. foreach (self::$configuration['core']['modules'] as $path)
  236. {
  237. if ($path = str_replace('\\', '/', realpath($path)))
  238. {
  239. // Add a valid path
  240. self::$include_paths[] = $path.'/';
  241. }
  242. }
  243. // Add SYSPATH as the last path
  244. self::$include_paths[] = SYSPATH;
  245. }
  246. return self::$include_paths;
  247. }
  248. /**
  249. * Get a config item or group.
  250. *
  251. * @param string item name
  252. * @param boolean force a forward slash (/) at the end of the item
  253. * @param boolean is the item required?
  254. * @return mixed
  255. */
  256. public static function config($key, $slash = FALSE, $required = TRUE)
  257. {
  258. if (self::$configuration === NULL)
  259. {
  260. // Load core configuration
  261. self::$configuration['core'] = self::config_load('core');
  262. // Re-parse the include paths
  263. self::include_paths(TRUE);
  264. }
  265. // Get the group name from the key
  266. $group = explode('.', $key, 2);
  267. $group = $group[0];
  268. if ( ! isset(self::$configuration[$group]))
  269. {
  270. // Load the configuration group
  271. self::$configuration[$group] = self::config_load($group, $required);
  272. }
  273. // Get the value of the key string
  274. $value = self::key_string(self::$configuration, $key);
  275. if ($slash === TRUE AND is_string($value) AND $value !== '')
  276. {
  277. // Force the value to end with "/"
  278. $value = rtrim($value, '/').'/';
  279. }
  280. return $value;
  281. }
  282. /**
  283. * Sets a configuration item, if allowed.
  284. *
  285. * @param string config key string
  286. * @param string config value
  287. * @return boolean
  288. */
  289. public static function config_set($key, $value)
  290. {
  291. // Do this to make sure that the config array is already loaded
  292. self::config($key);
  293. if (substr($key, 0, 7) === 'routes.')
  294. {
  295. // Routes cannot contain sub keys due to possible dots in regex
  296. $keys = explode('.', $key, 2);
  297. }
  298. else
  299. {
  300. // Convert dot-noted key string to an array
  301. $keys = explode('.', $key);
  302. }
  303. // Used for recursion
  304. $conf =& self::$configuration;
  305. $last = count($keys) - 1;
  306. foreach ($keys as $i => $k)
  307. {
  308. if ($i === $last)
  309. {
  310. $conf[$k] = $value;
  311. }
  312. else
  313. {
  314. $conf =& $conf[$k];
  315. }
  316. }
  317. if ($key === 'core.modules')
  318. {
  319. // Reprocess the include paths
  320. self::include_paths(TRUE);
  321. }
  322. return TRUE;
  323. }
  324. /**
  325. * Load a config file.
  326. *
  327. * @param string config filename, without extension
  328. * @param boolean is the file required?
  329. * @return array
  330. */
  331. public static function config_load($name, $required = TRUE)
  332. {
  333. if ($name === 'core')
  334. {
  335. // Load the application configuration file
  336. require APPPATH.'config/config'.EXT;
  337. if ( ! isset($config['site_domain']))
  338. {
  339. // Invalid config file
  340. die('Your Kohana application configuration file is not valid.');
  341. }
  342. return $config;
  343. }
  344. if (isset(self::$internal_cache['configuration'][$name]))
  345. return self::$internal_cache['configuration'][$name];
  346. // Load matching configs
  347. $configuration = array();
  348. if ($files = self::find_file('config', $name, $required))
  349. {
  350. foreach ($files as $file)
  351. {
  352. require $file;
  353. if (isset($config) AND is_array($config))
  354. {
  355. // Merge in configuration
  356. $configuration = array_merge($configuration, $config);
  357. }
  358. }
  359. }
  360. if ( ! isset(self::$write_cache['configuration']))
  361. {
  362. // Cache has changed
  363. self::$write_cache['configuration'] = TRUE;
  364. }
  365. return self::$internal_cache['configuration'][$name] = $configuration;
  366. }
  367. /**
  368. * Clears a config group from the cached configuration.
  369. *
  370. * @param string config group
  371. * @return void
  372. */
  373. public static function config_clear($group)
  374. {
  375. // Remove the group from config
  376. unset(self::$configuration[$group], self::$internal_cache['configuration'][$group]);
  377. if ( ! isset(self::$write_cache['configuration']))
  378. {
  379. // Cache has changed
  380. self::$write_cache['configuration'] = TRUE;
  381. }
  382. }
  383. /**
  384. * Add a new message to the log.
  385. *
  386. * @param string type of message
  387. * @param string message text
  388. * @return void
  389. */
  390. public static function log($type, $message)
  391. {
  392. if (self::$log_levels[$type] <= self::$configuration['core']['log_threshold'])
  393. {
  394. $message = array(date('Y-m-d H:i:s P'), $type, $message);
  395. // Run the system.log event
  396. Event::run('system.log', $message);
  397. self::$log[] = $message;
  398. }
  399. }
  400. /**
  401. * Save all currently logged messages.
  402. *
  403. * @return void
  404. */
  405. public static function log_save()
  406. {
  407. if (empty(self::$log) OR self::$configuration['core']['log_threshold'] < 1)
  408. return;
  409. // Filename of the log
  410. $filename = self::log_directory().date('Y-m-d').'.log'.EXT;
  411. if ( ! is_file($filename))
  412. {
  413. // Write the SYSPATH checking header
  414. file_put_contents($filename,
  415. '<?php defined(\'SYSPATH\') or die(\'No direct script access.\'); ?>'.PHP_EOL.PHP_EOL);
  416. // Prevent external writes
  417. chmod($filename, 0644);
  418. }
  419. // Messages to write
  420. $messages = array();
  421. do
  422. {
  423. // Load the next mess
  424. list ($date, $type, $text) = array_shift(self::$log);
  425. // Add a new message line
  426. $messages[] = $date.' --- '.$type.': '.$text;
  427. }
  428. while ( ! empty(self::$log));
  429. // Write messages to log file
  430. file_put_contents($filename, implode(PHP_EOL, $messages).PHP_EOL, FILE_APPEND);
  431. }
  432. /**
  433. * Get or set the logging directory.
  434. *
  435. * @param string new log directory
  436. * @return string
  437. */
  438. public static function log_directory($dir = NULL)
  439. {
  440. static $directory;
  441. if ( ! empty($dir))
  442. {
  443. // Get the directory path
  444. $dir = realpath($dir);
  445. if (is_dir($dir) AND is_writable($dir))
  446. {
  447. // Change the log directory
  448. $directory = str_replace('\\', '/', $dir).'/';
  449. }
  450. else
  451. {
  452. // Log directory is invalid
  453. throw new Kohana_Exception('core.log_dir_unwritable', $dir);
  454. }
  455. }
  456. return $directory;
  457. }
  458. /**
  459. * Load data from a simple cache file. This should only be used internally,
  460. * and is NOT a replacement for the Cache library.
  461. *
  462. * @param string unique name of cache
  463. * @param integer expiration in seconds
  464. * @return mixed
  465. */
  466. public static function cache($name, $lifetime)
  467. {
  468. if ($lifetime > 0)
  469. {
  470. $path = APPPATH.'cache/kohana_'.$name;
  471. if (is_file($path))
  472. {
  473. // Check the file modification time
  474. if ((time() - filemtime($path)) < $lifetime)
  475. {
  476. // Cache is valid
  477. return unserialize(file_get_contents($path));
  478. }
  479. else
  480. {
  481. // Cache is invalid, delete it
  482. unlink($path);
  483. }
  484. }
  485. }
  486. // No cache found
  487. return NULL;
  488. }
  489. /**
  490. * Save data to a simple cache file. This should only be used internally, and
  491. * is NOT a replacement for the Cache library.
  492. *
  493. * @param string cache name
  494. * @param mixed data to cache
  495. * @param integer expiration in seconds
  496. * @return boolean
  497. */
  498. public static function cache_save($name, $data, $lifetime)
  499. {
  500. if ($lifetime < 1)
  501. return FALSE;
  502. $path = APPPATH.'cache/kohana_'.$name;
  503. if ($data === NULL)
  504. {
  505. // Delete cache
  506. return (is_file($path) and unlink($path));
  507. }
  508. else
  509. {
  510. // Write data to cache file
  511. return (bool) file_put_contents($path, serialize($data));
  512. }
  513. }
  514. /**
  515. * Kohana output handler.
  516. *
  517. * @param string current output buffer
  518. * @return string
  519. */
  520. public static function output_buffer($output)
  521. {
  522. if ( ! Event::has_run('system.send_headers'))
  523. {
  524. // Run the send_headers event, specifically for cookies being set
  525. Event::run('system.send_headers');
  526. }
  527. // Set final output
  528. self::$output = $output;
  529. // Set and return the final output
  530. return $output;
  531. }
  532. /**
  533. * Closes all open output buffers, either by flushing or cleaning all
  534. * open buffers, including the Kohana output buffer.
  535. *
  536. * @param boolean disable to clear buffers, rather than flushing
  537. * @return void
  538. */
  539. public static function close_buffers($flush = TRUE)
  540. {
  541. if (ob_get_level() >= self::$buffer_level)
  542. {
  543. // Set the close function
  544. $close = ($flush === TRUE) ? 'ob_end_flush' : 'ob_end_clean';
  545. while (ob_get_level() > self::$buffer_level)
  546. {
  547. // Flush or clean the buffer
  548. $close();
  549. }
  550. // This will flush the Kohana buffer, which sets self::$output
  551. ob_end_clean();
  552. // Reset the buffer level
  553. self::$buffer_level = ob_get_level();
  554. }
  555. }
  556. /**
  557. * Triggers the shutdown of Kohana by closing the output buffer, runs the system.display event.
  558. *
  559. * @return void
  560. */
  561. public static function shutdown()
  562. {
  563. // Close output buffers
  564. self::close_buffers(TRUE);
  565. // Run the output event
  566. Event::run('system.display', self::$output);
  567. // Render the final output
  568. self::render(self::$output);
  569. }
  570. /**
  571. * Inserts global Kohana variables into the generated output and prints it.
  572. *
  573. * @param string final output that will displayed
  574. * @return void
  575. */
  576. public static function render($output)
  577. {
  578. // Fetch memory usage in MB
  579. $memory = function_exists('memory_get_usage') ? (memory_get_usage() / 1024 / 1024) : 0;
  580. // Fetch benchmark for page execution time
  581. $benchmark = Benchmark::get(SYSTEM_BENCHMARK.'_total_execution');
  582. if (self::config('core.render_stats') === TRUE)
  583. {
  584. // Replace the global template variables
  585. $output = str_replace(
  586. array
  587. (
  588. '{kohana_version}',
  589. '{kohana_codename}',
  590. '{execution_time}',
  591. '{memory_usage}',
  592. '{included_files}',
  593. ),
  594. array
  595. (
  596. KOHANA_VERSION,
  597. KOHANA_CODENAME,
  598. $benchmark['time'],
  599. number_format($memory, 2).'MB',
  600. count(get_included_files()),
  601. ),
  602. $output
  603. );
  604. }
  605. if ($level = self::config('core.output_compression') AND ini_get('output_handler') !== 'ob_gzhandler' AND (int) ini_get('zlib.output_compression') === 0)
  606. {
  607. if ($level < 1 OR $level > 9)
  608. {
  609. // Normalize the level to be an integer between 1 and 9. This
  610. // step must be done to prevent gzencode from triggering an error
  611. $level = max(1, min($level, 9));
  612. }
  613. if (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  614. {
  615. $compress = 'gzip';
  616. }
  617. elseif (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== FALSE)
  618. {
  619. $compress = 'deflate';
  620. }
  621. }
  622. if (isset($compress) AND $level > 0)
  623. {
  624. switch ($compress)
  625. {
  626. case 'gzip':
  627. // Compress output using gzip
  628. $output = gzencode($output, $level);
  629. break;
  630. case 'deflate':
  631. // Compress output using zlib (HTTP deflate)
  632. $output = gzdeflate($output, $level);
  633. break;
  634. }
  635. // This header must be sent with compressed content to prevent
  636. // browser caches from breaking
  637. header('Vary: Accept-Encoding');
  638. // Send the content encoding header
  639. header('Content-Encoding: '.$compress);
  640. // Sending Content-Length in CGI can result in unexpected behavior
  641. if (stripos(PHP_SAPI, 'cgi') === FALSE)
  642. {
  643. header('Content-Length: '.strlen($output));
  644. }
  645. }
  646. echo $output;
  647. }
  648. /**
  649. * Displays a 404 page.
  650. *
  651. * @throws Kohana_404_Exception
  652. * @param string URI of page
  653. * @param string custom template
  654. * @return void
  655. */
  656. public static function show_404($page = FALSE, $template = FALSE)
  657. {
  658. throw new Kohana_404_Exception($page, $template);
  659. }
  660. /**
  661. * Dual-purpose PHP error and exception handler. Uses the kohana_error_page
  662. * view to display the message.
  663. *
  664. * @param integer|object exception object or error code
  665. * @param string error message
  666. * @param string filename
  667. * @param integer line number
  668. * @return void
  669. */
  670. public static function exception_handler($exception, $message = NULL, $file = NULL, $line = NULL)
  671. {
  672. // PHP errors have 5 args, always
  673. $PHP_ERROR = (func_num_args() === 5);
  674. // Test to see if errors should be displayed
  675. if ($PHP_ERROR AND (error_reporting() & $exception) === 0)
  676. return;
  677. // This is useful for hooks to determine if a page has an error
  678. self::$has_error = TRUE;
  679. // Error handling will use exactly 5 args, every time
  680. if ($PHP_ERROR)
  681. {
  682. $code = $exception;
  683. $type = 'PHP Error';
  684. $template = 'kohana_error_page';
  685. }
  686. else
  687. {
  688. $code = $exception->getCode();
  689. $type = get_class($exception);
  690. $message = $exception->getMessage();
  691. $file = $exception->getFile();
  692. $line = $exception->getLine();
  693. $template = ($exception instanceof Kohana_Exception) ? $exception->getTemplate() : 'kohana_error_page';
  694. }
  695. if (is_numeric($code))
  696. {
  697. $codes = self::lang('errors');
  698. if ( ! empty($codes[$code]))
  699. {
  700. list($level, $error, $description) = $codes[$code];
  701. }
  702. else
  703. {
  704. $level = 1;
  705. $error = $PHP_ERROR ? 'Unknown Error' : get_class($exception);
  706. $description = '';
  707. }
  708. }
  709. else
  710. {
  711. // Custom error message, this will never be logged
  712. $level = 5;
  713. $error = $code;
  714. $description = '';
  715. }
  716. // Remove the DOCROOT from the path, as a security precaution
  717. $file = str_replace('\\', '/', realpath($file));
  718. $file = preg_replace('|^'.preg_quote(DOCROOT).'|', '', $file);
  719. if ($level <= self::$configuration['core']['log_threshold'])
  720. {
  721. // Log the error
  722. self::log('error', self::lang('core.uncaught_exception', $type, $message, $file, $line));
  723. }
  724. if ($PHP_ERROR)
  725. {
  726. $description = self::lang('errors.'.E_RECOVERABLE_ERROR);
  727. $description = is_array($description) ? $description[2] : '';
  728. if ( ! headers_sent())
  729. {
  730. // Send the 500 header
  731. header('HTTP/1.1 500 Internal Server Error');
  732. }
  733. }
  734. else
  735. {
  736. if (method_exists($exception, 'sendHeaders') AND ! headers_sent())
  737. {
  738. // Send the headers if they have not already been sent
  739. $exception->sendHeaders();
  740. }
  741. }
  742. while (ob_get_level() > self::$buffer_level)
  743. {
  744. // Close open buffers
  745. ob_end_clean();
  746. }
  747. // Test if display_errors is on
  748. if (self::$configuration['core']['display_errors'] === TRUE)
  749. {
  750. if ( ! IN_PRODUCTION AND $line != FALSE)
  751. {
  752. // Remove the first entry of debug_backtrace(), it is the exception_handler call
  753. $trace = $PHP_ERROR ? array_slice(debug_backtrace(), 1) : $exception->getTrace();
  754. // Beautify backtrace
  755. $trace = self::backtrace($trace);
  756. }
  757. // Load the error
  758. require self::find_file('views', empty($template) ? 'kohana_error_page' : $template);
  759. }
  760. else
  761. {
  762. // Get the i18n messages
  763. $error = self::lang('core.generic_error');
  764. $message = self::lang('core.errors_disabled', url::site(), url::site(Router::$current_uri));
  765. // Load the errors_disabled view
  766. require self::find_file('views', 'kohana_error_disabled');
  767. }
  768. if ( ! Event::has_run('system.shutdown'))
  769. {
  770. // Run the shutdown even to ensure a clean exit
  771. Event::run('system.shutdown');
  772. }
  773. // Turn off error reporting
  774. error_reporting(0);
  775. exit;
  776. }
  777. /**
  778. * Provides class auto-loading.
  779. *
  780. * @throws Kohana_Exception
  781. * @param string name of class
  782. * @return bool
  783. */
  784. public static function auto_load($class)
  785. {
  786. if (class_exists($class, FALSE))
  787. return TRUE;
  788. if (($suffix = strrpos($class, '_')) > 0)
  789. {
  790. // Find the class suffix
  791. $suffix = substr($class, $suffix + 1);
  792. }
  793. else
  794. {
  795. // No suffix
  796. $suffix = FALSE;
  797. }
  798. if ($suffix === 'Core')
  799. {
  800. $type = 'libraries';
  801. $file = substr($class, 0, -5);
  802. }
  803. elseif ($suffix === 'Controller')
  804. {
  805. $type = 'controllers';
  806. // Lowercase filename
  807. $file = strtolower(substr($class, 0, -11));
  808. }
  809. elseif ($suffix === 'Model')
  810. {
  811. $type = 'models';
  812. // Lowercase filename
  813. $file = strtolower(substr($class, 0, -6));
  814. }
  815. elseif ($suffix === 'Driver')
  816. {
  817. $type = 'libraries/drivers';
  818. $file = str_replace('_', '/', substr($class, 0, -7));
  819. }
  820. else
  821. {
  822. // This could be either a library or a helper, but libraries must
  823. // always be capitalized, so we check if the first character is
  824. // uppercase. If it is, we are loading a library, not a helper.
  825. $type = ($class[0] < 'a') ? 'libraries' : 'helpers';
  826. $file = $class;
  827. }
  828. if ($filename = self::find_file($type, $file))
  829. {
  830. // Load the class
  831. require $filename;
  832. }
  833. else
  834. {
  835. // The class could not be found
  836. return FALSE;
  837. }
  838. if ($filename = self::find_file($type, self::$configuration['core']['extension_prefix'].$class))
  839. {
  840. // Load the class extension
  841. require $filename;
  842. }
  843. elseif ($suffix !== 'Core' AND class_exists($class.'_Core', FALSE))
  844. {
  845. // Class extension to be evaluated
  846. $extension = 'class '.$class.' extends '.$class.'_Core { }';
  847. // Start class analysis
  848. $core = new ReflectionClass($class.'_Core');
  849. if ($core->isAbstract())
  850. {
  851. // Make the extension abstract
  852. $extension = 'abstract '.$extension;
  853. }
  854. // Transparent class extensions are handled using eval. This is
  855. // a disgusting hack, but it gets the job done.
  856. eval($extension);
  857. }
  858. return TRUE;
  859. }
  860. /**
  861. * Find a resource file in a given directory. Files will be located according
  862. * to the order of the include paths. Configuration and i18n files will be
  863. * returned in reverse order.
  864. *
  865. * @throws Kohana_Exception if file is required and not found
  866. * @param string directory to search in
  867. * @param string filename to look for (including extension only if 4th parameter is TRUE)
  868. * @param boolean file required
  869. * @param string file extension
  870. * @return array if the type is config, i18n or l10n
  871. * @return string if the file is found
  872. * @return FALSE if the file is not found
  873. */
  874. public static function find_file($directory, $filename, $required = FALSE, $ext = FALSE)
  875. {
  876. // NOTE: This test MUST be not be a strict comparison (===), or empty
  877. // extensions will be allowed!
  878. if ($ext == '')
  879. {
  880. // Use the default extension
  881. $ext = EXT;
  882. }
  883. else
  884. {
  885. // Add a period before the extension
  886. $ext = '.'.$ext;
  887. }
  888. // Search path
  889. $search = $directory.'/'.$filename.$ext;
  890. if (isset(self::$internal_cache['find_file_paths'][$search]))
  891. return self::$internal_cache['find_file_paths'][$search];
  892. // Load include paths
  893. $paths = self::$include_paths;
  894. // Nothing found, yet
  895. $found = NULL;
  896. if ($directory === 'config' OR $directory === 'i18n')
  897. {
  898. // Search in reverse, for merging
  899. $paths = array_reverse($paths);
  900. foreach ($paths as $path)
  901. {
  902. if (is_file($path.$search))
  903. {
  904. // A matching file has been found
  905. $found[] = $path.$search;
  906. }
  907. }
  908. }
  909. else
  910. {
  911. foreach ($paths as $path)
  912. {
  913. if (is_file($path.$search))
  914. {
  915. // A matching file has been found
  916. $found = $path.$search;
  917. // Stop searching
  918. break;
  919. }
  920. }
  921. }
  922. if ($found === NULL)
  923. {
  924. if ($required === TRUE)
  925. {
  926. // Directory i18n key
  927. $directory = 'core.'.inflector::singular($directory);
  928. // If the file is required, throw an exception
  929. throw new Kohana_Exception('core.resource_not_found', self::lang($directory), $filename);
  930. }
  931. else
  932. {
  933. // Nothing was found, return FALSE
  934. $found = FALSE;
  935. }
  936. }
  937. if ( ! isset(self::$write_cache['find_file_paths']))
  938. {
  939. // Write cache at shutdown
  940. self::$write_cache['find_file_paths'] = TRUE;
  941. }
  942. return self::$internal_cache['find_file_paths'][$search] = $found;
  943. }
  944. /**
  945. * Lists all files and directories in a resource path.
  946. *
  947. * @param string directory to search
  948. * @param boolean list all files to the maximum depth?
  949. * @param string full path to search (used for recursion, *never* set this manually)
  950. * @return array filenames and directories
  951. */
  952. public static function list_files($directory, $recursive = FALSE, $path = FALSE)
  953. {
  954. $files = array();
  955. if ($path === FALSE)
  956. {
  957. $paths = array_reverse(self::include_paths());
  958. foreach ($paths as $path)
  959. {
  960. // Recursively get and merge all files
  961. $files = array_merge($files, self::list_files($directory, $recursive, $path.$directory));
  962. }
  963. }
  964. else
  965. {
  966. $path = rtrim($path, '/').'/';
  967. if (is_readable($path))
  968. {
  969. $items = (array) glob($path.'*');
  970. foreach ($items as $index => $item)
  971. {
  972. $files[] = $item = str_replace('\\', '/', $item);
  973. // Handle recursion
  974. if (is_dir($item) AND $recursive == TRUE)
  975. {
  976. // Filename should only be the basename
  977. $item = pathinfo($item, PATHINFO_BASENAME);
  978. // Append sub-directory search
  979. $files = array_merge($files, self::list_files($directory, TRUE, $path.$item));
  980. }
  981. }
  982. }
  983. }
  984. return $files;
  985. }
  986. /**
  987. * Fetch an i18n language item.
  988. *
  989. * @param string language key to fetch
  990. * @param array additional information to insert into the line
  991. * @param string locale name to override settings
  992. * @return string i18n language string, or the requested key if the i18n item is not found
  993. */
  994. public static function lang($key, $args = array(), $force_locale = NULL)
  995. {
  996. // Extract the main group from the key
  997. $group = explode('.', $key, 2);
  998. $group = $group[0];
  999. // Get locale name
  1000. $locale = self::config('locale.language.0');
  1001. // If we are overriding the language settings then do it here.
  1002. if ($force_locale != NULL)
  1003. {
  1004. $locale = $force_locale;
  1005. }
  1006. if ( ! isset(self::$internal_cache['language'][$locale][$group]))
  1007. {
  1008. // Messages for this group
  1009. $messages = array();
  1010. if ($files = self::find_file('i18n', $locale.'/'.$group))
  1011. {
  1012. foreach ($files as $file)
  1013. {
  1014. include $file;
  1015. // Merge in configuration
  1016. if ( ! empty($lang) AND is_array($lang))
  1017. {
  1018. foreach ($lang as $k => $v)
  1019. {
  1020. $messages[$k] = $v;
  1021. }
  1022. }
  1023. }
  1024. }
  1025. if ( ! isset(self::$write_cache['language']))
  1026. {
  1027. // Write language cache
  1028. self::$write_cache['language'] = TRUE;
  1029. }
  1030. self::$internal_cache['language'][$locale][$group] = $messages;
  1031. }
  1032. // Get the line from cache
  1033. $line = self::key_string(self::$internal_cache['language'][$locale], $key);
  1034. if ($line === NULL)
  1035. {
  1036. self::log('error', 'Missing i18n entry '.$key.' for language '.$locale);
  1037. if ($force_locale != NULL)
  1038. {
  1039. // Return the key string as fallback
  1040. return $key;
  1041. }else{
  1042. // Try for English
  1043. return Kohana::lang($key, $args, 'en_US');
  1044. }
  1045. }
  1046. if (is_string($line) AND func_num_args() > 1)
  1047. {
  1048. $args = array_slice(func_get_args(), 1);
  1049. // Add the arguments into the line
  1050. $line = vsprintf($line, is_array($args[0]) ? $args[0] : $args);
  1051. }
  1052. return $line;
  1053. }
  1054. /**
  1055. * Returns the value of a key, defined by a 'dot-noted' string, from an array.
  1056. *
  1057. * @param array array to search
  1058. * @param string dot-noted string: foo.bar.baz
  1059. * @return string if the key is found
  1060. * @return void if the key is not found
  1061. */
  1062. public static function key_string($array, $keys)
  1063. {
  1064. if (empty($array))
  1065. return NULL;
  1066. // Prepare for loop
  1067. $keys = explode('.', $keys);
  1068. do
  1069. {
  1070. // Get the next key
  1071. $key = array_shift($keys);
  1072. if (isset($array[$key]))
  1073. {
  1074. if (is_array($array[$key]) AND ! empty($keys))
  1075. {
  1076. // Dig down to prepare the next loop
  1077. $array = $array[$key];
  1078. }
  1079. else
  1080. {
  1081. // Requested key was found
  1082. return $array[$key];
  1083. }
  1084. }
  1085. else
  1086. {
  1087. // Requested key is not set
  1088. break;
  1089. }
  1090. }
  1091. while ( ! empty($keys));
  1092. return NULL;
  1093. }
  1094. /**
  1095. * Sets values in an array by using a 'dot-noted' string.
  1096. *
  1097. * @param array array to set keys in (reference)
  1098. * @param string dot-noted string: foo.bar.baz
  1099. * @return mixed fill value for the key
  1100. * @return void
  1101. */
  1102. public static function key_string_set( & $array, $keys, $fill = NULL)
  1103. {
  1104. if (is_object($array) AND ($array instanceof ArrayObject))
  1105. {
  1106. // Copy the array
  1107. $array_copy = $array->getArrayCopy();
  1108. // Is an object
  1109. $array_object = TRUE;
  1110. }
  1111. else
  1112. {
  1113. if ( ! is_array($array))
  1114. {
  1115. // Must always be an array
  1116. $array = (array) $array;
  1117. }
  1118. // Copy is a reference to the array
  1119. $array_copy =& $array;
  1120. }
  1121. if (empty($keys))
  1122. return $array;
  1123. // Create keys
  1124. $keys = explode('.', $keys);
  1125. // Create reference to the array
  1126. $row =& $array_copy;
  1127. for ($i = 0, $end = count($keys) - 1; $i <= $end; $i++)
  1128. {
  1129. // Get the current key
  1130. $key = $keys[$i];
  1131. if ( ! isset($row[$key]))
  1132. {
  1133. if (isset($keys[$i + 1]))
  1134. {
  1135. // Make the value an array
  1136. $row[$key] = array();
  1137. }
  1138. else
  1139. {
  1140. // Add the fill key
  1141. $row[$key] = $fill;
  1142. }
  1143. }
  1144. elseif (isset($keys[$i + 1]))
  1145. {
  1146. // Make the value an array
  1147. $row[$key] = (array) $row[$key];
  1148. }
  1149. // Go down a level, creating a new row reference
  1150. $row =& $row[$key];
  1151. }
  1152. if (isset($array_object))
  1153. {
  1154. // Swap the array back in
  1155. $array->exchangeArray($array_copy);
  1156. }
  1157. }
  1158. /**
  1159. * Retrieves current user agent information:
  1160. * keys: browser, version, platform, mobile, robot, referrer, languages, charsets
  1161. * tests: is_browser, is_mobile, is_robot, accept_lang, accept_charset
  1162. *
  1163. * @param string key or test name
  1164. * @param string used with "accept" tests: user_agent(accept_lang, en)
  1165. * @return array languages and charsets
  1166. * @return string all other keys
  1167. * @return boolean all tests
  1168. */
  1169. public static function user_agent($key = 'agent', $compare = NULL)
  1170. {
  1171. static $info;
  1172. // Return the raw string
  1173. if ($key === 'agent')
  1174. return self::$user_agent;
  1175. if ($info === NULL)
  1176. {
  1177. // Parse the user agent and extract basic information
  1178. $agents = self::config('user_agents');
  1179. foreach ($agents as $type => $data)
  1180. {
  1181. foreach ($data as $agent => $name)
  1182. {
  1183. if (stripos(self::$user_agent, $agent) !== FALSE)
  1184. {
  1185. if ($type === 'browser' AND preg_match('|'.preg_quote($agent).'[^0-9.]*+([0-9.][0-9.a-z]*)|i', self::$user_agent, $match))
  1186. {
  1187. // Set the browser version
  1188. $info['version'] = $match[1];
  1189. }
  1190. // Set the agent name
  1191. $info[$type] = $name;
  1192. break;
  1193. }
  1194. }
  1195. }
  1196. }
  1197. if (empty($info[$key]))
  1198. {
  1199. switch ($key)
  1200. {
  1201. case 'is_robot':
  1202. case 'is_browser':
  1203. case 'is_mobile':
  1204. // A boolean result
  1205. $return = ! empty($info[substr($key, 3)]);
  1206. break;
  1207. case 'languages':
  1208. $return = array();
  1209. if ( ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
  1210. {
  1211. if (preg_match_all('/[-a-z]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])), $matches))
  1212. {
  1213. // Found a result
  1214. $return = $matches[0];
  1215. }
  1216. }
  1217. break;
  1218. case 'charsets':
  1219. $return = array();
  1220. if ( ! empty($_SERVER['HTTP_ACCEPT_CHARSET']))
  1221. {
  1222. if (preg_match_all('/[-a-z0-9]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_CHARSET'])), $matches))
  1223. {
  1224. // Found a result
  1225. $return = $matches[0];
  1226. }
  1227. }
  1228. break;
  1229. case 'referrer':
  1230. if ( ! empty($_SERVER['HTTP_REFERER']))
  1231. {
  1232. // Found a result
  1233. $return = trim($_SERVER['HTTP_REFERER']);
  1234. }
  1235. break;
  1236. }
  1237. // Cache the return value
  1238. isset($return) and $info[$key] = $return;
  1239. }
  1240. if ( ! empty($compare))
  1241. {
  1242. // The comparison must always be lowercase
  1243. $compare = strtolower($compare);
  1244. switch ($key)
  1245. {
  1246. case 'accept_lang':
  1247. // Check if the lange is accepted
  1248. return in_array($compare, self::user_agent('languages'));
  1249. break;
  1250. case 'accept_charset':
  1251. // Check if the charset is accepted
  1252. return in_array($compare, self::user_agent('charsets'));
  1253. break;
  1254. default:
  1255. // Invalid comparison
  1256. return FALSE;
  1257. break;
  1258. }
  1259. }
  1260. // Return the key, if set
  1261. return isset($info[$key]) ? $info[$key] : NULL;
  1262. }
  1263. /**
  1264. * Quick debugging of any variable. Any number of parameters can be set.
  1265. *
  1266. * @return string
  1267. */
  1268. public static function debug()
  1269. {
  1270. if (func_num_args() === 0)
  1271. return;
  1272. // Get params
  1273. $params = func_get_args();
  1274. $output = array();
  1275. foreach ($params as $var)
  1276. {
  1277. $output[] = '<pre>('.gettype($var).') '.html::specialchars(print_r($var, TRUE)).'</pre>';
  1278. }
  1279. return implode("\n", $output);
  1280. }
  1281. /**
  1282. * Displays nice backtrace information.
  1283. * @see http://php.net/debug_backtrace
  1284. *
  1285. * @param array backtrace generated by an exception or debug_backtrace
  1286. * @return string
  1287. */
  1288. public static function backtrace($trace)
  1289. {
  1290. if ( ! is_array($trace))
  1291. return;
  1292. // Final output
  1293. $output = array();
  1294. foreach ($trace as $entry)
  1295. {
  1296. $temp = '<li>';
  1297. if (isset($entry['file']))
  1298. {
  1299. $temp .= self::lang('core.error_file_line', preg_replace('!^'.preg_quote(DOCROOT).'!', '', $entry['file']), $entry['line']);
  1300. }
  1301. $temp .= '<pre>';
  1302. if (isset($entry['class']))
  1303. {
  1304. // Add class and call type
  1305. $temp .= $entry['class'].$entry['type'];
  1306. }
  1307. // Add function
  1308. $temp .= $entry['function'].'( ';
  1309. // Add function args
  1310. if (isset($entry['args']) AND is_array($entry['args']))
  1311. {
  1312. // Separator starts as nothing
  1313. $sep = '';
  1314. while ($arg = array_shift($entry['args']))
  1315. {
  1316. if (is_string($arg) AND is_file($arg))
  1317. {
  1318. // Remove docroot from filename
  1319. $arg = preg_replace('!^'.preg_quote(DOCROOT).'!', '', $arg);
  1320. }
  1321. $temp .= $sep.html::specialchars(print_r($arg, TRUE));
  1322. // Change separator to a comma
  1323. $sep = ', ';
  1324. }
  1325. }
  1326. $temp .= ' )</pre></li>';
  1327. $output[] = $temp;
  1328. }
  1329. return '<ul class="backtrace">'.implode("\n", $output).'</ul>';
  1330. }
  1331. /**
  1332. * Saves the internal caches: configuration, include paths, etc.
  1333. *
  1334. * @return boolean
  1335. */
  1336. public static function internal_cache_save()
  1337. {
  1338. if ( ! is_array(self::$write_cache))
  1339. return FALSE;
  1340. // Get internal cache names
  1341. $caches = array_keys(self::$write_cache);
  1342. // Nothing written
  1343. $written = FALSE;
  1344. foreach ($caches as $cache)
  1345. {
  1346. if (isset(self::$internal_cache[$cache]))
  1347. {
  1348. // Write the cache file
  1349. self::cache_save($cache, self::$internal_cache[$cache], self::$configuration['core']['internal_cache']);
  1350. // A cache has been written
  1351. $written = TRUE;
  1352. }
  1353. }
  1354. return $written;
  1355. }
  1356. } // End Kohana
  1357. /**
  1358. * Creates a generic i18n exception.
  1359. */
  1360. class Kohana_Exception extends Exception {
  1361. // Template file
  1362. protected $template = 'kohana_error_page';
  1363. // Header
  1364. protected $header = FALSE;
  1365. // Error code
  1366. protected $code = E_KOHANA;
  1367. /**
  1368. * Set exception message.
  1369. *
  1370. * @param string i18n language key for the message
  1371. * @param array addition line parameters
  1372. */
  1373. public function __construct($error)
  1374. {
  1375. $args = array_slice(func_get_args(), 1);
  1376. // Fetch the error message
  1377. $message = Kohana::lang($error, $args);
  1378. if ($message === $error OR empty($message))
  1379. {
  1380. // Unable to locate the message for the error
  1381. $message = 'Unknown Exception: '.$error;
  1382. }
  1383. // Sets $this->message the proper way
  1384. parent::__construct($message);
  1385. }
  1386. /**
  1387. * Magic method for converting an object to a string.
  1388. *
  1389. * @return string i18n message
  1390. */
  1391. public function __toString()
  1392. {
  1393. return (string) $this->message;
  1394. }
  1395. /**
  1396. * Fetch the template name.
  1397. *
  1398. * @return string
  1399. */
  1400. public function getTemplate()
  1401. {
  1402. return $this->template;
  1403. }
  1404. /**
  1405. * Sends an Internal Server Error header.
  1406. *
  1407. * @return void
  1408. */
  1409. public function sendHeaders()
  1410. {
  1411. // Send the 500 header
  1412. header('HTTP/1.1 500 Internal Server Error');
  1413. }
  1414. } // End Kohana Exception
  1415. /**
  1416. * Creates a custom exception.
  1417. */
  1418. class Kohana_User_Exception extends Kohana_Exception {
  1419. /**
  1420. * Set exception title and message.
  1421. *
  1422. * @param string exception title string
  1423. * @param string exception message string
  1424. * @param string custom error template
  1425. */
  1426. public function __construct($title, $message, $template = FALSE)
  1427. {
  1428. Exception::__construct($message);
  1429. $this->code = $title;
  1430. if ($template !== FALSE)
  1431. {
  1432. $this->template = $template;
  1433. }
  1434. }
  1435. } // End Kohana PHP Exception
  1436. /**
  1437. * Creates a Page Not Found exception.
  1438. */
  1439. class Kohana_404_Exception extends Kohana_Exception {
  1440. protected $code = E_PAGE_NOT_FOUND;
  1441. /**
  1442. * Set internal properties.
  1443. *
  1444. * @param string URL of page
  1445. * @param string custom error template
  1446. */
  1447. public function __construct($page = FALSE, $template = FALSE)
  1448. {
  1449. if ($page === FALSE)
  1450. {
  1451. // Construct the page URI using Router properties
  1452. $page = Router::$current_uri.Router::$url_suffix.Router::$query_string;
  1453. }
  1454. Exception::__construct(Kohana::lang('core.page_not_found', $page));
  1455. $this->template = $template;
  1456. }
  1457. /**
  1458. * Sends "File Not Found" headers, to emulate server behavior.
  1459. *
  1460. * @return void
  1461. */
  1462. public function sendHeaders()
  1463. {
  1464. // Send the 404 header
  1465. header('HTTP/1.1 404 File Not Found');
  1466. }
  1467. } // End Kohana 404 Exception