reports: Fix id of custom date selector
[ninja.git] / system / core / Kohana.php
bloba9318cce10c2e3cc67fefc9e0d1be2dda539d4fb
1 <?php defined('SYSPATH') OR die('No direct access allowed.');
3 require_once('op5/log.php');
6 # Hack to enable PHP version < 5.2
8 if (!defined('E_RECOVERABLE_ERROR'))
9 define('E_RECOVERABLE_ERROR', 4096);
10 if (!defined('FILTER_VALIDATE_IP'))
11 define('FILTER_VALIDATE_IP', false);
12 if (!defined('FILTER_FLAG_IPV4'))
13 define('FILTER_FLAG_IPV4', false);
15 if (!function_exists('spl_object_hash')) {
16 /**
17 * Returns the hash of the unique identifier for the object.
19 * @param object $object Object
20 * @author Rafael M. Salvioni
21 * @return string
23 function spl_object_hash($object)
25 if (!is_object($object))
26 $object = arr::to_object($object);
27 if (is_object($object)) {
28 ob_start(); var_dump($object); $dump = ob_get_contents(); ob_end_clean();
29 if (preg_match('/^object\(([a-z0-9_]+)\)\#(\d)+/i', $dump, $match)) {
30 return md5($match[1] . $match[2]);
33 trigger_error(__FUNCTION__ . "() expects parameter 1 to be object", E_USER_WARNING);
34 return null;
38 if (!function_exists('filter_var')) {
39 function filter_var($input) {
40 return $input;
44 # End PHP version hack
47 /**
48 * Provides Kohana-specific helper functions. This is where the magic happens!
50 * $Id: Kohana.php 3917 2009-01-21 03:06:22Z zombor $
52 * @package Core
53 * @author Kohana Team
54 * @copyright (c) 2007-2008 Kohana Team
55 * @license http://kohanaphp.com/license.html
57 final class Kohana {
59 // The singleton instance of the controller
60 public static $instance;
62 // Output buffering level
63 private static $buffer_level;
65 // Will be set to TRUE when an exception is caught
66 public static $has_error = FALSE;
68 // The final output that will displayed by Kohana
69 public static $output = '';
71 // The current user agent
72 public static $user_agent;
74 // The current locale
75 public static $locale;
77 // Configuration
78 private static $configuration;
80 // Include paths
81 private static $include_paths;
83 // Logged messages
84 private static $log;
86 // Cache lifetime
87 private static $cache_lifetime;
89 // Internal caches and write status
90 private static $internal_cache = array();
91 private static $write_cache;
93 /**
94 * Sets up the PHP environment. Adds error/exception handling, output
95 * buffering, and adds an auto-loading method for loading classes.
97 * This method is run immediately when this file is loaded, and is
98 * benchmarked as environment_setup.
100 * For security, this function also destroys the $_REQUEST global variable.
101 * Using the proper global (GET, POST, COOKIE, etc) is inherently more secure.
102 * The recommended way to fetch a global variable is using the Input library.
103 * @see http://www.php.net/globals
105 * @return void
107 public static function setup()
109 static $run;
111 // This function can only be run once
112 if ($run === TRUE)
113 return;
115 // Start the environment setup benchmark
116 Benchmark::start(SYSTEM_BENCHMARK.'_environment_setup');
118 // Define Kohana error constant
119 define('E_KOHANA', 42);
121 // Define 404 error constant
122 define('E_PAGE_NOT_FOUND', 43);
124 // Define database error constant
125 define('E_DATABASE_ERROR', 44);
127 if (self::$cache_lifetime = self::config('core.internal_cache'))
129 // Load cached configuration and language files
130 self::$internal_cache['configuration'] = self::cache('configuration', self::$cache_lifetime);
131 self::$internal_cache['language'] = self::cache('language', self::$cache_lifetime);
133 // Load cached file paths
134 self::$internal_cache['find_file_paths'] = self::cache('find_file_paths', self::$cache_lifetime);
136 // Enable cache saving
137 Event::add('system.shutdown', array(__CLASS__, 'internal_cache_save'));
140 // Disable notices and "strict" errors
141 $ER = error_reporting(~E_NOTICE & ~E_STRICT);
143 // Set the user agent
144 self::$user_agent = trim($_SERVER['HTTP_USER_AGENT']);
146 if (function_exists('date_default_timezone_set'))
148 $timezone = self::config('locale.timezone');
150 // Set default timezone, due to increased validation of date settings
151 // which cause massive amounts of E_NOTICEs to be generated in PHP 5.2+
152 date_default_timezone_set(empty($timezone) ? date_default_timezone_get() : $timezone);
155 // Restore error reporting
156 error_reporting($ER);
158 // Start output buffering
159 ob_start(array(__CLASS__, 'output_buffer'));
161 // Save buffering level
162 self::$buffer_level = ob_get_level();
164 // Set autoloader
165 spl_autoload_register(array('Kohana', 'auto_load'));
167 // Set error handler
168 if (PHP_SAPI !== 'cli') {
169 set_error_handler(array('Kohana', 'exception_handler'));
171 // Set exception handler
172 set_exception_handler(array('Kohana', 'exception_handler'));
174 // Send default text/html UTF-8 header
175 header('Content-Type: text/html; charset=UTF-8');
178 // Load locales
179 $locales = self::config('locale.language');
181 // Make first locale UTF-8
182 $locales[0] .= '.UTF-8';
184 // Set locale information
185 self::$locale = setlocale(LC_ALL, $locales);
187 // Enable Kohana routing
188 Event::add('system.routing', array('Router', 'find_uri'));
189 Event::add('system.routing', array('Router', 'setup'));
191 // Enable Kohana controller initialization
192 Event::add('system.execute', array('Kohana', 'instance'));
194 // Enable Kohana 404 pages
195 Event::add('system.404', array('Kohana', 'show_404'));
197 // Enable Kohana output handling
198 Event::add('system.shutdown', array('Kohana', 'shutdown'));
200 if (self::config('core.enable_hooks') === TRUE)
202 // Find all the hook files
203 $hooks = self::list_files('hooks', TRUE);
205 foreach ($hooks as $file)
207 // Load the hook
208 include $file;
212 // Setup is complete, prevent it from being run again
213 $run = TRUE;
215 // Stop the environment setup routine
216 Benchmark::stop(SYSTEM_BENCHMARK.'_environment_setup');
220 * Loads the controller and initializes it. Runs the pre_controller,
221 * post_controller_constructor, and post_controller events. Triggers
222 * a system.404 event when the route cannot be mapped to a controller.
224 * This method is benchmarked as controller_setup and controller_execution.
226 * @return object instance of controller
228 public static function & instance()
230 if (self::$instance === NULL)
232 Benchmark::start(SYSTEM_BENCHMARK.'_controller_setup');
234 if (Router::$method[0] === '_')
236 // Do not allow access to hidden methods
237 Event::run('system.404');
240 // Include the Controller file
241 require Router::$controller_path;
245 // Start validation of the controller
246 $class = new ReflectionClass(ucfirst(Router::$controller).'_Controller');
248 catch (ReflectionException $e)
250 // Controller does not exist
251 Event::run('system.404');
254 if ($class->isAbstract() OR (IN_PRODUCTION AND $class->getConstant('ALLOW_PRODUCTION') == FALSE))
256 // Controller is not allowed to run in production
257 Event::run('system.404');
260 // Run system.pre_controller
261 Event::run('system.pre_controller');
263 // Create a new controller instance
264 $controller = $class->newInstance();
266 // Controller constructor has been executed
267 Event::run('system.post_controller_constructor', $controller);
271 // Load the controller method
272 $method = $class->getMethod(Router::$method);
274 if ($method->isProtected() or $method->isPrivate())
276 // Do not attempt to invoke protected methods
277 throw new ReflectionException('protected controller method');
280 // Default arguments
281 $arguments = Router::$arguments;
283 catch (ReflectionException $e)
285 // Use __call instead
286 $method = $class->getMethod('__call');
288 // Use arguments in __call format
289 $arguments = array(Router::$method, Router::$arguments);
292 // Stop the controller setup benchmark
293 Benchmark::stop(SYSTEM_BENCHMARK.'_controller_setup');
295 // Start the controller execution benchmark
296 Benchmark::start(SYSTEM_BENCHMARK.'_controller_execution');
298 // Execute the controller method
299 $method->invokeArgs($controller, $arguments);
301 // Controller method has been executed
302 Event::run('system.post_controller', $controller);
304 // Stop the controller execution benchmark
305 Benchmark::stop(SYSTEM_BENCHMARK.'_controller_execution');
308 return self::$instance;
312 * Get all include paths. APPPATH is the first path, followed by module
313 * paths in the order they are configured, follow by the SYSPATH.
315 * @param boolean re-process the include paths
316 * @return array
318 public static function include_paths($process = FALSE)
320 if ($process === TRUE)
322 // Add APPPATH as the first path
323 self::$include_paths = array(APPPATH);
325 foreach (glob(MODPATH.'*', GLOB_ONLYDIR) as $path)
327 self::$include_paths[] = $path.'/';
330 // Add SYSPATH as the last path
331 self::$include_paths[] = SYSPATH;
334 return self::$include_paths;
338 * Get a config item or group.
340 * @param string item name
341 * @param boolean force a forward slash (/) at the end of the item
342 * @param boolean is the item required?
343 * @return mixed
345 public static function config($key, $slash = FALSE, $required = TRUE)
347 if (self::$configuration === NULL)
349 // Load core configuration
350 self::$configuration['core'] = self::config_load('core');
352 // Re-parse the include paths
353 self::include_paths(TRUE);
356 // Get the group name from the key
357 $group = explode('.', $key, 2);
358 $group = $group[0];
360 if ( ! isset(self::$configuration[$group]))
362 // Load the configuration group
363 self::$configuration[$group] = self::config_load($group, $required);
366 // Get the value of the key string
367 $value = self::key_string(self::$configuration, $key);
369 if ($slash === TRUE AND is_string($value) AND $value !== '')
371 // Force the value to end with "/"
372 $value = rtrim($value, '/').'/';
375 return $value;
379 * Sets a configuration item, if allowed.
381 * @param string config key string
382 * @param string config value
383 * @return boolean
385 public static function config_set($key, $value)
387 // Do this to make sure that the config array is already loaded
388 self::config($key);
390 if (substr($key, 0, 7) === 'routes.')
392 // Routes cannot contain sub keys due to possible dots in regex
393 $keys = explode('.', $key, 2);
395 else
397 // Convert dot-noted key string to an array
398 $keys = explode('.', $key);
401 // Used for recursion
402 $conf =& self::$configuration;
403 $last = count($keys) - 1;
405 foreach ($keys as $i => $k)
407 if ($i === $last)
409 $conf[$k] = $value;
411 else
413 $conf =& $conf[$k];
417 if ($key === 'core.modules')
419 // Reprocess the include paths
420 self::include_paths(TRUE);
423 return TRUE;
427 * Load a config file.
429 * @param string config filename, without extension
430 * @param boolean is the file required?
431 * @return array
433 public static function config_load($name, $required = TRUE)
435 if ($name === 'core')
437 // Load the application configuration file
438 require APPPATH.'config/config'.EXT;
439 if (is_file(APPPATH.'config/custom/config'.EXT))
440 include APPPATH.'config/custom/config'.EXT;
442 if ( ! isset($config['site_domain']))
444 // Invalid config file
445 die('Your Kohana application configuration file is not valid.');
448 return $config;
451 if (isset(self::$internal_cache['configuration'][$name]))
452 return self::$internal_cache['configuration'][$name];
454 // Load matching configs
455 $configuration = array();
457 if ($files = self::find_file('config', $name, $required))
460 foreach ($files as $file)
462 require $file;
464 if (isset($config) AND is_array($config))
466 // Merge in configuration
467 $configuration = array_merge($configuration, $config);
472 if ($files = self::find_file('config/custom', $name, false))
474 foreach ($files as $file)
476 require $file;
477 if (isset($config) and is_array($config))
479 $configuration = array_merge($configuration, $config);
484 if ( ! isset(self::$write_cache['configuration']))
486 // Cache has changed
487 self::$write_cache['configuration'] = TRUE;
490 return self::$internal_cache['configuration'][$name] = $configuration;
494 * Clears a config group from the cached configuration.
496 * @param string config group
497 * @return void
499 public static function config_clear($group)
501 // Remove the group from config
502 unset(self::$configuration[$group], self::$internal_cache['configuration'][$group]);
504 if ( ! isset(self::$write_cache['configuration']))
506 // Cache has changed
507 self::$write_cache['configuration'] = TRUE;
512 * Add a new message to the log.
514 * @param string type of message
515 * @param string message text
516 * @return void
518 public static function log($type, $message)
520 op5log::instance()->log('ninja', $type, $message);
524 * Load data from a simple cache file. This should only be used internally,
525 * and is NOT a replacement for the Cache library.
527 * @param string unique name of cache
528 * @param integer expiration in seconds
529 * @return mixed
531 public static function cache($name, $lifetime)
533 if ($lifetime > 0)
535 $path = APPPATH.'cache/kohana_'.$name;
537 if (is_file($path))
539 // Check the file modification time
540 if ((time() - filemtime($path)) < $lifetime)
542 // Cache is valid
543 return unserialize(file_get_contents($path));
545 else
547 // Cache is invalid, delete it
548 unlink($path);
553 // No cache found
554 return NULL;
558 * Save data to a simple cache file. This should only be used internally, and
559 * is NOT a replacement for the Cache library.
561 * @param string cache name
562 * @param mixed data to cache
563 * @param integer expiration in seconds
564 * @return boolean
566 public static function cache_save($name, $data, $lifetime)
568 if ($lifetime < 1)
569 return FALSE;
571 $path = APPPATH.'cache/kohana_'.$name;
573 if ($data === NULL)
575 // Delete cache
576 return (is_file($path) and unlink($path));
578 else
580 // Write data to cache file
581 return (bool) file_put_contents($path, serialize($data));
586 * Kohana output handler.
588 * @param string current output buffer
589 * @return string
591 public static function output_buffer($output)
593 if ( ! Event::has_run('system.send_headers'))
595 // Run the send_headers event, specifically for cookies being set
596 Event::run('system.send_headers');
599 // Set final output
600 self::$output = $output;
602 // Set and return the final output
603 return $output;
607 * Closes all open output buffers, either by flushing or cleaning all
608 * open buffers, including the Kohana output buffer.
610 * @param boolean disable to clear buffers, rather than flushing
611 * @return void
613 public static function close_buffers($flush = TRUE)
615 if (ob_get_level() >= self::$buffer_level)
617 // Set the close function
618 $close = ($flush === TRUE) ? 'ob_end_flush' : 'ob_end_clean';
620 while (ob_get_level() > self::$buffer_level)
622 // Flush or clean the buffer
623 $close();
626 // This will flush the Kohana buffer, which sets self::$output
627 ob_end_clean();
629 // Reset the buffer level
630 self::$buffer_level = ob_get_level();
635 * Triggers the shutdown of Kohana by closing the output buffer, runs the system.display event.
637 * @return void
639 public static function shutdown()
641 // Close output buffers
642 self::close_buffers(TRUE);
644 // Run the output event
645 Event::run('system.display', self::$output);
647 // Render the final output
648 self::render(self::$output);
652 * Inserts global Kohana variables into the generated output and prints it.
654 * @param string final output that will displayed
655 * @return void
657 public static function render($output)
659 // Fetch memory usage in MB
660 $memory = function_exists('memory_get_usage') ? (memory_get_usage() / 1024 / 1024) : 0;
662 // Fetch benchmark for page execution time
663 $benchmark = Benchmark::get(SYSTEM_BENCHMARK.'_total_execution');
665 if (self::config('core.render_stats') === TRUE)
667 // Replace the global template variables
668 $output = str_replace(
669 array
671 '{kohana_version}',
672 '{kohana_codename}',
673 '{execution_time}',
674 '{memory_usage}',
675 '{included_files}',
677 array
679 KOHANA_VERSION,
680 KOHANA_CODENAME,
681 $benchmark['time'],
682 number_format($memory, 2).'MB',
683 count(get_included_files()),
685 $output
689 if ($level = self::config('core.output_compression') AND ini_get('output_handler') !== 'ob_gzhandler' AND (int) ini_get('zlib.output_compression') === 0)
691 if ($level < 1 OR $level > 9)
693 // Normalize the level to be an integer between 1 and 9. This
694 // step must be done to prevent gzencode from triggering an error
695 $level = max(1, min($level, 9));
698 if (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
700 $compress = 'gzip';
702 elseif (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== FALSE)
704 $compress = 'deflate';
708 if (isset($compress) AND $level > 0)
710 switch ($compress)
712 case 'gzip':
713 // Compress output using gzip
714 $output = gzencode($output, $level);
715 break;
716 case 'deflate':
717 // Compress output using zlib (HTTP deflate)
718 $output = gzdeflate($output, $level);
719 break;
722 // This header must be sent with compressed content to prevent
723 // browser caches from breaking
724 header('Vary: Accept-Encoding');
726 // Send the content encoding header
727 header('Content-Encoding: '.$compress);
729 // Sending Content-Length in CGI can result in unexpected behavior
730 if (stripos(PHP_SAPI, 'cgi') === FALSE)
732 header('Content-Length: '.strlen($output));
736 echo $output;
740 * Displays a 404 page.
742 * @throws Kohana_404_Exception
743 * @param string URI of page
744 * @param string custom template
745 * @return void
747 public static function show_404($page = FALSE, $template = FALSE)
749 throw new Kohana_404_Exception($page, $template);
753 * Dual-purpose PHP error and exception handler. Uses the kohana_error_page
754 * view to display the message.
756 * @param integer|object exception object or error code
757 * @param string error message
758 * @param string filename
759 * @param integer line number
760 * @return void
762 public static function exception_handler($exception, $message = NULL, $file = NULL, $line = NULL)
764 try {
765 // PHP errors have 5 args, always
766 $PHP_ERROR = (func_num_args() === 5);
768 // Test to see if errors should be displayed
769 if ($PHP_ERROR AND (error_reporting() & $exception) === 0)
770 return;
772 // This is useful for hooks to determine if a page has an error
773 self::$has_error = TRUE;
775 // Error handling will use exactly 5 args, every time
776 if ($PHP_ERROR)
778 $code = $exception;
779 $type = 'PHP Error';
780 $template = 'kohana_error_page';
782 else
784 $code = $exception->getCode();
785 $type = get_class($exception);
786 $message = $exception->getMessage();
787 $file = $exception->getFile();
788 $line = $exception->getLine();
789 $template = ($exception instanceof Kohana_Exception) ? $exception->getTemplate() : 'kohana_error_page';
792 if (is_numeric($code))
794 $codes = self::lang('errors');
796 if ( ! empty($codes[$code]))
798 list($level, $error, $description) = $codes[$code];
800 else
802 $level = 1;
803 $error = $PHP_ERROR ? 'Unknown Error' : get_class($exception);
804 $description = '';
807 else
809 // Custom error message, this will never be logged
810 $level = 5;
811 $error = $code;
812 $description = '';
815 // Remove the DOCROOT from the path, as a security precaution
816 $file = str_replace('\\', '/', realpath($file));
817 $file = preg_replace('|^'.preg_quote(DOCROOT).'|', '', $file);
819 if ($level <= self::$configuration['core']['log_threshold'])
821 // Log the error
822 self::log('error', self::lang('core.uncaught_exception', $type, $message, $file, $line));
825 if ($PHP_ERROR)
827 $description = self::lang('errors.'.E_RECOVERABLE_ERROR);
828 $description = is_array($description) ? $description[2] : '';
830 if ( ! headers_sent())
832 header('HTTP/1.1 500 Internal Server Error');
835 else
837 if(!headers_sent()) {
838 if (method_exists($exception, 'sendHeaders'))
840 $exception->sendHeaders();
841 } else {
842 header('HTTP/1.1 500 Internal Server Error');
847 while (ob_get_level() > self::$buffer_level)
849 // Close open buffers
850 ob_end_clean();
853 // Test if display_errors is on
854 if (self::$configuration['core']['display_errors'] === TRUE)
856 if ($line != FALSE)
858 // Remove the first entry of debug_backtrace(), it is the exception_handler call
859 $trace = $PHP_ERROR ? array_slice(debug_backtrace(), 1) : $exception->getTrace();
861 // Beautify backtrace
862 $trace = self::backtrace($trace);
865 // Load the error
866 require self::find_file('views', empty($template) ? 'kohana_error_page' : $template);
868 else
870 // Get the i18n messages
871 $error = self::lang('core.generic_error');
872 $message = self::lang('core.errors_disabled', url::site(), url::site(Router::$current_uri));
874 // Load the errors_disabled view
875 require self::find_file('views', 'kohana_error_disabled');
878 if ( ! Event::has_run('system.shutdown'))
880 // Run the shutdown even to ensure a clean exit
881 Event::run('system.shutdown');
884 // Turn off error reporting
885 error_reporting(0);
886 } catch( Exception $e ) {
887 /* Exceptions in an exceptionhandler results in "Exception thrown without a stack trace in "Unkonwn"
888 * Better to just print the exception ugly, so we get some kind of useful information instaead
890 while( @ob_end_clean() ) {}
891 print "Exception during error handler: ".$e->getMessage()."\n";
892 print $e->getTraceAsString();
895 exit;
899 * Provides class auto-loading.
901 * @throws Kohana_Exception
902 * @param string name of class
903 * @return bool
905 public static function auto_load($class)
907 if (class_exists($class, FALSE))
908 return TRUE;
910 if (($suffix = strrpos($class, '_')) > 0)
912 // Find the class suffix
913 $suffix = substr($class, $suffix + 1);
915 else
917 // No suffix
918 $suffix = FALSE;
921 if ($suffix === 'Core')
923 $type = 'libraries';
924 $file = substr($class, 0, -5);
926 elseif ($suffix === 'Controller')
928 $type = 'controllers';
929 // Lowercase filename
930 $file = strtolower(substr($class, 0, -11));
932 elseif ($suffix === 'Model')
934 $type = 'models';
935 // Lowercase filename
936 $file = strtolower(substr($class, 0, -6));
938 elseif ($suffix === 'Driver')
940 $type = 'libraries/drivers';
941 $file = str_replace('_', '/', substr($class, 0, -7));
943 elseif ($suffix === 'Widget')
945 $type = 'widgets';
946 $classname = substr($class, 0, -7);
947 $file = $classname . '/' . $classname;
949 else
951 // This could be either a library or a helper, but libraries must
952 // always be capitalized, so we check if the first character is
953 // uppercase. If it is, we are loading a library, not a helper.
954 $type = ($class[0] < 'a') ? 'libraries' : 'helpers';
955 $file = $class;
958 if ($filename = self::find_file($type, $file))
960 // Load the class
961 require $filename;
963 else
965 // The class could not be found
966 return FALSE;
969 if ($filename = self::find_file($type, self::$configuration['core']['extension_prefix'].$class))
971 // Load the class extension
972 require $filename;
974 elseif ($suffix !== 'Core' AND class_exists($class.'_Core', FALSE))
976 // Class extension to be evaluated
977 $extension = 'class '.$class.' extends '.$class.'_Core { }';
979 // Start class analysis
980 $core = new ReflectionClass($class.'_Core');
982 if ($core->isAbstract())
984 // Make the extension abstract
985 $extension = 'abstract '.$extension;
988 // Transparent class extensions are handled using eval. This is
989 // a disgusting hack, but it gets the job done.
990 eval($extension);
993 return TRUE;
997 * Find a resource file in a given directory. Files will be located according
998 * to the order of the include paths. Configuration and i18n files will be
999 * returned in reverse order.
1001 * @throws Kohana_Exception if file is required and not found
1002 * @param string directory to search in
1003 * @param string filename to look for (including extension only if 4th parameter is TRUE)
1004 * @param boolean file required
1005 * @param string file extension
1006 * @return array if the type is config, i18n or l10n
1007 * @return string if the file is found
1008 * @return FALSE if the file is not found
1010 public static function find_file($directory, $filename, $required = FALSE, $ext = FALSE)
1012 // NOTE: This test MUST be not be a strict comparison (===), or empty
1013 // extensions will be allowed!
1014 if ($ext == '')
1016 // Use the default extension
1017 $ext = EXT;
1019 else
1021 // Add a period before the extension
1022 $ext = '.'.$ext;
1025 // Search path
1026 $search = $directory.'/'.$filename.$ext;
1028 if (isset(self::$internal_cache['find_file_paths'][$search]))
1029 return self::$internal_cache['find_file_paths'][$search];
1031 // Load include paths
1032 $paths = self::$include_paths;
1034 // Nothing found, yet
1035 $found = NULL;
1037 if ($directory === 'config' OR $directory === 'i18n' OR $directory === 'config/custom')
1039 // Search in reverse, for merging
1040 $paths = array_reverse($paths);
1042 foreach ($paths as $path)
1044 if (is_file($path.$search))
1046 // A matching file has been found
1047 $found[] = $path.$search;
1051 else
1053 foreach ($paths as $path)
1055 if (is_file($path.$search))
1057 // A matching file has been found
1058 $found = $path.$search;
1060 // Stop searching
1061 break;
1066 if ($found === NULL)
1068 if ($required === TRUE)
1070 // Directory i18n key
1071 $directory = 'core.'.inflector::singular($directory);
1073 // If the file is required, throw an exception
1074 throw new Kohana_Exception('core.resource_not_found', self::lang($directory), $filename);
1076 else
1078 // Nothing was found, return FALSE
1079 $found = FALSE;
1083 if ( ! isset(self::$write_cache['find_file_paths']))
1085 // Write cache at shutdown
1086 self::$write_cache['find_file_paths'] = TRUE;
1089 return self::$internal_cache['find_file_paths'][$search] = $found;
1093 * Lists all files and directories in a resource path.
1095 * @param string directory to search
1096 * @param boolean list all files to the maximum depth?
1097 * @param string full path to search (used for recursion, *never* set this manually)
1098 * @return array filenames and directories
1100 public static function list_files($directory, $recursive = FALSE, $path = FALSE)
1102 $files = array();
1104 if ($path === FALSE)
1106 $paths = array_reverse(self::include_paths());
1108 foreach ($paths as $path)
1110 // Recursively get and merge all files
1111 $files = array_merge($files, self::list_files($directory, $recursive, $path.$directory));
1114 else
1116 $path = rtrim($path, '/').'/';
1118 if (is_readable($path))
1120 $items = (array) glob($path.'*');
1122 foreach ($items as $index => $item)
1124 $files[] = $item = str_replace('\\', '/', $item);
1126 // Handle recursion
1127 if (is_dir($item) AND $recursive == TRUE)
1129 // Filename should only be the basename
1130 $item = pathinfo($item, PATHINFO_BASENAME);
1132 // Append sub-directory search
1133 $files = array_merge($files, self::list_files($directory, TRUE, $path.$item));
1139 return $files;
1143 * Fetch an i18n language item.
1145 * @param string language key to fetch
1146 * @param array additional information to insert into the line
1147 * @return string i18n language string, or the requested key if the i18n item is not found
1149 public static function lang($key, $args = array())
1151 // Extract the main group from the key
1152 $group = explode('.', $key, 2);
1153 $group = $group[0];
1155 // Get locale name
1156 $locale = self::config('locale.language.0');
1158 if ( ! isset(self::$internal_cache['language'][$locale][$group]))
1160 // Messages for this group
1161 $messages = array();
1163 if ($files = self::find_file('i18n', $locale.'/'.$group))
1165 foreach ($files as $file)
1167 include $file;
1169 // Merge in configuration
1170 if ( ! empty($lang) AND is_array($lang))
1172 foreach ($lang as $k => $v)
1174 $messages[$k] = $v;
1180 if ( ! isset(self::$write_cache['language']))
1182 // Write language cache
1183 self::$write_cache['language'] = TRUE;
1186 self::$internal_cache['language'][$locale][$group] = $messages;
1189 // Get the line from cache
1190 $line = self::key_string(self::$internal_cache['language'][$locale], $key);
1192 if ($line === NULL)
1194 self::log('error', 'Missing i18n entry '.$key.' for language '.$locale);
1196 // Return the key string as fallback
1197 return $key;
1200 if (is_string($line) AND func_num_args() > 1)
1202 $args = array_slice(func_get_args(), 1);
1204 // Add the arguments into the line
1205 $line = vsprintf($line, is_array($args[0]) ? $args[0] : $args);
1208 return $line;
1212 * Returns the value of a key, defined by a 'dot-noted' string, from an array.
1214 * @param array array to search
1215 * @param string dot-noted string: foo.bar.baz
1216 * @return string if the key is found
1217 * @return void if the key is not found
1219 public static function key_string($array, $keys)
1221 if (empty($array))
1222 return NULL;
1224 // Prepare for loop
1225 $keys = explode('.', $keys);
1229 // Get the next key
1230 $key = array_shift($keys);
1232 if (isset($array[$key]))
1234 if (is_array($array[$key]) AND ! empty($keys))
1236 // Dig down to prepare the next loop
1237 $array = $array[$key];
1239 else
1241 // Requested key was found
1242 return $array[$key];
1245 else
1247 // Requested key is not set
1248 break;
1251 while ( ! empty($keys));
1253 return NULL;
1257 * Sets values in an array by using a 'dot-noted' string.
1259 * @param array array to set keys in (reference)
1260 * @param string dot-noted string: foo.bar.baz
1261 * @return mixed fill value for the key
1262 * @return void
1264 public static function key_string_set( & $array, $keys, $fill = NULL)
1266 if (is_object($array) AND ($array instanceof ArrayObject))
1268 // Copy the array
1269 $array_copy = $array->getArrayCopy();
1271 // Is an object
1272 $array_object = TRUE;
1274 else
1276 if ( ! is_array($array))
1278 // Must always be an array
1279 $array = (array) $array;
1282 // Copy is a reference to the array
1283 $array_copy =& $array;
1286 if (empty($keys))
1287 return $array;
1289 // Create keys
1290 $keys = explode('.', $keys);
1292 // Create reference to the array
1293 $row =& $array_copy;
1295 for ($i = 0, $end = count($keys) - 1; $i <= $end; $i++)
1297 // Get the current key
1298 $key = $keys[$i];
1300 if ( ! isset($row[$key]))
1302 if (isset($keys[$i + 1]))
1304 // Make the value an array
1305 $row[$key] = array();
1307 else
1309 // Add the fill key
1310 $row[$key] = $fill;
1313 elseif (isset($keys[$i + 1]))
1315 // Make the value an array
1316 $row[$key] = (array) $row[$key];
1319 // Go down a level, creating a new row reference
1320 $row =& $row[$key];
1323 if (isset($array_object))
1325 // Swap the array back in
1326 $array->exchangeArray($array_copy);
1331 * Retrieves current user agent information:
1332 * keys: browser, version, platform, mobile, robot, referrer, languages, charsets
1333 * tests: is_browser, is_mobile, is_robot, accept_lang, accept_charset
1335 * @param string key or test name
1336 * @param string used with "accept" tests: user_agent(accept_lang, en)
1337 * @return array languages and charsets
1338 * @return string all other keys
1339 * @return boolean all tests
1341 public static function user_agent($key = 'agent', $compare = NULL)
1343 static $info;
1345 // Return the raw string
1346 if ($key === 'agent')
1347 return self::$user_agent;
1349 if ($info === NULL)
1351 // Parse the user agent and extract basic information
1352 $agents = self::config('user_agents');
1354 foreach ($agents as $type => $data)
1356 foreach ($data as $agent => $name)
1358 if (stripos(self::$user_agent, $agent) !== FALSE)
1360 if ($type === 'browser' AND preg_match('|'.preg_quote($agent).'[^0-9.]*+([0-9.][0-9.a-z]*)|i', self::$user_agent, $match))
1362 // Set the browser version
1363 $info['version'] = $match[1];
1366 // Set the agent name
1367 $info[$type] = $name;
1368 break;
1374 if (empty($info[$key]))
1376 switch ($key)
1378 case 'is_robot':
1379 case 'is_browser':
1380 case 'is_mobile':
1381 // A boolean result
1382 $return = ! empty($info[substr($key, 3)]);
1383 break;
1384 case 'languages':
1385 $return = array();
1386 if ( ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
1388 if (preg_match_all('/[-a-z]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])), $matches))
1390 // Found a result
1391 $return = $matches[0];
1394 break;
1395 case 'charsets':
1396 $return = array();
1397 if ( ! empty($_SERVER['HTTP_ACCEPT_CHARSET']))
1399 if (preg_match_all('/[-a-z0-9]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_CHARSET'])), $matches))
1401 // Found a result
1402 $return = $matches[0];
1405 break;
1406 case 'referrer':
1407 if ( ! empty($_SERVER['HTTP_REFERER']))
1409 // Found a result
1410 $return = trim($_SERVER['HTTP_REFERER']);
1412 break;
1415 // Cache the return value
1416 isset($return) and $info[$key] = $return;
1419 if ( ! empty($compare))
1421 // The comparison must always be lowercase
1422 $compare = strtolower($compare);
1424 switch ($key)
1426 case 'accept_lang':
1427 // Check if the lange is accepted
1428 return in_array($compare, self::user_agent('languages'));
1429 break;
1430 case 'accept_charset':
1431 // Check if the charset is accepted
1432 return in_array($compare, self::user_agent('charsets'));
1433 break;
1434 default:
1435 // Invalid comparison
1436 return FALSE;
1437 break;
1441 // Return the key, if set
1442 return isset($info[$key]) ? $info[$key] : NULL;
1446 * Quick debugging of any variable. Any number of parameters can be set.
1448 * @return string
1450 public static function debug()
1452 if (func_num_args() === 0)
1453 return;
1455 // Get params
1456 $params = func_get_args();
1457 $output = array();
1459 foreach ($params as $var)
1461 $output[] = '<pre>('.gettype($var).') '.html::specialchars(print_r($var, TRUE)).'</pre>';
1464 return implode("\n", $output);
1468 * Displays nice backtrace information.
1469 * @see http://php.net/debug_backtrace
1471 * @param array backtrace generated by an exception or debug_backtrace
1472 * @return string
1474 public static function backtrace($trace)
1476 $arg_badword = array(
1477 'passw', /* password and passwd */
1478 'pwd',
1479 'secret'
1483 if ( ! is_array($trace))
1484 return;
1486 // Final output
1487 $output = array();
1489 foreach ($trace as $entry)
1491 $temp = '<li>';
1493 if (isset($entry['file']))
1495 $temp .= self::lang('core.error_file_line', preg_replace('!^'.preg_quote(DOCROOT).'!', '', $entry['file']), $entry['line']);
1498 $temp .= '<pre>';
1500 $reflclass = false;
1501 if (isset($entry['class']))
1503 // Add class and call type
1504 $temp .= $entry['class'].$entry['type'];
1505 try {
1506 $reflclass = new ReflectionClass($entry['class']);
1507 } catch( Exception $e ) {
1508 // Don't care about the problem... just don't expand variable names in that case
1509 $reflclass = false;
1513 // Add function
1514 $temp .= $entry['function'].'( ';
1516 $reflmethod = false;
1517 try {
1518 if( $reflclass )
1519 $reflmethod = $reflclass->getMethod($entry['function']);
1520 else
1521 $reflmethod = new ReflectionFunction($entry['function']);
1522 } catch( Exception $e ) {
1523 // Don't care about the problem... just don't expand variable names in that case
1524 $reflmethod = false;
1526 // Add function args
1527 if (isset($entry['args']) AND is_array($entry['args']))
1529 // Separator starts as nothing
1530 $sep = '';
1532 $reflargs = false;
1533 try {
1534 if( $reflmethod )
1535 $reflargs = $reflmethod->getParameters();
1536 } catch( Exception $e ) {
1537 // Don't care about the problem... just don't expand variable names in that case
1538 $reflargs = false;
1541 while ($arg = array_shift($entry['args']))
1543 $argname = "...";
1544 try {
1545 if( !empty($reflargs) )
1546 $argname = array_shift( $reflargs )->getName();
1547 } catch( Exception $e ) {
1548 // Don't care about the problem... just don't expand variable names in that case
1549 $argname = "...";
1552 if (is_string($arg) AND substr($arg, 0, 4) !== "unix" AND is_file($arg))
1554 // Remove docroot from filename
1555 $arg = preg_replace('!^'.preg_quote(DOCROOT).'!', '', $arg);
1558 foreach($arg_badword as $badword) {
1559 if( stripos($argname, $badword) !== false )
1560 $arg = "*****";
1563 $temp .= $sep.$argname.' = '.html::specialchars(print_r($arg, TRUE));
1565 // Change separator to a comma
1566 $sep = ', ';
1570 $temp .= ' )</pre></li>';
1572 $output[] = $temp;
1575 return '<ul class="backtrace">'.implode("\n", $output).'</ul>';
1579 * Saves the internal caches: configuration, include paths, etc.
1581 * @return boolean
1583 public static function internal_cache_save()
1585 if ( ! is_array(self::$write_cache))
1586 return FALSE;
1588 // Get internal cache names
1589 $caches = array_keys(self::$write_cache);
1591 // Nothing written
1592 $written = FALSE;
1594 foreach ($caches as $cache)
1596 if (isset(self::$internal_cache[$cache]))
1598 // Write the cache file
1599 self::cache_save($cache, self::$internal_cache[$cache], self::$configuration['core']['internal_cache']);
1601 // A cache has been written
1602 $written = TRUE;
1606 return $written;
1609 } // End Kohana
1612 * Creates a generic i18n exception.
1614 class Kohana_Exception extends Exception {
1616 // Template file
1617 protected $template = 'kohana_error_page';
1619 // Header
1620 protected $header = FALSE;
1622 // Error code
1623 protected $code = E_KOHANA;
1626 * Set exception message.
1628 * @param string i18n language key for the message
1629 * @param array addition line parameters
1631 public function __construct($error)
1633 $args = array_slice(func_get_args(), 1);
1635 // Fetch the error message
1636 $message = Kohana::lang($error, $args);
1638 if ($message === $error OR empty($message))
1640 // Unable to locate the message for the error
1641 $message = 'Unknown Exception: '.$error;
1644 // Sets $this->message the proper way
1645 parent::__construct($message);
1649 * Magic method for converting an object to a string.
1651 * @return string i18n message
1653 public function __toString()
1655 return (string) $this->message;
1659 * Fetch the template name.
1661 * @return string
1663 public function getTemplate()
1665 return $this->template;
1669 * Sends an Internal Server Error header.
1671 * @return void
1673 public function sendHeaders()
1675 // Send the 500 header
1676 header('HTTP/1.1 500 Internal Server Error');
1679 } // End Kohana Exception
1682 * Creates a custom exception.
1684 class Kohana_User_Exception extends Kohana_Exception {
1687 * Set exception title and message.
1689 * @param string exception title string
1690 * @param string exception message string
1691 * @param string custom error template
1693 public function __construct($title, $message, $template = FALSE)
1695 Exception::__construct($message);
1697 $this->code = $title;
1699 if ($template !== FALSE)
1701 $this->template = $template;
1705 } // End Kohana PHP Exception
1708 * Creates a Page Not Found exception.
1710 class Kohana_404_Exception extends Kohana_Exception {
1712 protected $code = E_PAGE_NOT_FOUND;
1715 * Set internal properties.
1717 * @param string URL of page
1718 * @param string custom error template
1720 public function __construct($page = FALSE, $template = FALSE)
1722 if ($page === FALSE)
1724 // Construct the page URI using Router properties
1725 $page = Router::$current_uri.Router::$url_suffix.Router::$query_string;
1728 Exception::__construct(Kohana::lang('core.page_not_found', $page));
1730 $this->template = $template;
1734 * Sends "File Not Found" headers, to emulate server behavior.
1736 * @return void
1738 public function sendHeaders()
1740 // Send the 404 header
1741 header('HTTP/1.1 404 File Not Found');
1744 } // End Kohana 404 Exception