PageRenderTime 47ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/system/core/Kohana.php

https://github.com/MHordecki/milionkostek
PHP | 1480 lines | 992 code | 166 blank | 322 comment | 70 complexity | 01dd54559b1a7980622b6132f58205fc 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 2833 2008-06-12 08:18:12Z dlib $
  6. *
  7. * @package Core
  8. * @author Kohana Team
  9. * @copyright (c) 2007-2008 Kohana Team
  10. * @license http://kohanaphp.com/license.html
  11. */
  12. class Kohana {
  13. // The singleton instance of the controller
  14. public static $instance;
  15. // Output buffering level
  16. private static $buffer_level = 0;
  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. // File path cache
  24. private static $paths;
  25. private static $paths_changed = FALSE;
  26. /**
  27. * Sets up the PHP environment. Adds error/exception handling, output
  28. * buffering, and adds an auto-loading method for loading classes.
  29. *
  30. * This method is run immediately when this file is loaded, and is
  31. * benchmarked as environment_setup.
  32. *
  33. * For security, this function also destroys the $_REQUEST global variable.
  34. * Using the proper global (GET, POST, COOKIE, etc) is inherently more secure.
  35. * The recommended way to fetch a global variable is using the Input library.
  36. * @see http://www.php.net/globals
  37. *
  38. * @return void
  39. */
  40. final public static function setup()
  41. {
  42. static $run;
  43. // This function can only be run once
  44. if ($run === TRUE)
  45. return;
  46. // Start the environment setup benchmark
  47. Benchmark::start(SYSTEM_BENCHMARK.'_environment_setup');
  48. // Define Kohana error constant
  49. defined('E_KOHANA') or define('E_KOHANA', 42);
  50. // Define 404 error constant
  51. defined('E_PAGE_NOT_FOUND') or define('E_PAGE_NOT_FOUND', 43);
  52. // Define database error constant
  53. defined('E_DATABASE_ERROR') or define('E_DATABASE_ERROR', 44);
  54. // Disable error reporting
  55. $ER = error_reporting(0);
  56. // Set the user agent
  57. self::$user_agent = trim($_SERVER['HTTP_USER_AGENT']);
  58. if (function_exists('date_default_timezone_set'))
  59. {
  60. $timezone = Config::item('locale.timezone');
  61. // Set default timezone, due to increased validation of date settings
  62. // which cause massive amounts of E_NOTICEs to be generated in PHP 5.2+
  63. date_default_timezone_set(empty($timezone) ? date_default_timezone_get() : $timezone);
  64. }
  65. // Restore error reporting
  66. error_reporting($ER);
  67. // Start output buffering
  68. ob_start(array('Kohana', 'output_buffer'));
  69. // Save buffering level
  70. self::$buffer_level = ob_get_level();
  71. // Load path cache
  72. self::$paths = Kohana::load_cache('file_paths');
  73. // Set autoloader
  74. spl_autoload_register(array('Kohana', 'auto_load'));
  75. // Set error handler
  76. set_error_handler(array('Kohana', 'exception_handler'));
  77. // Set exception handler
  78. set_exception_handler(array('Kohana', 'exception_handler'));
  79. // Disable magic_quotes_runtime. The Input library takes care of
  80. // magic_quotes_gpc later.
  81. set_magic_quotes_runtime(0);
  82. // Send default text/html UTF-8 header
  83. header('Content-type: text/html; charset=UTF-8');
  84. // Set locale information
  85. setlocale(LC_ALL, Config::item('locale.language').'.UTF-8');
  86. if (Config::item('log.threshold') > 0)
  87. {
  88. // Get the configured log directory
  89. $log_dir = Config::item('log.directory');
  90. if ( ! is_dir($log_dir))
  91. {
  92. // Application log directory
  93. $log_dir = APPPATH.$log_dir;
  94. }
  95. // Set the log directory
  96. Log::directory($log_dir);
  97. // Enable log writing if the log threshold is above 0
  98. register_shutdown_function(array('Log', 'write'));
  99. }
  100. // Enable Kohana routing
  101. Event::add('system.routing', array('Router', 'find_uri'));
  102. Event::add('system.routing', array('Router', 'setup'));
  103. // Enable Kohana controller initialization
  104. Event::add('system.execute', array('Kohana', 'instance'));
  105. // Enable Kohana 404 pages
  106. Event::add('system.404', array('Kohana', 'show_404'));
  107. // Enable Kohana output handling
  108. Event::add('system.shutdown', array('Kohana', 'shutdown'));
  109. if ($config = Config::item('hooks.enable'))
  110. {
  111. $hooks = array();
  112. if ( ! is_array($config))
  113. {
  114. // All of the hooks are enabled, so we use list_files
  115. $hooks = Kohana::list_files('hooks', TRUE);
  116. }
  117. else
  118. {
  119. // Individual hooks need to be found
  120. foreach ($config as $name)
  121. {
  122. if ($hook = Kohana::find_file('hooks', $name, FALSE))
  123. {
  124. // Hook was found, add it to loaded hooks
  125. $hooks[] = $hook;
  126. }
  127. else
  128. {
  129. // This should never happen
  130. Log::add('error', 'Hook not found: '.$name);
  131. }
  132. }
  133. }
  134. // Length of extension, for offset
  135. $ext = -(strlen(EXT));
  136. foreach ($hooks as $hook)
  137. {
  138. // Validate the filename extension
  139. if (substr($hook, $ext) === EXT)
  140. {
  141. // Hook was found, include it
  142. include $hook;
  143. }
  144. else
  145. {
  146. // This should never happen
  147. Log::add('error', 'Hook not found: '.$hook);
  148. }
  149. }
  150. }
  151. // Setup is complete, prevent it from being run again
  152. $run = TRUE;
  153. // Stop the environment setup routine
  154. Benchmark::stop(SYSTEM_BENCHMARK.'_environment_setup');
  155. }
  156. /**
  157. * Loads the controller and initializes it. Runs the pre_controller,
  158. * post_controller_constructor, and post_controller events. Triggers
  159. * a system.404 event when the route cannot be mapped to a controller.
  160. *
  161. * This method is benchmarked as controller_setup and controller_execution.
  162. *
  163. * @return object instance of controller
  164. */
  165. final public static function & instance()
  166. {
  167. if (self::$instance === NULL)
  168. {
  169. Benchmark::start(SYSTEM_BENCHMARK.'_controller_setup');
  170. // Include the Controller file
  171. require Router::$directory.Router::$controller.EXT;
  172. // Set controller class name
  173. $controller = ucfirst(Router::$controller).'_Controller';
  174. // Make sure the controller class exists
  175. class_exists($controller, FALSE) or Event::run('system.404');
  176. // Production enviroment protection, based on the IN_PRODUCTION flag
  177. (IN_PRODUCTION AND constant($controller.'::ALLOW_PRODUCTION') === FALSE) and Event::run('system.404');
  178. // Run system.pre_controller
  179. Event::run('system.pre_controller');
  180. // Get the controller methods
  181. $methods = array_flip(get_class_methods($controller));
  182. if (isset($methods['_remap']))
  183. {
  184. // Make the arguments routed
  185. $arguments = array(Router::$method, Router::$arguments);
  186. // The method becomes part of the arguments
  187. array_unshift(Router::$arguments, Router::$method);
  188. // Set the method to _remap
  189. Router::$method = '_remap';
  190. }
  191. elseif (isset($methods[Router::$method]) AND Router::$method[0] !== '_')
  192. {
  193. // Use the arguments normally
  194. $arguments = Router::$arguments;
  195. }
  196. elseif (isset($methods['_default']))
  197. {
  198. // Make the arguments routed
  199. $arguments = array(Router::$method, Router::$arguments);
  200. // The method becomes part of the arguments
  201. array_unshift(Router::$arguments, Router::$method);
  202. // Set the method to _default
  203. Router::$method = '_default';
  204. }
  205. else
  206. {
  207. // Method was not found, run the system.404 event
  208. Event::run('system.404');
  209. }
  210. // Initialize the controller
  211. $controller = new $controller;
  212. // Run system.post_controller_constructor
  213. Event::run('system.post_controller_constructor');
  214. // Controller method name, used for calling
  215. $method = Router::$method;
  216. // Stop the controller setup benchmark
  217. Benchmark::stop(SYSTEM_BENCHMARK.'_controller_setup');
  218. // Start the controller execution benchmark
  219. Benchmark::start(SYSTEM_BENCHMARK.'_controller_execution');
  220. if (empty($arguments))
  221. {
  222. // Call the controller method with no arguments
  223. $controller->$method();
  224. }
  225. else
  226. {
  227. // Manually call the controller for up to 4 arguments, to increase performance
  228. switch (count($arguments))
  229. {
  230. case 1:
  231. $controller->$method($arguments[0]);
  232. break;
  233. case 2:
  234. $controller->$method($arguments[0], $arguments[1]);
  235. break;
  236. case 3:
  237. $controller->$method($arguments[0], $arguments[1], $arguments[2]);
  238. break;
  239. case 4:
  240. $controller->$method($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
  241. break;
  242. default:
  243. // Resort to using call_user_func_array for many segments
  244. call_user_func_array(array($controller, $method), $arguments);
  245. break;
  246. }
  247. }
  248. // Run system.post_controller
  249. Event::run('system.post_controller');
  250. // Stop the controller execution benchmark
  251. Benchmark::stop(SYSTEM_BENCHMARK.'_controller_execution');
  252. }
  253. return self::$instance;
  254. }
  255. /**
  256. * Kohana output handler.
  257. *
  258. * @param string current output buffer
  259. * @return string
  260. */
  261. final public static function output_buffer($output)
  262. {
  263. // Run the send_headers event, specifically for cookies being set
  264. Event::has_run('system.send_headers') or Event::run('system.send_headers');
  265. // Set final output
  266. self::$output = $output;
  267. // Set and return the final output
  268. return $output;
  269. }
  270. /**
  271. * Triggers the shutdown of Kohana by closing the output buffer, runs the system.display event.
  272. *
  273. * @return void
  274. */
  275. public static function shutdown()
  276. {
  277. while (ob_get_level() > self::$buffer_level)
  278. {
  279. // Flush all open output buffers above the internal buffer
  280. ob_end_flush();
  281. }
  282. // This will flush the Kohana buffer, which sets self::$output
  283. (ob_get_level() === self::$buffer_level) and ob_end_clean();
  284. // Run the output event
  285. Event::run('system.display', self::$output);
  286. // Render the final output
  287. self::render(self::$output);
  288. }
  289. /**
  290. * Inserts global Kohana variables into the generated output and prints it.
  291. *
  292. * @param string final output that will displayed
  293. * @return void
  294. */
  295. public static function render($output)
  296. {
  297. // Fetch memory usage in MB
  298. $memory = function_exists('memory_get_usage') ? (memory_get_usage() / 1024 / 1024) : 0;
  299. // Fetch benchmark for page execution time
  300. $benchmark = Benchmark::get(SYSTEM_BENCHMARK.'_total_execution');
  301. if (Config::item('core.render_stats') === TRUE)
  302. {
  303. // Replace the global template variables
  304. $output = str_replace(
  305. array
  306. (
  307. '{kohana_version}',
  308. '{kohana_codename}',
  309. '{execution_time}',
  310. '{memory_usage}',
  311. '{included_files}',
  312. ),
  313. array
  314. (
  315. KOHANA_VERSION,
  316. KOHANA_CODENAME,
  317. $benchmark['time'],
  318. number_format($memory, 2).'MB',
  319. count(get_included_files()),
  320. ),
  321. $output
  322. );
  323. }
  324. if ($level = Config::item('core.output_compression') AND ini_get('output_handler') !== 'ob_gzhandler' AND (int) ini_get('zlib.output_compression') === 0)
  325. {
  326. if ($level < 1 OR $level > 9)
  327. {
  328. // Normalize the level to be an integer between 1 and 9. This
  329. // step must be done to prevent gzencode from triggering an error
  330. $level = max(1, min($level, 9));
  331. }
  332. if (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  333. {
  334. $compress = 'gzip';
  335. }
  336. elseif (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== FALSE)
  337. {
  338. $compress = 'deflate';
  339. }
  340. }
  341. if (isset($compress) AND $level > 0)
  342. {
  343. switch ($compress)
  344. {
  345. case 'gzip':
  346. // Compress output using gzip
  347. $output = gzencode($output, $level);
  348. break;
  349. case 'deflate':
  350. // Compress output using zlib (HTTP deflate)
  351. $output = gzdeflate($output, $level);
  352. break;
  353. }
  354. // This header must be sent with compressed content to prevent
  355. // browser caches from breaking
  356. header('Vary: Accept-Encoding');
  357. // Send the content encoding header
  358. header('Content-Encoding: '.$compress);
  359. // Sending Content-Length in CGI can result in unexpected behavior
  360. if (stripos(PHP_SAPI, 'cgi') === FALSE)
  361. {
  362. header('Content-Length: '.strlen($output));
  363. }
  364. }
  365. echo $output;
  366. }
  367. /**
  368. * Dual-purpose PHP error and exception handler. Uses the kohana_error_page
  369. * view to display the message.
  370. *
  371. * @param integer|object exception object or error code
  372. * @param string error message
  373. * @param string filename
  374. * @param integer line number
  375. * @return void
  376. */
  377. public static function exception_handler($exception, $message = NULL, $file = NULL, $line = NULL)
  378. {
  379. // PHP errors have 5 args, always
  380. $PHP_ERROR = (func_num_args() === 5);
  381. // Test to see if errors should be displayed
  382. if ($PHP_ERROR AND (error_reporting() & $exception) === 0)
  383. return;
  384. // This is useful for hooks to determine if a page has an error
  385. self::$has_error = TRUE;
  386. // Error handling will use exactly 5 args, every time
  387. if ($PHP_ERROR)
  388. {
  389. $code = $exception;
  390. $type = 'PHP Error';
  391. $template = 'kohana_error_page';
  392. }
  393. else
  394. {
  395. $code = $exception->getCode();
  396. $type = get_class($exception);
  397. $message = $exception->getMessage();
  398. $file = $exception->getFile();
  399. $line = $exception->getLine();
  400. $template = ($exception instanceof Kohana_Exception) ? $exception->getTemplate() : 'kohana_error_page';
  401. }
  402. if (is_numeric($code))
  403. {
  404. $codes = Kohana::lang('errors');
  405. if ( ! empty($codes[$code]))
  406. {
  407. list($level, $error, $description) = $codes[$code];
  408. }
  409. else
  410. {
  411. $level = 1;
  412. $error = $PHP_ERROR ? 'Unknown Error' : get_class($exception);
  413. $description = '';
  414. }
  415. }
  416. else
  417. {
  418. // Custom error message, this will never be logged
  419. $level = 5;
  420. $error = $code;
  421. $description = '';
  422. }
  423. // Remove the DOCROOT from the path, as a security precaution
  424. $file = str_replace('\\', '/', realpath($file));
  425. $file = preg_replace('|^'.preg_quote(DOCROOT).'|', '', $file);
  426. if (Config::item('log.threshold') >= $level)
  427. {
  428. // Log the error
  429. Log::add('error', Kohana::lang('core.uncaught_exception', $type, $message, $file, $line));
  430. }
  431. if ($PHP_ERROR)
  432. {
  433. $description = Kohana::lang('errors.'.E_RECOVERABLE_ERROR);
  434. $description = is_array($description) ? $description[2] : '';
  435. }
  436. else
  437. {
  438. if (method_exists($exception, 'sendHeaders'))
  439. {
  440. // Send the headers if they have not already been sent
  441. headers_sent() or $exception->sendHeaders();
  442. }
  443. }
  444. while (ob_get_level() > self::$buffer_level)
  445. {
  446. // Clean all active output buffers
  447. ob_end_clean();
  448. }
  449. // Clear the current buffer
  450. (ob_get_level() === self::$buffer_level) and ob_clean();
  451. // Test if display_errors is on
  452. if (Config::item('core.display_errors'))
  453. {
  454. if ( ! IN_PRODUCTION AND $line != FALSE)
  455. {
  456. // Remove the first entry of debug_backtrace(), it is the exception_handler call
  457. $trace = $PHP_ERROR ? array_slice(debug_backtrace(), 1) : $exception->getTrace();
  458. // Beautify backtrace
  459. $trace = self::backtrace($trace);
  460. }
  461. // Load the error
  462. include self::find_file('views', empty($template) ? 'kohana_error_page' : $template);
  463. }
  464. else
  465. {
  466. // Get the i18n messages
  467. $error = Kohana::lang('core.generic_error');
  468. $message = sprintf(Kohana::lang('core.errors_disabled'), url::site(), url::site(Router::$current_uri));
  469. // Load the errors_disabled view
  470. include self::find_file('views', 'kohana_error_disabled');
  471. }
  472. // Run the system.shutdown event
  473. Event::has_run('system.shutdown') or Event::run('system.shutdown');
  474. // Turn off error reporting
  475. error_reporting(0);
  476. exit;
  477. }
  478. /**
  479. * Displays a 404 page.
  480. *
  481. * @throws Kohana_404_Exception
  482. * @param string URI of page
  483. * @param string custom template
  484. * @return void
  485. */
  486. public static function show_404($page = FALSE, $template = FALSE)
  487. {
  488. throw new Kohana_404_Exception($page, $template);
  489. }
  490. /**
  491. * Show a custom error message.
  492. *
  493. * @throws Kohana_User_Exception
  494. * @param string error title
  495. * @param string error message
  496. * @param string custom template
  497. * @return void
  498. */
  499. public static function show_error($title, $message, $template = FALSE)
  500. {
  501. throw new Kohana_User_Exception($title, $message, $template);
  502. }
  503. /**
  504. * Save data to a simple cache file. This should only be used internally, and
  505. * is NOT a replacement for the Cache library.
  506. *
  507. * @param string cache name
  508. * @param mixed data to cache
  509. * @return boolean
  510. */
  511. public static function save_cache($name, $data = NULL)
  512. {
  513. static $cache_time;
  514. if ($cache_time === NULL)
  515. {
  516. // Load cache time from config
  517. $cache_time = Config::item('core.internal_cache');
  518. }
  519. if ($cache_time > 0)
  520. {
  521. $path = APPPATH.'cache/kohana_'.$name;
  522. if ($data === NULL)
  523. {
  524. // Delete cache
  525. return unlink($path);
  526. }
  527. else
  528. {
  529. // Write data to cache file
  530. return (bool) file_put_contents($path, serialize($data));
  531. }
  532. }
  533. else
  534. {
  535. // No caching enabled
  536. return FALSE;
  537. }
  538. }
  539. /**
  540. * Load data from a simple cache file. This should only be used internally,
  541. * and is NOT a replacement for the Cache library.
  542. *
  543. * @param string cache name
  544. * @return mixed
  545. */
  546. public static function load_cache($name)
  547. {
  548. static $cache_time;
  549. if ($cache_time === NULL)
  550. {
  551. // Load cache time from config
  552. $cache_time = Config::item('core.internal_cache');
  553. }
  554. if ($cache_time > 0)
  555. {
  556. $path = APPPATH.'cache/kohana_'.$name;
  557. if (file_exists($path))
  558. {
  559. // Check the file modification time
  560. if ((time() - filemtime($path)) < $cache_time)
  561. {
  562. // Cache is valid
  563. return unserialize(file_get_contents($path));
  564. }
  565. else
  566. {
  567. // Cache is invalid, delete it
  568. unlink($path);
  569. }
  570. }
  571. }
  572. // No cache found
  573. return NULL;
  574. }
  575. /**
  576. * Provides class auto-loading.
  577. *
  578. * @throws Kohana_Exception
  579. * @param string name of class
  580. * @return bool
  581. */
  582. public static function auto_load($class)
  583. {
  584. static $prefix;
  585. // Set the extension prefix
  586. empty($prefix) and $prefix = Config::item('core.extension_prefix');
  587. if (class_exists($class, FALSE))
  588. return TRUE;
  589. if (($type = strrpos($class, '_')) !== FALSE)
  590. {
  591. // Find the class suffix
  592. $type = substr($class, $type + 1);
  593. }
  594. switch ($type)
  595. {
  596. case 'Core':
  597. $type = 'libraries';
  598. $file = substr($class, 0, -5);
  599. break;
  600. case 'Controller':
  601. $type = 'controllers';
  602. // Lowercase filename
  603. $file = strtolower(substr($class, 0, -11));
  604. break;
  605. case 'Model':
  606. $type = 'models';
  607. // Lowercase filename
  608. $file = strtolower(substr($class, 0, -6));
  609. break;
  610. case 'Driver':
  611. $type = 'libraries/drivers';
  612. $file = str_replace('_', '/', substr($class, 0, -7));
  613. break;
  614. default:
  615. // This can mean either a library or a helper, but libraries must
  616. // always be capitalized, so we check if the first character is
  617. // lowercase. If it is, we are loading a helper, not a library.
  618. $type = (ord($class[0]) > 96) ? 'helpers' : 'libraries';
  619. $file = $class;
  620. break;
  621. }
  622. // If the file doesn't exist, just return
  623. if (($filepath = self::find_file($type, $file)) === FALSE)
  624. {
  625. return FALSE;
  626. }
  627. // Load the requested file
  628. require_once $filepath;
  629. if ($type === 'libraries' OR $type === 'helpers')
  630. {
  631. if ($extension = self::find_file($type, $prefix.$class))
  632. {
  633. // Load the class extension
  634. require_once $extension;
  635. }
  636. elseif (substr($class, -5) !== '_Core' AND class_exists($class.'_Core', FALSE))
  637. {
  638. // Transparent class extensions are handled using eval. This is
  639. // a disgusting hack, but it works very well.
  640. eval('class '.$class.' extends '.$class.'_Core { }');
  641. }
  642. }
  643. return class_exists($class, FALSE);
  644. }
  645. /**
  646. * Find a resource file in a given directory.
  647. *
  648. * @throws Kohana_Exception if file is required and not found
  649. * @param string directory to search in
  650. * @param string filename to look for (including extension only if 4th parameter is TRUE)
  651. * @param boolean file required
  652. * @param boolean file extension
  653. * @return array if the type is config, i18n or l10n
  654. * @return string if the file is found
  655. * @return FALSE if the file is not found
  656. */
  657. public static function find_file($directory, $filename, $required = FALSE, $ext = FALSE)
  658. {
  659. // Users can define their own extensions, css, xml, html, etc
  660. $ext = ($ext === FALSE) ? EXT : '.'.ltrim($ext, '.');
  661. // Search path
  662. $search = $directory.'/'.$filename.$ext;
  663. if (isset(self::$paths[$search]))
  664. {
  665. // Return the cached path
  666. return self::$paths[$search];
  667. }
  668. // Nothing found, yet
  669. $found = NULL;
  670. if ($directory === 'config' OR $directory === 'i18n' OR $directory === 'l10n')
  671. {
  672. // Search from SYSPATH up
  673. foreach (array_reverse(Config::include_paths()) as $path)
  674. {
  675. if (is_file($path.$search))
  676. {
  677. // A file has been found
  678. $found[] = $path.$search;
  679. }
  680. }
  681. }
  682. else
  683. {
  684. // Find the file and return its filename
  685. foreach (Config::include_paths() as $path)
  686. {
  687. if (is_file($path.$search))
  688. {
  689. // A file has been found
  690. $found = $path.$search;
  691. break;
  692. }
  693. }
  694. }
  695. if ($found === NULL)
  696. {
  697. if ($required === TRUE)
  698. {
  699. // If the file is required, throw an exception
  700. throw new Kohana_Exception('core.resource_not_found', Kohana::lang('core.'.inflector::singular($directory)), $filename);
  701. }
  702. else
  703. {
  704. // Nothing was found
  705. $found = FALSE;
  706. }
  707. }
  708. // Add paths to cache
  709. self::$paths[$search] = $found;
  710. if (self::$paths_changed === FALSE)
  711. {
  712. // Cache has changed
  713. self::$paths_changed = TRUE;
  714. // Save cache at shutdown
  715. Event::add('system.shutdown', array(__CLASS__, 'write_path_cache'));
  716. }
  717. return $found;
  718. }
  719. /**
  720. * Writes the file path cache.
  721. *
  722. * @return boolean
  723. */
  724. public static function write_path_cache()
  725. {
  726. // Save updated cache
  727. return Kohana::save_cache('file_paths', self::$paths);
  728. }
  729. /**
  730. * Lists all files and directories in a resource path.
  731. *
  732. * @param string directory to search
  733. * @param boolean list all files to the maximum depth?
  734. * @param string full path to search (used for recursion, *never* set this manually)
  735. * @return array filenames and directories
  736. */
  737. public static function list_files($directory, $recursive = FALSE, $path = FALSE)
  738. {
  739. $files = array();
  740. if ($path === FALSE)
  741. {
  742. foreach (Config::include_paths() as $path)
  743. {
  744. // Recursively get and merge all files
  745. $files = array_merge($files, self::list_files($directory, $recursive, $path.$directory));
  746. }
  747. }
  748. else
  749. {
  750. $path = rtrim($path, '/').'/';
  751. if (is_readable($path))
  752. {
  753. foreach (glob($path.'*') as $index => $item)
  754. {
  755. $files[] = $item = str_replace('\\', '/', $item);
  756. // Handle recursion
  757. if (is_dir($item) AND $recursive == TRUE)
  758. {
  759. // Filename should only be the basename
  760. $item = pathinfo($item, PATHINFO_BASENAME);
  761. // Append sub-directory search
  762. $files = array_merge($files, self::list_files($directory, TRUE, $path.$item));
  763. }
  764. }
  765. }
  766. }
  767. return $files;
  768. }
  769. /**
  770. * Fetch an i18n language item.
  771. *
  772. * @param string language key to fetch
  773. * @param array additional information to insert into the line
  774. * @return string i18n language string, or the requested key if the i18n item is not found
  775. */
  776. public static function lang($key, $args = array())
  777. {
  778. static $language = array();
  779. // Extract the main group from the key
  780. $group = explode('.', $key, 2);
  781. $group = $group[0];
  782. if (empty($language[$group]))
  783. {
  784. // Messages from this file
  785. $messages = array();
  786. // The name of the file to search for
  787. $filename = Config::item('locale.language').'/'.$group;
  788. // Loop through the files and include each one, so SYSPATH files
  789. // can be overloaded by more localized files
  790. foreach (self::find_file('i18n', $filename) as $file)
  791. {
  792. include $file;
  793. // Merge in configuration
  794. if ( ! empty($lang) AND is_array($lang))
  795. {
  796. foreach ($lang as $k => $v)
  797. {
  798. $messages[$k] = $v;
  799. }
  800. }
  801. }
  802. // Cache the type
  803. $language[$group] = $messages;
  804. }
  805. // Get the line from the language
  806. $line = self::key_string($language, $key);
  807. // Return the key string as fallback
  808. if ($line === NULL)
  809. {
  810. Log::add('error', 'Missing i18n entry '.$key.' for language '.Config::item('locale.language'));
  811. return $key;
  812. }
  813. if (is_string($line) AND func_num_args() > 1)
  814. {
  815. $args = array_slice(func_get_args(), 1);
  816. // Add the arguments into the line
  817. $line = vsprintf($line, is_array($args[0]) ? $args[0] : $args);
  818. }
  819. return $line;
  820. }
  821. /**
  822. * Fetch an i10n locale item.
  823. *
  824. * @param string locale key to fetch
  825. * @return mixed NULL if the key is not found
  826. */
  827. public static function locale($key)
  828. {
  829. static $locale = array();
  830. if (empty($locale))
  831. {
  832. // Messages from this file
  833. $messages = array();
  834. // The name of the file to search for
  835. $filename = Config::item('locale.country');
  836. // Loop through the files and include each one, so SYSPATH files
  837. // can be overloaded by more localized files
  838. foreach (self::find_file('l10n', Config::item('locale.language').'/'.$filename) as $file)
  839. {
  840. include $file;
  841. // Merge in configuration
  842. if ( ! empty($locale) AND is_array($locale))
  843. {
  844. foreach ($locale as $k => $v)
  845. {
  846. $locale[$k] = $v;
  847. }
  848. }
  849. }
  850. }
  851. // Get the line from the language
  852. $line = self::key_string($locale, $key);
  853. // Return the key string as fallback
  854. if ($line === NULL)
  855. {
  856. Log::add('error', 'Missing i10n entry '.$key.' for locale '.$filename);
  857. return NULL;
  858. }
  859. return $line;
  860. }
  861. /**
  862. * Returns the value of a key, defined by a 'dot-noted' string, from an array.
  863. *
  864. * @param string dot-noted string: foo.bar.baz
  865. * @param array array to search
  866. * @return string if the key is found
  867. * @return void if the key is not found
  868. */
  869. public static function key_string($array, $keys)
  870. {
  871. // No array to search
  872. if ((empty($keys) AND is_string($keys)) OR (empty($array) AND is_array($array)))
  873. return NULL;
  874. if (substr($keys, -2) === '.*')
  875. {
  876. // Remove the wildcard from the keys
  877. $keys = substr($keys, 0, -2);
  878. }
  879. // Prepare for loop
  880. $keys = explode('.', $keys);
  881. // Loop down and find the key
  882. do
  883. {
  884. // Get the current key
  885. $key = array_shift($keys);
  886. // Value is set, dig deeper or return
  887. if (isset($array[$key]))
  888. {
  889. // If the key is an array, and we haven't hit bottom, prepare
  890. // for the next loop by re-referencing to the next child
  891. if (is_array($array[$key]) AND ! empty($keys))
  892. {
  893. $array =& $array[$key];
  894. }
  895. else
  896. {
  897. // Requested key was found
  898. return $array[$key];
  899. }
  900. }
  901. else
  902. {
  903. // Requested key is not set
  904. break;
  905. }
  906. }
  907. while ( ! empty($keys));
  908. // We return NULL, because it's less common than FALSE
  909. return NULL;
  910. }
  911. /**
  912. * Sets values in an array by using a 'dot-noted' string.
  913. *
  914. * @param array array to set keys in (reference)
  915. * @param string dot-noted string: foo.bar.baz
  916. * @return mixed fill value for the key
  917. * @return void
  918. */
  919. public static function key_string_set( & $array, $keys, $fill = NULL)
  920. {
  921. if (is_object($array) AND ($array instanceof ArrayObject))
  922. {
  923. // Copy the array
  924. $array_copy = $array->getArrayCopy();
  925. // Is an object
  926. $array_object = TRUE;
  927. }
  928. else
  929. {
  930. if ( ! is_array($array))
  931. {
  932. // Must always be an array
  933. $array = (array) $array;
  934. }
  935. // Copy is a reference to the array
  936. $array_copy =& $array;
  937. }
  938. if (empty($keys))
  939. return $array;
  940. // Create keys
  941. $keys = explode('.', $keys);
  942. // Create reference to the array
  943. $row =& $array_copy;
  944. for ($i = 0, $end = count($keys) - 1; $i <= $end; $i++)
  945. {
  946. // Get the current key
  947. $key = $keys[$i];
  948. if ( ! isset($row[$key]))
  949. {
  950. if (isset($keys[$i + 1]))
  951. {
  952. // Make the value an array
  953. $row[$key] = array();
  954. }
  955. else
  956. {
  957. // Add the fill key
  958. $row[$key] = $fill;
  959. }
  960. }
  961. elseif (isset($keys[$i + 1]))
  962. {
  963. // Make the value an array
  964. $row[$key] = (array) $row[$key];
  965. }
  966. // Go down a level, creating a new row reference
  967. $row =& $row[$key];
  968. }
  969. if (isset($array_object))
  970. {
  971. // Swap the array back in
  972. $array->exchangeArray($array_copy);
  973. }
  974. }
  975. /**
  976. * Retrieves current user agent information:
  977. * keys: browser, version, platform, mobile, robot, referrer, languages, charsets
  978. * tests: is_browser, is_mobile, is_robot, accept_lang, accept_charset
  979. *
  980. * @param string key or test name
  981. * @param string used with "accept" tests: user_agent(accept_lang, en)
  982. * @return array languages and charsets
  983. * @return string all other keys
  984. * @return boolean all tests
  985. */
  986. public static function user_agent($key = 'agent', $compare = NULL)
  987. {
  988. static $info;
  989. // Return the raw string
  990. if ($key === 'agent')
  991. return Kohana::$user_agent;
  992. if ($info === NULL)
  993. {
  994. // Parse the user agent and extract basic information
  995. foreach (Config::item('user_agents') as $type => $data)
  996. {
  997. foreach ($data as $agent => $name)
  998. {
  999. if (stripos(Kohana::$user_agent, $agent) !== FALSE)
  1000. {
  1001. if ($type === 'browser' AND preg_match('|'.preg_quote($agent).'[^0-9.]*+([0-9.]+)|i', Kohana::$user_agent, $match))
  1002. {
  1003. // Set the browser version
  1004. $info['version'] = $match[1];
  1005. }
  1006. // Set the agent name
  1007. $info[$type] = $name;
  1008. break;
  1009. }
  1010. }
  1011. }
  1012. }
  1013. if (empty($info[$key]))
  1014. {
  1015. switch ($key)
  1016. {
  1017. case 'is_robot':
  1018. case 'is_browser':
  1019. case 'is_mobile':
  1020. // A boolean result
  1021. $return = ! empty($info[substr($key, 3)]);
  1022. break;
  1023. case 'languages':
  1024. $return = array();
  1025. if ( ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
  1026. {
  1027. if (preg_match_all('/[-a-z]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])), $matches))
  1028. {
  1029. // Found a result
  1030. $return = $matches[0];
  1031. }
  1032. }
  1033. break;
  1034. case 'charsets':
  1035. $return = array();
  1036. if ( ! empty($_SERVER['HTTP_ACCEPT_CHARSET']))
  1037. {
  1038. if (preg_match_all('/[-a-z0-9]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_CHARSET'])), $matches))
  1039. {
  1040. // Found a result
  1041. $return = $matches[0];
  1042. }
  1043. }
  1044. break;
  1045. case 'referrer':
  1046. if ( ! empty($_SERVER['HTTP_REFERER']))
  1047. {
  1048. // Found a result
  1049. $return = trim($_SERVER['HTTP_REFERER']);
  1050. }
  1051. break;
  1052. }
  1053. // Cache the return value
  1054. isset($return) and $info[$key] = $return;
  1055. }
  1056. if ( ! empty($compare))
  1057. {
  1058. // The comparison must always be lowercase
  1059. $compare = strtolower($compare);
  1060. switch ($key)
  1061. {
  1062. case 'accept_lang':
  1063. // Check if the lange is accepted
  1064. return in_array($compare, Kohana::user_agent('languages'));
  1065. break;
  1066. case 'accept_charset':
  1067. // Check if the charset is accepted
  1068. return in_array($compare, Kohana::user_agent('charsets'));
  1069. break;
  1070. default:
  1071. // Invalid comparison
  1072. return FALSE;
  1073. break;
  1074. }
  1075. }
  1076. // Return the key, if set
  1077. return isset($info[$key]) ? $info[$key] : NULL;
  1078. }
  1079. /**
  1080. * Quick debugging of any variable. Any number of parameters can be set.
  1081. *
  1082. * @return string
  1083. */
  1084. public static function debug()
  1085. {
  1086. if (func_num_args() === 0)
  1087. return;
  1088. // Get params
  1089. $params = func_get_args();
  1090. $output = array();
  1091. foreach ($params as $var)
  1092. {
  1093. $output[] = '<pre>('.gettype($var).') '.html::specialchars(print_r($var, TRUE)).'</pre>';
  1094. }
  1095. return implode("\n", $output);
  1096. }
  1097. /**
  1098. * Displays nice backtrace information.
  1099. * @see http://php.net/debug_backtrace
  1100. *
  1101. * @param array backtrace generated by an exception or debug_backtrace
  1102. * @return string
  1103. */
  1104. public static function backtrace($trace)
  1105. {
  1106. if ( ! is_array($trace))
  1107. return;
  1108. // Final output
  1109. $output = array();
  1110. foreach ($trace as $entry)
  1111. {
  1112. $temp = '<li>';
  1113. if (isset($entry['file']))
  1114. {
  1115. $temp .= Kohana::lang('core.error_file_line', preg_replace('!^'.preg_quote(DOCROOT).'!', '', $entry['file']), $entry['line']);
  1116. }
  1117. $temp .= '<pre>';
  1118. if (isset($entry['class']))
  1119. {
  1120. // Add class and call type
  1121. $temp .= $entry['class'].$entry['type'];
  1122. }
  1123. // Add function
  1124. $temp .= $entry['function'].'( ';
  1125. // Add function args
  1126. if (isset($entry['args']) AND is_array($entry['args']))
  1127. {
  1128. // Separator starts as nothing
  1129. $sep = '';
  1130. while ($arg = array_shift($entry['args']))
  1131. {
  1132. if (is_string($arg) AND is_file($arg))
  1133. {
  1134. // Remove docroot from filename
  1135. $arg = preg_replace('!^'.preg_quote(DOCROOT).'!', '', $arg);
  1136. }
  1137. $temp .= $sep.html::specialchars(print_r($arg, TRUE));
  1138. // Change separator to a comma
  1139. $sep = ', ';
  1140. }
  1141. }
  1142. $temp .= ' )</pre></li>';
  1143. $output[] = $temp;
  1144. }
  1145. return '<ul class="backtrace">'.implode("\n", $output).'</ul>';
  1146. }
  1147. } // End Kohana
  1148. /**
  1149. * Creates a generic i18n exception.
  1150. */
  1151. class Kohana_Exception extends Exception {
  1152. // Template file
  1153. protected $template = 'kohana_error_page';
  1154. // Header
  1155. protected $header = FALSE;
  1156. // Error code
  1157. protected $code = E_KOHANA;
  1158. /**
  1159. * Set exception message.
  1160. *
  1161. * @param string i18n language key for the message
  1162. * @param array addition line parameters
  1163. */
  1164. public function __construct($error)
  1165. {
  1166. $args = array_slice(func_get_args(), 1);
  1167. // Fetch the error message
  1168. $message = Kohana::lang($error, $args);
  1169. if ($message === $error OR empty($message))
  1170. {
  1171. // Unable to locate the message for the error
  1172. $message = 'Unknown Exception: '.$error;
  1173. }
  1174. // Sets $this->message the proper way
  1175. parent::__construct($message);
  1176. }
  1177. /**
  1178. * Magic method for converting an object to a string.
  1179. *
  1180. * @return string i18n message
  1181. */
  1182. public function __toString()
  1183. {
  1184. return (string) $this->message;
  1185. }
  1186. /**
  1187. * Fetch the template name.
  1188. *
  1189. * @return string
  1190. */
  1191. public function getTemplate()
  1192. {
  1193. return $this->template;
  1194. }
  1195. /**
  1196. * Sends an Internal Server Error header.
  1197. *
  1198. * @return void
  1199. */
  1200. public function sendHeaders()
  1201. {
  1202. // Send the 500 header
  1203. header('HTTP/1.1 500 Internal Server Error');
  1204. }
  1205. } // End Kohana Exception
  1206. /**
  1207. * Creates a custom exception.
  1208. */
  1209. class Kohana_User_Exception extends Kohana_Exception {
  1210. /**
  1211. * Set exception title and message.
  1212. *
  1213. * @param string exception title string
  1214. * @param string exception message string
  1215. * @param string custom error template
  1216. */
  1217. public function __construct($title, $message, $template = FALSE)
  1218. {
  1219. Exception::__construct($message);
  1220. $this->code = $title;
  1221. if ($template !== FALSE)
  1222. {
  1223. $this->template = $template;
  1224. }
  1225. }
  1226. } // End Kohana PHP Exception
  1227. /**
  1228. * Creates a Page Not Found exception.
  1229. */
  1230. class Kohana_404_Exception extends Kohana_Exception {
  1231. protected $code = E_PAGE_NOT_FOUND;
  1232. /**
  1233. * Set internal properties.
  1234. *
  1235. * @param string URL of page
  1236. * @param string custom error template
  1237. */
  1238. public function __construct($page = FALSE, $template = FALSE)
  1239. {
  1240. if ($page === FALSE)
  1241. {
  1242. // Construct the page URI using Router properties
  1243. $page = Router::$current_uri.Router::$url_suffix.Router::$query_string;
  1244. }
  1245. Exception::__construct(Kohana::lang('core.page_not_found', $page));
  1246. $this->template = $template;
  1247. }
  1248. /**
  1249. * Sends "File Not Found" headers, to emulate server behavior.
  1250. *
  1251. * @return void
  1252. */
  1253. public function sendHeaders()
  1254. {
  1255. // Send the 404 header
  1256. header('HTTP/1.1 404 File Not Found');
  1257. }
  1258. } // End Kohana 404 Exception