diff options
Diffstat (limited to 'system/libraries')
51 files changed, 11856 insertions, 0 deletions
diff --git a/system/libraries/Cache.php b/system/libraries/Cache.php new file mode 100644 index 0000000..c7954d3 --- /dev/null +++ b/system/libraries/Cache.php @@ -0,0 +1,248 @@ +<?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. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Cache_Core { + + protected static $instances = array(); + + // 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 Cache_Exception('The :group: group is not defined in your configuration.', array(':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 Cache_Exception('The :driver: driver for the :class: library could not be found', + array(':driver:' => $this->config['driver'], ':class:' => get_class($this))); + + // Initialize the driver + $this->driver = new $driver($this->config['params']); + + // Validate the driver + if ( ! ($this->driver instanceof Cache_Driver)) + throw new Cache_Exception('The :driver: driver for the :library: library must implement the :interface: interface', + array(':driver:' => $this->config['driver'], ':library:' => get_class($this), ':interface:' => 'Cache_Driver')); + + Kohana_Log::add('debug', 'Cache Library initialized'); + } + + /** + * Set cache items + */ + public function set($key, $value = NULL, $tags = NULL, $lifetime = NULL) + { + if ($lifetime === NULL) + { + $lifetime = $this->config['lifetime']; + } + + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + if ($this->config['prefix'] !== NULL) + { + $key = $this->add_prefix($key); + + if ($tags !== NULL) + { + $tags = $this->add_prefix($tags, FALSE); + } + } + + return $this->driver->set($key, $tags, $lifetime); + } + + /** + * Get a cache items by key + */ + public function get($keys) + { + $single = FALSE; + + if ( ! is_array($keys)) + { + $keys = array($keys); + $single = TRUE; + } + + if ($this->config['prefix'] !== NULL) + { + $keys = $this->add_prefix($keys, FALSE); + + if ( ! $single) + { + return $this->strip_prefix($this->driver->get($keys, $single)); + } + + } + + return $this->driver->get($keys, $single); + } + + /** + * Get cache items by tags + */ + public function get_tag($tags) + { + if ( ! is_array($tags)) + { + $tags = array($tags); + } + + if ($this->config['prefix'] !== NULL) + { + $tags = $this->add_prefix($tags, FALSE); + return $this->strip_prefix($this->driver->get_tag($tags)); + } + else + { + return $this->driver->get_tag($tags); + } + } + + /** + * Delete cache item by key + */ + public function delete($keys) + { + if ( ! is_array($keys)) + { + $keys = array($keys); + } + + if ($this->config['prefix'] !== NULL) + { + $keys = $this->add_prefix($keys, FALSE); + } + + return $this->driver->delete($keys); + } + + /** + * Delete cache items by tag + */ + public function delete_tag($tags) + { + if ( ! is_array($tags)) + { + $tags = array($tags); + } + + if ($this->config['prefix'] !== NULL) + { + $tags = $this->add_prefix($tags, FALSE); + } + + return $this->driver->delete_tag($tags); + } + + /** + * Empty the cache + */ + public function delete_all() + { + return $this->driver->delete_all(); + } + + /** + * Add a prefix to keys or tags + */ + protected function add_prefix($array, $to_key = TRUE) + { + $out = array(); + + foreach($array as $key => $value) + { + if ($to_key) + { + $out[$this->config['prefix'].$key] = $value; + } + else + { + $out[$key] = $this->config['prefix'].$value; + } + } + + return $out; + } + + /** + * Strip a prefix to keys or tags + */ + protected function strip_prefix($array) + { + $out = array(); + + $start = strlen($this->config['prefix']); + + foreach($array as $key => $value) + { + $out[substr($key, $start)] = $value; + } + + return $out; + } + +} // End Cache Library
\ No newline at end of file diff --git a/system/libraries/Cache_Exception.php b/system/libraries/Cache_Exception.php new file mode 100644 index 0000000..706dc09 --- /dev/null +++ b/system/libraries/Cache_Exception.php @@ -0,0 +1,11 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ + +class Cache_Exception_Core extends Kohana_Exception {} +// End Kohana User Exception diff --git a/system/libraries/Controller.php b/system/libraries/Controller.php new file mode 100644 index 0000000..a1139f6 --- /dev/null +++ b/system/libraries/Controller.php @@ -0,0 +1,44 @@ +<?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. + * + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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; + } + } + + /** + * 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'); + } + +} // 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 0000000..253bb15 --- /dev/null +++ b/system/libraries/Database.php @@ -0,0 +1,648 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Database wrapper. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +abstract class Database_Core { + + const SELECT = 1; + const INSERT = 2; + const UPDATE = 3; + const DELETE = 4; + const CROSS_REQUEST = 5; + const PER_REQUEST = 6; + + protected static $instances = array(); + + // Global benchmarks + public static $benchmarks = array(); + + // Last execute query + protected $last_query; + + // Configuration array + protected $config; + + // Required configuration keys + protected $config_required = array(); + + // Raw server connection + protected $connection; + + // Cache (Cache object for cross-request, array for per-request) + protected $cache; + + // Quote character to use for identifiers (tables/columns/aliases) + protected $quote = '"'; + + /** + * Returns a singleton instance of Database. + * + * @param string Database name + * @return Database_Core + */ + public static function instance($name = 'default') + { + if ( ! isset(Database::$instances[$name])) + { + // Load the configuration for this database group + $config = Kohana::config('database.'.$name); + + if (is_string($config['connection'])) + { + // Parse the DSN into connection array + $config['connection'] = Database::parse_dsn($config['connection']); + } + + // Set the driver class name + $driver = 'Database_'.ucfirst($config['connection']['type']); + + // Create the database connection instance + Database::$instances[$name] = new $driver($config); + } + + return Database::$instances[$name]; + } + + /** + * Constructs a new Database object + * + * @param array Database config array + * @return Database_Core + */ + protected function __construct(array $config) + { + // Store the config locally + $this->config = $config; + + if ($this->config['cache'] !== FALSE) + { + if (is_string($this->config['cache'])) + { + // Use Cache library + $this->cache = new Cache($this->config['cache']); + } + elseif ($this->config['cache'] === TRUE) + { + // Use array + $this->cache = array(); + } + } + } + + public function __destruct() + { + $this->disconnect(); + } + + /** + * Connects to the database + * + * @return void + */ + abstract public function connect(); + + /** + * Disconnects from the database + * + * @return void + */ + abstract public function disconnect(); + + /** + * Sets the character set + * + * @return void + */ + abstract public function set_charset($charset); + + /** + * Executes the query + * + * @param string SQL + * @return Database_Result + */ + abstract public function query_execute($sql); + + /** + * Escapes the given value + * + * @param mixed Value + * @return mixed Escaped value + */ + abstract public function escape($value); + + /** + * List constraints for the given table + * + * @param string Table name + * @return array + */ + abstract public function list_constraints($table); + + /** + * List fields for the given table + * + * @param string Table name + * @return array + */ + abstract public function list_fields($table); + + /** + * List tables for the given connection (checks for prefix) + * + * @return array + */ + abstract public function list_tables(); + + /** + * Converts the given DSN string to an array of database connection components + * + * @param string DSN string + * @return array + */ + public static function parse_dsn($dsn) + { + $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('://', $dsn, 2); + + if ($connection[0] === '/') + { + // Strip leading slash + $db['database'] = substr($connection, 1); + } + else + { + $connection = parse_url('http://'.$connection); + + if (isset($connection['user'])) + { + $db['user'] = $connection['user']; + } + + if (isset($connection['pass'])) + { + $db['pass'] = $connection['pass']; + } + + if (isset($connection['port'])) + { + $db['port'] = $connection['port']; + } + + if (isset($connection['host'])) + { + if ($connection['host'] === 'unix(') + { + list($db['socket'], $connection['path']) = explode(')', $connection['path'], 2); + } + else + { + $db['host'] = $connection['host']; + } + } + + if (isset($connection['path']) AND $connection['path']) + { + // Strip leading slash + $db['database'] = substr($connection['path'], 1); + } + } + + return $db; + } + + /** + * Returns the last executed query for this database + * + * @return string + */ + public function last_query() + { + return $this->last_query; + } + + /** + * Executes the given query, returning the cached version if enabled + * + * @param string SQL query + * @return Database_Result + */ + public function query($sql) + { + // Start the benchmark + $start = microtime(TRUE); + + if (is_array($this->cache)) + { + $hash = $this->query_hash($sql); + + if (isset($this->cache[$hash])) + { + // Use cached result + $result = $this->cache[$hash]; + + // It's from cache + $sql .= ' [CACHE]'; + } + else + { + // No cache, execute query and store in cache + $result = $this->cache[$hash] = $this->query_execute($sql); + } + } + else + { + // Execute the query, cache is off + $result = $this->query_execute($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; + } + + /** + * Performs the query on the cache (and caches it if it's not found) + * + * @param string query + * @param int time-to-live (NULL for Cache default) + * @return Database_Cache_Result + */ + public function query_cache($sql, $ttl) + { + if ( ! $this->cache instanceof Cache) + { + throw new Database_Exception('Database :name has not been configured to use the Cache library.'); + } + + // Start the benchmark + $start = microtime(TRUE); + + $hash = $this->query_hash($sql); + + if (($data = $this->cache->get($hash)) !== NULL) + { + // Found in cache, create result + $result = new Database_Cache_Result($data, $sql, $this->config['object']); + + // It's from the cache + $sql .= ' [CACHE]'; + } + else + { + // Run the query and return the full array of rows + $data = $this->query_execute($sql)->as_array(TRUE); + + // Set the Cache + $this->cache->set($hash, $data, NULL, $ttl); + + // Create result + $result = new Database_Cache_Result($data, $sql, $this->config['object']); + } + + // 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; + } + + /** + * Generates a hash for the given query + * + * @param string SQL query string + * @return string + */ + protected function query_hash($sql) + { + return sha1(str_replace("\n", ' ', trim($sql))); + } + + /** + * Clears the internal query cache. + * + * @param mixed clear cache by SQL statement, NULL for all, or TRUE for last query + * @param integer Type of cache to clear, Database::CROSS_REQUEST or Database::PER_REQUEST + * @return Database + */ + public function clear_cache($sql = NULL, $type = NULL) + { + if ($this->cache instanceof Cache AND ($type == NULL OR $type == Database::CROSS_REQUEST)) + { + // Using cross-request Cache library + if ($sql === TRUE) + { + $this->cache->delete($this->query_hash($this->last_query)); + } + elseif (is_string($sql)) + { + $this->cache->delete($this->query_hash($sql)); + } + else + { + $this->cache->delete_all(); + } + } + elseif (is_array($this->cache) AND ($type == NULL OR $type == Database::PER_REQUEST)) + { + // Using per-request memory cache + if ($sql === TRUE) + { + unset($this->cache[$this->query_hash($this->last_query)]); + } + elseif (is_string($sql)) + { + unset($this->cache[$this->query_hash($sql)]); + } + else + { + $this->cache = array(); + } + } + } + + /** + * Quotes the given value + * + * @param mixed value + * @return mixed + */ + public function quote($value) + { + if ( ! $this->config['escape']) + return $value; + + if ($value === NULL) + { + return 'NULL'; + } + elseif ($value === TRUE) + { + return 'TRUE'; + } + elseif ($value === FALSE) + { + return 'FALSE'; + } + elseif (is_int($value)) + { + return (int) $value; + } + elseif ($value instanceof Database_Expression) + { + return (string) $value; + } + elseif (is_float($value)) + { + // Convert to non-locale aware float to prevent possible commas + return sprintf('%F', $value); + } + + return '\''.$this->escape($value).'\''; + } + + /** + * Quotes a table, adding the table prefix + * Reserved characters not allowed in table names for the builder are [ .*] (space, dot, asterisk) + * + * @param string|array table name or array - 'users u' or array('u' => 'users') both valid + * @param string table alias + * @return string + */ + public function quote_table($table, $alias = NULL) + { + if (is_array($table)) + { + // Using array('u' => 'user') + list($alias, $table) = each($table); + } + elseif (strpos(' ', $table) !== FALSE) + { + // Using format 'user u' + list($table, $alias) = explode(' ', $table); + } + + if ($table instanceof Database_Expression) + { + if ($alias) + { + if ($this->config['escape']) + { + $alias = $this->quote.$alias.$this->quote; + } + + return $table.' AS '.$alias; + } + + return (string) $table; + } + + if ($this->config['table_prefix']) + { + $table = $this->config['table_prefix'].$table; + } + + if ($alias) + { + if ($this->config['escape']) + { + $table = $this->quote.$table.$this->quote; + $alias = $this->quote.$alias.$this->quote; + } + + return $table.' AS '.$alias; + } + + if ($this->config['escape']) + { + $table = $this->quote.$table.$this->quote; + } + + return $table; + } + + /** + * Quotes column or table.column, adding the table prefix if necessary + * Reserved characters not allowed in table names for the builder are [ .*] (space, dot, asterisk) + * Complex column names must have table/columns in double quotes, e.g. array('mycount' => 'COUNT("users.id")') + * + * @param string|array column name or array('u' => 'COUNT("*")') + * @param string column alias + * @return string + */ + public function quote_column($column, $alias = NULL) + { + if ($column === '*') + return $column; + + if (is_array($column)) + { + list($alias, $column) = each($column); + } + + if ($column instanceof Database_Expression) + { + if ($alias) + { + if ($this->config['escape']) + { + $alias = $this->quote.$alias.$this->quote; + } + + return $column.' AS '.$alias; + } + + return (string) $column; + } + + if ($this->config['table_prefix'] AND strpos($column, '.') !== FALSE) + { + if (strpos($column, '"') !== FALSE) + { + // Find "table.column" and replace them with "[prefix]table.column" + $column = preg_replace('/"([^.]++)\.([^"]++)"/', '"'.$this->config['table_prefix'].'$1.$2"', $column); + } + else + { + // Attach table prefix if table.column format + $column = $this->config['table_prefix'].$column; + } + } + + if ($this->config['escape']) + { + if (strpos($column, '"') === FALSE) + { + // Quote the column + $column = $this->quote.$column.$this->quote; + } + elseif ($this->quote !== '"') + { + // Replace double quotes + $column = str_replace('"', $this->quote, $column); + } + + // Replace . with "." + $column = str_replace('.', $this->quote.'.'.$this->quote, $column); + + // Unescape any asterisks + $column = str_replace($this->quote.'*'.$this->quote, '*', $column); + + if ($alias) + { + // Quote the alias + return $column.' AS '.$this->quote.$alias.$this->quote; + } + + return $column; + } + + // Strip double quotes + $column = str_replace('"', '', $column); + + if ($alias) + return $column.' AS '.$alias; + + return $column; + } + + /** + * Get the table prefix + * + * @param string Optional new table prefix to set + * @return string + */ + public function table_prefix($new_prefix = NULL) + { + $prefix = $this->config['table_prefix']; + + if ($new_prefix !== NULL) + { + // Set a new prefix + $this->config['table_prefix'] = $new_prefix; + } + + return $prefix; + } + + /** + * 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 = trim($str); + + if (($open = strpos($str, '(')) !== FALSE) + { + // Closing bracket + $close = strpos($str, ')', $open); + + // Length without brackets + $length = substr($str, $open + 1, $close - 1 - $open); + + // Type without the length + $type = substr($str, 0, $open).substr($str, $close + 1); + } + else + { + // No length + $type = $str; + } + + if (empty($sql_types[$type])) + throw new Database_Exception('Undefined field type :type', array(':type' => $str)); + + // Fetch the field definition + $field = $sql_types[$type]; + + $field['sql_type'] = $type; + + if (isset($length)) + { + // Add the length to the field info + $field['length'] = $length; + } + + return $field; + } + +} // End Database diff --git a/system/libraries/Database_Builder.php b/system/libraries/Database_Builder.php new file mode 100644 index 0000000..e86ce37 --- /dev/null +++ b/system/libraries/Database_Builder.php @@ -0,0 +1,1241 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * The Database Query Builder provides methods for creating database agnostic queries and + * data manipulation. + * + * ##### A basic select query + * + * $builder = new Database_Builder; + * $kohana = $builder + * ->select() + * ->where('name', '=', 'Kohana') + * ->from('frameworks') + * ->execute(); + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Builder_Core { + + // Valid ORDER BY directions + protected $order_directions = array('ASC', 'DESC', 'RAND()'); + + // Database object + protected $db; + + // Builder members + protected $select = array(); + protected $from = array(); + protected $join = array(); + protected $where = array(); + protected $group_by = array(); + protected $having = array(); + protected $order_by = array(); + protected $limit = NULL; + protected $offset = NULL; + protected $set = array(); + protected $columns = array(); + protected $values = array(); + protected $type; + protected $distinct = FALSE; + protected $reset = TRUE; + + // TTL for caching (using Cache library) + protected $ttl = FALSE; + + public function __construct($db = 'default') + { + $this->db = $db; + } + + /** + * Compiles the builder object into a SQL query. Useful for debugging + * + * ##### Example + * + * echo $builder->select()->from('products'); + * // Output: SELECT * FROM `products` + * + * @return string Compiled query + */ + public function __toString() + { + return $this->compile(); + } + + /** + * Creates a `SELECT` query with support for column aliases, database functions, + * subqueries or a [Database_Expression] + * + * ##### Examples + * + * // Simple select + * echo $builder->select()->from('products'); + * + * // Select with database function + * echo $builder->select(array('records_found' => 'COUNT("*")'))->from('products'); + * + * // Select with sub query + * echo $builder->select(array('field', 'test' => db::select('test')->from('table')))->from('products'); + * + * @chainable + * @param string|array column name or array(alias => column) + * @return Database_Builder + */ + public function select($columns = NULL) + { + $this->type = Database::SELECT; + + if ($columns === NULL) + { + $columns = array('*'); + } + elseif ( ! is_array($columns)) + { + $columns = func_get_args(); + } + + $this->select = array_merge($this->select, $columns); + + return $this; + } + + /** + * Creates a `DISTINCT SELECT` query. For more information see see [Database_Builder::select]. + * + * @chainable + * @param string|array column name or array(alias => column) + * @return Database_Builder + */ + public function select_distinct($columns = NULL) + { + $this->select($columns); + $this->distinct = TRUE; + return $this; + } + + /** + * Add tables to the FROM portion of the builder + * + * ##### Example + * + * $builder->select()->from('products') + * ->from(array('other' => 'other_table')); + * // Output: SELECT * FROM `products`, `other_table` AS `other` + * + * @chainable + * @param string|array table name or array(alias => table) + * @return Database_Builder + */ + public function from($tables) + { + if ( ! is_array($tables)) + { + $tables = func_get_args(); + } + + $this->from = array_merge($this->from, $tables); + + return $this; + } + + /** + * Add conditions to the `WHERE` clause. Alias for [Database_Builder::and_where]. + * + * @chainable + * @param mixed Column name or array of columns => vals + * @param string Operation to perform + * @param mixed Value + * @return Database_Builder + */ + public function where($columns, $op = '=', $value = NULL) + { + return $this->and_where($columns, $op, $value); + } + + /** + * Add conditions to the `WHERE` clause separating multiple conditions with `AND`. + * This function supports all `WHERE` operators including `LIKE` and `IN`. It can + * also be used with a [Database_Expression] or subquery. + * + * ##### Examples + * + * // Basic where condition + * $builder->where('field', '=', 'value'); + * + * // Multiple conditions with an array (you can also chain where() function calls) + * $builder->where(array(array('field', '=', 'value'), array(...))); + * + * // With a database expression + * $builder->where('field', '=', db::expr('field + 1')); + * // or a function + * $builder->where('field', '=', db::expr('UNIX_TIMESTAMP()')); + * + * // With a subquery + * $builder->where('field', 'IN', db::select('id')->from('table')); + * + * [!!] You must manually escape all data you pass into a database expression! + * + * @chainable + * @param mixed Column name or array of triplets + * @param string Operation to perform + * @param mixed Value + * @return Database_Builder + */ + public function and_where($columns, $op = '=', $value = NULL) + { + if (is_array($columns)) + { + foreach ($columns as $column) + { + $this->where[] = array('AND' => $column); + } + } + else + { + $this->where[] = array('AND' => array($columns, $op, $value)); + } + return $this; + } + + /** + * Add conditions to the `WHERE` clause separating multiple conditions with `OR`. + * For more information about building a `WHERE` clause see [Database_Builder::and_where] + * + * @chainable + * @param mixed Column name or array of triplets + * @param string Operation to perform + * @param mixed Value + * @return Database_Builder + */ + public function or_where($columns, $op = '=', $value = NULL) + { + if (is_array($columns)) + { + foreach ($columns as $column) + { + $this->where[] = array('OR' => $column); + } + } + else + { + $this->where[] = array('OR' => array($columns, $op, $value)); + } + return $this; + } + + /** + * Join tables to the builder + * + * ##### Example + * + * // Basic join + * db::select()->from('products') + * ->join('reviews', 'reviews.product_id', 'products.id'); + * + * // Advanced joins + * echo db::select()->from('products') + * ->join('reviews', 'field', db::expr('advanced condition here'), 'RIGHT'); + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @param string Join type (LEFT, RIGHT, INNER, etc.) + * @return Database_Builder + */ + public function join($table, $keys, $value = NULL, $type = NULL) + { + if (is_string($keys)) + { + $keys = array($keys => $value); + } + + if ($type !== NULL) + { + $type = strtoupper($type); + } + + $this->join[] = array($table, $keys, $type); + + return $this; + } + + /** + * This function is an alias for [Database_Builder::join] + * with the join type set to `LEFT`. + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @return Database_Builder + */ + public function left_join($table, $keys, $value = NULL) + { + return $this->join($table, $keys, $value, 'LEFT'); + } + + /** + * This function is an alias for [Database_Builder::join] + * with the join type set to `RIGHT`. + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @return Database_Builder + */ + public function right_join($table, $keys, $value = NULL) + { + return $this->join($table, $keys, $value, 'RIGHT'); + } + + /** + * This function is an alias for [Database_Builder::join] + * with the join type set to `INNER`. + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @return Database_Builder + */ + public function inner_join($table, $keys, $value = NULL) + { + return $this->join($table, $keys, $value, 'INNER'); + } + + /** + * This function is an alias for [Database_Builder::join] + * with the join type set to `OUTER`. + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @return Database_Builder + */ + public function outer_join($table, $keys, $value = NULL) + { + return $this->join($table, $keys, $value, 'OUTER'); + } + + /** + * This function is an alias for [Database_Builder::join] + * with the join type set to `FULL`. + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @return Database_Builder + */ + public function full_join($table, $keys, $value = NULL) + { + return $this->join($table, $keys, $value, 'FULL'); + } + + /** + * This function is an alias for [Database_Builder::join] + * with the join type set to `LEFT INNER`. + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @return Database_Builder + */ + public function left_inner_join($table, $keys, $value = NULL) + { + return $this->join($table, $keys, $value, 'LEFT INNER'); + } + + /** + * This function is an alias for [Database_Builder::join] + * with the join type set to `RIGHT INNER`. + * + * @chainable + * @param mixed Table name + * @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression) + * @param mixed Value if $keys is not an array or Database_Expression + * @return Database_Builder + */ + public function right_inner_join($table, $keys, $value = NULL) + { + return $this->join($table, $keys, $value, 'RIGHT INNER'); + } + + /** + * Add fields to the GROUP BY portion + * + * ##### Example + * + * db::select()->from('products') + * ->group_by(array('name', 'cat_id')); + * // Output: SELECT * FROM `products` GROUP BY `name`, `cat_id` + * + * @chainable + * @param mixed Field names or an array of fields + * @return Database_Builder + */ + public function group_by($columns) + { + if ( ! is_array($columns)) + { + $columns = func_get_args(); + } + + $this->group_by = array_merge($this->group_by, $columns); + + return $this; + } + + /** + * Add conditions to the HAVING clause (AND) + * + * @chainable + * @param mixed Column name or array of columns => vals + * @param string Operation to perform + * @param mixed Value + * @return Database_Builder + */ + public function having($columns, $op = '=', $value = NULL) + { + return $this->and_having($columns, $op, $value); + } + + /** + * Add conditions to the HAVING clause (AND) + * + * @chainable + * @param mixed Column name or array of triplets + * @param string Operation to perform + * @param mixed Value + * @return Database_Builder + */ + public function and_having($columns, $op = '=', $value = NULL) + { + if (is_array($columns)) + { + foreach ($columns as $column) + { + $this->having[] = array('AND' => $column); + } + } + else + { + $this->having[] = array('AND' => array($columns, $op, $value)); + } + return $this; + } + + /** + * Add conditions to the HAVING clause (OR) + * + * @chainable + * @param mixed Column name or array of triplets + * @param string Operation to perform + * @param mixed Value + * @return Database_Builder + */ + public function or_having($columns, $op = '=', $value = NULL) + { + if (is_array($columns)) + { + foreach ($columns as $column) + { + $this->having[] = array('OR' => $column); + } + } + else + { + $this->having[] = array('OR' => array($columns, $op, $value)); + } + return $this; + } + + /** + * Add fields to the ORDER BY portion + * + * @chainable + * @param mixed Field names or an array of fields (field => direction) + * @param string Direction or NULL for ascending + * @return Database_Builder + */ + public function order_by($columns, $direction = NULL) + { + if (is_array($columns)) + { + foreach ($columns as $column => $direction) + { + if (is_string($column)) + { + $this->order_by[] = array($column => $direction); + } + else + { + // $direction is the column name when the array key is numeric + $this->order_by[] = array($direction => NULL); + } + } + } + else + { + $this->order_by[] = array($columns => $direction); + } + return $this; + } + + /** + * Limit rows returned + * + * @chainable + * @param int Number of rows + * @return Database_Builder + */ + public function limit($number) + { + $this->limit = (int) $number; + + return $this; + } + + /** + * Offset into result set + * + * @chainable + * @param int Offset + * @return Database_Builder + */ + public function offset($number) + { + $this->offset = (int) $number; + + return $this; + } + + /** + * Alias for [Database_Builder::and_open] + * + * @chainable + * @param string Clause (WHERE OR HAVING) + * @return Database_Builder + */ + public function open($clause = 'WHERE') + { + return $this->and_open($clause); + } + + /** + * Open new **ANDs** parenthesis set + * + * @chainable + * @param string Clause (WHERE OR HAVING) + * @return Database_Builder + */ + public function and_open($clause = 'WHERE') + { + if ($clause === 'WHERE') + { + $this->where[] = array('AND' => '('); + } + else + { + $this->having[] = array('AND' => '('); + } + + return $this; + } + + /** + * Open new **OR** parenthesis set + * + * @chainable + * @param string Clause (WHERE OR HAVING) + * @return Database_Builder + */ + public function or_open($clause = 'WHERE') + { + if ($clause === 'WHERE') + { + $this->where[] = array('OR' => '('); + } + else + { + $this->having[] = array('OR' => '('); + } + + return $this; + } + + /** + * Close close parenthesis set + * + * @chainable + * @param string Clause (WHERE OR HAVING) + * @return Database_Builder + */ + public function close($clause = 'WHERE') + { + if ($clause === 'WHERE') + { + $this->where[] = array(')'); + } + else + { + $this->having[] = array(')'); + } + + return $this; + } + + /** + * Set values for UPDATE + * + * @chainable + * @param mixed Column name or array of columns => vals + * @param mixed Value (can be a Database_Expression) + * @return Database_Builder + */ + public function set($keys, $value = NULL) + { + if (is_string($keys)) + { + $keys = array($keys => $value); + } + + $this->set = array_merge($keys, $this->set); + + return $this; + } + + /** + * Columns used for INSERT queries + * + * @chainable + * @param array Columns + * @return Database_Builder + */ + public function columns($columns) + { + if ( ! is_array($columns)) + { + $columns = func_get_args(); + } + + $this->columns = $columns; + + return $this; + } + + /** + * Values used for INSERT queries + * + * @chainable + * @param array Values + * @return Database_Builder + */ + public function values($values) + { + if ( ! is_array($values)) + { + $values = func_get_args(); + } + + $this->values[] = $values; + + return $this; + } + + /** + * Set caching for the query + * + * @chainable + * @param mixed Time-to-live (FALSE to disable, NULL for Cache default, seconds otherwise) + * @return Database_Builder + */ + public function cache($ttl = NULL) + { + $this->ttl = $ttl; + + return $this; + } + + /** + * Resets the database builder after execution. By default after you `execute()` a query + * the database builder will reset to its default state. You can use `reset(FALSE)` + * to stop this from happening. This is useful for pagination when you might want to + * apply a limit to the previous query. + * + * ##### Example + * + * $db = new Database_Builder; + * $all_results = $db->select() + * ->where('id', '=', 3) + * ->from('products') + * ->reset(FALSE) + * ->execute(); + * + * // Run the query again with a limit of 10 + * $ten_results = $db->limit(10) + * ->execute(); + * @chainable + * @param bool reset builder + * @return Database_Builder + */ + public function reset($reset = TRUE) + { + $this->reset = (bool) $reset; + return $this; + } + + /** + * Compiles the given clause's conditions + * + * @param array Clause conditions + * @return string + */ + protected function compile_conditions($groups) + { + $last_condition = NULL; + + $sql = ''; + foreach ($groups as $group) + { + // Process groups of conditions + foreach ($group as $logic => $condition) + { + if ($condition === '(') + { + if ( ! empty($sql) AND $last_condition !== '(') + { + // Include logic operator + $sql .= ' '.$logic.' '; + } + + $sql .= '('; + } + elseif ($condition === ')') + { + $sql .= ')'; + } + else + { + list($columns, $op, $value) = $condition; + + // Stores each individual condition + $vals = array(); + + if ($columns instanceof Database_Expression) + { + // Add directly to condition list + $vals[] = (string) $columns; + } + else + { + $op = strtoupper($op); + + if ( ! is_array($columns)) + { + $columns = array($columns => $value); + } + + foreach ($columns as $column => $value) + { + if ($value instanceof Database_Builder) + { + // Using a subquery + $value->db = $this->db; + $value = '('.(string) $value.')'; + } + elseif (is_array($value)) + { + if ($op === 'BETWEEN' OR $op === 'NOT BETWEEN') + { + // Falls between two values + $value = $this->db->quote($value[0]).' AND '.$this->db->quote($value[1]); + } + else + { + // Return as list + $value = array_map(array($this->db, 'quote'), $value); + $value = '('.implode(', ', $value).')'; + } + } + else + { + $value = $this->db->quote($value); + } + + if ( ! empty($column)) + { + // Ignore blank columns + $column = $this->db->quote_column($column); + } + + // Add to condition list + $vals[] = $column.' '.$op.' '.$value; + } + } + + if ( ! empty($sql) AND $last_condition !== '(') + { + // Add the logic operator + $sql .= ' '.$logic.' '; + } + + // Join the condition list items together by the given logic operator + $sql .= implode(' '.$logic.' ', $vals); + } + + $last_condition = $condition; + } + } + + return $sql; + } + + /** + * Compiles the columns portion of the query for INSERT + * + * @return string + */ + protected function compile_columns() + { + return '('.implode(', ', array_map(array($this->db, 'quote_column'), $this->columns)).')'; + } + + /** + * Compiles the VALUES portion of the query for INSERT + * + * @return string + */ + protected function compile_values() + { + $values = array(); + foreach ($this->values as $group) + { + // Each set of values to be inserted + $values[] = '('.implode(', ', array_map(array($this->db, 'quote'), $group)).')'; + } + + return implode(', ', $values); + } + + /** + * Create an UPDATE query + * + * @chainable + * @param string Table name + * @param array Array of Keys => Values + * @param array WHERE conditions + * @return Database_Builder + */ + public function update($table = NULL, $set = NULL, $where = NULL) + { + $this->type = Database::UPDATE; + + if (is_array($set)) + { + $this->set($set); + } + + if ($where !== NULL) + { + $this->where($where); + } + + if ($table !== NULL) + { + $this->from($table); + } + + return $this; + } + + /** + * Create an INSERT query. Use 'columns' and 'values' methods for multi-row inserts + * + * @chainable + * @param string Table name + * @param array Array of Keys => Values + * @return Database_Builder + */ + public function insert($table = NULL, $set = NULL) + { + $this->type = Database::INSERT; + + if (is_array($set)) + { + $this->columns(array_keys($set)); + $this->values(array_values($set)); + } + + if ($table !== NULL) + { + $this->from($table); + } + + return $this; + } + + /** + * Create a DELETE query + * + * @chainable + * @param string Table name + * @param array WHERE conditions + * @return Database_Builder + */ + public function delete($table, $where = NULL) + { + $this->type = Database::DELETE; + + if ($where !== NULL) + { + $this->where($where); + } + + if ($table !== NULL) + { + $this->from($table); + } + + return $this; + } + + /** + * Count records for a given table + * + * @param string Table name + * @param array WHERE conditions + * @return int + */ + public function count_records($table = FALSE, $where = NULL) + { + if (count($this->from) < 1) + { + if ($table === FALSE) + throw new Database_Exception('Database count_records requires a table'); + + $this->from($table); + } + + if ($where !== NULL) + { + $this->where($where); + } + + // Grab the count AS records_found + $result = $this->select(array('records_found' => 'COUNT("*")'))->execute(); + + return $result->get('records_found'); + } + + /** + * Executes the built query + * + * @param mixed Database name or object + * @return Database_Result + */ + public function execute($db = NULL) + { + if ($db !== NULL) + { + $this->db = $db; + } + + if ( ! is_object($this->db)) + { + // Get the database instance + $this->db = Database::instance($this->db); + } + + $query = $this->compile(); + + if ($this->reset) + { + // Reset the query after executing + $this->_reset(); + } + + if ($this->ttl !== FALSE AND $this->type === Database::SELECT) + { + // Return result from cache (only allowed with SELECT) + return $this->db->query_cache($query, $this->ttl); + } + else + { + // Load the result (no caching) + return $this->db->query($query); + } + } + + /** + * Compiles the builder object into a SQL query + * + * @return string Compiled query + */ + protected function compile() + { + if ( ! is_object($this->db)) + { + // Use default database for compiling to string if none is given + $this->db = Database::instance($this->db); + } + + if ($this->type === Database::SELECT) + { + // SELECT columns FROM table + $sql = $this->distinct ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= $this->compile_select(); + + if ( ! empty($this->from)) + { + $sql .= "\nFROM ".$this->compile_from(); + } + } + elseif ($this->type === Database::UPDATE) + { + $sql = 'UPDATE '.$this->compile_from()."\n".'SET '.$this->compile_set(); + } + elseif ($this->type === Database::INSERT) + { + $sql = 'INSERT INTO '.$this->compile_from()."\n".$this->compile_columns()."\nVALUES ".$this->compile_values(); + } + elseif ($this->type === Database::DELETE) + { + $sql = 'DELETE FROM '.$this->compile_from(); + } + + if ( ! empty($this->join)) + { + $sql .= $this->compile_join(); + } + + if ( ! empty($this->where)) + { + $sql .= "\n".'WHERE '.$this->compile_conditions($this->where); + } + + if ( ! empty($this->group_by)) + { + $sql .= "\n".'GROUP BY '.$this->compile_group_by(); + } + + if ( ! empty($this->having)) + { + $sql .= "\n".'HAVING '.$this->compile_conditions($this->having); + } + + if ( ! empty($this->order_by)) + { + $sql .= "\nORDER BY ".$this->compile_order_by(); + } + + if (is_int($this->limit)) + { + $sql .= "\nLIMIT ".$this->limit; + } + + if (is_int($this->offset)) + { + $sql .= "\nOFFSET ".$this->offset; + } + + return $sql; + } + + /** + * Compiles the SELECT portion of the query + * + * @return string + */ + protected function compile_select() + { + $vals = array(); + + foreach ($this->select as $alias => $name) + { + if ($name instanceof Database_Builder) + { + // Using a subquery + $name->db = $this->db; + $vals[] = '('.(string) $name.') AS '.$this->db->quote_column($alias); + } + elseif (is_string($alias)) + { + $vals[] = $this->db->quote_column($name, $alias); + } + else + { + $vals[] = $this->db->quote_column($name); + } + } + + return implode(', ', $vals); + } + + /** + * Compiles the FROM portion of the query + * + * @return string + */ + protected function compile_from() + { + $vals = array(); + + foreach ($this->from as $alias => $name) + { + if (is_string($alias)) + { + // Using AS format so escape both + $vals[] = $this->db->quote_table($name, $alias); + } + else + { + // Just using the table name itself + $vals[] = $this->db->quote_table($name); + } + } + + return implode(', ', $vals); + } + + /** + * Compiles the JOIN portion of the query + * + * @return string + */ + protected function compile_join() + { + $sql = ''; + foreach ($this->join as $join) + { + list($table, $keys, $type) = $join; + + if ($type !== NULL) + { + // Join type + $sql .= ' '.$type; + } + + $sql .= ' JOIN '.$this->db->quote_table($table); + + $condition = ''; + if ($keys instanceof Database_Expression) + { + $condition = (string) $keys; + } + elseif (is_array($keys)) + { + // ON condition is an array of matches + foreach ($keys as $key => $value) + { + if ( ! empty($condition)) + { + $condition .= ' AND '; + } + + $condition .= $this->db->quote_column($key).' = '.$this->db->quote_column($value); + } + } + + if ( ! empty($condition)) + { + // Add ON condition + $sql .= ' ON ('.$condition.')'; + } + } + + return $sql; + } + + /** + * Compiles the GROUP BY portion of the query + * + * @return string + */ + protected function compile_group_by() + { + $vals = array(); + + foreach ($this->group_by as $column) + { + // Escape the column + $vals[] = $this->db->quote_column($column); + } + + return implode(', ', $vals); + } + + /** + * Compiles the ORDER BY portion of the query + * + * @return string + */ + protected function compile_order_by() + { + $ordering = array(); + + foreach ($this->order_by as $column => $order_by) + { + list($column, $direction) = each($order_by); + + $column = $this->db->quote_column($column); + + if ($direction !== NULL) + { + $direction = ' '.$direction; + } + + $ordering[] = $column.$direction; + } + + return implode(', ', $ordering); + } + + /** + * Compiles the SET portion of the query for UPDATE + * + * @return string + */ + protected function compile_set() + { + $vals = array(); + + foreach ($this->set as $key => $value) + { + // Using an UPDATE so Key = Val + $vals[] = $this->db->quote_column($key).' = '.$this->db->quote($value); + } + + return implode(', ', $vals); + } + + /** + * Resets all query components + */ + protected function _reset() + { + $this->select = array(); + $this->from = array(); + $this->join = array(); + $this->where = array(); + $this->group_by = array(); + $this->having = array(); + $this->order_by = array(); + $this->limit = NULL; + $this->offset = NULL; + $this->set = array(); + $this->values = array(); + $this->type = NULL; + $this->distinct = FALSE; + $this->reset = TRUE; + $this->ttl = FALSE; + } + +} // End Database_Builder diff --git a/system/libraries/Database_Cache_Result.php b/system/libraries/Database_Cache_Result.php new file mode 100644 index 0000000..3945c42 --- /dev/null +++ b/system/libraries/Database_Cache_Result.php @@ -0,0 +1,81 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Cached database result. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Cache_Result_Core extends Database_Result { + + /** + * Result data (array of rows) + * @var array + */ + protected $data; + + public function __construct($data, $sql, $return_objects) + { + $this->data = $data; + $this->sql = $sql; + $this->total_rows = count($data); + $this->return_objects = $return_objects; + } + + public function __destruct() + { + // Not used + } + + public function as_array($return = FALSE) + { + // Return arrays rather than objects + $this->return_objects = FALSE; + + if ( ! $return ) + { + // Return this result object + return $this; + } + + // Return the entire array of rows + return $this->data; + } + + public function as_object($class = NULL, $return = FALSE) + { + if ($class !== NULL) + throw new Database_Exception('Database cache results do not support object casting'); + + // Return objects of type $class (or stdClass if none given) + $this->return_objects = TRUE; + + return $this; + } + + public function seek($offset) + { + if ( ! $this->offsetExists($offset)) + return FALSE; + + $this->current_row = $offset; + + return TRUE; + } + + public function current() + { + if ($this->return_objects) + { + // Return a new object with the current row of data + return (object) $this->data[$this->current_row]; + } + else + { + // Return an array of the row + return $this->data[$this->current_row]; + } + } + +} // End Database_Cache_Result
\ No newline at end of file diff --git a/system/libraries/Database_Exception.php b/system/libraries/Database_Exception.php new file mode 100644 index 0000000..0f6bb75 --- /dev/null +++ b/system/libraries/Database_Exception.php @@ -0,0 +1,15 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Database exceptions. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Exception_Core extends Kohana_Exception { + + // Database error code + protected $code = E_DATABASE_ERROR; + +} // End Database_Exception
\ No newline at end of file diff --git a/system/libraries/Database_Expression.php b/system/libraries/Database_Expression.php new file mode 100644 index 0000000..007a0cb --- /dev/null +++ b/system/libraries/Database_Expression.php @@ -0,0 +1,23 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Database expression. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Expression_Core { + + protected $expression; + + public function __construct($expression) + { + $this->expression = $expression; + } + + public function __toString() + { + return $this->expression; + } +} diff --git a/system/libraries/Database_Mysql.php b/system/libraries/Database_Mysql.php new file mode 100644 index 0000000..a325cbc --- /dev/null +++ b/system/libraries/Database_Mysql.php @@ -0,0 +1,226 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * MySQL database connection. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Mysql_Core extends Database { + + // Quote character to use for identifiers (tables/columns/aliases) + protected $quote = '`'; + + // Use SET NAMES to set the character set + protected static $set_names; + + public function connect() + { + if ($this->connection) + return; + + if (Database_Mysql::$set_names === NULL) + { + // Determine if we can use mysql_set_charset(), which is only + // available on PHP 5.2.3+ when compiled against MySQL 5.0+ + Database_Mysql::$set_names = ! function_exists('mysql_set_charset'); + } + + extract($this->config['connection']); + + $host = isset($host) ? $host : $socket; + $port = isset($port) ? ':'.$port : ''; + + try + { + // Connect to the database + $this->connection = ($this->config['persistent'] === TRUE) + ? mysql_pconnect($host.$port, $user, $pass, $params) + : mysql_connect($host.$port, $user, $pass, TRUE, $params); + } + catch (Kohana_PHP_Exception $e) + { + // No connection exists + $this->connection = NULL; + + // Unable to connect to the database + throw new Database_Exception('#:errno: :error', + array(':error' => mysql_error(), + ':errno' => mysql_errno())); + } + + if ( ! mysql_select_db($database, $this->connection)) + { + // Unable to select database + throw new Database_Exception('#:errno: :error', + array(':error' => mysql_error($this->connection), + ':errno' => mysql_errno($this->connection))); + } + + if (isset($this->config['character_set'])) + { + // Set the character set + $this->set_charset($this->config['character_set']); + } + } + + public function disconnect() + { + try + { + // Database is assumed disconnected + $status = TRUE; + + if (is_resource($this->connection)) + { + $status = mysql_close($this->connection); + } + } + catch (Exception $e) + { + // Database is probably not disconnected + $status = is_resource($this->connection); + } + + return $status; + } + + public function set_charset($charset) + { + // Make sure the database is connected + $this->connection or $this->connect(); + + if (Database_Mysql::$set_names === TRUE) + { + // PHP is compiled against MySQL 4.x + $status = (bool) mysql_query('SET NAMES '.$this->quote($charset), $this->connection); + } + else + { + // PHP is compiled against MySQL 5.x + $status = mysql_set_charset($charset, $this->connection); + } + + if ($status === FALSE) + { + // Unable to set charset + throw new Database_Exception('#:errno: :error', + array(':error' => mysql_error($this->connection), + ':errno' => mysql_errno($this->connection))); + } + } + + public function query_execute($sql) + { + // Make sure the database is connected + $this->connection or $this->connect(); + + $result = mysql_query($sql, $this->connection); + + // Set the last query + $this->last_query = $sql; + + return new Database_Mysql_Result($result, $sql, $this->connection, $this->config['object']); + } + + public function escape($value) + { + // Make sure the database is connected + $this->connection or $this->connect(); + + if (($value = mysql_real_escape_string($value, $this->connection)) === FALSE) + { + throw new Database_Exception('#:errno: :error', + array(':error' => mysql_error($this->connection), + ':errno' => mysql_errno($this->connection))); + } + + return $value; + } + + public function list_constraints($table) + { + $prefix = strlen($this->table_prefix()); + $result = array(); + + $constraints = $this->query(' + SELECT c.constraint_name, c.constraint_type, k.column_name, k.referenced_table_name, k.referenced_column_name + FROM information_schema.table_constraints c + JOIN information_schema.key_column_usage k ON (k.table_schema = c.table_schema AND k.table_name = c.table_name AND k.constraint_name = c.constraint_name) + WHERE c.table_schema = '.$this->quote($this->config['connection']['database']).' + AND c.table_name = '.$this->quote($this->table_prefix().$table).' + AND (k.referenced_table_schema IS NULL OR k.referenced_table_schema ='.$this->quote($this->config['connection']['database']).') + ORDER BY k.ordinal_position + '); + + foreach ($constraints->as_array() as $row) + { + switch ($row['constraint_type']) + { + case 'FOREIGN KEY': + if (isset($result[$row['constraint_name']])) + { + $result[$row['constraint_name']][1][] = $row['column_name']; + $result[$row['constraint_name']][3][] = $row['referenced_column_name']; + } + else + { + $result[$row['constraint_name']] = array($row['constraint_type'], array($row['column_name']), substr($row['referenced_table_name'], $prefix), array($row['referenced_column_name'])); + } + break; + case 'PRIMARY KEY': + case 'UNIQUE': + if (isset($result[$row['constraint_name']])) + { + $result[$row['constraint_name']][1][] = $row['column_name']; + } + else + { + $result[$row['constraint_name']] = array($row['constraint_type'], array($row['column_name'])); + } + break; + } + } + + return $result; + } + + public function list_fields($table) + { + $result = array(); + + foreach ($this->query('SHOW COLUMNS FROM '.$this->quote_table($table))->as_array() as $row) + { + $column = $this->sql_type($row['Type']); + + $column['default'] = $row['Default']; + $column['nullable'] = $row['Null'] === 'YES'; + $column['sequenced'] = $row['Extra'] === 'auto_increment'; + + if (isset($column['length']) AND $column['type'] === 'float') + { + list($column['precision'], $column['scale']) = explode(',', $column['length']); + } + + $result[$row['Field']] = $column; + } + + return $result; + } + + public function list_tables() + { + $prefix = strlen($this->table_prefix()); + $tables = array(); + + foreach ($this->query('SHOW TABLES LIKE '.$this->quote($this->table_prefix().'%'))->as_array() as $row) + { + // The value is the table name + $tables[] = substr(current($row), $prefix); + } + + return $tables; + } + +} // End Database_MySQL diff --git a/system/libraries/Database_Mysql_Result.php b/system/libraries/Database_Mysql_Result.php new file mode 100644 index 0000000..0f89898 --- /dev/null +++ b/system/libraries/Database_Mysql_Result.php @@ -0,0 +1,174 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * MySQL database result. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Mysql_Result_Core extends Database_Result { + + protected $internal_row = 0; + + public function __construct($result, $sql, $link, $return_objects) + { + if (is_resource($result)) + { + // True to return objects, false for arrays + $this->return_objects = $return_objects; + + $this->total_rows = mysql_num_rows($result); + } + elseif (is_bool($result)) + { + if ($result == FALSE) + { + throw new Database_Exception('#:errno: :error [ :query ]', + array(':error' => mysql_error($link), + ':query' => $sql, + ':errno' => mysql_errno($link))); + + } + else + { + // It's a DELETE, INSERT, REPLACE, or UPDATE query + $this->insert_id = mysql_insert_id($link); + $this->total_rows = mysql_affected_rows($link); + } + } + + // Store the result locally + $this->result = $result; + + $this->sql = $sql; + } + + public function __destruct() + { + if (is_resource($this->result)) + { + mysql_free_result($this->result); + } + } + + public function as_array($return = FALSE) + { + // Return arrays rather than objects + $this->return_objects = FALSE; + + if ( ! $return ) + { + // Return this result object + return $this; + } + + // Return a nested array of all results + $array = array(); + + if ($this->total_rows > 0) + { + // Seek to the beginning of the result + mysql_data_seek($this->result, 0); + + while ($row = mysql_fetch_assoc($this->result)) + { + // Add each row to the array + $array[] = $row; + } + + $this->internal_row = $this->total_rows; + } + + return $array; + } + + public function as_object($class = NULL, $return = FALSE) + { + // Return objects of type $class (or stdClass if none given) + $this->return_objects = ($class !== NULL) ? $class : TRUE; + + if ( ! $return ) + { + // Return this result object + return $this; + } + + // Return a nested array of all results + $array = array(); + + if ($this->total_rows > 0) + { + // Seek to the beginning of the result + mysql_data_seek($this->result, 0); + + if (is_string($this->return_objects)) + { + while ($row = mysql_fetch_object($this->result, $this->return_objects)) + { + // Add each row to the array + $array[] = $row; + } + } + else + { + while ($row = mysql_fetch_object($this->result)) + { + // Add each row to the array + $array[] = $row; + } + } + + $this->internal_row = $this->total_rows; + } + + return $array; + } + + /** + * SeekableIterator: seek + */ + 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 = $this->internal_row = $offset; + + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Iterator: current + */ + public function current() + { + if ($this->current_row !== $this->internal_row AND ! $this->seek($this->current_row)) + return NULL; + + ++$this->internal_row; + + if ($this->return_objects) + { + if (is_string($this->return_objects)) + { + return mysql_fetch_object($this->result, $this->return_objects); + } + else + { + return mysql_fetch_object($this->result); + } + } + else + { + // Return an array of the row + return mysql_fetch_assoc($this->result); + } + } + +} // End Database_MySQL_Result
\ No newline at end of file diff --git a/system/libraries/Database_Mysqli.php b/system/libraries/Database_Mysqli.php new file mode 100644 index 0000000..41b635d --- /dev/null +++ b/system/libraries/Database_Mysqli.php @@ -0,0 +1,90 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * MySQL database connection. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ + +define('RUNS_MYSQLND', function_exists('mysqli_fetch_all')); + +class Database_Mysqli_Core extends Database_Mysql { + + public function connect() + { + if (is_object($this->connection)) + return; + + extract($this->config['connection']); + + // Persistent connections are supported as of PHP 5.3 + if (RUNS_MYSQLND AND $this->config['persistent'] === TRUE) + { + $host = 'p:'.$host; + } + + $host = isset($host) ? $host : $socket; + + $mysqli = mysqli_init(); + + if ( ! $mysqli->real_connect($host, $user, $pass, $database, $port, $socket, $params)) + throw new Database_Exception('#:errno: :error', + array(':error' => $mysqli->connect_error, ':errno' => $mysqli->connect_errno)); + + $this->connection = $mysqli; + + if (isset($this->config['character_set'])) + { + // Set the character set + $this->set_charset($this->config['character_set']); + } + } + + public function disconnect() + { + if (is_object($this->connection)) + { + $this->connection->close(); + } + + $this->connection = NULL; + } + + public function set_charset($charset) + { + // Make sure the database is connected + is_object($this->connection) or $this->connect(); + + if ( ! $this->connection->set_charset($charset)) + { + // Unable to set charset + throw new Database_Exception('#:errno: :error', + array(':error' => $this->connection->connect_error, + ':errno' => $this->connection->connect_errno)); + } + } + + public function query_execute($sql) + { + // Make sure the database is connected + is_object($this->connection) or $this->connect(); + + $result = $this->connection->query($sql); + + // Set the last query + $this->last_query = $sql; + + return new Database_Mysqli_Result($result, $sql, $this->connection, $this->config['object']); + } + + public function escape($value) + { + // Make sure the database is connected + is_object($this->connection) or $this->connect(); + + return $this->connection->real_escape_string($value); + } + +} // End Database_MySQLi diff --git a/system/libraries/Database_Mysqli_Result.php b/system/libraries/Database_Mysqli_Result.php new file mode 100644 index 0000000..f8b7b58 --- /dev/null +++ b/system/libraries/Database_Mysqli_Result.php @@ -0,0 +1,175 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * MySQL database result. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Mysqli_Result_Core extends Database_Result { + + protected $internal_row = 0; + + public function __construct($result, $sql, $link, $return_objects) + { + if (is_object($result)) + { + // True to return objects, false for arrays + $this->return_objects = $return_objects; + + $this->total_rows = $result->num_rows; + } + elseif (is_bool($result)) + { + if ($result == FALSE) + { + throw new Database_Exception('#:errno: :error [ :query ]', + array(':error' => $link->error, + ':query' => $sql, + ':errno' => $link->errno)); + } + else + { + // It's a DELETE, INSERT, REPLACE, or UPDATE query + $this->insert_id = $link->insert_id; + $this->total_rows = $link->affected_rows; + } + } + + // Store the result locally + $this->result = $result; + + $this->sql = $sql; + } + + public function __destruct() + { + if (is_object($this->result)) + { + $this->result->free(); + } + } + + public function as_array($return = FALSE) + { + // Return arrays rather than objects + $this->return_objects = FALSE; + + if ( ! $return ) + { + // Return this result object + return $this; + } + + // Return a nested array of all results + if (RUNS_MYSQLND) + return $this->result->fetch_all(MYSQLI_ASSOC); + + $array = array(); + + if ($this->total_rows > 0) + { + // Seek to the beginning of the result + $this->result->data_seek(0); + + while ($row = $this->result->fetch_assoc()) + { + // Add each row to the array + $array[] = $row; + } + $this->internal_row = $this->total_rows; + } + + return $array; + } + + public function as_object($class = NULL, $return = FALSE) + { + // Return objects of type $class (or stdClass if none given) + $this->return_objects = ($class !== NULL) ? $class : TRUE; + + if ( ! $return ) + { + // Return this result object + return $this; + } + + // Return a nested array of all results + $array = array(); + + if ($this->total_rows > 0) + { + // Seek to the beginning of the result + $this->result->data_seek(0); + + if (is_string($this->return_objects)) + { + while ($row = $this->result->fetch_object($this->return_objects)) + { + // Add each row to the array + $array[] = $row; + } + } + else + { + while ($row = $this->result->fetch_object()) + { + // Add each row to the array + $array[] = $row; + } + } + + $this->internal_row = $this->total_rows; + } + + return $array; + } + + /** + * SeekableIterator: seek + */ + 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; + } + else + { + return FALSE; + } + } + + /** + * Iterator: current + */ + public function current() + { + if ($this->current_row !== $this->internal_row AND ! $this->seek($this->current_row)) + return NULL; + + ++$this->internal_row; + + if ($this->return_objects) + { + if (is_string($this->return_objects)) + { + return $this->result->fetch_object($this->return_objects); + } + else + { + return $this->result->fetch_object(); + } + } + else + { + // Return an array of the row + return $this->result->fetch_assoc(); + } + } + +} // End Database_MySQLi_Result
\ No newline at end of file diff --git a/system/libraries/Database_Query.php b/system/libraries/Database_Query.php new file mode 100644 index 0000000..d9399d6 --- /dev/null +++ b/system/libraries/Database_Query.php @@ -0,0 +1,95 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Database query wrapper. + * + * $Id: Database_Query.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Database_Query_Core { + + protected $sql; + protected $params; + protected $ttl = FALSE; + + public function __construct($sql = NULL) + { + $this->sql = $sql; + } + + public function __toString() + { + // Return the SQL of this query + return $this->sql; + } + + public function sql($sql) + { + $this->sql = $sql; + + return $this; + } + + public function value($key, $value) + { + $this->params[$key] = $value; + + return $this; + } + + public function bind($key, & $value) + { + $this->params[$key] =& $value; + + return $this; + } + + public function execute($db = 'default') + { + if ( ! is_object($db)) + { + // Get the database instance + $db = Database::instance($db); + } + + // Import the SQL locally + $sql = $this->sql; + + if ( ! empty($this->params)) + { + // Quote all of the values + $params = array_map(array($db, 'quote'), $this->params); + + // Replace the values in the SQL + $sql = strtr($sql, $params); + } + + if ($this->ttl !== FALSE) + { + // Load the result from the cache + return $db->query_cache($sql, $this->ttl); + } + else + { + // Load the result (no caching) + return $db->query($sql); + } + } + + /** + * Set caching for the query + * + * @param mixed Time-to-live (FALSE to disable, NULL for Cache default, seconds otherwise) + * @return Database_Query + */ + public function cache($ttl = NULL) + { + $this->ttl = $ttl; + + return $this; + } + +} // End Database_Query
\ No newline at end of file diff --git a/system/libraries/Database_Result.php b/system/libraries/Database_Result.php new file mode 100644 index 0000000..cf2056f --- /dev/null +++ b/system/libraries/Database_Result.php @@ -0,0 +1,170 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Database result wrapper. + * + * $Id: Database_Result.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +abstract class Database_Result_Core implements Countable, Iterator, SeekableIterator, ArrayAccess { + + protected $result; + + protected $total_rows = 0; + protected $current_row = 0; + protected $insert_id; + + // Return objects or arrays for each row + protected $return_objects; + + /** + * Sets the total number of rows and stores the result locally. + * + * @param mixed $result query result + * @param boolean $return_objects True for results as objects, false for arrays + * @return void + */ + abstract public function __construct($result, $sql, $link, $return_objects); + + /** + * Result destruction cleans up all open result sets. + */ + abstract public function __destruct(); + + /** + * Return arrays for reach result, or the entire set of results + * + * @param boolean $return True to return entire result array + * @return Database_Result|array + */ + abstract public function as_array($return = FALSE); + + /** + * Returns objects for each result + * + * @param string $class Class name to return objects as or NULL for stdClass + * @return Database_Result + */ + abstract public function as_object($class = NULL, $return = FALSE); + + /** + * Returns the insert id + * + * @return int + */ + public function insert_id() + { + return $this->insert_id; + } + + /** + * Return the named column from the current row. + * + * @param string Column name + * @return mixed + */ + public function get($name) + { + // Get the current row + $row = $this->current(); + + if ( ! $this->return_objects) + return $row[$name]; + + return $row->$name; + } + + /** + * Countable: count + */ + public function count() + { + return $this->total_rows; + } + + /** + * ArrayAccess: offsetExists + */ + public function offsetExists($offset) + { + return ($offset >= 0 AND $offset < $this->total_rows); + } + + /** + * ArrayAccess: offsetGet + */ + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return NULL; + + return $this->current(); + } + + /** + * ArrayAccess: offsetSet + * + * @throws Kohana_Database_Exception + */ + final public function offsetSet($offset, $value) + { + throw new Kohana_Exception('Database results are read-only'); + } + + /** + * ArrayAccess: offsetUnset + * + * @throws Kohana_Database_Exception + */ + final public function offsetUnset($offset) + { + throw new Kohana_Exception('Database results are read-only'); + } + + /** + * 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
\ No newline at end of file diff --git a/system/libraries/Encrypt.php b/system/libraries/Encrypt.php new file mode 100644 index 0000000..15d4087 --- /dev/null +++ b/system/libraries/Encrypt.php @@ -0,0 +1,176 @@ +<?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 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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('To use the Encrypt library, mcrypt must be enabled in your PHP installation'); + + if (is_string($config)) + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('encryption.'.$config)) === NULL) + throw new Kohana_Exception('The :name: group is not defined in your configuration.', array(':name:' => $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('To use the Encrypt library, you must set an encryption key in your config file'); + + // 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::add('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 or FALSE if decryption fails + */ + public function decode($data) + { + // Convert the data back to binary + $data = base64_decode($data, TRUE); + + if ( ! $data) + { + // Invalid base64 data + return FALSE; + } + + // Extract the initialization vector from the data + $iv = substr($data, 0, $this->config['iv_size']); + + if ($this->config['iv_size'] !== strlen($iv)) + { + // The iv is not the correct size + return FALSE; + } + + // 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/I18n.php b/system/libraries/I18n.php new file mode 100644 index 0000000..9401ddc --- /dev/null +++ b/system/libraries/I18n.php @@ -0,0 +1,100 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Kohana I18N System + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ + +class I18n_Core +{ + protected static $locale; + // All the translations will be cached in here, after the first call of get_text() + protected static $translations = array(); + + public static function set_locale($locale) + { + // Reset the translations array + I18n::$translations = array(); + + I18n::$locale = $locale; + } + + + /** + * + * Returns the locale. + * If $ext is true, the UTF8 extension gets returned as well, otherwise, just the language code. + * Defaults to true. + * + * @return The locale + * @param boolean $ext[optional] Get the Extension? + */ + public static function get_locale($ext = true) + { + if($ext) + return I18n::$locale; + else + return arr::get(explode('.', I18n::$locale), 0); + } + + + /** + * + * Translates $string into language I18n::$locale and caches all found translations on the first call + * + * @return The translated String + * @param string $string The String to translate + */ + public static function get_text($string) + { + if ( ! I18n::$translations) + { + $locale = explode('_', I18n::get_locale(FALSE)); + + // Get the translation files + $translation_files = Kohana::find_file('i18n', $locale[0]); + + if($local_translation_files = Kohana::find_file('i18n', $locale[0].'/'.$locale[1])) + $translation_files = array_merge($translation_files, $local_translation_files); + + if ($translation_files) + { + // Merge the translations + foreach ($translation_files as $file) + { + include $file; + I18n::$translations = array_merge(I18n::$translations, $translations); + } + } + } + + if (isset(I18n::$translations[$string])) + return I18n::$translations[$string]; + else + return $string; + } +} + +/** + * Loads the configured driver and validates it. + * + * @param string Text to output + * @param array Key/Value pairs of arguments to replace in the string + * @return string Translated text + */ +function __($string, $args = NULL) +{ + // KOHANA_LOCALE is the default locale, in which all of Kohana's __() calls are written in + if (I18n::get_locale() != Kohana::LOCALE) + { + $string = I18n::get_text($string); + } + + if ($args === NULL) + return $string; + + return strtr($string, $args); +} diff --git a/system/libraries/Image.php b/system/libraries/Image.php new file mode 100644 index 0000000..991c8d5 --- /dev/null +++ b/system/libraries/Image.php @@ -0,0 +1,501 @@ +<?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. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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; + + // Orientations + const PORTRAIT = 7; + const LANDSCAPE = 8; + const SQUARE = 9; + + // 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('The Image library requires the getimagesize() PHP function, which is not available in your installation.'); + + // Check to make sure the image exists + if ( ! is_file($image)) + throw new Kohana_Exception('The specified image, :image:, was not found. Please verify that images exist by using file_exists() before manipulating them.', array(':image:' => $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('The file specified, :file:, is not readable or is not an image', array(':file:' => $image)); + + // Check to make sure the image type is allowed + if ( ! isset(Image::$allowed_types[$image_info[2]])) + throw new Kohana_Exception('The specified image, :type:, is not an allowed image type.', array(':type:' => $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'] + ); + + $this->determine_orientation(); + + // 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('The :driver: driver for the :library: library could not be found', + array(':driver:' => $this->config['driver'], ':library:' => 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('The :driver: driver for the :library: library must implement the :interface: interface', + array(':driver:' => $this->config['driver'], ':library:' => get_class($this), ':interface:' => 'Image_Driver')); + } + + /** + * Works out the correct orientation for the image + * + * @return void + */ + protected function determine_orientation() + { + switch (TRUE) + { + case $this->image['height'] > $this->image['width']: + $orientation = Image::PORTRAIT; + break; + + case $this->image['height'] < $this->image['width']: + $orientation = Image::LANDSCAPE; + break; + + default: + $orientation = Image::SQUARE; + } + + $this->image['orientation'] = $orientation; + } + + /** + * 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('The :property: property does not exist in the :class: class.', + array(':property:' => $property, ':class:' => 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('The width you specified, :width:, is not valid.', array(':width:' => $width)); + + if ( ! $this->valid_size('height', $height)) + throw new Kohana_Exception('The height you specified, :height:, is not valid.', array(':height:' => $height)); + + if (empty($width) AND empty($height)) + throw new Kohana_Exception('The dimensions specified for :function: are not valid.', array(':function:' => __FUNCTION__)); + + if ($master === NULL) + { + // Maintain the aspect ratio by default + $master = Image::AUTO; + } + elseif ( ! $this->valid_size('master', $master)) + throw new Kohana_Exception('The master dimension specified is not valid.'); + + $this->actions['resize'] = array + ( + 'width' => $width, + 'height' => $height, + 'master' => $master, + ); + + $this->determine_orientation(); + + 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('The width you specified, :width:, is not valid.', array(':width:' => $width)); + + if ( ! $this->valid_size('height', $height)) + throw new Kohana_Exception('The height you specified, :height:, is not valid.', array(':height:' => $height)); + + if ( ! $this->valid_size('top', $top)) + throw new Kohana_Exception('The top offset you specified, :top:, is not valid.', array(':top:' => $top)); + + if ( ! $this->valid_size('left', $left)) + throw new Kohana_Exception('The left offset you specified, :left:, is not valid.', array(':left:' => $left)); + + if (empty($width) AND empty($height)) + throw new Kohana_Exception('The dimensions specified for :function: are not valid.', array(':function:' => __FUNCTION__)); + + $this->actions['crop'] = array + ( + 'width' => $width, + 'height' => $height, + 'top' => $top, + 'left' => $left, + ); + + $this->determine_orientation(); + + 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('The specified image, :type:, is not an allowed image type.', array(':type:' => $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('The flip direction specified is not valid.'); + + $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, $background = NULL) + { + // 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('The specified directory, :dir:, is not writable.', array(':dir:' => $dir)); + + if ($status = $this->driver->process($this->image, $this->actions, $dir, $file, FALSE, $background)) + { + if ($chmod !== FALSE) + { + // Set permissions + chmod($new_image, $chmod); + } + } + + if ($keep_actions !== TRUE) + { + // Reset actions. Subsequent save() or render() will not apply previous actions. + $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, $background = NULL) + { + $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, TRUE, $background); + + if ($keep_actions !== TRUE) + { + // Reset actions. Subsequent save() or render() will not apply previous actions. + $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 0000000..fa984a8 --- /dev/null +++ b/system/libraries/Input.php @@ -0,0 +1,507 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Input library. + * + * $Id: Input.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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() + { + // Convert all global variables to Kohana charset + $_GET = Input::clean($_GET); + $_POST = Input::clean($_POST); + $_COOKIE = Input::clean($_COOKIE); + $_SERVER = Input::clean($_SERVER); + + if (Kohana::$server_api === 'cli') + { + // Convert command line arguments + $_SERVER['argv'] = Input::clean($_SERVER['argv']); + } + + // 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::add('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::add('debug', 'Disable magic_quotes_gpc! It is evil and deprecated: http://php.net/magic_quotes'); + } + + 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::add('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::get() ($_COOKIE won't work with signed + * cookies.) + * + * @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::get(), $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 (is_bool($tool)) + { + $tool = 'default'; + } + elseif ( ! method_exists($this, 'xss_filter_'.$tool)) + { + Kohana_Log::add('error', 'Unable to use Input::xss_filter_'.$tool.'(), no such method exists'); + $tool = 'default'; + } + + $method = 'xss_filter_'.$tool; + + return $this->$method($data); + } + + /** + * Default built-in cross site scripting filter. + * + * @param string data to clean + * @return string + */ + protected function xss_filter_default($data) + { + // 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 + // + // Gallery Modifications: + // * Wrap the loop around all the changes to detect nested exploits + + do + { + $old_data = $data; + + // 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('#(?:on[a-z]+|xmlns)\s*=\s*[\'"\x00-\x20]?[^\'>"]*[\'"\x00-\x20]?\s?#iu', '', $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); + + //remove any style attributes, IE allows too much stupid things in them, eg. + //<span style="width: expression(alert('Ping!'));"></span> + // and in general you really don't want style declarations in your UGC + $data = preg_replace('#(<[^>]+[\x00-\x20\"\'\/])style[^>]*>#iUu', "$1>", $data); + + // Remove namespaced elements (we do not need them) + $data = preg_replace('#</*\w+:\w[^>]*+>#i', '', $data); + + // Remove really unwanted tags + $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); + + return $data; + } + + /** + * HTMLPurifier cross site scripting filter. This version assumes the + * existence of the "Standalone Distribution" htmlpurifier library, and is set to not tidy + * input. + * + * @param string data to clean + * @return string + */ + protected function xss_filter_htmlpurifier($data) + { + /** + * @todo License should go here, http://htmlpurifier.org/ + */ + if ( ! class_exists('HTMLPurifier_Config', FALSE)) + { + // Load HTMLPurifier + require Kohana::find_file('vendor', 'htmlpurifier/HTMLPurifier.standalone', TRUE); + } + + // Set configuration + $config = HTMLPurifier_Config::createDefault(); + $config->set('HTML.TidyLevel', 'none'); // Only XSS cleaning now + + $cache = Kohana::config('html_purifier.cache'); + + if ($cache AND is_string($cache)) + { + $config->set('Cache.SerializerPath', $cache); + } + + // Run HTMLPurifier + $data = HTMLPurifier::instance($config)->purify($data); + + 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) + { + if ( ! preg_match('#^[\pL0-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; + } + + /** + * Recursively cleans arrays, objects, and strings. Removes ASCII control + * codes and converts to UTF-8 while silently discarding incompatible + * UTF-8 characters. + * + * @param string string to clean + * @return string + */ + public static function clean($str) + { + if (is_array($str) OR is_object($str)) + { + foreach ($str as $key => $val) + { + // Recursion! + $str[Input::clean($key)] = Input::clean($val); + } + } + elseif (is_string($str) AND $str !== '') + { + // Remove control characters + $str = text::strip_ascii_ctrl($str); + + if ( ! text::is_ascii($str)) + { + // Disable notices + $ER = error_reporting(~E_NOTICE); + + // iconv is expensive, so it is only used when needed + $str = iconv(Kohana::CHARSET, Kohana::CHARSET.'//IGNORE', $str); + + // Turn notices back on + error_reporting($ER); + } + } + + return $str; + } + +} // End Input Class diff --git a/system/libraries/Kohana_404_Exception.php b/system/libraries/Kohana_404_Exception.php new file mode 100644 index 0000000..7bb7708 --- /dev/null +++ b/system/libraries/Kohana_404_Exception.php @@ -0,0 +1,56 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Creates a "Page Not Found" exception. + * + * $Id: Kohana_404_Exception.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ + +class Kohana_404_Exception_Core extends Kohana_Exception { + + protected $code = E_PAGE_NOT_FOUND; + + /** + * Set internal properties. + * + * @param string URI of page + * @param string custom error template + */ + public function __construct($page = NULL) + { + if ($page === NULL) + { + // Use the complete URI + $page = Router::$complete_uri; + } + + parent::__construct('The page you requested, %page%, could not be found.', array('%page%' => $page)); + } + + /** + * Throws a new 404 exception. + * + * @throws Kohana_404_Exception + * @return void + */ + public static function trigger($page = NULL) + { + throw new Kohana_404_Exception($page); + } + + /** + * Sends 404 headers, to emulate server behavior. + * + * @return void + */ + public function sendHeaders() + { + // Send the 404 header + header('HTTP/1.1 404 File Not Found'); + } + +} // End Kohana 404 Exception
\ No newline at end of file diff --git a/system/libraries/Kohana_Log.php b/system/libraries/Kohana_Log.php new file mode 100644 index 0000000..5126013 --- /dev/null +++ b/system/libraries/Kohana_Log.php @@ -0,0 +1,90 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Logging class. + * + * $Id: Kohana_Log.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Log_Core { + + // Configuration + protected static $config; + + // Drivers + protected static $drivers; + + // Logged messages + protected static $messages; + + /** + * Add a new message to the log. + * + * @param string type of message + * @param string message text + * @return void + */ + public static function add($type, $message) + { + // Make sure the drivers and config are loaded + if ( ! is_array(Kohana_Log::$config)) + { + Kohana_Log::$config = Kohana::config('log'); + } + + if ( ! is_array(Kohana_Log::$drivers)) + { + foreach ( (array) Kohana::config('log.drivers') as $driver_name) + { + // Set driver name + $driver = 'Log_'.ucfirst($driver_name).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('Log Driver Not Found: %driver%', array('%driver%' => $driver)); + + // Initialize the driver + $driver = new $driver(array_merge(Kohana::config('log'), Kohana::config('log_'.$driver_name))); + + // Validate the driver + if ( ! ($driver instanceof Log_Driver)) + throw new Kohana_Exception('%driver% does not implement the Log_Driver interface', array('%driver%' => $driver)); + + Kohana_Log::$drivers[] = $driver; + } + + // Always save logs on shutdown + Event::add('system.shutdown', array('Kohana_Log', 'save')); + } + + Kohana_Log::$messages[] = array('date' => time(), 'type' => $type, 'message' => $message); + } + + /** + * Save all currently logged messages. + * + * @return void + */ + public static function save() + { + if (empty(Kohana_Log::$messages)) + return; + + foreach (Kohana_Log::$drivers as $driver) + { + // We can't throw exceptions here or else we will get a + // Exception thrown without a stack frame error + try + { + $driver->save(Kohana_Log::$messages); + } + catch(Exception $e){} + } + + // Reset the messages + Kohana_Log::$messages = array(); + } +}
\ No newline at end of file diff --git a/system/libraries/Kohana_PHP_Exception.php b/system/libraries/Kohana_PHP_Exception.php new file mode 100644 index 0000000..779c229 --- /dev/null +++ b/system/libraries/Kohana_PHP_Exception.php @@ -0,0 +1,99 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Kohana PHP Error Exceptions + * + * $Id: Kohana_PHP_Exception.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ + +class Kohana_PHP_Exception_Core extends Kohana_Exception { + + public static $enabled = FALSE; + + /** + * Enable Kohana PHP error handling. + * + * @return void + */ + public static function enable() + { + if ( ! Kohana_PHP_Exception::$enabled) + { + // Handle runtime errors + set_error_handler(array('Kohana_PHP_Exception', 'error_handler')); + + // Handle errors which halt execution + Event::add('system.shutdown', array('Kohana_PHP_Exception', 'shutdown_handler')); + + Kohana_PHP_Exception::$enabled = TRUE; + } + } + + /** + * Disable Kohana PHP error handling. + * + * @return void + */ + public static function disable() + { + if (Kohana_PHP_Exception::$enabled) + { + restore_error_handler(); + + Event::clear('system.shutdown', array('Kohana_PHP_Exception', 'shutdown_handler')); + + Kohana_PHP_Exception::$enabled = FALSE; + } + } + + /** + * Create a new PHP error exception. + * + * @return void + */ + public function __construct($code, $error, $file, $line, $context = NULL) + { + parent::__construct($error); + + // Set the error code, file, line, and context manually + $this->code = $code; + $this->file = $file; + $this->line = $line; + } + + /** + * PHP error handler. + * + * @throws Kohana_PHP_Exception + * @return void + */ + public static function error_handler($code, $error, $file, $line, $context = NULL) + { + // Respect error_reporting settings + if (error_reporting() & $code) + { + // Throw an exception + throw new Kohana_PHP_Exception($code, $error, $file, $line, $context); + } + } + + /** + * Catches errors that are not caught by the error handler, such as E_PARSE. + * + * @uses Kohana_Exception::handle() + * @return void + */ + public static function shutdown_handler() + { + if (Kohana_PHP_Exception::$enabled AND $error = error_get_last() AND (error_reporting() & $error['type'])) + { + // Fake an exception for nice debugging + Kohana_Exception::handle(new Kohana_PHP_Exception($error['type'], $error['message'], $error['file'], $error['line'])); + } + } + +} // End Kohana PHP Exception diff --git a/system/libraries/Kohana_User_Exception.php b/system/libraries/Kohana_User_Exception.php new file mode 100644 index 0000000..a0ec3ac --- /dev/null +++ b/system/libraries/Kohana_User_Exception.php @@ -0,0 +1,30 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Creates a custom exception message. + * + * $Id: Kohana_User_Exception.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ + +class Kohana_User_Exception_Core extends Kohana_Exception { + + /** + * Set exception title and message. + * + * @param string exception title string + * @param string exception message string + * @param string custom error template + */ + public function __construct($title, $message, array $variables = NULL) + { + parent::__construct($message, $variables); + + // Code is the error title + $this->code = $title; + } + +} // End Kohana User Exception diff --git a/system/libraries/Model.php b/system/libraries/Model.php new file mode 100644 index 0000000..ee50d50 --- /dev/null +++ b/system/libraries/Model.php @@ -0,0 +1,62 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Model base class. + * + * $Id: Model.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Model_Core { + + /** + * Creates and returns a new model. + * + * @param string model name + * @param mixed constructor arguments + * @param boolean construct the model with multiple arguments + * @return Model + */ + public static function factory($name, $args = NULL, $multiple = FALSE) + { + // Model class name + $class = ucfirst($name).'_Model'; + + if ($args === NULL) + { + // Create a new model with no arguments + return new $class; + } + + if ($multiple !== TRUE) + { + // Create a model with a single argument + return new $class($args); + } + + $class = new ReflectionClass($class); + + // Create a model with multiple arguments + return $class->newInstanceArgs($args); + } + + // 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 0000000..16e047b --- /dev/null +++ b/system/libraries/ORM.php @@ -0,0 +1,1528 @@ +<?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 + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class ORM_Core { + + // Current relationships + protected $has_one = array(); + protected $belongs_to = array(); + protected $has_many = array(); + protected $has_and_belongs_to_many = array(); + protected $has_many_through = array(); + + // Relationships that should always be joined + protected $load_with = array(); + + // Current object + protected $object = array(); + protected $changed = array(); + protected $related = array(); + protected $_valid = FALSE; + protected $_loaded = FALSE; + protected $_saved = FALSE; + protected $sorting; + protected $rules = array(); + + // 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; + + // Auto-update columns for creation and updates + protected $updated_column = NULL; + protected $created_column = NULL; + + // 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(); + protected $db_builder; + + // 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)) + { + // Set the object's primary key, but don't load it until needed + $this->object[$this->primary_key] = $id; + + // Object is considered saved until something is set + $this->_saved = TRUE; + } + } + + /** + * 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(); + + // Initialize the builder + $this->db_builder = db::build(); + } + + /** + * 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_builder, $method)) + { + if (in_array($method, array('execute', 'insert', 'update', 'delete'))) + throw new Kohana_Exception('Query methods cannot be used through ORM'); + + // 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_builder->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', 'and_open', 'or_open', 'close', 'cache'))) + { + // Should return ORM, not Database + $this->db_builder->$method(); + } + else + { + // Support for things like reset_select, reset_write, list_tables + return $this->db_builder->$method(); + } + break; + case 1: + $this->db_builder->$method($args[0]); + break; + case 2: + $this->db_builder->$method($args[0], $args[1]); + break; + case 3: + $this->db_builder->$method($args[0], $args[1], $args[2]); + break; + case 4: + $this->db_builder->$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('Invalid method :method called in :class', + array(':method' => $method, ':class' => 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)) + { + if( ! $this->loaded() AND ! $this->empty_primary_key()) + { + // Column asked for but the object hasn't been loaded yet, so do it now + // Ignore loading of any columns that have been changed + $this->find($this->object[$this->primary_key], TRUE); + } + + return $this->object[$column]; + } + elseif (isset($this->related[$column])) + { + return $this->related[$column]; + } + elseif ($column === 'primary_key_value') + { + if( ! $this->loaded() AND ! $this->empty_primary_key() AND $this->unique_key($this->object[$this->primary_key]) !== $this->primary_key) + { + // Load if object hasn't been loaded and the key given isn't the primary_key + // that we need (i.e. passing an email address to ORM::factory rather than the id) + $this->find($this->object[$this->primary_key], TRUE); + } + + 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)) + { + if ( ! $this->loaded() AND ! $this->empty_primary_key()) + { + // Load this object first so we know what id to look for in the foreign table + $this->find($this->object[$this->primary_key], TRUE); + } + + // Foreign key lies in this table (this model belongs_to target model) + $where = array($model->foreign_key(TRUE), '=', $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_through[$column])) + { + // Load the "middle" model + $through = ORM::factory(inflector::singular($this->has_many_through[$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->foreign_key(TRUE); + + // one<>alias:many relationship + return $model + ->join($join_table, $join_col1, $join_col2) + ->where($through->foreign_key($this->object_name, $join_table), '=', $this->primary_key_value); + } + elseif (isset($this->has_many[$column])) + { + // one<>many aliased relationship + $model_name = $this->has_many[$column]; + + $model = ORM::factory(inflector::singular($model_name)); + + return $model->where($this->foreign_key($column, $model->table_name), '=', $this->primary_key_value); + } + elseif (in_array($column, $this->has_many)) + { + // one<>many relationship + $model = ORM::factory(inflector::singular($column)); + return $model->where($this->foreign_key($column, $model->table_name), '=', $this->primary_key_value); + } + 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 $model->where($model->foreign_key(TRUE), 'IN', $this->changed_relations[$column]); + } + else + { + // empty many<>many relationship + return $model->where($model->foreign_key(TRUE), 'IS', NULL); + } + } + elseif (isset($this->ignored_columns[$column])) + { + return NULL; + } + elseif (in_array($column, array + ( + 'object_name', 'object_plural','_valid', // Object + 'primary_key', 'primary_val', 'table_name', 'table_columns', // Table + 'has_one', 'belongs_to', 'has_many', 'has_many_through', 'has_and_belongs_to_many', 'load_with' // Relationships + ))) + { + // Model meta information + return $this->$column; + } + else + { + throw new Kohana_Exception('The :property property does not exist in the :class class', + array(':property' => $column, ':class' => get_class($this))); + } + } + + /** + * Tells you if the Model has been loaded or not + * + * @return bool + */ + public function loaded() { + if ( ! $this->_loaded AND ! $this->empty_primary_key()) + { + // If returning the loaded member and no load has been attempted, do it now + $this->find($this->object[$this->primary_key], TRUE); + } + return $this->_loaded; + } + + /** + * Tells you if the model was saved successfully or not + * + * @return bool + */ + public function saved() { + return $this->_saved; + } + + /** + * 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 or valid + $this->_saved = $this->_valid = FALSE; + } + + $this->object[$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('The :property: property does not exist in the :class: class', + array(':property:' => $column, ':class:' => get_class($this))); + } + } + + /** + * Chainable set method + * + * @param string name of field or array of key => val + * @param mixed value + * @return ORM + */ + 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 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]); + } + + /** + * 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; + } + + foreach ($this->with_applied as $model => $enabled) + { + // Generate arrays for relationships + if ($enabled) + { + $object[$model] = $this->$model->as_array(); + } + } + + 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 model to bind to + * @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; + + $select = array(); + + // Use the keys of the empty object to determine the columns + foreach (array_keys($target->object) as $column) + { + // Add the prefix so that load_result can determine the relationship + $select[$target_path.':'.$column] = $target_path.'.'.$column; + } + + // Select all of the prefixed keys in the object + $this->db_builder->select($select); + + if (in_array($target->object_name, $parent->belongs_to)) + { + // 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 trick allows for models to use different table prefixes (sharing the same database) + $join_table = array($this->db->quote_table($target_path) => $target->db->quote_table($target->table_name)); + + // Join the related object into the result + // Use Database_Expression to disable prefixing + $this->db_builder->join(new Database_Expression($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 + * @param bool ignore loading of columns that have been modified + * @return ORM + */ + public function find($id = NULL, $ignore_changed = FALSE) + { + if ($id !== NULL) + { + if (is_array($id)) + { + // Search for all clauses + $this->db_builder->where(array($id)); + } + else + { + // Search for a specific column + $this->db_builder->where($this->table_name.'.'.$this->unique_key($id), '=', $id); + } + } + + return $this->load_result(FALSE, $ignore_changed); + } + + /** + * 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($this->table_name.'.'.$key, $this->table_name.'.'.$val)->find_all()->select_list($key, $val); + } + + /** + * Validates the current object. This method requires that rules are set to be useful, if called with out + * any rules set, or if a Validation object isn't passed, nothing happens. + * + * @param object Validation array + * @param boolean Save on validate + * @return ORM + * @chainable + */ + public function validate(Validation $array = NULL) + { + if ($array === NULL) + $array = new Validation($this->object); + + if (count($this->rules) > 0) + { + foreach ($this->rules as $field => $parameters) + { + foreach ($parameters as $type => $value) { + switch ($type) { + case 'pre_filter': + $array->pre_filter($value,$field); + break; + case 'rules': + $rules = array_merge(array($field),$value); + call_user_func_array(array($array,'add_rules'), $rules); + break; + case 'callbacks': + $callbacks = array_merge(array($field),$value); + call_user_func_array(array($array,'add_callbacks'), $callbacks); + break; + } + } + } + } + // Validate the array + if (($this->_valid = $array->validate()) === FALSE) + { + ORM_Validation_Exception::handle_validation($this->table_name, $array); + } + + // Fields may have been modified by filters + $this->object = array_merge($this->object, $array->getArrayCopy()); + + // Return validation status + return $this; + } + + /** + * Saves the current object. + * + * @chainable + * @return ORM + */ + public function save() + { + if ( ! empty($this->changed)) + { + // Require model validation before saving + if ( ! $this->_valid) + { + $this->validate(); + } + + $data = array(); + foreach ($this->changed as $column) + { + // Compile changed data + $data[$column] = $this->object[$column]; + } + + if ( ! $this->empty_primary_key() AND ! isset($this->changed[$this->primary_key])) + { + // Primary key isn't empty and hasn't been changed so do an update + + if (is_array($this->updated_column)) + { + // Fill the updated column + $column = $this->updated_column['column']; + $format = $this->updated_column['format']; + + $data[$column] = $this->object[$column] = ($format === TRUE) ? time() : date($format); + } + + $query = db::update($this->table_name) + ->set($data) + ->where($this->primary_key, '=', $this->primary_key_value) + ->execute($this->db); + + // Object has been saved + $this->_saved = TRUE; + } + else + { + if (is_array($this->created_column)) + { + // Fill the created column + $column = $this->created_column['column']; + $format = $this->created_column['format']; + + $data[$column] = $this->object[$column] = ($format === TRUE) ? time() : date($format); + } + + $result = db::insert($this->table_name) + ->columns(array_keys($data)) + ->values(array_values($data)) + ->execute($this->db); + + if ($result->count() > 0) + { + if (empty($this->object[$this->primary_key])) + { + // Load the insert id as the primary key + $this->object[$this->primary_key] = $result->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($join_table); + $related_fk = $model->foreign_key($join_table); + + if ( ! empty($added)) + { + foreach ($added as $id) + { + // Insert the new relationship + db::insert($join_table) + ->columns($object_fk, $related_fk) + ->values($this->primary_key_value, $id) + ->execute($this->db); + } + } + + if ( ! empty($removed)) + { + db::delete($join_table) + ->where($object_fk, '=', $this->primary_key_value) + ->where($related_fk, 'IN', $removed) + ->execute($this->db); + } + + // Clear all relations for this column + unset($this->object_relations[$column], $this->changed_relations[$column]); + } + } + + if ($this->saved() === TRUE) + { + // Always force revalidation after saving + $this->_valid = FALSE; + + // Clear the per-request database cache + $this->db->clear_cache(NULL, Database::PER_REQUEST); + } + + return $this; + } + + /** + * Deletes the current object from the database. This does NOT destroy + * relationships that have been created with other objects. + * + * @chainable + * @param mixed id to delete + * @return ORM + */ + public function delete($id = NULL) + { + if ($id === NULL) + { + // Use the the primary key value + $id = $this->primary_key_value; + } + + // Delete this object + db::delete($this->table_name) + ->where($this->primary_key, '=', $id) + ->execute($this->db); + + // Clear the per-request database cache + $this->db->clear_cache(NULL, Database::PER_REQUEST); + + 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 + db::delete($this->table_name) + ->where($this->primary_key, 'IN', $ids) + ->execute($this->db); + } + elseif ($ids === NULL) + { + // Delete all records + db::delete($this->table_name) + ->execute($this->db); + } + else + { + // Do nothing - safeguard + return $this; + } + + // Clear the per-request database cache + $this->db->clear_cache(NULL, Database::PER_REQUEST); + + 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() + { + if ($this->_loaded) { + return $this->find($this->object[$this->primary_key]); + } else { + return $this->clear(); + } + } + + /** + * 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->loaded() AND ! $model->empty_primary_key()) + { + // Load the related model if it hasn't already been + $model->find($model->object[$model->primary_key]); + } + + 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_builder->count_records($this->table_name); + } + + /** + * Proxy method to Database list_fields. + * + * @return array + */ + public function list_fields() + { + // Proxy to database + return $this->db->list_fields($this->table_name); + } + + /** + * Proxy method to Database clear_cache. + * + * @chainable + * @param string SQL query to clear + * @param integer Type of cache to clear, Database::CROSS_REQUEST or Database::PER_REQUEST + * @return ORM + */ + public function clear_cache($sql = NULL, $type = NULL) + { + // Proxy to database + $this->db->clear_cache($sql, $type); + + 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) + { + // Related objects + $related = array(); + + foreach ($values as $column => $value) + { + if (strpos($column, ':') === FALSE) + { + if ( ! isset($this->changed[$column])) + { + if (isset($this->table_columns[$column])) + { + //Update the column, respects __get() + $this->$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 an array of values into into the current object. Only used internally + * + * @chainable + * @param array values to load + * @param bool ignore loading of columns that have been modified + * @return ORM + */ + public function _load_values(array $values, $ignore_changed = FALSE) + { + if (array_key_exists($this->primary_key, $values)) + { + if ( ! $ignore_changed) + { + // 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 ( ! $ignore_changed OR ! isset($this->changed[$column])) + { + $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 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 + * @param boolean ignore loading of columns that have been modified + * @return ORM for single rows + * @return ORM_Iterator for multiple rows + */ + protected function load_result($array = FALSE, $ignore_changed = FALSE) + { + $this->db_builder->from($this->table_name); + + if ($array === FALSE) + { + // Only fetch 1 record + $this->db_builder->limit(1); + } + + if ( ! isset($this->db_applied['select'])) + { + // Select all columns by default + $this->db_builder->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['order_by']) 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_builder->order_by($sorting); + } + + // Load the result + $result = $this->db_builder->execute($this->db); + $this->db_applied = array(); + + if ($array === TRUE) + { + // Return an iterated result + return new ORM_Iterator($this, $result); + } + + if ($result->count() === 1) + { + // Load object values + $this->_load_values($result->as_array()->current(), $ignore_changed); + } + 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) + { + $result = db::select(array('id' => $model->foreign_key($table))) + ->from($table) + ->where($this->foreign_key($table, $table), '=', $this->primary_key_value) + ->execute($this->db) + ->as_object(); + + $relations = array(); + foreach ($result 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 0000000..0bf2b47 --- /dev/null +++ b/system/libraries/ORM_Iterator.php @@ -0,0 +1,266 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Object Relational Mapping (ORM) result iterator. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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 (make sure rows are returned as arrays) + $this->result = $result; + } + + /** + * Returns an array of the results as ORM objects or a nested array + * + * @param bool TRUE to return an array of ORM objects, FALSE for an array of arrays + * @param string key column to index on, NULL to ignore + * @return array + */ + public function as_array($objects = TRUE, $key = NULL) + { + $array = array(); + + // Import class name + $class = $this->class_name; + + if ($objects) + { + // Generate an array of objects + foreach ($this->result as $data) + { + if ($key === NULL) + { + // No indexing + $array[] = new $class($data); + } + else + { + // Index on the given key + $array[$data->$key] = new $class($data); + } + } + } + else + { + // Generate an array of arrays (and the subarrays may be nested in the case of relationships) + // This could be done by creating a new ORM object and calling as_array on it, but this is much faster + foreach ($this->result as $data) + { + // Have to do a bit of magic here to handle any relationships and generate a nested array for them + $temp = array(); + + foreach ($data as $key => $val) + { + $ptr = & $temp; + + foreach (explode(':', $key) as $subkey) + { + // Walk thru the relationships (separated in the key name by a ':') + // 'user:email:address' will be array['user']['email']['address'] + $ptr = & $ptr[$subkey]; + } + + // Set the value + $ptr = $val; + } + + // Append the result + $array[] = $temp; + } + } + + 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 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_Validation_Exception.php b/system/libraries/ORM_Validation_Exception.php new file mode 100644 index 0000000..9044aa6 --- /dev/null +++ b/system/libraries/ORM_Validation_Exception.php @@ -0,0 +1,24 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * ORM Validation exceptions. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class ORM_Validation_Exception_Core extends Database_Exception { + + /** + * Handles Database Validation Exceptions + * + * @param Validation $array + * @return + */ + public static function handle_validation($table, Validation $array) + { + $exception = new ORM_Validation_Exception('ORM Validation has failed for :table model',array(':table'=>$table)); + $exception->validation = $array; + throw $exception; + } +} // End ORM_Validation_Exception
\ No newline at end of file diff --git a/system/libraries/Profiler.php b/system/libraries/Profiler.php new file mode 100644 index 0000000..3ac8707 --- /dev/null +++ b/system/libraries/Profiler.php @@ -0,0 +1,306 @@ +<?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. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Profiler_Core { + + protected static $profiles = array(); + protected static $show; + + /** + * Enable the profiler. + * + * @return void + */ + public static function enable() + { + // Add all built in profiles to event + Event::add('profiler.run', array('Profiler', 'benchmarks')); + Event::add('profiler.run', array('Profiler', 'database')); + Event::add('profiler.run', array('Profiler', 'session')); + Event::add('profiler.run', array('Profiler', 'post')); + Event::add('profiler.run', array('Profiler', 'cookies')); + + // Add profiler to page output automatically + Event::add('system.display', array('Profiler', 'render')); + + Kohana_Log::add('debug', 'Profiler library enabled'); + + } + + /** + * Disables the profiler for this page only. + * Best used when profiler is autoloaded. + * + * @return void + */ + public static function disable() + { + // Removes itself from the event queue + Event::clear('system.display', array('Profiler', 'render')); + } + + /** + * Return whether a profile should be shown. + * Determined by the config setting or GET parameter. + * + * @param string profile name + * @return boolean + */ + public static function show($name) + { + return (Profiler::$show === TRUE OR (is_array(Profiler::$show) AND in_array($name, Profiler::$show))) ? TRUE : FALSE; + } + + /** + * Add a new profile. + * + * @param object profile object + * @return boolean + * @throws Kohana_Exception + */ + public static function add($profile) + { + if (is_object($profile)) + { + Profiler::$profiles[] = $profile; + return TRUE; + } + + throw new Kohana_Exception('The profile must be an object'); + } + + /** + * Render the profiler. + * + * @param boolean return the output instead of adding it to bottom of page + * @return void|string + */ + public static function render($return = FALSE) + { + $start = microtime(TRUE); + + // Determine the profiles that should be shown + $get = isset($_GET['profiler']) ? explode(',', $_GET['profiler']) : array(); + Profiler::$show = empty($get) ? Kohana::config('profiler.show') : $get; + + Event::run('profiler.run'); + + // Don't display if there's no profiles + if (empty(Profiler::$profiles)) + return Kohana::$output; + + $styles = ''; + foreach (Profiler::$profiles as $profile) + { + $styles .= $profile->styles(); + } + + // Load the profiler view + $data = array + ( + 'profiles' => Profiler::$profiles, + 'styles' => $styles, + 'execution_time' => microtime(TRUE) - $start + ); + $view = new View('profiler/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 static function benchmarks() + { + if ( ! Profiler::show('benchmarks')) + return; + + $table = new Profiler_Table(); + $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'), __('Count'), __('Time'), __('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), $benchmark['count'], number_format($benchmark['time'], Kohana::config('profiler.time_decimals')), number_format($benchmark['memory'] / 1024 / 1024, Kohana::config('profiler.memory_decimals')).'MB'); + $class = text::alternate('', 'kp-altrow'); + + if ($name == 'Total Execution') + { + // Clear the count column + $data[1] = ''; + $class = 'kp-totalrow'; + } + + $table->add_row($data, $class); + } + + Profiler::add($table); + } + + /** + * Database query benchmarks. + * + * @return void + */ + public static function database() + { + if ( ! Profiler::show('database')) + return; + + $queries = Database::$benchmarks; + + // Don't show if there are no queries + if (empty($queries)) return; + + $table = new Profiler_Table(); + $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'); + + text::alternate(); + $total_time = $total_rows = 0; + foreach ($queries as $query) + { + $data = array($query['query'], number_format($query['time'], Kohana::config('profiler.time_decimals')), $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, Kohana::config('profiler.time_decimals')), $total_rows); + $table->add_row($data, 'kp-totalrow'); + + Profiler::add($table); + } + + /** + * Session data. + * + * @return void + */ + public static function session() + { + if (empty($_SESSION)) return; + + if ( ! Profiler::show('session')) + return; + + $table = new Profiler_Table(); + $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); + } + + Profiler::add($table); + } + + /** + * POST data. + * + * @return void + */ + public static function post() + { + if (empty($_POST)) return; + + if ( ! Profiler::show('post')) + return; + + $table = new Profiler_Table(); + $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); + } + + Profiler::add($table); + } + + /** + * Cookie data. + * + * @return void + */ + public static function cookies() + { + if (empty($_COOKIE)) return; + + if ( ! Profiler::show('cookies')) + return; + + $table = new Profiler_Table(); + $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); + } + + Profiler::add($table); + } +} diff --git a/system/libraries/Profiler_Table.php b/system/libraries/Profiler_Table.php new file mode 100644 index 0000000..e590ad7 --- /dev/null +++ b/system/libraries/Profiler_Table.php @@ -0,0 +1,67 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Provides a table layout for sections in the Profiler library. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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', '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('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 0000000..c36121d --- /dev/null +++ b/system/libraries/Router.php @@ -0,0 +1,315 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Router + * + * $Id: Router.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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 = '?'.urldecode(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 (empty(Router::$routes['_default'])) + throw new Kohana_Exception('Please set a default route in config/routes.php.'); + + // Use the default route when no segments exist + Router::$current_uri = Router::$routes['_default']; + + // Default route is in use + $default_route = TRUE; + } + + // Remove all dot-paths from the URI, they are not valid + Router::$current_uri = preg_replace('#\.[\s./]*/#', '', Router::$current_uri); + + // At this point routed URI and current URI are the same + Router::$routed_uri = Router::$current_uri = trim(Router::$current_uri, '/'); + + if ($default_route === TRUE) + { + Router::$complete_uri = Router::$query_string; + Router::$current_uri = ''; + Router::$segments = array(); + } + else + { + Router::$complete_uri = Router::$current_uri.Router::$query_string; + + // Explode the segments by slashes + Router::$segments = explode('/', Router::$current_uri); + + if (count(Router::$routes) > 1) + { + // Custom routing + Router::$routed_uri = Router::routed_uri(Router::$current_uri); + } + } + + // Explode the routed segments by slashes + Router::$rsegments = explode('/', Router::$routed_uri); + + // 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 (Kohana::$server_api === '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 (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 = Input::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']); + } + else + { + if (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']) + { + // PATH_INFO is empty during requests to the front controller + Router::$current_uri = $_SERVER['PHP_SELF']; + } + + if (isset($_SERVER['SCRIPT_NAME']) AND $_SERVER['SCRIPT_NAME']) + { + // Clean up PATH_INFO fallbacks + // PATH_INFO may be formatted for ISAPI instead of CGI on IIS + if (strncmp(Router::$current_uri, $_SERVER['SCRIPT_NAME'], strlen($_SERVER['SCRIPT_NAME'])) === 0) + { + // Remove the front controller from the current uri + Router::$current_uri = (string) substr(Router::$current_uri, strlen($_SERVER['SCRIPT_NAME'])); + } + } + } + + // 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 0000000..e57908e --- /dev/null +++ b/system/libraries/Session.php @@ -0,0 +1,500 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session library. + * + * $Id: Session.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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; + + // Automatically save the session by default + public static $should_save = true; + + /** + * Singleton instance of Session. + * + * @param string Force a specific session_id + */ + public static function instance($session_id = NULL) + { + if (Session::$instance == NULL) + { + // Create a new instance + new Session($session_id); + } + elseif( ! is_null($session_id) AND $session_id != session_id() ) + { + throw new Kohana_Exception('A session (SID: :session:) is already open, cannot open the specified session (SID: :new_session:).', array(':session:' => session_id(), ':new_session:' => $session_id)); + } + + return Session::$instance; + } + + /** + * Be sure to block the use of __clone. + */ + private function __clone(){} + + /** + * On first session instance creation, sets up the driver and creates session. + * + * @param string Force a specific session_id + */ + protected function __construct($session_id = NULL) + { + $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(NULL, $session_id); + + 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 on system shutdown (run before sending the headers), so that + // the session cookie(s) can be written. + Event::add('system.shutdown', array($this, 'write_close')); + + // Singleton instance + Session::$instance = $this; + } + + Kohana_Log::add('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 + * @param string Force a specific session_id + * @return void + */ + public function create($vars = NULL, $session_id = 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('The :driver: driver for the :library: library could not be found', + array(':driver:' => Session::$config['driver'], ':library:' => get_class($this))); + + // Initialize the driver + Session::$driver = new $driver(); + + // Validate the driver + if ( ! (Session::$driver instanceof Session_Driver)) + throw new Kohana_Exception('The :driver: driver for the :library: library must implement the :interface: interface', + array(':driver:' => Session::$config['driver'], ':library:' => get_class($this), ':interface:' => '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('The session_name, :session:, is invalid. It must contain only alphanumeric characters and underscores. Also at least one letter must be present.', array(':session:' => 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') + ); + + $cookie = cookie::get(Session::$config['name']); + + if ($session_id === NULL) + { + // Reopen session from signed cookie value. + $session_id = $cookie; + } + + // Reopen an existing session if supplied + if ( ! is_null($session_id)) + { + session_id($session_id); + } + + // 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'] = request::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] !== request::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::set($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]); + } + } + + /** + * Do not save this session. + * This is a performance feature only, if using the native + * session "driver" the save will NOT be aborted. + * + * @return void + */ + public function abort_save() + { + Session::$should_save = FALSE; + } + +} // End Session Class diff --git a/system/libraries/URI.php b/system/libraries/URI.php new file mode 100644 index 0000000..16d101a --- /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 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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 0000000..9917fbb --- /dev/null +++ b/system/libraries/Validation.php @@ -0,0 +1,815 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Validation library. + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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(); + + // Field labels + protected $labels = array(); + + // Fields that are expected to be arrays + protected $array_fields = array(); + + /** + * 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) + { + 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; + } + + /** + * 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; + } + + /** + * Sets or overwrites the label name for a field. + * + * @param string field name + * @param string label + * @return $this + */ + public function label($field, $label = NULL) + { + if ($label === NULL AND ($field !== TRUE OR $field !== '*') AND ! isset($this->labels[$field])) + { + // Set the field label to the field name + $this->labels[$field] = ucfirst(preg_replace('/[^\pL]+/u', ' ', $field)); + } + elseif ($label !== NULL) + { + // Set the label for this field + $this->labels[$field] = $label; + } + + return $this; + } + + /** + * Sets labels using an array. + * + * @param array list of field => label names + * @return $this + */ + public function labels(array $labels) + { + $this->labels = $labels + $this->labels; + + 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('Callback %name% used for Validation is not callable', array('%name%' => $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); + + // Set a default field label + $this->label($field); + + if ($field === TRUE) + { + // Use wildcard + $field = '*'; + } + + foreach ($rules as $rule) + { + // Arguments for rule + $args = NULL; + + // False rule + $false_rule = FALSE; + + $rule_tmp = trim(is_string($rule) ? $rule : $rule[1]); + + // Should the rule return false? + if ($rule_tmp !== ($rule_name = ltrim($rule_tmp, '! '))) + { + $false_rule = TRUE; + } + + if (is_string($rule)) + { + // Use the updated rule name + $rule = $rule_name; + + // Have arguments? + 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); + } + } + else + { + $rule[1] = $rule_name; + } + + 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, $false_rule); + } + + 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); + + // Set a default field label + $this->label($field); + + 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; + } + + $array = $this->safe_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) + { + $array[$f] = is_array($array[$f]) ? array_map($callback, $array[$f]) : call_user_func($callback, $array[$f]); + } + } + else + { + $array[$field] = is_array($array[$field]) ? array_map($callback, $array[$field]) : call_user_func($callback, $array[$field]); + } + } + } + + foreach ($this->rules as $field => $callbacks) + { + foreach ($callbacks as $callback) + { + // Separate the callback, arguments and is false bool + list ($callback, $args, $is_false) = $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($array[$f]) AND ! in_array($rule, $this->empty_rules)) + { + // This rule does not need to be processed on empty fields + continue; + } + + $result = ($args === NULL) ? call_user_func($callback, $array[$f]) : call_user_func($callback, $array[$f], $args); + + if (($result == $is_false)) + { + $this->add_error($f, $rule, $args); + + // 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($array[$field])) + { + // This rule does not need to be processed on empty fields + continue; + } + + // Results of our test + $result = ($args === NULL) ? call_user_func($callback, $array[$field]) : call_user_func($callback, $array[$field], $args); + + if (($result == $is_false)) + { + $rule = $is_false ? '!'.$rule : $rule; + $this->add_error($field, $rule, $args); + + // 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) + { + $array[$f] = is_array($array[$f]) ? array_map($callback, $array[$f]) : call_user_func($callback, $array[$f]); + } + } + else + { + $array[$field] = is_array($array[$field]) ? array_map($callback, $array[$field]) : call_user_func($callback, $array[$field]); + } + } + } + + // Swap the array back into the object + $this->exchangeArray($array); + + // 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 + * @param string arguments to pass to lang file + * @return object + */ + public function add_error($field, $name, $args = NULL) + { + $this->errors[$field] = array($name, $args); + + return $this; + } + + /** + * Return the errors array. + * + * @param boolean load errors from a message file + * @return array + */ + public function errors($file = NULL) + { + if ($file === NULL) + { + $errors = array(); + foreach($this->errors as $field => $error) + { + $errors[$field] = $error[0]; + } + return $errors; + } + else + { + $errors = array(); + foreach ($this->errors as $input => $error) + { + // Locations to check for error messages + $error_locations = array + ( + "validation/{$file}.{$input}.{$error[0]}", + "validation/{$file}.{$input}.default", + "validation/default.{$error[0]}" + ); + + if (($message = Kohana::message($error_locations[0])) !== $error_locations[0]) + { + // Found a message for this field and error + } + elseif (($message = Kohana::message($error_locations[1])) !== $error_locations[1]) + { + // Found a default message for this field + } + elseif (($message = Kohana::message($error_locations[2])) !== $error_locations[2]) + { + // Found a default message for this error + } + else + { + // No message exists, display the path expected + $message = "validation/{$file}.{$input}.{$error[0]}"; + } + + // Start the translation values list + $values = array(':field' => __($this->labels[$input])); + + if ( ! empty($error[1])) + { + foreach ($error[1] as $key => $value) + { + // Add each parameter as a numbered value, starting from 1 + $values[':param'.($key + 1)] = __($value); + } + } + + // Translate the message using the default language + $errors[$input] = __($message, $values); + } + + 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 = mb_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
\ No newline at end of file diff --git a/system/libraries/View.php b/system/libraries/View.php new file mode 100644 index 0000000..ba482bb --- /dev/null +++ b/system/libraries/View.php @@ -0,0 +1,329 @@ +<?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 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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(); + + /** + * 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('The requested filetype, .:type:, is not allowed in your view configuration file', array(':type:' => $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)) ? TRUE : FALSE; + } + } + else + { + // Otherwise just check one property + $result = (array_key_exists($key, $this->kohana_local_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; + } + + /** + * 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]; + } + elseif (isset($this->$key)) + { + return $this->$key; + } + else + { + throw new Kohana_Exception('Undefined view variable: :var', + array(':var' => $key)); + } + } + + /** + * Magically converts view object to string. + * + * @return string + */ + public function __toString() + { + try + { + return $this->render(); + } + catch (Exception $e) + { + Kohana_Exception::handle($e); + return (string) ''; + } + } + + /** + * 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 + * @param callback modifier to pass the data through before rendering + * @return string if print is FALSE + * @return void if print is TRUE + */ + public function render($print = FALSE, $renderer = FALSE, $modifier = FALSE) + { + if (empty($this->kohana_filename)) + throw new Kohana_Exception('You must set the the view filename before calling render'); + + if (is_string($this->kohana_filetype)) + { + // Merge global and local data, local overrides global with the same name + $data = $this->kohana_local_data; + + if ($modifier !== FALSE AND is_callable($modifier, TRUE)) + { + // Pass the data through the user defined modifier + $data = call_user_func($modifier, $data); + } + + $output = $this->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; + } + + /** + * Includes a View within the controller scope. + * + * @param string view filename + * @param array array of view variables + * @return string + */ + public function 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); + + try + { + include $kohana_view_filename; + } + catch (Exception $e) + { + ob_end_clean(); + throw $e; + } + + // Fetch the output and close the buffer + return ob_get_clean(); + } +} // End View diff --git a/system/libraries/drivers/Cache.php b/system/libraries/drivers/Cache.php new file mode 100644 index 0000000..9741509 --- /dev/null +++ b/system/libraries/drivers/Cache.php @@ -0,0 +1,42 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Cache driver abstract class. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +abstract class Cache_Driver { + /** + * Set cache items + */ + abstract public function set($items, $tags = NULL, $lifetime = NULL); + + /** + * Get a cache items by key + */ + abstract public function get($keys, $single = FALSE); + + /** + * Get cache items by tag + */ + abstract public function get_tag($tags); + + /** + * Delete cache item by key + */ + abstract public function delete($keys); + + /** + * Delete cache items by tag + */ + abstract public function delete_tag($tags); + + /** + * Empty the cache + */ + abstract public function delete_all(); +} // End Cache 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 0000000..d6ec037 --- /dev/null +++ b/system/libraries/drivers/Cache/File.php @@ -0,0 +1,255 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Memcache-based Cache driver. + * + * $Id: File.php 4605 2009-09-14 17:22:21Z kiall $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Cache_File_Driver extends Cache_Driver { + protected $config; + protected $backend; + + public function __construct($config) + { + $this->config = $config; + $this->config['directory'] = str_replace('\\', '/', realpath($this->config['directory'])).'/'; + + if ( ! is_dir($this->config['directory']) OR ! is_writable($this->config['directory'])) + throw new Cache_Exception('The configured cache directory, :directory:, is not writable.', array(':directory:' => $this->config['directory'])); + } + + /** + * Finds an array of files matching the given id or tag. + * + * @param string cache key or tag + * @param bool search for tags + * @return array of filenames matching the id or tag + */ + public function exists($keys, $tag = FALSE) + { + if ($keys === TRUE) + { + // Find all the files + return glob($this->config['directory'].'*~*~*'); + } + elseif ($tag === TRUE) + { + // Find all the files that have the tag name + $paths = array(); + + foreach ( (array) $keys as $tag) + { + $paths = array_merge($paths, glob($this->config['directory'].'*~*'.$tag.'*~*')); + } + + // 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 + $item_tags = explode('+', $tags[1]); + + // Check each supplied tag, and match aginst the tags on each item. + foreach ($keys as $tag) + { + if (in_array($tag, $item_tags)) + { + // Add the file to the array, it has the requested tag + $files[] = $path; + } + } + } + + return $files; + } + else + { + $paths = array(); + + foreach ( (array) $keys as $key) + { + // Find the file matching the given key + $paths = array_merge($paths, glob($this->config['directory'].str_replace(array('/', '\\', ' '), '_', $key).'~*')); + } + + return $paths; + } + } + + public function set($items, $tags = NULL, $lifetime = NULL) + { + if ($lifetime !== 0) + { + // File driver expects unix timestamp + $lifetime += time(); + } + + + if ( ! is_null($tags) AND ! empty($tags)) + { + // Convert the tags into a string list + $tags = implode('+', (array) $tags); + } + + $success = TRUE; + + foreach ($items as $key => $value) + { + if (is_resource($value)) + throw new Cache_Exception('Caching of resources is impossible, because resources cannot be serialised.'); + + // Remove old cache file + $this->delete($key); + + if ( ! (bool) file_put_contents($this->config['directory'].str_replace(array('/', '\\', ' '), '_', $key).'~'.$tags.'~'.$lifetime, serialize($value))) + { + $success = FALSE; + } + } + + return $success; + } + + public function get($keys, $single = FALSE) + { + $items = array(); + + if ($files = $this->exists($keys)) + { + // Turn off errors while reading the files + $ER = error_reporting(0); + + foreach ($files as $file) + { + // Validate that the item has not expired + if ($this->expired($file)) + continue; + + list($key, $junk) = explode('~', basename($file), 2); + + if (($data = file_get_contents($file)) !== FALSE) + { + // Unserialize the data + $data = unserialize($data); + } + else + { + $data = NULL; + } + + $items[$key] = $data; + } + + // Turn errors back on + error_reporting($ER); + } + + // Reutrn a single item if only one key was requested + if ($single) + { + return (count($items) > 0) ? current($items) : NULL; + } + else + { + return $items; + } + } + + /** + * Get cache items by tag + */ + public function get_tag($tags) + { + // An array will always be returned + $result = array(); + + if ($paths = $this->exists($tags, TRUE)) + { + // Find all the files with the given tag + foreach ($paths as $path) + { + // Get the id from the filename + list($key, $junk) = explode('~', basename($path), 2); + + if (($data = $this->get($key, TRUE)) !== FALSE) + { + // Add the result to the array + $result[$key] = $data; + } + } + } + + return $result; + } + + /** + * Delete cache items by keys or tags + */ + public function delete($keys, $tag = FALSE) + { + $success = TRUE; + + $paths = $this->exists($keys, $tag); + + // Disable all error reporting while deleting + $ER = error_reporting(0); + + foreach ($paths as $path) + { + // Remove the cache file + if ( ! unlink($path)) + { + Kohana_Log::add('error', 'Cache: Unable to delete cache file: '.$path); + $success = FALSE; + } + } + + // Turn on error reporting again + error_reporting($ER); + + return $success; + } + + /** + * Delete cache items by tag + */ + public function delete_tag($tags) + { + return $this->delete($tags, TRUE); + } + + /** + * Empty the cache + */ + public function delete_all() + { + return $this->delete(TRUE); + } + + /** + * Check if a cache file has expired by filename. + * + * @param string|array array of filenames + * @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 Memcache Driver diff --git a/system/libraries/drivers/Cache/Memcache.php b/system/libraries/drivers/Cache/Memcache.php new file mode 100644 index 0000000..13d61d8 --- /dev/null +++ b/system/libraries/drivers/Cache/Memcache.php @@ -0,0 +1,132 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Memcache-based Cache driver. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Cache_Memcache_Driver extends Cache_Driver { + protected $config; + protected $backend; + protected $flags; + + public function __construct($config) + { + if ( ! extension_loaded('memcache')) + throw new Cache_Exception('The memcache PHP extension must be loaded to use this driver.'); + + ini_set('memcache.allow_failover', (isset($config['allow_failover']) AND $config['allow_failover']) ? TRUE : FALSE); + + $this->config = $config; + $this->backend = new Memcache; + + $this->flags = (isset($config['compression']) AND $config['compression']) ? MEMCACHE_COMPRESSED : FALSE; + + foreach ($config['servers'] as $server) + { + // Make sure all required keys are set + $server += array('host' => '127.0.0.1', + 'port' => 11211, + 'persistent' => FALSE, + 'weight' => 1, + 'timeout' => 1, + 'retry_interval' => 15 + ); + + // Add the server to the pool + $this->backend->addServer($server['host'], $server['port'], (bool) $server['persistent'], (int) $server['weight'], (int) $server['timeout'], (int) $server['retry_interval'], TRUE, array($this,'_memcache_failure_callback')); + } + } + + public function _memcache_failure_callback($host, $port) + { + $this->backend->setServerParams($host, $port, 1, -1, FALSE); + Kohana_Log::add('error', __('Cache: Memcache server down: :host:::port:',array(':host:' => $host,':port:' => $port))); + } + + public function set($items, $tags = NULL, $lifetime = NULL) + { + if ($lifetime !== 0) + { + // Memcache driver expects unix timestamp + $lifetime += time(); + } + + if ($tags !== NULL) + throw new Cache_Exception('Memcache driver does not support tags'); + + foreach ($items as $key => $value) + { + if (is_resource($value)) + throw new Cache_Exception('Caching of resources is impossible, because resources cannot be serialised.'); + + if ( ! $this->backend->set($key, $value, $this->flags, $lifetime)) + { + return FALSE; + } + } + + return TRUE; + } + + public function get($keys, $single = FALSE) + { + $items = $this->backend->get($keys); + + if ($single) + { + if ($items === FALSE) + return NULL; + + return (count($items) > 0) ? current($items) : NULL; + } + else + { + return ($items === FALSE) ? array() : $items; + } + } + + /** + * Get cache items by tag + */ + public function get_tag($tags) + { + throw new Cache_Exception('Memcache driver does not support tags'); + } + + /** + * Delete cache item by key + */ + public function delete($keys) + { + foreach ($keys as $key) + { + if ( ! $this->backend->delete($key)) + { + return FALSE; + } + } + + return TRUE; + } + + /** + * Delete cache items by tag + */ + public function delete_tag($tags) + { + throw new Cache_Exception('Memcache driver does not support tags'); + } + + /** + * Empty the cache + */ + public function delete_all() + { + return $this->backend->flush(); + } +} // End Cache Memcache Driver diff --git a/system/libraries/drivers/Cache/Xcache.php b/system/libraries/drivers/Cache/Xcache.php new file mode 100644 index 0000000..6761983 --- /dev/null +++ b/system/libraries/drivers/Cache/Xcache.php @@ -0,0 +1,161 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * XCache-based Cache driver. + * + * $Id: Memcache.php 4605 2009-09-14 17:22:21Z kiall $ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + * @TODO Check if XCache cleans its own keys. + */ +class Cache_Xcache_Driver extends Cache_Driver { + protected $config; + + public function __construct($config) + { + if ( ! extension_loaded('xcache')) + throw new Cache_Exception('The xcache PHP extension must be loaded to use this driver.'); + + $this->config = $config; + } + + public function set($items, $tags = NULL, $lifetime = NULL) + { + if ($tags !== NULL) + { + Kohana_Log::add('debug', __('Cache: XCache driver does not support tags')); + } + + foreach ($items as $key => $value) + { + if (is_resource($value)) + throw new Cache_Exception('Caching of resources is impossible, because resources cannot be serialised.'); + + if ( ! xcache_set($key, $value, $lifetime)) + { + return FALSE; + } + } + + return TRUE; + } + + public function get($keys, $single = FALSE) + { + $items = array(); + + foreach ($keys as $key) + { + if (xcache_isset($key)) + { + $items[$key] = xcache_get($key); + } + else + { + $items[$key] = NULL; + } + } + + if ($single) + { + return ($items === FALSE OR count($items) > 0) ? current($items) : NULL; + } + else + { + return ($items === FALSE) ? array() : $items; + } + } + + /** + * Get cache items by tag + */ + public function get_tag($tags) + { + Kohana_Log::add('debug', __('Cache: XCache driver does not support tags')); + return NULL; + } + + /** + * Delete cache item by key + */ + public function delete($keys) + { + foreach ($keys as $key) + { + if ( ! xcache_unset($key)) + { + return FALSE; + } + } + + return TRUE; + } + + /** + * Delete cache items by tag + */ + public function delete_tag($tags) + { + Kohana_Log::add('debug', __('Cache: XCache driver does not support tags')); + return NULL; + } + + /** + * Empty the cache + */ + public function delete_all() + { + $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; + } + + 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] = $this->config->{$key}; + } + } + } +} // End Cache XCache Driver diff --git a/system/libraries/drivers/Config.php b/system/libraries/drivers/Config.php new file mode 100644 index 0000000..a82684b --- /dev/null +++ b/system/libraries/drivers/Config.php @@ -0,0 +1,257 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Kohana_Config abstract driver to get and set + * configuration options. + * + * Specific drivers should implement caching and encryption + * as they deem appropriate. + * + * $Id: Config.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Kohana_Config + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + * @abstract + */ +abstract class Config_Driver { + + /** + * Internal caching + * + * @var Cache + */ + protected $cache; + + /** + * The name of the internal cache + * + * @var string + */ + protected $cache_name = 'Kohana_Config_Cache'; + + /** + * Cache Lifetime + * + * @var mixed + */ + protected $cache_lifetime = FALSE; + + /** + * The Encryption library + * + * @var Encrypt + */ + protected $encrypt; + + /** + * The config loaded + * + * @var array + */ + protected $config = array(); + + /** + * The changed status of configuration values, + * current state versus the stored state. + * + * @var bool + */ + protected $changed = FALSE; + + /** + * Determines if any config has been loaded yet + */ + public $loaded = FALSE; + + /** + * Array driver constructor. Sets up the PHP array + * driver, including caching and encryption if + * required + * + * @access public + */ + public function __construct($config) + { + + if (($cache_setting = $config['internal_cache']) !== FALSE) + { + $this->cache_lifetime = $cache_setting; + // Restore the cached configuration + $this->config = $this->load_cache(); + + if (count($this->config) > 0) + $this->loaded = TRUE; + + // Add the save cache method to system.shutshut event + Event::add('system.shutdown', array($this, 'save_cache')); + } + + } + + /** + * Gets a value from config. If required is TRUE + * then get will throw an exception if value cannot + * be loaded. + * + * @param string key the setting to get + * @param bool slash remove trailing slashes + * @param bool required is setting required? + * @return mixed + * @access public + */ + public function get($key, $slash = FALSE, $required = FALSE) + { + // Get the group name from the key + $group = explode('.', $key, 2); + $group = $group[0]; + + // Check for existing value and load it dynamically if required + if ( ! isset($this->config[$group])) + $this->config[$group] = $this->load($group, $required); + + // Get the value of the key string + $value = Kohana::key_string($this->config, $key); + + if ($slash === TRUE AND is_string($value) AND $value !== '') + { + // Force the value to end with "/" + $value = rtrim($value, '/').'/'; + } + + if (($required === TRUE) AND ($value === null)) + throw new Kohana_Config_Exception('Value not found in config driver'); + + $this->loaded = TRUE; + return $value; + } + + /** + * Sets a new value to the configuration + * + * @param string key + * @param mixed value + * @return bool + * @access public + */ + public function set($key, $value) + { + // Do this to make sure that the config array is already loaded + $this->get($key); + + if (substr($key, 0, 7) === 'routes.') + { + // Routes cannot contain sub keys due to possible dots in regex + $keys = explode('.', $key, 2); + } + else + { + // Convert dot-noted key string to an array + $keys = explode('.', $key); + } + + // Used for recursion + $conf =& $this->config; + $last = count($keys) - 1; + + foreach ($keys as $i => $k) + { + if ($i === $last) + { + $conf[$k] = $value; + } + else + { + $conf =& $conf[$k]; + } + } + + if (substr($key,0,12) === 'core.modules') + { + // Reprocess the include paths + Kohana::include_paths(TRUE); + } + + // Set config to changed + return $this->changed = TRUE; + } + + /** + * Clear the configuration + * + * @param string group + * @return bool + * @access public + */ + public function clear($group) + { + // Remove the group from config + unset($this->config[$group]); + + // Set config to changed + return $this->changed = TRUE; + } + + /** + * Checks whether the setting exists in + * config + * + * @param string $key + * @return bool + * @access public + */ + public function setting_exists($key) + { + return $this->get($key) === NULL; + } + + /** + * Loads a configuration group based on the setting + * + * @param string group + * @param bool required + * @return array + * @access public + * @abstract + */ + abstract public function load($group, $required = FALSE); + + /** + * Loads the cached version of this configuration driver + * + * @return array + * @access public + */ + public function load_cache() + { + // Load the cache for this configuration + $cached_config = Kohana::cache($this->cache_name, $this->cache_lifetime); + + // If the configuration wasn't loaded from the cache + if ($cached_config === NULL) + $cached_config = array(); + + // Return the cached config + return $cached_config; + } + + /** + * Saves a cached version of this configuration driver + * + * @return bool + * @access public + */ + public function save_cache() + { + // If this configuration has changed + if ($this->get('core.internal_cache') !== FALSE AND $this->changed) + { + $data = $this->config; + + // Save the cache + return Kohana::cache_save($this->cache_name, $data, $this->cache_lifetime); + } + + return TRUE; + } +} // End Kohana_Config_Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Config/Array.php b/system/libraries/drivers/Config/Array.php new file mode 100644 index 0000000..b2ca19b --- /dev/null +++ b/system/libraries/drivers/Config/Array.php @@ -0,0 +1,83 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Kohana_Config Array driver to get and set + * configuration options using PHP arrays. + * + * This driver can cache and encrypt settings + * if required. + * + * $Id: Array.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Kohana_Config + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Config_Array_Driver extends Config_Driver { + + /** + * Internal caching + * + * @var Cache + */ + protected $cache; + + /** + * The name of the internal cache + * + * @var string + */ + protected $cache_name = 'Kohana_Config_Array_Cache'; + + /** + * The Encryption library + * + * @var Encrypt + */ + protected $encrypt; + + /** + * Loads a configuration group based on the setting + * + * @param string group + * @param bool required + * @return array + * @access public + */ + public function load($group, $required = FALSE) + { + if ($group === 'core') + { + // Load the application configuration file + require APPPATH.'config/config'.EXT; + + if ( ! isset($config['site_domain'])) + { + // Invalid config file + throw new Kohana_Config_Exception('Your Kohana application configuration file is not valid.'); + } + + return $config; + } + + // Load matching configs + $configuration = array(); + + if ($files = Kohana::find_file('config', $group, $required)) + { + foreach ($files as $file) + { + require $file; + + if (isset($config) AND is_array($config)) + { + // Merge in configuration + $configuration = array_merge($configuration, $config); + } + } + } + + // Return merged configuration + return $configuration; + } +} // End Config_Array_Driver
\ No newline at end of file diff --git a/system/libraries/drivers/Image.php b/system/libraries/drivers/Image.php new file mode 100644 index 0000000..39936c3 --- /dev/null +++ b/system/libraries/drivers/Image.php @@ -0,0 +1,158 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Image API driver. + * + * $Id: Image.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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 + * @param boolean render the image + * @param string background color + * @return boolean + */ + abstract public function process($image, $actions, $dir, $file, $render = FALSE, $background = NULL); + + /** + * 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 0000000..6ffffe8 --- /dev/null +++ b/system/libraries/drivers/Image/GD.php @@ -0,0 +1,440 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * GD Image Driver. + * + * $Id: GD.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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('The Image library requires GD2. Please see http://php.net/gd_info for more information.'); + + // 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('The Image library requires GD2. Please see http://php.net/gd_info for more information.'); + } + + public function process($image, $actions, $dir, $file, $render = FALSE, $background = NULL) + { + // 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('The specified image, :type:, is not an allowed image type.', array(':type:' => $image['file'])); + + // Make sure the image type is supported for saving + if (empty($save) OR ! function_exists($save)) + throw new Kohana_Exception('The specified image, :type:, is not an allowed image type.', array(':type:' => $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('Your configured driver does not support the :method: image transformation.', array(':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; + } + + $this->imagecopymerge_alpha($this->tmp_image, $overlay_img, $properties['x'], $properties['y'], 0, 0, imagesx($overlay_img), imagesy($overlay_img), $properties['transparency']); + + imagedestroy($overlay_img); + + return TRUE; + } + + /** + * A replacement for php's imagecopymerge() function that supports the alpha channel + * See php bug #23815: http://bugs.php.net/bug.php?id=23815 + * + * @param resource $dst_im Destination image link resource + * @param resource $src_im Source image link resource + * @param integer $dst_x x-coordinate of destination point + * @param integer $dst_y y-coordinate of destination point + * @param integer $src_x x-coordinate of source point + * @param integer $src_y y-coordinate of source point + * @param integer $src_w Source width + * @param integer $src_h Source height + * @param integer $pct Transparency percent (0 to 100) + */ + protected function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) + { + // Create a new blank image the site of our source image + $cut = imagecreatetruecolor($src_w, $src_h); + + // Copy the blank image into the destination image where the source goes + imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h); + + // Place the source image in the destination image + imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h); + imagecopymerge($dst_im, $cut, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct); + } + + 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 ($width < 1) + { + $width = 1; + } + + if ($height < 1) + { + $height = 1; + } + + 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 0000000..89b40b4 --- /dev/null +++ b/system/libraries/drivers/Image/GraphicsMagick.php @@ -0,0 +1,225 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * GraphicsMagick Image Driver. + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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('The GraphicsMagick directory specified does not contain a required program.'); + + $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('The GraphicsMagick directory specified does not contain a required program, :gm:.', array(':gm:' => '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, $background = NULL) + { + // Need to implement $background support + if ($background !== NULL) + throw new Kohana_Exception('The GraphicsMagick driver does not support setting a background color'); + + // 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 0000000..55c0ba2 --- /dev/null +++ b/system/libraries/drivers/Image/ImageMagick.php @@ -0,0 +1,233 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * ImageMagick Image Driver. + * + * $Id: ImageMagick.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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('The ImageMagick directory specified does not contain a required program.'); + + $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('The ImageMagick directory specified does not contain a required program, :im:', array(':im:' => '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, $background = NULL) + { + // 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; + + if (is_string($background)) + { + // Set the background color + $this->background = escapeshellarg($background); + } + else + { + // Use a transparent background + $this->background = 'transparent'; + } + + // 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).' -background '.$this->background.' -flatten -quality '.$quality.'% '.$this->cmd_image.' '.$this->new_image)) + { + $this->errors[] = $error; + } + else + { + // Output the image directly to the browser + if ($render === TRUE) + { + $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).' -background '.$this->background.' -flatten -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).' -background '.$this->background.' -flatten '.$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).' -background '.$this->background.' -flatten -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).' -background '.$this->background.' -flatten -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).' -background '.$this->background.' -flatten -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/Log.php b/system/libraries/drivers/Log.php new file mode 100644 index 0000000..cd6dba7 --- /dev/null +++ b/system/libraries/drivers/Log.php @@ -0,0 +1,22 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Log API driver. + * + * $Id: Log.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Kohana_Log + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +abstract class Log_Driver { + + protected $config = array(); + + public function __construct(array $config) + { + $this->config = $config; + } + + abstract public function save(array $messages); +}
\ No newline at end of file diff --git a/system/libraries/drivers/Log/Database.php b/system/libraries/drivers/Log/Database.php new file mode 100644 index 0000000..19db974 --- /dev/null +++ b/system/libraries/drivers/Log/Database.php @@ -0,0 +1,40 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Log API driver. + * + * $Id: Database.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Kohana_Log + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Log_Database_Driver extends Log_Driver { + + public function save(array $messages) + { + $insert = db::build($this->config['group']) + ->insert($this->config['table']) + ->columns(array('date', 'level', 'message')); + + $run_insert = FALSE; + + foreach ($messages AS $message) + { + if ($this->config['log_levels'][$message['type']] <= $this->config['log_threshold']) + { + // Add new message to database + $insert->values($message); + + // There is data to insert + $run_insert = TRUE; + } + } + + // Update the database + if ($run_insert) + { + $insert->execute(); + } + } +}
\ No newline at end of file diff --git a/system/libraries/drivers/Log/File.php b/system/libraries/drivers/Log/File.php new file mode 100644 index 0000000..6ad565b --- /dev/null +++ b/system/libraries/drivers/Log/File.php @@ -0,0 +1,44 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Log API driver. + * + * $Id: File.php 4679 2009-11-10 01:45:52Z isaiah $ + * + * @package Kohana_Log + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Log_File_Driver extends Log_Driver { + + public function save(array $messages) + { + // Filename of the log + $filename = $this->config['log_directory'].'/'.date('Y-m-d').'.log'.EXT; + + if ( ! is_file($filename)) + { + // Write the SYSPATH checking header + file_put_contents($filename, + '<?php defined(\'SYSPATH\') or die(\'No direct script access.\'); ?>'.PHP_EOL.PHP_EOL); + + // Prevent external writes + chmod($filename, $this->config['posix_permissions']); + } + + foreach ($messages AS $message) + { + if ($this->config['log_levels'][$message['type']] <= $this->config['log_threshold']) + { + // Add a new message line + $messages_to_write[] = date($this->config['date_format'], $message['date']).' --- '.$message['type'].': '.$message['message']; + } + } + + if ( ! empty($messages_to_write)) + { + // Write messages to log file + file_put_contents($filename, implode(PHP_EOL, $messages_to_write).PHP_EOL, FILE_APPEND); + } + } +}
\ No newline at end of file diff --git a/system/libraries/drivers/Log/Syslog.php b/system/libraries/drivers/Log/Syslog.php new file mode 100644 index 0000000..5da5d25 --- /dev/null +++ b/system/libraries/drivers/Log/Syslog.php @@ -0,0 +1,34 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Log API driver. + * + * @package Kohana_Log + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Log_Syslog_Driver extends Log_Driver { + + protected $syslog_levels = array('error' => LOG_ERR, + 'alert' => LOG_WARNING, + 'info' => LOG_INFO, + 'debug' => LOG_DEBUG); + + public function save(array $messages) + { + // Open the connection to syslog + openlog($this->config['ident'], LOG_CONS, LOG_USER); + + do + { + // Load the next message + list ($date, $type, $text) = array_shift($messages); + + syslog($this->syslog_levels[$type], $text); + } + while ( ! empty($messages)); + + // Close connection to syslog + closelog(); + } +}
\ 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 0000000..e591b91 --- /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 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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 0000000..459f8b0 --- /dev/null +++ b/system/libraries/drivers/Session/Cache.php @@ -0,0 +1,108 @@ +<?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 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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::add('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('The :group: group is not defined in your configuration.', array(':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) + { + if ( ! Session::$should_save) + return TRUE; + + $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 0000000..88f5e21 --- /dev/null +++ b/system/libraries/drivers/Session/Cookie.php @@ -0,0 +1,83 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session cookie driver. + * + * $Id: Cookie.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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::add('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) + { + if ( ! Session::$should_save) + return TRUE; + + $data = empty($this->encrypt) ? base64_encode($data) : $this->encrypt->encode($data); + + if (strlen($data) > 4048) + { + Kohana_Log::add('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 0000000..7b372d2 --- /dev/null +++ b/system/libraries/drivers/Session/Database.php @@ -0,0 +1,178 @@ +<?php defined('SYSPATH') OR die('No direct access allowed.'); +/** + * Session database driver. + * + * $Id: Database.php 4729 2009-12-29 20:35:19Z isaiah $ + * + * @package Kohana + * @author Kohana Team + * @copyright (c) 2007-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +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']; + } + } + + Kohana_Log::add('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 = db::select('data') + ->from($this->table) + ->where('session_id', '=', $id) + ->limit(1) + ->execute($this->db); + + 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) + { + if ( ! Session::$should_save) + return TRUE; + + $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 = db::insert($this->table, $data) + ->execute($this->db); + } + elseif ($id === $this->session_id) + { + // Do not update the session_id + unset($data['session_id']); + + // Update the existing session + $query = db::update($this->table) + ->set($data) + ->where('session_id', '=', $id) + ->execute($this->db); + } + else + { + // Update the session and id + $query = db::update($this->table) + ->set($data) + ->where('session_id', '=', $this->session_id) + ->execute($this->db); + + // Set the new session id + $this->session_id = $id; + } + + return (bool) $query->count(); + } + + public function destroy($id) + { + // Delete the requested session + db::delete($this->table) + ->where('session_id', '=', $id) + ->execute($this->db); + + // 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 = db::delete($this->table) + ->where('last_activity', '<', time() - $maxlifetime) + ->execute($this->db); + + Kohana_Log::add('debug', 'Session garbage collected: '.$query->count().' row(s) deleted.'); + + return TRUE; + } + +} // End Session Database Driver |
