diff options
Diffstat (limited to 'kohana/libraries/drivers')
27 files changed, 5531 insertions, 0 deletions
diff --git a/kohana/libraries/drivers/Cache.php b/kohana/libraries/drivers/Cache.php new file mode 100644 index 00000000..4357b839 --- /dev/null +++ b/kohana/libraries/drivers/Cache.php @@ -0,0 +1,40 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Cache driver interface. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +interface Cache_Driver { + + /** + * Set a cache item. + */ + public function set($id, $data, $tags, $lifetime); + + /** + * Find all of the cache ids for a given tag. + */ + public function find($tag); + + /** + * Get a cache item. + * Return NULL if the cache item is not found. + */ + public function get($id); + + /** + * Delete cache items by id or tag. + */ + public function delete($id, $tag = FALSE); + + /** + * Deletes all expired cache items. + */ + public function delete_expired(); + +} // End Cache Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Cache/Apc.php b/kohana/libraries/drivers/Cache/Apc.php new file mode 100644 index 00000000..1bf6beac --- /dev/null +++ b/kohana/libraries/drivers/Cache/Apc.php @@ -0,0 +1,53 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * APC-based Cache driver. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Apc_Driver implements Cache_Driver { + + public function __construct() + { + if ( ! extension_loaded('apc')) + throw new Kohana_Exception('cache.extension_not_loaded', 'apc'); + } + + public function get($id) + { + return (($return = apc_fetch($id)) === FALSE) ? NULL : $return; + } + + public function set($id, $data, $tags, $lifetime) + { + count($tags) and Kohana::log('error', 'Cache: tags are unsupported by the APC driver'); + + return apc_store($id, $data, $lifetime); + } + + public function find($tag) + { + return FALSE; + } + + public function delete($id, $tag = FALSE) + { + if ($id === TRUE) + return apc_clear_cache('user'); + + if ($tag == FALSE) + return apc_delete($id); + + return TRUE; + } + + public function delete_expired() + { + return TRUE; + } + +} // End Cache APC Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Cache/Eaccelerator.php b/kohana/libraries/drivers/Cache/Eaccelerator.php new file mode 100644 index 00000000..f39be1ab --- /dev/null +++ b/kohana/libraries/drivers/Cache/Eaccelerator.php @@ -0,0 +1,53 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Eaccelerator-based Cache driver. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Eaccelerator_Driver implements Cache_Driver { + + public function __construct() + { + if ( ! extension_loaded('eaccelerator')) + throw new Kohana_Exception('cache.extension_not_loaded', 'eaccelerator'); + } + + public function get($id) + { + return eaccelerator_get($id); + } + + public function find($tag) + { + return FALSE; + } + + public function set($id, $data, $tags, $lifetime) + { + count($tags) and Kohana::log('error', 'tags are unsupported by the eAccelerator driver'); + + return eaccelerator_put($id, $data, $lifetime); + } + + public function delete($id, $tag = FALSE) + { + if ($id === TRUE) + return eaccelerator_clean(); + + if ($tag == FALSE) + return eaccelerator_rm($id); + + return TRUE; + } + + public function delete_expired() + { + eaccelerator_gc(); + } + +} // End Cache eAccelerator Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Cache/File.php b/kohana/libraries/drivers/Cache/File.php new file mode 100644 index 00000000..01d6c383 --- /dev/null +++ b/kohana/libraries/drivers/Cache/File.php @@ -0,0 +1,245 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * File-based Cache driver. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_File_Driver implements Cache_Driver { + + protected $directory = ''; + + /** + * Tests that the storage location is a directory and is writable. + */ + public function __construct($directory) + { + // Find the real path to the directory + $directory = str_replace('\\', '/', realpath($directory)).'/'; + + // Make sure the cache directory is writable + if ( ! is_dir($directory) OR ! is_writable($directory)) + throw new Kohana_Exception('cache.unwritable', $directory); + + // Directory is valid + $this->directory = $directory; + } + + /** + * Finds an array of files matching the given id or tag. + * + * @param string cache id or tag + * @param bool search for tags + * @return array of filenames matching the id or tag + * @return void if no matching files are found + */ + public function exists($id, $tag = FALSE) + { + if ($id === TRUE) + { + // Find all the files + $files = glob($this->directory.'*~*~*'); + } + elseif ($tag == TRUE) + { + // Find all the files that have the tag name + $files = glob($this->directory.'*~*'.$id.'*~*'); + + // Find all tags matching the given tag + foreach ($files as $i => $file) + { + // Split the files + $tags = explode('~', $file); + + // Find valid tags + if (count($tags) !== 3 OR empty($tags[1])) + continue; + + // Split the tags by plus signs, used to separate tags + $tags = explode('+', $tags[1]); + + if ( ! in_array($tag, $tags)) + { + // This entry does not match the tag + unset($files[$i]); + } + } + } + else + { + // Find all the files matching the given id + $files = glob($this->directory.$id.'~*'); + } + + return empty($files) ? NULL : $files; + } + + /** + * Sets a cache item to the given data, tags, and lifetime. + * + * @param string cache id to set + * @param string data in the cache + * @param array cache tags + * @param integer lifetime + * @return bool + */ + public function set($id, $data, $tags, $lifetime) + { + // Remove old cache files + $this->delete($id); + + // Cache File driver expects unix timestamp + if ($lifetime !== 0) + { + $lifetime += time(); + } + + // Construct the filename + $filename = $id.'~'.implode('+', $tags).'~'.$lifetime; + + // Write the file, appending the sha1 signature to the beginning of the data + return (bool) file_put_contents($this->directory.$filename, sha1($data).$data); + } + + /** + * Finds an array of ids for a given tag. + * + * @param string tag name + * @return array of ids that match the tag + */ + public function find($tag) + { + if ($files = $this->exists($tag, TRUE)) + { + // Length of directory name + $offset = strlen($this->directory); + + // Find all the files with the given tag + $array = array(); + foreach ($files as $file) + { + // Get the id from the filename + $array[] = substr(current(explode('~', $file)), $offset); + } + + return $array; + } + + return FALSE; + } + + /** + * Fetches a cache item. This will delete the item if it is expired or if + * the hash does not match the stored hash. + * + * @param string cache id + * @return mixed|NULL + */ + public function get($id) + { + if ($file = $this->exists($id)) + { + // Always process the first result + $file = current($file); + + // Validate that the cache has not expired + if ($this->expired($file)) + { + // Remove this cache, it has expired + $this->delete($id); + } + else + { + $data = file_get_contents($file); + + // Find the hash of the data + $hash = substr($data, 0, 40); + + // Remove the hash from the data + $data = substr($data, 40); + + if ($hash !== sha1($data)) + { + // Remove this cache, it doesn't validate + $this->delete($id); + + // Unset data to prevent it from being returned + unset($data); + } + } + } + + // Return NULL if there is no data + return isset($data) ? $data : NULL; + } + + /** + * Deletes a cache item by id or tag + * + * @param string cache id or tag, or TRUE for "all items" + * @param boolean use tags + * @return boolean + */ + public function delete($id, $tag = FALSE) + { + $files = $this->exists($id, $tag); + + if (empty($files)) + return FALSE; + + // Disable all error reporting while deleting + $ER = error_reporting(0); + + foreach ($files as $file) + { + // Remove the cache file + if ( ! unlink($file)) + Kohana::log('error', 'Cache: Unable to delete cache file: '.$file); + } + + // Turn on error reporting again + error_reporting($ER); + + return TRUE; + } + + /** + * Deletes all cache files that are older than the current time. + * + * @return void + */ + public function delete_expired() + { + if ($files = $this->exists(TRUE)) + { + foreach ($files as $file) + { + if ($this->expired($file)) + { + // The cache file has already expired, delete it + @unlink($file) or Kohana::log('error', 'Cache: Unable to delete cache file: '.$file); + } + } + } + } + + /** + * Check if a cache file has expired by filename. + * + * @param string filename + * @return bool + */ + protected function expired($file) + { + // Get the expiration time + $expires = (int) substr($file, strrpos($file, '~') + 1); + + // Expirations of 0 are "never expire" + return ($expires !== 0 AND $expires <= time()); + } + +} // End Cache File Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Cache/Memcache.php b/kohana/libraries/drivers/Cache/Memcache.php new file mode 100644 index 00000000..ef3e14e7 --- /dev/null +++ b/kohana/libraries/drivers/Cache/Memcache.php @@ -0,0 +1,78 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Memcache-based Cache driver. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Memcache_Driver implements Cache_Driver { + + // Cache backend object and flags + protected $backend; + protected $flags; + + public function __construct() + { + if ( ! extension_loaded('memcache')) + throw new Kohana_Exception('cache.extension_not_loaded', 'memcache'); + + $this->backend = new Memcache; + $this->flags = Kohana::config('cache_memcache.compression') ? MEMCACHE_COMPRESSED : 0; + + $servers = Kohana::config('cache_memcache.servers'); + + foreach ($servers as $server) + { + // Make sure all required keys are set + $server += array('host' => '127.0.0.1', 'port' => 11211, 'persistent' => FALSE); + + // Add the server to the pool + $this->backend->addServer($server['host'], $server['port'], (bool) $server['persistent']) + or Kohana::log('error', 'Cache: Connection failed: '.$server['host']); + } + } + + public function find($tag) + { + return FALSE; + } + + public function get($id) + { + return (($return = $this->backend->get($id)) === FALSE) ? NULL : $return; + } + + public function set($id, $data, $tags, $lifetime) + { + count($tags) and Kohana::log('error', 'Cache: Tags are unsupported by the memcache driver'); + + // Memcache driver expects unix timestamp + if ($lifetime !== 0) + { + $lifetime += time(); + } + + return $this->backend->set($id, $data, $this->flags, $lifetime); + } + + public function delete($id, $tag = FALSE) + { + if ($id === TRUE) + return $this->backend->flush(); + + if ($tag == FALSE) + return $this->backend->delete($id); + + return TRUE; + } + + public function delete_expired() + { + return TRUE; + } + +} // End Cache Memcache Driver diff --git a/kohana/libraries/drivers/Cache/Sqlite.php b/kohana/libraries/drivers/Cache/Sqlite.php new file mode 100644 index 00000000..bc3ea7a7 --- /dev/null +++ b/kohana/libraries/drivers/Cache/Sqlite.php @@ -0,0 +1,228 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * SQLite-based Cache driver. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Sqlite_Driver implements Cache_Driver { + + // SQLite database instance + protected $db; + + // Database error messages + protected $error; + + /** + * Logs an SQLite error. + */ + protected static function log_error($code) + { + // Log an error + Kohana::log('error', 'Cache: SQLite error: '.sqlite_error_string($error)); + } + + /** + * Tests that the storage location is a directory and is writable. + */ + public function __construct($filename) + { + // Get the directory name + $directory = str_replace('\\', '/', realpath(pathinfo($filename, PATHINFO_DIRNAME))).'/'; + + // Set the filename from the real directory path + $filename = $directory.basename($filename); + + // Make sure the cache directory is writable + if ( ! is_dir($directory) OR ! is_writable($directory)) + throw new Kohana_Exception('cache.unwritable', $directory); + + // Make sure the cache database is writable + if (is_file($filename) AND ! is_writable($filename)) + throw new Kohana_Exception('cache.unwritable', $filename); + + // Open up an instance of the database + $this->db = new SQLiteDatabase($filename, '0666', $error); + + // Throw an exception if there's an error + if ( ! empty($error)) + throw new Kohana_Exception('cache.driver_error', sqlite_error_string($error)); + + $query = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'caches'"; + $tables = $this->db->query($query, SQLITE_BOTH, $error); + + // Throw an exception if there's an error + if ( ! empty($error)) + throw new Kohana_Exception('cache.driver_error', sqlite_error_string($error)); + + if ($tables->numRows() == 0) + { + Kohana::log('error', 'Cache: Initializing new SQLite cache database'); + + // Issue a CREATE TABLE command + $this->db->unbufferedQuery(Kohana::config('cache_sqlite.schema')); + } + } + + /** + * Checks if a cache id is already set. + * + * @param string cache id + * @return boolean + */ + public function exists($id) + { + // Find the id that matches + $query = "SELECT id FROM caches WHERE id = '$id'"; + + return ($this->db->query($query)->numRows() > 0); + } + + /** + * Sets a cache item to the given data, tags, and lifetime. + * + * @param string cache id to set + * @param string data in the cache + * @param array cache tags + * @param integer lifetime + * @return bool + */ + public function set($id, $data, $tags, $lifetime) + { + // Find the data hash + $hash = sha1($data); + + // Escape the data + $data = sqlite_escape_string($data); + + // Escape the tags + $tags = sqlite_escape_string(implode(',', $tags)); + + // Cache Sqlite driver expects unix timestamp + if ($lifetime !== 0) + { + $lifetime += time(); + } + + $query = $this->exists($id) + ? "UPDATE caches SET hash = '$hash', tags = '$tags', expiration = '$lifetime', cache = '$data' WHERE id = '$id'" + : "INSERT INTO caches VALUES('$id', '$hash', '$tags', '$lifetime', '$data')"; + + // Run the query + $this->db->unbufferedQuery($query, SQLITE_BOTH, $error); + + empty($error) or self::log_error($error); + + return empty($error); + } + + /** + * Finds an array of ids for a given tag. + * + * @param string tag name + * @return array of ids that match the tag + */ + public function find($tag) + { + $query = "SELECT id FROM caches WHERE tags LIKE '%{$tag}%'"; + $query = $this->db->query($query, SQLITE_BOTH, $error); + + empty($error) or self::log_error($error); + + if (empty($error) AND $query->numRows() > 0) + { + $array = array(); + while ($row = $query->fetchObject()) + { + // Add each id to the array + $array[] = $row->id; + } + return $array; + } + + return FALSE; + } + + /** + * Fetches a cache item. This will delete the item if it is expired or if + * the hash does not match the stored hash. + * + * @param string cache id + * @return mixed|NULL + */ + public function get($id) + { + $query = "SELECT id, hash, expiration, cache FROM caches WHERE id = '{$id}' LIMIT 0, 1"; + $query = $this->db->query($query, SQLITE_BOTH, $error); + + empty($error) or self::log_error($error); + + if (empty($error) AND $cache = $query->fetchObject()) + { + // Make sure the expiration is valid and that the hash matches + if (($cache->expiration != 0 AND $cache->expiration <= time()) OR $cache->hash !== sha1($cache->cache)) + { + // Cache is not valid, delete it now + $this->delete($cache->id); + } + else + { + // Return the valid cache data + return $cache->cache; + } + } + + // No valid cache found + return NULL; + } + + /** + * Deletes a cache item by id or tag + * + * @param string cache id or tag, or TRUE for "all items" + * @param bool use tags + * @return bool + */ + public function delete($id, $tag = FALSE) + { + if ($id === TRUE) + { + // Delete all caches + $where = '1'; + } + elseif ($tag == FALSE) + { + // Delete by id + $where = "id = '{$id}'"; + } + else + { + // Delete by tag + $where = "tags LIKE '%{$tag}%'"; + } + + $this->db->unbufferedQuery('DELETE FROM caches WHERE '.$where, SQLITE_BOTH, $error); + + empty($error) or self::log_error($error); + + return empty($error); + } + + /** + * Deletes all cache files that are older than the current time. + */ + public function delete_expired() + { + // Delete all expired caches + $query = 'DELETE FROM caches WHERE expiration != 0 AND expiration <= '.time(); + + $this->db->unbufferedQuery($query); + + return TRUE; + } + +} // End Cache SQLite Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Cache/Xcache.php b/kohana/libraries/drivers/Cache/Xcache.php new file mode 100644 index 00000000..98fec8bf --- /dev/null +++ b/kohana/libraries/drivers/Cache/Xcache.php @@ -0,0 +1,116 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Xcache Cache driver. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Xcache_Driver implements Cache_Driver { + + public function __construct() + { + if ( ! extension_loaded('xcache')) + throw new Kohana_Exception('cache.extension_not_loaded', 'xcache'); + } + + public function get($id) + { + if (xcache_isset($id)) + return xcache_get($id); + + return NULL; + } + + public function set($id, $data, $tags, $lifetime) + { + count($tags) and Kohana::log('error', 'Cache: tags are unsupported by the Xcache driver'); + + return xcache_set($id, $data, $lifetime); + } + + public function find($tag) + { + Kohana::log('error', 'Cache: tags are unsupported by the Xcache driver'); + return FALSE; + } + + public function delete($id, $tag = FALSE) + { + if ($tag !== FALSE) + { + Kohana::log('error', 'Cache: tags are unsupported by the Xcache driver'); + return TRUE; + } + elseif ($id !== TRUE) + { + if (xcache_isset($id)) + return xcache_unset($id); + + return FALSE; + } + else + { + // Do the login + $this->auth(); + $result = TRUE; + for ($i = 0, $max = xcache_count(XC_TYPE_VAR); $i < $max; $i++) + { + if (xcache_clear_cache(XC_TYPE_VAR, $i) !== NULL) + { + $result = FALSE; + break; + } + } + + // Undo the login + $this->auth(TRUE); + return $result; + } + + return TRUE; + } + + public function delete_expired() + { + return TRUE; + } + + private function auth($reverse = FALSE) + { + static $backup = array(); + + $keys = array('PHP_AUTH_USER', 'PHP_AUTH_PW'); + + foreach ($keys as $key) + { + if ($reverse) + { + if (isset($backup[$key])) + { + $_SERVER[$key] = $backup[$key]; + unset($backup[$key]); + } + else + { + unset($_SERVER[$key]); + } + } + else + { + $value = getenv($key); + + if ( ! empty($value)) + { + $backup[$key] = $value; + } + + $_SERVER[$key] = Kohana::config('cache_xcache.'.$key); + } + } + } + +} // End Cache Xcache Driver diff --git a/kohana/libraries/drivers/Captcha.php b/kohana/libraries/drivers/Captcha.php new file mode 100644 index 00000000..81358c4d --- /dev/null +++ b/kohana/libraries/drivers/Captcha.php @@ -0,0 +1,227 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha driver class. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Captcha_Driver { + + // The correct Captcha challenge answer + protected $response; + + // Image resource identifier and type ("png", "gif" or "jpeg") + protected $image; + protected $image_type = 'png'; + + /** + * Constructs a new challenge. + * + * @return void + */ + public function __construct() + { + // Generate a new challenge + $this->response = $this->generate_challenge(); + + // Store the correct Captcha response in a session + Event::add('system.post_controller', array($this, 'update_response_session')); + } + + /** + * Generate a new Captcha challenge. + * + * @return string the challenge answer + */ + abstract public function generate_challenge(); + + /** + * Output the Captcha challenge. + * + * @param boolean html output + * @return mixed the rendered Captcha (e.g. an image, riddle, etc.) + */ + abstract public function render($html); + + /** + * Stores the response for the current Captcha challenge in a session so it is available + * on the next page load for Captcha::valid(). This method is called after controller + * execution (in the system.post_controller event) in order not to overwrite itself too soon. + * + * @return void + */ + public function update_response_session() + { + Session::instance()->set('captcha_response', sha1(strtoupper($this->response))); + } + + /** + * Validates a Captcha response from a user. + * + * @param string captcha response + * @return boolean + */ + public function valid($response) + { + return (sha1(strtoupper($response)) === Session::instance()->get('captcha_response')); + } + + /** + * Returns the image type. + * + * @param string filename + * @return string|FALSE image type ("png", "gif" or "jpeg") + */ + public function image_type($filename) + { + switch (strtolower(substr(strrchr($filename, '.'), 1))) + { + case 'png': + return 'png'; + + case 'gif': + return 'gif'; + + case 'jpg': + case 'jpeg': + // Return "jpeg" and not "jpg" because of the GD2 function names + return 'jpeg'; + + default: + return FALSE; + } + } + + /** + * Creates an image resource with the dimensions specified in config. + * If a background image is supplied, the image dimensions are used. + * + * @throws Kohana_Exception if no GD2 support + * @param string path to the background image file + * @return void + */ + public function image_create($background = NULL) + { + // Check for GD2 support + if ( ! function_exists('imagegd2')) + throw new Kohana_Exception('captcha.requires_GD2'); + + // Create a new image (black) + $this->image = imagecreatetruecolor(Captcha::$config['width'], Captcha::$config['height']); + + // Use a background image + if ( ! empty($background)) + { + // Create the image using the right function for the filetype + $function = 'imagecreatefrom'.$this->image_type($filename); + $this->background_image = $function($background); + + // Resize the image if needed + if (imagesx($this->background_image) !== Captcha::$config['width'] + OR imagesy($this->background_image) !== Captcha::$config['height']) + { + imagecopyresampled + ( + $this->image, $this->background_image, 0, 0, 0, 0, + Captcha::$config['width'], Captcha::$config['height'], + imagesx($this->background_image), imagesy($this->background_image) + ); + } + + // Free up resources + imagedestroy($this->background_image); + } + } + + /** + * Fills the background with a gradient. + * + * @param resource gd image color identifier for start color + * @param resource gd image color identifier for end color + * @param string direction: 'horizontal' or 'vertical', 'random' by default + * @return void + */ + public function image_gradient($color1, $color2, $direction = NULL) + { + $directions = array('horizontal', 'vertical'); + + // Pick a random direction if needed + if ( ! in_array($direction, $directions)) + { + $direction = $directions[array_rand($directions)]; + + // Switch colors + if (mt_rand(0, 1) === 1) + { + $temp = $color1; + $color1 = $color2; + $color2 = $temp; + } + } + + // Extract RGB values + $color1 = imagecolorsforindex($this->image, $color1); + $color2 = imagecolorsforindex($this->image, $color2); + + // Preparations for the gradient loop + $steps = ($direction === 'horizontal') ? Captcha::$config['width'] : Captcha::$config['height']; + + $r1 = ($color1['red'] - $color2['red']) / $steps; + $g1 = ($color1['green'] - $color2['green']) / $steps; + $b1 = ($color1['blue'] - $color2['blue']) / $steps; + + if ($direction === 'horizontal') + { + $x1 =& $i; + $y1 = 0; + $x2 =& $i; + $y2 = Captcha::$config['height']; + } + else + { + $x1 = 0; + $y1 =& $i; + $x2 = Captcha::$config['width']; + $y2 =& $i; + } + + // Execute the gradient loop + for ($i = 0; $i <= $steps; $i++) + { + $r2 = $color1['red'] - floor($i * $r1); + $g2 = $color1['green'] - floor($i * $g1); + $b2 = $color1['blue'] - floor($i * $b1); + $color = imagecolorallocate($this->image, $r2, $g2, $b2); + + imageline($this->image, $x1, $y1, $x2, $y2, $color); + } + } + + /** + * Returns the img html element or outputs the image to the browser. + * + * @param boolean html output + * @return mixed html string or void + */ + public function image_render($html) + { + // Output html element + if ($html) + return '<img alt="Captcha" src="'.url::site('captcha/'.Captcha::$config['group']).'" width="'.Captcha::$config['width'].'" height="'.Captcha::$config['height'].'" />'; + + // Send the correct HTTP header + header('Content-Type: image/'.$this->image_type); + + // Pick the correct output function + $function = 'image'.$this->image_type; + $function($this->image); + + // Free up resources + imagedestroy($this->image); + } + +} // End Captcha Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Captcha/Alpha.php b/kohana/libraries/drivers/Captcha/Alpha.php new file mode 100644 index 00000000..2d13e6f8 --- /dev/null +++ b/kohana/libraries/drivers/Captcha/Alpha.php @@ -0,0 +1,92 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha driver for "alpha" style. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Alpha_Driver extends Captcha_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Complexity setting is used as character count + return text::random('distinct', max(1, Captcha::$config['complexity'])); + } + + /** + * Outputs the Captcha image. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + // Creates $this->image + $this->image_create(Captcha::$config['background']); + + // Add a random gradient + if (empty(Captcha::$config['background'])) + { + $color1 = imagecolorallocate($this->image, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)); + $color2 = imagecolorallocate($this->image, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100)); + $this->image_gradient($color1, $color2); + } + + // Add a few random circles + for ($i = 0, $count = mt_rand(10, Captcha::$config['complexity'] * 3); $i < $count; $i++) + { + $color = imagecolorallocatealpha($this->image, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255), mt_rand(80, 120)); + $size = mt_rand(5, Captcha::$config['height'] / 3); + imagefilledellipse($this->image, mt_rand(0, Captcha::$config['width']), mt_rand(0, Captcha::$config['height']), $size, $size, $color); + } + + // Calculate character font-size and spacing + $default_size = min(Captcha::$config['width'], Captcha::$config['height'] * 2) / strlen($this->response); + $spacing = (int) (Captcha::$config['width'] * 0.9 / strlen($this->response)); + + // Background alphabetic character attributes + $color_limit = mt_rand(96, 160); + $chars = 'ABEFGJKLPQRTVY'; + + // Draw each Captcha character with varying attributes + for ($i = 0, $strlen = strlen($this->response); $i < $strlen; $i++) + { + // Use different fonts if available + $font = Captcha::$config['fontpath'].Captcha::$config['fonts'][array_rand(Captcha::$config['fonts'])]; + + $angle = mt_rand(-40, 20); + // Scale the character size on image height + $size = $default_size / 10 * mt_rand(8, 12); + $box = imageftbbox($size, $angle, $font, $this->response[$i]); + + // Calculate character starting coordinates + $x = $spacing / 4 + $i * $spacing; + $y = Captcha::$config['height'] / 2 + ($box[2] - $box[5]) / 4; + + // Draw captcha text character + // Allocate random color, size and rotation attributes to text + $color = imagecolorallocate($this->image, mt_rand(150, 255), mt_rand(200, 255), mt_rand(0, 255)); + + // Write text character to image + imagefttext($this->image, $size, $angle, $x, $y, $color, $font, $this->response[$i]); + + // Draw "ghost" alphabetic character + $text_color = imagecolorallocatealpha($this->image, mt_rand($color_limit + 8, 255), mt_rand($color_limit + 8, 255), mt_rand($color_limit + 8, 255), mt_rand(70, 120)); + $char = substr($chars, mt_rand(0, 14), 1); + imagettftext($this->image, $size * 2, mt_rand(-45, 45), ($x - (mt_rand(5, 10))), ($y + (mt_rand(5, 10))), $text_color, $font, $char); + } + + // Output + return $this->image_render($html); + } + +} // End Captcha Alpha Driver Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Captcha/Basic.php b/kohana/libraries/drivers/Captcha/Basic.php new file mode 100644 index 00000000..9edf346a --- /dev/null +++ b/kohana/libraries/drivers/Captcha/Basic.php @@ -0,0 +1,81 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha driver for "basic" style. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Basic_Driver extends Captcha_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Complexity setting is used as character count + return text::random('distinct', max(1, Captcha::$config['complexity'])); + } + + /** + * Outputs the Captcha image. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + // Creates $this->image + $this->image_create(Captcha::$config['background']); + + // Add a random gradient + if (empty(Captcha::$config['background'])) + { + $color1 = imagecolorallocate($this->image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(150, 255)); + $color2 = imagecolorallocate($this->image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(150, 255)); + $this->image_gradient($color1, $color2); + } + + // Add a few random lines + for ($i = 0, $count = mt_rand(5, Captcha::$config['complexity'] * 4); $i < $count; $i++) + { + $color = imagecolorallocatealpha($this->image, mt_rand(0, 255), mt_rand(0, 255), mt_rand(100, 255), mt_rand(50, 120)); + imageline($this->image, mt_rand(0, Captcha::$config['width']), 0, mt_rand(0, Captcha::$config['width']), Captcha::$config['height'], $color); + } + + // Calculate character font-size and spacing + $default_size = min(Captcha::$config['width'], Captcha::$config['height'] * 2) / (strlen($this->response) + 1); + $spacing = (int) (Captcha::$config['width'] * 0.9 / strlen($this->response)); + + // Draw each Captcha character with varying attributes + for ($i = 0, $strlen = strlen($this->response); $i < $strlen; $i++) + { + // Use different fonts if available + $font = Captcha::$config['fontpath'].Captcha::$config['fonts'][array_rand(Captcha::$config['fonts'])]; + + // Allocate random color, size and rotation attributes to text + $color = imagecolorallocate($this->image, mt_rand(0, 150), mt_rand(0, 150), mt_rand(0, 150)); + $angle = mt_rand(-40, 20); + + // Scale the character size on image height + $size = $default_size / 10 * mt_rand(8, 12); + $box = imageftbbox($size, $angle, $font, $this->response[$i]); + + // Calculate character starting coordinates + $x = $spacing / 4 + $i * $spacing; + $y = Captcha::$config['height'] / 2 + ($box[2] - $box[5]) / 4; + + // Write text character to image + imagefttext($this->image, $size, $angle, $x, $y, $color, $font, $this->response[$i]); + } + + // Output + return $this->image_render($html); + } + +} // End Captcha Basic Driver Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Captcha/Black.php b/kohana/libraries/drivers/Captcha/Black.php new file mode 100644 index 00000000..6f1997e9 --- /dev/null +++ b/kohana/libraries/drivers/Captcha/Black.php @@ -0,0 +1,72 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha driver for "black" style. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Black_Driver extends Captcha_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Complexity setting is used as character count + return text::random('distinct', max(1, ceil(Captcha::$config['complexity'] / 1.5))); + } + + /** + * Outputs the Captcha image. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + // Creates a black image to start from + $this->image_create(Captcha::$config['background']); + + // Add random white/gray arcs, amount depends on complexity setting + $count = (Captcha::$config['width'] + Captcha::$config['height']) / 2; + $count = $count / 5 * min(10, Captcha::$config['complexity']); + for ($i = 0; $i < $count; $i++) + { + imagesetthickness($this->image, mt_rand(1, 2)); + $color = imagecolorallocatealpha($this->image, 255, 255, 255, mt_rand(0, 120)); + imagearc($this->image, mt_rand(-Captcha::$config['width'], Captcha::$config['width']), mt_rand(-Captcha::$config['height'], Captcha::$config['height']), mt_rand(-Captcha::$config['width'], Captcha::$config['width']), mt_rand(-Captcha::$config['height'], Captcha::$config['height']), mt_rand(0, 360), mt_rand(0, 360), $color); + } + + // Use different fonts if available + $font = Captcha::$config['fontpath'].Captcha::$config['fonts'][array_rand(Captcha::$config['fonts'])]; + + // Draw the character's white shadows + $size = (int) min(Captcha::$config['height'] / 2, Captcha::$config['width'] * 0.8 / strlen($this->response)); + $angle = mt_rand(-15 + strlen($this->response), 15 - strlen($this->response)); + $x = mt_rand(1, Captcha::$config['width'] * 0.9 - $size * strlen($this->response)); + $y = ((Captcha::$config['height'] - $size) / 2) + $size; + $color = imagecolorallocate($this->image, 255, 255, 255); + imagefttext($this->image, $size, $angle, $x + 1, $y + 1, $color, $font, $this->response); + + // Add more shadows for lower complexities + (Captcha::$config['complexity'] < 10) and imagefttext($this->image, $size, $angle, $x - 1, $y - 1, $color, $font , $this->response); + (Captcha::$config['complexity'] < 8) and imagefttext($this->image, $size, $angle, $x - 2, $y + 2, $color, $font , $this->response); + (Captcha::$config['complexity'] < 6) and imagefttext($this->image, $size, $angle, $x + 2, $y - 2, $color, $font , $this->response); + (Captcha::$config['complexity'] < 4) and imagefttext($this->image, $size, $angle, $x + 3, $y + 3, $color, $font , $this->response); + (Captcha::$config['complexity'] < 2) and imagefttext($this->image, $size, $angle, $x - 3, $y - 3, $color, $font , $this->response); + + // Finally draw the foreground characters + $color = imagecolorallocate($this->image, 0, 0, 0); + imagefttext($this->image, $size, $angle, $x, $y, $color, $font, $this->response); + + // Output + return $this->image_render($html); + } + +} // End Captcha Black Driver Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Captcha/Math.php b/kohana/libraries/drivers/Captcha/Math.php new file mode 100644 index 00000000..15dca8e7 --- /dev/null +++ b/kohana/libraries/drivers/Captcha/Math.php @@ -0,0 +1,61 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha driver for "math" style. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Math_Driver extends Captcha_Driver { + + private $math_exercice; + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Easy + if (Captcha::$config['complexity'] < 4) + { + $numbers[] = mt_rand(1, 5); + $numbers[] = mt_rand(1, 4); + } + // Normal + elseif (Captcha::$config['complexity'] < 7) + { + $numbers[] = mt_rand(10, 20); + $numbers[] = mt_rand(1, 10); + } + // Difficult, well, not really ;) + else + { + $numbers[] = mt_rand(100, 200); + $numbers[] = mt_rand(10, 20); + $numbers[] = mt_rand(1, 10); + } + + // Store the question for output + $this->math_exercice = implode(' + ', $numbers).' = '; + + // Return the answer + return array_sum($numbers); + } + + /** + * Outputs the Captcha riddle. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + return $this->math_exercice; + } + +} // End Captcha Math Driver Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Captcha/Riddle.php b/kohana/libraries/drivers/Captcha/Riddle.php new file mode 100644 index 00000000..b79e595f --- /dev/null +++ b/kohana/libraries/drivers/Captcha/Riddle.php @@ -0,0 +1,47 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha driver for "riddle" style. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Riddle_Driver extends Captcha_Driver { + + private $riddle; + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Load riddles from the current language + $riddles = Kohana::lang('captcha.riddles'); + + // Pick a random riddle + $riddle = $riddles[array_rand($riddles)]; + + // Store the question for output + $this->riddle = $riddle[0]; + + // Return the answer + return $riddle[1]; + } + + /** + * Outputs the Captcha riddle. + * + * @param boolean html output + * @return mixed + */ + public function render($html) + { + return $this->riddle; + } + +} // End Captcha Riddle Driver Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Captcha/Word.php b/kohana/libraries/drivers/Captcha/Word.php new file mode 100644 index 00000000..940c4f92 --- /dev/null +++ b/kohana/libraries/drivers/Captcha/Word.php @@ -0,0 +1,37 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha driver for "word" style. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Word_Driver extends Captcha_Basic_Driver { + + /** + * Generates a new Captcha challenge. + * + * @return string the challenge answer + */ + public function generate_challenge() + { + // Load words from the current language and randomize them + $words = Kohana::lang('captcha.words'); + shuffle($words); + + // Loop over each word... + foreach ($words as $word) + { + // ...until we find one of the desired length + if (abs(Captcha::$config['complexity'] - strlen($word)) < 2) + return strtoupper($word); + } + + // Return any random word as final fallback + return strtoupper($words[array_rand($words)]); + } + +} // End Captcha Word Driver Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Database.php b/kohana/libraries/drivers/Database.php new file mode 100644 index 00000000..74d89c36 --- /dev/null +++ b/kohana/libraries/drivers/Database.php @@ -0,0 +1,633 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Database API driver + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Database_Driver { + + static $query_cache; + + /** + * Connect to our database. + * Returns FALSE on failure or a MySQL resource. + * + * @return mixed + */ + abstract public function connect(); + + /** + * Perform a query based on a manually written query. + * + * @param string SQL query to execute + * @return Database_Result + */ + abstract public function query($sql); + + /** + * Builds a DELETE query. + * + * @param string table name + * @param array where clause + * @return string + */ + public function delete($table, $where) + { + return 'DELETE FROM '.$this->escape_table($table).' WHERE '.implode(' ', $where); + } + + /** + * Builds an UPDATE query. + * + * @param string table name + * @param array key => value pairs + * @param array where clause + * @return string + */ + public function update($table, $values, $where) + { + foreach ($values as $key => $val) + { + $valstr[] = $this->escape_column($key).' = '.$val; + } + return 'UPDATE '.$this->escape_table($table).' SET '.implode(', ', $valstr).' WHERE '.implode(' ',$where); + } + + /** + * Set the charset using 'SET NAMES <charset>'. + * + * @param string character set to use + */ + public function set_charset($charset) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Wrap the tablename in backticks, has support for: table.field syntax. + * + * @param string table name + * @return string + */ + abstract public function escape_table($table); + + /** + * Escape a column/field name, has support for special commands. + * + * @param string column name + * @return string + */ + abstract public function escape_column($column); + + /** + * Builds a WHERE portion of a query. + * + * @param mixed key + * @param string value + * @param string type + * @param int number of where clauses + * @param boolean escape the value + * @return string + */ + public function where($key, $value, $type, $num_wheres, $quote) + { + $prefix = ($num_wheres == 0) ? '' : $type; + + if ($quote === -1) + { + $value = ''; + } + else + { + if ($value === NULL) + { + if ( ! $this->has_operator($key)) + { + $key .= ' IS'; + } + + $value = ' NULL'; + } + elseif (is_bool($value)) + { + if ( ! $this->has_operator($key)) + { + $key .= ' ='; + } + + $value = ($value == TRUE) ? ' 1' : ' 0'; + } + else + { + if ( ! $this->has_operator($key)) + { + $key = $this->escape_column($key).' ='; + } + else + { + preg_match('/^(.+?)([<>!=]+|\bIS(?:\s+NULL))\s*$/i', $key, $matches); + if (isset($matches[1]) AND isset($matches[2])) + { + $key = $this->escape_column(trim($matches[1])).' '.trim($matches[2]); + } + } + + $value = ' '.(($quote == TRUE) ? $this->escape($value) : $value); + } + } + + return $prefix.$key.$value; + } + + /** + * Builds a LIKE portion of a query. + * + * @param mixed field name + * @param string value to match with field + * @param boolean add wildcards before and after the match + * @param string clause type (AND or OR) + * @param int number of likes + * @return string + */ + public function like($field, $match = '', $auto = TRUE, $type = 'AND ', $num_likes) + { + $prefix = ($num_likes == 0) ? '' : $type; + + $match = $this->escape_str($match); + + if ($auto === TRUE) + { + // Add the start and end quotes + $match = '%'.$match.'%'; + } + + return $prefix.' '.$this->escape_column($field).' LIKE \''.$match . '\''; + } + + /** + * Builds a NOT LIKE portion of a query. + * + * @param mixed field name + * @param string value to match with field + * @param string clause type (AND or OR) + * @param int number of likes + * @return string + */ + public function notlike($field, $match = '', $auto = TRUE, $type = 'AND ', $num_likes) + { + $prefix = ($num_likes == 0) ? '' : $type; + + $match = $this->escape_str($match); + + if ($auto === TRUE) + { + // Add the start and end quotes + $match = '%'.$match.'%'; + } + + return $prefix.' '.$this->escape_column($field).' NOT LIKE \''.$match.'\''; + } + + /** + * Builds a REGEX portion of a query. + * + * @param string field name + * @param string value to match with field + * @param string clause type (AND or OR) + * @param integer number of regexes + * @return string + */ + public function regex($field, $match, $type, $num_regexs) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Builds a NOT REGEX portion of a query. + * + * @param string field name + * @param string value to match with field + * @param string clause type (AND or OR) + * @param integer number of regexes + * @return string + */ + public function notregex($field, $match, $type, $num_regexs) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Builds an INSERT query. + * + * @param string table name + * @param array keys + * @param array values + * @return string + */ + public function insert($table, $keys, $values) + { + // Escape the column names + foreach ($keys as $key => $value) + { + $keys[$key] = $this->escape_column($value); + } + return 'INSERT INTO '.$this->escape_table($table).' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + /** + * Builds a MERGE portion of a query. + * + * @param string table name + * @param array keys + * @param array values + * @return string + */ + public function merge($table, $keys, $values) + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Builds a LIMIT portion of a query. + * + * @param integer limit + * @param integer offset + * @return string + */ + abstract public function limit($limit, $offset = 0); + + /** + * Creates a prepared statement. + * + * @param string SQL query + * @return Database_Stmt + */ + public function stmt_prepare($sql = '') + { + throw new Kohana_Database_Exception('database.not_implemented', __FUNCTION__); + } + + /** + * Compiles the SELECT statement. + * Generates a query string based on which functions were used. + * Should not be called directly, the get() function calls it. + * + * @param array select query values + * @return string + */ + abstract public function compile_select($database); + + /** + * Determines if the string has an arithmetic operator in it. + * + * @param string string to check + * @return boolean + */ + public function has_operator($str) + { + return (bool) preg_match('/[<>!=]|\sIS(?:\s+NOT\s+)?\b/i', trim($str)); + } + + /** + * Escapes any input value. + * + * @param mixed value to escape + * @return string + */ + public function escape($value) + { + if ( ! $this->db_config['escape']) + return $value; + + switch (gettype($value)) + { + case 'string': + $value = '\''.$this->escape_str($value).'\''; + break; + case 'boolean': + $value = (int) $value; + break; + case 'double': + // Convert to non-locale aware float to prevent possible commas + $value = sprintf('%F', $value); + break; + default: + $value = ($value === NULL) ? 'NULL' : $value; + break; + } + + return (string) $value; + } + + /** + * Escapes a string for a query. + * + * @param mixed value to escape + * @return string + */ + abstract public function escape_str($str); + + /** + * Lists all tables in the database. + * + * @return array + */ + abstract public function list_tables(); + + /** + * Lists all fields in a table. + * + * @param string table name + * @return array + */ + abstract function list_fields($table); + + /** + * Returns the last database error. + * + * @return string + */ + abstract public function show_error(); + + /** + * Returns field data about a table. + * + * @param string table name + * @return array + */ + abstract public function field_data($table); + + /** + * Fetches SQL type information about a field, in a generic format. + * + * @param string field datatype + * @return array + */ + protected function sql_type($str) + { + static $sql_types; + + if ($sql_types === NULL) + { + // Load SQL data types + $sql_types = Kohana::config('sql_types'); + } + + $str = strtolower(trim($str)); + + if (($open = strpos($str, '(')) !== FALSE) + { + // Find closing bracket + $close = strpos($str, ')', $open) - 1; + + // Find the type without the size + $type = substr($str, 0, $open); + } + else + { + // No length + $type = $str; + } + + empty($sql_types[$type]) and exit + ( + 'Unknown field type: '.$type.'. '. + 'Please report this: http://trac.kohanaphp.com/newticket' + ); + + // Fetch the field definition + $field = $sql_types[$type]; + + switch ($field['type']) + { + case 'string': + case 'float': + if (isset($close)) + { + // Add the length to the field info + $field['length'] = substr($str, $open + 1, $close - $open); + } + break; + case 'int': + // Add unsigned value + $field['unsigned'] = (strpos($str, 'unsigned') !== FALSE); + break; + } + + return $field; + } + + /** + * Clears the internal query cache. + * + * @param string SQL query + */ + public function clear_cache($sql = NULL) + { + if (empty($sql)) + { + self::$query_cache = array(); + } + else + { + unset(self::$query_cache[$this->query_hash($sql)]); + } + + Kohana::log('debug', 'Database cache cleared: '.get_class($this)); + } + + /** + * Creates a hash for an SQL query string. Replaces newlines with spaces, + * trims, and hashes. + * + * @param string SQL query + * @return string + */ + protected function query_hash($sql) + { + return sha1(str_replace("\n", ' ', trim($sql))); + } + +} // End Database Driver Interface + +/** + * Database_Result + * + */ +abstract class Database_Result implements ArrayAccess, Iterator, Countable { + + // Result resource, insert id, and SQL + protected $result; + protected $insert_id; + protected $sql; + + // Current and total rows + protected $current_row = 0; + protected $total_rows = 0; + + // Fetch function and return type + protected $fetch_type; + protected $return_type; + + /** + * Returns the SQL used to fetch the result. + * + * @return string + */ + public function sql() + { + return $this->sql; + } + + /** + * Returns the insert id from the result. + * + * @return mixed + */ + public function insert_id() + { + return $this->insert_id; + } + + /** + * Prepares the query result. + * + * @param boolean return rows as objects + * @param mixed type + * @return Database_Result + */ + abstract function result($object = TRUE, $type = FALSE); + + /** + * Builds an array of query results. + * + * @param boolean return rows as objects + * @param mixed type + * @return array + */ + abstract function result_array($object = NULL, $type = FALSE); + + /** + * Gets the fields of an already run query. + * + * @return array + */ + abstract public function list_fields(); + + /** + * Seek to an offset in the results. + * + * @return boolean + */ + abstract public function seek($offset); + + /** + * Countable: count + */ + public function count() + { + return $this->total_rows; + } + + /** + * ArrayAccess: offsetExists + */ + public function offsetExists($offset) + { + if ($this->total_rows > 0) + { + $min = 0; + $max = $this->total_rows - 1; + + return ! ($offset < $min OR $offset > $max); + } + + return FALSE; + } + + /** + * ArrayAccess: offsetGet + */ + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return FALSE; + + // Return the row by calling the defined fetching callback + return call_user_func($this->fetch_type, $this->result, $this->return_type); + } + + /** + * ArrayAccess: offsetSet + * + * @throws Kohana_Database_Exception + */ + final public function offsetSet($offset, $value) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + + /** + * ArrayAccess: offsetUnset + * + * @throws Kohana_Database_Exception + */ + final public function offsetUnset($offset) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + + /** + * Iterator: current + */ + public function current() + { + return $this->offsetGet($this->current_row); + } + + /** + * Iterator: key + */ + public function key() + { + return $this->current_row; + } + + /** + * Iterator: next + */ + public function next() + { + return ++$this->current_row; + } + + /** + * Iterator: prev + */ + public function prev() + { + return --$this->current_row; + } + + /** + * Iterator: rewind + */ + public function rewind() + { + return $this->current_row = 0; + } + + /** + * Iterator: valid + */ + public function valid() + { + return $this->offsetExists($this->current_row); + } + +} // End Database Result Interface
\ No newline at end of file diff --git a/kohana/libraries/drivers/Database/Mssql.php b/kohana/libraries/drivers/Database/Mssql.php new file mode 100644 index 00000000..5482423b --- /dev/null +++ b/kohana/libraries/drivers/Database/Mssql.php @@ -0,0 +1,453 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * MSSQL Database Driver + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Mssql_Driver extends Database_Driver +{ + /** + * Database connection link + */ + protected $link; + + /** + * Database configuration + */ + protected $db_config; + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'MSSQL Database Driver Initialized'); + } + + /** + * Closes the database connection. + */ + public function __destruct() + { + is_resource($this->link) and mssql_close($this->link); + } + + /** + * Make the connection + * + * @return return connection + */ + public function connect() + { + // Check if link already exists + if (is_resource($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Persistent connections enabled? + $connect = ($this->db_config['persistent'] == TRUE) ? 'mssql_pconnect' : 'mssql_connect'; + + // Build the connection info + $host = isset($host) ? $host : $socket; + $port = (isset($port) AND is_string($port)) ? ':'.$port : ''; + + // Make the connection and select the database + if (($this->link = $connect($host.$port, $user, $pass, TRUE)) AND mssql_select_db($database, $this->link)) + { + /* This is being removed so I can use it, will need to come up with a more elegant workaround in the future... + * + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + */ + + // Clear password after successful connect + $this->config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|REPLACE|SET)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset(self::$query_cache[$hash])) + { + // Set the cached object + self::$query_cache[$hash] = new Mssql_Result(mssql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + + // Return the cached query + return self::$query_cache[$hash]; + } + + return new Mssql_Result(mssql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + + public function escape_table($table) + { + if (stripos($table, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $table = str_ireplace(' AS ', ' AS ', $table); + + // Runs escape_table on both sides of an AS statement + $table = array_map(array($this, __FUNCTION__), explode(' AS ', $table)); + + // Re-create the AS statement + return implode(' AS ', $table); + } + return '['.str_replace('.', '[.]', $table).']'; + } + + public function escape_column($column) + { + if (!$this->db_config['escape']) + return $column; + + if (strtolower($column) == 'count(*)' OR $column == '*') + return $column; + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:rand|all|distinct(?:row)?|high_priority|sql_(?:small_result|b(?:ig_result|uffer_result)|no_cache|ca(?:che|lc_found_rows)))\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '[$0]', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '[$0]', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + /** + * Limit in SQL Server 2000 only uses the keyword + * 'TOP'; 2007 may have an offset keyword, but + * I am unsure - for pagination style limit,offset + * functionality, a fancy query needs to be built. + * + * @param unknown_type $limit + * @return unknown + */ + public function limit($limit, $offset=null) + { + return 'TOP '.$limit; + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + // Escape the tables + $froms = array(); + foreach ($database['from'] as $from) + $froms[] = $this->escape_column($from); + $sql .= "\nFROM "; + $sql .= implode(', ', $froms); + } + + if (count($database['join']) > 0) + { + $sql .= ' '.$database['join']['type'].'JOIN ('.implode(', ', $database['join']['tables']).') ON '.implode(' AND ', $database['join']['conditions']); + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit']); + } + + return $sql; + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_resource($this->link) or $this->connect(); + //mssql_real_escape_string($str, $this->link); <-- this function doesn't exist + + $characters = array('/\x00/', '/\x1a/', '/\n/', '/\r/', '/\\\/', '/\'/'); + $replace = array('\\\x00', '\\x1a', '\\n', '\\r', '\\\\', "''"); + return preg_replace($characters, $replace, $str); + } + + public function list_tables() + { + $sql = 'SHOW TABLES FROM ['.$this->db_config['connection']['database'].']'; + $result = $this->query($sql)->result(FALSE, MSSQL_ASSOC); + + $retval = array(); + foreach ($result as $row) + { + $retval[] = current($row); + } + + return $retval; + } + + public function show_error() + { + return mssql_error($this->link); + } + + public function list_fields($table) + { + static $tables; + + if (empty($tables[$table])) + { + foreach ($this->field_data($table) as $row) + { + // Make an associative array + $tables[$table][$row->Field] = $this->sql_type($row->Type); + } + } + + return $tables[$table]; + } + + public function field_data($table) + { + $columns = array(); + + if ($query = MSSQL_query('SHOW COLUMNS FROM '.$this->escape_table($table), $this->link)) + { + if (MSSQL_num_rows($query) > 0) + { + while ($row = MSSQL_fetch_object($query)) + { + $columns[] = $row; + } + } + } + + return $columns; + } +} + +/** + * MSSQL Result + */ +class Mssql_Result extends Database_Result { + + // Fetch function and return type + protected $fetch_type = 'mssql_fetch_object'; + protected $return_type = MSSQL_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + $this->result = $result; + + // If the query is a resource, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_resource($result)) + { + $this->current_row = 0; + $this->total_rows = mssql_num_rows($this->result); + $this->fetch_type = ($object === TRUE) ? 'mssql_fetch_object' : 'mssql_fetch_array'; + } + elseif (is_bool($result)) + { + if ($result == FALSE) + { + // SQL error + throw new Kohana_Database_Exception('database.error', MSSQL_error($link).' - '.$sql); + } + else + { + // Its an DELETE, INSERT, REPLACE, or UPDATE querys + $last_id = mssql_query('SELECT @@IDENTITY AS last_id', $link); + $result = mssql_fetch_assoc($last_id); + $this->insert_id = $result['last_id']; + $this->total_rows = mssql_rows_affected($link); + } + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Destruct, the cleanup crew! + */ + public function __destruct() + { + if (is_resource($this->result)) + { + mssql_free_result($this->result); + } + } + + public function result($object = TRUE, $type = MSSQL_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'mssql_fetch_object' : 'mssql_fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'mssql_fetch_object') + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = MSSQL_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = MSSQL_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'mssql_fetch_object'; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'mssql_fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'mssql_fetch_object') + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + + if (mssql_num_rows($this->result)) + { + // Reset the pointer location to make sure things work properly + mssql_data_seek($this->result, 0); + + while ($row = $fetch($this->result, $type)) + { + $rows[] = $row; + } + } + + return isset($rows) ? $rows : array(); + } + + public function list_fields() + { + $field_names = array(); + while ($field = mssql_fetch_field($this->result)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + public function seek($offset) + { + if ( ! $this->offsetExists($offset)) + return FALSE; + + return mssql_data_seek($this->result, $offset); + } + +} // End mssql_Result Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Database/Mysql.php b/kohana/libraries/drivers/Database/Mysql.php new file mode 100644 index 00000000..57459b4e --- /dev/null +++ b/kohana/libraries/drivers/Database/Mysql.php @@ -0,0 +1,479 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * MySQL Database Driver + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Mysql_Driver extends Database_Driver { + + /** + * Database connection link + */ + protected $link; + + /** + * Database configuration + */ + protected $db_config; + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'MySQL Database Driver Initialized'); + } + + /** + * Closes the database connection. + */ + public function __destruct() + { + is_resource($this->link) and mysql_close($this->link); + } + + public function connect() + { + // Check if link already exists + if (is_resource($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Persistent connections enabled? + $connect = ($this->db_config['persistent'] == TRUE) ? 'mysql_pconnect' : 'mysql_connect'; + + // Build the connection info + $host = isset($host) ? $host : $socket; + $port = isset($port) ? ':'.$port : ''; + + // Make the connection and select the database + if (($this->link = $connect($host.$port, $user, $pass, TRUE)) AND mysql_select_db($database, $this->link)) + { + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + + // Clear password after successful connect + $this->config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|REPLACE|SET)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset(self::$query_cache[$hash])) + { + // Set the cached object + self::$query_cache[$hash] = new Mysql_Result(mysql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + + // Return the cached query + return self::$query_cache[$hash]; + } + + return new Mysql_Result(mysql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + $this->query('SET NAMES '.$this->escape_str($charset)); + } + + public function escape_table($table) + { + if (!$this->db_config['escape']) + return $table; + + if (stripos($table, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $table = str_ireplace(' AS ', ' AS ', $table); + + // Runs escape_table on both sides of an AS statement + $table = array_map(array($this, __FUNCTION__), explode(' AS ', $table)); + + // Re-create the AS statement + return implode(' AS ', $table); + } + return '`'.str_replace('.', '`.`', $table).'`'; + } + + public function escape_column($column) + { + if (!$this->db_config['escape']) + return $column; + + if (strtolower($column) == 'count(*)' OR $column == '*') + return $column; + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:rand|all|distinct(?:row)?|high_priority|sql_(?:small_result|b(?:ig_result|uffer_result)|no_cache|ca(?:che|lc_found_rows)))\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '`$0`', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '`$0`', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + public function regex($field, $match = '', $type = 'AND ', $num_regexs) + { + $prefix = ($num_regexs == 0) ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' REGEXP \''.$this->escape_str($match).'\''; + } + + public function notregex($field, $match = '', $type = 'AND ', $num_regexs) + { + $prefix = $num_regexs == 0 ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' NOT REGEXP \''.$this->escape_str($match) . '\''; + } + + public function merge($table, $keys, $values) + { + // Escape the column names + foreach ($keys as $key => $value) + { + $keys[$key] = $this->escape_column($value); + } + return 'REPLACE INTO '.$this->escape_table($table).' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + } + + public function limit($limit, $offset = 0) + { + return 'LIMIT '.$offset.', '.$limit; + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + // Escape the tables + $froms = array(); + foreach ($database['from'] as $from) + $froms[] = $this->escape_column($from); + $sql .= "\nFROM "; + $sql .= implode(', ', $froms); + } + + if (count($database['join']) > 0) + { + $sql .= ' '.$database['join']['type'].'JOIN ('.implode(', ', $database['join']['tables']).') ON '.implode(' AND ', $database['join']['conditions']); + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit'], $database['offset']); + } + + return $sql; + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_resource($this->link) or $this->connect(); + + return mysql_real_escape_string($str, $this->link); + } + + public function list_tables() + { + $sql = 'SHOW TABLES FROM `'.$this->db_config['connection']['database'].'`'; + $result = $this->query($sql)->result(FALSE, MYSQL_ASSOC); + + $retval = array(); + foreach ($result as $row) + { + $retval[] = current($row); + } + + return $retval; + } + + public function show_error() + { + return mysql_error($this->link); + } + + public function list_fields($table) + { + static $tables; + + if (empty($tables[$table])) + { + foreach ($this->field_data($table) as $row) + { + // Make an associative array + $tables[$table][$row->Field] = $this->sql_type($row->Type); + + if ($row->Key === 'PRI' AND $row->Extra === 'auto_increment') + { + // For sequenced (AUTO_INCREMENT) tables + $tables[$table][$row->Field]['sequenced'] = TRUE; + } + + if ($row->Null === 'YES') + { + // Set NULL status + $tables[$table][$row->Field]['null'] = TRUE; + } + } + } + + if (!isset($tables[$table])) + throw new Kohana_Database_Exception('database.table_not_found', $table); + + return $tables[$table]; + } + + public function field_data($table) + { + $columns = array(); + + if ($query = mysql_query('SHOW COLUMNS FROM '.$this->escape_table($table), $this->link)) + { + if (mysql_num_rows($query) > 0) + { + while ($row = mysql_fetch_object($query)) + { + $columns[] = $row; + } + } + } + + return $columns; + } + +} // End Database_Mysql_Driver Class + +/** + * MySQL Result + */ +class Mysql_Result extends Database_Result { + + // Fetch function and return type + protected $fetch_type = 'mysql_fetch_object'; + protected $return_type = MYSQL_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + $this->result = $result; + + // If the query is a resource, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_resource($result)) + { + $this->current_row = 0; + $this->total_rows = mysql_num_rows($this->result); + $this->fetch_type = ($object === TRUE) ? 'mysql_fetch_object' : 'mysql_fetch_array'; + } + elseif (is_bool($result)) + { + if ($result == FALSE) + { + // SQL error + throw new Kohana_Database_Exception('database.error', mysql_error($link).' - '.$sql); + } + else + { + // Its an DELETE, INSERT, REPLACE, or UPDATE query + $this->insert_id = mysql_insert_id($link); + $this->total_rows = mysql_affected_rows($link); + } + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Destruct, the cleanup crew! + */ + public function __destruct() + { + if (is_resource($this->result)) + { + mysql_free_result($this->result); + } + } + + public function result($object = TRUE, $type = MYSQL_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'mysql_fetch_object' : 'mysql_fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'mysql_fetch_object' AND $object === TRUE) + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = MYSQL_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = MYSQL_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'mysql_fetch_object'; + + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'mysql_fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'mysql_fetch_object') + { + $type = (is_string($this->return_type) AND Kohana::auto_load($this->return_type)) ? $this->return_type : 'stdClass'; + } + } + + if (mysql_num_rows($this->result)) + { + // Reset the pointer location to make sure things work properly + mysql_data_seek($this->result, 0); + + while ($row = $fetch($this->result, $type)) + { + $rows[] = $row; + } + } + + return isset($rows) ? $rows : array(); + } + + public function list_fields() + { + $field_names = array(); + while ($field = mysql_fetch_field($this->result)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + public function seek($offset) + { + if ( ! $this->offsetExists($offset)) + return FALSE; + + return mysql_data_seek($this->result, $offset); + } + +} // End Mysql_Result Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Database/Mysqli.php b/kohana/libraries/drivers/Database/Mysqli.php new file mode 100644 index 00000000..3b033a85 --- /dev/null +++ b/kohana/libraries/drivers/Database/Mysqli.php @@ -0,0 +1,369 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * MySQLi Database Driver + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Mysqli_Driver extends Database_Mysql_Driver { + + // Database connection link + protected $link; + protected $db_config; + protected $statements = array(); + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'MySQLi Database Driver Initialized'); + } + + /** + * Closes the database connection. + */ + public function __destruct() + { + is_object($this->link) and $this->link->close(); + } + + public function connect() + { + // Check if link already exists + if (is_object($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Build the connection info + $host = isset($host) ? $host : $socket; + + // Make the connection and select the database + if ($this->link = new mysqli($host, $user, $pass, $database)) + { + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + + // Clear password after successful connect + $this->config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|REPLACE|SET)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset(self::$query_cache[$hash])) + { + // Set the cached object + self::$query_cache[$hash] = new Kohana_Mysqli_Result($this->link, $this->db_config['object'], $sql); + } + + // Return the cached query + return self::$query_cache[$hash]; + } + + return new Kohana_Mysqli_Result($this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + if ($this->link->set_charset($charset) === FALSE) + throw new Kohana_Database_Exception('database.error', $this->show_error()); + } + + public function stmt_prepare($sql = '') + { + is_object($this->link) or $this->connect(); + return new Kohana_Mysqli_Statement($sql, $this->link); + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_object($this->link) or $this->connect(); + + return $this->link->real_escape_string($str); + } + + public function show_error() + { + return $this->link->error; + } + + public function field_data($table) + { + $query = $this->link->query('SHOW COLUMNS FROM '.$this->escape_table($table)); + + $table = array(); + while ($row = $query->fetch_object()) + { + $table[] = $row; + } + + return $table; + } + +} // End Database_Mysqli_Driver Class + +/** + * MySQLi Result + */ +class Kohana_Mysqli_Result extends Database_Result { + + // Database connection + protected $link; + + // Data fetching types + protected $fetch_type = 'mysqli_fetch_object'; + protected $return_type = MYSQLI_ASSOC; + + /** + * Sets up the result variables. + * + * @param object database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($link, $object = TRUE, $sql) + { + $this->link = $link; + + if ( ! $this->link->multi_query($sql)) + { + // SQL error + throw new Kohana_Database_Exception('database.error', $this->link->error.' - '.$sql); + } + else + { + $this->result = $this->link->store_result(); + + // If the query is an object, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_object($this->result)) + { + $this->current_row = 0; + $this->total_rows = $this->result->num_rows; + $this->fetch_type = ($object === TRUE) ? 'fetch_object' : 'fetch_array'; + } + elseif ($this->link->error) + { + // SQL error + throw new Kohana_Database_Exception('database.error', $this->link->error.' - '.$sql); + } + else + { + // Its an DELETE, INSERT, REPLACE, or UPDATE query + $this->insert_id = $this->link->insert_id; + $this->total_rows = $this->link->affected_rows; + } + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Magic __destruct function, frees the result. + */ + public function __destruct() + { + if (is_object($this->result)) + { + $this->result->free_result(); + + // this is kinda useless, but needs to be done to avoid the "Commands out of sync; you + // can't run this command now" error. Basically, we get all results after the first one + // (the one we actually need) and free them. + if (is_resource($this->link) AND $this->link->more_results()) + { + do + { + if ($result = $this->link->store_result()) + { + $result->free_result(); + } + } while ($this->link->next_result()); + } + } + } + + public function result($object = TRUE, $type = MYSQLI_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'fetch_object' : 'fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'fetch_object') + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = MYSQLI_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = MYSQLI_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'fetch_object'; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'fetch_object') + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + + if ($this->result->num_rows) + { + // Reset the pointer location to make sure things work properly + $this->result->data_seek(0); + + while ($row = $this->result->$fetch($type)) + { + $rows[] = $row; + } + } + + return isset($rows) ? $rows : array(); + } + + public function list_fields() + { + $field_names = array(); + while ($field = $this->result->fetch_field()) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + public function seek($offset) + { + if ( ! $this->offsetExists($offset)) + return FALSE; + + $this->result->data_seek($offset); + + return TRUE; + } + + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return FALSE; + + // Return the row + $fetch = $this->fetch_type; + return $this->result->$fetch($this->return_type); + } + +} // End Mysqli_Result Class + +/** + * MySQLi Prepared Statement (experimental) + */ +class Kohana_Mysqli_Statement { + + protected $link = NULL; + protected $stmt; + protected $var_names = array(); + protected $var_values = array(); + + public function __construct($sql, $link) + { + $this->link = $link; + + $this->stmt = $this->link->prepare($sql); + + return $this; + } + + public function __destruct() + { + $this->stmt->close(); + } + + // Sets the bind parameters + public function bind_params($param_types, $params) + { + $this->var_names = array_keys($params); + $this->var_values = array_values($params); + call_user_func_array(array($this->stmt, 'bind_param'), array_merge($param_types, $var_names)); + + return $this; + } + + public function bind_result($params) + { + call_user_func_array(array($this->stmt, 'bind_result'), $params); + } + + // Runs the statement + public function execute() + { + foreach ($this->var_names as $key => $name) + { + $$name = $this->var_values[$key]; + } + $this->stmt->execute(); + return $this->stmt; + } +}
\ No newline at end of file diff --git a/kohana/libraries/drivers/Database/Pdosqlite.php b/kohana/libraries/drivers/Database/Pdosqlite.php new file mode 100644 index 00000000..1d83bba9 --- /dev/null +++ b/kohana/libraries/drivers/Database/Pdosqlite.php @@ -0,0 +1,470 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/* + * Class: Database_PdoSqlite_Driver + * Provides specific database items for Sqlite. + * + * Connection string should be, eg: "pdosqlite://path/to/database.db" + * + * Version 1.0 alpha + * author - Doutu, updated by gregmac + * copyright - (c) BSD + * license - <no> + */ + +class Database_Pdosqlite_Driver extends Database_Driver { + + // Database connection link + protected $link; + protected $db_config; + + /* + * Constructor: __construct + * Sets up the config for the class. + * + * Parameters: + * config - database configuration + * + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'PDO:Sqlite Database Driver Initialized'); + } + + public function connect() + { + // Import the connect variables + extract($this->db_config['connection']); + + try + { + $this->link = new PDO('sqlite:'.$socket.$database, $user, $pass, + array(PDO::ATTR_PERSISTENT => $this->db_config['persistent'])); + + $this->link->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL); + $this->link->query('PRAGMA count_changes=1;'); + + if ($charset = $this->db_config['character_set']) + { + $this->set_charset($charset); + } + } + catch (PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + + // Clear password after successful connect + $this->db_config['connection']['pass'] = NULL; + + return $this->link; + } + + public function query($sql) + { + try + { + $sth = $this->link->prepare($sql); + } + catch (PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + return new Pdosqlite_Result($sth, $this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + $this->link->query('PRAGMA encoding = '.$this->escape_str($charset)); + } + + public function escape_table($table) + { + if ( ! $this->db_config['escape']) + return $table; + + return '`'.str_replace('.', '`.`', $table).'`'; + } + + public function escape_column($column) + { + if ( ! $this->db_config['escape']) + return $column; + + if (strtolower($column) == 'count(*)' OR $column == '*') + return $column; + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:rand|all|distinct(?:row)?|high_priority|sql_(?:small_result|b(?:ig_result|uffer_result)|no_cache|ca(?:che|lc_found_rows)))\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '`$0`', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '`$0`', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + public function limit($limit, $offset = 0) + { + return 'LIMIT '.$offset.', '.$limit; + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + $sql .= "\nFROM "; + $sql .= implode(', ', $database['from']); + } + + if (count($database['join']) > 0) + { + $sql .= ' '.$database['join']['type'].'JOIN ('.implode(', ', $database['join']['tables']).') ON '.implode(' AND ', $database['join']['conditions']); + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit'], $database['offset']); + } + + return $sql; + } + + public function escape_str($str) + { + if ( ! $this->db_config['escape']) + return $str; + + if (function_exists('sqlite_escape_string')) + { + $res = sqlite_escape_string($str); + } + else + { + $res = str_replace("'", "''", $str); + } + return $res; + } + + public function list_tables() + { + $sql = "SELECT `name` FROM `sqlite_master` WHERE `type`='table' ORDER BY `name`;"; + try + { + $result = $this->query($sql)->result(FALSE, PDO::FETCH_ASSOC); + $retval = array(); + foreach ($result as $row) + { + $retval[] = current($row); + } + } + catch (PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + return $retval; + } + + public function show_error() + { + $err = $this->link->errorInfo(); + return isset($err[2]) ? $err[2] : 'Unknown error!'; + } + + public function list_fields($table, $query = FALSE) + { + static $tables; + if (is_object($query)) + { + if (empty($tables[$table])) + { + $tables[$table] = array(); + + foreach ($query->result() as $row) + { + $tables[$table][] = $row->name; + } + } + + return $tables[$table]; + } + else + { + $result = $this->link->query( 'PRAGMA table_info('.$this->escape_table($table).')' ); + + foreach ($result as $row) + { + $tables[$table][$row['name']] = $this->sql_type($row['type']); + } + + return $tables[$table]; + } + } + + public function field_data($table) + { + Kohana::log('error', 'This method is under developing'); + } + /** + * Version number query string + * + * @access public + * @return string + */ + function version() + { + return $this->link->getAttribute(constant("PDO::ATTR_SERVER_VERSION")); + } + +} // End Database_PdoSqlite_Driver Class + +/* + * PDO-sqlite Result + */ +class Pdosqlite_Result extends Database_Result { + + // Data fetching types + protected $fetch_type = PDO::FETCH_OBJ; + protected $return_type = PDO::FETCH_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + if (is_object($result) OR $result = $link->prepare($sql)) + { + // run the query + try + { + $result->execute(); + } + catch (PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + + if (preg_match('/^SELECT|PRAGMA|EXPLAIN/i', $sql)) + { + $this->result = $result; + $this->current_row = 0; + + $this->total_rows = $this->sqlite_row_count(); + + $this->fetch_type = ($object === TRUE) ? PDO::FETCH_OBJ : PDO::FETCH_ASSOC; + } + elseif (preg_match('/^DELETE|INSERT|UPDATE/i', $sql)) + { + $this->insert_id = $link->lastInsertId(); + } + } + else + { + // SQL error + throw new Kohana_Database_Exception('database.error', $link->errorInfo().' - '.$sql); + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + private function sqlite_row_count() + { + $count = 0; + while ($this->result->fetch()) + { + $count++; + } + + // The query must be re-fetched now. + $this->result->execute(); + + return $count; + } + + /* + * Destructor: __destruct + * Magic __destruct function, frees the result. + */ + public function __destruct() + { + if (is_object($this->result)) + { + $this->result->closeCursor(); + $this->result = NULL; + } + } + + public function result($object = TRUE, $type = PDO::FETCH_BOTH) + { + $this->fetch_type = (bool) $object ? PDO::FETCH_OBJ : PDO::FETCH_BOTH; + + if ($this->fetch_type == PDO::FETCH_OBJ) + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = PDO::FETCH_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = PDO::FETCH_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = PDO::FETCH_OBJ; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = PDO::FETCH_OBJ; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == PDO::FETCH_OBJ) + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + try + { + while ($row = $this->result->fetch($fetch)) + { + $rows[] = $row; + } + } + catch(PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + return FALSE; + } + return $rows; + } + + public function list_fields() + { + $field_names = array(); + for ($i = 0, $max = $this->result->columnCount(); $i < $max; $i++) + { + $info = $this->result->getColumnMeta($i); + $field_names[] = $info['name']; + } + return $field_names; + } + + public function seek($offset) + { + // To request a scrollable cursor for your PDOStatement object, you must + // set the PDO::ATTR_CURSOR attribute to PDO::CURSOR_SCROLL when you + // prepare the statement. + Kohana::log('error', get_class($this).' does not support scrollable cursors, '.__FUNCTION__.' call ignored'); + + return FALSE; + } + + public function offsetGet($offset) + { + try + { + return $this->result->fetch($this->fetch_type, PDO::FETCH_ORI_ABS, $offset); + } + catch(PDOException $e) + { + throw new Kohana_Database_Exception('database.error', $e->getMessage()); + } + } + + public function rewind() + { + // Same problem that seek() has, see above. + return $this->seek(0); + } + +} // End PdoSqlite_Result Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Database/Pgsql.php b/kohana/libraries/drivers/Database/Pgsql.php new file mode 100644 index 00000000..0025d6f0 --- /dev/null +++ b/kohana/libraries/drivers/Database/Pgsql.php @@ -0,0 +1,539 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * PostgreSQL 8.1+ Database Driver + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Pgsql_Driver extends Database_Driver { + + // Database connection link + protected $link; + protected $db_config; + + /** + * Sets the config for the class. + * + * @param array database configuration + */ + public function __construct($config) + { + $this->db_config = $config; + + Kohana::log('debug', 'PgSQL Database Driver Initialized'); + } + + public function connect() + { + // Check if link already exists + if (is_resource($this->link)) + return $this->link; + + // Import the connect variables + extract($this->db_config['connection']); + + // Persistent connections enabled? + $connect = ($this->db_config['persistent'] == TRUE) ? 'pg_pconnect' : 'pg_connect'; + + // Build the connection info + $port = isset($port) ? 'port=\''.$port.'\'' : ''; + $host = isset($host) ? 'host=\''.$host.'\' '.$port : ''; // if no host, connect with the socket + + $connection_string = $host.' dbname=\''.$database.'\' user=\''.$user.'\' password=\''.$pass.'\''; + // Make the connection and select the database + if ($this->link = $connect($connection_string)) + { + if ($charset = $this->db_config['character_set']) + { + echo $this->set_charset($charset); + } + + // Clear password after successful connect + $this->config['connection']['pass'] = NULL; + + return $this->link; + } + + return FALSE; + } + + public function query($sql) + { + // Only cache if it's turned on, and only cache if it's not a write statement + if ($this->db_config['cache'] AND ! preg_match('#\b(?:INSERT|UPDATE|SET)\b#i', $sql)) + { + $hash = $this->query_hash($sql); + + if ( ! isset(self::$query_cache[$hash])) + { + // Set the cached object + self::$query_cache[$hash] = new Pgsql_Result(pg_query($this->link, $sql), $this->link, $this->db_config['object'], $sql); + } + + return self::$query_cache[$hash]; + } + + return new Pgsql_Result(pg_query($this->link, $sql), $this->link, $this->db_config['object'], $sql); + } + + public function set_charset($charset) + { + $this->query('SET client_encoding TO '.pg_escape_string($this->link, $charset)); + } + + public function escape_table($table) + { + if (!$this->db_config['escape']) + return $table; + + return '"'.str_replace('.', '"."', $table).'"'; + } + + public function escape_column($column) + { + if (!$this->db_config['escape']) + return $column; + + if (strtolower($column) == 'count(*)' OR $column == '*') + return $column; + + // This matches any modifiers we support to SELECT. + if ( ! preg_match('/\b(?:all|distinct)\s/i', $column)) + { + if (stripos($column, ' AS ') !== FALSE) + { + // Force 'AS' to uppercase + $column = str_ireplace(' AS ', ' AS ', $column); + + // Runs escape_column on both sides of an AS statement + $column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); + + // Re-create the AS statement + return implode(' AS ', $column); + } + + return preg_replace('/[^.*]+/', '"$0"', $column); + } + + $parts = explode(' ', $column); + $column = ''; + + for ($i = 0, $c = count($parts); $i < $c; $i++) + { + // The column is always last + if ($i == ($c - 1)) + { + $column .= preg_replace('/[^.*]+/', '"$0"', $parts[$i]); + } + else // otherwise, it's a modifier + { + $column .= $parts[$i].' '; + } + } + return $column; + } + + public function regex($field, $match = '', $type = 'AND ', $num_regexs) + { + $prefix = ($num_regexs == 0) ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' REGEXP \''.$this->escape_str($match).'\''; + } + + public function notregex($field, $match = '', $type = 'AND ', $num_regexs) + { + $prefix = $num_regexs == 0 ? '' : $type; + + return $prefix.' '.$this->escape_column($field).' NOT REGEXP \''.$this->escape_str($match) . '\''; + } + + public function limit($limit, $offset = 0) + { + return 'LIMIT '.$limit.' OFFSET '.$offset; + } + + public function stmt_prepare($sql = '') + { + is_object($this->link) or $this->connect(); + return new Kohana_Mysqli_Statement($sql, $this->link); + } + + public function compile_select($database) + { + $sql = ($database['distinct'] == TRUE) ? 'SELECT DISTINCT ' : 'SELECT '; + $sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; + + if (count($database['from']) > 0) + { + $sql .= "\nFROM "; + $sql .= implode(', ', $database['from']); + } + + if (count($database['join']) > 0) + { + $sql .= ' '.$database['join']['type'].'JOIN ('.implode(', ', $database['join']['tables']).') ON '.implode(' AND ', $database['join']['conditions']); + } + + if (count($database['where']) > 0) + { + $sql .= "\nWHERE "; + } + + $sql .= implode("\n", $database['where']); + + if (count($database['groupby']) > 0) + { + $sql .= "\nGROUP BY "; + $sql .= implode(', ', $database['groupby']); + } + + if (count($database['having']) > 0) + { + $sql .= "\nHAVING "; + $sql .= implode("\n", $database['having']); + } + + if (count($database['orderby']) > 0) + { + $sql .= "\nORDER BY "; + $sql .= implode(', ', $database['orderby']); + } + + if (is_numeric($database['limit'])) + { + $sql .= "\n"; + $sql .= $this->limit($database['limit'], $database['offset']); + } + + return $sql; + } + + public function escape_str($str) + { + if (!$this->db_config['escape']) + return $str; + + is_resource($this->link) or $this->connect(); + + return pg_escape_string($this->link, $str); + } + + public function list_tables() + { + $sql = 'SELECT table_schema || \'.\' || table_name FROM information_schema.tables WHERE table_schema NOT IN (\'pg_catalog\', \'information_schema\')'; + $result = $this->query($sql)->result(FALSE, PGSQL_ASSOC); + + $retval = array(); + foreach ($result as $row) + { + $retval[] = current($row); + } + + return $retval; + } + + public function show_error() + { + return pg_last_error($this->link); + } + + public function list_fields($table, $query = FALSE) + { + static $tables; + + if (is_object($query)) + { + if (empty($tables[$table])) + { + $tables[$table] = array(); + + foreach ($query as $row) + { + $tables[$table][] = $row->Field; + } + } + + return $tables[$table]; + } + + // WOW...REALLY?!? + // Taken from http://www.postgresql.org/docs/7.4/interactive/catalogs.html + $query = $this->query('SELECT + -- Field + pg_attribute.attname AS "Field", + -- Type + CASE pg_type.typname + WHEN \'int2\' THEN \'smallint\' + WHEN \'int4\' THEN \'int\' + WHEN \'int8\' THEN \'bigint\' + WHEN \'varchar\' THEN \'varchar(\' || pg_attribute.atttypmod-4 || \')\' + ELSE pg_type.typname + END AS "Type", + -- Null + CASE WHEN pg_attribute.attnotnull THEN \'NO\' + ELSE \'YES\' + END AS "Null", + -- Default + CASE pg_type.typname + WHEN \'varchar\' THEN substring(pg_attrdef.adsrc from \'^(.*).*$\') + ELSE pg_attrdef.adsrc + END AS "Default" +FROM pg_class + INNER JOIN pg_attribute + ON (pg_class.oid=pg_attribute.attrelid) + INNER JOIN pg_type + ON (pg_attribute.atttypid=pg_type.oid) + LEFT JOIN pg_attrdef + ON (pg_class.oid=pg_attrdef.adrelid AND pg_attribute.attnum=pg_attrdef.adnum) +WHERE pg_class.relname=\''.$this->escape_str($table).'\' AND pg_attribute.attnum>=1 AND NOT pg_attribute.attisdropped +ORDER BY pg_attribute.attnum'); + $fields = array(); + foreach ($query as $row) + { + $fields[$row->Field]=$row->Type; + } + + return $fields; + + } + + public function field_data($table) + { + // TODO: This whole function needs to be debugged. + $query = pg_query('SELECT * FROM '.$this->escape_table($table).' LIMIT 1', $this->link); + $fields = pg_num_fields($query); + $table = array(); + + for ($i=0; $i < $fields; $i++) + { + $table[$i]['type'] = pg_field_type($query, $i); + $table[$i]['name'] = pg_field_name($query, $i); + $table[$i]['len'] = pg_field_prtlen($query, $i); + } + + return $table; + } + +} // End Database_Pgsql_Driver Class + +/** + * PostgreSQL Result + */ +class Pgsql_Result extends Database_Result { + + // Data fetching types + protected $fetch_type = 'pgsql_fetch_object'; + protected $return_type = PGSQL_ASSOC; + + /** + * Sets up the result variables. + * + * @param resource query result + * @param resource database link + * @param boolean return objects or arrays + * @param string SQL query that was run + */ + public function __construct($result, $link, $object = TRUE, $sql) + { + $this->result = $result; + + // If the query is a resource, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query + if (is_resource($result)) + { + // Its an DELETE, INSERT, REPLACE, or UPDATE query + if (preg_match('/^(?:delete|insert|replace|update)\s+/i', trim($sql), $matches)) + { + $this->insert_id = (strtolower($matches[0]) == 'insert') ? $this->insert_id() : FALSE; + $this->total_rows = pg_affected_rows($this->result); + } + else + { + $this->current_row = 0; + $this->total_rows = pg_num_rows($this->result); + $this->fetch_type = ($object === TRUE) ? 'pg_fetch_object' : 'pg_fetch_array'; + } + } + else + { + throw new Kohana_Database_Exception('database.error', pg_last_error().' - '.$sql); + } + + // Set result type + $this->result($object); + + // Store the SQL + $this->sql = $sql; + } + + /** + * Magic __destruct function, frees the result. + */ + public function __destruct() + { + if (is_resource($this->result)) + { + pg_free_result($this->result); + } + } + + public function result($object = TRUE, $type = PGSQL_ASSOC) + { + $this->fetch_type = ((bool) $object) ? 'pg_fetch_object' : 'pg_fetch_array'; + + // This check has to be outside the previous statement, because we do not + // know the state of fetch_type when $object = NULL + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + if ($this->fetch_type == 'pg_fetch_object') + { + $this->return_type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $this->return_type = $type; + } + + return $this; + } + + public function as_array($object = NULL, $type = PGSQL_ASSOC) + { + return $this->result_array($object, $type); + } + + public function result_array($object = NULL, $type = PGSQL_ASSOC) + { + $rows = array(); + + if (is_string($object)) + { + $fetch = $object; + } + elseif (is_bool($object)) + { + if ($object === TRUE) + { + $fetch = 'pg_fetch_object'; + + // NOTE - The class set by $type must be defined before fetching the result, + // autoloading is disabled to save a lot of stupid overhead. + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + else + { + $fetch = 'pg_fetch_array'; + } + } + else + { + // Use the default config values + $fetch = $this->fetch_type; + + if ($fetch == 'pg_fetch_object') + { + $type = (is_string($type) AND Kohana::auto_load($type)) ? $type : 'stdClass'; + } + } + + while ($row = $fetch($this->result, NULL, $type)) + { + $rows[] = $row; + } + + return $rows; + } + + public function insert_id() + { + if ($this->insert_id === NULL) + { + $query = 'SELECT LASTVAL() AS insert_id'; + + $result = pg_query($link, $query); + $insert_id = pg_fetch_array($result, NULL, PGSQL_ASSOC); + + $this->insert_id = $insert_id['insert_id']; + } + + return $this->insert_id; + } + + public function seek($offset) + { + if ( ! $this->offsetExists($offset)) + return FALSE; + + return pg_result_seek($this->result, $offset); + } + + public function list_fields() + { + $field_names = array(); + while ($field = pg_field_name($this->result)) + { + $field_names[] = $field->name; + } + + return $field_names; + } + + /** + * ArrayAccess: offsetGet + */ + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return FALSE; + + // Return the row by calling the defined fetching callback + $fetch = $this->fetch_type; + return $fetch($this->result, NULL, $this->return_type); + } + +} // End Pgsql_Result Class + +/** + * PostgreSQL Prepared Statement (experimental) + */ +class Kohana_Pgsql_Statement { + + protected $link = NULL; + protected $stmt; + + public function __construct($sql, $link) + { + $this->link = $link; + + $this->stmt = $this->link->prepare($sql); + + return $this; + } + + public function __destruct() + { + $this->stmt->close(); + } + + // Sets the bind parameters + public function bind_params() + { + $argv = func_get_args(); + return $this; + } + + // sets the statement values to the bound parameters + public function set_vals() + { + return $this; + } + + // Runs the statement + public function execute() + { + return $this; + } +}
\ No newline at end of file diff --git a/kohana/libraries/drivers/Image.php b/kohana/libraries/drivers/Image.php new file mode 100644 index 00000000..73f80610 --- /dev/null +++ b/kohana/libraries/drivers/Image.php @@ -0,0 +1,149 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Image API driver. + * + * $Id$ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Image_Driver { + + // Reference to the current image + protected $image; + + // Reference to the temporary processing image + protected $tmp_image; + + // Processing errors + protected $errors = array(); + + /** + * Executes a set of actions, defined in pairs. + * + * @param array actions + * @return boolean + */ + public function execute($actions) + { + foreach ($actions as $func => $args) + { + if ( ! $this->$func($args)) + return FALSE; + } + + return TRUE; + } + + /** + * Sanitize and normalize a geometry array based on the temporary image + * width and height. Valid properties are: width, height, top, left. + * + * @param array geometry properties + * @return void + */ + protected function sanitize_geometry( & $geometry) + { + list($width, $height) = $this->properties(); + + // Turn off error reporting + $reporting = error_reporting(0); + + // Width and height cannot exceed current image size + $geometry['width'] = min($geometry['width'], $width); + $geometry['height'] = min($geometry['height'], $height); + + // Set standard coordinates if given, otherwise use pixel values + if ($geometry['top'] === 'center') + { + $geometry['top'] = floor(($height / 2) - ($geometry['height'] / 2)); + } + elseif ($geometry['top'] === 'top') + { + $geometry['top'] = 0; + } + elseif ($geometry['top'] === 'bottom') + { + $geometry['top'] = $height - $geometry['height']; + } + + // Set standard coordinates if given, otherwise use pixel values + if ($geometry['left'] === 'center') + { + $geometry['left'] = floor(($width / 2) - ($geometry['width'] / 2)); + } + elseif ($geometry['left'] === 'left') + { + $geometry['left'] = 0; + } + elseif ($geometry['left'] === 'right') + { + $geometry['left'] = $width - $geometry['height']; + } + + // Restore error reporting + error_reporting($reporting); + } + + /** + * Return the current width and height of the temporary image. This is mainly + * needed for sanitizing the geometry. + * + * @return array width, height + */ + abstract protected function properties(); + + /** + * Process an image with a set of actions. + * + * @param string image filename + * @param array actions to execute + * @param string destination directory path + * @param string destination filename + * @return boolean + */ + abstract public function process($image, $actions, $dir, $file); + + /** + * Flip an image. Valid directions are horizontal and vertical. + * + * @param integer direction to flip + * @return boolean + */ + abstract function flip($direction); + + /** + * Crop an image. Valid properties are: width, height, top, left. + * + * @param array new properties + * @return boolean + */ + abstract function crop($properties); + + /** + * Resize an image. Valid properties are: width, height, and master. + * + * @param array new properties + * @return boolean + */ + abstract public function resize($properties); + + /** + * Rotate an image. Valid amounts are -180 to 180. + * + * @param integer amount to rotate + * @return boolean + */ + abstract public function rotate($amount); + + /** + * Sharpen and image. Valid amounts are 1 to 100. + * + * @param integer amount to sharpen + * @return boolean + */ + abstract public function sharpen($amount); + +} // End Image Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Image/GD.php b/kohana/libraries/drivers/Image/GD.php new file mode 100644 index 00000000..e7010ff5 --- /dev/null +++ b/kohana/libraries/drivers/Image/GD.php @@ -0,0 +1,379 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * GD Image Driver. + * + * $Id$ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Image_GD_Driver extends Image_Driver { + + // A transparent PNG as a string + protected static $blank_png; + protected static $blank_png_width; + protected static $blank_png_height; + + public function __construct() + { + // Make sure that GD2 is available + if ( ! function_exists('gd_info')) + throw new Kohana_Exception('image.gd.requires_v2'); + + // Get the GD information + $info = gd_info(); + + // Make sure that the GD2 is installed + if (strpos($info['GD Version'], '2.') === FALSE) + throw new Kohana_Exception('image.gd.requires_v2'); + } + + public function process($image, $actions, $dir, $file, $render = FALSE) + { + // Set the "create" function + switch ($image['type']) + { + case IMAGETYPE_JPEG: + $create = 'imagecreatefromjpeg'; + break; + case IMAGETYPE_GIF: + $create = 'imagecreatefromgif'; + break; + case IMAGETYPE_PNG: + $create = 'imagecreatefrompng'; + break; + } + + // Set the "save" function + switch (strtolower(substr(strrchr($file, '.'), 1))) + { + case 'jpg': + case 'jpeg': + $save = 'imagejpeg'; + break; + case 'gif': + $save = 'imagegif'; + break; + case 'png': + $save = 'imagepng'; + break; + } + + // Make sure the image type is supported for import + if (empty($create) OR ! function_exists($create)) + throw new Kohana_Exception('image.type_not_allowed', $image['file']); + + // Make sure the image type is supported for saving + if (empty($save) OR ! function_exists($save)) + throw new Kohana_Exception('image.type_not_allowed', $dir.$file); + + // Load the image + $this->image = $image; + + // Create the GD image resource + $this->tmp_image = $create($image['file']); + + // Get the quality setting from the actions + $quality = arr::remove('quality', $actions); + + if ($status = $this->execute($actions)) + { + // Prevent the alpha from being lost + imagealphablending($this->tmp_image, TRUE); + imagesavealpha($this->tmp_image, TRUE); + + switch ($save) + { + case 'imagejpeg': + // Default the quality to 95 + ($quality === NULL) and $quality = 95; + break; + case 'imagegif': + // Remove the quality setting, GIF doesn't use it + unset($quality); + break; + case 'imagepng': + // Always use a compression level of 9 for PNGs. This does not + // affect quality, it only increases the level of compression! + $quality = 9; + break; + } + + if ($render === FALSE) + { + // Set the status to the save return value, saving with the quality requested + $status = isset($quality) ? $save($this->tmp_image, $dir.$file, $quality) : $save($this->tmp_image, $dir.$file); + } + else + { + // Output the image directly to the browser + switch ($save) + { + case 'imagejpeg': + header('Content-Type: image/jpeg'); + break; + case 'imagegif': + header('Content-Type: image/gif'); + break; + case 'imagepng': + header('Content-Type: image/png'); + break; + } + + $status = isset($quality) ? $save($this->tmp_image, NULL, $quality) : $save($this->tmp_image); + } + + // Destroy the temporary image + imagedestroy($this->tmp_image); + } + + return $status; + } + + public function flip($direction) + { + // Get the current width and height + $width = imagesx($this->tmp_image); + $height = imagesy($this->tmp_image); + + // Create the flipped image + $flipped = $this->imagecreatetransparent($width, $height); + + if ($direction === Image::HORIZONTAL) + { + for ($x = 0; $x < $width; $x++) + { + $status = imagecopy($flipped, $this->tmp_image, $x, 0, $width - $x - 1, 0, 1, $height); + } + } + elseif ($direction === Image::VERTICAL) + { + for ($y = 0; $y < $height; $y++) + { + $status = imagecopy($flipped, $this->tmp_image, 0, $y, 0, $height - $y - 1, $width, 1); + } + } + else + { + // Do nothing + return TRUE; + } + + if ($status === TRUE) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $flipped; + } + + return $status; + } + + public function crop($properties) + { + // Sanitize the cropping settings + $this->sanitize_geometry($properties); + + // Get the current width and height + $width = imagesx($this->tmp_image); + $height = imagesy($this->tmp_image); + + // Create the temporary image to copy to + $img = $this->imagecreatetransparent($properties['width'], $properties['height']); + + // Execute the crop + if ($status = imagecopyresampled($img, $this->tmp_image, 0, 0, $properties['left'], $properties['top'], $width, $height, $width, $height)) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + return $status; + } + + public function resize($properties) + { + // Get the current width and height + $width = imagesx($this->tmp_image); + $height = imagesy($this->tmp_image); + + if (substr($properties['width'], -1) === '%') + { + // Recalculate the percentage to a pixel size + $properties['width'] = round($width * (substr($properties['width'], 0, -1) / 100)); + } + + if (substr($properties['height'], -1) === '%') + { + // Recalculate the percentage to a pixel size + $properties['height'] = round($height * (substr($properties['height'], 0, -1) / 100)); + } + + 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; + } + + // 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 (empty($properties['height']) OR $properties['master'] === Image::WIDTH) + { + // Recalculate the height based on the width + $properties['height'] = round($height * $properties['width'] / $width); + } + + if (empty($properties['width']) OR $properties['master'] === Image::HEIGHT) + { + // Recalculate the width based on the height + $properties['width'] = round($width * $properties['height'] / $height); + } + + // Test if we can do a resize without resampling to speed up the final resize + if ($properties['width'] > $width / 2 AND $properties['height'] > $height / 2) + { + // Presize width and height + $pre_width = $width; + $pre_height = $height; + + // The maximum reduction is 10% greater than the final size + $max_reduction_width = round($properties['width'] * 1.1); + $max_reduction_height = round($properties['height'] * 1.1); + + // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction + while ($pre_width / 2 > $max_reduction_width AND $pre_height / 2 > $max_reduction_height) + { + $pre_width /= 2; + $pre_height /= 2; + } + + // Create the temporary image to copy to + $img = $this->imagecreatetransparent($pre_width, $pre_height); + + if ($status = imagecopyresized($img, $this->tmp_image, 0, 0, 0, 0, $pre_width, $pre_height, $width, $height)) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + // Set the width and height to the presize + $width = $pre_width; + $height = $pre_height; + } + + // Create the temporary image to copy to + $img = $this->imagecreatetransparent($properties['width'], $properties['height']); + + // Execute the resize + if ($status = imagecopyresampled($img, $this->tmp_image, 0, 0, 0, 0, $properties['width'], $properties['height'], $width, $height)) + { + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + return $status; + } + + public function rotate($amount) + { + // Use current image to rotate + $img = $this->tmp_image; + + // White, with an alpha of 0 + $transparent = imagecolorallocatealpha($img, 255, 255, 255, 127); + + // Rotate, setting the transparent color + $img = imagerotate($img, 360 - $amount, $transparent, -1); + + // Fill the background with the transparent "color" + imagecolortransparent($img, $transparent); + + // Merge the images + if ($status = imagecopymerge($this->tmp_image, $img, 0, 0, 0, 0, imagesx($this->tmp_image), imagesy($this->tmp_image), 100)) + { + // Prevent the alpha from being lost + imagealphablending($img, TRUE); + imagesavealpha($img, TRUE); + + // Swap the new image for the old one + imagedestroy($this->tmp_image); + $this->tmp_image = $img; + } + + return $status; + } + + public function sharpen($amount) + { + // Make sure that the sharpening function is available + if ( ! function_exists('imageconvolution')) + throw new Kohana_Exception('image.unsupported_method', __FUNCTION__); + + // Amount should be in the range of 18-10 + $amount = round(abs(-18 + ($amount * 0.08)), 2); + + // Gaussian blur matrix + $matrix = array + ( + array(-1, -1, -1), + array(-1, $amount, -1), + array(-1, -1, -1), + ); + + // Perform the sharpen + return imageconvolution($this->tmp_image, $matrix, $amount - 8, 0); + } + + protected function properties() + { + return array(imagesx($this->tmp_image), imagesy($this->tmp_image)); + } + + /** + * Returns an image with a transparent background. Used for rotating to + * prevent unfilled backgrounds. + * + * @param integer image width + * @param integer image height + * @return resource + */ + protected function imagecreatetransparent($width, $height) + { + if (self::$blank_png === NULL) + { + // Decode the blank PNG if it has not been done already + self::$blank_png = imagecreatefromstring(base64_decode + ( + 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29'. + 'mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAADqSURBVHjaYvz//z/DYAYAAcTEMMgBQAANegcCBN'. + 'CgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQ'. + 'AANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoH'. + 'AgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAAA16BwIE0KB'. + '3IEAADXoHAgTQoHcgQAANegcCBNCgdyBAgAEAMpcDTTQWJVEAAAAASUVORK5CYII=' + )); + + // Set the blank PNG width and height + self::$blank_png_width = imagesx(self::$blank_png); + self::$blank_png_height = imagesy(self::$blank_png); + } + + $img = imagecreatetruecolor($width, $height); + + // Resize the blank image + imagecopyresized($img, self::$blank_png, 0, 0, 0, 0, $width, $height, self::$blank_png_width, self::$blank_png_height); + + // Prevent the alpha from being lost + imagealphablending($img, FALSE); + imagesavealpha($img, TRUE); + + return $img; + } + +} // End Image GD Driver
\ No newline at end of file diff --git a/kohana/libraries/drivers/Image/ImageMagick.php b/kohana/libraries/drivers/Image/ImageMagick.php new file mode 100644 index 00000000..b56ca496 --- /dev/null +++ b/kohana/libraries/drivers/Image/ImageMagick.php @@ -0,0 +1,212 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * ImageMagick Image Driver. + * + * $Id$ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Image_ImageMagick_Driver extends Image_Driver { + + // Directory that IM is installed in + protected $dir = ''; + + // Command extension (exe for windows) + protected $ext = ''; + + // Temporary image filename + protected $tmp_image; + + /** + * Attempts to detect the ImageMagick installation directory. + * + * @throws Kohana_Exception + * @param array configuration + * @return void + */ + public function __construct($config) + { + if (empty($config['directory'])) + { + // Attempt to locate IM by using "which" (only works for *nix!) + if ( ! is_file($path = exec('which convert'))) + throw new Kohana_Exception('image.imagemagick.not_found'); + + $config['directory'] = dirname($path); + } + + // Set the command extension + $this->ext = (PHP_SHLIB_SUFFIX === 'dll') ? '.exe' : ''; + + // Check to make sure the provided path is correct + if ( ! is_file(realpath($config['directory']).'/convert'.$this->ext)) + throw new Kohana_Exception('image.imagemagick.not_found', 'convert'.$this->ext); + + // Set the installation directory + $this->dir = str_replace('\\', '/', realpath($config['directory'])).'/'; + } + + /** + * Creates a temporary image and executes the given actions. By creating a + * temporary copy of the image before manipulating it, this process is atomic. + */ + public function process($image, $actions, $dir, $file, $render = FALSE) + { + // We only need the filename + $image = $image['file']; + + // Unique temporary filename + $this->tmp_image = $dir.'k2img--'.sha1(time().$dir.$file).substr($file, strrpos($file, '.')); + + // Copy the image to the temporary file + copy($image, $this->tmp_image); + + // Quality change is done last + $quality = (int) arr::remove('quality', $actions); + + // Use 95 for the default quality + empty($quality) and $quality = 95; + + // All calls to these will need to be escaped, so do it now + $this->cmd_image = escapeshellarg($this->tmp_image); + $this->new_image = ($render)? $this->cmd_image : escapeshellarg($dir.$file); + + if ($status = $this->execute($actions)) + { + // Use convert to change the image into its final version. This is + // done to allow the file type to change correctly, and to handle + // the quality conversion in the most effective way possible. + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -quality '.$quality.'% '.$this->cmd_image.' '.$this->new_image)) + { + $this->errors[] = $error; + } + else + { + // Output the image directly to the browser + if ($render !== FALSE) + { + $contents = file_get_contents($this->tmp_image); + switch (substr($file, strrpos($file, '.') + 1)) + { + case 'jpg': + case 'jpeg': + header('Content-Type: image/jpeg'); + break; + case 'gif': + header('Content-Type: image/gif'); + break; + case 'png': + header('Content-Type: image/png'); + break; + } + echo $contents; + } + } + } + + // Remove the temporary image + unlink($this->tmp_image); + $this->tmp_image = ''; + + return $status; + } + + public function crop($prop) + { + // Sanitize and normalize the properties into geometry + $this->sanitize_geometry($prop); + + // Set the IM geometry based on the properties + $geometry = escapeshellarg($prop['width'].'x'.$prop['height'].'+'.$prop['left'].'+'.$prop['top']); + + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -crop '.$geometry.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function flip($dir) + { + // Convert the direction into a IM command + $dir = ($dir === Image::HORIZONTAL) ? '-flop' : '-flip'; + + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' '.$dir.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function resize($prop) + { + switch ($prop['master']) + { + case Image::WIDTH: // Wx + $dim = escapeshellarg($prop['width'].'x'); + break; + case Image::HEIGHT: // xH + $dim = escapeshellarg('x'.$prop['height']); + break; + case Image::AUTO: // WxH + $dim = escapeshellarg($prop['width'].'x'.$prop['height']); + break; + case Image::NONE: // WxH! + $dim = escapeshellarg($prop['width'].'x'.$prop['height'].'!'); + break; + } + + // Use "convert" to change the width and height + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -resize '.$dim.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function rotate($amt) + { + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -rotate '.escapeshellarg($amt).' -background transparent '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + public function sharpen($amount) + { + // Set the sigma, radius, and amount. The amount formula allows a nice + // spread between 1 and 100 without pixelizing the image badly. + $sigma = 0.5; + $radius = $sigma * 2; + $amount = round(($amount / 80) * 3.14, 2); + + // Convert the amount to an IM command + $sharpen = escapeshellarg($radius.'x'.$sigma.'+'.$amount.'+0'); + + if ($error = exec(escapeshellcmd($this->dir.'convert'.$this->ext).' -unsharp '.$sharpen.' '.$this->cmd_image.' '.$this->cmd_image)) + { + $this->errors[] = $error; + return FALSE; + } + + return TRUE; + } + + 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/kohana/libraries/drivers/Session.php b/kohana/libraries/drivers/Session.php new file mode 100644 index 00000000..b579698c --- /dev/null +++ b/kohana/libraries/drivers/Session.php @@ -0,0 +1,70 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Session driver interface + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +interface Session_Driver { + + /** + * Opens a session. + * + * @param string save path + * @param string session name + * @return boolean + */ + public function open($path, $name); + + /** + * Closes a session. + * + * @return boolean + */ + public function close(); + + /** + * Reads a session. + * + * @param string session id + * @return string + */ + public function read($id); + + /** + * Writes a session. + * + * @param string session id + * @param string session data + * @return boolean + */ + public function write($id, $data); + + /** + * Destroys a session. + * + * @param string session id + * @return boolean + */ + public function destroy($id); + + /** + * Regenerates the session id. + * + * @return string + */ + public function regenerate(); + + /** + * Garbage collection. + * + * @param integer session expiration period + * @return boolean + */ + public function gc($maxlifetime); + +} // End Session Driver Interface
\ No newline at end of file diff --git a/kohana/libraries/drivers/Session/Cache.php b/kohana/libraries/drivers/Session/Cache.php new file mode 100644 index 00000000..608010c4 --- /dev/null +++ b/kohana/libraries/drivers/Session/Cache.php @@ -0,0 +1,105 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * 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$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Cache_Driver implements Session_Driver { + + protected $cache; + protected $encrypt; + + public function __construct() + { + // Load Encrypt library + if (Kohana::config('session.encryption')) + { + $this->encrypt = new Encrypt; + } + + Kohana::log('debug', 'Session Cache Driver Initialized'); + } + + public function open($path, $name) + { + $config = Kohana::config('session.storage'); + + if (empty($config)) + { + // Load the default group + $config = Kohana::config('cache.default'); + } + elseif (is_string($config)) + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('cache.'.$config)) === NULL) + throw new Kohana_Exception('cache.undefined_group', $name); + } + + $config['lifetime'] = (Kohana::config('session.expiration') == 0) ? 86400 : Kohana::config('session.expiration'); + $this->cache = new Cache($config); + + return is_object($this->cache); + } + + public function close() + { + return TRUE; + } + + public function read($id) + { + $id = 'session_'.$id; + if ($data = $this->cache->get($id)) + { + return Kohana::config('session.encryption') ? $this->encrypt->decode($data) : $data; + } + + // Return value must be string, NOT a boolean + return ''; + } + + public function write($id, $data) + { + $id = 'session_'.$id; + $data = Kohana::config('session.encryption') ? $this->encrypt->encode($data) : $data; + + return $this->cache->set($id, $data); + } + + public function destroy($id) + { + $id = 'session_'.$id; + return $this->cache->delete($id); + } + + public function regenerate() + { + session_regenerate_id(TRUE); + + // Return new session id + return session_id(); + } + + public function gc($maxlifetime) + { + // Just return, caches are automatically cleaned up + return TRUE; + } + +} // End Session Cache Driver diff --git a/kohana/libraries/drivers/Session/Cookie.php b/kohana/libraries/drivers/Session/Cookie.php new file mode 100644 index 00000000..9077d4d7 --- /dev/null +++ b/kohana/libraries/drivers/Session/Cookie.php @@ -0,0 +1,80 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Session cookie driver. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Cookie_Driver implements Session_Driver { + + protected $cookie_name; + protected $encrypt; // Library + + public function __construct() + { + $this->cookie_name = Kohana::config('session.name').'_data'; + + if (Kohana::config('session.encryption')) + { + $this->encrypt = Encrypt::instance(); + } + + Kohana::log('debug', 'Session Cookie Driver Initialized'); + } + + public function open($path, $name) + { + return TRUE; + } + + public function close() + { + return TRUE; + } + + public function read($id) + { + $data = (string) cookie::get($this->cookie_name); + + if ($data == '') + return $data; + + return empty($this->encrypt) ? base64_decode($data) : $this->encrypt->decode($data); + } + + public function write($id, $data) + { + $data = empty($this->encrypt) ? base64_encode($data) : $this->encrypt->encode($data); + + if (strlen($data) > 4048) + { + Kohana::log('error', 'Session ('.$id.') data exceeds the 4KB limit, ignoring write.'); + return FALSE; + } + + return cookie::set($this->cookie_name, $data, Kohana::config('session.expiration')); + } + + public function destroy($id) + { + return cookie::delete($this->cookie_name); + } + + public function regenerate() + { + session_regenerate_id(TRUE); + + // Return new id + return session_id(); + } + + public function gc($maxlifetime) + { + return TRUE; + } + +} // End Session Cookie Driver Class
\ No newline at end of file diff --git a/kohana/libraries/drivers/Session/Database.php b/kohana/libraries/drivers/Session/Database.php new file mode 100644 index 00000000..261541e7 --- /dev/null +++ b/kohana/libraries/drivers/Session/Database.php @@ -0,0 +1,163 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Session database driver. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Database_Driver implements Session_Driver { + + /* + CREATE TABLE sessions + ( + session_id VARCHAR(127) NOT NULL, + last_activity INT(10) UNSIGNED NOT NULL, + data TEXT NOT NULL, + PRIMARY KEY (session_id) + ); + */ + + // Database settings + protected $db = 'default'; + protected $table = 'sessions'; + + // Encryption + protected $encrypt; + + // Session settings + protected $session_id; + protected $written = FALSE; + + public function __construct() + { + // Load configuration + $config = Kohana::config('session'); + + if ( ! empty($config['encryption'])) + { + // Load encryption + $this->encrypt = Encrypt::instance(); + } + + if (is_array($config['storage'])) + { + if ( ! empty($config['storage']['group'])) + { + // Set the group name + $this->db = $config['storage']['group']; + } + + if ( ! empty($config['storage']['table'])) + { + // Set the table name + $this->table = $config['storage']['table']; + } + } + + // Load database + $this->db = Database::instance($this->db); + + Kohana::log('debug', 'Session Database Driver Initialized'); + } + + public function open($path, $name) + { + return TRUE; + } + + public function close() + { + return TRUE; + } + + public function read($id) + { + // Load the session + $query = $this->db->from($this->table)->where('session_id', $id)->limit(1)->get()->result(TRUE); + + if ($query->count() === 0) + { + // No current session + $this->session_id = NULL; + + return ''; + } + + // Set the current session id + $this->session_id = $id; + + // Load the data + $data = $query->current()->data; + + return ($this->encrypt === NULL) ? base64_decode($data) : $this->encrypt->decode($data); + } + + public function write($id, $data) + { + $data = array + ( + 'session_id' => $id, + 'last_activity' => time(), + 'data' => ($this->encrypt === NULL) ? base64_encode($data) : $this->encrypt->encode($data) + ); + + if ($this->session_id === NULL) + { + // Insert a new session + $query = $this->db->insert($this->table, $data); + } + elseif ($id === $this->session_id) + { + // Do not update the session_id + unset($data['session_id']); + + // Update the existing session + $query = $this->db->update($this->table, $data, array('session_id' => $id)); + } + else + { + // Update the session and id + $query = $this->db->update($this->table, $data, array('session_id' => $this->session_id)); + + // Set the new session id + $this->session_id = $id; + } + + return (bool) $query->count(); + } + + public function destroy($id) + { + // Delete the requested session + $this->db->delete($this->table, array('session_id' => $id)); + + // Session id is no longer valid + $this->session_id = NULL; + + return TRUE; + } + + public function regenerate() + { + // Generate a new session id + session_regenerate_id(); + + // Return new session id + return session_id(); + } + + public function gc($maxlifetime) + { + // Delete all expired sessions + $query = $this->db->delete($this->table, array('last_activity <' => time() - $maxlifetime)); + + Kohana::log('debug', 'Session garbage collected: '.$query->count().' row(s) deleted.'); + + return TRUE; + } + +} // End Session Database Driver |