PageRenderTime 50ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/system/core/Kohana.php

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