PageRenderTime 59ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/system/core/Kohana.php

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