diff options
author | Bharat Mediratta <bharat@menalto.com> | 2009-05-27 15:11:53 -0700 |
---|---|---|
committer | Bharat Mediratta <bharat@menalto.com> | 2009-05-27 15:11:53 -0700 |
commit | 12fe58d997d2066dc362fd393a18b4e5da190513 (patch) | |
tree | 3ad8e5afb77829e1541ec96d86785760d65c04ac /system/libraries | |
parent | 00f47d4ddddcd1902db817018dd79ac01bcc8e82 (diff) |
Rename 'kohana' to 'system' to conform to the Kohana filesystem layout. I'm comfortable with us not clearly drawing the distinction about the fact that it's Kohana.
Diffstat (limited to 'system/libraries')
50 files changed, 13639 insertions, 0 deletions
diff --git a/system/libraries/Cache.php b/system/libraries/Cache.php new file mode 100644 index 00000000..8a02a905 --- /dev/null +++ b/system/libraries/Cache.php @@ -0,0 +1,208 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Provides a driver-based interface for finding, creating, and deleting cached + * resources. Caches are identified by a unique string. Tagging of caches is + * also supported, and caches can be found and deleted by id or tag. + * + * $Id: Cache.php 4321 2009-05-04 01:39:44Z kiall $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Core { + + protected static $instances = array(); + + // For garbage collection + protected static $loaded; + + // Configuration + protected $config; + + // Driver object + protected $driver; + + /** + * Returns a singleton instance of Cache. + * + * @param string configuration + * @return Cache_Core + */ + public static function & instance($config = FALSE) + { + if ( ! isset(Cache::$instances[$config])) + { + // Create a new instance + Cache::$instances[$config] = new Cache($config); + } + + return Cache::$instances[$config]; + } + + /** + * Loads the configured driver and validates it. + * + * @param array|string custom configuration or config group name + * @return void + */ + public function __construct($config = FALSE) + { + if (is_string($config)) + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('cache.'.$config)) === NULL) + throw new Kohana_Exception('cache.undefined_group', $name); + } + + if (is_array($config)) + { + // Append the default configuration options + $config += Kohana::config('cache.default'); + } + else + { + // Load the default group + $config = Kohana::config('cache.default'); + } + + // Cache the config in the object + $this->config = $config; + + // Set driver name + $driver = 'Cache_'.ucfirst($this->config['driver']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('core.driver_not_found', $this->config['driver'], get_class($this)); + + // Initialize the driver + $this->driver = new $driver($this->config['params']); + + // Validate the driver + if ( ! ($this->driver instanceof Cache_Driver)) + throw new Kohana_Exception('core.driver_implements', $this->config['driver'], get_class($this), 'Cache_Driver'); + + Kohana::log('debug', 'Cache Library initialized'); + + if (Cache::$loaded !== TRUE) + { + $this->config['requests'] = (int) $this->config['requests']; + + if ($this->config['requests'] > 0 AND mt_rand(1, $this->config['requests']) === 1) + { + // Do garbage collection + $this->driver->delete_expired(); + + Kohana::log('debug', 'Cache: Expired caches deleted.'); + } + + // Cache has been loaded once + Cache::$loaded = TRUE; + } + } + + /** + * Fetches a cache by id. NULL is returned when a cache item is not found. + * + * @param string cache id + * @return mixed cached data or NULL + */ + public function get($id) + { + // Sanitize the ID + $id = $this->sanitize_id($id); + + return $this->driver->get($id); + } + + /** + * Fetches all of the caches for a given tag. An empty array will be + * returned when no matching caches are found. + * + * @param string cache tag + * @return array all cache items matching the tag + */ + public function find($tag) + { + return $this->driver->find($tag); + } + + /** + * Set a cache item by id. Tags may also be added and a custom lifetime + * can be set. Non-string data is automatically serialized. + * + * @param string unique cache id + * @param mixed data to cache + * @param array|string tags for this item + * @param integer number of seconds until the cache expires + * @return boolean + */ + function set($id, $data, $tags = NULL, $lifetime = NULL) + { + if (is_resource($data)) + throw new Kohana_Exception('cache.resources'); + + // Sanitize the ID + $id = $this->sanitize_id($id); + + if ($lifetime === NULL) + { + // Get the default lifetime + $lifetime = $this->config['lifetime']; + } + + return $this->driver->set($id, $data, (array) $tags, $lifetime); + } + + /** + * Delete a cache item by id. + * + * @param string cache id + * @return boolean + */ + public function delete($id) + { + // Sanitize the ID + $id = $this->sanitize_id($id); + + return $this->driver->delete($id); + } + + /** + * Delete all cache items with a given tag. + * + * @param string cache tag name + * @return boolean + */ + public function delete_tag($tag) + { + return $this->driver->delete($tag, TRUE); + } + + /** + * Delete ALL cache items items. + * + * @return boolean + */ + public function delete_all() + { + return $this->driver->delete(TRUE); + } + + /** + * Replaces troublesome characters with underscores. + * + * @param string cache id + * @return string + */ + protected function sanitize_id($id) + { + // Change slashes and spaces to underscores + return str_replace(array('/', '\\', ' '), '_', $id); + } + +} // End Cache diff --git a/system/libraries/Controller.php b/system/libraries/Controller.php new file mode 100644 index 00000000..d111f25e --- /dev/null +++ b/system/libraries/Controller.php @@ -0,0 +1,78 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Kohana Controller class. The controller class must be extended to work + * properly, so this class is defined as abstract. + * + * $Id: Controller.php 3979 2009-02-13 16:46:12Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Controller_Core { + + // Allow all controllers to run in production by default + const ALLOW_PRODUCTION = TRUE; + + /** + * Loads URI, and Input into this controller. + * + * @return void + */ + public function __construct() + { + if (Kohana::$instance == NULL) + { + // Set the instance to the first controller loaded + Kohana::$instance = $this; + } + + // URI should always be available + $this->uri = URI::instance(); + + // Input should always be available + $this->input = Input::instance(); + } + + /** + * Handles methods that do not exist. + * + * @param string method name + * @param array arguments + * @return void + */ + public function __call($method, $args) + { + // Default to showing a 404 page + Event::run('system.404'); + } + + /** + * Includes a View within the controller scope. + * + * @param string view filename + * @param array array of view variables + * @return string + */ + public function _kohana_load_view($kohana_view_filename, $kohana_input_data) + { + if ($kohana_view_filename == '') + return; + + // Buffering on + ob_start(); + + // Import the view variables to local namespace + extract($kohana_input_data, EXTR_SKIP); + + // Views are straight HTML pages with embedded PHP, so importing them + // this way insures that $this can be accessed as if the user was in + // the controller, which gives the easiest access to libraries in views + include $kohana_view_filename; + + // Fetch the output and close the buffer + return ob_get_clean(); + } + +} // End Controller Class
\ No newline at end of file diff --git a/system/libraries/Database.php b/system/libraries/Database.php new file mode 100644 index 00000000..6267f63a --- /dev/null +++ b/system/libraries/Database.php @@ -0,0 +1,1444 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Provides database access in a platform agnostic way, using simple query building blocks. + * + * $Id: Database.php 4342 2009-05-08 16:56:01Z jheathco $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Core { + + // Database instances + public static $instances = array(); + + // Global benchmark + public static $benchmarks = array(); + + // Configuration + protected $config = array + ( + 'benchmark' => TRUE, + 'persistent' => FALSE, + 'connection' => '', + 'character_set' => 'utf8', + 'table_prefix' => '', + 'object' => TRUE, + 'cache' => FALSE, + 'escape' => TRUE, + ); + + // Database driver object + protected $driver; + protected $link; + + // Un-compiled parts of the SQL query + protected $select = array(); + protected $set = array(); + protected $from = array(); + protected $join = array(); + protected $where = array(); + protected $orderby = array(); + protected $order = array(); + protected $groupby = array(); + protected $having = array(); + protected $distinct = FALSE; + protected $limit = FALSE; + protected $offset = FALSE; + protected $last_query = ''; + + // Stack of queries for push/pop + protected $query_history = array(); + + /** + * Returns a singleton instance of Database. + * + * @param mixed configuration array or DSN + * @return Database_Core + */ + public static function & instance($name = 'default', $config = NULL) + { + if ( ! isset(Database::$instances[$name])) + { + // Create a new instance + Database::$instances[$name] = new Database($config === NULL ? $name : $config); + } + + return Database::$instances[$name]; + } + + /** + * Returns the name of a given database instance. + * + * @param Database instance of Database + * @return string + */ + public static function instance_name(Database $db) + { + return array_search($db, Database::$instances, TRUE); + } + + /** + * Sets up the database configuration, loads the Database_Driver. + * + * @throws Kohana_Database_Exception + */ + public function __construct($config = array()) + { + if (empty($config)) + { + // Load the default group + $config = Kohana::config('database.default'); + } + elseif (is_array($config) AND count($config) > 0) + { + if ( ! array_key_exists('connection', $config)) + { + $config = array('connection' => $config); + } + } + elseif (is_string($config)) + { + // The config is a DSN string + if (strpos($config, '://') !== FALSE) + { + $config = array('connection' => $config); + } + // The config is a group name + else + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('database.'.$config)) === NULL) + throw new Kohana_Database_Exception('database.undefined_group', $name); + } + } + + // Merge the default config with the passed config + $this->config = array_merge($this->config, $config); + + if (is_string($this->config['connection'])) + { + // Make sure the connection is valid + if (strpos($this->config['connection'], '://') === FALSE) + throw new Kohana_Database_Exception('database.invalid_dsn', $this->config['connection']); + + // Parse the DSN, creating an array to hold the connection parameters + $db = array + ( + 'type' => FALSE, + 'user' => FALSE, + 'pass' => FALSE, + 'host' => FALSE, + 'port' => FALSE, + 'socket' => FALSE, + 'database' => FALSE + ); + + // Get the protocol and arguments + list ($db['type'], $connection) = explode('://', $this->config['connection'], 2); + + if (strpos($connection, '@') !== FALSE) + { + // Get the username and password + list ($db['pass'], $connection) = explode('@', $connection, 2); + // Check if a password is supplied + $logindata = explode(':', $db['pass'], 2); + $db['pass'] = (count($logindata) > 1) ? $logindata[1] : ''; + $db['user'] = $logindata[0]; + + // Prepare for finding the database + $connection = explode('/', $connection); + + // Find the database name + $db['database'] = array_pop($connection); + + // Reset connection string + $connection = implode('/', $connection); + + // Find the socket + if (preg_match('/^unix\([^)]++\)/', $connection)) + { + // This one is a little hairy: we explode based on the end of + // the socket, removing the 'unix(' from the connection string + list ($db['socket'], $connection) = explode(')', substr($connection, 5), 2); + } + elseif (strpos($connection, ':') !== FALSE) + { + // Fetch the host and port name + list ($db['host'], $db['port']) = explode(':', $connection, 2); + } + else + { + $db['host'] = $connection; + } + } + else + { + // File connection + $connection = explode('/', $connection); + + // Find database file name + $db['database'] = array_pop($connection); + + // Find database directory name + $db['socket'] = implode('/', $connection).'/'; + } + + // Reset the connection array to the database config + $this->config['connection'] = $db; + } + // Set driver name + $driver = 'Database_'.ucfirst($this->config['connection']['type']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Database_Exception('core.driver_not_found', $this->config['connection']['type'], get_class($this)); + + // Initialize the driver + $this->driver = new $driver($this->config); + + // Validate the driver + if ( ! ($this->driver instanceof Database_Driver)) + throw new Kohana_Database_Exception('core.driver_implements', $this->config['connection']['type'], get_class($this), 'Database_Driver'); + + Kohana::log('debug', 'Database Library initialized'); + } + + /** + * Simple connect method to get the database queries up and running. + * + * @return void + */ + public function connect() + { + // A link can be a resource or an object + if ( ! is_resource($this->link) AND ! is_object($this->link)) + { + $this->link = $this->driver->connect(); + if ( ! is_resource($this->link) AND ! is_object($this->link)) + throw new Kohana_Database_Exception('database.connection', $this->driver->show_error()); + + // Clear password after successful connect + $this->config['connection']['pass'] = NULL; + } + } + + /** + * Runs a query into the driver and returns the result. + * + * @param string SQL query to execute + * @return Database_Result + */ + public function query($sql = '') + { + if ($sql == '') return FALSE; + + // No link? Connect! + $this->link or $this->connect(); + + // Start the benchmark + $start = microtime(TRUE); + + if (func_num_args() > 1) //if we have more than one argument ($sql) + { + $argv = func_get_args(); + $binds = (is_array(next($argv))) ? current($argv) : array_slice($argv, 1); + } + + // Compile binds if needed + if (isset($binds)) + { + $sql = $this->compile_binds($sql, $binds); + } + + // Fetch the result + $result = $this->driver->query($this->last_query = $sql); + + // Stop the benchmark + $stop = microtime(TRUE); + + if ($this->config['benchmark'] == TRUE) + { + // Benchmark the query + Database::$benchmarks[] = array('query' => $sql, 'time' => $stop - $start, 'rows' => count($result)); + } + + return $result; + } + + /** + * Selects the column names for a database query. + * + * @param string string or array of column names to select + * @return Database_Core This Database object. + */ + public function select($sql = '*') + { + if (func_num_args() > 1) + { + $sql = func_get_args(); + } + elseif (is_string($sql)) + { + $sql = explode(',', $sql); + } + else + { + $sql = (array) $sql; + } + + foreach ($sql as $val) + { + if (($val = trim($val)) === '') continue; + + if (strpos($val, '(') === FALSE AND $val !== '*') + { + if (preg_match('/^DISTINCT\s++(.+)$/i', $val, $matches)) + { + // Only prepend with table prefix if table name is specified + $val = (strpos($matches[1], '.') !== FALSE) ? $this->config['table_prefix'].$matches[1] : $matches[1]; + + $this->distinct = TRUE; + } + else + { + $val = (strpos($val, '.') !== FALSE) ? $this->config['table_prefix'].$val : $val; + } + + $val = $this->driver->escape_column($val); + } + + $this->select[] = $val; + } + + return $this; + } + + /** + * Selects the from table(s) for a database query. + * + * @param string string or array of tables to select + * @return Database_Core This Database object. + */ + public function from($sql) + { + if (func_num_args() > 1) + { + $sql = func_get_args(); + } + elseif (is_string($sql)) + { + $sql = explode(',', $sql); + } + else + { + $sql = array($sql); + } + + foreach ($sql as $val) + { + if (is_string($val)) + { + if (($val = trim($val)) === '') continue; + + // TODO: Temporary solution, this should be moved to database driver (AS is checked for twice) + if (stripos($val, ' AS ') !== FALSE) + { + $val = str_ireplace(' AS ', ' AS ', $val); + + list($table, $alias) = explode(' AS ', $val); + + // Attach prefix to both sides of the AS + $val = $this->config['table_prefix'].$table.' AS '.$this->config['table_prefix'].$alias; + } + else + { + $val = $this->config['table_prefix'].$val; + } + } + + $this->from[] = $val; + } + + return $this; + } + + /** + * Generates the JOIN portion of the query. + * + * @param string table name + * @param string|array where key or array of key => value pairs + * @param string where value + * @param string type of join + * @return Database_Core This Database object. + */ + public function join($table, $key, $value = NULL, $type = '') + { + $join = array(); + + if ( ! empty($type)) + { + $type = strtoupper(trim($type)); + + if ( ! in_array($type, array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER'), TRUE)) + { + $type = ''; + } + else + { + $type .= ' '; + } + } + + $cond = array(); + $keys = is_array($key) ? $key : array($key => $value); + foreach ($keys as $key => $value) + { + $key = (strpos($key, '.') !== FALSE) ? $this->config['table_prefix'].$key : $key; + + if (is_string($value)) + { + // Only escape if it's a string + $value = $this->driver->escape_column($this->config['table_prefix'].$value); + } + + $cond[] = $this->driver->where($key, $value, 'AND ', count($cond), FALSE); + } + + if ( ! is_array($this->join)) + { + $this->join = array(); + } + + if ( ! is_array($table)) + { + $table = array($table); + } + + foreach ($table as $t) + { + if (is_string($t)) + { + // TODO: Temporary solution, this should be moved to database driver (AS is checked for twice) + if (stripos($t, ' AS ') !== FALSE) + { + $t = str_ireplace(' AS ', ' AS ', $t); + + list($table, $alias) = explode(' AS ', $t); + + // Attach prefix to both sides of the AS + $t = $this->config['table_prefix'].$table.' AS '.$this->config['table_prefix'].$alias; + } + else + { + $t = $this->config['table_prefix'].$t; + } + } + + $join['tables'][] = $this->driver->escape_column($t); + } + + $join['conditions'] = '('.trim(implode(' ', $cond)).')'; + $join['type'] = $type; + + $this->join[] = $join; + + return $this; + } + + + /** + * Selects the where(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function where($key, $value = NULL, $quote = TRUE) + { + $quote = (func_num_args() < 2 AND ! is_array($key)) ? -1 : $quote; + if (is_object($key)) + { + $keys = array((string) $key => ''); + } + elseif ( ! is_array($key)) + { + $keys = array($key => $value); + } + else + { + $keys = $key; + } + + foreach ($keys as $key => $value) + { + $key = (strpos($key, '.') !== FALSE) ? $this->config['table_prefix'].$key : $key; + $this->where[] = $this->driver->where($key, $value, 'AND ', count($this->where), $quote); + } + + return $this; + } + + /** + * Selects the or where(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function orwhere($key, $value = NULL, $quote = TRUE) + { + $quote = (func_num_args() < 2 AND ! is_array($key)) ? -1 : $quote; + if (is_object($key)) + { + $keys = array((string) $key => ''); + } + elseif ( ! is_array($key)) + { + $keys = array($key => $value); + } + else + { + $keys = $key; + } + + foreach ($keys as $key => $value) + { + $key = (strpos($key, '.') !== FALSE) ? $this->config['table_prefix'].$key : $key; + $this->where[] = $this->driver->where($key, $value, 'OR ', count($this->where), $quote); + } + + return $this; + } + + /** + * Selects the like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @param boolean automatically add starting and ending wildcards + * @return Database_Core This Database object. + */ + public function like($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->like($field, $match, $auto, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @param boolean automatically add starting and ending wildcards + * @return Database_Core This Database object. + */ + public function orlike($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->like($field, $match, $auto, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Selects the not like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @param boolean automatically add starting and ending wildcards + * @return Database_Core This Database object. + */ + public function notlike($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notlike($field, $match, $auto, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or not like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @return Database_Core This Database object. + */ + public function ornotlike($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notlike($field, $match, $auto, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Selects the like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @return Database_Core This Database object. + */ + public function regex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->regex($field, $match, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @return Database_Core This Database object. + */ + public function orregex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->regex($field, $match, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Selects the not regex(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string regex value to match with field + * @return Database_Core This Database object. + */ + public function notregex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notregex($field, $match, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or not regex(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string regex value to match with field + * @return Database_Core This Database object. + */ + public function ornotregex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notregex($field, $match, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Chooses the column to group by in a select query. + * + * @param string column name to group by + * @return Database_Core This Database object. + */ + public function groupby($by) + { + if ( ! is_array($by)) + { + $by = explode(',', (string) $by); + } + + foreach ($by as $val) + { + $val = trim($val); + + if ($val != '') + { + // Add the table prefix if we are using table.column names + if(strpos($val, '.')) + { + $val = $this->config['table_prefix'].$val; + } + + $this->groupby[] = $this->driver->escape_column($val); + } + } + + return $this; + } + + /** + * Selects the having(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function having($key, $value = '', $quote = TRUE) + { + $this->having[] = $this->driver->where($key, $value, 'AND', count($this->having), TRUE); + return $this; + } + + /** + * Selects the or having(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function orhaving($key, $value = '', $quote = TRUE) + { + $this->having[] = $this->driver->where($key, $value, 'OR', count($this->having), TRUE); + return $this; + } + + /** + * Chooses which column(s) to order the select query by. + * + * @param string|array column(s) to order on, can be an array, single column, or comma seperated list of columns + * @param string direction of the order + * @return Database_Core This Database object. + */ + public function orderby($orderby, $direction = NULL) + { + if ( ! is_array($orderby)) + { + $orderby = array($orderby => $direction); + } + + foreach ($orderby as $column => $direction) + { + $direction = strtoupper(trim($direction)); + + // Add a direction if the provided one isn't valid + if ( ! in_array($direction, array('ASC', 'DESC', 'RAND()', 'RANDOM()', 'NULL'))) + { + $direction = 'ASC'; + } + + // Add the table prefix if a table.column was passed + if (strpos($column, '.')) + { + $column = $this->config['table_prefix'].$column; + } + + $this->orderby[] = $this->driver->escape_column($column).' '.$direction; + } + + return $this; + } + + /** + * Selects the limit section of a query. + * + * @param integer number of rows to limit result to + * @param integer offset in result to start returning rows from + * @return Database_Core This Database object. + */ + public function limit($limit, $offset = NULL) + { + $this->limit = (int) $limit; + + if ($offset !== NULL OR ! is_int($this->offset)) + { + $this->offset($offset); + } + + return $this; + } + + /** + * Sets the offset portion of a query. + * + * @param integer offset value + * @return Database_Core This Database object. + */ + public function offset($value) + { + $this->offset = (int) $value; + + return $this; + } + + /** + * Allows key/value pairs to be set for inserting or updating. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @return Database_Core This Database object. + */ + public function set($key, $value = '') + { + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + foreach ($key as $k => $v) + { + // Add a table prefix if the column includes the table. + if (strpos($k, '.')) + $k = $this->config['table_prefix'].$k; + + $this->set[$k] = $this->driver->escape($v); + } + + return $this; + } + + /** + * Compiles the select statement based on the other functions called and runs the query. + * + * @param string table name + * @param string limit clause + * @param string offset clause + * @return Database_Result + */ + public function get($table = '', $limit = NULL, $offset = NULL) + { + if ($table != '') + { + $this->from($table); + } + + if ( ! is_null($limit)) + { + $this->limit($limit, $offset); + } + + $sql = $this->driver->compile_select(get_object_vars($this)); + + $this->reset_select(); + + $result = $this->query($sql); + + $this->last_query = $sql; + + return $result; + } + + /** + * Compiles the select statement based on the other functions called and runs the query. + * + * @param string table name + * @param array where clause + * @param string limit clause + * @param string offset clause + * @return Database_Core This Database object. + */ + public function getwhere($table = '', $where = NULL, $limit = NULL, $offset = NULL) + { + if ($table != '') + { + $this->from($table); + } + + if ( ! is_null($where)) + { + $this->where($where); + } + + if ( ! is_null($limit)) + { + $this->limit($limit, $offset); + } + + $sql = $this->driver->compile_select(get_object_vars($this)); + + $this->reset_select(); + + $result = $this->query($sql); + + return $result; + } + + /** + * Compiles the select statement based on the other functions called and returns the query string. + * + * @param string table name + * @param string limit clause + * @param string offset clause + * @return string sql string + */ + public function compile($table = '', $limit = NULL, $offset = NULL) + { + if ($table != '') + { + $this->from($table); + } + + if ( ! is_null($limit)) + { + $this->limit($limit, $offset); + } + + $sql = $this->driver->compile_select(get_object_vars($this)); + + $this->reset_select(); + + return $sql; + } + + /** + * Compiles an insert string and runs the query. + * + * @param string table name + * @param array array of key/value pairs to insert + * @return Database_Result Query result + */ + public function insert($table = '', $set = NULL) + { + if ( ! is_null($set)) + { + $this->set($set); + } + + if ($this->set == NULL) + throw new Kohana_Database_Exception('database.must_use_set'); + + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + + // If caching is enabled, clear the cache before inserting + ($this->config['cache'] === TRUE) and $this->clear_cache(); + + $sql = $this->driver->insert($this->config['table_prefix'].$table, array_keys($this->set), array_values($this->set)); + + $this->reset_write(); + + return $this->query($sql); + } + + /** + * Adds an "IN" condition to the where clause + * + * @param string Name of the column being examined + * @param mixed An array or string to match against + * @param bool Generate a NOT IN clause instead + * @return Database_Core This Database object. + */ + public function in($field, $values, $not = FALSE) + { + if (is_array($values)) + { + $escaped_values = array(); + foreach ($values as $v) + { + if (is_numeric($v)) + { + $escaped_values[] = $v; + } + else + { + $escaped_values[] = "'".$this->driver->escape_str($v)."'"; + } + } + $values = implode(",", $escaped_values); + } + + $where = $this->driver->escape_column(((strpos($field,'.') !== FALSE) ? $this->config['table_prefix'] : ''). $field).' '.($not === TRUE ? 'NOT ' : '').'IN ('.$values.')'; + $this->where[] = $this->driver->where($where, '', 'AND ', count($this->where), -1); + + return $this; + } + + /** + * Adds a "NOT IN" condition to the where clause + * + * @param string Name of the column being examined + * @param mixed An array or string to match against + * @return Database_Core This Database object. + */ + public function notin($field, $values) + { + return $this->in($field, $values, TRUE); + } + + /** + * Compiles a merge string and runs the query. + * + * @param string table name + * @param array array of key/value pairs to merge + * @return Database_Result Query result + */ + public function merge($table = '', $set = NULL) + { + if ( ! is_null($set)) + { + $this->set($set); + } + + if ($this->set == NULL) + throw new Kohana_Database_Exception('database.must_use_set'); + + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + + $sql = $this->driver->merge($this->config['table_prefix'].$table, array_keys($this->set), array_values($this->set)); + + $this->reset_write(); + return $this->query($sql); + } + + /** + * Compiles an update string and runs the query. + * + * @param string table name + * @param array associative array of update values + * @param array where clause + * @return Database_Result Query result + */ + public function update($table = '', $set = NULL, $where = NULL) + { + if ( is_array($set)) + { + $this->set($set); + } + + if ( ! is_null($where)) + { + $this->where($where); + } + + if ($this->set == FALSE) + throw new Kohana_Database_Exception('database.must_use_set'); + + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + + $sql = $this->driver->update($this->config['table_prefix'].$table, $this->set, $this->where); + + $this->reset_write(); + return $this->query($sql); + } + + /** + * Compiles a delete string and runs the query. + * + * @param string table name + * @param array where clause + * @return Database_Result Query result + */ + public function delete($table = '', $where = NULL) + { + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + else + { + $table = $this->config['table_prefix'].$table; + } + + if (! is_null($where)) + { + $this->where($where); + } + + if (count($this->where) < 1) + throw new Kohana_Database_Exception('database.must_use_where'); + + $sql = $this->driver->delete($table, $this->where); + + $this->reset_write(); + return $this->query($sql); + } + + /** + * Returns the last query run. + * + * @return string SQL + */ + public function last_query() + { + return $this->last_query; + } + + /** + * Count query records. + * + * @param string table name + * @param array where clause + * @return integer + */ + public function count_records($table = FALSE, $where = NULL) + { + if (count($this->from) < 1) + { + if ($table == FALSE) + throw new Kohana_Database_Exception('database.must_use_table'); + + $this->from($table); + } + + if ($where !== NULL) + { + $this->where($where); + } + + $query = $this->select('COUNT(*) AS '.$this->escape_column('records_found'))->get()->result(TRUE); + + return (int) $query->current()->records_found; + } + + /** + * Resets all private select variables. + * + * @return void + */ + protected function reset_select() + { + $this->select = array(); + $this->from = array(); + $this->join = array(); + $this->where = array(); + $this->orderby = array(); + $this->groupby = array(); + $this->having = array(); + $this->distinct = FALSE; + $this->limit = FALSE; + $this->offset = FALSE; + } + + /** + * Resets all private insert and update variables. + * + * @return void + */ + protected function reset_write() + { + $this->set = array(); + $this->from = array(); + $this->where = array(); + } + + /** + * Lists all the tables in the current database. + * + * @return array + */ + public function list_tables() + { + $this->link or $this->connect(); + + return $this->driver->list_tables(); + } + + /** + * See if a table exists in the database. + * + * @param string table name + * @param boolean True to attach table prefix + * @return boolean + */ + public function table_exists($table_name, $prefix = TRUE) + { + if ($prefix) + return in_array($this->config['table_prefix'].$table_name, $this->list_tables()); + else + return in_array($table_name, $this->list_tables()); + } + + /** + * Combine a SQL statement with the bind values. Used for safe queries. + * + * @param string query to bind to the values + * @param array array of values to bind to the query + * @return string + */ + public function compile_binds($sql, $binds) + { + foreach ((array) $binds as $val) + { + // If the SQL contains no more bind marks ("?"), we're done. + if (($next_bind_pos = strpos($sql, '?')) === FALSE) + break; + + // Properly escape the bind value. + $val = $this->driver->escape($val); + + // Temporarily replace possible bind marks ("?"), in the bind value itself, with a placeholder. + $val = str_replace('?', '{%B%}', $val); + + // Replace the first bind mark ("?") with its corresponding value. + $sql = substr($sql, 0, $next_bind_pos).$val.substr($sql, $next_bind_pos + 1); + } + + // Restore placeholders. + return str_replace('{%B%}', '?', $sql); + } + + /** + * Get the field data for a database table, along with the field's attributes. + * + * @param string table name + * @return array + */ + public function field_data($table = '') + { + $this->link or $this->connect(); + + return $this->driver->field_data($this->config['table_prefix'].$table); + } + + /** + * Get the field data for a database table, along with the field's attributes. + * + * @param string table name + * @return array + */ + public function list_fields($table = '') + { + $this->link or $this->connect(); + + return $this->driver->list_fields($this->config['table_prefix'].$table); + } + + /** + * Escapes a value for a query. + * + * @param mixed value to escape + * @return string + */ + public function escape($value) + { + return $this->driver->escape($value); + } + + /** + * Escapes a string for a query. + * + * @param string string to escape + * @return string + */ + public function escape_str($str) + { + return $this->driver->escape_str($str); + } + + /** + * Escapes a table name for a query. + * + * @param string string to escape + * @return string + */ + public function escape_table($table) + { + return $this->driver->escape_table($table); + } + + /** + * Escapes a column name for a query. + * + * @param string string to escape + * @return string + */ + public function escape_column($table) + { + return $this->driver->escape_column($table); + } + + /** + * Returns table prefix of current configuration. + * + * @return string + */ + public function table_prefix() + { + return $this->config['table_prefix']; + } + + /** + * Clears the query cache. + * + * @param string|TRUE clear cache by SQL statement or TRUE for last query + * @return Database_Core This Database object. + */ + public function clear_cache($sql = NULL) + { + if ($sql === TRUE) + { + $this->driver->clear_cache($this->last_query); + } + elseif (is_string($sql)) + { + $this->driver->clear_cache($sql); + } + else + { + $this->driver->clear_cache(); + } + + return $this; + } + + /** + * Pushes existing query space onto the query stack. Use push + * and pop to prevent queries from clashing before they are + * executed + * + * @return Database_Core This Databaes object + */ + public function push() + { + array_push($this->query_history, array( + $this->select, + $this->from, + $this->join, + $this->where, + $this->orderby, + $this->order, + $this->groupby, + $this->having, + $this->distinct, + $this->limit, + $this->offset + )); + + $this->reset_select(); + + return $this; + } + + /** + * Pops from query stack into the current query space. + * + * @return Database_Core This Databaes object + */ + public function pop() + { + if (count($this->query_history) == 0) + { + // No history + return $this; + } + + list( + $this->select, + $this->from, + $this->join, + $this->where, + $this->orderby, + $this->order, + $this->groupby, + $this->having, + $this->distinct, + $this->limit, + $this->offset + ) = array_pop($this->query_history); + + return $this; + } + + /** + * Count the number of records in the last query, without LIMIT or OFFSET applied. + * + * @return integer + */ + public function count_last_query() + { + if ($sql = $this->last_query()) + { + if (stripos($sql, 'LIMIT') !== FALSE) + { + // Remove LIMIT from the SQL + $sql = preg_replace('/\sLIMIT\s+[^a-z]+/i', ' ', $sql); + } + + if (stripos($sql, 'OFFSET') !== FALSE) + { + // Remove OFFSET from the SQL + $sql = preg_replace('/\sOFFSET\s+\d+/i', '', $sql); + } + + // Get the total rows from the last query executed + $result = $this->query + ( + 'SELECT COUNT(*) AS '.$this->escape_column('total_rows').' '. + 'FROM ('.trim($sql).') AS '.$this->escape_table('counted_results') + ); + + // Return the total number of rows from the query + return (int) $result->current()->total_rows; + } + + return FALSE; + } + +} // End Database Class + + +/** + * Sets the code for a Database exception. + */ +class Kohana_Database_Exception extends Kohana_Exception { + + protected $code = E_DATABASE_ERROR; + +} // End Kohana Database Exception diff --git a/system/libraries/Database_Expression.php b/system/libraries/Database_Expression.php new file mode 100644 index 00000000..940a6363 --- /dev/null +++ b/system/libraries/Database_Expression.php @@ -0,0 +1,26 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Database expression class to allow for explicit joins and where expressions. + * + * $Id: Database_Expression.php 4037 2009-03-04 23:35:53Z jheathco $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Expression_Core { + + protected $expression; + + public function __construct($expression) + { + $this->expression = $expression; + } + + public function __toString() + { + return (string) $this->expression; + } + +} // End Database Expr Class
\ No newline at end of file diff --git a/system/libraries/Encrypt.php b/system/libraries/Encrypt.php new file mode 100644 index 00000000..3d564f99 --- /dev/null +++ b/system/libraries/Encrypt.php @@ -0,0 +1,164 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * The Encrypt library provides two-way encryption of text and binary strings + * using the MCrypt extension. + * @see http://php.net/mcrypt + * + * $Id: Encrypt.php 4072 2009-03-13 17:20:38Z jheathco $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Encrypt_Core { + + // OS-dependant RAND type to use + protected static $rand; + + // Configuration + protected $config; + + /** + * Returns a singleton instance of Encrypt. + * + * @param array configuration options + * @return Encrypt_Core + */ + public static function instance($config = NULL) + { + static $instance; + + // Create the singleton + empty($instance) and $instance = new Encrypt((array) $config); + + return $instance; + } + + /** + * Loads encryption configuration and validates the data. + * + * @param array|string custom configuration or config group name + * @throws Kohana_Exception + */ + public function __construct($config = FALSE) + { + if ( ! defined('MCRYPT_ENCRYPT')) + throw new Kohana_Exception('encrypt.requires_mcrypt'); + + if (is_string($config)) + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('encryption.'.$config)) === NULL) + throw new Kohana_Exception('encrypt.undefined_group', $name); + } + + if (is_array($config)) + { + // Append the default configuration options + $config += Kohana::config('encryption.default'); + } + else + { + // Load the default group + $config = Kohana::config('encryption.default'); + } + + if (empty($config['key'])) + throw new Kohana_Exception('encrypt.no_encryption_key'); + + // Find the max length of the key, based on cipher and mode + $size = mcrypt_get_key_size($config['cipher'], $config['mode']); + + if (strlen($config['key']) > $size) + { + // Shorten the key to the maximum size + $config['key'] = substr($config['key'], 0, $size); + } + + // Find the initialization vector size + $config['iv_size'] = mcrypt_get_iv_size($config['cipher'], $config['mode']); + + // Cache the config in the object + $this->config = $config; + + Kohana::log('debug', 'Encrypt Library initialized'); + } + + /** + * Encrypts a string and returns an encrypted string that can be decoded. + * + * @param string data to be encrypted + * @return string encrypted data + */ + public function encode($data) + { + // Set the rand type if it has not already been set + if (Encrypt::$rand === NULL) + { + if (KOHANA_IS_WIN) + { + // Windows only supports the system random number generator + Encrypt::$rand = MCRYPT_RAND; + } + else + { + if (defined('MCRYPT_DEV_URANDOM')) + { + // Use /dev/urandom + Encrypt::$rand = MCRYPT_DEV_URANDOM; + } + elseif (defined('MCRYPT_DEV_RANDOM')) + { + // Use /dev/random + Encrypt::$rand = MCRYPT_DEV_RANDOM; + } + else + { + // Use the system random number generator + Encrypt::$rand = MCRYPT_RAND; + } + } + } + + if (Encrypt::$rand === MCRYPT_RAND) + { + // The system random number generator must always be seeded each + // time it is used, or it will not produce true random results + mt_srand(); + } + + // Create a random initialization vector of the proper size for the current cipher + $iv = mcrypt_create_iv($this->config['iv_size'], Encrypt::$rand); + + // Encrypt the data using the configured options and generated iv + $data = mcrypt_encrypt($this->config['cipher'], $this->config['key'], $data, $this->config['mode'], $iv); + + // Use base64 encoding to convert to a string + return base64_encode($iv.$data); + } + + /** + * Decrypts an encoded string back to its original value. + * + * @param string encoded string to be decrypted + * @return string decrypted data + */ + public function decode($data) + { + // Convert the data back to binary + $data = base64_decode($data); + + // Extract the initialization vector from the data + $iv = substr($data, 0, $this->config['iv_size']); + + // Remove the iv from the data + $data = substr($data, $this->config['iv_size']); + + // Return the decrypted data, trimming the \0 padding bytes from the end of the data + return rtrim(mcrypt_decrypt($this->config['cipher'], $this->config['key'], $data, $this->config['mode'], $iv), "\0"); + } + +} // End Encrypt diff --git a/system/libraries/Event_Observer.php b/system/libraries/Event_Observer.php new file mode 100644 index 00000000..086c8a23 --- /dev/null +++ b/system/libraries/Event_Observer.php @@ -0,0 +1,70 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Kohana event observer. Uses the SPL observer pattern. + * + * $Id: Event_Observer.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Event_Observer implements SplObserver { + + // Calling object + protected $caller; + + /** + * Initializes a new observer and attaches the subject as the caller. + * + * @param object Event_Subject + * @return void + */ + public function __construct(SplSubject $caller) + { + // Update the caller + $this->update($caller); + } + + /** + * Updates the observer subject with a new caller. + * + * @chainable + * @param object Event_Subject + * @return object + */ + public function update(SplSubject $caller) + { + if ( ! ($caller instanceof Event_Subject)) + throw new Kohana_Exception('event.invalid_subject', get_class($caller), get_class($this)); + + // Update the caller + $this->caller = $caller; + + return $this; + } + + /** + * Detaches this observer from the subject. + * + * @chainable + * @return object + */ + public function remove() + { + // Detach this observer from the caller + $this->caller->detach($this); + + return $this; + } + + /** + * Notify the observer of a new message. This function must be defined in + * all observers and must take exactly one parameter of any type. + * + * @param mixed message string, object, or array + * @return void + */ + abstract public function notify($message); + +} // End Event Observer
\ No newline at end of file diff --git a/system/libraries/Event_Subject.php b/system/libraries/Event_Subject.php new file mode 100644 index 00000000..d1ccc544 --- /dev/null +++ b/system/libraries/Event_Subject.php @@ -0,0 +1,67 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Kohana event subject. Uses the SPL observer pattern. + * + * $Id: Event_Subject.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Event_Subject implements SplSubject { + + // Attached subject listeners + protected $listeners = array(); + + /** + * Attach an observer to the object. + * + * @chainable + * @param object Event_Observer + * @return object + */ + public function attach(SplObserver $obj) + { + if ( ! ($obj instanceof Event_Observer)) + throw new Kohana_Exception('eventable.invalid_observer', get_class($obj), get_class($this)); + + // Add a new listener + $this->listeners[spl_object_hash($obj)] = $obj; + + return $this; + } + + /** + * Detach an observer from the object. + * + * @chainable + * @param object Event_Observer + * @return object + */ + public function detach(SplObserver $obj) + { + // Remove the listener + unset($this->listeners[spl_object_hash($obj)]); + + return $this; + } + + /** + * Notify all attached observers of a new message. + * + * @chainable + * @param mixed message string, object, or array + * @return object + */ + public function notify($message) + { + foreach ($this->listeners as $obj) + { + $obj->notify($message); + } + + return $this; + } + +} // End Event Subject
\ No newline at end of file diff --git a/system/libraries/Image.php b/system/libraries/Image.php new file mode 100644 index 00000000..08c2957c --- /dev/null +++ b/system/libraries/Image.php @@ -0,0 +1,460 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Manipulate images using standard methods such as resize, crop, rotate, etc. + * This class must be re-initialized for every image you wish to manipulate. + * + * $Id: Image.php 4072 2009-03-13 17:20:38Z jheathco $ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Image_Core { + + // Master Dimension + const NONE = 1; + const AUTO = 2; + const HEIGHT = 3; + const WIDTH = 4; + // Flip Directions + const HORIZONTAL = 5; + const VERTICAL = 6; + + // Allowed image types + public static $allowed_types = array + ( + IMAGETYPE_GIF => 'gif', + IMAGETYPE_JPEG => 'jpg', + IMAGETYPE_PNG => 'png', + IMAGETYPE_TIFF_II => 'tiff', + IMAGETYPE_TIFF_MM => 'tiff', + ); + + // Driver instance + protected $driver; + + // Driver actions + protected $actions = array(); + + // Reference to the current image filename + protected $image = ''; + + /** + * Creates a new Image instance and returns it. + * + * @param string filename of image + * @param array non-default configurations + * @return object + */ + public static function factory($image, $config = NULL) + { + return new Image($image, $config); + } + + /** + * Creates a new image editor instance. + * + * @throws Kohana_Exception + * @param string filename of image + * @param array non-default configurations + * @return void + */ + public function __construct($image, $config = NULL) + { + static $check; + + // Make the check exactly once + ($check === NULL) and $check = function_exists('getimagesize'); + + if ($check === FALSE) + throw new Kohana_Exception('image.getimagesize_missing'); + + // Check to make sure the image exists + if ( ! is_file($image)) + throw new Kohana_Exception('image.file_not_found', $image); + + // Disable error reporting, to prevent PHP warnings + $ER = error_reporting(0); + + // Fetch the image size and mime type + $image_info = getimagesize($image); + + // Turn on error reporting again + error_reporting($ER); + + // Make sure that the image is readable and valid + if ( ! is_array($image_info) OR count($image_info) < 3) + throw new Kohana_Exception('image.file_unreadable', $image); + + // Check to make sure the image type is allowed + if ( ! isset(Image::$allowed_types[$image_info[2]])) + throw new Kohana_Exception('image.type_not_allowed', $image); + + // Image has been validated, load it + $this->image = array + ( + 'file' => str_replace('\\', '/', realpath($image)), + 'width' => $image_info[0], + 'height' => $image_info[1], + 'type' => $image_info[2], + 'ext' => Image::$allowed_types[$image_info[2]], + 'mime' => $image_info['mime'] + ); + + // Load configuration + $this->config = (array) $config + Kohana::config('image'); + + // Set driver class name + $driver = 'Image_'.ucfirst($this->config['driver']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('core.driver_not_found', $this->config['driver'], get_class($this)); + + // Initialize the driver + $this->driver = new $driver($this->config['params']); + + // Validate the driver + if ( ! ($this->driver instanceof Image_Driver)) + throw new Kohana_Exception('core.driver_implements', $this->config['driver'], get_class($this), 'Image_Driver'); + } + + /** + * Handles retrieval of pre-save image properties + * + * @param string property name + * @return mixed + */ + public function __get($property) + { + if (isset($this->image[$property])) + { + return $this->image[$property]; + } + else + { + throw new Kohana_Exception('core.invalid_property', $property, get_class($this)); + } + } + + /** + * Resize an image to a specific width and height. By default, Kohana will + * maintain the aspect ratio using the width as the master dimension. If you + * wish to use height as master dim, set $image->master_dim = Image::HEIGHT + * This method is chainable. + * + * @throws Kohana_Exception + * @param integer width + * @param integer height + * @param integer one of: Image::NONE, Image::AUTO, Image::WIDTH, Image::HEIGHT + * @return object + */ + public function resize($width, $height, $master = NULL) + { + if ( ! $this->valid_size('width', $width)) + throw new Kohana_Exception('image.invalid_width', $width); + + if ( ! $this->valid_size('height', $height)) + throw new Kohana_Exception('image.invalid_height', $height); + + if (empty($width) AND empty($height)) + throw new Kohana_Exception('image.invalid_dimensions', __FUNCTION__); + + if ($master === NULL) + { + // Maintain the aspect ratio by default + $master = Image::AUTO; + } + elseif ( ! $this->valid_size('master', $master)) + throw new Kohana_Exception('image.invalid_master'); + + $this->actions['resize'] = array + ( + 'width' => $width, + 'height' => $height, + 'master' => $master, + ); + + return $this; + } + + /** + * Crop an image to a specific width and height. You may also set the top + * and left offset. + * This method is chainable. + * + * @throws Kohana_Exception + * @param integer width + * @param integer height + * @param integer top offset, pixel value or one of: top, center, bottom + * @param integer left offset, pixel value or one of: left, center, right + * @return object + */ + public function crop($width, $height, $top = 'center', $left = 'center') + { + if ( ! $this->valid_size('width', $width)) + throw new Kohana_Exception('image.invalid_width', $width); + + if ( ! $this->valid_size('height', $height)) + throw new Kohana_Exception('image.invalid_height', $height); + + if ( ! $this->valid_size('top', $top)) + throw new Kohana_Exception('image.invalid_top', $top); + + if ( ! $this->valid_size('left', $left)) + throw new Kohana_Exception('image.invalid_left', $left); + + if (empty($width) AND empty($height)) + throw new Kohana_Exception('image.invalid_dimensions', __FUNCTION__); + + $this->actions['crop'] = array + ( + 'width' => $width, + 'height' => $height, + 'top' => $top, + 'left' => $left, + ); + + return $this; + } + + /** + * Allows rotation of an image by 180 degrees clockwise or counter clockwise. + * + * @param integer degrees + * @return object + */ + public function rotate($degrees) + { + $degrees = (int) $degrees; + + if ($degrees > 180) + { + do + { + // Keep subtracting full circles until the degrees have normalized + $degrees -= 360; + } + while($degrees > 180); + } + + if ($degrees < -180) + { + do + { + // Keep adding full circles until the degrees have normalized + $degrees += 360; + } + while($degrees < -180); + } + + $this->actions['rotate'] = $degrees; + + return $this; + } + + /** + * Overlay a second image on top of this one. + * + * @throws Kohana_Exception + * @param string $overlay_file path to an image file + * @param integer $x x offset for the overlay + * @param integer $y y offset for the overlay + * @param integer $transparency transparency percent + */ + public function composite($overlay_file, $x, $y, $transparency) + { + $image_info = getimagesize($overlay_file); + + // Check to make sure the image type is allowed + if ( ! isset(Image::$allowed_types[$image_info[2]])) + throw new Kohana_Exception('image.type_not_allowed', $overlay_file); + + $this->actions['composite'] = array + ( + 'overlay_file' => $overlay_file, + 'mime' => $image_info['mime'], + 'x' => $x, + 'y' => $y, + 'transparency' => $transparency + ); + + return $this; + } + + /** + * Flip an image horizontally or vertically. + * + * @throws Kohana_Exception + * @param integer direction + * @return object + */ + public function flip($direction) + { + if ($direction !== Image::HORIZONTAL AND $direction !== Image::VERTICAL) + throw new Kohana_Exception('image.invalid_flip'); + + $this->actions['flip'] = $direction; + + return $this; + } + + /** + * Change the quality of an image. + * + * @param integer quality as a percentage + * @return object + */ + public function quality($amount) + { + $this->actions['quality'] = max(1, min($amount, 100)); + + return $this; + } + + /** + * Sharpen an image. + * + * @param integer amount to sharpen, usually ~20 is ideal + * @return object + */ + public function sharpen($amount) + { + $this->actions['sharpen'] = max(1, min($amount, 100)); + + return $this; + } + + /** + * Save the image to a new image or overwrite this image. + * + * @throws Kohana_Exception + * @param string new image filename + * @param integer permissions for new image + * @param boolean keep or discard image process actions + * @return object + */ + public function save($new_image = FALSE, $chmod = 0644, $keep_actions = FALSE) + { + // If no new image is defined, use the current image + empty($new_image) and $new_image = $this->image['file']; + + // Separate the directory and filename + $dir = pathinfo($new_image, PATHINFO_DIRNAME); + $file = pathinfo($new_image, PATHINFO_BASENAME); + + // Normalize the path + $dir = str_replace('\\', '/', realpath($dir)).'/'; + + if ( ! is_writable($dir)) + throw new Kohana_Exception('image.directory_unwritable', $dir); + + if ($status = $this->driver->process($this->image, $this->actions, $dir, $file)) + { + if ($chmod !== FALSE) + { + // Set permissions + chmod($new_image, $chmod); + } + } + + // Reset actions. Subsequent save() or render() will not apply previous actions. + if ($keep_actions === FALSE) + $this->actions = array(); + + return $status; + } + + /** + * Output the image to the browser. + * + * @param boolean keep or discard image process actions + * @return object + */ + public function render($keep_actions = FALSE) + { + $new_image = $this->image['file']; + + // Separate the directory and filename + $dir = pathinfo($new_image, PATHINFO_DIRNAME); + $file = pathinfo($new_image, PATHINFO_BASENAME); + + // Normalize the path + $dir = str_replace('\\', '/', realpath($dir)).'/'; + + // Process the image with the driver + $status = $this->driver->process($this->image, $this->actions, $dir, $file, $render = TRUE); + + // Reset actions. Subsequent save() or render() will not apply previous actions. + if ($keep_actions === FALSE) + $this->actions = array(); + + return $status; + } + + /** + * Sanitize a given value type. + * + * @param string type of property + * @param mixed property value + * @return boolean + */ + protected function valid_size($type, & $value) + { + if (is_null($value)) + return TRUE; + + if ( ! is_scalar($value)) + return FALSE; + + switch ($type) + { + case 'width': + case 'height': + if (is_string($value) AND ! ctype_digit($value)) + { + // Only numbers and percent signs + if ( ! preg_match('/^[0-9]++%$/D', $value)) + return FALSE; + } + else + { + $value = (int) $value; + } + break; + case 'top': + if (is_string($value) AND ! ctype_digit($value)) + { + if ( ! in_array($value, array('top', 'bottom', 'center'))) + return FALSE; + } + else + { + $value = (int) $value; + } + break; + case 'left': + if (is_string($value) AND ! ctype_digit($value)) + { + if ( ! in_array($value, array('left', 'right', 'center'))) + return FALSE; + } + else + { + $value = (int) $value; + } + break; + case 'master': + if ($value !== Image::NONE AND + $value !== Image::AUTO AND + $value !== Image::WIDTH AND + $value !== Image::HEIGHT) + return FALSE; + break; + } + + return TRUE; + } + +} // End Image
\ No newline at end of file diff --git a/system/libraries/Input.php b/system/libraries/Input.php new file mode 100644 index 00000000..0e23c800 --- /dev/null +++ b/system/libraries/Input.php @@ -0,0 +1,452 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Input library. + * + * $Id: Input.php 4346 2009-05-11 17:08:15Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Input_Core { + + // Enable or disable automatic XSS cleaning + protected $use_xss_clean = FALSE; + + // Are magic quotes enabled? + protected $magic_quotes_gpc = FALSE; + + // IP address of current user + public $ip_address; + + // Input singleton + protected static $instance; + + /** + * Retrieve a singleton instance of Input. This will always be the first + * created instance of this class. + * + * @return object + */ + public static function instance() + { + if (Input::$instance === NULL) + { + // Create a new instance + return new Input; + } + + return Input::$instance; + } + + /** + * Sanitizes global GET, POST and COOKIE data. Also takes care of + * magic_quotes and register_globals, if they have been enabled. + * + * @return void + */ + public function __construct() + { + // Use XSS clean? + $this->use_xss_clean = (bool) Kohana::config('core.global_xss_filtering'); + + if (Input::$instance === NULL) + { + // magic_quotes_runtime is enabled + if (get_magic_quotes_runtime()) + { + set_magic_quotes_runtime(0); + Kohana::log('debug', 'Disable magic_quotes_runtime! It is evil and deprecated: http://php.net/magic_quotes'); + } + + // magic_quotes_gpc is enabled + if (get_magic_quotes_gpc()) + { + $this->magic_quotes_gpc = TRUE; + Kohana::log('debug', 'Disable magic_quotes_gpc! It is evil and deprecated: http://php.net/magic_quotes'); + } + + // register_globals is enabled + if (ini_get('register_globals')) + { + if (isset($_REQUEST['GLOBALS'])) + { + // Prevent GLOBALS override attacks + exit('Global variable overload attack.'); + } + + // Destroy the REQUEST global + $_REQUEST = array(); + + // These globals are standard and should not be removed + $preserve = array('GLOBALS', '_REQUEST', '_GET', '_POST', '_FILES', '_COOKIE', '_SERVER', '_ENV', '_SESSION'); + + // This loop has the same effect as disabling register_globals + foreach (array_diff(array_keys($GLOBALS), $preserve) as $key) + { + global $$key; + $$key = NULL; + + // Unset the global variable + unset($GLOBALS[$key], $$key); + } + + // Warn the developer about register globals + Kohana::log('debug', 'Disable register_globals! It is evil and deprecated: http://php.net/register_globals'); + } + + if (is_array($_GET)) + { + foreach ($_GET as $key => $val) + { + // Sanitize $_GET + $_GET[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + } + else + { + $_GET = array(); + } + + if (is_array($_POST)) + { + foreach ($_POST as $key => $val) + { + // Sanitize $_POST + $_POST[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + } + else + { + $_POST = array(); + } + + if (is_array($_COOKIE)) + { + foreach ($_COOKIE as $key => $val) + { + // Ignore special attributes in RFC2109 compliant cookies + if ($key == '$Version' OR $key == '$Path' OR $key == '$Domain') + continue; + + // Sanitize $_COOKIE + $_COOKIE[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + } + else + { + $_COOKIE = array(); + } + + // Create a singleton + Input::$instance = $this; + + Kohana::log('debug', 'Global GET, POST and COOKIE data sanitized'); + } + } + + /** + * Fetch an item from the $_GET array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function get($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_GET, $key, $default, $xss_clean); + } + + /** + * Fetch an item from the $_POST array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function post($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_POST, $key, $default, $xss_clean); + } + + /** + * Fetch an item from the $_COOKIE array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function cookie($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_COOKIE, $key, $default, $xss_clean); + } + + /** + * Fetch an item from the $_SERVER array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function server($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_SERVER, $key, $default, $xss_clean); + } + + /** + * Fetch an item from a global array. + * + * @param array array to search + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + protected function search_array($array, $key, $default = NULL, $xss_clean = FALSE) + { + if ($key === array()) + return $array; + + if ( ! isset($array[$key])) + return $default; + + // Get the value + $value = $array[$key]; + + if ($this->use_xss_clean === FALSE AND $xss_clean === TRUE) + { + // XSS clean the value + $value = $this->xss_clean($value); + } + + return $value; + } + + /** + * Fetch the IP Address. + * + * @return string + */ + public function ip_address() + { + if ($this->ip_address !== NULL) + return $this->ip_address; + + // Server keys that could contain the client IP address + $keys = array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'); + + foreach ($keys as $key) + { + if ($ip = $this->server($key)) + { + $this->ip_address = $ip; + + // An IP address has been found + break; + } + } + + if ($comma = strrpos($this->ip_address, ',') !== FALSE) + { + $this->ip_address = substr($this->ip_address, $comma + 1); + } + + if ( ! valid::ip($this->ip_address)) + { + // Use an empty IP + $this->ip_address = '0.0.0.0'; + } + + return $this->ip_address; + } + + /** + * Clean cross site scripting exploits from string. + * HTMLPurifier may be used if installed, otherwise defaults to built in method. + * Note - This function should only be used to deal with data upon submission. + * It's not something that should be used for general runtime processing + * since it requires a fair amount of processing overhead. + * + * @param string data to clean + * @param string xss_clean method to use ('htmlpurifier' or defaults to built-in method) + * @return string + */ + public function xss_clean($data, $tool = NULL) + { + if ($tool === NULL) + { + // Use the default tool + $tool = Kohana::config('core.global_xss_filtering'); + } + + if (is_array($data)) + { + foreach ($data as $key => $val) + { + $data[$key] = $this->xss_clean($val, $tool); + } + + return $data; + } + + // Do not clean empty strings + if (trim($data) === '') + return $data; + + if ($tool === TRUE) + { + // NOTE: This is necessary because switch is NOT type-sensative! + $tool = 'default'; + } + + switch ($tool) + { + case 'htmlpurifier': + /** + * @todo License should go here, http://htmlpurifier.org/ + */ + if ( ! class_exists('HTMLPurifier_Config', FALSE)) + { + // Load HTMLPurifier + require Kohana::find_file('vendor', 'htmlpurifier/HTMLPurifier.auto', TRUE); + require 'HTMLPurifier.func.php'; + } + + // Set configuration + $config = HTMLPurifier_Config::createDefault(); + $config->set('HTML', 'TidyLevel', 'none'); // Only XSS cleaning now + + // Run HTMLPurifier + $data = HTMLPurifier($data, $config); + break; + default: + // http://svn.bitflux.ch/repos/public/popoon/trunk/classes/externalinput.php + // +----------------------------------------------------------------------+ + // | Copyright (c) 2001-2006 Bitflux GmbH | + // +----------------------------------------------------------------------+ + // | Licensed under the Apache License, Version 2.0 (the "License"); | + // | you may not use this file except in compliance with the License. | + // | You may obtain a copy of the License at | + // | http://www.apache.org/licenses/LICENSE-2.0 | + // | Unless required by applicable law or agreed to in writing, software | + // | distributed under the License is distributed on an "AS IS" BASIS, | + // | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | + // | implied. See the License for the specific language governing | + // | permissions and limitations under the License. | + // +----------------------------------------------------------------------+ + // | Author: Christian Stocker <chregu@bitflux.ch> | + // +----------------------------------------------------------------------+ + // + // Kohana Modifications: + // * Changed double quotes to single quotes, changed indenting and spacing + // * Removed magic_quotes stuff + // * Increased regex readability: + // * Used delimeters that aren't found in the pattern + // * Removed all unneeded escapes + // * Deleted U modifiers and swapped greediness where needed + // * Increased regex speed: + // * Made capturing parentheses non-capturing where possible + // * Removed parentheses where possible + // * Split up alternation alternatives + // * Made some quantifiers possessive + + // Fix &entity\n; + $data = str_replace(array('&','<','>'), array('&amp;','&lt;','&gt;'), $data); + $data = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $data); + $data = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $data); + $data = html_entity_decode($data, ENT_COMPAT, 'UTF-8'); + + // Remove any attribute starting with "on" or xmlns + $data = preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*+>#iu', '$1>', $data); + + // Remove javascript: and vbscript: protocols + $data = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $data); + $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $data); + $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $data); + + // Only works in IE: <span style="width: expression(alert('Ping!'));"></span> + $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i', '$1>', $data); + $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i', '$1>', $data); + $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu', '$1>', $data); + + // Remove namespaced elements (we do not need them) + $data = preg_replace('#</*\w+:\w[^>]*+>#i', '', $data); + + do + { + // Remove really unwanted tags + $old_data = $data; + $data = preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^>]*+>#i', '', $data); + } + while ($old_data !== $data); + break; + } + + return $data; + } + + /** + * This is a helper method. It enforces W3C specifications for allowed + * key name strings, to prevent malicious exploitation. + * + * @param string string to clean + * @return string + */ + public function clean_input_keys($str) + { + $chars = PCRE_UNICODE_PROPERTIES ? '\pL' : 'a-zA-Z'; + + if ( ! preg_match('#^['.$chars.'0-9:_.-]++$#uD', $str)) + { + exit('Disallowed key characters in global data.'); + } + + return $str; + } + + /** + * This is a helper method. It escapes data and forces all newline + * characters to "\n". + * + * @param unknown_type string to clean + * @return string + */ + public function clean_input_data($str) + { + if (is_array($str)) + { + $new_array = array(); + foreach ($str as $key => $val) + { + // Recursion! + $new_array[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + return $new_array; + } + + if ($this->magic_quotes_gpc === TRUE) + { + // Remove annoying magic quotes + $str = stripslashes($str); + } + + if ($this->use_xss_clean === TRUE) + { + $str = $this->xss_clean($str); + } + + if (strpos($str, "\r") !== FALSE) + { + // Standardize newlines + $str = str_replace(array("\r\n", "\r"), "\n", $str); + } + + return $str; + } + +} // End Input Class diff --git a/system/libraries/Model.php b/system/libraries/Model.php new file mode 100644 index 00000000..0c9fd8d6 --- /dev/null +++ b/system/libraries/Model.php @@ -0,0 +1,31 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Model base class. + * + * $Id: Model.php 4007 2009-02-20 01:54:00Z jheathco $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Model_Core { + + // Database object + protected $db = 'default'; + + /** + * Loads the database instance, if the database is not already loaded. + * + * @return void + */ + public function __construct() + { + if ( ! is_object($this->db)) + { + // Load the default database + $this->db = Database::instance($this->db); + } + } + +} // End Model
\ No newline at end of file diff --git a/system/libraries/ORM.php b/system/libraries/ORM.php new file mode 100644 index 00000000..c1048604 --- /dev/null +++ b/system/libraries/ORM.php @@ -0,0 +1,1431 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * [Object Relational Mapping][ref-orm] (ORM) is a method of abstracting database + * access to standard PHP calls. All table rows are represented as model objects, + * with object properties representing row data. ORM in Kohana generally follows + * the [Active Record][ref-act] pattern. + * + * [ref-orm]: http://wikipedia.org/wiki/Object-relational_mapping + * [ref-act]: http://wikipedia.org/wiki/Active_record + * + * $Id: ORM.php 4354 2009-05-15 16:51:37Z kiall $ + * + * @package ORM + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class ORM_Core { + + // Current relationships + protected $has_one = array(); + protected $belongs_to = array(); + protected $has_many = array(); + protected $has_and_belongs_to_many = array(); + + // Relationships that should always be joined + protected $load_with = array(); + + // Current object + protected $object = array(); + protected $changed = array(); + protected $related = array(); + protected $loaded = FALSE; + protected $saved = FALSE; + protected $sorting; + + // Related objects + protected $object_relations = array(); + protected $changed_relations = array(); + + // Model table information + protected $object_name; + protected $object_plural; + protected $table_name; + protected $table_columns; + protected $ignored_columns; + + // Table primary key and value + protected $primary_key = 'id'; + protected $primary_val = 'name'; + + // Array of foreign key name overloads + protected $foreign_key = array(); + + // Model configuration + protected $table_names_plural = TRUE; + protected $reload_on_wakeup = TRUE; + + // Database configuration + protected $db = 'default'; + protected $db_applied = array(); + + // With calls already applied + protected $with_applied = array(); + + // Stores column information for ORM models + protected static $column_cache = array(); + + /** + * Creates and returns a new model. + * + * @chainable + * @param string model name + * @param mixed parameter for find() + * @return ORM + */ + public static function factory($model, $id = NULL) + { + // Set class name + $model = ucfirst($model).'_Model'; + + return new $model($id); + } + + /** + * Prepares the model database connection and loads the object. + * + * @param mixed parameter for find or object to load + * @return void + */ + public function __construct($id = NULL) + { + // Set the object name and plural name + $this->object_name = strtolower(substr(get_class($this), 0, -6)); + $this->object_plural = inflector::plural($this->object_name); + + if (!isset($this->sorting)) + { + // Default sorting + $this->sorting = array($this->primary_key => 'asc'); + } + + // Initialize database + $this->__initialize(); + + // Clear the object + $this->clear(); + + if (is_object($id)) + { + // Load an object + $this->load_values((array) $id); + } + elseif (!empty($id)) + { + // Find an object + $this->find($id); + } + } + + /** + * Prepares the model database connection, determines the table name, + * and loads column information. + * + * @return void + */ + public function __initialize() + { + if ( ! is_object($this->db)) + { + // Get database instance + $this->db = Database::instance($this->db); + } + + if (empty($this->table_name)) + { + // Table name is the same as the object name + $this->table_name = $this->object_name; + + if ($this->table_names_plural === TRUE) + { + // Make the table name plural + $this->table_name = inflector::plural($this->table_name); + } + } + + if (is_array($this->ignored_columns)) + { + // Make the ignored columns mirrored = mirrored + $this->ignored_columns = array_combine($this->ignored_columns, $this->ignored_columns); + } + + // Load column information + $this->reload_columns(); + } + + /** + * Allows serialization of only the object data and state, to prevent + * "stale" objects being unserialized, which also requires less memory. + * + * @return array + */ + public function __sleep() + { + // Store only information about the object + return array('object_name', 'object', 'changed', 'loaded', 'saved', 'sorting'); + } + + /** + * Prepares the database connection and reloads the object. + * + * @return void + */ + public function __wakeup() + { + // Initialize database + $this->__initialize(); + + if ($this->reload_on_wakeup === TRUE) + { + // Reload the object + $this->reload(); + } + } + + /** + * Handles pass-through to database methods. Calls to query methods + * (query, get, insert, update) are not allowed. Query builder methods + * are chainable. + * + * @param string method name + * @param array method arguments + * @return mixed + */ + public function __call($method, array $args) + { + if (method_exists($this->db, $method)) + { + if (in_array($method, array('query', 'get', 'insert', 'update', 'delete'))) + throw new Kohana_Exception('orm.query_methods_not_allowed'); + + // Method has been applied to the database + $this->db_applied[$method] = $method; + + // Number of arguments passed + $num_args = count($args); + + if ($method === 'select' AND $num_args > 3) + { + // Call select() manually to avoid call_user_func_array + $this->db->select($args); + } + else + { + // We use switch here to manually call the database methods. This is + // done for speed: call_user_func_array can take over 300% longer to + // make calls. Most database methods are 4 arguments or less, so this + // avoids almost any calls to call_user_func_array. + + switch ($num_args) + { + case 0: + if (in_array($method, array('open_paren', 'close_paren', 'enable_cache', 'disable_cache'))) + { + // Should return ORM, not Database + $this->db->$method(); + } + else + { + // Support for things like reset_select, reset_write, list_tables + return $this->db->$method(); + } + break; + case 1: + $this->db->$method($args[0]); + break; + case 2: + $this->db->$method($args[0], $args[1]); + break; + case 3: + $this->db->$method($args[0], $args[1], $args[2]); + break; + case 4: + $this->db->$method($args[0], $args[1], $args[2], $args[3]); + break; + default: + // Here comes the snail... + call_user_func_array(array($this->db, $method), $args); + break; + } + } + + return $this; + } + else + { + throw new Kohana_Exception('core.invalid_method', $method, get_class($this)); + } + } + + /** + * Handles retrieval of all model values, relationships, and metadata. + * + * @param string column name + * @return mixed + */ + public function __get($column) + { + if (array_key_exists($column, $this->object)) + { + return $this->object[$column]; + } + elseif (isset($this->related[$column])) + { + return $this->related[$column]; + } + elseif ($column === 'primary_key_value') + { + return $this->object[$this->primary_key]; + } + elseif ($model = $this->related_object($column)) + { + // This handles the has_one and belongs_to relationships + + if (in_array($model->object_name, $this->belongs_to) OR ! array_key_exists($this->foreign_key($column), $model->object)) + { + // Foreign key lies in this table (this model belongs_to target model) OR an invalid has_one relationship + $where = array($model->table_name.'.'.$model->primary_key => $this->object[$this->foreign_key($column)]); + } + else + { + // Foreign key lies in the target table (this model has_one target model) + $where = array($this->foreign_key($column, $model->table_name) => $this->primary_key_value); + } + + // one<>alias:one relationship + return $this->related[$column] = $model->find($where); + } + elseif (isset($this->has_many[$column])) + { + // Load the "middle" model + $through = ORM::factory(inflector::singular($this->has_many[$column])); + + // Load the "end" model + $model = ORM::factory(inflector::singular($column)); + + // Join ON target model's primary key set to 'through' model's foreign key + // User-defined foreign keys must be defined in the 'through' model + $join_table = $through->table_name; + $join_col1 = $through->foreign_key($model->object_name, $join_table); + $join_col2 = $model->table_name.'.'.$model->primary_key; + + // one<>alias:many relationship + return $this->related[$column] = $model + ->join($join_table, $join_col1, $join_col2) + ->where($through->foreign_key($this->object_name, $join_table), $this->object[$this->primary_key]) + ->find_all(); + } + elseif (in_array($column, $this->has_many)) + { + // one<>many relationship + $model = ORM::factory(inflector::singular($column)); + return $this->related[$column] = $model + ->where($this->foreign_key($column, $model->table_name), $this->object[$this->primary_key]) + ->find_all(); + } + elseif (in_array($column, $this->has_and_belongs_to_many)) + { + // Load the remote model, always singular + $model = ORM::factory(inflector::singular($column)); + + if ($this->has($model, TRUE)) + { + // many<>many relationship + return $this->related[$column] = $model + ->in($model->table_name.'.'.$model->primary_key, $this->changed_relations[$column]) + ->find_all(); + } + else + { + // empty many<>many relationship + return $this->related[$column] = $model + ->where($model->table_name.'.'.$model->primary_key, NULL) + ->find_all(); + } + } + elseif (isset($this->ignored_columns[$column])) + { + return NULL; + } + elseif (in_array($column, array + ( + 'object_name', 'object_plural', // Object + 'primary_key', 'primary_val', 'table_name', 'table_columns', // Table + 'loaded', 'saved', // Status + 'has_one', 'belongs_to', 'has_many', 'has_and_belongs_to_many', 'load_with' // Relationships + ))) + { + // Model meta information + return $this->$column; + } + else + { + throw new Kohana_Exception('core.invalid_property', $column, get_class($this)); + } + } + + /** + * Handles setting of all model values, and tracks changes between values. + * + * @param string column name + * @param mixed column value + * @return void + */ + public function __set($column, $value) + { + if (isset($this->ignored_columns[$column])) + { + return NULL; + } + elseif (isset($this->object[$column]) OR array_key_exists($column, $this->object)) + { + if (isset($this->table_columns[$column])) + { + // Data has changed + $this->changed[$column] = $column; + + // Object is no longer saved + $this->saved = FALSE; + } + + $this->object[$column] = $this->load_type($column, $value); + } + elseif (in_array($column, $this->has_and_belongs_to_many) AND is_array($value)) + { + // Load relations + $model = ORM::factory(inflector::singular($column)); + + if ( ! isset($this->object_relations[$column])) + { + // Load relations + $this->has($model); + } + + // Change the relationships + $this->changed_relations[$column] = $value; + + if (isset($this->related[$column])) + { + // Force a reload of the relationships + unset($this->related[$column]); + } + } + else + { + throw new Kohana_Exception('core.invalid_property', $column, get_class($this)); + } + } + + /** + * Checks if object data is set. + * + * @param string column name + * @return boolean + */ + public function __isset($column) + { + return (isset($this->object[$column]) OR isset($this->related[$column])); + } + + /** + * Unsets object data. + * + * @param string column name + * @return void + */ + public function __unset($column) + { + unset($this->object[$column], $this->changed[$column], $this->related[$column]); + } + + /** + * Displays the primary key of a model when it is converted to a string. + * + * @return string + */ + public function __toString() + { + return (string) $this->object[$this->primary_key]; + } + + /** + * Returns the values of this object as an array. + * + * @return array + */ + public function as_array() + { + $object = array(); + + foreach ($this->object as $key => $val) + { + // Reconstruct the array (calls __get) + $object[$key] = $this->$key; + } + + return $object; + } + + /** + * Binds another one-to-one object to this model. One-to-one objects + * can be nested using 'object1:object2' syntax + * + * @param string $target_path + * @return void + */ + public function with($target_path) + { + if (isset($this->with_applied[$target_path])) + { + // Don't join anything already joined + return $this; + } + + // Split object parts + $objects = explode(':', $target_path); + $target = $this; + foreach ($objects as $object) + { + // Go down the line of objects to find the given target + $parent = $target; + $target = $parent->related_object($object); + + if ( ! $target) + { + // Can't find related object + return $this; + } + } + + $target_name = $object; + + // Pop-off top object to get the parent object (user:photo:tag becomes user:photo - the parent table prefix) + array_pop($objects); + $parent_path = implode(':', $objects); + + if (empty($parent_path)) + { + // Use this table name itself for the parent object + $parent_path = $this->table_name; + } + else + { + if( ! isset($this->with_applied[$parent_path])) + { + // If the parent object hasn't been joined yet, do it first (otherwise LEFT JOINs fail) + $this->with($parent_path); + } + } + + // Add to with_applied to prevent duplicate joins + $this->with_applied[$target_path] = TRUE; + + // Use the keys of the empty object to determine the columns + $select = array_keys($target->object); + foreach ($select as $i => $column) + { + // Add the prefix so that load_result can determine the relationship + $select[$i] = $target_path.'.'.$column.' AS '.$target_path.':'.$column; + } + + + // Select all of the prefixed keys in the object + $this->db->select($select); + + if (in_array($target->object_name, $parent->belongs_to) OR ! isset($target->object[$parent->foreign_key($target_name)])) + { + // Parent belongs_to target, use target's primary key as join column + $join_col1 = $target->foreign_key(TRUE, $target_path); + $join_col2 = $parent->foreign_key($target_name, $parent_path); + } + else + { + // Parent has_one target, use parent's primary key as join column + $join_col2 = $parent->foreign_key(TRUE, $parent_path); + $join_col1 = $parent->foreign_key($target_name, $target_path); + } + + // This allows for models to use different table prefixes (sharing the same database) + $join_table = new Database_Expression($target->db->table_prefix().$target->table_name.' AS '.$this->db->table_prefix().$target_path); + + // Join the related object into the result + $this->db->join($join_table, $join_col1, $join_col2, 'LEFT'); + + return $this; + } + + /** + * Finds and loads a single database row into the object. + * + * @chainable + * @param mixed primary key or an array of clauses + * @return ORM + */ + public function find($id = NULL) + { + if ($id !== NULL) + { + if (is_array($id)) + { + // Search for all clauses + $this->db->where($id); + } + else + { + // Search for a specific column + $this->db->where($this->table_name.'.'.$this->unique_key($id), $id); + } + } + + return $this->load_result(); + } + + /** + * Finds multiple database rows and returns an iterator of the rows found. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @return ORM_Iterator + */ + public function find_all($limit = NULL, $offset = NULL) + { + if ($limit !== NULL AND ! isset($this->db_applied['limit'])) + { + // Set limit + $this->limit($limit); + } + + if ($offset !== NULL AND ! isset($this->db_applied['offset'])) + { + // Set offset + $this->offset($offset); + } + + return $this->load_result(TRUE); + } + + /** + * Creates a key/value array from all of the objects available. Uses find_all + * to find the objects. + * + * @param string key column + * @param string value column + * @return array + */ + public function select_list($key = NULL, $val = NULL) + { + if ($key === NULL) + { + $key = $this->primary_key; + } + + if ($val === NULL) + { + $val = $this->primary_val; + } + + // Return a select list from the results + return $this->select($key, $val)->find_all()->select_list($key, $val); + } + + /** + * Validates the current object. This method should generally be called + * via the model, after the $_POST Validation object has been created. + * + * @param object Validation array + * @return boolean + */ + public function validate(Validation $array, $save = FALSE) + { + $safe_array = $array->safe_array(); + + if ( ! $array->submitted()) + { + foreach ($safe_array as $key => $value) + { + // Get the value from this object + $value = $this->$key; + + if (is_object($value) AND $value instanceof ORM_Iterator) + { + // Convert the value to an array of primary keys + $value = $value->primary_key_array(); + } + + // Pre-fill data + $array[$key] = $value; + } + } + + // Validate the array + if ($status = $array->validate()) + { + // Grab only set fields (excludes missing data, unlike safe_array) + $fields = $array->as_array(); + + foreach ($fields as $key => $value) + { + if (isset($safe_array[$key])) + { + // Set new data, ignoring any missing fields or fields without rules + $this->$key = $value; + } + } + + if ($save === TRUE OR is_string($save)) + { + // Save this object + $this->save(); + + if (is_string($save)) + { + // Redirect to the saved page + url::redirect($save); + } + } + } + + // Return validation status + return $status; + } + + /** + * Saves the current object. + * + * @chainable + * @return ORM + */ + public function save() + { + if ( ! empty($this->changed)) + { + $data = array(); + foreach ($this->changed as $column) + { + // Compile changed data + $data[$column] = $this->object[$column]; + } + + if ($this->loaded === TRUE) + { + $query = $this->db + ->where($this->primary_key, $this->object[$this->primary_key]) + ->update($this->table_name, $data); + + // Object has been saved + $this->saved = TRUE; + } + else + { + $query = $this->db + ->insert($this->table_name, $data); + + if ($query->count() > 0) + { + if (empty($this->object[$this->primary_key])) + { + // Load the insert id as the primary key + $this->object[$this->primary_key] = $query->insert_id(); + } + + // Object is now loaded and saved + $this->loaded = $this->saved = TRUE; + } + } + + if ($this->saved === TRUE) + { + // All changes have been saved + $this->changed = array(); + } + } + + if ($this->saved === TRUE AND ! empty($this->changed_relations)) + { + foreach ($this->changed_relations as $column => $values) + { + // All values that were added + $added = array_diff($values, $this->object_relations[$column]); + + // All values that were saved + $removed = array_diff($this->object_relations[$column], $values); + + if (empty($added) AND empty($removed)) + { + // No need to bother + continue; + } + + // Clear related columns + unset($this->related[$column]); + + // Load the model + $model = ORM::factory(inflector::singular($column)); + + if (($join_table = array_search($column, $this->has_and_belongs_to_many)) === FALSE) + continue; + + if (is_int($join_table)) + { + // No "through" table, load the default JOIN table + $join_table = $model->join_table($this->table_name); + } + + // Foreign keys for the join table + $object_fk = $this->foreign_key(NULL); + $related_fk = $model->foreign_key(NULL); + + if ( ! empty($added)) + { + foreach ($added as $id) + { + // Insert the new relationship + $this->db->insert($join_table, array + ( + $object_fk => $this->object[$this->primary_key], + $related_fk => $id, + )); + } + } + + if ( ! empty($removed)) + { + $this->db + ->where($object_fk, $this->object[$this->primary_key]) + ->in($related_fk, $removed) + ->delete($join_table); + } + + // Clear all relations for this column + unset($this->object_relations[$column], $this->changed_relations[$column]); + } + } + + return $this; + } + + /** + * Deletes the current object from the database. This does NOT destroy + * relationships that have been created with other objects. + * + * @chainable + * @return ORM + */ + public function delete($id = NULL) + { + if ($id === NULL AND $this->loaded) + { + // Use the the primary key value + $id = $this->object[$this->primary_key]; + } + + // Delete this object + $this->db->where($this->primary_key, $id)->delete($this->table_name); + + return $this->clear(); + } + + /** + * Delete all objects in the associated table. This does NOT destroy + * relationships that have been created with other objects. + * + * @chainable + * @param array ids to delete + * @return ORM + */ + public function delete_all($ids = NULL) + { + if (is_array($ids)) + { + // Delete only given ids + $this->db->in($this->primary_key, $ids); + } + elseif (is_null($ids)) + { + // Delete all records + $this->db->where('1=1'); + } + else + { + // Do nothing - safeguard + return $this; + } + + // Delete all objects + $this->db->delete($this->table_name); + + return $this->clear(); + } + + /** + * Unloads the current object and clears the status. + * + * @chainable + * @return ORM + */ + public function clear() + { + // Create an array with all the columns set to NULL + $columns = array_keys($this->table_columns); + $values = array_combine($columns, array_fill(0, count($columns), NULL)); + + // Replace the current object with an empty one + $this->load_values($values); + + return $this; + } + + /** + * Reloads the current object from the database. + * + * @chainable + * @return ORM + */ + public function reload() + { + return $this->find($this->object[$this->primary_key]); + } + + /** + * Reload column definitions. + * + * @chainable + * @param boolean force reloading + * @return ORM + */ + public function reload_columns($force = FALSE) + { + if ($force === TRUE OR empty($this->table_columns)) + { + if (isset(ORM::$column_cache[$this->object_name])) + { + // Use cached column information + $this->table_columns = ORM::$column_cache[$this->object_name]; + } + else + { + // Load table columns + ORM::$column_cache[$this->object_name] = $this->table_columns = $this->list_fields(); + } + } + + return $this; + } + + /** + * Tests if this object has a relationship to a different model. + * + * @param object related ORM model + * @param boolean check for any relations to given model + * @return boolean + */ + public function has(ORM $model, $any = FALSE) + { + // Determine plural or singular relation name + $related = ($model->table_names_plural === TRUE) ? $model->object_plural : $model->object_name; + + if (($join_table = array_search($related, $this->has_and_belongs_to_many)) === FALSE) + return FALSE; + + if (is_int($join_table)) + { + // No "through" table, load the default JOIN table + $join_table = $model->join_table($this->table_name); + } + + if ( ! isset($this->object_relations[$related])) + { + // Load the object relationships + $this->changed_relations[$related] = $this->object_relations[$related] = $this->load_relations($join_table, $model); + } + + if ( ! $model->empty_primary_key()) + { + // Check if a specific object exists + return in_array($model->primary_key_value, $this->changed_relations[$related]); + } + elseif ($any) + { + // Check if any relations to given model exist + return ! empty($this->changed_relations[$related]); + } + else + { + return FALSE; + } + } + + /** + * Adds a new relationship to between this model and another. + * + * @param object related ORM model + * @return boolean + */ + public function add(ORM $model) + { + if ($this->has($model)) + return TRUE; + + // Get the faked column name + $column = $model->object_plural; + + // Add the new relation to the update + $this->changed_relations[$column][] = $model->primary_key_value; + + if (isset($this->related[$column])) + { + // Force a reload of the relationships + unset($this->related[$column]); + } + + return TRUE; + } + + /** + * Adds a new relationship to between this model and another. + * + * @param object related ORM model + * @return boolean + */ + public function remove(ORM $model) + { + if ( ! $this->has($model)) + return FALSE; + + // Get the faked column name + $column = $model->object_plural; + + if (($key = array_search($model->primary_key_value, $this->changed_relations[$column])) === FALSE) + return FALSE; + + // Remove the relationship + unset($this->changed_relations[$column][$key]); + + if (isset($this->related[$column])) + { + // Force a reload of the relationships + unset($this->related[$column]); + } + + return TRUE; + } + + /** + * Count the number of records in the table. + * + * @return integer + */ + public function count_all() + { + // Return the total number of records in a table + return $this->db->count_records($this->table_name); + } + + /** + * Proxy method to Database list_fields. + * + * @param string table name or NULL to use this table + * @return array + */ + public function list_fields($table = NULL) + { + if ($table === NULL) + { + $table = $this->table_name; + } + + // Proxy to database + return $this->db->list_fields($table); + } + + /** + * Proxy method to Database field_data. + * + * @param string table name + * @return array + */ + public function field_data($table) + { + // Proxy to database + return $this->db->field_data($table); + } + + /** + * Proxy method to Database field_data. + * + * @chainable + * @param string SQL query to clear + * @return ORM + */ + public function clear_cache($sql = NULL) + { + // Proxy to database + $this->db->clear_cache($sql); + + ORM::$column_cache = array(); + + return $this; + } + + /** + * Returns the unique key for a specific value. This method is expected + * to be overloaded in models if the model has other unique columns. + * + * @param mixed unique value + * @return string + */ + public function unique_key($id) + { + return $this->primary_key; + } + + /** + * Determines the name of a foreign key for a specific table. + * + * @param string related table name + * @param string prefix table name (used for JOINs) + * @return string + */ + public function foreign_key($table = NULL, $prefix_table = NULL) + { + if ($table === TRUE) + { + if (is_string($prefix_table)) + { + // Use prefix table name and this table's PK + return $prefix_table.'.'.$this->primary_key; + } + else + { + // Return the name of this table's PK + return $this->table_name.'.'.$this->primary_key; + } + } + + if (is_string($prefix_table)) + { + // Add a period for prefix_table.column support + $prefix_table .= '.'; + } + + if (isset($this->foreign_key[$table])) + { + // Use the defined foreign key name, no magic here! + $foreign_key = $this->foreign_key[$table]; + } + else + { + if ( ! is_string($table) OR ! array_key_exists($table.'_'.$this->primary_key, $this->object)) + { + // Use this table + $table = $this->table_name; + + if (strpos($table, '.') !== FALSE) + { + // Hack around support for PostgreSQL schemas + list ($schema, $table) = explode('.', $table, 2); + } + + if ($this->table_names_plural === TRUE) + { + // Make the key name singular + $table = inflector::singular($table); + } + } + + $foreign_key = $table.'_'.$this->primary_key; + } + + return $prefix_table.$foreign_key; + } + + /** + * This uses alphabetical comparison to choose the name of the table. + * + * Example: The joining table of users and roles would be roles_users, + * because "r" comes before "u". Joining products and categories would + * result in categories_products, because "c" comes before "p". + * + * Example: zoo > zebra > robber > ocean > angel > aardvark + * + * @param string table name + * @return string + */ + public function join_table($table) + { + if ($this->table_name > $table) + { + $table = $table.'_'.$this->table_name; + } + else + { + $table = $this->table_name.'_'.$table; + } + + return $table; + } + + /** + * Returns an ORM model for the given object name; + * + * @param string object name + * @return ORM + */ + protected function related_object($object) + { + if (isset($this->has_one[$object])) + { + $object = ORM::factory($this->has_one[$object]); + } + elseif (isset($this->belongs_to[$object])) + { + $object = ORM::factory($this->belongs_to[$object]); + } + elseif (in_array($object, $this->has_one) OR in_array($object, $this->belongs_to)) + { + $object = ORM::factory($object); + } + else + { + return FALSE; + } + + return $object; + } + + /** + * Loads an array of values into into the current object. + * + * @chainable + * @param array values to load + * @return ORM + */ + public function load_values(array $values) + { + if (array_key_exists($this->primary_key, $values)) + { + // Replace the object and reset the object status + $this->object = $this->changed = $this->related = array(); + + // Set the loaded and saved object status based on the primary key + $this->loaded = $this->saved = ($values[$this->primary_key] !== NULL); + } + + // Related objects + $related = array(); + + foreach ($values as $column => $value) + { + if (strpos($column, ':') === FALSE) + { + if (isset($this->table_columns[$column])) + { + // The type of the value can be determined, convert the value + $value = $this->load_type($column, $value); + } + + $this->object[$column] = $value; + } + else + { + list ($prefix, $column) = explode(':', $column, 2); + + $related[$prefix][$column] = $value; + } + } + + if ( ! empty($related)) + { + foreach ($related as $object => $values) + { + // Load the related objects with the values in the result + $this->related[$object] = $this->related_object($object)->load_values($values); + } + } + + return $this; + } + + /** + * Loads a value according to the types defined by the column metadata. + * + * @param string column name + * @param mixed value to load + * @return mixed + */ + protected function load_type($column, $value) + { + $type = gettype($value); + if ($type == 'object' OR $type == 'array' OR ! isset($this->table_columns[$column])) + return $value; + + // Load column data + $column = $this->table_columns[$column]; + + if ($value === NULL AND ! empty($column['null'])) + return $value; + + if ( ! empty($column['binary']) AND ! empty($column['exact']) AND (int) $column['length'] === 1) + { + // Use boolean for BINARY(1) fields + $column['type'] = 'boolean'; + } + + switch ($column['type']) + { + case 'int': + if ($value === '' AND ! empty($column['null'])) + { + // Forms will only submit strings, so empty integer values must be null + $value = NULL; + } + elseif ((float) $value > PHP_INT_MAX) + { + // This number cannot be represented by a PHP integer, so we convert it to a string + $value = (string) $value; + } + else + { + $value = (int) $value; + } + break; + case 'float': + $value = (float) $value; + break; + case 'boolean': + $value = (bool) $value; + break; + case 'string': + $value = (string) $value; + break; + } + + return $value; + } + + /** + * Loads a database result, either as a new object for this model, or as + * an iterator for multiple rows. + * + * @chainable + * @param boolean return an iterator or load a single row + * @return ORM for single rows + * @return ORM_Iterator for multiple rows + */ + protected function load_result($array = FALSE) + { + if ($array === FALSE) + { + // Only fetch 1 record + $this->db->limit(1); + } + + if ( ! isset($this->db_applied['select'])) + { + // Select all columns by default + $this->db->select($this->table_name.'.*'); + } + + if ( ! empty($this->load_with)) + { + foreach ($this->load_with as $alias => $object) + { + // Join each object into the results + if (is_string($alias)) + { + // Use alias + $this->with($alias); + } + else + { + // Use object + $this->with($object); + } + } + } + + if ( ! isset($this->db_applied['orderby']) AND ! empty($this->sorting)) + { + $sorting = array(); + foreach ($this->sorting as $column => $direction) + { + if (strpos($column, '.') === FALSE) + { + // Keeps sorting working properly when using JOINs on + // tables with columns of the same name + $column = $this->table_name.'.'.$column; + } + + $sorting[$column] = $direction; + } + + // Apply the user-defined sorting + $this->db->orderby($sorting); + } + + // Load the result + $result = $this->db->get($this->table_name); + + if ($array === TRUE) + { + // Return an iterated result + return new ORM_Iterator($this, $result); + } + + if ($result->count() === 1) + { + // Load object values + $this->load_values($result->result(FALSE)->current()); + } + else + { + // Clear the object, nothing was found + $this->clear(); + } + + return $this; + } + + /** + * Return an array of all the primary keys of the related table. + * + * @param string table name + * @param object ORM model to find relations of + * @return array + */ + protected function load_relations($table, ORM $model) + { + // Save the current query chain (otherwise the next call will clash) + $this->db->push(); + + $query = $this->db + ->select($model->foreign_key(NULL).' AS id') + ->from($table) + ->where($this->foreign_key(NULL, $table), $this->object[$this->primary_key]) + ->get() + ->result(TRUE); + + $this->db->pop(); + + $relations = array(); + foreach ($query as $row) + { + $relations[] = $row->id; + } + + return $relations; + } + + /** + * Returns whether or not primary key is empty + * + * @return bool + */ + protected function empty_primary_key() + { + return (empty($this->object[$this->primary_key]) AND $this->object[$this->primary_key] !== '0'); + } + +} // End ORM diff --git a/system/libraries/ORM_Iterator.php b/system/libraries/ORM_Iterator.php new file mode 100644 index 00000000..41aa8065 --- /dev/null +++ b/system/libraries/ORM_Iterator.php @@ -0,0 +1,228 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** +* Object Relational Mapping (ORM) result iterator. +* +* $Id: ORM_Iterator.php 3769 2008-12-15 00:48:56Z zombor $ +* +* @package ORM +* @author Kohana Team +* @copyright (c) 2007-2008 Kohana Team +* @license http://kohanaphp.com/license.html +*/ +class ORM_Iterator_Core implements Iterator, ArrayAccess, Countable { + + // Class attributes + protected $class_name; + protected $primary_key; + protected $primary_val; + + // Database result object + protected $result; + + public function __construct(ORM $model, Database_Result $result) + { + // Class attributes + $this->class_name = get_class($model); + $this->primary_key = $model->primary_key; + $this->primary_val = $model->primary_val; + + // Database result + $this->result = $result->result(TRUE); + } + + /** + * Returns an array of the results as ORM objects. + * + * @return array + */ + public function as_array() + { + $array = array(); + + if ($results = $this->result->result_array()) + { + // Import class name + $class = $this->class_name; + + foreach ($results as $obj) + { + $array[] = new $class($obj); + } + } + + return $array; + } + + /** + * Return an array of all of the primary keys for this object. + * + * @return array + */ + public function primary_key_array() + { + $ids = array(); + foreach ($this->result as $row) + { + $ids[] = $row->{$this->primary_key}; + } + return $ids; + } + + /** + * Create a key/value array from the results. + * + * @param string key column + * @param string value column + * @return array + */ + public function select_list($key = NULL, $val = NULL) + { + if ($key === NULL) + { + // Use the default key + $key = $this->primary_key; + } + + if ($val === NULL) + { + // Use the default value + $val = $this->primary_val; + } + + $array = array(); + foreach ($this->result->result_array() as $row) + { + $array[$row->$key] = $row->$val; + } + return $array; + } + + /** + * Return a range of offsets. + * + * @param integer start + * @param integer end + * @return array + */ + public function range($start, $end) + { + // Array of objects + $array = array(); + + if ($this->result->offsetExists($start)) + { + // Import the class name + $class = $this->class_name; + + // Set the end offset + $end = $this->result->offsetExists($end) ? $end : $this->count(); + + for ($i = $start; $i < $end; $i++) + { + // Insert each object in the range + $array[] = new $class($this->result->offsetGet($i)); + } + } + + return $array; + } + + /** + * Countable: count + */ + public function count() + { + return $this->result->count(); + } + + /** + * Iterator: current + */ + public function current() + { + if ($row = $this->result->current()) + { + // Import class name + $class = $this->class_name; + + $row = new $class($row); + } + + return $row; + } + + /** + * Iterator: key + */ + public function key() + { + return $this->result->key(); + } + + /** + * Iterator: next + */ + public function next() + { + return $this->result->next(); + } + + /** + * Iterator: rewind + */ + public function rewind() + { + $this->result->rewind(); + } + + /** + * Iterator: valid + */ + public function valid() + { + return $this->result->valid(); + } + + /** + * ArrayAccess: offsetExists + */ + public function offsetExists($offset) + { + return $this->result->offsetExists($offset); + } + + /** + * ArrayAccess: offsetGet + */ + public function offsetGet($offset) + { + if ($this->result->offsetExists($offset)) + { + // Import class name + $class = $this->class_name; + + return new $class($this->result->offsetGet($offset)); + } + } + + /** + * ArrayAccess: offsetSet + * + * @throws Kohana_Database_Exception + */ + public function offsetSet($offset, $value) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + + /** + * ArrayAccess: offsetUnset + * + * @throws Kohana_Database_Exception + */ + public function offsetUnset($offset) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + +} // End ORM Iterator
\ No newline at end of file diff --git a/system/libraries/ORM_Tree.php b/system/libraries/ORM_Tree.php new file mode 100644 index 00000000..cdb09fd0 --- /dev/null +++ b/system/libraries/ORM_Tree.php @@ -0,0 +1,76 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Object Relational Mapping (ORM) "tree" extension. Allows ORM objects to act + * as trees, with parents and children. + * + * $Id: ORM_Tree.php 3923 2009-01-22 15:37:04Z samsoir $ + * + * @package ORM + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class ORM_Tree_Core extends ORM { + + // Name of the child + protected $ORM_Tree_children; + + // Parent keyword name + protected $ORM_Tree_parent_key = 'parent_id'; + + /** + * Overload ORM::__get to support "parent" and "children" properties. + * + * @param string column name + * @return mixed + */ + public function __get($column) + { + if ($column === 'parent') + { + if (empty($this->related[$column])) + { + // Load child model + $model = ORM::factory(inflector::singular($this->ORM_Tree_children)); + + if (array_key_exists($this->ORM_Tree_parent_key, $this->object)) + { + // Find children of this parent + $model->where($model->primary_key, $this->object[$this->ORM_Tree_parent_key])->find(); + } + + $this->related[$column] = $model; + } + + return $this->related[$column]; + } + elseif ($column === 'children') + { + if (empty($this->related[$column])) + { + $model = ORM::factory(inflector::singular($this->ORM_Tree_children)); + + if ($this->ORM_Tree_children === $this->table_name) + { + // Load children within this table + $this->related[$column] = $model + ->where($this->ORM_Tree_parent_key, $this->object[$this->primary_key]) + ->find_all(); + } + else + { + // Find first selection of children + $this->related[$column] = $model + ->where($this->foreign_key(), $this->object[$this->primary_key]) + ->where($this->ORM_Tree_parent_key, NULL) + ->find_all(); + } + } + + return $this->related[$column]; + } + + return parent::__get($column); + } + +} // End ORM Tree
\ No newline at end of file diff --git a/system/libraries/ORM_Versioned.php b/system/libraries/ORM_Versioned.php new file mode 100644 index 00000000..078fe16a --- /dev/null +++ b/system/libraries/ORM_Versioned.php @@ -0,0 +1,143 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Object Relational Mapping (ORM) "versioned" extension. Allows ORM objects to + * be revisioned instead of updated. + * + * $Id$ + * + * @package ORM + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class ORM_Versioned_Core extends ORM { + + protected $last_version = NULL; + + /** + * Overload ORM::save() to support versioned data + * + * @chainable + * @return ORM + */ + public function save() + { + $this->last_version = 1 + ($this->last_version === NULL ? $this->object['version'] : $this->last_version); + $this->__set('version', $this->last_version); + + parent::save(); + + if ($this->saved) + { + $data = array(); + foreach ($this->object as $key => $value) + { + if ($key === 'id') + continue; + + $data[$key] = $value; + } + $data[$this->foreign_key()] = $this->id; + + $this->db->insert($this->table_name.'_versions', $data); + } + + return $this; + } + + /** + * Loads previous version from current object + * + * @chainable + * @return ORM + */ + public function previous() + { + if ( ! $this->loaded) + return $this; + + $this->last_version = ($this->last_version === NULL) ? $this->object['version'] : $this->last_version; + $version = $this->last_version - 1; + + $query = $this->db + ->where($this->foreign_key(), $this->object[$this->primary_key]) + ->where('version', $version) + ->limit(1) + ->get($this->table_name.'_versions'); + + if ($query->count()) + { + $this->load_values($query->result(FALSE)->current()); + } + + return $this; + } + + /** + * Restores the object with data from stored version + * + * @param integer version number you want to restore + * @return ORM + */ + public function restore($version) + { + if ( ! $this->loaded) + return $this; + + $query = $this->db + ->where($this->foreign_key(), $this->object[$this->primary_key]) + ->where('version', $version) + ->limit(1) + ->get($this->table_name.'_versions'); + + if ($query->count()) + { + $row = $query->result(FALSE)->current(); + + foreach ($row as $key => $value) + { + if ($key === $this->primary_key OR $key === $this->foreign_key()) + { + // Do not overwrite the primary key + continue; + } + + if ($key === 'version') + { + // Always use the current version + $value = $this->version; + } + + $this->__set($key, $value); + } + + $this->save(); + } + + return $this; + } + + /** + * Overloads ORM::delete() to delete all versioned entries of current object + * and the object itself + * + * @param integer id of the object you want to delete + * @return ORM + */ + public function delete($id = NULL) + { + if ($id === NULL) + { + // Use the current object id + $id = $this->object[$this->primary_key]; + } + + if ($status = parent::delete($id)) + { + $this->db->where($this->foreign_key(), $id)->delete($this->table_name.'_versions'); + } + + return $status; + } + +}
\ No newline at end of file diff --git a/system/libraries/Pagination.php b/system/libraries/Pagination.php new file mode 100644 index 00000000..a8f7bb19 --- /dev/null +++ b/system/libraries/Pagination.php @@ -0,0 +1,236 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Pagination library. + * + * $Id: Pagination.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Pagination_Core { + + // Config values + protected $base_url = ''; + protected $directory = 'pagination'; + protected $style = 'classic'; + protected $uri_segment = 3; + protected $query_string = ''; + protected $items_per_page = 20; + protected $total_items = 0; + protected $auto_hide = FALSE; + + // Autogenerated values + protected $url; + protected $current_page; + protected $total_pages; + protected $current_first_item; + protected $current_last_item; + protected $first_page; + protected $last_page; + protected $previous_page; + protected $next_page; + protected $sql_offset; + protected $sql_limit; + + /** + * Constructs and returns a new Pagination object. + * + * @param array configuration settings + * @return object + */ + public function factory($config = array()) + { + return new Pagination($config); + } + + /** + * Constructs a new Pagination object. + * + * @param array configuration settings + * @return void + */ + public function __construct($config = array()) + { + // No custom group name given + if ( ! isset($config['group'])) + { + $config['group'] = 'default'; + } + + // Pagination setup + $this->initialize($config); + + Kohana::log('debug', 'Pagination Library initialized'); + } + + /** + * Sets config values. + * + * @throws Kohana_Exception + * @param array configuration settings + * @return void + */ + public function initialize($config = array()) + { + // Load config group + if (isset($config['group'])) + { + // Load and validate config group + if ( ! is_array($group_config = Kohana::config('pagination.'.$config['group']))) + throw new Kohana_Exception('pagination.undefined_group', $config['group']); + + // All pagination config groups inherit default config group + if ($config['group'] !== 'default') + { + // Load and validate default config group + if ( ! is_array($default_config = Kohana::config('pagination.default'))) + throw new Kohana_Exception('pagination.undefined_group', 'default'); + + // Merge config group with default config group + $group_config += $default_config; + } + + // Merge custom config items with config group + $config += $group_config; + } + + // Assign config values to the object + foreach ($config as $key => $value) + { + if (property_exists($this, $key)) + { + $this->$key = $value; + } + } + + // Clean view directory + $this->directory = trim($this->directory, '/').'/'; + + // Build generic URL with page in query string + if ($this->query_string !== '') + { + // Extract current page + $this->current_page = isset($_GET[$this->query_string]) ? (int) $_GET[$this->query_string] : 1; + + // Insert {page} placeholder + $_GET[$this->query_string] = '{page}'; + + // Create full URL + $base_url = ($this->base_url === '') ? Router::$current_uri : $this->base_url; + $this->url = url::site($base_url).'?'.str_replace('%7Bpage%7D', '{page}', http_build_query($_GET)); + + // Reset page number + $_GET[$this->query_string] = $this->current_page; + } + + // Build generic URL with page as URI segment + else + { + // Use current URI if no base_url set + $this->url = ($this->base_url === '') ? Router::$segments : explode('/', trim($this->base_url, '/')); + + // Convert uri 'label' to corresponding integer if needed + if (is_string($this->uri_segment)) + { + if (($key = array_search($this->uri_segment, $this->url)) === FALSE) + { + // If uri 'label' is not found, auto add it to base_url + $this->url[] = $this->uri_segment; + $this->uri_segment = count($this->url) + 1; + } + else + { + $this->uri_segment = $key + 2; + } + } + + // Insert {page} placeholder + $this->url[$this->uri_segment - 1] = '{page}'; + + // Create full URL + $this->url = url::site(implode('/', $this->url)).Router::$query_string; + + // Extract current page + $this->current_page = URI::instance()->segment($this->uri_segment); + } + + // Core pagination values + $this->total_items = (int) max(0, $this->total_items); + $this->items_per_page = (int) max(1, $this->items_per_page); + $this->total_pages = (int) ceil($this->total_items / $this->items_per_page); + $this->current_page = (int) min(max(1, $this->current_page), max(1, $this->total_pages)); + $this->current_first_item = (int) min((($this->current_page - 1) * $this->items_per_page) + 1, $this->total_items); + $this->current_last_item = (int) min($this->current_first_item + $this->items_per_page - 1, $this->total_items); + + // If there is no first/last/previous/next page, relative to the + // current page, value is set to FALSE. Valid page number otherwise. + $this->first_page = ($this->current_page === 1) ? FALSE : 1; + $this->last_page = ($this->current_page >= $this->total_pages) ? FALSE : $this->total_pages; + $this->previous_page = ($this->current_page > 1) ? $this->current_page - 1 : FALSE; + $this->next_page = ($this->current_page < $this->total_pages) ? $this->current_page + 1 : FALSE; + + // SQL values + $this->sql_offset = (int) ($this->current_page - 1) * $this->items_per_page; + $this->sql_limit = sprintf(' LIMIT %d OFFSET %d ', $this->items_per_page, $this->sql_offset); + } + + /** + * Generates the HTML for the chosen pagination style. + * + * @param string pagination style + * @return string pagination html + */ + public function render($style = NULL) + { + // Hide single page pagination + if ($this->auto_hide === TRUE AND $this->total_pages <= 1) + return ''; + + if ($style === NULL) + { + // Use default style + $style = $this->style; + } + + // Return rendered pagination view + return View::factory($this->directory.$style, get_object_vars($this))->render(); + } + + /** + * Magically converts Pagination object to string. + * + * @return string pagination html + */ + public function __toString() + { + return $this->render(); + } + + /** + * Magically gets a pagination variable. + * + * @param string variable key + * @return mixed variable value if the key is found + * @return void if the key is not found + */ + public function __get($key) + { + if (isset($this->$key)) + return $this->$key; + } + + /** + * Adds a secondary interface for accessing properties, e.g. $pagination->total_pages(). + * Note that $pagination->total_pages is the recommended way to access properties. + * + * @param string function name + * @return string + */ + public function __call($func, $args = NULL) + { + return $this->__get($func); + } + +} // End Pagination Class
\ No newline at end of file diff --git a/system/libraries/Profiler.php b/system/libraries/Profiler.php new file mode 100644 index 00000000..47d82ace --- /dev/null +++ b/system/libraries/Profiler.php @@ -0,0 +1,271 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Adds useful information to the bottom of the current page for debugging and optimization purposes. + * + * Benchmarks - The times and memory usage of benchmarks run by the Benchmark library. + * Database - The raw SQL and number of affected rows of Database queries. + * Session Data - Data stored in the current session if using the Session library. + * POST Data - The name and values of any POST data submitted to the current page. + * Cookie Data - All cookies sent for the current request. + * + * $Id: Profiler.php 4090 2009-03-19 01:27:45Z bharat $ + * + * @package Profiler + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Profiler_Core { + + protected $profiles = array(); + protected $show; + + public function __construct() + { + // Add all built in profiles to event + Event::add('profiler.run', array($this, 'benchmarks')); + Event::add('profiler.run', array($this, 'database')); + Event::add('profiler.run', array($this, 'session')); + Event::add('profiler.run', array($this, 'post')); + Event::add('profiler.run', array($this, 'cookies')); + + // Add profiler to page output automatically + Event::add('system.display', array($this, 'render')); + + Kohana::log('debug', 'Profiler Library initialized'); + } + + /** + * Magic __call method. Creates a new profiler section object. + * + * @param string input type + * @param string input name + * @return object + */ + public function __call($method, $args) + { + if ( ! $this->show OR (is_array($this->show) AND ! in_array($args[0], $this->show))) + return FALSE; + + // Class name + $class = 'Profiler_'.ucfirst($method); + + $class = new $class(); + + $this->profiles[$args[0]] = $class; + + return $class; + } + + /** + * Disables the profiler for this page only. + * Best used when profiler is autoloaded. + * + * @return void + */ + public function disable() + { + // Removes itself from the event queue + Event::clear('system.display', array($this, 'render')); + } + + /** + * Render the profiler. Output is added to the bottom of the page by default. + * + * @param boolean return the output if TRUE + * @return void|string + */ + public function render($return = FALSE) + { + $start = microtime(TRUE); + + $get = isset($_GET['profiler']) ? explode(',', $_GET['profiler']) : array(); + $this->show = empty($get) ? Kohana::config('profiler.show') : $get; + + Event::run('profiler.run', $this); + + $styles = ''; + foreach ($this->profiles as $profile) + { + $styles .= $profile->styles(); + } + + // Don't display if there's no profiles + if (empty($this->profiles)) + return; + + // Load the profiler view + $data = array + ( + 'profiles' => $this->profiles, + 'styles' => $styles, + 'execution_time' => microtime(TRUE) - $start + ); + $view = new View('kohana_profiler', $data); + + // Return rendered view if $return is TRUE + if ($return == TRUE) + return $view->render(); + + // Add profiler data to the output + if (stripos(Kohana::$output, '</body>') !== FALSE) + { + // Closing body tag was found, insert the profiler data before it + Kohana::$output = str_ireplace('</body>', $view->render().'</body>', Kohana::$output); + } + else + { + // Append the profiler data to the output + Kohana::$output .= $view->render(); + } + } + + /** + * Benchmark times and memory usage from the Benchmark library. + * + * @return void + */ + public function benchmarks() + { + if ( ! $table = $this->table('benchmarks')) + return; + + $table->add_column(); + $table->add_column('kp-column kp-data'); + $table->add_column('kp-column kp-data'); + $table->add_column('kp-column kp-data'); + $table->add_row(array('Benchmarks', 'Time', 'Count', 'Memory'), 'kp-title', 'background-color: #FFE0E0'); + + $benchmarks = Benchmark::get(TRUE); + + // Moves the first benchmark (total execution time) to the end of the array + $benchmarks = array_slice($benchmarks, 1) + array_slice($benchmarks, 0, 1); + + text::alternate(); + foreach ($benchmarks as $name => $benchmark) + { + // Clean unique id from system benchmark names + $name = ucwords(str_replace(array('_', '-'), ' ', str_replace(SYSTEM_BENCHMARK.'_', '', $name))); + + $data = array($name, number_format($benchmark['time'], 3), $benchmark['count'], number_format($benchmark['memory'] / 1024 / 1024, 2).'MB'); + $class = text::alternate('', 'kp-altrow'); + + if ($name == 'Total Execution') + $class = 'kp-totalrow'; + + $table->add_row($data, $class); + } + } + + /** + * Database query benchmarks. + * + * @return void + */ + public function database() + { + if ( ! $table = $this->table('database')) + return; + + $table->add_column(); + $table->add_column('kp-column kp-data'); + $table->add_column('kp-column kp-data'); + $table->add_row(array('Queries', 'Time', 'Rows'), 'kp-title', 'background-color: #E0FFE0'); + + $queries = Database::$benchmarks; + + text::alternate(); + $total_time = $total_rows = 0; + foreach ($queries as $query) + { + $data = array($query['query'], number_format($query['time'], 3), $query['rows']); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + $total_time += $query['time']; + $total_rows += $query['rows']; + } + + $data = array('Total: ' . count($queries), number_format($total_time, 3), $total_rows); + $table->add_row($data, 'kp-totalrow'); + } + + /** + * Session data. + * + * @return void + */ + public function session() + { + if (empty($_SESSION)) return; + + if ( ! $table = $this->table('session')) + return; + + $table->add_column('kp-name'); + $table->add_column(); + $table->add_row(array('Session', 'Value'), 'kp-title', 'background-color: #CCE8FB'); + + text::alternate(); + foreach($_SESSION as $name => $value) + { + if (is_object($value)) + { + $value = get_class($value).' [object]'; + } + + $data = array($name, $value); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + } + } + + /** + * POST data. + * + * @return void + */ + public function post() + { + if (empty($_POST)) return; + + if ( ! $table = $this->table('post')) + return; + + $table->add_column('kp-name'); + $table->add_column(); + $table->add_row(array('POST', 'Value'), 'kp-title', 'background-color: #E0E0FF'); + + text::alternate(); + foreach($_POST as $name => $value) + { + $data = array($name, $value); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + } + } + + /** + * Cookie data. + * + * @return void + */ + public function cookies() + { + if (empty($_COOKIE)) return; + + if ( ! $table = $this->table('cookies')) + return; + + $table->add_column('kp-name'); + $table->add_column(); + $table->add_row(array('Cookies', 'Value'), 'kp-title', 'background-color: #FFF4D7'); + + text::alternate(); + foreach($_COOKIE as $name => $value) + { + $data = array($name, $value); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + } + } +}
\ No newline at end of file diff --git a/system/libraries/Profiler_Table.php b/system/libraries/Profiler_Table.php new file mode 100644 index 00000000..a0058a58 --- /dev/null +++ b/system/libraries/Profiler_Table.php @@ -0,0 +1,69 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Provides a table layout for sections in the Profiler library. + * + * $Id$ + * + * @package Profiler + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Profiler_Table_Core { + + protected $columns = array(); + protected $rows = array(); + + /** + * Get styles for table. + * + * @return string + */ + public function styles() + { + static $styles_output; + + if ( ! $styles_output) + { + $styles_output = TRUE; + return file_get_contents(Kohana::find_file('views', 'kohana_profiler_table', FALSE, 'css')); + } + + return ''; + } + + /** + * Add column to table. + * + * @param string CSS class + * @param string CSS style + */ + public function add_column($class = '', $style = '') + { + $this->columns[] = array('class' => $class, 'style' => $style); + } + + /** + * Add row to table. + * + * @param array data to go in table cells + * @param string CSS class + * @param string CSS style + */ + public function add_row($data, $class = '', $style = '') + { + $this->rows[] = array('data' => $data, 'class' => $class, 'style' => $style); + } + + /** + * Render table. + * + * @return string + */ + public function render() + { + $data['rows'] = $this->rows; + $data['columns'] = $this->columns; + return View::factory('kohana_profiler_table', $data)->render(); + } +}
\ No newline at end of file diff --git a/system/libraries/Router.php b/system/libraries/Router.php new file mode 100644 index 00000000..6dc9b10c --- /dev/null +++ b/system/libraries/Router.php @@ -0,0 +1,304 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Router + * + * $Id: Router.php 4350 2009-05-14 18:58:18Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Router_Core { + + protected static $routes; + + public static $current_uri = ''; + public static $query_string = ''; + public static $complete_uri = ''; + public static $routed_uri = ''; + public static $url_suffix = ''; + + public static $segments; + public static $rsegments; + + public static $controller; + public static $controller_path; + + public static $method = 'index'; + public static $arguments = array(); + + /** + * Router setup routine. Automatically called during Kohana setup process. + * + * @return void + */ + public static function setup() + { + if ( ! empty($_SERVER['QUERY_STRING'])) + { + // Set the query string to the current query string + Router::$query_string = '?'.trim($_SERVER['QUERY_STRING'], '&/'); + } + + if (Router::$routes === NULL) + { + // Load routes + Router::$routes = Kohana::config('routes'); + } + + // Default route status + $default_route = FALSE; + + if (Router::$current_uri === '') + { + // Make sure the default route is set + if ( ! isset(Router::$routes['_default'])) + throw new Kohana_Exception('core.no_default_route'); + + // Use the default route when no segments exist + Router::$current_uri = Router::$routes['_default']; + + // Default route is in use + $default_route = TRUE; + } + + // Make sure the URL is not tainted with HTML characters + Router::$current_uri = html::specialchars(Router::$current_uri, FALSE); + + // Remove all dot-paths from the URI, they are not valid + Router::$current_uri = preg_replace('#\.[\s./]*/#', '', Router::$current_uri); + + // At this point segments, rsegments, and current URI are all the same + Router::$segments = Router::$rsegments = Router::$current_uri = trim(Router::$current_uri, '/'); + + // Set the complete URI + Router::$complete_uri = Router::$current_uri.Router::$query_string; + + // Explode the segments by slashes + Router::$segments = ($default_route === TRUE OR Router::$segments === '') ? array() : explode('/', Router::$segments); + + if ($default_route === FALSE AND count(Router::$routes) > 1) + { + // Custom routing + Router::$rsegments = Router::routed_uri(Router::$current_uri); + } + + // The routed URI is now complete + Router::$routed_uri = Router::$rsegments; + + // Routed segments will never be empty + Router::$rsegments = explode('/', Router::$rsegments); + + // Prepare to find the controller + $controller_path = ''; + $method_segment = NULL; + + // Paths to search + $paths = Kohana::include_paths(); + + foreach (Router::$rsegments as $key => $segment) + { + // Add the segment to the search path + $controller_path .= $segment; + + $found = FALSE; + foreach ($paths as $dir) + { + // Search within controllers only + $dir .= 'controllers/'; + + if (is_dir($dir.$controller_path) OR is_file($dir.$controller_path.EXT)) + { + // Valid path + $found = TRUE; + + // The controller must be a file that exists with the search path + if ($c = str_replace('\\', '/', realpath($dir.$controller_path.EXT)) + AND is_file($c) AND strpos($c, $dir) === 0) + { + // Set controller name + Router::$controller = $segment; + + // Change controller path + Router::$controller_path = $c; + + // Set the method segment + $method_segment = $key + 1; + + // Stop searching + break; + } + } + } + + if ($found === FALSE) + { + // Maximum depth has been reached, stop searching + break; + } + + // Add another slash + $controller_path .= '/'; + } + + if ($method_segment !== NULL AND isset(Router::$rsegments[$method_segment])) + { + // Set method + Router::$method = Router::$rsegments[$method_segment]; + + if (isset(Router::$rsegments[$method_segment + 1])) + { + // Set arguments + Router::$arguments = array_slice(Router::$rsegments, $method_segment + 1); + } + } + + // Last chance to set routing before a 404 is triggered + Event::run('system.post_routing'); + + if (Router::$controller === NULL) + { + // No controller was found, so no page can be rendered + Event::run('system.404'); + } + } + + /** + * Attempts to determine the current URI using CLI, GET, PATH_INFO, ORIG_PATH_INFO, or PHP_SELF. + * + * @return void + */ + public static function find_uri() + { + if (PHP_SAPI === 'cli') + { + // Command line requires a bit of hacking + if (isset($_SERVER['argv'][1])) + { + Router::$current_uri = $_SERVER['argv'][1]; + + // Remove GET string from segments + if (($query = strpos(Router::$current_uri, '?')) !== FALSE) + { + list (Router::$current_uri, $query) = explode('?', Router::$current_uri, 2); + + // Parse the query string into $_GET + parse_str($query, $_GET); + + // Convert $_GET to UTF-8 + $_GET = utf8::clean($_GET); + } + } + } + elseif (isset($_GET['kohana_uri'])) + { + // Use the URI defined in the query string + Router::$current_uri = $_GET['kohana_uri']; + + // Remove the URI from $_GET + unset($_GET['kohana_uri']); + + // Remove the URI from $_SERVER['QUERY_STRING'] + $_SERVER['QUERY_STRING'] = preg_replace('~\bkohana_uri\b[^&]*+&?~', '', $_SERVER['QUERY_STRING']); + } + elseif (isset($_SERVER['PATH_INFO']) AND $_SERVER['PATH_INFO']) + { + Router::$current_uri = $_SERVER['PATH_INFO']; + } + elseif (isset($_SERVER['ORIG_PATH_INFO']) AND $_SERVER['ORIG_PATH_INFO']) + { + Router::$current_uri = $_SERVER['ORIG_PATH_INFO']; + } + elseif (isset($_SERVER['PHP_SELF']) AND $_SERVER['PHP_SELF']) + { + Router::$current_uri = $_SERVER['PHP_SELF']; + + if (($strpos_fc = strpos(Router::$current_uri, KOHANA)) !== FALSE) + { + // Remove the front controller from the current uri + Router::$current_uri = substr(Router::$current_uri, $strpos_fc + strlen(KOHANA)); + } + } + + // Remove slashes from the start and end of the URI + Router::$current_uri = trim(Router::$current_uri, '/'); + + if (Router::$current_uri !== '') + { + if ($suffix = Kohana::config('core.url_suffix') AND strpos(Router::$current_uri, $suffix) !== FALSE) + { + // Remove the URL suffix + Router::$current_uri = preg_replace('#'.preg_quote($suffix).'$#u', '', Router::$current_uri); + + // Set the URL suffix + Router::$url_suffix = $suffix; + } + + // Reduce multiple slashes into single slashes + Router::$current_uri = preg_replace('#//+#', '/', Router::$current_uri); + } + } + + /** + * Generates routed URI from given URI. + * + * @param string URI to convert + * @return string Routed uri + */ + public static function routed_uri($uri) + { + if (Router::$routes === NULL) + { + // Load routes + Router::$routes = Kohana::config('routes'); + } + + // Prepare variables + $routed_uri = $uri = trim($uri, '/'); + + if (isset(Router::$routes[$uri])) + { + // Literal match, no need for regex + $routed_uri = Router::$routes[$uri]; + } + else + { + // Loop through the routes and see if anything matches + foreach (Router::$routes as $key => $val) + { + if ($key === '_default') continue; + + // Trim slashes + $key = trim($key, '/'); + $val = trim($val, '/'); + + if (preg_match('#^'.$key.'$#u', $uri)) + { + if (strpos($val, '$') !== FALSE) + { + // Use regex routing + $routed_uri = preg_replace('#^'.$key.'$#u', $val, $uri); + } + else + { + // Standard routing + $routed_uri = $val; + } + + // A valid route has been found + break; + } + } + } + + if (isset(Router::$routes[$routed_uri])) + { + // Check for double routing (without regex) + $routed_uri = Router::$routes[$routed_uri]; + } + + return trim($routed_uri, '/'); + } + +} // End Router
\ No newline at end of file diff --git a/system/libraries/Session.php b/system/libraries/Session.php new file mode 100644 index 00000000..e03f5dff --- /dev/null +++ b/system/libraries/Session.php @@ -0,0 +1,458 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session library. + * + * $Id: Session.php 4073 2009-03-13 17:53:58Z Shadowhand $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Core { + + // Session singleton + protected static $instance; + + // Protected key names (cannot be set by the user) + protected static $protect = array('session_id', 'user_agent', 'last_activity', 'ip_address', 'total_hits', '_kf_flash_'); + + // Configuration and driver + protected static $config; + protected static $driver; + + // Flash variables + protected static $flash; + + // Input library + protected $input; + + /** + * Singleton instance of Session. + */ + public static function instance() + { + if (Session::$instance == NULL) + { + // Create a new instance + new Session; + } + + return Session::$instance; + } + + /** + * On first session instance creation, sets up the driver and creates session. + */ + public function __construct() + { + $this->input = Input::instance(); + + // This part only needs to be run once + if (Session::$instance === NULL) + { + // Load config + Session::$config = Kohana::config('session'); + + // Makes a mirrored array, eg: foo=foo + Session::$protect = array_combine(Session::$protect, Session::$protect); + + // Configure garbage collection + ini_set('session.gc_probability', (int) Session::$config['gc_probability']); + ini_set('session.gc_divisor', 100); + ini_set('session.gc_maxlifetime', (Session::$config['expiration'] == 0) ? 86400 : Session::$config['expiration']); + + // Create a new session + $this->create(); + + if (Session::$config['regenerate'] > 0 AND ($_SESSION['total_hits'] % Session::$config['regenerate']) === 0) + { + // Regenerate session id and update session cookie + $this->regenerate(); + } + else + { + // Always update session cookie to keep the session alive + cookie::set(Session::$config['name'], $_SESSION['session_id'], Session::$config['expiration']); + } + + // Close the session just before sending the headers, so that + // the session cookie(s) can be written. + Event::add('system.send_headers', array($this, 'write_close')); + + // Make sure that sessions are closed before exiting + register_shutdown_function(array($this, 'write_close')); + + // Singleton instance + Session::$instance = $this; + } + + Kohana::log('debug', 'Session Library initialized'); + } + + /** + * Get the session id. + * + * @return string + */ + public function id() + { + return $_SESSION['session_id']; + } + + /** + * Create a new session. + * + * @param array variables to set after creation + * @return void + */ + public function create($vars = NULL) + { + // Destroy any current sessions + $this->destroy(); + + if (Session::$config['driver'] !== 'native') + { + // Set driver name + $driver = 'Session_'.ucfirst(Session::$config['driver']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('core.driver_not_found', Session::$config['driver'], get_class($this)); + + // Initialize the driver + Session::$driver = new $driver(); + + // Validate the driver + if ( ! (Session::$driver instanceof Session_Driver)) + throw new Kohana_Exception('core.driver_implements', Session::$config['driver'], get_class($this), 'Session_Driver'); + + // Register non-native driver as the session handler + session_set_save_handler + ( + array(Session::$driver, 'open'), + array(Session::$driver, 'close'), + array(Session::$driver, 'read'), + array(Session::$driver, 'write'), + array(Session::$driver, 'destroy'), + array(Session::$driver, 'gc') + ); + } + + // Validate the session name + if ( ! preg_match('~^(?=.*[a-z])[a-z0-9_]++$~iD', Session::$config['name'])) + throw new Kohana_Exception('session.invalid_session_name', Session::$config['name']); + + // Name the session, this will also be the name of the cookie + session_name(Session::$config['name']); + + // Set the session cookie parameters + session_set_cookie_params + ( + Session::$config['expiration'], + Kohana::config('cookie.path'), + Kohana::config('cookie.domain'), + Kohana::config('cookie.secure'), + Kohana::config('cookie.httponly') + ); + + // Start the session! + session_start(); + + // Put session_id in the session variable + $_SESSION['session_id'] = session_id(); + + // Set defaults + if ( ! isset($_SESSION['_kf_flash_'])) + { + $_SESSION['total_hits'] = 0; + $_SESSION['_kf_flash_'] = array(); + + $_SESSION['user_agent'] = Kohana::$user_agent; + $_SESSION['ip_address'] = $this->input->ip_address(); + } + + // Set up flash variables + Session::$flash =& $_SESSION['_kf_flash_']; + + // Increase total hits + $_SESSION['total_hits'] += 1; + + // Validate data only on hits after one + if ($_SESSION['total_hits'] > 1) + { + // Validate the session + foreach (Session::$config['validate'] as $valid) + { + switch ($valid) + { + // Check user agent for consistency + case 'user_agent': + if ($_SESSION[$valid] !== Kohana::$user_agent) + return $this->create(); + break; + + // Check ip address for consistency + case 'ip_address': + if ($_SESSION[$valid] !== $this->input->$valid()) + return $this->create(); + break; + + // Check expiration time to prevent users from manually modifying it + case 'expiration': + if (time() - $_SESSION['last_activity'] > ini_get('session.gc_maxlifetime')) + return $this->create(); + break; + } + } + } + + // Expire flash keys + $this->expire_flash(); + + // Update last activity + $_SESSION['last_activity'] = time(); + + // Set the new data + Session::set($vars); + } + + /** + * Regenerates the global session id. + * + * @return void + */ + public function regenerate() + { + if (Session::$config['driver'] === 'native') + { + // Generate a new session id + // Note: also sets a new session cookie with the updated id + session_regenerate_id(TRUE); + + // Update session with new id + $_SESSION['session_id'] = session_id(); + } + else + { + // Pass the regenerating off to the driver in case it wants to do anything special + $_SESSION['session_id'] = Session::$driver->regenerate(); + } + + // Get the session name + $name = session_name(); + + if (isset($_COOKIE[$name])) + { + // Change the cookie value to match the new session id to prevent "lag" + $_COOKIE[$name] = $_SESSION['session_id']; + } + } + + /** + * Destroys the current session. + * + * @return void + */ + public function destroy() + { + if (session_id() !== '') + { + // Get the session name + $name = session_name(); + + // Destroy the session + session_destroy(); + + // Re-initialize the array + $_SESSION = array(); + + // Delete the session cookie + cookie::delete($name); + } + } + + /** + * Runs the system.session_write event, then calls session_write_close. + * + * @return void + */ + public function write_close() + { + static $run; + + if ($run === NULL) + { + $run = TRUE; + + // Run the events that depend on the session being open + Event::run('system.session_write'); + + // Expire flash keys + $this->expire_flash(); + + // Close the session + session_write_close(); + } + } + + /** + * Set a session variable. + * + * @param string|array key, or array of values + * @param mixed value (if keys is not an array) + * @return void + */ + public function set($keys, $val = FALSE) + { + if (empty($keys)) + return FALSE; + + if ( ! is_array($keys)) + { + $keys = array($keys => $val); + } + + foreach ($keys as $key => $val) + { + if (isset(Session::$protect[$key])) + continue; + + // Set the key + $_SESSION[$key] = $val; + } + } + + /** + * Set a flash variable. + * + * @param string|array key, or array of values + * @param mixed value (if keys is not an array) + * @return void + */ + public function set_flash($keys, $val = FALSE) + { + if (empty($keys)) + return FALSE; + + if ( ! is_array($keys)) + { + $keys = array($keys => $val); + } + + foreach ($keys as $key => $val) + { + if ($key == FALSE) + continue; + + Session::$flash[$key] = 'new'; + Session::set($key, $val); + } + } + + /** + * Freshen one, multiple or all flash variables. + * + * @param string variable key(s) + * @return void + */ + public function keep_flash($keys = NULL) + { + $keys = ($keys === NULL) ? array_keys(Session::$flash) : func_get_args(); + + foreach ($keys as $key) + { + if (isset(Session::$flash[$key])) + { + Session::$flash[$key] = 'new'; + } + } + } + + /** + * Expires old flash data and removes it from the session. + * + * @return void + */ + public function expire_flash() + { + static $run; + + // Method can only be run once + if ($run === TRUE) + return; + + if ( ! empty(Session::$flash)) + { + foreach (Session::$flash as $key => $state) + { + if ($state === 'old') + { + // Flash has expired + unset(Session::$flash[$key], $_SESSION[$key]); + } + else + { + // Flash will expire + Session::$flash[$key] = 'old'; + } + } + } + + // Method has been run + $run = TRUE; + } + + /** + * Get a variable. Access to sub-arrays is supported with key.subkey. + * + * @param string variable key + * @param mixed default value returned if variable does not exist + * @return mixed Variable data if key specified, otherwise array containing all session data. + */ + public function get($key = FALSE, $default = FALSE) + { + if (empty($key)) + return $_SESSION; + + $result = isset($_SESSION[$key]) ? $_SESSION[$key] : Kohana::key_string($_SESSION, $key); + + return ($result === NULL) ? $default : $result; + } + + /** + * Get a variable, and delete it. + * + * @param string variable key + * @param mixed default value returned if variable does not exist + * @return mixed + */ + public function get_once($key, $default = FALSE) + { + $return = Session::get($key, $default); + Session::delete($key); + + return $return; + } + + /** + * Delete one or more variables. + * + * @param string variable key(s) + * @return void + */ + public function delete($keys) + { + $args = func_get_args(); + + foreach ($args as $key) + { + if (isset(Session::$protect[$key])) + continue; + + // Unset the key + unset($_SESSION[$key]); + } + } + +} // End Session Class diff --git a/system/libraries/URI.php b/system/libraries/URI.php new file mode 100644 index 00000000..d9ccdcf7 --- /dev/null +++ b/system/libraries/URI.php @@ -0,0 +1,279 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * URI library. + * + * $Id: URI.php 4072 2009-03-13 17:20:38Z jheathco $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class URI_Core extends Router { + + /** + * Returns a singleton instance of URI. + * + * @return object + */ + public static function instance() + { + static $instance; + + if ($instance == NULL) + { + // Initialize the URI instance + $instance = new URI; + } + + return $instance; + } + + /** + * Retrieve a specific URI segment. + * + * @param integer|string segment number or label + * @param mixed default value returned if segment does not exist + * @return string + */ + public function segment($index = 1, $default = FALSE) + { + if (is_string($index)) + { + if (($key = array_search($index, URI::$segments)) === FALSE) + return $default; + + $index = $key + 2; + } + + $index = (int) $index - 1; + + return isset(URI::$segments[$index]) ? URI::$segments[$index] : $default; + } + + /** + * Retrieve a specific routed URI segment. + * + * @param integer|string rsegment number or label + * @param mixed default value returned if segment does not exist + * @return string + */ + public function rsegment($index = 1, $default = FALSE) + { + if (is_string($index)) + { + if (($key = array_search($index, URI::$rsegments)) === FALSE) + return $default; + + $index = $key + 2; + } + + $index = (int) $index - 1; + + return isset(URI::$rsegments[$index]) ? URI::$rsegments[$index] : $default; + } + + /** + * Retrieve a specific URI argument. + * This is the part of the segments that does not indicate controller or method + * + * @param integer|string argument number or label + * @param mixed default value returned if segment does not exist + * @return string + */ + public function argument($index = 1, $default = FALSE) + { + if (is_string($index)) + { + if (($key = array_search($index, URI::$arguments)) === FALSE) + return $default; + + $index = $key + 2; + } + + $index = (int) $index - 1; + + return isset(URI::$arguments[$index]) ? URI::$arguments[$index] : $default; + } + + /** + * Returns an array containing all the URI segments. + * + * @param integer segment offset + * @param boolean return an associative array + * @return array + */ + public function segment_array($offset = 0, $associative = FALSE) + { + return $this->build_array(URI::$segments, $offset, $associative); + } + + /** + * Returns an array containing all the re-routed URI segments. + * + * @param integer rsegment offset + * @param boolean return an associative array + * @return array + */ + public function rsegment_array($offset = 0, $associative = FALSE) + { + return $this->build_array(URI::$rsegments, $offset, $associative); + } + + /** + * Returns an array containing all the URI arguments. + * + * @param integer segment offset + * @param boolean return an associative array + * @return array + */ + public function argument_array($offset = 0, $associative = FALSE) + { + return $this->build_array(URI::$arguments, $offset, $associative); + } + + /** + * Creates a simple or associative array from an array and an offset. + * Used as a helper for (r)segment_array and argument_array. + * + * @param array array to rebuild + * @param integer offset to start from + * @param boolean create an associative array + * @return array + */ + public function build_array($array, $offset = 0, $associative = FALSE) + { + // Prevent the keys from being improperly indexed + array_unshift($array, 0); + + // Slice the array, preserving the keys + $array = array_slice($array, $offset + 1, count($array) - 1, TRUE); + + if ($associative === FALSE) + return $array; + + $associative = array(); + $pairs = array_chunk($array, 2); + + foreach ($pairs as $pair) + { + // Add the key/value pair to the associative array + $associative[$pair[0]] = isset($pair[1]) ? $pair[1] : ''; + } + + return $associative; + } + + /** + * Returns the complete URI as a string. + * + * @return string + */ + public function string() + { + return URI::$current_uri; + } + + /** + * Magic method for converting an object to a string. + * + * @return string + */ + public function __toString() + { + return URI::$current_uri; + } + + /** + * Returns the total number of URI segments. + * + * @return integer + */ + public function total_segments() + { + return count(URI::$segments); + } + + /** + * Returns the total number of re-routed URI segments. + * + * @return integer + */ + public function total_rsegments() + { + return count(URI::$rsegments); + } + + /** + * Returns the total number of URI arguments. + * + * @return integer + */ + public function total_arguments() + { + return count(URI::$arguments); + } + + /** + * Returns the last URI segment. + * + * @param mixed default value returned if segment does not exist + * @return string + */ + public function last_segment($default = FALSE) + { + if (($end = $this->total_segments()) < 1) + return $default; + + return URI::$segments[$end - 1]; + } + + /** + * Returns the last re-routed URI segment. + * + * @param mixed default value returned if segment does not exist + * @return string + */ + public function last_rsegment($default = FALSE) + { + if (($end = $this->total_segments()) < 1) + return $default; + + return URI::$rsegments[$end - 1]; + } + + /** + * Returns the path to the current controller (not including the actual + * controller), as a web path. + * + * @param boolean return a full url, or only the path specifically + * @return string + */ + public function controller_path($full = TRUE) + { + return ($full) ? url::site(URI::$controller_path) : URI::$controller_path; + } + + /** + * Returns the current controller, as a web path. + * + * @param boolean return a full url, or only the controller specifically + * @return string + */ + public function controller($full = TRUE) + { + return ($full) ? url::site(URI::$controller_path.URI::$controller) : URI::$controller; + } + + /** + * Returns the current method, as a web path. + * + * @param boolean return a full url, or only the method specifically + * @return string + */ + public function method($full = TRUE) + { + return ($full) ? url::site(URI::$controller_path.URI::$controller.'/'.URI::$method) : URI::$method; + } + +} // End URI Class diff --git a/system/libraries/Validation.php b/system/libraries/Validation.php new file mode 100644 index 00000000..5a48bfc5 --- /dev/null +++ b/system/libraries/Validation.php @@ -0,0 +1,826 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Validation library. + * + * $Id: Validation.php 4120 2009-03-25 19:22:31Z jheathco $ + * + * @package Validation + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Validation_Core extends ArrayObject { + + // Filters + protected $pre_filters = array(); + protected $post_filters = array(); + + // Rules and callbacks + protected $rules = array(); + protected $callbacks = array(); + + // Rules that are allowed to run on empty fields + protected $empty_rules = array('required', 'matches'); + + // Errors + protected $errors = array(); + protected $messages = array(); + + // Fields that are expected to be arrays + protected $array_fields = array(); + + // Checks if there is data to validate. + protected $submitted; + + /** + * Creates a new Validation instance. + * + * @param array array to use for validation + * @return object + */ + public static function factory(array $array) + { + return new Validation($array); + } + + /** + * Sets the unique "any field" key and creates an ArrayObject from the + * passed array. + * + * @param array array to validate + * @return void + */ + public function __construct(array $array) + { + // The array is submitted if the array is not empty + $this->submitted = ! empty($array); + + parent::__construct($array, ArrayObject::ARRAY_AS_PROPS | ArrayObject::STD_PROP_LIST); + } + + /** + * Magic clone method, clears errors and messages. + * + * @return void + */ + public function __clone() + { + $this->errors = array(); + $this->messages = array(); + } + + /** + * Create a copy of the current validation rules and change the array. + * + * @chainable + * @param array new array to validate + * @return Validation + */ + public function copy(array $array) + { + $copy = clone $this; + + $copy->exchangeArray($array); + + return $copy; + } + + /** + * Test if the data has been submitted. + * + * @return boolean + */ + public function submitted($value = NULL) + { + if (is_bool($value)) + { + $this->submitted = $value; + } + + return $this->submitted; + } + + /** + * Returns an array of all the field names that have filters, rules, or callbacks. + * + * @return array + */ + public function field_names() + { + // All the fields that are being validated + $fields = array_keys(array_merge + ( + $this->pre_filters, + $this->rules, + $this->callbacks, + $this->post_filters + )); + + // Remove wildcard fields + $fields = array_diff($fields, array('*')); + + return $fields; + } + + /** + * Returns the array values of the current object. + * + * @return array + */ + public function as_array() + { + return $this->getArrayCopy(); + } + + /** + * Returns the ArrayObject values, removing all inputs without rules. + * To choose specific inputs, list the field name as arguments. + * + * @param boolean return only fields with filters, rules, and callbacks + * @return array + */ + public function safe_array() + { + // Load choices + $choices = func_get_args(); + $choices = empty($choices) ? NULL : array_combine($choices, $choices); + + // Get field names + $fields = $this->field_names(); + + $safe = array(); + foreach ($fields as $field) + { + if ($choices === NULL OR isset($choices[$field])) + { + if (isset($this[$field])) + { + $value = $this[$field]; + + if (is_object($value)) + { + // Convert the value back into an array + $value = $value->getArrayCopy(); + } + } + else + { + // Even if the field is not in this array, it must be set + $value = NULL; + } + + // Add the field to the array + $safe[$field] = $value; + } + } + + return $safe; + } + + /** + * Add additional rules that will forced, even for empty fields. All arguments + * passed will be appended to the list. + * + * @chainable + * @param string rule name + * @return object + */ + public function allow_empty_rules($rules) + { + // Any number of args are supported + $rules = func_get_args(); + + // Merge the allowed rules + $this->empty_rules = array_merge($this->empty_rules, $rules); + + return $this; + } + + /** + * Converts a filter, rule, or callback into a fully-qualified callback array. + * + * @return mixed + */ + protected function callback($callback) + { + if (is_string($callback)) + { + if (strpos($callback, '::') !== FALSE) + { + $callback = explode('::', $callback); + } + elseif (function_exists($callback)) + { + // No need to check if the callback is a method + $callback = $callback; + } + elseif (method_exists($this, $callback)) + { + // The callback exists in Validation + $callback = array($this, $callback); + } + elseif (method_exists('valid', $callback)) + { + // The callback exists in valid:: + $callback = array('valid', $callback); + } + } + + if ( ! is_callable($callback, FALSE)) + { + if (is_array($callback)) + { + if (is_object($callback[0])) + { + // Object instance syntax + $name = get_class($callback[0]).'->'.$callback[1]; + } + else + { + // Static class syntax + $name = $callback[0].'::'.$callback[1]; + } + } + else + { + // Function syntax + $name = $callback; + } + + throw new Kohana_Exception('validation.not_callable', $name); + } + + return $callback; + } + + /** + * Add a pre-filter to one or more inputs. Pre-filters are applied before + * rules or callbacks are executed. + * + * @chainable + * @param callback filter + * @param string fields to apply filter to, use TRUE for all fields + * @return object + */ + public function pre_filter($filter, $field = TRUE) + { + if ($field === TRUE OR $field === '*') + { + // Use wildcard + $fields = array('*'); + } + else + { + // Add the filter to specific inputs + $fields = func_get_args(); + $fields = array_slice($fields, 1); + } + + // Convert to a proper callback + $filter = $this->callback($filter); + + foreach ($fields as $field) + { + // Add the filter to specified field + $this->pre_filters[$field][] = $filter; + } + + return $this; + } + + /** + * Add a post-filter to one or more inputs. Post-filters are applied after + * rules and callbacks have been executed. + * + * @chainable + * @param callback filter + * @param string fields to apply filter to, use TRUE for all fields + * @return object + */ + public function post_filter($filter, $field = TRUE) + { + if ($field === TRUE) + { + // Use wildcard + $fields = array('*'); + } + else + { + // Add the filter to specific inputs + $fields = func_get_args(); + $fields = array_slice($fields, 1); + } + + // Convert to a proper callback + $filter = $this->callback($filter); + + foreach ($fields as $field) + { + // Add the filter to specified field + $this->post_filters[$field][] = $filter; + } + + return $this; + } + + /** + * Add rules to a field. Validation rules may only return TRUE or FALSE and + * can not manipulate the value of a field. + * + * @chainable + * @param string field name + * @param callback rules (one or more arguments) + * @return object + */ + public function add_rules($field, $rules) + { + // Get the rules + $rules = func_get_args(); + $rules = array_slice($rules, 1); + + if ($field === TRUE) + { + // Use wildcard + $field = '*'; + } + + foreach ($rules as $rule) + { + // Arguments for rule + $args = NULL; + + if (is_string($rule)) + { + if (preg_match('/^([^\[]++)\[(.+)\]$/', $rule, $matches)) + { + // Split the rule into the function and args + $rule = $matches[1]; + $args = preg_split('/(?<!\\\\),\s*/', $matches[2]); + + // Replace escaped comma with comma + $args = str_replace('\,', ',', $args); + } + } + + if ($rule === 'is_array') + { + // This field is expected to be an array + $this->array_fields[$field] = $field; + } + + // Convert to a proper callback + $rule = $this->callback($rule); + + // Add the rule, with args, to the field + $this->rules[$field][] = array($rule, $args); + } + + return $this; + } + + /** + * Add callbacks to a field. Callbacks must accept the Validation object + * and the input name. Callback returns are not processed. + * + * @chainable + * @param string field name + * @param callbacks callbacks (unlimited number) + * @return object + */ + public function add_callbacks($field, $callbacks) + { + // Get all callbacks as an array + $callbacks = func_get_args(); + $callbacks = array_slice($callbacks, 1); + + if ($field === TRUE) + { + // Use wildcard + $field = '*'; + } + + foreach ($callbacks as $callback) + { + // Convert to a proper callback + $callback = $this->callback($callback); + + // Add the callback to specified field + $this->callbacks[$field][] = $callback; + } + + return $this; + } + + /** + * Validate by processing pre-filters, rules, callbacks, and post-filters. + * All fields that have filters, rules, or callbacks will be initialized if + * they are undefined. Validation will only be run if there is data already + * in the array. + * + * @param object Validation object, used only for recursion + * @param object name of field for errors + * @return bool + */ + public function validate($object = NULL, $field_name = NULL) + { + if ($object === NULL) + { + // Use the current object + $object = $this; + } + + // Get all field names + $fields = $this->field_names(); + + // Copy the array from the object, to optimize multiple sets + $array = $this->getArrayCopy(); + + foreach ($fields as $field) + { + if ($field === '*') + { + // Ignore wildcard + continue; + } + + if ( ! isset($array[$field])) + { + if (isset($this->array_fields[$field])) + { + // This field must be an array + $array[$field] = array(); + } + else + { + $array[$field] = NULL; + } + } + } + + // Swap the array back into the object + $this->exchangeArray($array); + + // Get all defined field names + $fields = array_keys($array); + + foreach ($this->pre_filters as $field => $callbacks) + { + foreach ($callbacks as $callback) + { + if ($field === '*') + { + foreach ($fields as $f) + { + $this[$f] = is_array($this[$f]) ? array_map($callback, $this[$f]) : call_user_func($callback, $this[$f]); + } + } + else + { + $this[$field] = is_array($this[$field]) ? array_map($callback, $this[$field]) : call_user_func($callback, $this[$field]); + } + } + } + + if ($this->submitted === FALSE) + return FALSE; + + foreach ($this->rules as $field => $callbacks) + { + foreach ($callbacks as $callback) + { + // Separate the callback and arguments + list ($callback, $args) = $callback; + + // Function or method name of the rule + $rule = is_array($callback) ? $callback[1] : $callback; + + if ($field === '*') + { + foreach ($fields as $f) + { + // Note that continue, instead of break, is used when + // applying rules using a wildcard, so that all fields + // will be validated. + + if (isset($this->errors[$f])) + { + // Prevent other rules from being evaluated if an error has occurred + continue; + } + + if (empty($this[$f]) AND ! in_array($rule, $this->empty_rules)) + { + // This rule does not need to be processed on empty fields + continue; + } + + if ($args === NULL) + { + if ( ! call_user_func($callback, $this[$f])) + { + $this->errors[$f] = $rule; + + // Stop validating this field when an error is found + continue; + } + } + else + { + if ( ! call_user_func($callback, $this[$f], $args)) + { + $this->errors[$f] = $rule; + + // Stop validating this field when an error is found + continue; + } + } + } + } + else + { + if (isset($this->errors[$field])) + { + // Prevent other rules from being evaluated if an error has occurred + break; + } + + if ( ! in_array($rule, $this->empty_rules) AND ! $this->required($this[$field])) + { + // This rule does not need to be processed on empty fields + continue; + } + + if ($args === NULL) + { + if ( ! call_user_func($callback, $this[$field])) + { + $this->errors[$field] = $rule; + + // Stop validating this field when an error is found + break; + } + } + else + { + if ( ! call_user_func($callback, $this[$field], $args)) + { + $this->errors[$field] = $rule; + + // Stop validating this field when an error is found + break; + } + } + } + } + } + + foreach ($this->callbacks as $field => $callbacks) + { + foreach ($callbacks as $callback) + { + if ($field === '*') + { + foreach ($fields as $f) + { + // Note that continue, instead of break, is used when + // applying rules using a wildcard, so that all fields + // will be validated. + + if (isset($this->errors[$f])) + { + // Stop validating this field when an error is found + continue; + } + + call_user_func($callback, $this, $f); + } + } + else + { + if (isset($this->errors[$field])) + { + // Stop validating this field when an error is found + break; + } + + call_user_func($callback, $this, $field); + } + } + } + + foreach ($this->post_filters as $field => $callbacks) + { + foreach ($callbacks as $callback) + { + if ($field === '*') + { + foreach ($fields as $f) + { + $this[$f] = is_array($this[$f]) ? array_map($callback, $this[$f]) : call_user_func($callback, $this[$f]); + } + } + else + { + $this[$field] = is_array($this[$field]) ? array_map($callback, $this[$field]) : call_user_func($callback, $this[$field]); + } + } + } + + // Return TRUE if there are no errors + return $this->errors === array(); + } + + /** + * Add an error to an input. + * + * @chainable + * @param string input name + * @param string unique error name + * @return object + */ + public function add_error($field, $name) + { + $this->errors[$field] = $name; + + return $this; + } + + /** + * Sets or returns the message for an input. + * + * @chainable + * @param string input key + * @param string message to set + * @return string|object + */ + public function message($input = NULL, $message = NULL) + { + if ($message === NULL) + { + if ($input === NULL) + { + $messages = array(); + $keys = array_keys($this->messages); + + foreach ($keys as $input) + { + $messages[] = $this->message($input); + } + + return implode("\n", $messages); + } + + // Return nothing if no message exists + if (empty($this->messages[$input])) + return ''; + + // Return the HTML message string + return $this->messages[$input]; + } + else + { + $this->messages[$input] = $message; + } + + return $this; + } + + /** + * Return the errors array. + * + * @param boolean load errors from a lang file + * @return array + */ + public function errors($file = NULL) + { + if ($file === NULL) + { + return $this->errors; + } + else + { + + $errors = array(); + foreach ($this->errors as $input => $error) + { + // Key for this input error + $key = "$file.$input.$error"; + + if (($errors[$input] = Kohana::lang($key)) === $key) + { + // Get the default error message + $errors[$input] = Kohana::lang("$file.$input.default"); + } + } + + return $errors; + } + } + + /** + * Rule: required. Generates an error if the field has an empty value. + * + * @param mixed input value + * @return bool + */ + public function required($str) + { + if (is_object($str) AND $str instanceof ArrayObject) + { + // Get the array from the ArrayObject + $str = $str->getArrayCopy(); + } + + if (is_array($str)) + { + return ! empty($str); + } + else + { + return ! ($str === '' OR $str === NULL OR $str === FALSE); + } + } + + /** + * Rule: matches. Generates an error if the field does not match one or more + * other fields. + * + * @param mixed input value + * @param array input names to match against + * @return bool + */ + public function matches($str, array $inputs) + { + foreach ($inputs as $key) + { + if ($str !== (isset($this[$key]) ? $this[$key] : NULL)) + return FALSE; + } + + return TRUE; + } + + /** + * Rule: length. Generates an error if the field is too long or too short. + * + * @param mixed input value + * @param array minimum, maximum, or exact length to match + * @return bool + */ + public function length($str, array $length) + { + if ( ! is_string($str)) + return FALSE; + + $size = utf8::strlen($str); + $status = FALSE; + + if (count($length) > 1) + { + list ($min, $max) = $length; + + if ($size >= $min AND $size <= $max) + { + $status = TRUE; + } + } + else + { + $status = ($size === (int) $length[0]); + } + + return $status; + } + + /** + * Rule: depends_on. Generates an error if the field does not depend on one + * or more other fields. + * + * @param mixed field name + * @param array field names to check dependency + * @return bool + */ + public function depends_on($field, array $fields) + { + foreach ($fields as $depends_on) + { + if ( ! isset($this[$depends_on]) OR $this[$depends_on] == NULL) + return FALSE; + } + + return TRUE; + } + + /** + * Rule: chars. Generates an error if the field contains characters outside of the list. + * + * @param string field value + * @param array allowed characters + * @return bool + */ + public function chars($value, array $chars) + { + return ! preg_match('![^'.implode('', $chars).']!u', $value); + } + +} // End Validation diff --git a/system/libraries/View.php b/system/libraries/View.php new file mode 100644 index 00000000..2b8471c6 --- /dev/null +++ b/system/libraries/View.php @@ -0,0 +1,309 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Loads and displays Kohana view files. Can also handle output of some binary + * files, such as image, Javascript, and CSS files. + * + * $Id: View.php 4072 2009-03-13 17:20:38Z jheathco $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class View_Core { + + // The view file name and type + protected $kohana_filename = FALSE; + protected $kohana_filetype = FALSE; + + // View variable storage + protected $kohana_local_data = array(); + protected static $kohana_global_data = array(); + + /** + * Creates a new View using the given parameters. + * + * @param string view name + * @param array pre-load data + * @param string type of file: html, css, js, etc. + * @return object + */ + public static function factory($name = NULL, $data = NULL, $type = NULL) + { + return new View($name, $data, $type); + } + + /** + * Attempts to load a view and pre-load view data. + * + * @throws Kohana_Exception if the requested view cannot be found + * @param string view name + * @param array pre-load data + * @param string type of file: html, css, js, etc. + * @return void + */ + public function __construct($name = NULL, $data = NULL, $type = NULL) + { + if (is_string($name) AND $name !== '') + { + // Set the filename + $this->set_filename($name, $type); + } + + if (is_array($data) AND ! empty($data)) + { + // Preload data using array_merge, to allow user extensions + $this->kohana_local_data = array_merge($this->kohana_local_data, $data); + } + } + + /** + * Magic method access to test for view property + * + * @param string View property to test for + * @return boolean + */ + public function __isset($key = NULL) + { + return $this->is_set($key); + } + + /** + * Sets the view filename. + * + * @chainable + * @param string view filename + * @param string view file type + * @return object + */ + public function set_filename($name, $type = NULL) + { + if ($type == NULL) + { + // Load the filename and set the content type + $this->kohana_filename = Kohana::find_file('views', $name, TRUE); + $this->kohana_filetype = EXT; + } + else + { + // Check if the filetype is allowed by the configuration + if ( ! in_array($type, Kohana::config('view.allowed_filetypes'))) + throw new Kohana_Exception('core.invalid_filetype', $type); + + // Load the filename and set the content type + $this->kohana_filename = Kohana::find_file('views', $name, TRUE, $type); + $this->kohana_filetype = Kohana::config('mimes.'.$type); + + if ($this->kohana_filetype == NULL) + { + // Use the specified type + $this->kohana_filetype = $type; + } + } + + return $this; + } + + /** + * Sets a view variable. + * + * @param string|array name of variable or an array of variables + * @param mixed value when using a named variable + * @return object + */ + public function set($name, $value = NULL) + { + if (is_array($name)) + { + foreach ($name as $key => $value) + { + $this->__set($key, $value); + } + } + else + { + $this->__set($name, $value); + } + + return $this; + } + + /** + * Checks for a property existence in the view locally or globally. Unlike the built in __isset(), + * this method can take an array of properties to test simultaneously. + * + * @param string $key property name to test for + * @param array $key array of property names to test for + * @return boolean property test result + * @return array associative array of keys and boolean test result + */ + public function is_set( $key = FALSE ) + { + // Setup result; + $result = FALSE; + + // If key is an array + if (is_array($key)) + { + // Set the result to an array + $result = array(); + + // Foreach key + foreach ($key as $property) + { + // Set the result to an associative array + $result[$property] = (array_key_exists($property, $this->kohana_local_data) OR array_key_exists($property, View::$kohana_global_data)) ? TRUE : FALSE; + } + } + else + { + // Otherwise just check one property + $result = (array_key_exists($key, $this->kohana_local_data) OR array_key_exists($key, View::$kohana_global_data)) ? TRUE : FALSE; + } + + // Return the result + return $result; + } + + /** + * Sets a bound variable by reference. + * + * @param string name of variable + * @param mixed variable to assign by reference + * @return object + */ + public function bind($name, & $var) + { + $this->kohana_local_data[$name] =& $var; + + return $this; + } + + /** + * Sets a view global variable. + * + * @param string|array name of variable or an array of variables + * @param mixed value when using a named variable + * @return void + */ + public static function set_global($name, $value = NULL) + { + if (is_array($name)) + { + foreach ($name as $key => $value) + { + View::$kohana_global_data[$key] = $value; + } + } + else + { + View::$kohana_global_data[$name] = $value; + } + } + + /** + * Magically sets a view variable. + * + * @param string variable key + * @param string variable value + * @return void + */ + public function __set($key, $value) + { + $this->kohana_local_data[$key] = $value; + } + + /** + * Magically gets a view variable. + * + * @param string variable key + * @return mixed variable value if the key is found + * @return void if the key is not found + */ + public function &__get($key) + { + if (isset($this->kohana_local_data[$key])) + return $this->kohana_local_data[$key]; + + if (isset(View::$kohana_global_data[$key])) + return View::$kohana_global_data[$key]; + + if (isset($this->$key)) + return $this->$key; + } + + /** + * Magically converts view object to string. + * + * @return string + */ + public function __toString() + { + try + { + return $this->render(); + } + catch (Exception $e) + { + // Display the exception using its internal __toString method + return (string) $e; + } + } + + /** + * Renders a view. + * + * @param boolean set to TRUE to echo the output instead of returning it + * @param callback special renderer to pass the output through + * @return string if print is FALSE + * @return void if print is TRUE + */ + public function render($print = FALSE, $renderer = FALSE) + { + if (empty($this->kohana_filename)) + throw new Kohana_Exception('core.view_set_filename'); + + if (is_string($this->kohana_filetype)) + { + // Merge global and local data, local overrides global with the same name + $data = array_merge(View::$kohana_global_data, $this->kohana_local_data); + + // Load the view in the controller for access to $this + $output = Kohana::$instance->_kohana_load_view($this->kohana_filename, $data); + + if ($renderer !== FALSE AND is_callable($renderer, TRUE)) + { + // Pass the output through the user defined renderer + $output = call_user_func($renderer, $output); + } + + if ($print === TRUE) + { + // Display the output + echo $output; + return; + } + } + else + { + // Set the content type and size + header('Content-Type: '.$this->kohana_filetype[0]); + + if ($print === TRUE) + { + if ($file = fopen($this->kohana_filename, 'rb')) + { + // Display the output + fpassthru($file); + fclose($file); + } + return; + } + + // Fetch the file contents + $output = file_get_contents($this->kohana_filename); + } + + return $output; + } +} // End View
\ No newline at end of file diff --git a/system/libraries/drivers/Cache.php b/system/libraries/drivers/Cache.php new file mode 100644 index 00000000..7c5e3c31 --- /dev/null +++ b/system/libraries/drivers/Cache.php @@ -0,0 +1,40 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Cache driver interface. + * + * $Id: Cache.php 4046 2009-03-05 19:23:29Z Shadowhand $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +interface Cache_Driver { + + /** + * Set a cache item. + */ + public function set($id, $data, array $tags = NULL, $lifetime); + + /** + * Find all of the cache ids for a given tag. + */ + public function find($tag); + + /** + * Get a cache item. + * Return NULL if the cache item is not found. + */ + public function get($id); + + /** + * Delete cache items by id or tag. + */ + public function delete($id, $tag = FALSE); + + /** + * Deletes all expired cache items. + */ + public function delete_expired(); + +} // End Cache Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Cache/Apc.php b/system/libraries/drivers/Cache/Apc.php new file mode 100644 index 00000000..f7be048f --- /dev/null +++ b/system/libraries/drivers/Cache/Apc.php @@ -0,0 +1,64 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * APC-based Cache driver. + * + * $Id: Apc.php 4046 2009-03-05 19:23:29Z Shadowhand $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Apc_Driver implements Cache_Driver { + + public function __construct() + { + if ( ! extension_loaded('apc')) + throw new Kohana_Exception('cache.extension_not_loaded', 'apc'); + } + + public function get($id) + { + return (($return = apc_fetch($id)) === FALSE) ? NULL : $return; + } + + public function set($id, $data, array $tags = NULL, $lifetime) + { + if ( ! empty($tags)) + { + Kohana::log('error', 'Cache: tags are unsupported by the APC driver'); + } + + return apc_store($id, $data, $lifetime); + } + + public function find($tag) + { + Kohana::log('error', 'Cache: tags are unsupported by the APC driver'); + + return array(); + } + + public function delete($id, $tag = FALSE) + { + if ($tag === TRUE) + { + Kohana::log('error', 'Cache: tags are unsupported by the APC driver'); + return FALSE; + } + elseif ($id === TRUE) + { + return apc_clear_cache('user'); + } + else + { + return apc_delete($id); + } + } + + public function delete_expired() + { + return TRUE; + } + +} // End Cache APC Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Cache/Eaccelerator.php b/system/libraries/drivers/Cache/Eaccelerator.php new file mode 100644 index 00000000..a45616d5 --- /dev/null +++ b/system/libraries/drivers/Cache/Eaccelerator.php @@ -0,0 +1,66 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Eaccelerator-based Cache driver. + * + * $Id: Eaccelerator.php 4046 2009-03-05 19:23:29Z Shadowhand $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Eaccelerator_Driver implements Cache_Driver { + + public function __construct() + { + if ( ! extension_loaded('eaccelerator')) + throw new Kohana_Exception('cache.extension_not_loaded', 'eaccelerator'); + } + + public function get($id) + { + return eaccelerator_get($id); + } + + public function find($tag) + { + Kohana::log('error', 'tags are unsupported by the eAccelerator driver'); + + return array(); + } + + public function set($id, $data, array $tags = NULL, $lifetime) + { + if ( ! empty($tags)) + { + Kohana::log('error', 'tags are unsupported by the eAccelerator driver'); + } + + return eaccelerator_put($id, $data, $lifetime); + } + + public function delete($id, $tag = FALSE) + { + if ($tag === TRUE) + { + Kohana::log('error', 'tags are unsupported by the eAccelerator driver'); + return FALSE; + } + elseif ($id === TRUE) + { + return eaccelerator_clean(); + } + else + { + return eaccelerator_rm($id); + } + } + + public function delete_expired() + { + eaccelerator_gc(); + + return TRUE; + } + +} // End Cache eAccelerator Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Cache/File.php b/system/libraries/drivers/Cache/File.php new file mode 100644 index 00000000..cc9d48d3 --- /dev/null +++ b/system/libraries/drivers/Cache/File.php @@ -0,0 +1,261 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * File-based Cache driver. + * + * $Id: File.php 4046 2009-03-05 19:23:29Z Shadowhand $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_File_Driver implements Cache_Driver { + + protected $directory = ''; + + /** + * Tests that the storage location is a directory and is writable. + */ + public function __construct($directory) + { + // Find the real path to the directory + $directory = str_replace('\\', '/', realpath($directory)).'/'; + + // Make sure the cache directory is writable + if ( ! is_dir($directory) OR ! is_writable($directory)) + throw new Kohana_Exception('cache.unwritable', $directory); + + // Directory is valid + $this->directory = $directory; + } + + /** + * Finds an array of files matching the given id or tag. + * + * @param string cache id or tag + * @param bool search for tags + * @return array of filenames matching the id or tag + */ + public function exists($id, $tag = FALSE) + { + if ($id === TRUE) + { + // Find all the files + return glob($this->directory.'*~*~*'); + } + elseif ($tag === TRUE) + { + // Find all the files that have the tag name + $paths = glob($this->directory.'*~*'.$id.'*~*'); + + // Find all tags matching the given tag + $files = array(); + foreach ($paths as $path) + { + // Split the files + $tags = explode('~', basename($path)); + + // Find valid tags + if (count($tags) !== 3 OR empty($tags[1])) + continue; + + // Split the tags by plus signs, used to separate tags + $tags = explode('+', $tags[1]); + + if (in_array($tag, $tags)) + { + // Add the file to the array, it has the requested tag + $files[] = $path; + } + } + + return $files; + } + else + { + // Find the file matching the given id + return glob($this->directory.$id.'~*'); + } + } + + /** + * Sets a cache item to the given data, tags, and lifetime. + * + * @param string cache id to set + * @param string data in the cache + * @param array cache tags + * @param integer lifetime + * @return bool + */ + public function set($id, $data, array $tags = NULL, $lifetime) + { + // Remove old cache files + $this->delete($id); + + // Cache File driver expects unix timestamp + if ($lifetime !== 0) + { + $lifetime += time(); + } + + if ( ! empty($tags)) + { + // Convert the tags into a string list + $tags = implode('+', $tags); + } + + // Write out a serialized cache + return (bool) file_put_contents($this->directory.$id.'~'.$tags.'~'.$lifetime, serialize($data)); + } + + /** + * Finds an array of ids for a given tag. + * + * @param string tag name + * @return array of ids that match the tag + */ + public function find($tag) + { + // An array will always be returned + $result = array(); + + if ($paths = $this->exists($tag, TRUE)) + { + // Length of directory name + $offset = strlen($this->directory); + + // Find all the files with the given tag + foreach ($paths as $path) + { + // Get the id from the filename + list($id, $junk) = explode('~', basename($path), 2); + + if (($data = $this->get($id)) !== FALSE) + { + // Add the result to the array + $result[$id] = $data; + } + } + } + + return $result; + } + + /** + * Fetches a cache item. This will delete the item if it is expired or if + * the hash does not match the stored hash. + * + * @param string cache id + * @return mixed|NULL + */ + public function get($id) + { + if ($file = $this->exists($id)) + { + // Use the first file + $file = current($file); + + // Validate that the cache has not expired + if ($this->expired($file)) + { + // Remove this cache, it has expired + $this->delete($id); + } + else + { + // Turn off errors while reading the file + $ER = error_reporting(0); + + if (($data = file_get_contents($file)) !== FALSE) + { + // Unserialize the data + $data = unserialize($data); + } + else + { + // Delete the data + unset($data); + } + + // Turn errors back on + error_reporting($ER); + } + } + + // Return NULL if there is no data + return isset($data) ? $data : NULL; + } + + /** + * Deletes a cache item by id or tag + * + * @param string cache id or tag, or TRUE for "all items" + * @param boolean use tags + * @return boolean + */ + public function delete($id, $tag = FALSE) + { + $files = $this->exists($id, $tag); + + if (empty($files)) + return FALSE; + + // Disable all error reporting while deleting + $ER = error_reporting(0); + + foreach ($files as $file) + { + // Remove the cache file + if ( ! unlink($file)) + Kohana::log('error', 'Cache: Unable to delete cache file: '.$file); + } + + // Turn on error reporting again + error_reporting($ER); + + return TRUE; + } + + /** + * Deletes all cache files that are older than the current time. + * + * @return void + */ + public function delete_expired() + { + if ($files = $this->exists(TRUE)) + { + // Disable all error reporting while deleting + $ER = error_reporting(0); + + foreach ($files as $file) + { + if ($this->expired($file)) + { + // The cache file has already expired, delete it + if ( ! unlink($file)) + Kohana::log('error', 'Cache: Unable to delete cache file: '.$file); + } + } + + // Turn on error reporting again + error_reporting($ER); + } + } + + /** + * Check if a cache file has expired by filename. + * + * @param string filename + * @return bool + */ + protected function expired($file) + { + // Get the expiration time + $expires = (int) substr($file, strrpos($file, '~') + 1); + + // Expirations of 0 are "never expire" + return ($expires !== 0 AND $expires <= time()); + } + +} // End Cache File Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Cache/Memcache.php b/system/libraries/drivers/Cache/Memcache.php new file mode 100644 index 00000000..d801de9c --- /dev/null +++ b/system/libraries/drivers/Cache/Memcache.php @@ -0,0 +1,191 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Memcache-based Cache driver. + * + * $Id: Memcache.php 4102 2009-03-19 12:55:54Z Shadowhand $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Memcache_Driver implements Cache_Driver { + + const TAGS_KEY = 'memcache_tags_array'; + + // Cache backend object and flags + protected $backend; + protected $flags; + + // Tags array + protected static $tags; + + // Have the tags been changed? + protected static $tags_changed = FALSE; + + public function __construct() + { + if ( ! extension_loaded('memcache')) + throw new Kohana_Exception('cache.extension_not_loaded', 'memcache'); + + $this->backend = new Memcache; + $this->flags = Kohana::config('cache_memcache.compression') ? MEMCACHE_COMPRESSED : FALSE; + + $servers = Kohana::config('cache_memcache.servers'); + + foreach ($servers as $server) + { + // Make sure all required keys are set + $server += array('host' => '127.0.0.1', 'port' => 11211, 'persistent' => FALSE); + + // Add the server to the pool + $this->backend->addServer($server['host'], $server['port'], (bool) $server['persistent']) + or Kohana::log('error', 'Cache: Connection failed: '.$server['host']); + } + + // Load tags + self::$tags = $this->backend->get(self::TAGS_KEY); + + if ( ! is_array(self::$tags)) + { + // Create a new tags array + self::$tags = array(); + + // Tags have been created + self::$tags_changed = TRUE; + } + } + + public function __destruct() + { + if (self::$tags_changed === TRUE) + { + // Save the tags + $this->backend->set(self::TAGS_KEY, self::$tags, $this->flags, 0); + + // Tags are now unchanged + self::$tags_changed = FALSE; + } + } + + public function find($tag) + { + if (isset(self::$tags[$tag]) AND $results = $this->backend->get(self::$tags[$tag])) + { + // Return all the found caches + return $results; + } + else + { + // No matching tags + return array(); + } + } + + public function get($id) + { + return (($return = $this->backend->get($id)) === FALSE) ? NULL : $return; + } + + public function set($id, $data, array $tags = NULL, $lifetime) + { + if ( ! empty($tags)) + { + // Tags will be changed + self::$tags_changed = TRUE; + + foreach ($tags as $tag) + { + // Add the id to each tag + self::$tags[$tag][$id] = $id; + } + } + + if ($lifetime !== 0) + { + // Memcache driver expects unix timestamp + $lifetime += time(); + } + + // Set a new value + return $this->backend->set($id, $data, $this->flags, $lifetime); + } + + public function delete($id, $tag = FALSE) + { + // Tags will be changed + self::$tags_changed = TRUE; + + if ($id === TRUE) + { + if ($status = $this->backend->flush()) + { + // Remove all tags, all items have been deleted + self::$tags = array(); + + // We must sleep after flushing, or overwriting will not work! + // @see http://php.net/manual/en/function.memcache-flush.php#81420 + sleep(1); + } + + return $status; + } + elseif ($tag === TRUE) + { + if (isset(self::$tags[$id])) + { + foreach (self::$tags[$id] as $_id) + { + // Delete each id in the tag + $this->backend->delete($_id); + } + + // Delete the tag + unset(self::$tags[$id]); + } + + return TRUE; + } + else + { + foreach (self::$tags as $tag => $_ids) + { + if (isset(self::$tags[$tag][$id])) + { + // Remove the id from the tags + unset(self::$tags[$tag][$id]); + } + } + + return $this->backend->delete($id); + } + } + + public function delete_expired() + { + // Tags will be changed + self::$tags_changed = TRUE; + + foreach (self::$tags as $tag => $_ids) + { + foreach ($_ids as $id) + { + if ( ! $this->backend->get($id)) + { + // This id has disappeared, delete it from the tags + unset(self::$tags[$tag][$id]); + } + } + + if (empty(self::$tags[$tag])) + { + // The tag no longer has any valid ids + unset(self::$tags[$tag]); + } + } + + // Memcache handles garbage collection internally + return TRUE; + } + +} // End Cache Memcache Driver diff --git a/system/libraries/drivers/Cache/Sqlite.php b/system/libraries/drivers/Cache/Sqlite.php new file mode 100644 index 00000000..9458d851 --- /dev/null +++ b/system/libraries/drivers/Cache/Sqlite.php @@ -0,0 +1,257 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * SQLite-based Cache driver. + * + * $Id: Sqlite.php 4046 2009-03-05 19:23:29Z Shadowhand $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Sqlite_Driver implements Cache_Driver { + + // SQLite database instance + protected $db; + + // Database error messages + protected $error; + + /** + * Logs an SQLite error. + */ + protected static function log_error($code) + { + // Log an error + Kohana::log('error', 'Cache: SQLite error: '.sqlite_error_string($error)); + } + + /** + * Tests that the storage location is a directory and is writable. + */ + public function __construct($filename) + { + // Get the directory name + $directory = str_replace('\\', '/', realpath(pathinfo($filename, PATHINFO_DIRNAME))).'/'; + + // Set the filename from the real directory path + $filename = $directory.basename($filename); + + // Make sure the cache directory is writable + if ( ! is_dir($directory) OR ! is_writable($directory)) + throw new Kohana_Exception('cache.unwritable', $directory); + + // Make sure the cache database is writable + if (is_file($filename) AND ! is_writable($filename)) + throw new Kohana_Exception('cache.unwritable', $filename); + + // Open up an instance of the database + $this->db = new SQLiteDatabase($filename, '0666', $error); + + // Throw an exception if there's an error + if ( ! empty($error)) + throw new Kohana_Exception('cache.driver_error', sqlite_error_string($error)); + + $query = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'caches'"; + $tables = $this->db->query($query, SQLITE_BOTH, $error); + + // Throw an exception if there's an error + if ( ! empty($error)) + throw new Kohana_Exception('cache.driver_error', sqlite_error_string($error)); + + if ($tables->numRows() == 0) + { + Kohana::log('error', 'Cache: Initializing new SQLite cache database'); + + // Issue a CREATE TABLE command + $this->db->unbufferedQuery(Kohana::config('cache_sqlite.schema')); + } + } + + /** + * Checks if a cache id is already set. + * + * @param string cache id + * @return boolean + */ + public function exists($id) + { + // Find the id that matches + $query = "SELECT id FROM caches WHERE id = '$id'"; + + return ($this->db->query($query)->numRows() > 0); + } + + /** + * Sets a cache item to the given data, tags, and lifetime. + * + * @param string cache id to set + * @param string data in the cache + * @param array cache tags + * @param integer lifetime + * @return bool + */ + public function set($id, $data, array $tags = NULL, $lifetime) + { + // Serialize and escape the data + $data = sqlite_escape_string(serialize($data)); + + if ( ! empty($tags)) + { + // Escape the tags, adding brackets so the tag can be explicitly matched + $tags = sqlite_escape_string('<'.implode('>,<', $tags).'>'); + } + + // Cache Sqlite driver expects unix timestamp + if ($lifetime !== 0) + { + $lifetime += time(); + } + + $query = $this->exists($id) + ? "UPDATE caches SET tags = '$tags', expiration = '$lifetime', cache = '$data' WHERE id = '$id'" + : "INSERT INTO caches VALUES('$id', '$tags', '$lifetime', '$data')"; + + // Run the query + $this->db->unbufferedQuery($query, SQLITE_BOTH, $error); + + if ( ! empty($error)) + { + self::log_error($error); + return FALSE; + } + else + { + return TRUE; + } + } + + /** + * Finds an array of ids for a given tag. + * + * @param string tag name + * @return array of ids that match the tag + */ + public function find($tag) + { + $query = "SELECT id,cache FROM caches WHERE tags LIKE '%<{$tag}>%'"; + $query = $this->db->query($query, SQLITE_BOTH, $error); + + // An array will always be returned + $result = array(); + + if ( ! empty($error)) + { + self::log_error($error); + } + elseif ($query->numRows() > 0) + { + // Disable notices for unserializing + $ER = error_reporting(~E_NOTICE); + + while ($row = $query->fetchObject()) + { + // Add each cache to the array + $result[$row->id] = unserialize($row->cache); + } + + // Turn notices back on + error_reporting($ER); + } + + return $result; + } + + /** + * Fetches a cache item. This will delete the item if it is expired or if + * the hash does not match the stored hash. + * + * @param string cache id + * @return mixed|NULL + */ + public function get($id) + { + $query = "SELECT id, expiration, cache FROM caches WHERE id = '$id' LIMIT 0, 1"; + $query = $this->db->query($query, SQLITE_BOTH, $error); + + if ( ! empty($error)) + { + self::log_error($error); + } + elseif ($cache = $query->fetchObject()) + { + // Make sure the expiration is valid and that the hash matches + if ($cache->expiration != 0 AND $cache->expiration <= time()) + { + // Cache is not valid, delete it now + $this->delete($cache->id); + } + else + { + // Disable notices for unserializing + $ER = error_reporting(~E_NOTICE); + + // Return the valid cache data + $data = $cache->cache; + + // Turn notices back on + error_reporting($ER); + } + } + + // No valid cache found + return NULL; + } + + /** + * Deletes a cache item by id or tag + * + * @param string cache id or tag, or TRUE for "all items" + * @param bool delete a tag + * @return bool + */ + public function delete($id, $tag = FALSE) + { + if ($id === TRUE) + { + // Delete all caches + $where = '1'; + } + elseif ($tag === TRUE) + { + // Delete by tag + $where = "tags LIKE '%<{$id}>%'"; + } + else + { + // Delete by id + $where = "id = '$id'"; + } + + $this->db->unbufferedQuery('DELETE FROM caches WHERE '.$where, SQLITE_BOTH, $error); + + if ( ! empty($error)) + { + self::log_error($error); + return FALSE; + } + else + { + return TRUE; + } + } + + /** + * Deletes all cache files that are older than the current time. + */ + public function delete_expired() + { + // Delete all expired caches + $query = 'DELETE FROM caches WHERE expiration != 0 AND expiration <= '.time(); + + $this->db->unbufferedQuery($query); + + return TRUE; + } + +} // End Cache SQLite Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Cache/Xcache.php b/system/libraries/drivers/Cache/Xcache.php new file mode 100644 index 00000000..6254bbb6 --- /dev/null +++ b/system/libraries/drivers/Cache/Xcache.php @@ -0,0 +1,119 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Xcache Cache driver. + * + * $Id: Xcache.php 4046 2009-03-05 19:23:29Z Shadowhand $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Xcache_Driver implements Cache_Driver { + + public function __construct() + { + if ( ! extension_loaded('xcache')) + throw new Kohana_Exception('cache.extension_not_loaded', 'xcache'); + } + + public function get($id) + { + if (xcache_isset($id)) + return xcache_get($id); + + return NULL; + } + + public function set($id, $data, array $tags = NULL, $lifetime) + { + if ( ! empty($tags)) + { + Kohana::log('error', 'Cache: tags are unsupported by the Xcache driver'); + } + + return xcache_set($id, $data, $lifetime); + } + + public function find($tag) + { + Kohana::log('error', 'Cache: tags are unsupported by the Xcache driver'); + return FALSE; + } + + public function delete($id, $tag = FALSE) + { + if ($tag !== FALSE) + { + Kohana::log('error', 'Cache: tags are unsupported by the Xcache driver'); + return TRUE; + } + elseif ($id !== TRUE) + { + if (xcache_isset($id)) + return xcache_unset($id); + + return FALSE; + } + else + { + // Do the login + $this->auth(); + $result = TRUE; + for ($i = 0, $max = xcache_count(XC_TYPE_VAR); $i < $max; $i++) + { + if (xcache_clear_cache(XC_TYPE_VAR, $i) !== NULL) + { + $result = FALSE; + break; + } + } + + // Undo the login + $this->auth(TRUE); + return $result; + } + + return TRUE; + } + + public function delete_expired() + { + return TRUE; + } + + private function auth($reverse = FALSE) + { + static $backup = array(); + + $keys = array('PHP_AUTH_USER', 'PHP_AUTH_PW'); + + foreach ($keys as $key) + { + if ($reverse) + { + if (isset($backup[$key])) + { + $_SERVER[$key] = $backup[$key]; + unset($backup[$key]); + } + else + { + unset($_SERVER[$key]); + } + } + else + { + $value = getenv($key); + + if ( ! empty($value)) + { + $backup[$key] = $value; + } + + $_SERVER[$key] = Kohana::config('cache_xcache.'.$key); + } + } + } + +} // End Cache Xcache Driver diff --git a/system/libraries/drivers/Captcha.php b/system/libraries/drivers/Captcha.php new file mode 100644 index 00000000..a4343e19 --- /dev/null +++ b/system/libraries/drivers/Captcha.php @@ -0,0 +1,227 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Captcha driver class. + * + * $Id: Captcha.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Captcha_Driver { + + // The correct Captcha challenge answer + protected $response; + + // Image resource identifier and type ("png", "gif" or "jpeg") + protected $image; + protected $image_type = 'png'; + + /** + * Constructs a new challenge. + * + * @return void + */ + public function __construct() + { + // Generate a new challenge + $this->response = $this->generate_challenge(); + + // Store the correct Captcha response in a session + Event::add('system.post_controller', array($this, 'update_response_session')); + } + + /** + * Generate a new Captcha challenge. + * + * @return string the challenge answer + */ + abstract public function generate_challenge(); + + /** + * Output the Captcha challenge. + * + * @param boolean html output + * @return mixed the rendered Captcha (e.g. an image, riddle, etc.) + */ + abstract public function render($html); + + /** + * Stores the response for the current Captcha challenge in a session so it is available + * on the next page load for Captcha::valid(). This method is called after controller + * execution (in the system.post_controller event) in order not to overwrite itself too soon. + * + * @return void + */ + public function update_response_session() + { + Session::instance()->set('captcha_response', sha1(strtoupper($this->response))); + } + + /** + * Validates a Captcha response from a user. + * + * @param string captcha response + * @return boolean + */ + public function valid($response) + { + return (sha1(strtoupper($response)) === Session::instance()->get('captcha_response')); + } + + /** + * Returns the image type. + * + * @param string filename + * @return string|FALSE image type ("png", "gif" or "jpeg") + */ + public function image_type($filename) + { + switch (strtolower(substr(strrchr($filename, '.'), 1))) + { + case 'png': + return 'png'; + + case 'gif': + return 'gif'; + + case 'jpg': + case 'jpeg': + // Return "jpeg" and not "jpg" because of the GD2 function names + return 'jpeg'; + + default: + return FALSE; + } + } + + /** + * Creates an image resource with the dimensions specified in config. + * If a background image is supplied, the image dimensions are used. + * + * @throws Kohana_Exception if no GD2 support + * @param string path to the background image file + * @return void + */ + public function image_create($background = NULL) + { + // Check for GD2 support + if ( ! function_exists('imagegd2')) + throw new Kohana_Exception('captcha.requires_GD2'); + + // Create a new image (black) + $this->image = imagecreatetruecolor(Captcha::$config['width'], Captcha::$config['height']); + + // Use a background image + if ( ! empty($background)) + { + // Create the image using the right function for the filetype + $function = 'imagecreatefrom'.$this->image_type($background); + $this->background_image = $function($background); + + // Resize the image if needed + if (imagesx($this->background_image) !== Captcha::$config['width'] + OR imagesy($this->background_image) !== Captcha::$config['height']) + { + imagecopyresampled + ( + $this->image, $this->background_image, 0, 0, 0, 0, + Captcha::$config['width'], Captcha::$config['height'], + imagesx($this->background_image), imagesy($this->background_image) + ); + } + + // Free up resources + imagedestroy($this->background_image); + } + } + + /** + * Fills the background with a gradient. + * + * @param resource gd image color identifier for start color + * @param resource gd image color identifier for end color + * @param string direction: 'horizontal' or 'vertical', 'random' by default + * @return void + */ + public function image_gradient($color1, $color2, $direction = NULL) + { + $directions = array('horizontal', 'vertical'); + + // Pick a random direction if needed + if ( ! in_array($direction, $directions)) + { + $direction = $directions[array_rand($directions)]; + + // Switch colors + if (mt_rand(0, 1) === 1) + { + $temp = $color1; + $color1 = $color2; + $color2 = $temp; + } + } + + // Extract RGB values + $color1 = imagecolorsforindex($this->image, $color1); + $color2 = imagecolorsforindex($this->image, $color2); + + // Preparations for the gradient loop + $steps = ($direction === 'horizontal') ? Captcha::$config['width'] : Captcha::$config['height']; + + $r1 = ($color1['red'] - $color2['red']) / $steps; + $g1 = ($color1['green'] - $color2['green']) / $steps; + $b1 = ($color1['blue'] - $color2['blue']) / $steps; + + if ($direction === 'horizontal') + { + $x1 =& $i; + $y1 = 0; + $x2 =& $i; + $y2 = Captcha::$config['height']; + } + else + { + $x1 = 0; + $y1 =& $i; + $x2 = Captcha::$config['width']; + $y2 =& $i; + } + + // Execute the gradient loop + for ($i = 0; $i <= $steps; $i++) + { + $r2 = $color1['red'] - floor($i * $r1); + $g2 = $color1['green'] - floor($i * $g1); + $b2 = $color1['blue'] - floor($i * $b1); + $color = imagecolorallocate($this->image, $r2, $g2, $b2); + + imageline($this->image, $x1, $y1, $x2, $y2, $color); + } + } + + /** + * Returns the img html element or outputs the image to the browser. + * + * @param boolean html output + * @return mixed html string or void + */ + public function image_render($html) + { + // Output html element + if ($html) + return '<img alt="Captcha" src="'.url::site('captcha/'.Captcha::$config['group']).'" width="'.Captcha::$config['width'].'" height="'.Captcha::$config['height'].'" />'; + + // Send the correct HTTP header + header('Content-Type: image/'.$this->image_type); + + // Pick the correct output function + $function = 'image'.$this->image_type; + $function($this->image); + + // Free up resources + imagedestroy($this->image); + } + +} // End Captcha Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Captcha/Alpha.php b/system/libraries/drivers/Captcha/Alpha.php new file mode 100644 index 00000000..b3a9c9d7 --- /dev/null +++ b/system/libraries/drivers/Captcha/Alpha.php @@ -0,0 +1,92 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Captcha driver for "alpha" style. + * + * $Id: Alpha.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Alpha_Driver extends Captcha_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Complexity setting is used as character count + return text::random('distinct', max(1, Captcha::$config['complexity'])); + } + + /** + * Outputs the Captcha image. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + // Creates $this->image + $this->image_create(Captcha::$config['background']); + + // Add a random gradient + if (empty(Captcha::$config['background'])) + { + $color1 = imagecolorallocate($this->image, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)); + $color2 = imagecolorallocate($this->image, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)); + $this->image_gradient($color1, $color2); + } + + // Add a few random circles + for ($i = 0, $count = mt_rand(10, Captcha::$config['complexity'] * 3); $i < $count; $i++) + { + $color = imagecolorallocatealpha($this->image, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(80, 120)); + $size = mt_rand(5, Captcha::$config['height'] / 3); + imagefilledellipse($this->image, mt_rand(0, Captcha::$config['width']), mt_rand(0, Captcha::$config['height']), $size, $size, $color); + } + + // Calculate character font-size and spacing + $default_size = min(Captcha::$config['width'], Captcha::$config['height'] * 2) / strlen($this->response); + $spacing = (int) (Captcha::$config['width'] * 0.9 / strlen($this->response)); + + // Background alphabetic character attributes + $color_limit = mt_rand(96, 160); + $chars = 'ABEFGJKLPQRTVY'; + + // Draw each Captcha character with varying attributes + for ($i = 0, $strlen = strlen($this->response); $i < $strlen; $i++) + { + // Use different fonts if available + $font = Captcha::$config['fontpath'].Captcha::$config['fonts'][array_rand(Captcha::$config['fonts'])]; + + $angle = mt_rand(-40, 20); + // Scale the character size on image height + $size = $default_size / 10 * mt_rand(8, 12); + $box = imageftbbox($size, $angle, $font, $this->response[$i]); + + // Calculate character starting coordinates + $x = $spacing / 4 + $i * $spacing; + $y = Captcha::$config['height'] / 2 + ($box[2] - $box[5]) / 4; + + // Draw captcha text character + // Allocate random color, size and rotation attributes to text + $color = imagecolorallocate($this->image, mt_rand(150, 255), mt_rand(200, 255), mt_rand(0, 255)); + + // Write text character to image + imagefttext($this->image, $size, $angle, $x, $y, $color, $font, $this->response[$i]); + + // Draw "ghost" alphabetic character + $text_color = imagecolorallocatealpha($this->image, mt_rand($color_limit + 8, 255), mt_rand($color_limit + 8, 255), mt_rand($color_limit + 8, 255), mt_rand(70, 120)); + $char = substr($chars, mt_rand(0, 14), 1); + imagettftext($this->image, $size * 2, mt_rand(-45, 45), ($x - (mt_rand(5, 10))), ($y + (mt_rand(5, 10))), $text_color, $font, $char); + } + + // Output + return $this->image_render($html); + } + +} // End Captcha Alpha Driver Class
\ No newline at end of file diff --git a/system/libraries/drivers/Captcha/Basic.php b/system/libraries/drivers/Captcha/Basic.php new file mode 100644 index 00000000..d212e72c --- /dev/null +++ b/system/libraries/drivers/Captcha/Basic.php @@ -0,0 +1,81 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Captcha driver for "basic" style. + * + * $Id: Basic.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Basic_Driver extends Captcha_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Complexity setting is used as character count + return text::random('distinct', max(1, Captcha::$config['complexity'])); + } + + /** + * Outputs the Captcha image. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + // Creates $this->image + $this->image_create(Captcha::$config['background']); + + // Add a random gradient + if (empty(Captcha::$config['background'])) + { + $color1 = imagecolorallocate($this->image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(150, 255)); + $color2 = imagecolorallocate($this->image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(150, 255)); + $this->image_gradient($color1, $color2); + } + + // Add a few random lines + for ($i = 0, $count = mt_rand(5, Captcha::$config['complexity'] * 4); $i < $count; $i++) + { + $color = imagecolorallocatealpha($this->image, mt_rand(0, 255), mt_rand(0, 255), mt_rand(100, 255), mt_rand(50, 120)); + imageline($this->image, mt_rand(0, Captcha::$config['width']), 0, mt_rand(0, Captcha::$config['width']), Captcha::$config['height'], $color); + } + + // Calculate character font-size and spacing + $default_size = min(Captcha::$config['width'], Captcha::$config['height'] * 2) / (strlen($this->response) + 1); + $spacing = (int) (Captcha::$config['width'] * 0.9 / strlen($this->response)); + + // Draw each Captcha character with varying attributes + for ($i = 0, $strlen = strlen($this->response); $i < $strlen; $i++) + { + // Use different fonts if available + $font = Captcha::$config['fontpath'].Captcha::$config['fonts'][array_rand(Captcha::$config['fonts'])]; + + // Allocate random color, size and rotation attributes to text + $color = imagecolorallocate($this->image, mt_rand(0, 150), mt_rand(0, 150), mt_rand(0, 150)); + $angle = mt_rand(-40, 20); + + // Scale the character size on image height + $size = $default_size / 10 * mt_rand(8, 12); + $box = imageftbbox($size, $angle, $font, $this->response[$i]); + + // Calculate character starting coordinates + $x = $spacing / 4 + $i * $spacing; + $y = Captcha::$config['height'] / 2 + ($box[2] - $box[5]) / 4; + + // Write text character to image + imagefttext($this->image, $size, $angle, $x, $y, $color, $font, $this->response[$i]); + } + + // Output + return $this->image_render($html); + } + +} // End Captcha Basic Driver Class
\ No newline at end of file diff --git a/system/libraries/drivers/Captcha/Black.php b/system/libraries/drivers/Captcha/Black.php new file mode 100644 index 00000000..6a2e2226 --- /dev/null +++ b/system/libraries/drivers/Captcha/Black.php @@ -0,0 +1,72 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Captcha driver for "black" style. + * + * $Id: Black.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Black_Driver extends Captcha_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Complexity setting is used as character count + return text::random('distinct', max(1, ceil(Captcha::$config['complexity'] / 1.5))); + } + + /** + * Outputs the Captcha image. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + // Creates a black image to start from + $this->image_create(Captcha::$config['background']); + + // Add random white/gray arcs, amount depends on complexity setting + $count = (Captcha::$config['width'] + Captcha::$config['height']) / 2; + $count = $count / 5 * min(10, Captcha::$config['complexity']); + for ($i = 0; $i < $count; $i++) + { + imagesetthickness($this->image, mt_rand(1, 2)); + $color = imagecolorallocatealpha($this->image, 255, 255, 255, mt_rand(0, 120)); + imagearc($this->image, mt_rand(-Captcha::$config['width'], Captcha::$config['width']), mt_rand(-Captcha::$config['height'], Captcha::$config['height']), mt_rand(-Captcha::$config['width'], Captcha::$config['width']), mt_rand(-Captcha::$config['height'], Captcha::$config['height']), mt_rand(0, 360), mt_rand(0, 360), $color); + } + + // Use different fonts if available + $font = Captcha::$config['fontpath'].Captcha::$config['fonts'][array_rand(Captcha::$config['fonts'])]; + + // Draw the character's white shadows + $size = (int) min(Captcha::$config['height'] / 2, Captcha::$config['width'] * 0.8 / strlen($this->response)); + $angle = mt_rand(-15 + strlen($this->response), 15 - strlen($this->response)); + $x = mt_rand(1, Captcha::$config['width'] * 0.9 - $size * strlen($this->response)); + $y = ((Captcha::$config['height'] - $size) / 2) + $size; + $color = imagecolorallocate($this->image, 255, 255, 255); + imagefttext($this->image, $size, $angle, $x + 1, $y + 1, $color, $font, $this->response); + + // Add more shadows for lower complexities + (Captcha::$config['complexity'] < 10) and imagefttext($this->image, $size, $angle, $x - 1, $y - 1, $color, $font , $this->response); + (Captcha::$config['complexity'] < 8) and imagefttext($this->image, $size, $angle, $x - 2, $y + 2, $color, $font , $this->response); + (Captcha::$config['complexity'] < 6) and imagefttext($this->image, $size, $angle, $x + 2, $y - 2, $color, $font , $this->response); + (Captcha::$config['complexity'] < 4) and imagefttext($this->image, $size, $angle, $x + 3, $y + 3, $color, $font , $this->response); + (Captcha::$config['complexity'] < 2) and imagefttext($this->image, $size, $angle, $x - 3, $y - 3, $color, $font , $this->response); + + // Finally draw the foreground characters + $color = imagecolorallocate($this->image, 0, 0, 0); + imagefttext($this->image, $size, $angle, $x, $y, $color, $font, $this->response); + + // Output + return $this->image_render($html); + } + +} // End Captcha Black Driver Class
\ No newline at end of file diff --git a/system/libraries/drivers/Captcha/Math.php b/system/libraries/drivers/Captcha/Math.php new file mode 100644 index 00000000..4ac20248 --- /dev/null +++ b/system/libraries/drivers/Captcha/Math.php @@ -0,0 +1,61 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Captcha driver for "math" style. + * + * $Id: Math.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Math_Driver extends Captcha_Driver { + + private $math_exercice; + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Easy + if (Captcha::$config['complexity'] < 4) + { + $numbers[] = mt_rand(1, 5); + $numbers[] = mt_rand(1, 4); + } + // Normal + elseif (Captcha::$config['complexity'] < 7) + { + $numbers[] = mt_rand(10, 20); + $numbers[] = mt_rand(1, 10); + } + // Difficult, well, not really ;) + else + { + $numbers[] = mt_rand(100, 200); + $numbers[] = mt_rand(10, 20); + $numbers[] = mt_rand(1, 10); + } + + // Store the question for output + $this->math_exercice = implode(' + ', $numbers).' = '; + + // Return the answer + return array_sum($numbers); + } + + /** + * Outputs the Captcha riddle. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + return $this->math_exercice; + } + +} // End Captcha Math Driver Class
\ No newline at end of file diff --git a/system/libraries/drivers/Captcha/Riddle.php b/system/libraries/drivers/Captcha/Riddle.php new file mode 100644 index 00000000..765eeaad --- /dev/null +++ b/system/libraries/drivers/Captcha/Riddle.php @@ -0,0 +1,47 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Captcha driver for "riddle" style. + * + * $Id: Riddle.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Riddle_Driver extends Captcha_Driver { + + private $riddle; + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Load riddles from the current language + $riddles = Kohana::lang('captcha.riddles'); + + // Pick a random riddle + $riddle = $riddles[array_rand($riddles)]; + + // Store the question for output + $this->riddle = $riddle[0]; + + // Return the answer + return $riddle[1]; + } + + /** + * Outputs the Captcha riddle. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + return $this->riddle; + } + +} // End Captcha Riddle Driver Class
\ No newline at end of file diff --git a/system/libraries/drivers/Captcha/Word.php b/system/libraries/drivers/Captcha/Word.php new file mode 100644 index 00000000..856bd9b4 --- /dev/null +++ b/system/libraries/drivers/Captcha/Word.php @@ -0,0 +1,37 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Captcha driver for "word" style. + * + * $Id: Word.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Word_Driver extends Captcha_Basic_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Load words from the current language and randomize them + $words = Kohana::lang('captcha.words'); + shuffle($words); + + // Loop over each word... + foreach ($words as $word) + { + // ...until we find one of the desired length + if (abs(Captcha::$config['complexity'] - strlen($word)) < 2) + return strtoupper($word); + } + + // Return any random word as final fallback + return strtoupper($words[array_rand($words)]); + } + +} // End Captcha Word Driver Class
\ No newline at end of file diff --git a/system/libraries/drivers/Database.php b/system/libraries/drivers/Database.php new file mode 100644 index 00000000..807469f6 --- /dev/null +++ b/system/libraries/drivers/Database.php @@ -0,0 +1,636 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Database API driver + * + * $Id: Database.php 4343 2009-05-08 17:04:48Z jheathco $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Database_Driver { + + protected $query_cache; + + /** + * Connect to our database. + * Returns FALSE on failure or a MySQL resource. + * + * @return mixed + */ + abstract public function connect(); + + /** + * Perform a query based on a manually written query. + * + * @param string SQL query to execute + * @return Database_Result + */ + abstract public function query($sql); + + /** + * Builds a DELETE query. + * + * @param string table name + * @param array where clause + * @return string + */ + public function delete($table, $where) + { + return 'DELETE FROM '.$this->escape_table($table).' WHERE '.implode(' ', $where); + } + + /** + * Builds an UPDATE query. + * + * @param string table name + * @param array key => value pairs + * @param array where clause + * @return string + */ + public function update($table, $values, $where) + { + foreach ($values as $key => $val) + { + $valstr[] = $this->escape_column($key).' = '.$val; + } + return 'UPDATE '.$this->escape_table($table).' SET '.implode(', ', $valstr).' WHERE '.implode(' ',$where); + } + + /** + * Set the charset using 'SET NAMES <charset>'. + * + * @param string character set to use + */ + public function set_charset($charset) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Wrap the tablename in backticks, has support for: table.field syntax. + * + * @param string table name + * @return string + */ + abstract public function escape_table($table); + + /** + * Escape a column/field name, has support for special commands. + * + * @param string column name + * @return string + */ + abstract public function escape_column($column); + + /** + * Builds a WHERE portion of a query. + * + * @param mixed key + * @param string value + * @param string type + * @param int number of where clauses + * @param boolean escape the value + * @return string + */ + public function where($key, $value, $type, $num_wheres, $quote) + { + $prefix = ($num_wheres == 0) ? '' : $type; + + if ($quote === -1) + { + $value = ''; + } + else + { + if ($value === NULL) + { + if ( ! $this->has_operator($key)) + { + $key .= ' IS'; + } + + $value = ' NULL'; + } + elseif (is_bool($value)) + { + if ( ! $this->has_operator($key)) + { + $key .= ' ='; + } + + $value = ($value == TRUE) ? ' 1' : ' 0'; + } + else + { + if ( ! $this->has_operator($key) AND ! empty($key)) + { + $key = $this->escape_column($key).' ='; + } + else + { + preg_match('/^(.+?)([<>!=]+|\bIS(?:\s+NULL))\s*$/i', $key, $matches); + if (isset($matches[1]) AND isset($matches[2])) + { + $key = $this->escape_column(trim($matches[1])).' '.trim($matches[2]); + } + } + + $value = ' '.(($quote == TRUE) ? $this->escape($value) : $value); + } + } + + return $prefix.$key.$value; + } + + /** + * Builds a LIKE portion of a query. + * + * @param mixed field name + * @param string value to match with field + * @param boolean add wildcards before and after the match + * @param string clause type (AND or OR) + * @param int number of likes + * @return string + */ + public function like($field, $match, $auto, $type, $num_likes) + { + $prefix = ($num_likes == 0) ? '' : $type; + + $match = $this->escape_str($match); + + if ($auto === TRUE) + { + // Add the start and end quotes + $match = '%'.str_replace('%', '\\%', $match).'%'; + } + + return $prefix.' '.$this->escape_column($field).' LIKE \''.$match . '\''; + } + + /** + * Builds a NOT LIKE portion of a query. + * + * @param mixed field name + * @param string value to match with field + * @param string clause type (AND or OR) + * @param int number of likes + * @return string + */ + public function notlike($field, $match, $auto, $type, $num_likes) + { + $prefix = ($num_likes == 0) ? '' : $type; + + $match = $this->escape_str($match); + + if ($auto === TRUE) + { + // Add the start and end quotes + $match = '%'.$match.'%'; + } + + return $prefix.' '.$this->escape_column($field).' NOT LIKE \''.$match.'\''; + } + + /** + * Builds a REGEX portion of a query. + * + * @param string field name + * @param string value to match with field + * @param string clause type (AND or OR) + * @param integer number of regexes + * @return string + */ + public function regex($field, $match, $type, $num_regexs) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Builds a NOT REGEX portion of a query. + * + * @param string field name + * @param string value to match with field + * @param string clause type (AND or OR) + * @param integer number of regexes + * @return string + */ + public function notregex($field, $match, $type, $num_regexs) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Builds an INSERT query. + * + * @param string table name + * @param array keys + * @param array values + * @return string + */ + public function insert($table, $keys, $values) + { + // Escape the column names + foreach ($keys as $key => $value) + { + $keys[$key] = $this->escape_column($value); + } + return 'INSERT INTO '.$this->escape_table($table).' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + /** + * Builds a MERGE portion of a query. + * + * @param string table name + * @param array keys + * @param array values + * @return string + */ + public function merge($table, $keys, $values) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Builds a LIMIT portion of a query. + * + * @param integer limit + * @param integer offset + * @return string + */ + abstract public function limit($limit, $offset = 0); + + /** + * Creates a prepared statement. + * + * @param string SQL query + * @return Database_Stmt + */ + public function stmt_prepare($sql = '') + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Compiles the SELECT statement. + * Generates a query string based on which functions were used. + * Should not be called directly, the get() function calls it. + * + * @param array select query values + * @return string + */ + abstract public function compile_select($database); + + /** + * Determines if the string has an arithmetic operator in it. + * + * @param string string to check + * @return boolean + */ + public function has_operator($str) + { + return (bool) preg_match('/[<>!=]|\sIS(?:\s+NOT\s+)?\b|BETWEEN/i', trim($str)); + } + + /** + * Escapes any input value. + * + * @param mixed value to escape + * @return string + */ + public function escape($value) + { + if ( ! $this->db_config['escape']) + return $value; + + switch (gettype($value)) + { + case 'string': + $value = '\''.$this->escape_str($value).'\''; + break; + case 'boolean': + $value = (int) $value; + break; + case 'double': + // Convert to non-locale aware float to prevent possible commas + $value = sprintf('%F', $value); + break; + default: + $value = ($value === NULL) ? 'NULL' : $value; + break; + } + + return (string) $value; + } + + /** + * Escapes a string for a query. + * + * @param mixed value to escape + * @return string + */ + abstract public function escape_str($str); + + /** + * Lists all tables in the database. + * + * @return array + */ + abstract public function list_tables(); + + /** + * Lists all fields in a table. + * + * @param string table name + * @return array + */ + abstract function list_fields($table); + + /** + * Returns the last database error. + * + * @return string + */ + abstract public function show_error(); + + /** + * Returns field data about a table. + * + * @param string table name + * @return array + */ + abstract public function field_data($table); + + /** + * Fetches SQL type information about a field, in a generic format. + * + * @param string field datatype + * @return array + */ + protected function sql_type($str) + { + static $sql_types; + + if ($sql_types === NULL) + { + // Load SQL data types + $sql_types = Kohana::config('sql_types'); + } + + $str = strtolower(trim($str)); + + if (($open = strpos($str, '(')) !== FALSE) + { + // Find closing bracket + $close = strpos($str, ')', $open) - 1; + + // Find the type without the size + $type = substr($str, 0, $open); + } + else + { + // No length + $type = $str; + } + + empty($sql_types[$type]) and exit + ( + 'Unknown field type: '.$type.'. '. + 'Please report this: http://trac.kohanaphp.com/newticket' + ); + + // Fetch the field definition + $field = $sql_types[$type]; + + switch ($field['type']) + { + case 'string': + case 'float': + if (isset($close)) + { + // Add the length to the field info + $field['length'] = substr($str, $open + 1, $close - $open); + } + break; + case 'int': + // Add unsigned value + $field['unsigned'] = (strpos($str, 'unsigned') !== FALSE); + break; + } + + return $field; + } + + /** + * Clears the internal query cache. + * + * @param string SQL query + */ + public function clear_cache($sql = NULL) + { + if (empty($sql)) + { + $this->query_cache = array(); + } + else + { + unset($this->query_cache[$this->query_hash($sql)]); + } + + Kohana::log('debug', 'Database cache cleared: '.get_class($this)); + } + + /** + * Creates a hash for an SQL query string. Replaces newlines with spaces, + * trims, and hashes. + * + * @param string SQL query + * @return string + */ + protected function query_hash($sql) + { + return sha1(str_replace("\n", ' ', trim($sql))); + } + +} // End Database Driver Interface + +/** + * Database_Result + * + */ +abstract class Database_Result implements ArrayAccess, Iterator, Countable { + + // Result resource, insert id, and SQL + protected $result; + protected $insert_id; + protected $sql; + + // Current and total rows + protected $current_row = 0; + protected $total_rows = 0; + + // Fetch function and return type + protected $fetch_type; + protected $return_type; + + /** + * Returns the SQL used to fetch the result. + * + * @return string + */ + public function sql() + { + return $this->sql; + } + + /** + * Returns the insert id from the result. + * + * @return mixed + */ + public function insert_id() + { + return $this->insert_id; + } + + /** + * Prepares the query result. + * + * @param boolean return rows as objects + * @param mixed type + * @return Database_Result + */ + abstract function result($object = TRUE, $type = FALSE); + + /** + * Builds an array of query results. + * + * @param boolean return rows as objects + * @param mixed type + * @return array + */ + abstract function result_array($object = NULL, $type = FALSE); + + /** + * Gets the fields of an already run query. + * + * @return array + */ + abstract public function list_fields(); + + /** + * Seek to an offset in the results. + * + * @return boolean + */ + abstract public function seek($offset); + + /** + * Countable: count + */ + public function count() + { + return $this->total_rows; + } + + /** + * ArrayAccess: offsetExists + */ + public function offsetExists($offset) + { + if ($this->total_rows > 0) + { + $min = 0; + $max = $this->total_rows - 1; + + return ! ($offset < $min OR $offset > $max); + } + + return FALSE; + } + + /** + * ArrayAccess: offsetGet + */ + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return FALSE; + + // Return the row by calling the defined fetching callback + return call_user_func($this->fetch_type, $this->result, $this->return_type); + } + + /** + * ArrayAccess: offsetSet + * + * @throws Kohana_Database_Exception + */ + final public function offsetSet($offset, $value) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + + /** + * ArrayAccess: offsetUnset + * + * @throws Kohana_Database_Exception + */ + final public function offsetUnset($offset) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + + /** + * Iterator: current + */ + public function current() + { + return $this->offsetGet($this->current_row); + } + + /** + * Iterator: key + */ + public function key() + { + return $this->current_row; + } + + /** + * Iterator: next + */ + public function next() + { + ++$this->current_row; + return $this; + } + + /** + * Iterator: prev + */ + public function prev() + { + --$this->current_row; + return $this; + } + + /** + * Iterator: rewind + */ + public function rewind() + { + $this->current_row = 0; + return $this; + } + + /** + * Iterator: valid + */ + public function valid() + { + return $this->offsetExists($this->current_row); + } + +} // End Database Result Interface diff --git a/system/libraries/drivers/Database/Mssql.php b/system/libraries/drivers/Database/Mssql.php new file mode 100644 index 00000000..6947679a --- /dev/null +++ b/system/libraries/drivers/Database/Mssql.php @@ -0,0 +1,462 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * MSSQL Database Driver + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Mssql_Driver extends Database_Driver +{ + /** + * Database connection link + */ + protected $link; + + /** + * Database configuration + */ + protected $db_config; + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'MSSQL Database Driver Initialized'); + } + + /** + * Closes the database connection. + */ + public function __destruct() + { + is_resource($this->link) and mssql_close($this->link); + } + + /** + * Make the connection + * + * @return return connection + */ + public function connect() + { + // Check if link already exists + if (is_resource($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Persistent connections enabled? + $connect = ($this->db_config['persistent'] == TRUE) ? 'mssql_pconnect' : 'mssql_connect'; + + // Build the connection info + $host = isset($host) ? $host : $socket; + + // Windows uses a comma instead of a colon + $port = (isset($port) AND is_string($port)) ? (KOHANA_IS_WIN ? ',' : ':').$port : ''; + + // Make the connection and select the database + if (($this->link = $connect($host.$port, $user, $pass, TRUE)) AND mssql_select_db($database, $this->link)) + { + /* This is being removed so I can use it, will need to come up with a more elegant workaround in the future... + * + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + */ + + // Clear password after successful connect + $this->db_config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|REPLACE|SET)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset($this->query_cache[$hash])) + { + // Set the cached object + $this->query_cache[$hash] = new Mssql_Result(mssql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + else + { + // Rewind cached result + $this->query_cache[$hash]->rewind(); + } + + // Return the cached query + return $this->query_cache[$hash]; + } + + return new Mssql_Result(mssql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + + public function escape_table($table) + { + if (stripos($table, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $table = str_ireplace(' AS ', ' AS ', $table); + + // Runs escape_table on both sides of an AS statement + $table = array_map(array($this, __FUNCTION__), explode(' AS ', $table)); + + // Re-create the AS statement + return implode(' AS ', $table); + } + return '['.str_replace('.', '[.]', $table).']'; + } + + public function escape_column($column) + { + if (!$this->db_config['escape']) + return $column; + + if ($column == '*') + return $column; + + // This matches any functions we support to SELECT. + if ( preg_match('/(avg|count|sum|max|min)\(\s*(.*)\s*\)(\s*as\s*(.+)?)?/i', $column, $matches)) + { + if ( count($matches) == 3) + { + return $matches[1].'('.$this->escape_column($matches[2]).')'; + } + else if ( count($matches) == 5) + { + return $matches[1].'('.$this->escape_column($matches[2]).') AS '.$this->escape_column($matches[2]); + } + } + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:rand|all|distinct(?:row)?|high_priority|sql_(?:small_result|b(?:ig_result|uffer_result)|no_cache|ca(?:che|lc_found_rows)))\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '[$0]', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '[$0]', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + /** + * Limit in SQL Server 2000 only uses the keyword + * 'TOP'; 2007 may have an offset keyword, but + * I am unsure - for pagination style limit,offset + * functionality, a fancy query needs to be built. + * + * @param unknown_type $limit + * @return unknown + */ + public function limit($limit, $offset=null) + { + return 'TOP '.$limit; + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + // Escape the tables + $froms = array(); + foreach ($database['from'] as $from) + $froms[] = $this->escape_column($from); + $sql .= "\nFROM "; + $sql .= implode(', ', $froms); + } + + if (count($database['join']) > 0) + { + foreach($database['join'] AS $join) + { + $sql .= "\n".$join['type'].'JOIN '.implode(', ', $join['tables']).' ON '.$join['conditions']; + } + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit']); + } + + return $sql; + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_resource($this->link) or $this->connect(); + //mssql_real_escape_string($str, $this->link); <-- this function doesn't exist + + $characters = array('/\x00/', '/\x1a/', '/\n/', '/\r/', '/\\\/', '/\'/'); + $replace = array('\\\x00', '\\x1a', '\\n', '\\r', '\\\\', "''"); + return preg_replace($characters, $replace, $str); + } + + public function list_tables() + { + $sql = 'SHOW TABLES FROM ['.$this->db_config['connection']['database'].']'; + $result = $this->query($sql)->result(FALSE, MSSQL_ASSOC); + + $retval = array(); + foreach ($result as $row) + { + $retval[] = current($row); + } + + return $retval; + } + + public function show_error() + { + return mssql_get_last_message($this->link); + } + + public function list_fields($table) + { + $result = array(); + + foreach ($this->field_data($table) as $row) + { + // Make an associative array + $result[$row->Field] = $this->sql_type($row->Type); + } + + return $result; + } + + public function field_data($table) + { + $query = $this->query('SHOW COLUMNS FROM '.$this->escape_table($table), $this->link); + + return $query->result_array(TRUE); + } +} + +/** + * MSSQL Result + */ +class Mssql_Result extends Database_Result { + + // Fetch function and return type + protected $fetch_type = 'mssql_fetch_object'; + protected $return_type = MSSQL_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + $this->result = $result; + + // If the query is a resource, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_resource($result)) + { + $this->current_row = 0; + $this->total_rows = mssql_num_rows($this->result); + $this->fetch_type = ($object === TRUE) ? 'mssql_fetch_object' : 'mssql_fetch_array'; + } + elseif (is_bool($result)) + { + if ($result == FALSE) + { + // SQL error + throw new Kohana_Database_Exception('database.error', mssql_get_last_message($link).' - '.$sql); + } + else + { + // Its an DELETE, INSERT, REPLACE, or UPDATE querys + $last_id = mssql_query('SELECT @@IDENTITY AS last_id', $link); + $result = mssql_fetch_assoc($last_id); + $this->insert_id = $result['last_id']; + $this->total_rows = mssql_rows_affected($link); + } + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Destruct, the cleanup crew! + */ + public function __destruct() + { + if (is_resource($this->result)) + { + mssql_free_result($this->result); + } + } + + public function result($object = TRUE, $type = MSSQL_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'mssql_fetch_object' : 'mssql_fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'mssql_fetch_object') + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = MSSQL_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = MSSQL_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'mssql_fetch_object'; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'mssql_fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'mssql_fetch_object') + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + + if (mssql_num_rows($this->result)) + { + // Reset the pointer location to make sure things work properly + mssql_data_seek($this->result, 0); + + while ($row = $fetch($this->result, $type)) + { + $rows[] = $row; + } + } + + return isset($rows) ? $rows : array(); + } + + public function list_fields() + { + $field_names = array(); + while ($field = mssql_fetch_field($this->result)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + public function seek($offset) + { + if ( ! $this->offsetExists($offset)) + return FALSE; + + return mssql_data_seek($this->result, $offset); + } + +} // End mssql_Result Class diff --git a/system/libraries/drivers/Database/Mysql.php b/system/libraries/drivers/Database/Mysql.php new file mode 100644 index 00000000..d5222f50 --- /dev/null +++ b/system/libraries/drivers/Database/Mysql.php @@ -0,0 +1,496 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * MySQL Database Driver + * + * $Id: Mysql.php 4344 2009-05-11 16:41:39Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Mysql_Driver extends Database_Driver { + + /** + * Database connection link + */ + protected $link; + + /** + * Database configuration + */ + protected $db_config; + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'MySQL Database Driver Initialized'); + } + + /** + * Closes the database connection. + */ + public function __destruct() + { + is_resource($this->link) and mysql_close($this->link); + } + + public function connect() + { + // Check if link already exists + if (is_resource($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Persistent connections enabled? + $connect = ($this->db_config['persistent'] == TRUE) ? 'mysql_pconnect' : 'mysql_connect'; + + // Build the connection info + $host = isset($host) ? $host : $socket; + $port = isset($port) ? ':'.$port : ''; + + // Make the connection and select the database + if (($this->link = $connect($host.$port, $user, $pass, TRUE)) AND mysql_select_db($database, $this->link)) + { + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + + // Clear password after successful connect + $this->db_config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|REPLACE|SET|DELETE|TRUNCATE)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset($this->query_cache[$hash])) + { + // Set the cached object + $this->query_cache[$hash] = new Mysql_Result(mysql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + else + { + // Rewind cached result + $this->query_cache[$hash]->rewind(); + } + + // Return the cached query + return $this->query_cache[$hash]; + } + + return new Mysql_Result(mysql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + $this->query('SET NAMES '.$this->escape_str($charset)); + } + + public function escape_table($table) + { + if (!$this->db_config['escape']) + return $table; + + if (stripos($table, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $table = str_ireplace(' AS ', ' AS ', $table); + + // Runs escape_table on both sides of an AS statement + $table = array_map(array($this, __FUNCTION__), explode(' AS ', $table)); + + // Re-create the AS statement + return implode(' AS ', $table); + } + return '`'.str_replace('.', '`.`', $table).'`'; + } + + public function escape_column($column) + { + if (!$this->db_config['escape']) + return $column; + + if ($column == '*') + return $column; + + // This matches any functions we support to SELECT. + if ( preg_match('/(avg|count|sum|max|min)\(\s*(.*)\s*\)(\s*as\s*(.+)?)?/i', $column, $matches)) + { + if ( count($matches) == 3) + { + return $matches[1].'('.$this->escape_column($matches[2]).')'; + } + else if ( count($matches) == 5) + { + return $matches[1].'('.$this->escape_column($matches[2]).') AS '.$this->escape_column($matches[2]); + } + } + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:rand|all|distinct(?:row)?|high_priority|sql_(?:small_result|b(?:ig_result|uffer_result)|no_cache|ca(?:che|lc_found_rows)))\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '`$0`', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '`$0`', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + public function regex($field, $match, $type, $num_regexs) + { + $prefix = ($num_regexs == 0) ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' REGEXP \''.$this->escape_str($match).'\''; + } + + public function notregex($field, $match, $type, $num_regexs) + { + $prefix = $num_regexs == 0 ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' NOT REGEXP \''.$this->escape_str($match) . '\''; + } + + public function merge($table, $keys, $values) + { + // Escape the column names + foreach ($keys as $key => $value) + { + $keys[$key] = $this->escape_column($value); + } + return 'REPLACE INTO '.$this->escape_table($table).' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + public function limit($limit, $offset = 0) + { + return 'LIMIT '.$offset.', '.$limit; + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + // Escape the tables + $froms = array(); + foreach ($database['from'] as $from) + { + $froms[] = $this->escape_column($from); + } + $sql .= "\nFROM ("; + $sql .= implode(', ', $froms).")"; + } + + if (count($database['join']) > 0) + { + foreach($database['join'] AS $join) + { + $sql .= "\n".$join['type'].'JOIN '.implode(', ', $join['tables']).' ON '.$join['conditions']; + } + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit'], $database['offset']); + } + + return $sql; + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_resource($this->link) or $this->connect(); + + return mysql_real_escape_string($str, $this->link); + } + + public function list_tables() + { + $tables = array(); + + if ($query = $this->query('SHOW TABLES FROM '.$this->escape_table($this->db_config['connection']['database']))) + { + foreach ($query->result(FALSE) as $row) + { + $tables[] = current($row); + } + } + + return $tables; + } + + public function show_error() + { + return mysql_error($this->link); + } + + public function list_fields($table) + { + $result = NULL; + + foreach ($this->field_data($table) as $row) + { + // Make an associative array + $result[$row->Field] = $this->sql_type($row->Type); + + if ($row->Key === 'PRI' AND $row->Extra === 'auto_increment') + { + // For sequenced (AUTO_INCREMENT) tables + $result[$row->Field]['sequenced'] = TRUE; + } + + if ($row->Null === 'YES') + { + // Set NULL status + $result[$row->Field]['null'] = TRUE; + } + } + + if (!isset($result)) + throw new Kohana_Database_Exception('database.table_not_found', $table); + + return $result; + } + + public function field_data($table) + { + $result = $this->query('SHOW COLUMNS FROM '.$this->escape_table($table)); + + return $result->result_array(TRUE); + } + +} // End Database_Mysql_Driver Class + +/** + * MySQL Result + */ +class Mysql_Result extends Database_Result { + + // Fetch function and return type + protected $fetch_type = 'mysql_fetch_object'; + protected $return_type = MYSQL_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + $this->result = $result; + + // If the query is a resource, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_resource($result)) + { + $this->current_row = 0; + $this->total_rows = mysql_num_rows($this->result); + $this->fetch_type = ($object === TRUE) ? 'mysql_fetch_object' : 'mysql_fetch_array'; + } + elseif (is_bool($result)) + { + if ($result == FALSE) + { + // SQL error + throw new Kohana_Database_Exception('database.error', mysql_error($link).' - '.$sql); + } + else + { + // Its an DELETE, INSERT, REPLACE, or UPDATE query + $this->insert_id = mysql_insert_id($link); + $this->total_rows = mysql_affected_rows($link); + } + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Destruct, the cleanup crew! + */ + public function __destruct() + { + if (is_resource($this->result)) + { + mysql_free_result($this->result); + } + } + + public function result($object = TRUE, $type = MYSQL_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'mysql_fetch_object' : 'mysql_fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'mysql_fetch_object' AND $object === TRUE) + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = MYSQL_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = MYSQL_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'mysql_fetch_object'; + + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'mysql_fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'mysql_fetch_object') + { + $type = (is_string($this->return_type) AND Kohana::auto_load($this->return_type)) ? $this->return_type : 'stdClass'; + } + } + + if (mysql_num_rows($this->result)) + { + // Reset the pointer location to make sure things work properly + mysql_data_seek($this->result, 0); + + while ($row = $fetch($this->result, $type)) + { + $rows[] = $row; + } + } + + return isset($rows) ? $rows : array(); + } + + public function list_fields() + { + $field_names = array(); + while ($field = mysql_fetch_field($this->result)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + public function seek($offset) + { + if ($this->offsetExists($offset) AND mysql_data_seek($this->result, $offset)) + { + // Set the current row to the offset + $this->current_row = $offset; + + return TRUE; + } + else + { + return FALSE; + } + } + +} // End Mysql_Result Class diff --git a/system/libraries/drivers/Database/Mysqli.php b/system/libraries/drivers/Database/Mysqli.php new file mode 100644 index 00000000..0dd9f05c --- /dev/null +++ b/system/libraries/drivers/Database/Mysqli.php @@ -0,0 +1,358 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * MySQLi Database Driver + * + * $Id: Mysqli.php 4344 2009-05-11 16:41:39Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Mysqli_Driver extends Database_Mysql_Driver { + + // Database connection link + protected $link; + protected $db_config; + protected $statements = array(); + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'MySQLi Database Driver Initialized'); + } + + /** + * Closes the database connection. + */ + public function __destruct() + { + is_object($this->link) and $this->link->close(); + } + + public function connect() + { + // Check if link already exists + if (is_object($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Build the connection info + $host = isset($host) ? $host : $socket; + + // Make the connection and select the database + if ($this->link = new mysqli($host, $user, $pass, $database, $port)) + { + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + + // Clear password after successful connect + $this->db_config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|REPLACE|SET|DELETE|TRUNCATE)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset($this->query_cache[$hash])) + { + // Set the cached object + $this->query_cache[$hash] = new Kohana_Mysqli_Result($this->link, $this->db_config['object'], $sql); + } + else + { + // Rewind cached result + $this->query_cache[$hash]->rewind(); + } + + // Return the cached query + return $this->query_cache[$hash]; + } + + return new Kohana_Mysqli_Result($this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + if ($this->link->set_charset($charset) === FALSE) + throw new Kohana_Database_Exception('database.error', $this->show_error()); + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_object($this->link) or $this->connect(); + + return $this->link->real_escape_string($str); + } + + public function show_error() + { + return $this->link->error; + } + +} // End Database_Mysqli_Driver Class + +/** + * MySQLi Result + */ +class Kohana_Mysqli_Result extends Database_Result { + + // Database connection + protected $link; + + // Data fetching types + protected $fetch_type = 'mysqli_fetch_object'; + protected $return_type = MYSQLI_ASSOC; + + /** + * Sets up the result variables. + * + * @param object database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($link, $object = TRUE, $sql) + { + $this->link = $link; + + if ( ! $this->link->multi_query($sql)) + { + // SQL error + throw new Kohana_Database_Exception('database.error', $this->link->error.' - '.$sql); + } + else + { + $this->result = $this->link->store_result(); + + // If the query is an object, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_object($this->result)) + { + $this->current_row = 0; + $this->total_rows = $this->result->num_rows; + $this->fetch_type = ($object === TRUE) ? 'fetch_object' : 'fetch_array'; + } + elseif ($this->link->error) + { + // SQL error + throw new Kohana_Database_Exception('database.error', $this->link->error.' - '.$sql); + } + else + { + // Its an DELETE, INSERT, REPLACE, or UPDATE query + $this->insert_id = $this->link->insert_id; + $this->total_rows = $this->link->affected_rows; + } + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Magic __destruct function, frees the result. + */ + public function __destruct() + { + if (is_object($this->result)) + { + $this->result->free_result(); + + // this is kinda useless, but needs to be done to avoid the "Commands out of sync; you + // can't run this command now" error. Basically, we get all results after the first one + // (the one we actually need) and free them. + if (is_resource($this->link) AND $this->link->more_results()) + { + do + { + if ($result = $this->link->store_result()) + { + $result->free_result(); + } + } while ($this->link->next_result()); + } + } + } + + public function result($object = TRUE, $type = MYSQLI_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'fetch_object' : 'fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'fetch_object') + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = MYSQLI_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = MYSQLI_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'fetch_object'; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'fetch_object') + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + + if ($this->result->num_rows) + { + // Reset the pointer location to make sure things work properly + $this->result->data_seek(0); + + while ($row = $this->result->$fetch($type)) + { + $rows[] = $row; + } + } + + return isset($rows) ? $rows : array(); + } + + public function list_fields() + { + $field_names = array(); + while ($field = $this->result->fetch_field()) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + public function seek($offset) + { + if ($this->offsetExists($offset) AND $this->result->data_seek($offset)) + { + // Set the current row to the offset + $this->current_row = $offset; + + return TRUE; + } + + return FALSE; + } + + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return FALSE; + + // Return the row + $fetch = $this->fetch_type; + return $this->result->$fetch($this->return_type); + } + +} // End Mysqli_Result Class + +/** + * MySQLi Prepared Statement (experimental) + */ +class Kohana_Mysqli_Statement { + + protected $link = NULL; + protected $stmt; + protected $var_names = array(); + protected $var_values = array(); + + public function __construct($sql, $link) + { + $this->link = $link; + + $this->stmt = $this->link->prepare($sql); + + return $this; + } + + public function __destruct() + { + $this->stmt->close(); + } + + // Sets the bind parameters + public function bind_params($param_types, $params) + { + $this->var_names = array_keys($params); + $this->var_values = array_values($params); + call_user_func_array(array($this->stmt, 'bind_param'), array_merge($param_types, $var_names)); + + return $this; + } + + public function bind_result($params) + { + call_user_func_array(array($this->stmt, 'bind_result'), $params); + } + + // Runs the statement + public function execute() + { + foreach ($this->var_names as $key => $name) + { + $$name = $this->var_values[$key]; + } + $this->stmt->execute(); + return $this->stmt; + } +} diff --git a/system/libraries/drivers/Database/Pdosqlite.php b/system/libraries/drivers/Database/Pdosqlite.php new file mode 100644 index 00000000..c2d1bb21 --- /dev/null +++ b/system/libraries/drivers/Database/Pdosqlite.php @@ -0,0 +1,486 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/* + * Class: Database_PdoSqlite_Driver + * Provides specific database items for Sqlite. + * + * Connection string should be, eg: "pdosqlite://path/to/database.db" + * + * Version 1.0 alpha + * author - Doutu, updated by gregmac + * copyright - (c) BSD + * license - <no> + */ + +class Database_Pdosqlite_Driver extends Database_Driver { + + // Database connection link + protected $link; + protected $db_config; + + /* + * Constructor: __construct + * Sets up the config for the class. + * + * Parameters: + * config - database configuration + * + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'PDO:Sqlite Database Driver Initialized'); + } + + public function connect() + { + // Import the connect variables + extract($this->db_config['connection']); + + try + { + $this->link = new PDO('sqlite:'.$socket.$database, $user, $pass, + array(PDO::ATTR_PERSISTENT => $this->db_config['persistent'])); + + $this->link->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL); + //$this->link->query('PRAGMA count_changes=1;'); + + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + } + catch (PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + + // Clear password after successful connect + $this->db_config['connection']['pass'] = NULL; + + return $this->link; + } + + public function query($sql) + { + try + { + $sth = $this->link->prepare($sql); + } + catch (PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + return new Pdosqlite_Result($sth, $this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + $this->link->query('PRAGMA encoding = '.$this->escape_str($charset)); + } + + public function escape_table($table) + { + if ( ! $this->db_config['escape']) + return $table; + + return '`'.str_replace('.', '`.`', $table).'`'; + } + + public function escape_column($column) + { + if ( ! $this->db_config['escape']) + return $column; + + if ($column == '*') + return $column; + + // This matches any functions we support to SELECT. + if ( preg_match('/(avg|count|sum|max|min)\(\s*(.*)\s*\)(\s*as\s*(.+)?)?/i', $column, $matches)) + { + if ( count($matches) == 3) + { + return $matches[1].'('.$this->escape_column($matches[2]).')'; + } + else if ( count($matches) == 5) + { + return $matches[1].'('.$this->escape_column($matches[2]).') AS '.$this->escape_column($matches[2]); + } + } + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:rand|all|distinct(?:row)?|high_priority|sql_(?:small_result|b(?:ig_result|uffer_result)|no_cache|ca(?:che|lc_found_rows)))\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '`$0`', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '`$0`', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + public function limit($limit, $offset = 0) + { + return 'LIMIT '.$offset.', '.$limit; + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + $sql .= "\nFROM "; + $sql .= implode(', ', $database['from']); + } + + if (count($database['join']) > 0) + { + foreach($database['join'] AS $join) + { + $sql .= "\n".$join['type'].'JOIN '.implode(', ', $join['tables']).' ON '.$join['conditions']; + } + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit'], $database['offset']); + } + + return $sql; + } + + public function escape_str($str) + { + if ( ! $this->db_config['escape']) + return $str; + + if (function_exists('sqlite_escape_string')) + { + $res = sqlite_escape_string($str); + } + else + { + $res = str_replace("'", "''", $str); + } + return $res; + } + + public function list_tables() + { + $sql = "SELECT `name` FROM `sqlite_master` WHERE `type`='table' ORDER BY `name`;"; + try + { + $result = $this->query($sql)->result(FALSE, PDO::FETCH_ASSOC); + $tables = array(); + foreach ($result as $row) + { + $tables[] = current($row); + } + } + catch (PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + return $tables; + } + + public function show_error() + { + $err = $this->link->errorInfo(); + return isset($err[2]) ? $err[2] : 'Unknown error!'; + } + + public function list_fields($table, $query = FALSE) + { + static $tables; + if (is_object($query)) + { + if (empty($tables[$table])) + { + $tables[$table] = array(); + + foreach ($query->result() as $row) + { + $tables[$table][] = $row->name; + } + } + + return $tables[$table]; + } + else + { + $result = $this->link->query( 'PRAGMA table_info('.$this->escape_table($table).')' ); + + foreach ($result as $row) + { + $tables[$table][$row['name']] = $this->sql_type($row['type']); + } + + return $tables[$table]; + } + } + + public function field_data($table) + { + Kohana::log('error', 'This method is under developing'); + } + /** + * Version number query string + * + * @access public + * @return string + */ + function version() + { + return $this->link->getAttribute(constant("PDO::ATTR_SERVER_VERSION")); + } + +} // End Database_PdoSqlite_Driver Class + +/* + * PDO-sqlite Result + */ +class Pdosqlite_Result extends Database_Result { + + // Data fetching types + protected $fetch_type = PDO::FETCH_OBJ; + protected $return_type = PDO::FETCH_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + if (is_object($result) OR $result = $link->prepare($sql)) + { + // run the query. Return true if success, false otherwise + if( ! $result->execute()) + { + // Throw Kohana Exception with error message. See PDOStatement errorInfo() method + $arr_infos = $result->errorInfo(); + throw new Kohana_Database_Exception('database.error', $arr_infos[2]); + } + + if (preg_match('/^SELECT|PRAGMA|EXPLAIN/i', $sql)) + { + $this->result = $result; + $this->current_row = 0; + + $this->total_rows = $this->sqlite_row_count(); + + $this->fetch_type = ($object === TRUE) ? PDO::FETCH_OBJ : PDO::FETCH_ASSOC; + } + elseif (preg_match('/^DELETE|INSERT|UPDATE/i', $sql)) + { + $this->insert_id = $link->lastInsertId(); + + $this->total_rows = $result->rowCount(); + } + } + else + { + // SQL error + throw new Kohana_Database_Exception('database.error', $link->errorInfo().' - '.$sql); + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + private function sqlite_row_count() + { + $count = 0; + while ($this->result->fetch()) + { + $count++; + } + + // The query must be re-fetched now. + $this->result->execute(); + + return $count; + } + + /* + * Destructor: __destruct + * Magic __destruct function, frees the result. + */ + public function __destruct() + { + if (is_object($this->result)) + { + $this->result->closeCursor(); + $this->result = NULL; + } + } + + public function result($object = TRUE, $type = PDO::FETCH_BOTH) + { + $this->fetch_type = (bool) $object ? PDO::FETCH_OBJ : PDO::FETCH_BOTH; + + if ($this->fetch_type == PDO::FETCH_OBJ) + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = PDO::FETCH_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = PDO::FETCH_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = PDO::FETCH_OBJ; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = PDO::FETCH_OBJ; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == PDO::FETCH_OBJ) + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + try + { + while ($row = $this->result->fetch($fetch)) + { + $rows[] = $row; + } + } + catch(PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + return FALSE; + } + return $rows; + } + + public function list_fields() + { + $field_names = array(); + for ($i = 0, $max = $this->result->columnCount(); $i < $max; $i++) + { + $info = $this->result->getColumnMeta($i); + $field_names[] = $info['name']; + } + return $field_names; + } + + public function seek($offset) + { + // To request a scrollable cursor for your PDOStatement object, you must + // set the PDO::ATTR_CURSOR attribute to PDO::CURSOR_SCROLL when you + // prepare the statement. + Kohana::log('error', get_class($this).' does not support scrollable cursors, '.__FUNCTION__.' call ignored'); + + return FALSE; + } + + public function offsetGet($offset) + { + try + { + return $this->result->fetch($this->fetch_type, PDO::FETCH_ORI_ABS, $offset); + } + catch(PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + } + + public function rewind() + { + // Same problem that seek() has, see above. + return $this->seek(0); + } + +} // End PdoSqlite_Result Class
\ No newline at end of file diff --git a/system/libraries/drivers/Database/Pgsql.php b/system/libraries/drivers/Database/Pgsql.php new file mode 100644 index 00000000..c53c8439 --- /dev/null +++ b/system/libraries/drivers/Database/Pgsql.php @@ -0,0 +1,538 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * PostgreSQL 8.1+ Database Driver + * + * $Id: Pgsql.php 4344 2009-05-11 16:41:39Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Pgsql_Driver extends Database_Driver { + + // Database connection link + protected $link; + protected $db_config; + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'PgSQL Database Driver Initialized'); + } + + public function connect() + { + // Check if link already exists + if (is_resource($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Persistent connections enabled? + $connect = ($this->db_config['persistent'] == TRUE) ? 'pg_pconnect' : 'pg_connect'; + + // Build the connection info + $port = isset($port) ? 'port=\''.$port.'\'' : ''; + $host = isset($host) ? 'host=\''.$host.'\' '.$port : ''; // if no host, connect with the socket + + $connection_string = $host.' dbname=\''.$database.'\' user=\''.$user.'\' password=\''.$pass.'\''; + // Make the connection and select the database + if ($this->link = $connect($connection_string)) + { + if ($charset = $this->db_config['character_set']) + { + echo $this->set_charset($charset); + } + + // Clear password after successful connect + $this->db_config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|SET)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset($this->query_cache[$hash])) + { + // Set the cached object + $this->query_cache[$hash] = new Pgsql_Result(pg_query($this->link, $sql), $this->link, $this->db_config['object'], $sql); + } + else + { + // Rewind cached result + $this->query_cache[$hash]->rewind(); + } + + return $this->query_cache[$hash]; + } + + // Suppress warning triggered when a database error occurs (e.g., a constraint violation) + return new Pgsql_Result(@pg_query($this->link, $sql), $this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + $this->query('SET client_encoding TO '.pg_escape_string($this->link, $charset)); + } + + public function escape_table($table) + { + if (!$this->db_config['escape']) + return $table; + + return '"'.str_replace('.', '"."', $table).'"'; + } + + public function escape_column($column) + { + if (!$this->db_config['escape']) + return $column; + + if ($column == '*') + return $column; + + // This matches any functions we support to SELECT. + if ( preg_match('/(avg|count|sum|max|min)\(\s*(.*)\s*\)(\s*as\s*(.+)?)?/i', $column, $matches)) + { + if ( count($matches) == 3) + { + return $matches[1].'('.$this->escape_column($matches[2]).')'; + } + else if ( count($matches) == 5) + { + return $matches[1].'('.$this->escape_column($matches[2]).') AS '.$this->escape_column($matches[2]); + } + } + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:all|distinct)\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '"$0"', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '"$0"', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + public function regex($field, $match, $type, $num_regexs) + { + $prefix = ($num_regexs == 0) ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' ~* \''.$this->escape_str($match).'\''; + } + + public function notregex($field, $match, $type, $num_regexs) + { + $prefix = $num_regexs == 0 ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' !~* \''.$this->escape_str($match) . '\''; + } + + public function limit($limit, $offset = 0) + { + return 'LIMIT '.$limit.' OFFSET '.$offset; + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + $sql .= "\nFROM "; + $sql .= implode(', ', $database['from']); + } + + if (count($database['join']) > 0) + { + foreach($database['join'] AS $join) + { + $sql .= "\n".$join['type'].'JOIN '.implode(', ', $join['tables']).' ON '.$join['conditions']; + } + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit'], $database['offset']); + } + + return $sql; + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_resource($this->link) or $this->connect(); + + return pg_escape_string($this->link, $str); + } + + public function list_tables() + { + $sql = 'SELECT table_schema || \'.\' || table_name FROM information_schema.tables WHERE table_schema NOT IN (\'pg_catalog\', \'information_schema\')'; + $result = $this->query($sql)->result(FALSE, PGSQL_ASSOC); + + $retval = array(); + foreach ($result as $row) + { + $retval[] = current($row); + } + + return $retval; + } + + public function show_error() + { + return pg_last_error($this->link); + } + + public function list_fields($table) + { + $result = NULL; + + foreach ($this->field_data($table) as $row) + { + // Make an associative array + $result[$row->column_name] = $this->sql_type($row->data_type); + + if (!strncmp($row->column_default, 'nextval(', 8)) + { + $result[$row->column_name]['sequenced'] = TRUE; + } + + if ($row->is_nullable === 'YES') + { + $result[$row->column_name]['null'] = TRUE; + } + } + + if (!isset($result)) + throw new Kohana_Database_Exception('database.table_not_found', $table); + + return $result; + } + + public function field_data($table) + { + // http://www.postgresql.org/docs/8.3/static/infoschema-columns.html + $result = $this->query(' + SELECT column_name, column_default, is_nullable, data_type, udt_name, + character_maximum_length, numeric_precision, numeric_precision_radix, numeric_scale + FROM information_schema.columns + WHERE table_name = \''. $this->escape_str($table) .'\' + ORDER BY ordinal_position + '); + + return $result->result_array(TRUE); + } + +} // End Database_Pgsql_Driver Class + +/** + * PostgreSQL Result + */ +class Pgsql_Result extends Database_Result { + + // Data fetching types + protected $fetch_type = 'pgsql_fetch_object'; + protected $return_type = PGSQL_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + $this->link = $link; + $this->result = $result; + + // If the query is a resource, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_resource($result)) + { + // Its an DELETE, INSERT, REPLACE, or UPDATE query + if (preg_match('/^(?:delete|insert|replace|update)\b/iD', trim($sql), $matches)) + { + $this->insert_id = (strtolower($matches[0]) == 'insert') ? $this->insert_id() : FALSE; + $this->total_rows = pg_affected_rows($this->result); + } + else + { + $this->current_row = 0; + $this->total_rows = pg_num_rows($this->result); + $this->fetch_type = ($object === TRUE) ? 'pg_fetch_object' : 'pg_fetch_array'; + } + } + else + { + throw new Kohana_Database_Exception('database.error', pg_last_error().' - '.$sql); + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Magic __destruct function, frees the result. + */ + public function __destruct() + { + if (is_resource($this->result)) + { + pg_free_result($this->result); + } + } + + public function result($object = TRUE, $type = PGSQL_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'pg_fetch_object' : 'pg_fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'pg_fetch_object') + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = PGSQL_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = PGSQL_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'pg_fetch_object'; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'pg_fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'pg_fetch_object') + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + + if ($this->total_rows) + { + pg_result_seek($this->result, 0); + + while ($row = $fetch($this->result, NULL, $type)) + { + $rows[] = $row; + } + } + + return $rows; + } + + public function insert_id() + { + if ($this->insert_id === NULL) + { + $query = 'SELECT LASTVAL() AS insert_id'; + + // Disable error reporting for this, just to silence errors on + // tables that have no serial column. + $ER = error_reporting(0); + + $result = pg_query($this->link, $query); + $insert_id = pg_fetch_array($result, NULL, PGSQL_ASSOC); + + $this->insert_id = $insert_id['insert_id']; + + // Reset error reporting + error_reporting($ER); + } + + return $this->insert_id; + } + + public function seek($offset) + { + if ($this->offsetExists($offset) AND pg_result_seek($this->result, $offset)) + { + // Set the current row to the offset + $this->current_row = $offset; + + return TRUE; + } + + return FALSE; + } + + public function list_fields() + { + $field_names = array(); + + $fields = pg_num_fields($this->result); + for ($i = 0; $i < $fields; ++$i) + { + $field_names[] = pg_field_name($this->result, $i); + } + + return $field_names; + } + + /** + * ArrayAccess: offsetGet + */ + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return FALSE; + + // Return the row by calling the defined fetching callback + $fetch = $this->fetch_type; + return $fetch($this->result, NULL, $this->return_type); + } + +} // End Pgsql_Result Class + +/** + * PostgreSQL Prepared Statement (experimental) + */ +class Kohana_Pgsql_Statement { + + protected $link = NULL; + protected $stmt; + + public function __construct($sql, $link) + { + $this->link = $link; + + $this->stmt = $this->link->prepare($sql); + + return $this; + } + + public function __destruct() + { + $this->stmt->close(); + } + + // Sets the bind parameters + public function bind_params() + { + $argv = func_get_args(); + return $this; + } + + // sets the statement values to the bound parameters + public function set_vals() + { + return $this; + } + + // Runs the statement + public function execute() + { + return $this; + } +} diff --git a/system/libraries/drivers/Image.php b/system/libraries/drivers/Image.php new file mode 100644 index 00000000..f89ba953 --- /dev/null +++ b/system/libraries/drivers/Image.php @@ -0,0 +1,156 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Image API driver. + * + * $Id: Image.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Image_Driver { + + // Reference to the current image + protected $image; + + // Reference to the temporary processing image + protected $tmp_image; + + // Processing errors + protected $errors = array(); + + /** + * Executes a set of actions, defined in pairs. + * + * @param array actions + * @return boolean + */ + public function execute($actions) + { + foreach ($actions as $func => $args) + { + if ( ! $this->$func($args)) + return FALSE; + } + + return TRUE; + } + + /** + * Sanitize and normalize a geometry array based on the temporary image + * width and height. Valid properties are: width, height, top, left. + * + * @param array geometry properties + * @return void + */ + protected function sanitize_geometry( & $geometry) + { + list($width, $height) = $this->properties(); + + // Turn off error reporting + $reporting = error_reporting(0); + + // Width and height cannot exceed current image size + $geometry['width'] = min($geometry['width'], $width); + $geometry['height'] = min($geometry['height'], $height); + + // Set standard coordinates if given, otherwise use pixel values + if ($geometry['top'] === 'center') + { + $geometry['top'] = floor(($height / 2) - ($geometry['height'] / 2)); + } + elseif ($geometry['top'] === 'top') + { + $geometry['top'] = 0; + } + elseif ($geometry['top'] === 'bottom') + { + $geometry['top'] = $height - $geometry['height']; + } + + // Set standard coordinates if given, otherwise use pixel values + if ($geometry['left'] === 'center') + { + $geometry['left'] = floor(($width / 2) - ($geometry['width'] / 2)); + } + elseif ($geometry['left'] === 'left') + { + $geometry['left'] = 0; + } + elseif ($geometry['left'] === 'right') + { + $geometry['left'] = $width - $geometry['height']; + } + + // Restore error reporting + error_reporting($reporting); + } + + /** + * Return the current width and height of the temporary image. This is mainly + * needed for sanitizing the geometry. + * + * @return array width, height + */ + abstract protected function properties(); + + /** + * Process an image with a set of actions. + * + * @param string image filename + * @param array actions to execute + * @param string destination directory path + * @param string destination filename + * @return boolean + */ + abstract public function process($image, $actions, $dir, $file); + + /** + * Flip an image. Valid directions are horizontal and vertical. + * + * @param integer direction to flip + * @return boolean + */ + abstract function flip($direction); + + /** + * Crop an image. Valid properties are: width, height, top, left. + * + * @param array new properties + * @return boolean + */ + abstract function crop($properties); + + /** + * Resize an image. Valid properties are: width, height, and master. + * + * @param array new properties + * @return boolean + */ + abstract public function resize($properties); + + /** + * Rotate an image. Valid amounts are -180 to 180. + * + * @param integer amount to rotate + * @return boolean + */ + abstract public function rotate($amount); + + /** + * Sharpen and image. Valid amounts are 1 to 100. + * + * @param integer amount to sharpen + * @return boolean + */ + abstract public function sharpen($amount); + + /** + * Overlay a second image. Valid properties are: overlay_file, mime, x, y and transparency. + * + * @return boolean + */ + abstract public function composite($properties); + +} // End Image Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Image/GD.php b/system/libraries/drivers/Image/GD.php new file mode 100644 index 00000000..be2af4e2 --- /dev/null +++ b/system/libraries/drivers/Image/GD.php @@ -0,0 +1,401 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * GD Image Driver. + * + * $Id: GD.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Image_GD_Driver extends Image_Driver { + + // A transparent PNG as a string + protected static $blank_png; + protected static $blank_png_width; + protected static $blank_png_height; + + public function __construct() + { + // Make sure that GD2 is available + if ( ! function_exists('gd_info')) + throw new Kohana_Exception('image.gd.requires_v2'); + + // Get the GD information + $info = gd_info(); + + // Make sure that the GD2 is installed + if (strpos($info['GD Version'], '2.') === FALSE) + throw new Kohana_Exception('image.gd.requires_v2'); + } + + public function process($image, $actions, $dir, $file, $render = FALSE) + { + // Set the "create" function + switch ($image['type']) + { + case IMAGETYPE_JPEG: + $create = 'imagecreatefromjpeg'; + break; + case IMAGETYPE_GIF: + $create = 'imagecreatefromgif'; + break; + case IMAGETYPE_PNG: + $create = 'imagecreatefrompng'; + break; + } + + // Set the "save" function + switch (strtolower(substr(strrchr($file, '.'), 1))) + { + case 'jpg': + case 'jpeg': + $save = 'imagejpeg'; + break; + case 'gif': + $save = 'imagegif'; + break; + case 'png': + $save = 'imagepng'; + break; + } + + // Make sure the image type is supported for import + if (empty($create) OR ! function_exists($create)) + throw new Kohana_Exception('image.type_not_allowed', $image['file']); + + // Make sure the image type is supported for saving + if (empty($save) OR ! function_exists($save)) + throw new Kohana_Exception('image.type_not_allowed', $dir.$file); + + // Load the image + $this->image = $image; + + // Create the GD image resource + $this->tmp_image = $create($image['file']); + + // Get the quality setting from the actions + $quality = arr::remove('quality', $actions); + + if ($status = $this->execute($actions)) + { + // Prevent the alpha from being lost + imagealphablending($this->tmp_image, TRUE); + imagesavealpha($this->tmp_image, TRUE); + + switch ($save) + { + case 'imagejpeg': + // Default the quality to 95 + ($quality === NULL) and $quality = 95; + break; + case 'imagegif': + // Remove the quality setting, GIF doesn't use it + unset($quality); + break; + case 'imagepng': + // Always use a compression level of 9 for PNGs. This does not + // affect quality, it only increases the level of compression! + $quality = 9; + break; + } + + if ($render === FALSE) + { + // Set the status to the save return value, saving with the quality requested + $status = isset($quality) ? $save($this->tmp_image, $dir.$file, $quality) : $save($this->tmp_image, $dir.$file); + } + else + { + // Output the image directly to the browser + switch ($save) + { + case 'imagejpeg': + header('Content-Type: image/jpeg'); + break; + case 'imagegif': + header('Content-Type: image/gif'); + break; + case 'imagepng': + header('Content-Type: image/png'); + break; + } + + $status = isset($quality) ? $save($this->tmp_image, NULL, $quality) : $save($this->tmp_image); + } + + // Destroy the temporary image + imagedestroy($this->tmp_image); + } + + return $status; + } + + public function flip($direction) + { + // Get the current width and height + $width = imagesx($this->tmp_image); + $height = imagesy($this->tmp_image); + + // Create the flipped image + $flipped = $this->imagecreatetransparent($width, $height); + + if ($direction === Image::HORIZONTAL) + { + for ($x = 0; $x < $width; $x++) + { + $status = imagecopy($flipped, $this->tmp_image, $x, 0, $width - $x - 1, 0, 1, $height); + } + } + elseif ($direction === Image::VERTICAL) + { + for ($y = 0; $y < $height; $y++) + { + $status = imagecopy($flipped, $this->tmp_image, 0, $y, 0, $height - $y - 1, $width, 1); + } + } + else + { + // Do nothing + return TRUE; + } + + if ($status === TRUE) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $flipped; + } + + return $status; + } + + public function crop($properties) + { + // Sanitize the cropping settings + $this->sanitize_geometry($properties); + + // Get the current width and height + $width = imagesx($this->tmp_image); + $height = imagesy($this->tmp_image); + + // Create the temporary image to copy to + $img = $this->imagecreatetransparent($properties['width'], $properties['height']); + + // Execute the crop + if ($status = imagecopyresampled($img, $this->tmp_image, 0, 0, $properties['left'], $properties['top'], $width, $height, $width, $height)) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + return $status; + } + + public function resize($properties) + { + // Get the current width and height + $width = imagesx($this->tmp_image); + $height = imagesy($this->tmp_image); + + if (substr($properties['width'], -1) === '%') + { + // Recalculate the percentage to a pixel size + $properties['width'] = round($width * (substr($properties['width'], 0, -1) / 100)); + } + + if (substr($properties['height'], -1) === '%') + { + // Recalculate the percentage to a pixel size + $properties['height'] = round($height * (substr($properties['height'], 0, -1) / 100)); + } + + // Recalculate the width and height, if they are missing + empty($properties['width']) and $properties['width'] = round($width * $properties['height'] / $height); + empty($properties['height']) and $properties['height'] = round($height * $properties['width'] / $width); + + if ($properties['master'] === Image::AUTO) + { + // Change an automatic master dim to the correct type + $properties['master'] = (($width / $properties['width']) > ($height / $properties['height'])) ? Image::WIDTH : Image::HEIGHT; + } + + if (empty($properties['height']) OR $properties['master'] === Image::WIDTH) + { + // Recalculate the height based on the width + $properties['height'] = round($height * $properties['width'] / $width); + } + + if (empty($properties['width']) OR $properties['master'] === Image::HEIGHT) + { + // Recalculate the width based on the height + $properties['width'] = round($width * $properties['height'] / $height); + } + + // Test if we can do a resize without resampling to speed up the final resize + if ($properties['width'] > $width / 2 AND $properties['height'] > $height / 2) + { + // Presize width and height + $pre_width = $width; + $pre_height = $height; + + // The maximum reduction is 10% greater than the final size + $max_reduction_width = round($properties['width'] * 1.1); + $max_reduction_height = round($properties['height'] * 1.1); + + // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction + while ($pre_width / 2 > $max_reduction_width AND $pre_height / 2 > $max_reduction_height) + { + $pre_width /= 2; + $pre_height /= 2; + } + + // Create the temporary image to copy to + $img = $this->imagecreatetransparent($pre_width, $pre_height); + + if ($status = imagecopyresized($img, $this->tmp_image, 0, 0, 0, 0, $pre_width, $pre_height, $width, $height)) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + // Set the width and height to the presize + $width = $pre_width; + $height = $pre_height; + } + + // Create the temporary image to copy to + $img = $this->imagecreatetransparent($properties['width'], $properties['height']); + + // Execute the resize + if ($status = imagecopyresampled($img, $this->tmp_image, 0, 0, 0, 0, $properties['width'], $properties['height'], $width, $height)) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + return $status; + } + + public function rotate($amount) + { + // Use current image to rotate + $img = $this->tmp_image; + + // White, with an alpha of 0 + $transparent = imagecolorallocatealpha($img, 255, 255, 255, 127); + + // Rotate, setting the transparent color + $img = imagerotate($img, 360 - $amount, $transparent, -1); + + // Fill the background with the transparent "color" + imagecolortransparent($img, $transparent); + + // Merge the images + if ($status = imagecopymerge($this->tmp_image, $img, 0, 0, 0, 0, imagesx($this->tmp_image), imagesy($this->tmp_image), 100)) + { + // Prevent the alpha from being lost + imagealphablending($img, TRUE); + imagesavealpha($img, TRUE); + + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + return $status; + } + + public function sharpen($amount) + { + // Make sure that the sharpening function is available + if ( ! function_exists('imageconvolution')) + throw new Kohana_Exception('image.unsupported_method', __FUNCTION__); + + // Amount should be in the range of 18-10 + $amount = round(abs(-18 + ($amount * 0.08)), 2); + + // Gaussian blur matrix + $matrix = array + ( + array(-1, -1, -1), + array(-1, $amount, -1), + array(-1, -1, -1), + ); + + // Perform the sharpen + return imageconvolution($this->tmp_image, $matrix, $amount - 8, 0); + } + + public function composite($properties) + { + switch($properties['mime']) + { + case "image/jpeg": + $overlay_img = imagecreatefromjpeg($properties['overlay_file']); + break; + + case "image/gif": + $overlay_img = imagecreatefromgif($properties['overlay_file']); + break; + + case "image/png": + $overlay_img = imagecreatefrompng($properties['overlay_file']); + break; + } + + imagecopymerge($this->tmp_image, $overlay_img, $properties['x'], $properties['y'], 0, 0, imagesx($overlay_img), imagesy($overlay_img), $properties['transparency']); + imagedestroy($overlay_img); + return TRUE; + } + + protected function properties() + { + return array(imagesx($this->tmp_image), imagesy($this->tmp_image)); + } + + /** + * Returns an image with a transparent background. Used for rotating to + * prevent unfilled backgrounds. + * + * @param integer image width + * @param integer image height + * @return resource + */ + protected function imagecreatetransparent($width, $height) + { + if (self::$blank_png === NULL) + { + // Decode the blank PNG if it has not been done already + self::$blank_png = imagecreatefromstring(base64_decode + ( + 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29'. + 'mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADqSURBVHjaYvz//z/DYAYAAcTEMMgBQAANegcCBN'. + 'CgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQ'. + 'AANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoH'. + 'AgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB'. + '3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAgAEAMpcDTTQWJVEAAAAASUVORK5CYII=' + )); + + // Set the blank PNG width and height + self::$blank_png_width = imagesx(self::$blank_png); + self::$blank_png_height = imagesy(self::$blank_png); + } + + $img = imagecreatetruecolor($width, $height); + + // Resize the blank image + imagecopyresized($img, self::$blank_png, 0, 0, 0, 0, $width, $height, self::$blank_png_width, self::$blank_png_height); + + // Prevent the alpha from being lost + imagealphablending($img, FALSE); + imagesavealpha($img, TRUE); + + return $img; + } + +} // End Image GD Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Image/GraphicsMagick.php b/system/libraries/drivers/Image/GraphicsMagick.php new file mode 100644 index 00000000..a8bc4d9b --- /dev/null +++ b/system/libraries/drivers/Image/GraphicsMagick.php @@ -0,0 +1,221 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * GraphicsMagick Image Driver. + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Image_GraphicsMagick_Driver extends Image_Driver { + + // Directory that GM is installed in + protected $dir = ''; + + // Command extension (exe for windows) + protected $ext = ''; + + // Temporary image filename + protected $tmp_image; + + /** + * Attempts to detect the GraphicsMagick installation directory. + * + * @throws Kohana_Exception + * @param array configuration + * @return void + */ + public function __construct($config) + { + if (empty($config['directory'])) + { + // Attempt to locate GM by using "which" (only works for *nix!) + if ( ! is_file($path = exec('which gm'))) + throw new Kohana_Exception('image.graphicsmagick.not_found'); + + $config['directory'] = dirname($path); + } + + // Set the command extension + $this->ext = (PHP_SHLIB_SUFFIX === 'dll') ? '.exe' : ''; + + // Check to make sure the provided path is correct + if ( ! is_file(realpath($config['directory']).'/gm'.$this->ext)) + throw new Kohana_Exception('image.graphicsmagick.not_found', 'gm'.$this->ext); + + + // Set the installation directory + $this->dir = str_replace('\\', '/', realpath($config['directory'])).'/'; + } + + /** + * Creates a temporary image and executes the given actions. By creating a + * temporary copy of the image before manipulating it, this process is atomic. + */ + public function process($image, $actions, $dir, $file, $render = FALSE) + { + // We only need the filename + $image = $image['file']; + + // Unique temporary filename + $this->tmp_image = $dir.'k2img--'.sha1(time().$dir.$file).substr($file, strrpos($file, '.')); + + // Copy the image to the temporary file + copy($image, $this->tmp_image); + + // Quality change is done last + $quality = (int) arr::remove('quality', $actions); + + // Use 95 for the default quality + empty($quality) and $quality = 95; + + // All calls to these will need to be escaped, so do it now + $this->cmd_image = escapeshellarg($this->tmp_image); + $this->new_image = ($render)? $this->cmd_image : escapeshellarg($dir.$file); + + if ($status = $this->execute($actions)) + { + // Use convert to change the image into its final version. This is + // done to allow the file type to change correctly, and to handle + // the quality conversion in the most effective way possible. + if ($error = exec(escapeshellcmd($this->dir.'gm'.$this->ext.' convert').' -quality '.$quality.'% '.$this->cmd_image.' '.$this->new_image)) + { + $this->errors[] = $error; + } + else + { + // Output the image directly to the browser + if ($render !== FALSE) + { + $contents = file_get_contents($this->tmp_image); + switch (substr($file, strrpos($file, '.') + 1)) + { + case 'jpg': + case 'jpeg': + header('Content-Type: image/jpeg'); + break; + case 'gif': + header('Content-Type: image/gif'); + break; + case 'png': + header('Content-Type: image/png'); + break; + } + echo $contents; + } + } + } + + // Remove the temporary image + unlink($this->tmp_image); + $this->tmp_image = ''; + + return $status; + } + + public function crop($prop) + { + // Sanitize and normalize the properties into geometry + $this->sanitize_geometry($prop); + + // Set the IM geometry based on the properties + $geometry = escapeshellarg($prop['width'].'x'.$prop['height'].'+'.$prop['left'].'+'.$prop['top']); + + if ($error = exec(escapeshellcmd($this->dir.'gm'.$this->ext.' convert').' -crop '.$geometry.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function flip($dir) + { + // Convert the direction into a GM command + $dir = ($dir === Image::HORIZONTAL) ? '-flop' : '-flip'; + + if ($error = exec(escapeshellcmd($this->dir.'gm'.$this->ext.' convert').' '.$dir.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function resize($prop) + { + switch ($prop['master']) + { + case Image::WIDTH: // Wx + $dim = escapeshellarg($prop['width'].'x'); + break; + case Image::HEIGHT: // xH + $dim = escapeshellarg('x'.$prop['height']); + break; + case Image::AUTO: // WxH + $dim = escapeshellarg($prop['width'].'x'.$prop['height']); + break; + case Image::NONE: // WxH! + $dim = escapeshellarg($prop['width'].'x'.$prop['height'].'!'); + break; + } + + // Use "convert" to change the width and height + if ($error = exec(escapeshellcmd($this->dir.'gm'.$this->ext.' convert').' -resize '.$dim.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function rotate($amt) + { + if ($error = exec(escapeshellcmd($this->dir.'gm'.$this->ext.' convert').' -rotate '.escapeshellarg($amt).' -background transparent '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function sharpen($amount) + { + // Set the sigma, radius, and amount. The amount formula allows a nice + // spread between 1 and 100 without pixelizing the image badly. + $sigma = 0.5; + $radius = $sigma * 2; + $amount = round(($amount / 80) * 3.14, 2); + + // Convert the amount to an GM command + $sharpen = escapeshellarg($radius.'x'.$sigma.'+'.$amount.'+0'); + + if ($error = exec(escapeshellcmd($this->dir.'gm'.$this->ext.' convert').' -unsharp '.$sharpen.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function composite($properties) + { + if ($error = exec(escapeshellcmd($this->dir.'gm'.$this->ext.' composite').' -geometry ' . escapeshellarg('+'.$properties['x'].'+'.$properties['y']).' -dissolve '.escapeshellarg($properties['transparency']).' '.escapeshellarg($properties['overlay_file']).' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + return TRUE; + } + + protected function properties() + { + return array_slice(getimagesize($this->tmp_image), 0, 2, FALSE); + } + +} // End Image GraphicsMagick Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Image/ImageMagick.php b/system/libraries/drivers/Image/ImageMagick.php new file mode 100644 index 00000000..4b381fd6 --- /dev/null +++ b/system/libraries/drivers/Image/ImageMagick.php @@ -0,0 +1,222 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * ImageMagick Image Driver. + * + * $Id: ImageMagick.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Image_ImageMagick_Driver extends Image_Driver { + + // Directory that IM is installed in + protected $dir = ''; + + // Command extension (exe for windows) + protected $ext = ''; + + // Temporary image filename + protected $tmp_image; + + /** + * Attempts to detect the ImageMagick installation directory. + * + * @throws Kohana_Exception + * @param array configuration + * @return void + */ + public function __construct($config) + { + if (empty($config['directory'])) + { + // Attempt to locate IM by using "which" (only works for *nix!) + if ( ! is_file($path = exec('which convert'))) + throw new Kohana_Exception('image.imagemagick.not_found'); + + $config['directory'] = dirname($path); + } + + // Set the command extension + $this->ext = (PHP_SHLIB_SUFFIX === 'dll') ? '.exe' : ''; + + // Check to make sure the provided path is correct + if ( ! is_file(realpath($config['directory']).'/convert'.$this->ext)) + throw new Kohana_Exception('image.imagemagick.not_found', 'convert'.$this->ext); + + // Set the installation directory + $this->dir = str_replace('\\', '/', realpath($config['directory'])).'/'; + } + + /** + * Creates a temporary image and executes the given actions. By creating a + * temporary copy of the image before manipulating it, this process is atomic. + */ + public function process($image, $actions, $dir, $file, $render = FALSE) + { + // We only need the filename + $image = $image['file']; + + // Unique temporary filename + $this->tmp_image = $dir.'k2img--'.sha1(time().$dir.$file).substr($file, strrpos($file, '.')); + + // Copy the image to the temporary file + copy($image, $this->tmp_image); + + // Quality change is done last + $quality = (int) arr::remove('quality', $actions); + + // Use 95 for the default quality + empty($quality) and $quality = 95; + + // All calls to these will need to be escaped, so do it now + $this->cmd_image = escapeshellarg($this->tmp_image); + $this->new_image = ($render)? $this->cmd_image : escapeshellarg($dir.$file); + + if ($status = $this->execute($actions)) + { + // Use convert to change the image into its final version. This is + // done to allow the file type to change correctly, and to handle + // the quality conversion in the most effective way possible. + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -quality '.$quality.'% '.$this->cmd_image.' '.$this->new_image)) + { + $this->errors[] = $error; + } + else + { + // Output the image directly to the browser + if ($render !== FALSE) + { + $contents = file_get_contents($this->tmp_image); + switch (substr($file, strrpos($file, '.') + 1)) + { + case 'jpg': + case 'jpeg': + header('Content-Type: image/jpeg'); + break; + case 'gif': + header('Content-Type: image/gif'); + break; + case 'png': + header('Content-Type: image/png'); + break; + } + echo $contents; + } + } + } + + // Remove the temporary image + unlink($this->tmp_image); + $this->tmp_image = ''; + + return $status; + } + + public function crop($prop) + { + // Sanitize and normalize the properties into geometry + $this->sanitize_geometry($prop); + + // Set the IM geometry based on the properties + $geometry = escapeshellarg($prop['width'].'x'.$prop['height'].'+'.$prop['left'].'+'.$prop['top']); + + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -crop '.$geometry.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function flip($dir) + { + // Convert the direction into a IM command + $dir = ($dir === Image::HORIZONTAL) ? '-flop' : '-flip'; + + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' '.$dir.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function resize($prop) + { + switch ($prop['master']) + { + case Image::WIDTH: // Wx + $dim = escapeshellarg($prop['width'].'x'); + break; + case Image::HEIGHT: // xH + $dim = escapeshellarg('x'.$prop['height']); + break; + case Image::AUTO: // WxH + $dim = escapeshellarg($prop['width'].'x'.$prop['height']); + break; + case Image::NONE: // WxH! + $dim = escapeshellarg($prop['width'].'x'.$prop['height'].'!'); + break; + } + + // Use "convert" to change the width and height + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -resize '.$dim.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function rotate($amt) + { + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -rotate '.escapeshellarg($amt).' -background transparent '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function sharpen($amount) + { + // Set the sigma, radius, and amount. The amount formula allows a nice + // spread between 1 and 100 without pixelizing the image badly. + $sigma = 0.5; + $radius = $sigma * 2; + $amount = round(($amount / 80) * 3.14, 2); + + // Convert the amount to an IM command + $sharpen = escapeshellarg($radius.'x'.$sigma.'+'.$amount.'+0'); + + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -unsharp '.$sharpen.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function composite($properties) + { + if ($error = exec(escapeshellcmd($this->dir.'composite'.$this->ext).' -geometry ' . escapeshellarg('+'.$properties['x'].'+'.$properties['y']).' -dissolve '.escapeshellarg($properties['transparency']).' '.escapeshellarg($properties['overlay_file']).' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + return TRUE; + } + + protected function properties() + { + return array_slice(getimagesize($this->tmp_image), 0, 2, FALSE); + } + +} // End Image ImageMagick Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Session.php b/system/libraries/drivers/Session.php new file mode 100644 index 00000000..fb58c8d3 --- /dev/null +++ b/system/libraries/drivers/Session.php @@ -0,0 +1,70 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session driver interface + * + * $Id: Session.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +interface Session_Driver { + + /** + * Opens a session. + * + * @param string save path + * @param string session name + * @return boolean + */ + public function open($path, $name); + + /** + * Closes a session. + * + * @return boolean + */ + public function close(); + + /** + * Reads a session. + * + * @param string session id + * @return string + */ + public function read($id); + + /** + * Writes a session. + * + * @param string session id + * @param string session data + * @return boolean + */ + public function write($id, $data); + + /** + * Destroys a session. + * + * @param string session id + * @return boolean + */ + public function destroy($id); + + /** + * Regenerates the session id. + * + * @return string + */ + public function regenerate(); + + /** + * Garbage collection. + * + * @param integer session expiration period + * @return boolean + */ + public function gc($maxlifetime); + +} // End Session Driver Interface
\ No newline at end of file diff --git a/system/libraries/drivers/Session/Cache.php b/system/libraries/drivers/Session/Cache.php new file mode 100644 index 00000000..7221c9f2 --- /dev/null +++ b/system/libraries/drivers/Session/Cache.php @@ -0,0 +1,105 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session cache driver. + * + * Cache library config goes in the session.storage config entry: + * $config['storage'] = array( + * 'driver' => 'apc', + * 'requests' => 10000 + * ); + * Lifetime does not need to be set as it is + * overridden by the session expiration setting. + * + * $Id: Cache.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Cache_Driver implements Session_Driver { + + protected $cache; + protected $encrypt; + + public function __construct() + { + // Load Encrypt library + if (Kohana::config('session.encryption')) + { + $this->encrypt = new Encrypt; + } + + Kohana::log('debug', 'Session Cache Driver Initialized'); + } + + public function open($path, $name) + { + $config = Kohana::config('session.storage'); + + if (empty($config)) + { + // Load the default group + $config = Kohana::config('cache.default'); + } + elseif (is_string($config)) + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('cache.'.$config)) === NULL) + throw new Kohana_Exception('cache.undefined_group', $name); + } + + $config['lifetime'] = (Kohana::config('session.expiration') == 0) ? 86400 : Kohana::config('session.expiration'); + $this->cache = new Cache($config); + + return is_object($this->cache); + } + + public function close() + { + return TRUE; + } + + public function read($id) + { + $id = 'session_'.$id; + if ($data = $this->cache->get($id)) + { + return Kohana::config('session.encryption') ? $this->encrypt->decode($data) : $data; + } + + // Return value must be string, NOT a boolean + return ''; + } + + public function write($id, $data) + { + $id = 'session_'.$id; + $data = Kohana::config('session.encryption') ? $this->encrypt->encode($data) : $data; + + return $this->cache->set($id, $data); + } + + public function destroy($id) + { + $id = 'session_'.$id; + return $this->cache->delete($id); + } + + public function regenerate() + { + session_regenerate_id(TRUE); + + // Return new session id + return session_id(); + } + + public function gc($maxlifetime) + { + // Just return, caches are automatically cleaned up + return TRUE; + } + +} // End Session Cache Driver diff --git a/system/libraries/drivers/Session/Cookie.php b/system/libraries/drivers/Session/Cookie.php new file mode 100644 index 00000000..7b791064 --- /dev/null +++ b/system/libraries/drivers/Session/Cookie.php @@ -0,0 +1,80 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session cookie driver. + * + * $Id: Cookie.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Cookie_Driver implements Session_Driver { + + protected $cookie_name; + protected $encrypt; // Library + + public function __construct() + { + $this->cookie_name = Kohana::config('session.name').'_data'; + + if (Kohana::config('session.encryption')) + { + $this->encrypt = Encrypt::instance(); + } + + Kohana::log('debug', 'Session Cookie Driver Initialized'); + } + + public function open($path, $name) + { + return TRUE; + } + + public function close() + { + return TRUE; + } + + public function read($id) + { + $data = (string) cookie::get($this->cookie_name); + + if ($data == '') + return $data; + + return empty($this->encrypt) ? base64_decode($data) : $this->encrypt->decode($data); + } + + public function write($id, $data) + { + $data = empty($this->encrypt) ? base64_encode($data) : $this->encrypt->encode($data); + + if (strlen($data) > 4048) + { + Kohana::log('error', 'Session ('.$id.') data exceeds the 4KB limit, ignoring write.'); + return FALSE; + } + + return cookie::set($this->cookie_name, $data, Kohana::config('session.expiration')); + } + + public function destroy($id) + { + return cookie::delete($this->cookie_name); + } + + public function regenerate() + { + session_regenerate_id(TRUE); + + // Return new id + return session_id(); + } + + public function gc($maxlifetime) + { + return TRUE; + } + +} // End Session Cookie Driver Class
\ No newline at end of file diff --git a/system/libraries/drivers/Session/Database.php b/system/libraries/drivers/Session/Database.php new file mode 100644 index 00000000..b4144ffb --- /dev/null +++ b/system/libraries/drivers/Session/Database.php @@ -0,0 +1,163 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session database driver. + * + * $Id: Database.php 3769 2008-12-15 00:48:56Z zombor $ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Database_Driver implements Session_Driver { + + /* + CREATE TABLE sessions + ( + session_id VARCHAR(127) NOT NULL, + last_activity INT(10) UNSIGNED NOT NULL, + data TEXT NOT NULL, + PRIMARY KEY (session_id) + ); + */ + + // Database settings + protected $db = 'default'; + protected $table = 'sessions'; + + // Encryption + protected $encrypt; + + // Session settings + protected $session_id; + protected $written = FALSE; + + public function __construct() + { + // Load configuration + $config = Kohana::config('session'); + + if ( ! empty($config['encryption'])) + { + // Load encryption + $this->encrypt = Encrypt::instance(); + } + + if (is_array($config['storage'])) + { + if ( ! empty($config['storage']['group'])) + { + // Set the group name + $this->db = $config['storage']['group']; + } + + if ( ! empty($config['storage']['table'])) + { + // Set the table name + $this->table = $config['storage']['table']; + } + } + + // Load database + $this->db = Database::instance($this->db); + + Kohana::log('debug', 'Session Database Driver Initialized'); + } + + public function open($path, $name) + { + return TRUE; + } + + public function close() + { + return TRUE; + } + + public function read($id) + { + // Load the session + $query = $this->db->from($this->table)->where('session_id', $id)->limit(1)->get()->result(TRUE); + + if ($query->count() === 0) + { + // No current session + $this->session_id = NULL; + + return ''; + } + + // Set the current session id + $this->session_id = $id; + + // Load the data + $data = $query->current()->data; + + return ($this->encrypt === NULL) ? base64_decode($data) : $this->encrypt->decode($data); + } + + public function write($id, $data) + { + $data = array + ( + 'session_id' => $id, + 'last_activity' => time(), + 'data' => ($this->encrypt === NULL) ? base64_encode($data) : $this->encrypt->encode($data) + ); + + if ($this->session_id === NULL) + { + // Insert a new session + $query = $this->db->insert($this->table, $data); + } + elseif ($id === $this->session_id) + { + // Do not update the session_id + unset($data['session_id']); + + // Update the existing session + $query = $this->db->update($this->table, $data, array('session_id' => $id)); + } + else + { + // Update the session and id + $query = $this->db->update($this->table, $data, array('session_id' => $this->session_id)); + + // Set the new session id + $this->session_id = $id; + } + + return (bool) $query->count(); + } + + public function destroy($id) + { + // Delete the requested session + $this->db->delete($this->table, array('session_id' => $id)); + + // Session id is no longer valid + $this->session_id = NULL; + + return TRUE; + } + + public function regenerate() + { + // Generate a new session id + session_regenerate_id(); + + // Return new session id + return session_id(); + } + + public function gc($maxlifetime) + { + // Delete all expired sessions + $query = $this->db->delete($this->table, array('last_activity <' => time() - $maxlifetime)); + + Kohana::log('debug', 'Session garbage collected: '.$query->count().' row(s) deleted.'); + + return TRUE; + } + +} // End Session Database Driver |