diff options
Diffstat (limited to 'kohana')
156 files changed, 23381 insertions, 0 deletions
diff --git a/kohana/KohanaLicense.html b/kohana/KohanaLicense.html new file mode 100644 index 00000000..de2d5299 --- /dev/null +++ b/kohana/KohanaLicense.html @@ -0,0 +1,30 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + +<title>Kohana License</title> + +</head> +<body> + +<h2>Kohana License Agreement</h2> + +<p>This license is a legal agreement between you and the Kohana Software Foundation for the use of Kohana Framework (the "Software"). By obtaining the Software you agree to comply with the terms and conditions of this license.</p> + +<p>Copyright (c) 2007-2008 Kohana Team<br/>All rights reserved.</p> + +<p>Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:</p> + +<ul> +<li>Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.</li> +<li>Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.</li> +<li>Neither the name of the Kohana nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.</li> +</ul> + +<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p> + +<p><small>NOTE: This license is modeled after the BSD software license.</small></p> + +</body> +</html>
\ No newline at end of file diff --git a/kohana/config/cache.php b/kohana/config/cache.php new file mode 100644 index 00000000..e93f9abc --- /dev/null +++ b/kohana/config/cache.php @@ -0,0 +1,32 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Cache + * + * Cache settings, defined as arrays, or "groups". If no group name is + * used when loading the cache library, the group named "default" will be used. + * + * Each group can be used independently, and multiple groups can be used at once. + * + * Group Options: + * driver - Cache backend driver. Kohana comes with file, database, and memcache drivers. + * > File cache is fast and reliable, but requires many filesystem lookups. + * > Database cache can be used to cache items remotely, but is slower. + * > Memcache is very high performance, but prevents cache tags from being used. + * + * params - Driver parameters, specific to each driver. + * + * lifetime - Default lifetime of caches in seconds. By default caches are stored for + * thirty minutes. Specific lifetime can also be set when creating a new cache. + * Setting this to 0 will never automatically delete caches. + * + * requests - Average number of cache requests that will processed before all expired + * caches are deleted. This is commonly referred to as "garbage collection". + * Setting this to 0 or a negative number will disable automatic garbage collection. + */ +$config['default'] = array +( + 'driver' => 'file', + 'params' => APPPATH.'cache', + 'lifetime' => 1800, + 'requests' => 1000 +); diff --git a/kohana/config/cache_memcache.php b/kohana/config/cache_memcache.php new file mode 100644 index 00000000..c9e10232 --- /dev/null +++ b/kohana/config/cache_memcache.php @@ -0,0 +1,20 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Cache:Memcache + * + * memcache server configuration. + */ +$config['servers'] = array +( + array + ( + 'host' => '127.0.0.1', + 'port' => 11211, + 'persistent' => FALSE, + ) +); + +/** + * Enable cache data compression. + */ +$config['compression'] = FALSE; diff --git a/kohana/config/cache_sqlite.php b/kohana/config/cache_sqlite.php new file mode 100644 index 00000000..199252df --- /dev/null +++ b/kohana/config/cache_sqlite.php @@ -0,0 +1,11 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Cache:SQLite + */ +$config['schema'] = +'CREATE TABLE caches( + id varchar(127) PRIMARY KEY, + hash char(40) NOT NULL, + tags varchar(255), + expiration int, + cache blob);';
\ No newline at end of file diff --git a/kohana/config/cache_xcache.php b/kohana/config/cache_xcache.php new file mode 100644 index 00000000..8b436fb6 --- /dev/null +++ b/kohana/config/cache_xcache.php @@ -0,0 +1,12 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Cache:Xcache + * + * Xcache administrator username. + */ +$config['PHP_AUTH_USER'] = 'kohana'; + +/** + * Xcache administrator password. + */ +$config['PHP_AUTH_PW'] = 'kohana'; diff --git a/kohana/config/captcha.php b/kohana/config/captcha.php new file mode 100644 index 00000000..a1d64c2e --- /dev/null +++ b/kohana/config/captcha.php @@ -0,0 +1,29 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Core + * + * Captcha configuration is defined in groups which allows you to easily switch + * between different Captcha settings for different forms on your website. + * Note: all groups inherit and overwrite the default group. + * + * Group Options: + * style - Captcha type, e.g. basic, alpha, word, math, riddle + * width - Width of the Captcha image + * height - Height of the Captcha image + * complexity - Difficulty level (0-10), usage depends on chosen style + * background - Path to background image file + * fontpath - Path to font folder + * fonts - Font files + * promote - Valid response count threshold to promote user (FALSE to disable) + */ +$config['default'] = array +( + 'style' => 'basic', + 'width' => 150, + 'height' => 50, + 'complexity' => 4, + 'background' => '', + 'fontpath' => SYSPATH.'fonts/', + 'fonts' => array('DejaVuSerif.ttf'), + 'promote' => FALSE, +);
\ No newline at end of file diff --git a/kohana/config/cookie.php b/kohana/config/cookie.php new file mode 100644 index 00000000..5175f172 --- /dev/null +++ b/kohana/config/cookie.php @@ -0,0 +1,32 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Core + * + * Domain, to restrict the cookie to a specific website domain. For security, + * you are encouraged to set this option. An empty setting allows the cookie + * to be read by any website domain. + */ +$config['domain'] = ''; + +/** + * Restrict cookies to a specific path, typically the installation directory. + */ +$config['path'] = '/'; + +/** + * Lifetime of the cookie. A setting of 0 makes the cookie active until the + * users browser is closed or the cookie is deleted. + */ +$config['expire'] = 0; + +/** + * Enable this option to only allow the cookie to be read when using the a + * secure protocol. + */ +$config['secure'] = FALSE; + +/** + * Enable this option to disable the cookie from being accessed when using a + * secure protocol. This option is only available in PHP 5.2 and above. + */ +$config['httponly'] = FALSE;
\ No newline at end of file diff --git a/kohana/config/credit_cards.php b/kohana/config/credit_cards.php new file mode 100644 index 00000000..ee643168 --- /dev/null +++ b/kohana/config/credit_cards.php @@ -0,0 +1,60 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Credit card validation configuration. + * + * Options for each credit card: + * length - All the allowed card number lengths, in a comma separated string + * prefix - The digits the card needs to start with, in regex format + * luhn - Enable or disable card number validation by the Luhn algorithm + */ +$config = array +( + 'default' => array + ( + 'length' => '13,14,15,16,17,18,19', + 'prefix' => '', + 'luhn' => TRUE + ), + 'american express' => array + ( + 'length' => '15', + 'prefix' => '3[47]', + 'luhn' => TRUE + ), + 'diners club' => array + ( + 'length' => '14,16', + 'prefix' => '36|55|30[0-5]', + 'luhn' => TRUE + ), + 'discover' => array + ( + 'length' => '16', + 'prefix' => '6(?:5|011)', + 'luhn' => TRUE, + ), + 'jcb' => array + ( + 'length' => '15,16', + 'prefix' => '3|1800|2131', + 'luhn' => TRUE + ), + 'maestro' => array + ( + 'length' => '16,18', + 'prefix' => '50(?:20|38)|6(?:304|759)', + 'luhn' => TRUE + ), + 'mastercard' => array + ( + 'length' => '16', + 'prefix' => '5[1-5]', + 'luhn' => TRUE + ), + 'visa' => array + ( + 'length' => '13,16', + 'prefix' => '4', + 'luhn' => TRUE + ), +);
\ No newline at end of file diff --git a/kohana/config/database.php b/kohana/config/database.php new file mode 100644 index 00000000..f34e51e3 --- /dev/null +++ b/kohana/config/database.php @@ -0,0 +1,45 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Database + * + * Database connection settings, defined as arrays, or "groups". If no group + * name is used when loading the database library, the group named "default" + * will be used. + * + * Each group can be connected to independently, and multiple groups can be + * connected at once. + * + * Group Options: + * benchmark - Enable or disable database benchmarking + * persistent - Enable or disable a persistent connection + * connection - Array of connection specific parameters; alternatively, + * you can use a DSN though it is not as fast and certain + * characters could create problems (like an '@' character + * in a password): + * 'connection' => 'mysql://dbuser:secret@localhost/kohana' + * character_set - Database character set + * table_prefix - Database table prefix + * object - Enable or disable object results + * cache - Enable or disable query caching + * escape - Enable automatic query builder escaping + */ +$config['default'] = array +( + 'benchmark' => TRUE, + 'persistent' => FALSE, + 'connection' => array + ( + 'type' => 'mysql', + 'user' => 'dbuser', + 'pass' => 'p@ssw0rd', + 'host' => 'localhost', + 'port' => FALSE, + 'socket' => FALSE, + 'database' => 'kohana' + ), + 'character_set' => 'utf8', + 'table_prefix' => '', + 'object' => TRUE, + 'cache' => FALSE, + 'escape' => TRUE +);
\ No newline at end of file diff --git a/kohana/config/email.php b/kohana/config/email.php new file mode 100644 index 00000000..7f0cdd7e --- /dev/null +++ b/kohana/config/email.php @@ -0,0 +1,22 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * SwiftMailer driver, used with the email helper. + * + * @see http://www.swiftmailer.org/wikidocs/v3/connections/nativemail + * @see http://www.swiftmailer.org/wikidocs/v3/connections/sendmail + * @see http://www.swiftmailer.org/wikidocs/v3/connections/smtp + * + * Valid drivers are: native, sendmail, smtp + */ +$config['driver'] = 'native'; + +/** + * To use secure connections with SMTP, set "port" to 465 instead of 25. + * To enable TLS, set "encryption" to "tls". + * + * Driver options: + * @param null native: no options + * @param string sendmail: executable path, with -bs or equivalent attached + * @param array smtp: hostname, (username), (password), (port), (auth), (encryption) + */ +$config['options'] = NULL; diff --git a/kohana/config/encryption.php b/kohana/config/encryption.php new file mode 100644 index 00000000..a3740b2c --- /dev/null +++ b/kohana/config/encryption.php @@ -0,0 +1,31 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Encrypt + * + * Encrypt configuration is defined in groups which allows you to easily switch + * between different encryption settings for different uses. + * Note: all groups inherit and overwrite the default group. + * + * Group Options: + * key - Encryption key used to do encryption and decryption. The default option + * should never be used for a production website. + * + * For best security, your encryption key should be at least 16 characters + * long and contain letters, numbers, and symbols. + * @note Do not use a hash as your key. This significantly lowers encryption entropy. + * + * mode - MCrypt encryption mode. By default, MCRYPT_MODE_NOFB is used. This mode + * offers initialization vector support, is suited to short strings, and + * produces the shortest encrypted output. + * @see http://php.net/mcrypt + * + * cipher - MCrypt encryption cipher. By default, the MCRYPT_RIJNDAEL_128 cipher is used. + * This is also known as 128-bit AES. + * @see http://php.net/mcrypt + */ +$config['default'] = array +( + 'key' => 'K0H@NA+PHP_7hE-SW!FtFraM3w0R|<', + 'mode' => MCRYPT_MODE_NOFB, + 'cipher' => MCRYPT_RIJNDAEL_128 +); diff --git a/kohana/config/http.php b/kohana/config/http.php new file mode 100644 index 00000000..f6714639 --- /dev/null +++ b/kohana/config/http.php @@ -0,0 +1,19 @@ +<?php defined('SYSPATH') or die('No direct script access.'); + +// HTTP-EQUIV type meta tags +$config['meta_equiv'] = array +( + 'cache-control', + 'content-type', 'content-script-type', 'content-style-type', + 'content-disposition', + 'content-language', + 'default-style', + 'expires', + 'ext-cache', + 'pics-label', + 'pragma', + 'refresh', + 'set-cookie', + 'vary', + 'window-target', +);
\ No newline at end of file diff --git a/kohana/config/image.php b/kohana/config/image.php new file mode 100644 index 00000000..ffcc899b --- /dev/null +++ b/kohana/config/image.php @@ -0,0 +1,13 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Image + * + * Driver name. Default: GD + */ +$config['driver'] = 'GD'; + +/** + * Driver parameters: + * ImageMagick - set the "directory" parameter to your ImageMagick installation directory + */ +$config['params'] = array();
\ No newline at end of file diff --git a/kohana/config/inflector.php b/kohana/config/inflector.php new file mode 100644 index 00000000..1887a37e --- /dev/null +++ b/kohana/config/inflector.php @@ -0,0 +1,55 @@ +<?php defined('SYSPATH') or die('No direct script access.'); + +$config['uncountable'] = array +( + 'access', + 'advice', + 'art', + 'baggage', + 'dances', + 'equipment', + 'fish', + 'fuel', + 'furniture', + 'food', + 'heat', + 'honey', + 'homework', + 'impatience', + 'information', + 'knowledge', + 'luggage', + 'money', + 'music', + 'news', + 'patience', + 'progress', + 'pollution', + 'research', + 'rice', + 'sand', + 'series', + 'sheep', + 'sms', + 'species', + 'toothpaste', + 'traffic', + 'understanding', + 'water', + 'weather', + 'work', +); + +$config['irregular'] = array +( + 'child' => 'children', + 'clothes' => 'clothing', + 'man' => 'men', + 'movie' => 'movies', + 'person' => 'people', + 'woman' => 'women', + 'mouse' => 'mice', + 'goose' => 'geese', + 'ox' => 'oxen', + 'leaf' => 'leaves', +); diff --git a/kohana/config/locale.php b/kohana/config/locale.php new file mode 100644 index 00000000..5d82fa9f --- /dev/null +++ b/kohana/config/locale.php @@ -0,0 +1,16 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Core + * + * Default language locale name(s). + * First item must be a valid i18n directory name, subsequent items are alternative locales + * for OS's that don't support the first (e.g. Windows). The first valid locale in the array will be used. + * @see http://php.net/setlocale + */ +$config['language'] = array('en_US', 'English_United States'); + +/** + * Locale timezone. Defaults to use the server timezone. + * @see http://php.net/timezones + */ +$config['timezone'] = '';
\ No newline at end of file diff --git a/kohana/config/mimes.php b/kohana/config/mimes.php new file mode 100644 index 00000000..3ff869ea --- /dev/null +++ b/kohana/config/mimes.php @@ -0,0 +1,224 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Core + * + * A list of mime types. Our list is generally more complete and accurate than + * the operating system MIME list. + * + * If there are any missing options, please create a ticket on our issue tracker, + * http://kohanaphp.com/trac/newticket. Be sure to give the filename and + * expected MIME type, as well as any additional information you can provide. + */ +$config = array +( + '323' => array('text/h323'), + '7z' => array('application/x-7z-compressed'), + 'abw' => array('application/x-abiword'), + 'acx' => array('application/internet-property-stream'), + 'ai' => array('application/postscript'), + 'aif' => array('audio/x-aiff'), + 'aifc' => array('audio/x-aiff'), + 'aiff' => array('audio/x-aiff'), + 'asf' => array('video/x-ms-asf'), + 'asr' => array('video/x-ms-asf'), + 'asx' => array('video/x-ms-asf'), + 'atom' => array('application/atom+xml'), + 'avi' => array('video/avi', 'video/msvideo', 'video/x-msvideo'), + 'bin' => array('application/octet-stream','application/macbinary'), + 'bmp' => array('image/bmp'), + 'c' => array('text/x-csrc'), + 'c++' => array('text/x-c++src'), + 'cab' => array('application/x-cab'), + 'cc' => array('text/x-c++src'), + 'cda' => array('application/x-cdf'), + 'class' => array('application/octet-stream'), + 'cpp' => array('text/x-c++src'), + 'cpt' => array('application/mac-compactpro'), + 'csh' => array('text/x-csh'), + 'css' => array('text/css'), + 'csv' => array('text/x-comma-separated-values', 'application/vnd.ms-excel'), + 'dbk' => array('application/docbook+xml'), + 'dcr' => array('application/x-director'), + 'deb' => array('application/x-debian-package'), + 'diff' => array('text/x-diff'), + 'dir' => array('application/x-director'), + 'divx' => array('video/divx'), + 'dll' => array('application/octet-stream', 'application/x-msdos-program'), + 'dmg' => array('application/x-apple-diskimage'), + 'dms' => array('application/octet-stream'), + 'doc' => array('application/msword'), + 'dvi' => array('application/x-dvi'), + 'dxr' => array('application/x-director'), + 'eml' => array('message/rfc822'), + 'eps' => array('application/postscript'), + 'evy' => array('application/envoy'), + 'exe' => array('application/x-msdos-program', 'application/octet-stream'), + 'fla' => array('application/octet-stream'), + 'flac' => array('application/x-flac'), + 'flc' => array('video/flc'), + 'fli' => array('video/fli'), + 'flv' => array('video/x-flv'), + 'gif' => array('image/gif'), + 'gtar' => array('application/x-gtar'), + 'gz' => array('application/x-gzip'), + 'h' => array('text/x-chdr'), + 'h++' => array('text/x-c++hdr'), + 'hh' => array('text/x-c++hdr'), + 'hpp' => array('text/x-c++hdr'), + 'hqx' => array('application/mac-binhex40'), + 'hs' => array('text/x-haskell'), + 'htm' => array('text/html'), + 'html' => array('text/html'), + 'ico' => array('image/x-icon'), + 'ics' => array('text/calendar'), + 'iii' => array('application/x-iphone'), + 'ins' => array('application/x-internet-signup'), + 'iso' => array('application/x-iso9660-image'), + 'isp' => array('application/x-internet-signup'), + 'jar' => array('application/java-archive'), + 'java' => array('application/x-java-applet'), + 'jpe' => array('image/jpeg', 'image/pjpeg'), + 'jpeg' => array('image/jpeg', 'image/pjpeg'), + 'jpg' => array('image/jpeg', 'image/pjpeg'), + 'js' => array('application/x-javascript'), + 'json' => array('application/json'), + 'latex' => array('application/x-latex'), + 'lha' => array('application/octet-stream'), + 'log' => array('text/plain', 'text/x-log'), + 'lzh' => array('application/octet-stream'), + 'm4a' => array('audio/mpeg'), + 'm4p' => array('video/mp4v-es'), + 'm4v' => array('video/mp4'), + 'man' => array('application/x-troff-man'), + 'mdb' => array('application/x-msaccess'), + 'midi' => array('audio/midi'), + 'mid' => array('audio/midi'), + 'mif' => array('application/vnd.mif'), + 'mka' => array('audio/x-matroska'), + 'mkv' => array('video/x-matroska'), + 'mov' => array('video/quicktime'), + 'movie' => array('video/x-sgi-movie'), + 'mp2' => array('audio/mpeg'), + 'mp3' => array('audio/mpeg'), + 'mp4' => array('application/mp4','audio/mp4','video/mp4'), + 'mpa' => array('video/mpeg'), + 'mpe' => array('video/mpeg'), + 'mpeg' => array('video/mpeg'), + 'mpg' => array('video/mpeg'), + 'mpg4' => array('video/mp4'), + 'mpga' => array('audio/mpeg'), + 'mpp' => array('application/vnd.ms-project'), + 'mpv' => array('video/x-matroska'), + 'mpv2' => array('video/mpeg'), + 'ms' => array('application/x-troff-ms'), + 'msg' => array('application/msoutlook','application/x-msg'), + 'msi' => array('application/x-msi'), + 'nws' => array('message/rfc822'), + 'oda' => array('application/oda'), + 'odb' => array('application/vnd.oasis.opendocument.database'), + 'odc' => array('application/vnd.oasis.opendocument.chart'), + 'odf' => array('application/vnd.oasis.opendocument.forumla'), + 'odg' => array('application/vnd.oasis.opendocument.graphics'), + 'odi' => array('application/vnd.oasis.opendocument.image'), + 'odm' => array('application/vnd.oasis.opendocument.text-master'), + 'odp' => array('application/vnd.oasis.opendocument.presentation'), + 'ods' => array('application/vnd.oasis.opendocument.spreadsheet'), + 'odt' => array('application/vnd.oasis.opendocument.text'), + 'oga' => array('audio/ogg'), + 'ogg' => array('application/ogg'), + 'ogv' => array('video/ogg'), + 'otg' => array('application/vnd.oasis.opendocument.graphics-template'), + 'oth' => array('application/vnd.oasis.opendocument.web'), + 'otp' => array('application/vnd.oasis.opendocument.presentation-template'), + 'ots' => array('application/vnd.oasis.opendocument.spreadsheet-template'), + 'ott' => array('application/vnd.oasis.opendocument.template'), + 'p' => array('text/x-pascal'), + 'pas' => array('text/x-pascal'), + 'patch' => array('text/x-diff'), + 'pbm' => array('image/x-portable-bitmap'), + 'pdf' => array('application/pdf', 'application/x-download'), + 'php' => array('application/x-httpd-php'), + 'php3' => array('application/x-httpd-php'), + 'php4' => array('application/x-httpd-php'), + 'php5' => array('application/x-httpd-php'), + 'phps' => array('application/x-httpd-php-source'), + 'phtml' => array('application/x-httpd-php'), + 'pl' => array('text/x-perl'), + 'pm' => array('text/x-perl'), + 'png' => array('image/png', 'image/x-png'), + 'po' => array('text/x-gettext-translation'), + 'pot' => array('application/vnd.ms-powerpoint'), + 'pps' => array('application/vnd.ms-powerpoint'), + 'ppt' => array('application/powerpoint'), + 'ps' => array('application/postscript'), + 'psd' => array('application/x-photoshop', 'image/x-photoshop'), + 'pub' => array('application/x-mspublisher'), + 'py' => array('text/x-python'), + 'qt' => array('video/quicktime'), + 'ra' => array('audio/x-realaudio'), + 'ram' => array('audio/x-realaudio', 'audio/x-pn-realaudio'), + 'rar' => array('application/rar'), + 'rgb' => array('image/x-rgb'), + 'rm' => array('audio/x-pn-realaudio'), + 'rpm' => array('audio/x-pn-realaudio-plugin', 'application/x-redhat-package-manager'), + 'rss' => array('application/rss+xml'), + 'rtf' => array('text/rtf'), + 'rtx' => array('text/richtext'), + 'rv' => array('video/vnd.rn-realvideo'), + 'sea' => array('application/octet-stream'), + 'sh' => array('text/x-sh'), + 'shtml' => array('text/html'), + 'sit' => array('application/x-stuffit'), + 'smi' => array('application/smil'), + 'smil' => array('application/smil'), + 'so' => array('application/octet-stream'), + 'src' => array('application/x-wais-source'), + 'svg' => array('image/svg+xml'), + 'swf' => array('application/x-shockwave-flash'), + 't' => array('application/x-troff'), + 'tar' => array('application/x-tar'), + 'tcl' => array('text/x-tcl'), + 'tex' => array('application/x-tex'), + 'text' => array('text/plain'), + 'texti' => array('application/x-texinfo'), + 'textinfo' => array('application/x-texinfo'), + 'tgz' => array('application/x-tar'), + 'tif' => array('image/tiff'), + 'tiff' => array('image/tiff'), + 'torrent' => array('application/x-bittorrent'), + 'tr' => array('application/x-troff'), + 'tsv' => array('text/tab-separated-values'), + 'txt' => array('text/plain'), + 'wav' => array('audio/x-wav'), + 'wax' => array('audio/x-ms-wax'), + 'wbxml' => array('application/wbxml'), + 'wm' => array('video/x-ms-wm'), + 'wma' => array('audio/x-ms-wma'), + 'wmd' => array('application/x-ms-wmd'), + 'wmlc' => array('application/wmlc'), + 'wmv' => array('video/x-ms-wmv', 'application/octet-stream'), + 'wmx' => array('video/x-ms-wmx'), + 'wmz' => array('application/x-ms-wmz'), + 'word' => array('application/msword', 'application/octet-stream'), + 'wp5' => array('application/wordperfect5.1'), + 'wpd' => array('application/vnd.wordperfect'), + 'wvx' => array('video/x-ms-wvx'), + 'xbm' => array('image/x-xbitmap'), + 'xcf' => array('image/xcf'), + 'xhtml' => array('application/xhtml+xml'), + 'xht' => array('application/xhtml+xml'), + 'xl' => array('application/excel', 'application/vnd.ms-excel'), + 'xla' => array('application/excel', 'application/vnd.ms-excel'), + 'xlc' => array('application/excel', 'application/vnd.ms-excel'), + 'xlm' => array('application/excel', 'application/vnd.ms-excel'), + 'xls' => array('application/excel', 'application/vnd.ms-excel'), + 'xlt' => array('application/excel', 'application/vnd.ms-excel'), + 'xml' => array('text/xml'), + 'xof' => array('x-world/x-vrml'), + 'xpm' => array('image/x-xpixmap'), + 'xsl' => array('text/xml'), + 'xvid' => array('video/x-xvid'), + 'xwd' => array('image/x-xwindowdump'), + 'z' => array('application/x-compress'), + 'zip' => array('application/x-zip', 'application/zip', 'application/x-zip-compressed') +); diff --git a/kohana/config/pagination.php b/kohana/config/pagination.php new file mode 100644 index 00000000..49b115fe --- /dev/null +++ b/kohana/config/pagination.php @@ -0,0 +1,25 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Pagination + * + * Pagination configuration is defined in groups which allows you to easily switch + * between different pagination settings for different website sections. + * Note: all groups inherit and overwrite the default group. + * + * Group Options: + * directory - Views folder in which your pagination style templates reside + * style - Pagination style template (matches view filename) + * uri_segment - URI segment (int or 'label') in which the current page number can be found + * query_string - Alternative to uri_segment: query string key that contains the page number + * items_per_page - Number of items to display per page + * auto_hide - Automatically hides pagination for single pages + */ +$config['default'] = array +( + 'directory' => 'pagination', + 'style' => 'classic', + 'uri_segment' => 3, + 'query_string' => '', + 'items_per_page' => 20, + 'auto_hide' => FALSE, +); diff --git a/kohana/config/payment.php b/kohana/config/payment.php new file mode 100644 index 00000000..8476ec46 --- /dev/null +++ b/kohana/config/payment.php @@ -0,0 +1,148 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Payment + * + * Settings related to the Payment library. + * This file has settings for each driver. + * You should copy the 'default' and the specific + * driver you are working with to your application/config/payment.php file. + * + * Options: + * driver - default driver to use + * test_mode - Turn TEST MODE on or off + * curl_settings - Set any custom cURL settings here. These defaults usualy work well. + * see http://us.php.net/manual/en/function.curl-setopt.php for details + */ +$config['default'] = array +( + 'driver' => 'Authorize', + 'test_mode' => TRUE, + 'curl_config' => array(CURLOPT_HEADER => FALSE, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => FALSE) +); + +/** + * Authorize.net Options: + * auth_net_login_id - the transaction login ID; provided by gateway provider + * auth_net_tran_key - the transaction key; provided by gateway provider + */ +$config['Authorize'] = array +( + 'auth_net_login_id' => '', + 'auth_net_tran_key' => '' +); + +/** + * YourPay.net Options: + * merchant_id - the merchant ID number + * certificate - the location on your server of the certificate file. + */ +$config['Yourpay'] = array +( + 'merchant_id' => '', + 'certificate' => './path/to/certificate.pem' +); + +/** + * TrustCommerce Options: + * custid - the customer ID assigned to you by TrustCommerce + * password - the password assigned to you by TrustCommerce + * media - "cc" for credit card or "ach" for ACH. + * tclink_library - the location of the tclink library (relative to your index file) you need to compile to get this driver to work. + */ +$config['Trustcommerce'] = array +( + 'custid' => '', + 'password' => '', + 'media' => 'cc', + 'tclink_library' => './path/to/library.so' +); + +/** + * TridentGateway Options: + * profile_id - the profile ID assigned to you by Merchant e-Services + * profile_key - the profile password assigned to you by Merchant e-Services + * transaction_type - D=Sale, C=Credit, P=Pre-Auth, O=Offline, V-Void, S=Settle Pre-Auth, U=Refund, T= Store card data., X=Delete Card Store Data + */ +$config['Trident'] = array +( + 'profile_id' => '', + 'profile_key' => '', + 'transaction_type' => 'D' +); + +/** + * PayPal Options: + * API_UserName - the username to use + * API_Password - the password to use + * API_Signature - the api signature to use + * ReturnUrl - the URL to send the user to after they login with paypal + * CANCELURL - the URL to send the user to if they cancel the paypal transaction + * CURRENCYCODE - the Currency Code to to the transactions in (What do you want to get paid in?) + */ +$config['Paypal'] = array +( + 'USER' => '-your-paypal-api-username', + 'PWD' => '-your-paypal-api-password', + 'SIGNATURE' => '-your-paypal-api-security-signiature', + 'ENDPOINT' => 'https://api-3t.paypal.com/nvp', + + 'RETURNURL' => 'http://yoursite.com', + 'CANCELURL' => 'http://yoursite.com/canceled', + + // -- sandbox authorization details are generic + 'SANDBOX_USER' => 'sdk-three_api1.sdk.com', + 'SANDBOX_PWD' => 'QFZCWN5HZM8VBG7Q', + 'SANDBOX_SIGNATURE' => 'A.d9eRKfd1yVkRrtmMfCFLTqa6M9AyodL0SJkhYztxUi8W9pCXF6.4NI', + 'SANDBOX_ENDPOINT' => 'https://api-3t.sandbox.paypal.com/nvp', + + 'VERSION' => '3.2', + 'CURRENCYCODE' => 'USD', +); + +/** + * PayPalpro Options: + * USER - API user name to use + * PWD - API password to use + * SIGNATURE - API signature to use + * + * ENDPOINT - API url used by live transaction + * + * SANDBOX_USER - User name used in test mode + * SANDBOX_PWD - Pass word used in test mode + * SANDBOX_SIGNATURE - Security signiature used in test mode + * SANDBOX_ENDPOINT - API url used for test mode transaction + * + * VERSION - API version to use + * CURRENCYCODE - can only currently be USD + * + */ +$config['Paypalpro'] = array +( + + 'USER' => '-your-paypal-api-username', + 'PWD' => '-your-paypal-api-password', + 'SIGNATURE' => '-your-paypal-api-security-signiature', + 'ENDPOINT' => 'https://api-3t.paypal.com/nvp', + + // -- sandbox authorization details are generic + 'SANDBOX_USER' => 'sdk-three_api1.sdk.com', + 'SANDBOX_PWD' => 'QFZCWN5HZM8VBG7Q', + 'SANDBOX_SIGNATURE' => 'A.d9eRKfd1yVkRrtmMfCFLTqa6M9AyodL0SJkhYztxUi8W9pCXF6.4NI', + 'SANDBOX_ENDPOINT' => 'https://api-3t.sandbox.paypal.com/nvp', + + 'VERSION' => '3.2', + 'CURRENCYCODE' => 'USD', + + 'curl_config' => array + ( + CURLOPT_HEADER => FALSE, + CURLOPT_SSL_VERIFYPEER => FALSE, + CURLOPT_SSL_VERIFYHOST => FALSE, + CURLOPT_VERBOSE => TRUE, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_POST => TRUE + ) + +);
\ No newline at end of file diff --git a/kohana/config/profiler.php b/kohana/config/profiler.php new file mode 100644 index 00000000..56cd1464 --- /dev/null +++ b/kohana/config/profiler.php @@ -0,0 +1,8 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Profiler + * + * Array of section names to display in the Profiler, TRUE to display all of them. + * Built in sections are benchmarks, database, session, post and cookies, custom sections can be used too. + */ +$config['show'] = TRUE; diff --git a/kohana/config/routes.php b/kohana/config/routes.php new file mode 100644 index 00000000..54fdc343 --- /dev/null +++ b/kohana/config/routes.php @@ -0,0 +1,7 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Core + * + * Sets the default route to "welcome" + */ +$config['_default'] = 'welcome'; diff --git a/kohana/config/session.php b/kohana/config/session.php new file mode 100644 index 00000000..966239b2 --- /dev/null +++ b/kohana/config/session.php @@ -0,0 +1,47 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Session + * + * Session driver name. + */ +$config['driver'] = 'cookie'; + +/** + * Session storage parameter, used by drivers. + */ +$config['storage'] = ''; + +/** + * Session name. + * It must contain only alphanumeric characters and underscores. At least one letter must be present. + */ +$config['name'] = 'kohanasession'; + +/** + * Session parameters to validate: user_agent, ip_address, expiration. + */ +$config['validate'] = array('user_agent'); + +/** + * Enable or disable session encryption. + * Note: this has no effect on the native session driver. + * Note: the cookie driver always encrypts session data. Set to TRUE for stronger encryption. + */ +$config['encryption'] = FALSE; + +/** + * Session lifetime. Number of seconds that each session will last. + * A value of 0 will keep the session active until the browser is closed (with a limit of 24h). + */ +$config['expiration'] = 7200; + +/** + * Number of page loads before the session id is regenerated. + * A value of 0 will disable automatic session id regeneration. + */ +$config['regenerate'] = 3; + +/** + * Percentage probability that the gc (garbage collection) routine is started. + */ +$config['gc_probability'] = 2;
\ No newline at end of file diff --git a/kohana/config/sql_types.php b/kohana/config/sql_types.php new file mode 100644 index 00000000..1b63a9b3 --- /dev/null +++ b/kohana/config/sql_types.php @@ -0,0 +1,47 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Database + * + * SQL data types. If there are missing values, please report them: + * + * @link http://trac.kohanaphp.com/newticket + */ +$config = array +( + 'tinyint' => array('type' => 'int', 'max' => 127), + 'smallint' => array('type' => 'int', 'max' => 32767), + 'mediumint' => array('type' => 'int', 'max' => 8388607), + 'int' => array('type' => 'int', 'max' => 2147483647), + 'integer' => array('type' => 'int', 'max' => 2147483647), + 'bigint' => array('type' => 'int', 'max' => 9223372036854775807), + 'float' => array('type' => 'float'), + 'boolean' => array('type' => 'boolean'), + 'time' => array('type' => 'string', 'format' => '00:00:00'), + 'date' => array('type' => 'string', 'format' => '0000-00-00'), + 'year' => array('type' => 'string', 'format' => '0000'), + 'datetime' => array('type' => 'string', 'format' => '0000-00-00 00:00:00'), + 'char' => array('type' => 'string', 'exact' => TRUE), + 'binary' => array('type' => 'string', 'binary' => TRUE, 'exact' => TRUE), + 'varchar' => array('type' => 'string'), + 'varbinary' => array('type' => 'string', 'binary' => TRUE), + 'blob' => array('type' => 'string', 'binary' => TRUE), + 'text' => array('type' => 'string') +); + +// DOUBLE +$config['double'] = $config['decimal'] = $config['real'] = $config['numeric'] = $config['float']; + +// BIT +$config['bit'] = $config['boolean']; + +// TIMESTAMP +$config['timestamp'] = $config['datetime']; + +// ENUM +$config['enum'] = $config['set'] = $config['varchar']; + +// TEXT +$config['tinytext'] = $config['mediumtext'] = $config['longtext'] = $config['text']; + +// BLOB +$config['tinyblob'] = $config['mediumblob'] = $config['longblob'] = $config['clob'] = $config['blob']; diff --git a/kohana/config/upload.php b/kohana/config/upload.php new file mode 100644 index 00000000..8e42576c --- /dev/null +++ b/kohana/config/upload.php @@ -0,0 +1,17 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Core + * + * This path is relative to your index file. Absolute paths are also supported. + */ +$config['directory'] = DOCROOT.'upload'; + +/** + * Enable or disable directory creation. + */ +$config['create_directories'] = FALSE; + +/** + * Remove spaces from uploaded filenames. + */ +$config['remove_spaces'] = TRUE;
\ No newline at end of file diff --git a/kohana/config/user_agents.php b/kohana/config/user_agents.php new file mode 100644 index 00000000..8ae09b96 --- /dev/null +++ b/kohana/config/user_agents.php @@ -0,0 +1,109 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); +/** + * @package Core + * + * This file contains four arrays of user agent data. It is used by the + * User Agent library to help identify browser, platform, robot, and + * mobile device data. The array keys are used to identify the device + * and the array values are used to set the actual name of the item. + */ +$config['platform'] = array +( + 'windows nt 6.0' => 'Windows Vista', + 'windows nt 5.2' => 'Windows 2003', + 'windows nt 5.0' => 'Windows 2000', + 'windows nt 5.1' => 'Windows XP', + 'windows nt 4.0' => 'Windows NT', + 'winnt4.0' => 'Windows NT', + 'winnt 4.0' => 'Windows NT', + 'winnt' => 'Windows NT', + 'windows 98' => 'Windows 98', + 'win98' => 'Windows 98', + 'windows 95' => 'Windows 95', + 'win95' => 'Windows 95', + 'windows' => 'Unknown Windows OS', + 'os x' => 'Mac OS X', + 'intel mac' => 'Intel Mac', + 'ppc mac' => 'PowerPC Mac', + 'powerpc' => 'PowerPC', + 'ppc' => 'PowerPC', + 'cygwin' => 'Cygwin', + 'linux' => 'Linux', + 'debian' => 'Debian', + 'openvms' => 'OpenVMS', + 'sunos' => 'Sun Solaris', + 'amiga' => 'Amiga', + 'beos' => 'BeOS', + 'apachebench' => 'ApacheBench', + 'freebsd' => 'FreeBSD', + 'netbsd' => 'NetBSD', + 'bsdi' => 'BSDi', + 'openbsd' => 'OpenBSD', + 'os/2' => 'OS/2', + 'warp' => 'OS/2', + 'aix' => 'AIX', + 'irix' => 'Irix', + 'osf' => 'DEC OSF', + 'hp-ux' => 'HP-UX', + 'hurd' => 'GNU/Hurd', + 'unix' => 'Unknown Unix OS', +); + +// The order of this array should NOT be changed. Many browsers return +// multiple browser types so we want to identify the sub-type first. +$config['browser'] = array +( + 'Opera' => 'Opera', + 'MSIE' => 'Internet Explorer', + 'Internet Explorer' => 'Internet Explorer', + 'Shiira' => 'Shiira', + 'Firefox' => 'Firefox', + 'Chimera' => 'Chimera', + 'Phoenix' => 'Phoenix', + 'Firebird' => 'Firebird', + 'Camino' => 'Camino', + 'Netscape' => 'Netscape', + 'OmniWeb' => 'OmniWeb', + 'Safari' => 'Safari', + 'Konqueror' => 'Konqueror', + 'Epiphany' => 'Epiphany', + 'Galeon' => 'Galeon', + 'Mozilla' => 'Mozilla', + 'icab' => 'iCab', + 'lynx' => 'Lynx', + 'links' => 'Links', + 'hotjava' => 'HotJava', + 'amaya' => 'Amaya', + 'IBrowse' => 'IBrowse', +); + +$config['mobile'] = array +( + 'mobileexplorer' => 'Mobile Explorer', + 'openwave' => 'Open Wave', + 'opera mini' => 'Opera Mini', + 'operamini' => 'Opera Mini', + 'elaine' => 'Palm', + 'palmsource' => 'Palm', + 'digital paths' => 'Palm', + 'avantgo' => 'Avantgo', + 'xiino' => 'Xiino', + 'palmscape' => 'Palmscape', + 'nokia' => 'Nokia', + 'ericsson' => 'Ericsson', + 'blackBerry' => 'BlackBerry', + 'motorola' => 'Motorola', +); + +// There are hundreds of bots but these are the most common. +$config['robot'] = array +( + 'googlebot' => 'Googlebot', + 'msnbot' => 'MSNBot', + 'slurp' => 'Inktomi Slurp', + 'yahoo' => 'Yahoo', + 'askjeeves' => 'AskJeeves', + 'fastcrawler' => 'FastCrawler', + 'infoseek' => 'InfoSeek Robot 1.0', + 'lycos' => 'Lycos', +);
\ No newline at end of file diff --git a/kohana/config/view.php b/kohana/config/view.php new file mode 100644 index 00000000..ed6b13b1 --- /dev/null +++ b/kohana/config/view.php @@ -0,0 +1,17 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * @package Core + * + * Allowed non-php view types. Most file extensions are supported. + */ +$config['allowed_filetypes'] = array +( + 'gif', + 'jpg', 'jpeg', + 'png', + 'tif', 'tiff', + 'swf', + 'htm', 'html', + 'css', + 'js' +); diff --git a/kohana/controllers/captcha.php b/kohana/controllers/captcha.php new file mode 100644 index 00000000..60f9f401 --- /dev/null +++ b/kohana/controllers/captcha.php @@ -0,0 +1,23 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Outputs the dynamic Captcha resource. + * Usage: Call the Captcha controller from a view, e.g. + * <img src="<?php echo url::site('captcha') ?>" /> + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Controller extends Controller { + + public function __call($method, $args) + { + // Output the Captcha challenge resource (no html) + // Pull the config group name from the URL + Captcha::factory($this->uri->segment(2))->render(FALSE); + } + +} // End Captcha_Controller
\ No newline at end of file diff --git a/kohana/controllers/template.php b/kohana/controllers/template.php new file mode 100644 index 00000000..189f2da8 --- /dev/null +++ b/kohana/controllers/template.php @@ -0,0 +1,54 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Allows a template to be automatically loaded and displayed. Display can be + * dynamically turned off in the controller methods, and the template file + * can be overloaded. + * + * To use it, declare your controller to extend this class: + * `class Your_Controller extends Template_Controller` + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Template_Controller extends Controller { + + // Template view name + public $template = 'template'; + + // Default to do auto-rendering + public $auto_render = TRUE; + + /** + * Template loading and setup routine. + */ + public function __construct() + { + parent::__construct(); + + // Load the template + $this->template = new View($this->template); + + if ($this->auto_render == TRUE) + { + // Render the template immediately after the controller method + Event::add('system.post_controller', array($this, '_render')); + } + } + + /** + * Render the loaded template. + */ + public function _render() + { + if ($this->auto_render == TRUE) + { + // Render the template when the class is destroyed + $this->template->render(TRUE); + } + } + +} // End Template_Controller
\ No newline at end of file diff --git a/kohana/core/Benchmark.php b/kohana/core/Benchmark.php new file mode 100644 index 00000000..18d1e5f1 --- /dev/null +++ b/kohana/core/Benchmark.php @@ -0,0 +1,94 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Simple benchmarking. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @license http://kohanaphp.com/license.html + */ +final class Benchmark { + + // Benchmark timestamps + private static $marks; + + /** + * Set a benchmark start point. + * + * @param string benchmark name + * @return void + */ + public static function start($name) + { + if ( ! isset(self::$marks[$name])) + { + self::$marks[$name] = array + ( + 'start' => microtime(TRUE), + 'stop' => FALSE, + 'memory_start' => function_exists('memory_get_usage') ? memory_get_usage() : 0, + 'memory_stop' => FALSE + ); + } + } + + /** + * Set a benchmark stop point. + * + * @param string benchmark name + * @return void + */ + public static function stop($name) + { + if (isset(self::$marks[$name]) AND self::$marks[$name]['stop'] === FALSE) + { + self::$marks[$name]['stop'] = microtime(TRUE); + self::$marks[$name]['memory_stop'] = function_exists('memory_get_usage') ? memory_get_usage() : 0; + } + } + + /** + * Get the elapsed time between a start and stop. + * + * @param string benchmark name, TRUE for all + * @param integer number of decimal places to count to + * @return array + */ + public static function get($name, $decimals = 4) + { + if ($name === TRUE) + { + $times = array(); + $names = array_keys(self::$marks); + + foreach ($names as $name) + { + // Get each mark recursively + $times[$name] = self::get($name, $decimals); + } + + // Return the array + return $times; + } + + if ( ! isset(self::$marks[$name])) + return FALSE; + + if (self::$marks[$name]['stop'] === FALSE) + { + // Stop the benchmark to prevent mis-matched results + self::stop($name); + } + + // Return a string version of the time between the start and stop points + // Properly reading a float requires using number_format or sprintf + return array + ( + 'time' => number_format(self::$marks[$name]['stop'] - self::$marks[$name]['start'], $decimals), + 'memory' => (self::$marks[$name]['memory_stop'] - self::$marks[$name]['memory_start']) + ); + } + +} // End Benchmark diff --git a/kohana/core/Bootstrap.php b/kohana/core/Bootstrap.php new file mode 100644 index 00000000..3826571d --- /dev/null +++ b/kohana/core/Bootstrap.php @@ -0,0 +1,58 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Kohana process control file, loaded by the front controller. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @license http://kohanaphp.com/license.html + */ + +define('KOHANA_VERSION', '2.2'); +define('KOHANA_CODENAME', 'efímera'); + +// Test of Kohana is running in Windows +define('KOHANA_IS_WIN', PHP_SHLIB_SUFFIX === 'dll'); + +// Kohana benchmarks are prefixed to prevent collisions +define('SYSTEM_BENCHMARK', 'system_benchmark'); + +// Load benchmarking support +require SYSPATH.'core/Benchmark'.EXT; + +// Start total_execution +Benchmark::start(SYSTEM_BENCHMARK.'_total_execution'); + +// Start kohana_loading +Benchmark::start(SYSTEM_BENCHMARK.'_kohana_loading'); + +// Load core files +require SYSPATH.'core/utf8'.EXT; +require SYSPATH.'core/Event'.EXT; +require SYSPATH.'core/Kohana'.EXT; + +// Prepare the environment +Kohana::setup(); + +// End kohana_loading +Benchmark::stop(SYSTEM_BENCHMARK.'_kohana_loading'); + +// Start system_initialization +Benchmark::start(SYSTEM_BENCHMARK.'_system_initialization'); + +// Prepare the system +Event::run('system.ready'); + +// Determine routing +Event::run('system.routing'); + +// End system_initialization +Benchmark::stop(SYSTEM_BENCHMARK.'_system_initialization'); + +// Make the magic happen! +Event::run('system.execute'); + +// Clean up and exit +Event::run('system.shutdown');
\ No newline at end of file diff --git a/kohana/core/Event.php b/kohana/core/Event.php new file mode 100644 index 00000000..1d6feae4 --- /dev/null +++ b/kohana/core/Event.php @@ -0,0 +1,232 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Process queuing/execution class. Allows an unlimited number of callbacks + * to be added to 'events'. Events can be run multiple times, and can also + * process event-specific data. By default, Kohana has several system events. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @license http://kohanaphp.com/license.html + * @link http://docs.kohanaphp.com/general/events + */ +final class Event { + + // Event callbacks + private static $events = array(); + + // Cache of events that have been run + private static $has_run = array(); + + // Data that can be processed during events + public static $data; + + /** + * Add a callback to an event queue. + * + * @param string event name + * @param array http://php.net/callback + * @return boolean + */ + public static function add($name, $callback) + { + if ( ! isset(self::$events[$name])) + { + // Create an empty event if it is not yet defined + self::$events[$name] = array(); + } + elseif (in_array($callback, self::$events[$name], TRUE)) + { + // The event already exists + return FALSE; + } + + // Add the event + self::$events[$name][] = $callback; + + return TRUE; + } + + /** + * Add a callback to an event queue, before a given event. + * + * @param string event name + * @param array existing event callback + * @param array event callback + * @return boolean + */ + public static function add_before($name, $existing, $callback) + { + if (empty(self::$events[$name]) OR ($key = array_search($existing, self::$events[$name])) === FALSE) + { + // Just add the event if there are no events + return self::add($name, $callback); + } + else + { + // Insert the event immediately before the existing event + return self::insert_event($name, $key, $callback); + } + } + + /** + * Add a callback to an event queue, after a given event. + * + * @param string event name + * @param array existing event callback + * @param array event callback + * @return boolean + */ + public static function add_after($name, $existing, $callback) + { + if (empty(self::$events[$name]) OR ($key = array_search($existing, self::$events[$name])) === FALSE) + { + // Just add the event if there are no events + return self::add($name, $callback); + } + else + { + // Insert the event immediately after the existing event + return self::insert_event($name, $key + 1, $callback); + } + } + + /** + * Inserts a new event at a specfic key location. + * + * @param string event name + * @param integer key to insert new event at + * @param array event callback + * @return void + */ + private static function insert_event($name, $key, $callback) + { + if (in_array($callback, self::$events[$name], TRUE)) + return FALSE; + + // Add the new event at the given key location + self::$events[$name] = array_merge + ( + // Events before the key + array_slice(self::$events[$name], 0, $key), + // New event callback + array($callback), + // Events after the key + array_slice(self::$events[$name], $key) + ); + + return TRUE; + } + + /** + * Replaces an event with another event. + * + * @param string event name + * @param array event to replace + * @param array new callback + * @return boolean + */ + public static function replace($name, $existing, $callback) + { + if (empty(self::$events[$name]) OR ($key = array_search($existing, self::$events[$name], TRUE)) === FALSE) + return FALSE; + + if ( ! in_array($callback, self::$events[$name], TRUE)) + { + // Replace the exisiting event with the new event + self::$events[$name][$key] = $callback; + } + else + { + // Remove the existing event from the queue + unset(self::$events[$name][$key]); + + // Reset the array so the keys are ordered properly + self::$events[$name] = array_values(self::$events[$name]); + } + + return TRUE; + } + + /** + * Get all callbacks for an event. + * + * @param string event name + * @return array + */ + public static function get($name) + { + return empty(self::$events[$name]) ? array() : self::$events[$name]; + } + + /** + * Clear some or all callbacks from an event. + * + * @param string event name + * @param array specific callback to remove, FALSE for all callbacks + * @return void + */ + public static function clear($name, $callback = FALSE) + { + if ($callback === FALSE) + { + self::$events[$name] = array(); + } + elseif (isset(self::$events[$name])) + { + // Loop through each of the event callbacks and compare it to the + // callback requested for removal. The callback is removed if it + // matches. + foreach (self::$events[$name] as $i => $event_callback) + { + if ($callback === $event_callback) + { + unset(self::$events[$name][$i]); + } + } + } + } + + /** + * Execute all of the callbacks attached to an event. + * + * @param string event name + * @param array data can be processed as Event::$data by the callbacks + * @return void + */ + public static function run($name, & $data = NULL) + { + if ( ! empty(self::$events[$name])) + { + // So callbacks can access Event::$data + self::$data =& $data; + $callbacks = self::get($name); + + foreach ($callbacks as $callback) + { + call_user_func($callback); + } + + // Do this to prevent data from getting 'stuck' + $clear_data = ''; + self::$data =& $clear_data; + + // The event has been run! + self::$has_run[$name] = $name; + } + } + + /** + * Check if a given event has been run. + * + * @param string event name + * @return boolean + */ + public static function has_run($name) + { + return isset(self::$has_run[$name]); + } + +} // End Event
\ No newline at end of file diff --git a/kohana/core/Kohana.php b/kohana/core/Kohana.php new file mode 100644 index 00000000..109853b1 --- /dev/null +++ b/kohana/core/Kohana.php @@ -0,0 +1,1744 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Provides Kohana-specific helper functions. This is where the magic happens! + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +final class Kohana { + + // The singleton instance of the controller + public static $instance; + + // Output buffering level + private static $buffer_level; + + // Will be set to TRUE when an exception is caught + public static $has_error = FALSE; + + // The final output that will displayed by Kohana + public static $output = ''; + + // The current user agent + public static $user_agent; + + // The current locale + public static $locale; + + // Configuration + private static $configuration; + + // Include paths + private static $include_paths; + + // Logged messages + private static $log; + + // Cache lifetime + private static $cache_lifetime; + + // Log levels + private static $log_levels = array + ( + 'error' => 1, + 'alert' => 2, + 'info' => 3, + 'debug' => 4, + ); + + // Internal caches and write status + private static $internal_cache = array(); + private static $write_cache; + + /** + * Sets up the PHP environment. Adds error/exception handling, output + * buffering, and adds an auto-loading method for loading classes. + * + * This method is run immediately when this file is loaded, and is + * benchmarked as environment_setup. + * + * For security, this function also destroys the $_REQUEST global variable. + * Using the proper global (GET, POST, COOKIE, etc) is inherently more secure. + * The recommended way to fetch a global variable is using the Input library. + * @see http://www.php.net/globals + * + * @return void + */ + public static function setup() + { + static $run; + + // This function can only be run once + if ($run === TRUE) + return; + + // Start the environment setup benchmark + Benchmark::start(SYSTEM_BENCHMARK.'_environment_setup'); + + // Define Kohana error constant + define('E_KOHANA', 42); + + // Define 404 error constant + define('E_PAGE_NOT_FOUND', 43); + + // Define database error constant + define('E_DATABASE_ERROR', 44); + + if (self::$cache_lifetime = self::config('core.internal_cache')) + { + // Load cached configuration and language files + self::$internal_cache['configuration'] = self::cache('configuration', self::$cache_lifetime); + self::$internal_cache['language'] = self::cache('language', self::$cache_lifetime); + + // Load cached file paths + self::$internal_cache['find_file_paths'] = self::cache('find_file_paths', self::$cache_lifetime); + + // Enable cache saving + Event::add('system.shutdown', array(__CLASS__, 'internal_cache_save')); + } + + // Disable notices and "strict" errors + $ER = error_reporting(~E_NOTICE & ~E_STRICT); + + // Set the user agent + self::$user_agent = trim($_SERVER['HTTP_USER_AGENT']); + + if (function_exists('date_default_timezone_set')) + { + $timezone = Kohana::config('locale.timezone'); + + // Set default timezone, due to increased validation of date settings + // which cause massive amounts of E_NOTICEs to be generated in PHP 5.2+ + date_default_timezone_set(empty($timezone) ? date_default_timezone_get() : $timezone); + } + + // Restore error reporting + error_reporting($ER); + + // Start output buffering + ob_start(array(__CLASS__, 'output_buffer')); + + // Save buffering level + self::$buffer_level = ob_get_level(); + + // Set autoloader + spl_autoload_register(array('Kohana', 'auto_load')); + + // Set error handler + set_error_handler(array('Kohana', 'exception_handler')); + + // Set exception handler + set_exception_handler(array('Kohana', 'exception_handler')); + + // Send default text/html UTF-8 header + header('Content-Type: text/html; charset=UTF-8'); + + // Load locales + $locales = self::config('locale.language'); + + // Make first locale UTF-8 + $locales[0] .= '.UTF-8'; + + // Set locale information + self::$locale = setlocale(LC_ALL, $locales); + + if (self::$configuration['core']['log_threshold'] > 0) + { + // Set the log directory + self::log_directory(self::$configuration['core']['log_directory']); + + // Enable log writing at shutdown + register_shutdown_function(array(__CLASS__, 'log_save')); + } + + // Enable Kohana routing + Event::add('system.routing', array('Router', 'find_uri')); + Event::add('system.routing', array('Router', 'setup')); + + // Enable Kohana controller initialization + Event::add('system.execute', array('Kohana', 'instance')); + + // Enable Kohana 404 pages + Event::add('system.404', array('Kohana', 'show_404')); + + // Enable Kohana output handling + Event::add('system.shutdown', array('Kohana', 'shutdown')); + + if ($config = Kohana::config('core.enable_hooks')) + { + $hooks = array(); + + if ( ! is_array($config)) + { + // All of the hooks are enabled, so we use list_files + $hooks = Kohana::list_files('hooks', TRUE); + } + else + { + // Individual hooks need to be found + foreach ($config as $name) + { + if ($hook = Kohana::find_file('hooks', $name, FALSE)) + { + // Hook was found, add it to loaded hooks + $hooks[] = $hook; + } + else + { + // This should never happen + Kohana::log('error', 'Hook not found: '.$name); + } + } + } + + // Length of extension, for offset + $ext = -(strlen(EXT)); + + foreach ($hooks as $hook) + { + // Validate the filename extension + if (substr($hook, $ext) === EXT) + { + // Hook was found, include it + include $hook; + } + else + { + // This should never happen + Kohana::log('error', 'Hook not found: '.$hook); + } + } + } + + // Setup is complete, prevent it from being run again + $run = TRUE; + + // Stop the environment setup routine + Benchmark::stop(SYSTEM_BENCHMARK.'_environment_setup'); + } + + /** + * Loads the controller and initializes it. Runs the pre_controller, + * post_controller_constructor, and post_controller events. Triggers + * a system.404 event when the route cannot be mapped to a controller. + * + * This method is benchmarked as controller_setup and controller_execution. + * + * @return object instance of controller + */ + public static function & instance() + { + if (self::$instance === NULL) + { + Benchmark::start(SYSTEM_BENCHMARK.'_controller_setup'); + + if (Router::$method[0] === '_') + { + // Do not allow access to hidden methods + Event::run('system.404'); + } + + // Include the Controller file + require Router::$controller_path; + + try + { + // Start validation of the controller + $class = new ReflectionClass(ucfirst(Router::$controller).'_Controller'); + } + catch (ReflectionException $e) + { + // Controller does not exist + Event::run('system.404'); + } + + if (IN_PRODUCTION AND $class->getConstant('ALLOW_PRODUCTION') == FALSE) + { + // Controller is not allowed to run in production + Event::run('system.404'); + } + + // Run system.pre_controller + Event::run('system.pre_controller'); + + // Create a new controller instance + $controller = $class->newInstance(); + + // Controller constructor has been executed + Event::run('system.post_controller_constructor'); + + try + { + // Load the controller method + $method = $class->getMethod(Router::$method); + + if ($method->isProtected() or $method->isPrivate()) + { + // Do not attempt to invoke protected methods + throw new ReflectionException('protected controller method'); + } + + // Default arguments + $arguments = Router::$arguments; + } + catch (ReflectionException $e) + { + // Use __call instead + $method = $class->getMethod('__call'); + + // Use arguments in __call format + $arguments = array(Router::$method, Router::$arguments); + } + + // Stop the controller setup benchmark + Benchmark::stop(SYSTEM_BENCHMARK.'_controller_setup'); + + // Start the controller execution benchmark + Benchmark::start(SYSTEM_BENCHMARK.'_controller_execution'); + + // Execute the controller method + $method->invokeArgs($controller, $arguments); + + // Controller method has been executed + Event::run('system.post_controller'); + + // Stop the controller execution benchmark + Benchmark::stop(SYSTEM_BENCHMARK.'_controller_execution'); + } + + return self::$instance; + } + + /** + * Get all include paths. APPPATH is the first path, followed by module + * paths in the order they are configured, follow by the SYSPATH. + * + * @param boolean re-process the include paths + * @return array + */ + public static function include_paths($process = FALSE) + { + if ($process === TRUE) + { + // Add APPPATH as the first path + self::$include_paths = array(APPPATH); + + foreach (self::$configuration['core']['modules'] as $path) + { + if ($path = str_replace('\\', '/', realpath($path))) + { + // Add a valid path + self::$include_paths[] = $path.'/'; + } + } + + // Add SYSPATH as the last path + self::$include_paths[] = SYSPATH; + } + + return self::$include_paths; + } + + /** + * Get a config item or group. + * + * @param string item name + * @param boolean force a forward slash (/) at the end of the item + * @param boolean is the item required? + * @return mixed + */ + public static function config($key, $slash = FALSE, $required = TRUE) + { + if (self::$configuration === NULL) + { + // Load core configuration + self::$configuration['core'] = self::config_load('core'); + + // Re-parse the include paths + self::include_paths(TRUE); + } + + // Get the group name from the key + $group = explode('.', $key, 2); + $group = $group[0]; + + if ( ! isset(self::$configuration[$group])) + { + // Load the configuration group + self::$configuration[$group] = self::config_load($group, $required); + } + + // Get the value of the key string + $value = self::key_string(self::$configuration, $key); + + if ($slash === TRUE AND is_string($value) AND $value !== '') + { + // Force the value to end with "/" + $value = rtrim($value, '/').'/'; + } + + return $value; + } + + /** + * Sets a configuration item, if allowed. + * + * @param string config key string + * @param string config value + * @return boolean + */ + public static function config_set($key, $value) + { + // Do this to make sure that the config array is already loaded + self::config($key); + + if (substr($key, 0, 7) === 'routes.') + { + // Routes cannot contain sub keys due to possible dots in regex + $keys = explode('.', $key, 2); + } + else + { + // Convert dot-noted key string to an array + $keys = explode('.', $key); + } + + // Used for recursion + $conf =& self::$configuration; + $last = count($keys) - 1; + + foreach ($keys as $i => $k) + { + if ($i === $last) + { + $conf[$k] = $value; + } + else + { + $conf =& $conf[$k]; + } + } + + if ($key === 'core.modules') + { + // Reprocess the include paths + self::include_paths(TRUE); + } + + return TRUE; + } + + /** + * Load a config file. + * + * @param string config filename, without extension + * @param boolean is the file required? + * @return array + */ + public static function config_load($name, $required = TRUE) + { + if ($name === 'core') + { + // Load the application configuration file + require APPPATH.'config/config'.EXT; + + if ( ! isset($config['site_domain'])) + { + // Invalid config file + die('Your Kohana application configuration file is not valid.'); + } + + return $config; + } + + if (isset(self::$internal_cache['configuration'][$name])) + return self::$internal_cache['configuration'][$name]; + + // Load matching configs + $configuration = array(); + + if ($files = self::find_file('config', $name, $required)) + { + foreach ($files as $file) + { + require $file; + + if (isset($config) AND is_array($config)) + { + // Merge in configuration + $configuration = array_merge($configuration, $config); + } + } + } + + if ( ! isset(self::$write_cache['configuration'])) + { + // Cache has changed + self::$write_cache['configuration'] = TRUE; + } + + return self::$internal_cache['configuration'][$name] = $configuration; + } + + /** + * Clears a config group from the cached configuration. + * + * @param string config group + * @return void + */ + public static function config_clear($group) + { + // Remove the group from config + unset(self::$configuration[$group], self::$internal_cache['configuration'][$group]); + + if ( ! isset(self::$write_cache['configuration'])) + { + // Cache has changed + self::$write_cache['configuration'] = TRUE; + } + } + + /** + * Add a new message to the log. + * + * @param string type of message + * @param string message text + * @return void + */ + public static function log($type, $message) + { + if (self::$log_levels[$type] <= self::$configuration['core']['log_threshold']) + { + self::$log[] = array(date('Y-m-d H:i:s P'), $type, $message); + } + } + + /** + * Save all currently logged messages. + * + * @return void + */ + public static function log_save() + { + if (empty(self::$log)) + return; + + // Filename of the log + $filename = self::log_directory().date('Y-m-d').'.log'.EXT; + + if ( ! is_file($filename)) + { + // Write the SYSPATH checking header + file_put_contents($filename, + '<?php defined(\'SYSPATH\') or die(\'No direct script access.\'); ?>'.PHP_EOL.PHP_EOL); + + // Prevent external writes + chmod($filename, 0644); + } + + // Messages to write + $messages = array(); + + do + { + // Load the next mess + list ($date, $type, $text) = array_shift(self::$log); + + // Add a new message line + $messages[] = $date.' --- '.$type.': '.$text; + } + while ( ! empty(self::$log)); + + // Write messages to log file + file_put_contents($filename, implode(PHP_EOL, $messages).PHP_EOL, FILE_APPEND); + } + + /** + * Get or set the logging directory. + * + * @param string new log directory + * @return string + */ + public static function log_directory($dir = NULL) + { + static $directory; + + if ( ! empty($dir)) + { + // Get the directory path + $dir = realpath($dir); + + if (is_dir($dir) AND is_writable($dir)) + { + // Change the log directory + $directory = str_replace('\\', '/', $dir).'/'; + } + else + { + // Log directory is invalid + throw new Kohana_Exception('core.log_dir_unwritable', $dir); + } + } + + return $directory; + } + + /** + * Load data from a simple cache file. This should only be used internally, + * and is NOT a replacement for the Cache library. + * + * @param string unique name of cache + * @param integer expiration in seconds + * @return mixed + */ + public static function cache($name, $lifetime) + { + if ($lifetime > 0) + { + $path = APPPATH.'cache/kohana_'.$name; + + if (is_file($path)) + { + // Check the file modification time + if ((time() - filemtime($path)) < $lifetime) + { + // Cache is valid + return unserialize(file_get_contents($path)); + } + else + { + // Cache is invalid, delete it + unlink($path); + } + } + } + + // No cache found + return NULL; + } + + /** + * Save data to a simple cache file. This should only be used internally, and + * is NOT a replacement for the Cache library. + * + * @param string cache name + * @param mixed data to cache + * @param integer expiration in seconds + * @return boolean + */ + public static function cache_save($name, $data, $lifetime) + { + if ($lifetime < 1) + return FALSE; + + $path = APPPATH.'cache/kohana_'.$name; + + if ($data === NULL) + { + // Delete cache + return (is_file($path) and unlink($path)); + } + else + { + // Write data to cache file + return (bool) file_put_contents($path, serialize($data)); + } + } + + /** + * Kohana output handler. + * + * @param string current output buffer + * @return string + */ + public static function output_buffer($output) + { + if ( ! Event::has_run('system.send_headers')) + { + // Run the send_headers event, specifically for cookies being set + Event::run('system.send_headers'); + } + + // Set final output + self::$output = $output; + + // Set and return the final output + return $output; + } + + /** + * Closes all open output buffers, either by flushing or cleaning all + * open buffers, including the Kohana output buffer. + * + * @param boolean disable to clear buffers, rather than flushing + * @return void + */ + public static function close_buffers($flush = TRUE) + { + if (ob_get_level() >= self::$buffer_level) + { + // Set the close function + $close = ($flush === TRUE) ? 'ob_end_flush' : 'ob_end_clean'; + + while (ob_get_level() > self::$buffer_level) + { + // Flush or clean the buffer + $close(); + } + + // This will flush the Kohana buffer, which sets self::$output + ob_end_clean(); + + // Reset the buffer level + self::$buffer_level = ob_get_level(); + } + } + + /** + * Triggers the shutdown of Kohana by closing the output buffer, runs the system.display event. + * + * @return void + */ + public static function shutdown() + { + // Close output buffers + self::close_buffers(TRUE); + + // Run the output event + Event::run('system.display', self::$output); + + // Render the final output + self::render(self::$output); + } + + /** + * Inserts global Kohana variables into the generated output and prints it. + * + * @param string final output that will displayed + * @return void + */ + public static function render($output) + { + // Fetch memory usage in MB + $memory = function_exists('memory_get_usage') ? (memory_get_usage() / 1024 / 1024) : 0; + + // Fetch benchmark for page execution time + $benchmark = Benchmark::get(SYSTEM_BENCHMARK.'_total_execution'); + + if (Kohana::config('core.render_stats') === TRUE) + { + // Replace the global template variables + $output = str_replace( + array + ( + '{kohana_version}', + '{kohana_codename}', + '{execution_time}', + '{memory_usage}', + '{included_files}', + ), + array + ( + KOHANA_VERSION, + KOHANA_CODENAME, + $benchmark['time'], + number_format($memory, 2).'MB', + count(get_included_files()), + ), + $output + ); + } + + if ($level = Kohana::config('core.output_compression') AND ini_get('output_handler') !== 'ob_gzhandler' AND (int) ini_get('zlib.output_compression') === 0) + { + if ($level < 1 OR $level > 9) + { + // Normalize the level to be an integer between 1 and 9. This + // step must be done to prevent gzencode from triggering an error + $level = max(1, min($level, 9)); + } + + if (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) + { + $compress = 'gzip'; + } + elseif (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== FALSE) + { + $compress = 'deflate'; + } + } + + if (isset($compress) AND $level > 0) + { + switch ($compress) + { + case 'gzip': + // Compress output using gzip + $output = gzencode($output, $level); + break; + case 'deflate': + // Compress output using zlib (HTTP deflate) + $output = gzdeflate($output, $level); + break; + } + + // This header must be sent with compressed content to prevent + // browser caches from breaking + header('Vary: Accept-Encoding'); + + // Send the content encoding header + header('Content-Encoding: '.$compress); + + // Sending Content-Length in CGI can result in unexpected behavior + if (stripos(PHP_SAPI, 'cgi') === FALSE) + { + header('Content-Length: '.strlen($output)); + } + } + + echo $output; + } + + /** + * Displays a 404 page. + * + * @throws Kohana_404_Exception + * @param string URI of page + * @param string custom template + * @return void + */ + public static function show_404($page = FALSE, $template = FALSE) + { + throw new Kohana_404_Exception($page, $template); + } + + /** + * Dual-purpose PHP error and exception handler. Uses the kohana_error_page + * view to display the message. + * + * @param integer|object exception object or error code + * @param string error message + * @param string filename + * @param integer line number + * @return void + */ + public static function exception_handler($exception, $message = NULL, $file = NULL, $line = NULL) + { + // PHP errors have 5 args, always + $PHP_ERROR = (func_num_args() === 5); + + // Test to see if errors should be displayed + if ($PHP_ERROR AND (error_reporting() & $exception) === 0) + return; + + // This is useful for hooks to determine if a page has an error + self::$has_error = TRUE; + + // Error handling will use exactly 5 args, every time + if ($PHP_ERROR) + { + $code = $exception; + $type = 'PHP Error'; + $template = 'kohana_error_page'; + } + else + { + $code = $exception->getCode(); + $type = get_class($exception); + $message = $exception->getMessage(); + $file = $exception->getFile(); + $line = $exception->getLine(); + $template = ($exception instanceof Kohana_Exception) ? $exception->getTemplate() : 'kohana_error_page'; + } + + if (is_numeric($code)) + { + $codes = self::lang('errors'); + + if ( ! empty($codes[$code])) + { + list($level, $error, $description) = $codes[$code]; + } + else + { + $level = 1; + $error = $PHP_ERROR ? 'Unknown Error' : get_class($exception); + $description = ''; + } + } + else + { + // Custom error message, this will never be logged + $level = 5; + $error = $code; + $description = ''; + } + + // Remove the DOCROOT from the path, as a security precaution + $file = str_replace('\\', '/', realpath($file)); + $file = preg_replace('|^'.preg_quote(DOCROOT).'|', '', $file); + + if ($level >= self::$configuration['core']['log_threshold']) + { + // Log the error + self::log('error', self::lang('core.uncaught_exception', $type, $message, $file, $line)); + } + + if ($PHP_ERROR) + { + $description = self::lang('errors.'.E_RECOVERABLE_ERROR); + $description = is_array($description) ? $description[2] : ''; + + if ( ! headers_sent()) + { + // Send the 500 header + header('HTTP/1.1 500 Internal Server Error'); + } + } + else + { + if (method_exists($exception, 'sendHeaders') AND ! headers_sent()) + { + // Send the headers if they have not already been sent + $exception->sendHeaders(); + } + } + + while (ob_get_level() > self::$buffer_level) + { + // Close open buffers + ob_end_clean(); + } + + // Test if display_errors is on + if (self::$configuration['core']['display_errors'] === TRUE) + { + if ( ! IN_PRODUCTION AND $line != FALSE) + { + // Remove the first entry of debug_backtrace(), it is the exception_handler call + $trace = $PHP_ERROR ? array_slice(debug_backtrace(), 1) : $exception->getTrace(); + + // Beautify backtrace + $trace = self::backtrace($trace); + } + + // Load the error + require self::find_file('views', empty($template) ? 'kohana_error_page' : $template); + } + else + { + // Get the i18n messages + $error = self::lang('core.generic_error'); + $message = self::lang('core.errors_disabled', url::site(), url::site(Router::$current_uri)); + + // Load the errors_disabled view + require self::find_file('views', 'kohana_error_disabled'); + } + + if ( ! Event::has_run('system.shutdown')) + { + // Run the shutdown even to ensure a clean exit + Event::run('system.shutdown'); + } + + // Turn off error reporting + error_reporting(0); + exit; + } + + /** + * Provides class auto-loading. + * + * @throws Kohana_Exception + * @param string name of class + * @return bool + */ + public static function auto_load($class) + { + if (class_exists($class, FALSE)) + return TRUE; + + if (($suffix = strrpos($class, '_')) > 0) + { + // Find the class suffix + $suffix = substr($class, $suffix + 1); + } + else + { + // No suffix + $suffix = FALSE; + } + + if ($suffix === 'Core') + { + $type = 'libraries'; + $file = substr($class, 0, -5); + } + elseif ($suffix === 'Controller') + { + $type = 'controllers'; + // Lowercase filename + $file = strtolower(substr($class, 0, -11)); + } + elseif ($suffix === 'Model') + { + $type = 'models'; + // Lowercase filename + $file = strtolower(substr($class, 0, -6)); + } + elseif ($suffix === 'Driver') + { + $type = 'libraries/drivers'; + $file = str_replace('_', '/', substr($class, 0, -7)); + } + else + { + // This could be either a library or a helper, but libraries must + // always be capitalized, so we check if the first character is + // uppercase. If it is, we are loading a library, not a helper. + $type = ($class[0] < 'a') ? 'libraries' : 'helpers'; + $file = $class; + } + + if (($filepath = self::find_file($type, $file)) === FALSE) + return FALSE; + + // Load the file + require $filepath; + + if ($type === 'libraries' OR $type === 'helpers') + { + if ($extension = self::find_file($type, self::$configuration['core']['extension_prefix'].$class)) + { + // Load the extension + require $extension; + } + elseif ($suffix !== 'Core' AND class_exists($class.'_Core', FALSE)) + { + // Class extension to be evaluated + $extension = 'class '.$class.' extends '.$class.'_Core { }'; + + // Start class analysis + $core = new ReflectionClass($class.'_Core'); + + if ($core->isAbstract()) + { + // Make the extension abstract + $extension = 'abstract '.$extension; + } + + // Transparent class extensions are handled using eval. This is + // a disgusting hack, but it gets the job done. + eval($extension); + } + } + + return TRUE; + } + + /** + * Find a resource file in a given directory. Files will be located according + * to the order of the include paths. Configuration and i18n files will be + * returned in reverse order. + * + * @throws Kohana_Exception if file is required and not found + * @param string directory to search in + * @param string filename to look for (including extension only if 4th parameter is TRUE) + * @param boolean file required + * @param string file extension + * @return array if the type is config, i18n or l10n + * @return string if the file is found + * @return FALSE if the file is not found + */ + public static function find_file($directory, $filename, $required = FALSE, $ext = FALSE) + { + // NOTE: This test MUST be not be a strict comparison (===), or empty + // extensions will be allowed! + if ($ext == '') + { + // Use the default extension + $ext = EXT; + } + else + { + // Add a period before the extension + $ext = '.'.$ext; + } + + // Search path + $search = $directory.'/'.$filename.$ext; + + if (isset(self::$internal_cache['find_file_paths'][$search])) + return self::$internal_cache['find_file_paths'][$search]; + + // Load include paths + $paths = self::$include_paths; + + // Nothing found, yet + $found = NULL; + + if ($directory === 'config' OR $directory === 'i18n') + { + // Search in reverse, for merging + $paths = array_reverse($paths); + + foreach ($paths as $path) + { + if (is_file($path.$search)) + { + // A matching file has been found + $found[] = $path.$search; + } + } + } + else + { + foreach ($paths as $path) + { + if (is_file($path.$search)) + { + // A matching file has been found + $found = $path.$search; + + // Stop searching + break; + } + } + } + + if ($found === NULL) + { + if ($required === TRUE) + { + // Directory i18n key + $directory = 'core.'.inflector::singular($directory); + + // If the file is required, throw an exception + throw new Kohana_Exception('core.resource_not_found', self::lang($directory), $filename); + } + else + { + // Nothing was found, return FALSE + $found = FALSE; + } + } + + if ( ! isset(self::$write_cache['find_file_paths'])) + { + // Write cache at shutdown + self::$write_cache['find_file_paths'] = TRUE; + } + + return self::$internal_cache['find_file_paths'][$search] = $found; + } + + /** + * Lists all files and directories in a resource path. + * + * @param string directory to search + * @param boolean list all files to the maximum depth? + * @param string full path to search (used for recursion, *never* set this manually) + * @return array filenames and directories + */ + public static function list_files($directory, $recursive = FALSE, $path = FALSE) + { + $files = array(); + + if ($path === FALSE) + { + $paths = array_reverse(Kohana::include_paths()); + + foreach ($paths as $path) + { + // Recursively get and merge all files + $files = array_merge($files, self::list_files($directory, $recursive, $path.$directory)); + } + } + else + { + $path = rtrim($path, '/').'/'; + + if (is_readable($path)) + { + $items = (array) glob($path.'*'); + + foreach ($items as $index => $item) + { + $files[] = $item = str_replace('\\', '/', $item); + + // Handle recursion + if (is_dir($item) AND $recursive == TRUE) + { + // Filename should only be the basename + $item = pathinfo($item, PATHINFO_BASENAME); + + // Append sub-directory search + $files = array_merge($files, self::list_files($directory, TRUE, $path.$item)); + } + } + } + } + + return $files; + } + + /** + * Fetch an i18n language item. + * + * @param string language key to fetch + * @param array additional information to insert into the line + * @return string i18n language string, or the requested key if the i18n item is not found + */ + public static function lang($key, $args = array()) + { + // Extract the main group from the key + $group = explode('.', $key, 2); + $group = $group[0]; + + // Get locale name + $locale = Kohana::config('locale.language.0'); + + if ( ! isset(self::$internal_cache['language'][$locale][$group])) + { + // Messages for this group + $messages = array(); + + if ($files = self::find_file('i18n', $locale.'/'.$group)) + { + foreach ($files as $file) + { + include $file; + + // Merge in configuration + if ( ! empty($lang) AND is_array($lang)) + { + foreach ($lang as $k => $v) + { + $messages[$k] = $v; + } + } + } + } + + if ( ! isset(self::$write_cache['language'])) + { + // Write language cache + self::$write_cache['language'] = TRUE; + } + + self::$internal_cache['language'][$locale][$group] = $messages; + } + + // Get the line from cache + $line = self::key_string(self::$internal_cache['language'][$locale], $key); + + if ($line === NULL) + { + Kohana::log('error', 'Missing i18n entry '.$key.' for language '.$locale); + + // Return the key string as fallback + return $key; + } + + if (is_string($line) AND func_num_args() > 1) + { + $args = array_slice(func_get_args(), 1); + + // Add the arguments into the line + $line = vsprintf($line, is_array($args[0]) ? $args[0] : $args); + } + + return $line; + } + + /** + * Returns the value of a key, defined by a 'dot-noted' string, from an array. + * + * @param array array to search + * @param string dot-noted string: foo.bar.baz + * @return string if the key is found + * @return void if the key is not found + */ + public static function key_string($array, $keys) + { + if (empty($array)) + return NULL; + + // Prepare for loop + $keys = explode('.', $keys); + + do + { + // Get the next key + $key = array_shift($keys); + + if (isset($array[$key])) + { + if (is_array($array[$key]) AND ! empty($keys)) + { + // Dig down to prepare the next loop + $array = $array[$key]; + } + else + { + // Requested key was found + return $array[$key]; + } + } + else + { + // Requested key is not set + break; + } + } + while ( ! empty($keys)); + + return NULL; + } + + /** + * Sets values in an array by using a 'dot-noted' string. + * + * @param array array to set keys in (reference) + * @param string dot-noted string: foo.bar.baz + * @return mixed fill value for the key + * @return void + */ + public static function key_string_set( & $array, $keys, $fill = NULL) + { + if (is_object($array) AND ($array instanceof ArrayObject)) + { + // Copy the array + $array_copy = $array->getArrayCopy(); + + // Is an object + $array_object = TRUE; + } + else + { + if ( ! is_array($array)) + { + // Must always be an array + $array = (array) $array; + } + + // Copy is a reference to the array + $array_copy =& $array; + } + + if (empty($keys)) + return $array; + + // Create keys + $keys = explode('.', $keys); + + // Create reference to the array + $row =& $array_copy; + + for ($i = 0, $end = count($keys) - 1; $i <= $end; $i++) + { + // Get the current key + $key = $keys[$i]; + + if ( ! isset($row[$key])) + { + if (isset($keys[$i + 1])) + { + // Make the value an array + $row[$key] = array(); + } + else + { + // Add the fill key + $row[$key] = $fill; + } + } + elseif (isset($keys[$i + 1])) + { + // Make the value an array + $row[$key] = (array) $row[$key]; + } + + // Go down a level, creating a new row reference + $row =& $row[$key]; + } + + if (isset($array_object)) + { + // Swap the array back in + $array->exchangeArray($array_copy); + } + } + + /** + * Retrieves current user agent information: + * keys: browser, version, platform, mobile, robot, referrer, languages, charsets + * tests: is_browser, is_mobile, is_robot, accept_lang, accept_charset + * + * @param string key or test name + * @param string used with "accept" tests: user_agent(accept_lang, en) + * @return array languages and charsets + * @return string all other keys + * @return boolean all tests + */ + public static function user_agent($key = 'agent', $compare = NULL) + { + static $info; + + // Return the raw string + if ($key === 'agent') + return Kohana::$user_agent; + + if ($info === NULL) + { + // Parse the user agent and extract basic information + $agents = Kohana::config('user_agents'); + + foreach ($agents as $type => $data) + { + foreach ($data as $agent => $name) + { + if (stripos(Kohana::$user_agent, $agent) !== FALSE) + { + if ($type === 'browser' AND preg_match('|'.preg_quote($agent).'[^0-9.]*+([0-9.][0-9.a-z]*)|i', Kohana::$user_agent, $match)) + { + // Set the browser version + $info['version'] = $match[1]; + } + + // Set the agent name + $info[$type] = $name; + break; + } + } + } + } + + if (empty($info[$key])) + { + switch ($key) + { + case 'is_robot': + case 'is_browser': + case 'is_mobile': + // A boolean result + $return = ! empty($info[substr($key, 3)]); + break; + case 'languages': + $return = array(); + if ( ! empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) + { + if (preg_match_all('/[-a-z]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])), $matches)) + { + // Found a result + $return = $matches[0]; + } + } + break; + case 'charsets': + $return = array(); + if ( ! empty($_SERVER['HTTP_ACCEPT_CHARSET'])) + { + if (preg_match_all('/[-a-z0-9]{2,}/', strtolower(trim($_SERVER['HTTP_ACCEPT_CHARSET'])), $matches)) + { + // Found a result + $return = $matches[0]; + } + } + break; + case 'referrer': + if ( ! empty($_SERVER['HTTP_REFERER'])) + { + // Found a result + $return = trim($_SERVER['HTTP_REFERER']); + } + break; + } + + // Cache the return value + isset($return) and $info[$key] = $return; + } + + if ( ! empty($compare)) + { + // The comparison must always be lowercase + $compare = strtolower($compare); + + switch ($key) + { + case 'accept_lang': + // Check if the lange is accepted + return in_array($compare, Kohana::user_agent('languages')); + break; + case 'accept_charset': + // Check if the charset is accepted + return in_array($compare, Kohana::user_agent('charsets')); + break; + default: + // Invalid comparison + return FALSE; + break; + } + } + + // Return the key, if set + return isset($info[$key]) ? $info[$key] : NULL; + } + + /** + * Quick debugging of any variable. Any number of parameters can be set. + * + * @return string + */ + public static function debug() + { + if (func_num_args() === 0) + return; + + // Get params + $params = func_get_args(); + $output = array(); + + foreach ($params as $var) + { + $output[] = '<pre>('.gettype($var).') '.html::specialchars(print_r($var, TRUE)).'</pre>'; + } + + return implode("\n", $output); + } + + /** + * Displays nice backtrace information. + * @see http://php.net/debug_backtrace + * + * @param array backtrace generated by an exception or debug_backtrace + * @return string + */ + public static function backtrace($trace) + { + if ( ! is_array($trace)) + return; + + // Final output + $output = array(); + + foreach ($trace as $entry) + { + $temp = '<li>'; + + if (isset($entry['file'])) + { + $temp .= Kohana::lang('core.error_file_line', preg_replace('!^'.preg_quote(DOCROOT).'!', '', $entry['file']), $entry['line']); + } + + $temp .= '<pre>'; + + if (isset($entry['class'])) + { + // Add class and call type + $temp .= $entry['class'].$entry['type']; + } + + // Add function + $temp .= $entry['function'].'( '; + + // Add function args + if (isset($entry['args']) AND is_array($entry['args'])) + { + // Separator starts as nothing + $sep = ''; + + while ($arg = array_shift($entry['args'])) + { + if (is_string($arg) AND is_file($arg)) + { + // Remove docroot from filename + $arg = preg_replace('!^'.preg_quote(DOCROOT).'!', '', $arg); + } + + $temp .= $sep.html::specialchars(print_r($arg, TRUE)); + + // Change separator to a comma + $sep = ', '; + } + } + + $temp .= ' )</pre></li>'; + + $output[] = $temp; + } + + return '<ul class="backtrace">'.implode("\n", $output).'</ul>'; + } + + /** + * Saves the internal caches: configuration, include paths, etc. + * + * @return boolean + */ + public static function internal_cache_save() + { + if ( ! is_array(self::$write_cache)) + return FALSE; + + // Get internal cache names + $caches = array_keys(self::$write_cache); + + // Nothing written + $written = FALSE; + + foreach ($caches as $cache) + { + if (isset(self::$internal_cache[$cache])) + { + // Write the cache file + self::cache_save($cache, self::$internal_cache[$cache], self::$configuration['core']['internal_cache']); + + // A cache has been written + $written = TRUE; + } + } + + return $written; + } + +} // End Kohana + +/** + * Creates a generic i18n exception. + */ +class Kohana_Exception extends Exception { + + // Template file + protected $template = 'kohana_error_page'; + + // Header + protected $header = FALSE; + + // Error code + protected $code = E_KOHANA; + + /** + * Set exception message. + * + * @param string i18n language key for the message + * @param array addition line parameters + */ + public function __construct($error) + { + $args = array_slice(func_get_args(), 1); + + // Fetch the error message + $message = Kohana::lang($error, $args); + + if ($message === $error OR empty($message)) + { + // Unable to locate the message for the error + $message = 'Unknown Exception: '.$error; + } + + // Sets $this->message the proper way + parent::__construct($message); + } + + /** + * Magic method for converting an object to a string. + * + * @return string i18n message + */ + public function __toString() + { + return (string) $this->message; + } + + /** + * Fetch the template name. + * + * @return string + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Sends an Internal Server Error header. + * + * @return void + */ + public function sendHeaders() + { + // Send the 500 header + header('HTTP/1.1 500 Internal Server Error'); + } + +} // End Kohana Exception + +/** + * Creates a custom exception. + */ +class Kohana_User_Exception extends Kohana_Exception { + + /** + * Set exception title and message. + * + * @param string exception title string + * @param string exception message string + * @param string custom error template + */ + public function __construct($title, $message, $template = FALSE) + { + Exception::__construct($message); + + $this->code = $title; + + if ($template !== FALSE) + { + $this->template = $template; + } + } + +} // End Kohana PHP Exception + +/** + * Creates a Page Not Found exception. + */ +class Kohana_404_Exception extends Kohana_Exception { + + protected $code = E_PAGE_NOT_FOUND; + + /** + * Set internal properties. + * + * @param string URL of page + * @param string custom error template + */ + public function __construct($page = FALSE, $template = FALSE) + { + if ($page === FALSE) + { + // Construct the page URI using Router properties + $page = Router::$current_uri.Router::$url_suffix.Router::$query_string; + } + + Exception::__construct(Kohana::lang('core.page_not_found', $page)); + + $this->template = $template; + } + + /** + * Sends "File Not Found" headers, to emulate server behavior. + * + * @return void + */ + public function sendHeaders() + { + // Send the 404 header + header('HTTP/1.1 404 File Not Found'); + } + +} // End Kohana 404 Exception diff --git a/kohana/core/utf8.php b/kohana/core/utf8.php new file mode 100644 index 00000000..e102da4e --- /dev/null +++ b/kohana/core/utf8.php @@ -0,0 +1,743 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * A port of phputf8 to a unified file/class. Checks PHP status to ensure that + * UTF-8 support is available and normalize global variables to UTF-8. It also + * provides multi-byte aware replacement string functions. + * + * This file is licensed differently from the rest of Kohana. As a port of + * phputf8, which is LGPL software, this file is released under the LGPL. + * + * PCRE needs to be compiled with UTF-8 support (--enable-utf8). + * Support for Unicode properties is highly recommended (--enable-unicode-properties). + * @see http://php.net/manual/reference.pcre.pattern.modifiers.php + * + * UTF-8 conversion will be much more reliable if the iconv extension is loaded. + * @see http://php.net/iconv + * + * The mbstring extension is highly recommended, but must not be overloading + * string functions. + * @see http://php.net/mbstring + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ + +if ( ! preg_match('/^.$/u', 'ñ')) +{ + trigger_error + ( + '<a href="http://php.net/pcre">PCRE</a> has not been compiled with UTF-8 support. '. + 'See <a href="http://php.net/manual/reference.pcre.pattern.modifiers.php">PCRE Pattern Modifiers</a> '. + 'for more information. This application cannot be run without UTF-8 support.', + E_USER_ERROR + ); +} + +if ( ! extension_loaded('iconv')) +{ + trigger_error + ( + 'The <a href="http://php.net/iconv">iconv</a> extension is not loaded. '. + 'Without iconv, strings cannot be properly translated to UTF-8 from user input. '. + 'This application cannot be run without UTF-8 support.', + E_USER_ERROR + ); +} + +if (extension_loaded('mbstring') AND (ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING)) +{ + trigger_error + ( + 'The <a href="http://php.net/mbstring">mbstring</a> extension is overloading PHP\'s native string functions. '. + 'Disable this by setting mbstring.func_overload to 0, 1, 4 or 5 in php.ini or a .htaccess file.'. + 'This application cannot be run without UTF-8 support.', + E_USER_ERROR + ); +} + +// Check PCRE support for Unicode properties such as \p and \X. +$ER = error_reporting(0); +define('PCRE_UNICODE_PROPERTIES', (bool) preg_match('/^\pL$/u', 'ñ')); +error_reporting($ER); + +// SERVER_UTF8 ? use mb_* functions : use non-native functions +if (extension_loaded('mbstring')) +{ + mb_internal_encoding('UTF-8'); + define('SERVER_UTF8', TRUE); +} +else +{ + define('SERVER_UTF8', FALSE); +} + +// Convert all global variables to UTF-8. +$_GET = utf8::clean($_GET); +$_POST = utf8::clean($_POST); +$_COOKIE = utf8::clean($_COOKIE); +$_SERVER = utf8::clean($_SERVER); + +if (PHP_SAPI == 'cli') +{ + // Convert command line arguments + $_SERVER['argv'] = utf8::clean($_SERVER['argv']); +} + +final class utf8 { + + // Called methods + static $called = array(); + + /** + * Recursively cleans arrays, objects, and strings. Removes ASCII control + * codes and converts to UTF-8 while silently discarding incompatible + * UTF-8 characters. + * + * @param string string to clean + * @return string + */ + public static function clean($str) + { + if (is_array($str) OR is_object($str)) + { + foreach ($str as $key => $val) + { + // Recursion! + $str[self::clean($key)] = self::clean($val); + } + } + elseif (is_string($str) AND $str !== '') + { + // Remove control characters + $str = self::strip_ascii_ctrl($str); + + if ( ! self::is_ascii($str)) + { + // Disable notices + $ER = error_reporting(~E_NOTICE); + + // iconv is expensive, so it is only used when needed + $str = iconv('UTF-8', 'UTF-8//IGNORE', $str); + + // Turn notices back on + error_reporting($ER); + } + } + + return $str; + } + + /** + * Tests whether a string contains only 7bit ASCII bytes. This is used to + * determine when to use native functions or UTF-8 functions. + * + * @param string string to check + * @return bool + */ + public static function is_ascii($str) + { + return ! preg_match('/[^\x00-\x7F]/S', $str); + } + + /** + * Strips out device control codes in the ASCII range. + * + * @param string string to clean + * @return string + */ + public static function strip_ascii_ctrl($str) + { + return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S', '', $str); + } + + /** + * Strips out all non-7bit ASCII bytes. + * + * @param string string to clean + * @return string + */ + public static function strip_non_ascii($str) + { + return preg_replace('/[^\x00-\x7F]+/S', '', $str); + } + + /** + * Replaces special/accented UTF-8 characters by ASCII-7 'equivalents'. + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string string to transliterate + * @param integer -1 lowercase only, +1 uppercase only, 0 both cases + * @return string + */ + public static function transliterate_to_ascii($str, $case = 0) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _transliterate_to_ascii($str, $case); + } + + /** + * Returns the length of the given string. + * @see http://php.net/strlen + * + * @param string string being measured for length + * @return integer + */ + public static function strlen($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strlen($str); + } + + /** + * Finds position of first occurrence of a UTF-8 string. + * @see http://php.net/strlen + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string haystack + * @param string needle + * @param integer offset from which character in haystack to start searching + * @return integer position of needle + * @return boolean FALSE if the needle is not found + */ + public static function strpos($str, $search, $offset = 0) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strpos($str, $search, $offset); + } + + /** + * Finds position of last occurrence of a char in a UTF-8 string. + * @see http://php.net/strrpos + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string haystack + * @param string needle + * @param integer offset from which character in haystack to start searching + * @return integer position of needle + * @return boolean FALSE if the needle is not found + */ + public static function strrpos($str, $search, $offset = 0) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strrpos($str, $search, $offset); + } + + /** + * Returns part of a UTF-8 string. + * @see http://php.net/substr + * + * @author Chris Smith <chris@jalakai.co.uk> + * + * @param string input string + * @param integer offset + * @param integer length limit + * @return string + */ + public static function substr($str, $offset, $length = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _substr($str, $offset, $length); + } + + /** + * Replaces text within a portion of a UTF-8 string. + * @see http://php.net/substr_replace + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string input string + * @param string replacement string + * @param integer offset + * @return string + */ + public static function substr_replace($str, $replacement, $offset, $length = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _substr_replace($str, $replacement, $offset, $length); + } + + /** + * Makes a UTF-8 string lowercase. + * @see http://php.net/strtolower + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string mixed case string + * @return string + */ + public static function strtolower($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strtolower($str); + } + + /** + * Makes a UTF-8 string uppercase. + * @see http://php.net/strtoupper + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string mixed case string + * @return string + */ + public static function strtoupper($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strtoupper($str); + } + + /** + * Makes a UTF-8 string's first character uppercase. + * @see http://php.net/ucfirst + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string mixed case string + * @return string + */ + public static function ucfirst($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ucfirst($str); + } + + /** + * Makes the first character of every word in a UTF-8 string uppercase. + * @see http://php.net/ucwords + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string mixed case string + * @return string + */ + public static function ucwords($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ucwords($str); + } + + /** + * Case-insensitive UTF-8 string comparison. + * @see http://php.net/strcasecmp + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string string to compare + * @param string string to compare + * @return integer less than 0 if str1 is less than str2 + * @return integer greater than 0 if str1 is greater than str2 + * @return integer 0 if they are equal + */ + public static function strcasecmp($str1, $str2) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strcasecmp($str1, $str2); + } + + /** + * Returns a string or an array with all occurrences of search in subject (ignoring case). + * replaced with the given replace value. + * @see http://php.net/str_ireplace + * + * @note It's not fast and gets slower if $search and/or $replace are arrays. + * @author Harry Fuecks <hfuecks@gmail.com + * + * @param string|array text to replace + * @param string|array replacement text + * @param string|array subject text + * @param integer number of matched and replaced needles will be returned via this parameter which is passed by reference + * @return string if the input was a string + * @return array if the input was an array + */ + public static function str_ireplace($search, $replace, $str, & $count = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _str_ireplace($search, $replace, $str, $count); + } + + /** + * Case-insenstive UTF-8 version of strstr. Returns all of input string + * from the first occurrence of needle to the end. + * @see http://php.net/stristr + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string input string + * @param string needle + * @return string matched substring if found + * @return boolean FALSE if the substring was not found + */ + public static function stristr($str, $search) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _stristr($str, $search); + } + + /** + * Finds the length of the initial segment matching mask. + * @see http://php.net/strspn + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string input string + * @param string mask for search + * @param integer start position of the string to examine + * @param integer length of the string to examine + * @return integer length of the initial segment that contains characters in the mask + */ + public static function strspn($str, $mask, $offset = NULL, $length = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strspn($str, $mask, $offset, $length); + } + + /** + * Finds the length of the initial segment not matching mask. + * @see http://php.net/strcspn + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string input string + * @param string mask for search + * @param integer start position of the string to examine + * @param integer length of the string to examine + * @return integer length of the initial segment that contains characters not in the mask + */ + public static function strcspn($str, $mask, $offset = NULL, $length = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strcspn($str, $mask, $offset, $length); + } + + /** + * Pads a UTF-8 string to a certain length with another string. + * @see http://php.net/str_pad + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string input string + * @param integer desired string length after padding + * @param string string to use as padding + * @param string padding type: STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH + * @return string + */ + public static function str_pad($str, $final_str_length, $pad_str = ' ', $pad_type = STR_PAD_RIGHT) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _str_pad($str, $final_str_length, $pad_str, $pad_type); + } + + /** + * Converts a UTF-8 string to an array. + * @see http://php.net/str_split + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string input string + * @param integer maximum length of each chunk + * @return array + */ + public static function str_split($str, $split_length = 1) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _str_split($str, $split_length); + } + + /** + * Reverses a UTF-8 string. + * @see http://php.net/strrev + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string string to be reversed + * @return string + */ + public static function strrev($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strrev($str); + } + + /** + * Strips whitespace (or other UTF-8 characters) from the beginning and + * end of a string. + * @see http://php.net/trim + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string input string + * @param string string of characters to remove + * @return string + */ + public static function trim($str, $charlist = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _trim($str, $charlist); + } + + /** + * Strips whitespace (or other UTF-8 characters) from the beginning of a string. + * @see http://php.net/ltrim + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string input string + * @param string string of characters to remove + * @return string + */ + public static function ltrim($str, $charlist = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ltrim($str, $charlist); + } + + /** + * Strips whitespace (or other UTF-8 characters) from the end of a string. + * @see http://php.net/rtrim + * + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string input string + * @param string string of characters to remove + * @return string + */ + public static function rtrim($str, $charlist = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _rtrim($str, $charlist); + } + + /** + * Returns the unicode ordinal for a character. + * @see http://php.net/ord + * + * @author Harry Fuecks <hfuecks@gmail.com> + * + * @param string UTF-8 encoded character + * @return integer + */ + public static function ord($chr) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ord($chr); + } + + /** + * Takes an UTF-8 string and returns an array of ints representing the Unicode characters. + * Astral planes are supported i.e. the ints in the output can be > 0xFFFF. + * Occurrances of the BOM are ignored. Surrogates are not allowed. + * + * The Original Code is Mozilla Communicator client code. + * The Initial Developer of the Original Code is Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 the Initial Developer. + * Ported to PHP by Henri Sivonen <hsivonen@iki.fi>, see http://hsivonen.iki.fi/php-utf8/. + * Slight modifications to fit with phputf8 library by Harry Fuecks <hfuecks@gmail.com>. + * + * @param string UTF-8 encoded string + * @return array unicode code points + * @return boolean FALSE if the string is invalid + */ + public static function to_unicode($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _to_unicode($str); + } + + /** + * Takes an array of ints representing the Unicode characters and returns a UTF-8 string. + * Astral planes are supported i.e. the ints in the input can be > 0xFFFF. + * Occurrances of the BOM are ignored. Surrogates are not allowed. + * + * The Original Code is Mozilla Communicator client code. + * The Initial Developer of the Original Code is Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 the Initial Developer. + * Ported to PHP by Henri Sivonen <hsivonen@iki.fi>, see http://hsivonen.iki.fi/php-utf8/. + * Slight modifications to fit with phputf8 library by Harry Fuecks <hfuecks@gmail.com>. + * + * @param array unicode code points representing a string + * @return string utf8 string of characters + * @return boolean FALSE if a code point cannot be found + */ + public static function from_unicode($arr) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'core/utf8/'.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _from_unicode($arr); + } + +} // End utf8
\ No newline at end of file diff --git a/kohana/core/utf8/from_unicode.php b/kohana/core/utf8/from_unicode.php new file mode 100644 index 00000000..49e25ddf --- /dev/null +++ b/kohana/core/utf8/from_unicode.php @@ -0,0 +1,68 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::from_unicode + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _from_unicode($arr) +{ + ob_start(); + + $keys = array_keys($arr); + + foreach ($keys as $k) + { + // ASCII range (including control chars) + if (($arr[$k] >= 0) AND ($arr[$k] <= 0x007f)) + { + echo chr($arr[$k]); + } + // 2 byte sequence + elseif ($arr[$k] <= 0x07ff) + { + echo chr(0xc0 | ($arr[$k] >> 6)); + echo chr(0x80 | ($arr[$k] & 0x003f)); + } + // Byte order mark (skip) + elseif ($arr[$k] == 0xFEFF) + { + // nop -- zap the BOM + } + // Test for illegal surrogates + elseif ($arr[$k] >= 0xD800 AND $arr[$k] <= 0xDFFF) + { + // Found a surrogate + trigger_error('utf8::from_unicode: Illegal surrogate at index: '.$k.', value: '.$arr[$k], E_USER_WARNING); + return FALSE; + } + // 3 byte sequence + elseif ($arr[$k] <= 0xffff) + { + echo chr(0xe0 | ($arr[$k] >> 12)); + echo chr(0x80 | (($arr[$k] >> 6) & 0x003f)); + echo chr(0x80 | ($arr[$k] & 0x003f)); + } + // 4 byte sequence + elseif ($arr[$k] <= 0x10ffff) + { + echo chr(0xf0 | ($arr[$k] >> 18)); + echo chr(0x80 | (($arr[$k] >> 12) & 0x3f)); + echo chr(0x80 | (($arr[$k] >> 6) & 0x3f)); + echo chr(0x80 | ($arr[$k] & 0x3f)); + } + // Out of range + else + { + trigger_error('utf8::from_unicode: Codepoint out of Unicode range at index: '.$k.', value: '.$arr[$k], E_USER_WARNING); + return FALSE; + } + } + + $result = ob_get_contents(); + ob_end_clean(); + return $result; +} diff --git a/kohana/core/utf8/ltrim.php b/kohana/core/utf8/ltrim.php new file mode 100644 index 00000000..45297342 --- /dev/null +++ b/kohana/core/utf8/ltrim.php @@ -0,0 +1,22 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::ltrim + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _ltrim($str, $charlist = NULL) +{ + if ($charlist === NULL) + return ltrim($str); + + if (utf8::is_ascii($charlist)) + return ltrim($str, $charlist); + + $charlist = preg_replace('#[-\[\]:\\\\^/]#', '\\\\$0', $charlist); + + return preg_replace('/^['.$charlist.']+/u', '', $str); +}
\ No newline at end of file diff --git a/kohana/core/utf8/ord.php b/kohana/core/utf8/ord.php new file mode 100644 index 00000000..c21288a6 --- /dev/null +++ b/kohana/core/utf8/ord.php @@ -0,0 +1,88 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::ord + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _ord($chr) +{ + $ord0 = ord($chr); + + if ($ord0 >= 0 AND $ord0 <= 127) + { + return $ord0; + } + + if ( ! isset($chr[1])) + { + trigger_error('Short sequence - at least 2 bytes expected, only 1 seen', E_USER_WARNING); + return FALSE; + } + + $ord1 = ord($chr[1]); + + if ($ord0 >= 192 AND $ord0 <= 223) + { + return ($ord0 - 192) * 64 + ($ord1 - 128); + } + + if ( ! isset($chr[2])) + { + trigger_error('Short sequence - at least 3 bytes expected, only 2 seen', E_USER_WARNING); + return FALSE; + } + + $ord2 = ord($chr[2]); + + if ($ord0 >= 224 AND $ord0 <= 239) + { + return ($ord0 - 224) * 4096 + ($ord1 - 128) * 64 + ($ord2 - 128); + } + + if ( ! isset($chr[3])) + { + trigger_error('Short sequence - at least 4 bytes expected, only 3 seen', E_USER_WARNING); + return FALSE; + } + + $ord3 = ord($chr[3]); + + if ($ord0 >= 240 AND $ord0 <= 247) + { + return ($ord0 - 240) * 262144 + ($ord1 - 128) * 4096 + ($ord2-128) * 64 + ($ord3 - 128); + } + + if ( ! isset($chr[4])) + { + trigger_error('Short sequence - at least 5 bytes expected, only 4 seen', E_USER_WARNING); + return FALSE; + } + + $ord4 = ord($chr[4]); + + if ($ord0 >= 248 AND $ord0 <= 251) + { + return ($ord0 - 248) * 16777216 + ($ord1-128) * 262144 + ($ord2 - 128) * 4096 + ($ord3 - 128) * 64 + ($ord4 - 128); + } + + if ( ! isset($chr[5])) + { + trigger_error('Short sequence - at least 6 bytes expected, only 5 seen', E_USER_WARNING); + return FALSE; + } + + if ($ord0 >= 252 AND $ord0 <= 253) + { + return ($ord0 - 252) * 1073741824 + ($ord1 - 128) * 16777216 + ($ord2 - 128) * 262144 + ($ord3 - 128) * 4096 + ($ord4 - 128) * 64 + (ord($c[5]) - 128); + } + + if ($ord0 >= 254 AND $ord0 <= 255) + { + trigger_error('Invalid UTF-8 with surrogate ordinal '.$ord0, E_USER_WARNING); + return FALSE; + } +}
\ No newline at end of file diff --git a/kohana/core/utf8/rtrim.php b/kohana/core/utf8/rtrim.php new file mode 100644 index 00000000..c7571eb2 --- /dev/null +++ b/kohana/core/utf8/rtrim.php @@ -0,0 +1,22 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::rtrim + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _rtrim($str, $charlist = NULL) +{ + if ($charlist === NULL) + return rtrim($str); + + if (utf8::is_ascii($charlist)) + return rtrim($str, $charlist); + + $charlist = preg_replace('#[-\[\]:\\\\^/]#', '\\\\$0', $charlist); + + return preg_replace('/['.$charlist.']++$/uD', '', $str); +}
\ No newline at end of file diff --git a/kohana/core/utf8/str_ireplace.php b/kohana/core/utf8/str_ireplace.php new file mode 100644 index 00000000..e10c707e --- /dev/null +++ b/kohana/core/utf8/str_ireplace.php @@ -0,0 +1,70 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::str_ireplace + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _str_ireplace($search, $replace, $str, & $count = NULL) +{ + if (utf8::is_ascii($search) AND utf8::is_ascii($replace) AND utf8::is_ascii($str)) + return str_ireplace($search, $replace, $str, $count); + + if (is_array($str)) + { + foreach ($str as $key => $val) + { + $str[$key] = utf8::str_ireplace($search, $replace, $val, $count); + } + return $str; + } + + if (is_array($search)) + { + $keys = array_keys($search); + + foreach ($keys as $k) + { + if (is_array($replace)) + { + if (array_key_exists($k, $replace)) + { + $str = utf8::str_ireplace($search[$k], $replace[$k], $str, $count); + } + else + { + $str = utf8::str_ireplace($search[$k], '', $str, $count); + } + } + else + { + $str = utf8::str_ireplace($search[$k], $replace, $str, $count); + } + } + return $str; + } + + $search = utf8::strtolower($search); + $str_lower = utf8::strtolower($str); + + $total_matched_strlen = 0; + $i = 0; + + while (preg_match('/(.*?)'.preg_quote($search, '/').'/s', $str_lower, $matches)) + { + $matched_strlen = strlen($matches[0]); + $str_lower = substr($str_lower, $matched_strlen); + + $offset = $total_matched_strlen + strlen($matches[1]) + ($i * (strlen($replace) - 1)); + $str = substr_replace($str, $replace, $offset, strlen($search)); + + $total_matched_strlen += $matched_strlen; + $i++; + } + + $count += $i; + return $str; +} diff --git a/kohana/core/utf8/str_pad.php b/kohana/core/utf8/str_pad.php new file mode 100644 index 00000000..9b7f391a --- /dev/null +++ b/kohana/core/utf8/str_pad.php @@ -0,0 +1,54 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::str_pad + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _str_pad($str, $final_str_length, $pad_str = ' ', $pad_type = STR_PAD_RIGHT) +{ + if (utf8::is_ascii($str) AND utf8::is_ascii($pad_str)) + { + return str_pad($str, $final_str_length, $pad_str, $pad_type); + } + + $str_length = utf8::strlen($str); + + if ($final_str_length <= 0 OR $final_str_length <= $str_length) + { + return $str; + } + + $pad_str_length = utf8::strlen($pad_str); + $pad_length = $final_str_length - $str_length; + + if ($pad_type == STR_PAD_RIGHT) + { + $repeat = ceil($pad_length / $pad_str_length); + return utf8::substr($str.str_repeat($pad_str, $repeat), 0, $final_str_length); + } + + if ($pad_type == STR_PAD_LEFT) + { + $repeat = ceil($pad_length / $pad_str_length); + return utf8::substr(str_repeat($pad_str, $repeat), 0, floor($pad_length)).$str; + } + + if ($pad_type == STR_PAD_BOTH) + { + $pad_length /= 2; + $pad_length_left = floor($pad_length); + $pad_length_right = ceil($pad_length); + $repeat_left = ceil($pad_length_left / $pad_str_length); + $repeat_right = ceil($pad_length_right / $pad_str_length); + + $pad_left = utf8::substr(str_repeat($pad_str, $repeat_left), 0, $pad_length_left); + $pad_right = utf8::substr(str_repeat($pad_str, $repeat_right), 0, $pad_length_left); + return $pad_left.$str.$pad_right; + } + + trigger_error('utf8::str_pad: Unknown padding type (' . $type . ')', E_USER_ERROR); +}
\ No newline at end of file diff --git a/kohana/core/utf8/str_split.php b/kohana/core/utf8/str_split.php new file mode 100644 index 00000000..128caa94 --- /dev/null +++ b/kohana/core/utf8/str_split.php @@ -0,0 +1,33 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::str_split + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _str_split($str, $split_length = 1) +{ + $split_length = (int) $split_length; + + if (utf8::is_ascii($str)) + { + return str_split($str, $split_length); + } + + if ($split_length < 1) + { + return FALSE; + } + + if (utf8::strlen($str) <= $split_length) + { + return array($str); + } + + preg_match_all('/.{'.$split_length.'}|[^\x00]{1,'.$split_length.'}$/us', $str, $matches); + + return $matches[0]; +}
\ No newline at end of file diff --git a/kohana/core/utf8/strcasecmp.php b/kohana/core/utf8/strcasecmp.php new file mode 100644 index 00000000..c18cf870 --- /dev/null +++ b/kohana/core/utf8/strcasecmp.php @@ -0,0 +1,19 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strcasecmp + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strcasecmp($str1, $str2) +{ + if (utf8::is_ascii($str1) AND utf8::is_ascii($str2)) + return strcasecmp($str1, $str2); + + $str1 = utf8::strtolower($str1); + $str2 = utf8::strtolower($str2); + return strcmp($str1, $str2); +}
\ No newline at end of file diff --git a/kohana/core/utf8/strcspn.php b/kohana/core/utf8/strcspn.php new file mode 100644 index 00000000..ce8460ae --- /dev/null +++ b/kohana/core/utf8/strcspn.php @@ -0,0 +1,30 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strcspn + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strcspn($str, $mask, $offset = NULL, $length = NULL) +{ + if ($str == '' OR $mask == '') + return 0; + + if (utf8::is_ascii($str) AND utf8::is_ascii($mask)) + return ($offset === NULL) ? strcspn($str, $mask) : (($length === NULL) ? strcspn($str, $mask, $offset) : strcspn($str, $mask, $offset, $length)); + + if ($start !== NULL OR $length !== NULL) + { + $str = utf8::substr($str, $offset, $length); + } + + // Escape these characters: - [ ] . : \ ^ / + // The . and : are escaped to prevent possible warnings about POSIX regex elements + $mask = preg_replace('#[-[\].:\\\\^/]#', '\\\\$0', $mask); + preg_match('/^[^'.$mask.']+/u', $str, $matches); + + return isset($matches[0]) ? utf8::strlen($matches[0]) : 0; +}
\ No newline at end of file diff --git a/kohana/core/utf8/stristr.php b/kohana/core/utf8/stristr.php new file mode 100644 index 00000000..b72871cf --- /dev/null +++ b/kohana/core/utf8/stristr.php @@ -0,0 +1,28 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::stristr + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _stristr($str, $search) +{ + if (utf8::is_ascii($str) AND utf8::is_ascii($search)) + return stristr($str, $search); + + if ($search == '') + return $str; + + $str_lower = utf8::strtolower($str); + $search_lower = utf8::strtolower($search); + + preg_match('/^(.*?)'.preg_quote($search, '/').'/s', $str_lower, $matches); + + if (isset($matches[1])) + return substr($str, strlen($matches[1])); + + return FALSE; +}
\ No newline at end of file diff --git a/kohana/core/utf8/strlen.php b/kohana/core/utf8/strlen.php new file mode 100644 index 00000000..1a864328 --- /dev/null +++ b/kohana/core/utf8/strlen.php @@ -0,0 +1,21 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strlen + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strlen($str) +{ + // Try mb_strlen() first because it's faster than combination of is_ascii() and strlen() + if (SERVER_UTF8) + return mb_strlen($str); + + if (utf8::is_ascii($str)) + return strlen($str); + + return strlen(utf8_decode($str)); +}
\ No newline at end of file diff --git a/kohana/core/utf8/strpos.php b/kohana/core/utf8/strpos.php new file mode 100644 index 00000000..577bc4ee --- /dev/null +++ b/kohana/core/utf8/strpos.php @@ -0,0 +1,30 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strpos + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strpos($str, $search, $offset = 0) +{ + $offset = (int) $offset; + + if (SERVER_UTF8) + return mb_strpos($str, $search, $offset); + + if (utf8::is_ascii($str) AND utf8::is_ascii($search)) + return strpos($str, $search, $offset); + + if ($offset == 0) + { + $array = explode($search, $str, 2); + return isset($array[1]) ? utf8::strlen($array[0]) : FALSE; + } + + $str = utf8::substr($str, $offset); + $pos = utf8::strpos($str, $search); + return ($pos === FALSE) ? FALSE : $pos + $offset; +}
\ No newline at end of file diff --git a/kohana/core/utf8/strrev.php b/kohana/core/utf8/strrev.php new file mode 100644 index 00000000..a1e46de4 --- /dev/null +++ b/kohana/core/utf8/strrev.php @@ -0,0 +1,18 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strrev + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strrev($str) +{ + if (utf8::is_ascii($str)) + return strrev($str); + + preg_match_all('/./us', $str, $matches); + return implode('', array_reverse($matches[0])); +}
\ No newline at end of file diff --git a/kohana/core/utf8/strrpos.php b/kohana/core/utf8/strrpos.php new file mode 100644 index 00000000..3ebba400 --- /dev/null +++ b/kohana/core/utf8/strrpos.php @@ -0,0 +1,30 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strrpos + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strrpos($str, $search, $offset = 0) +{ + $offset = (int) $offset; + + if (SERVER_UTF8) + return mb_strrpos($str, $search, $offset); + + if (utf8::is_ascii($str) AND utf8::is_ascii($search)) + return strrpos($str, $search, $offset); + + if ($offset == 0) + { + $array = explode($search, $str, -1); + return isset($array[0]) ? utf8::strlen(implode($search, $array)) : FALSE; + } + + $str = utf8::substr($str, $offset); + $pos = utf8::strrpos($str, $search); + return ($pos === FALSE) ? FALSE : $pos + $offset; +}
\ No newline at end of file diff --git a/kohana/core/utf8/strspn.php b/kohana/core/utf8/strspn.php new file mode 100644 index 00000000..de03b1f8 --- /dev/null +++ b/kohana/core/utf8/strspn.php @@ -0,0 +1,30 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strspn + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strspn($str, $mask, $offset = NULL, $length = NULL) +{ + if ($str == '' OR $mask == '') + return 0; + + if (utf8::is_ascii($str) AND utf8::is_ascii($mask)) + return ($offset === NULL) ? strspn($str, $mask) : (($length === NULL) ? strspn($str, $mask, $offset) : strspn($str, $mask, $offset, $length)); + + if ($offset !== NULL OR $length !== NULL) + { + $str = utf8::substr($str, $offset, $length); + } + + // Escape these characters: - [ ] . : \ ^ / + // The . and : are escaped to prevent possible warnings about POSIX regex elements + $mask = preg_replace('#[-[\].:\\\\^/]#', '\\\\$0', $mask); + preg_match('/^[^'.$mask.']+/u', $str, $matches); + + return isset($matches[0]) ? utf8::strlen($matches[0]) : 0; +}
\ No newline at end of file diff --git a/kohana/core/utf8/strtolower.php b/kohana/core/utf8/strtolower.php new file mode 100644 index 00000000..a33b9fd0 --- /dev/null +++ b/kohana/core/utf8/strtolower.php @@ -0,0 +1,84 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strtolower + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strtolower($str) +{ + if (SERVER_UTF8) + return mb_strtolower($str); + + if (utf8::is_ascii($str)) + return strtolower($str); + + static $UTF8_UPPER_TO_LOWER = NULL; + + if ($UTF8_UPPER_TO_LOWER === NULL) + { + $UTF8_UPPER_TO_LOWER = array( + 0x0041=>0x0061, 0x03A6=>0x03C6, 0x0162=>0x0163, 0x00C5=>0x00E5, 0x0042=>0x0062, + 0x0139=>0x013A, 0x00C1=>0x00E1, 0x0141=>0x0142, 0x038E=>0x03CD, 0x0100=>0x0101, + 0x0490=>0x0491, 0x0394=>0x03B4, 0x015A=>0x015B, 0x0044=>0x0064, 0x0393=>0x03B3, + 0x00D4=>0x00F4, 0x042A=>0x044A, 0x0419=>0x0439, 0x0112=>0x0113, 0x041C=>0x043C, + 0x015E=>0x015F, 0x0143=>0x0144, 0x00CE=>0x00EE, 0x040E=>0x045E, 0x042F=>0x044F, + 0x039A=>0x03BA, 0x0154=>0x0155, 0x0049=>0x0069, 0x0053=>0x0073, 0x1E1E=>0x1E1F, + 0x0134=>0x0135, 0x0427=>0x0447, 0x03A0=>0x03C0, 0x0418=>0x0438, 0x00D3=>0x00F3, + 0x0420=>0x0440, 0x0404=>0x0454, 0x0415=>0x0435, 0x0429=>0x0449, 0x014A=>0x014B, + 0x0411=>0x0431, 0x0409=>0x0459, 0x1E02=>0x1E03, 0x00D6=>0x00F6, 0x00D9=>0x00F9, + 0x004E=>0x006E, 0x0401=>0x0451, 0x03A4=>0x03C4, 0x0423=>0x0443, 0x015C=>0x015D, + 0x0403=>0x0453, 0x03A8=>0x03C8, 0x0158=>0x0159, 0x0047=>0x0067, 0x00C4=>0x00E4, + 0x0386=>0x03AC, 0x0389=>0x03AE, 0x0166=>0x0167, 0x039E=>0x03BE, 0x0164=>0x0165, + 0x0116=>0x0117, 0x0108=>0x0109, 0x0056=>0x0076, 0x00DE=>0x00FE, 0x0156=>0x0157, + 0x00DA=>0x00FA, 0x1E60=>0x1E61, 0x1E82=>0x1E83, 0x00C2=>0x00E2, 0x0118=>0x0119, + 0x0145=>0x0146, 0x0050=>0x0070, 0x0150=>0x0151, 0x042E=>0x044E, 0x0128=>0x0129, + 0x03A7=>0x03C7, 0x013D=>0x013E, 0x0422=>0x0442, 0x005A=>0x007A, 0x0428=>0x0448, + 0x03A1=>0x03C1, 0x1E80=>0x1E81, 0x016C=>0x016D, 0x00D5=>0x00F5, 0x0055=>0x0075, + 0x0176=>0x0177, 0x00DC=>0x00FC, 0x1E56=>0x1E57, 0x03A3=>0x03C3, 0x041A=>0x043A, + 0x004D=>0x006D, 0x016A=>0x016B, 0x0170=>0x0171, 0x0424=>0x0444, 0x00CC=>0x00EC, + 0x0168=>0x0169, 0x039F=>0x03BF, 0x004B=>0x006B, 0x00D2=>0x00F2, 0x00C0=>0x00E0, + 0x0414=>0x0434, 0x03A9=>0x03C9, 0x1E6A=>0x1E6B, 0x00C3=>0x00E3, 0x042D=>0x044D, + 0x0416=>0x0436, 0x01A0=>0x01A1, 0x010C=>0x010D, 0x011C=>0x011D, 0x00D0=>0x00F0, + 0x013B=>0x013C, 0x040F=>0x045F, 0x040A=>0x045A, 0x00C8=>0x00E8, 0x03A5=>0x03C5, + 0x0046=>0x0066, 0x00DD=>0x00FD, 0x0043=>0x0063, 0x021A=>0x021B, 0x00CA=>0x00EA, + 0x0399=>0x03B9, 0x0179=>0x017A, 0x00CF=>0x00EF, 0x01AF=>0x01B0, 0x0045=>0x0065, + 0x039B=>0x03BB, 0x0398=>0x03B8, 0x039C=>0x03BC, 0x040C=>0x045C, 0x041F=>0x043F, + 0x042C=>0x044C, 0x00DE=>0x00FE, 0x00D0=>0x00F0, 0x1EF2=>0x1EF3, 0x0048=>0x0068, + 0x00CB=>0x00EB, 0x0110=>0x0111, 0x0413=>0x0433, 0x012E=>0x012F, 0x00C6=>0x00E6, + 0x0058=>0x0078, 0x0160=>0x0161, 0x016E=>0x016F, 0x0391=>0x03B1, 0x0407=>0x0457, + 0x0172=>0x0173, 0x0178=>0x00FF, 0x004F=>0x006F, 0x041B=>0x043B, 0x0395=>0x03B5, + 0x0425=>0x0445, 0x0120=>0x0121, 0x017D=>0x017E, 0x017B=>0x017C, 0x0396=>0x03B6, + 0x0392=>0x03B2, 0x0388=>0x03AD, 0x1E84=>0x1E85, 0x0174=>0x0175, 0x0051=>0x0071, + 0x0417=>0x0437, 0x1E0A=>0x1E0B, 0x0147=>0x0148, 0x0104=>0x0105, 0x0408=>0x0458, + 0x014C=>0x014D, 0x00CD=>0x00ED, 0x0059=>0x0079, 0x010A=>0x010B, 0x038F=>0x03CE, + 0x0052=>0x0072, 0x0410=>0x0430, 0x0405=>0x0455, 0x0402=>0x0452, 0x0126=>0x0127, + 0x0136=>0x0137, 0x012A=>0x012B, 0x038A=>0x03AF, 0x042B=>0x044B, 0x004C=>0x006C, + 0x0397=>0x03B7, 0x0124=>0x0125, 0x0218=>0x0219, 0x00DB=>0x00FB, 0x011E=>0x011F, + 0x041E=>0x043E, 0x1E40=>0x1E41, 0x039D=>0x03BD, 0x0106=>0x0107, 0x03AB=>0x03CB, + 0x0426=>0x0446, 0x00DE=>0x00FE, 0x00C7=>0x00E7, 0x03AA=>0x03CA, 0x0421=>0x0441, + 0x0412=>0x0432, 0x010E=>0x010F, 0x00D8=>0x00F8, 0x0057=>0x0077, 0x011A=>0x011B, + 0x0054=>0x0074, 0x004A=>0x006A, 0x040B=>0x045B, 0x0406=>0x0456, 0x0102=>0x0103, + 0x039B=>0x03BB, 0x00D1=>0x00F1, 0x041D=>0x043D, 0x038C=>0x03CC, 0x00C9=>0x00E9, + 0x00D0=>0x00F0, 0x0407=>0x0457, 0x0122=>0x0123, + ); + } + + $uni = utf8::to_unicode($str); + + if ($uni === FALSE) + return FALSE; + + for ($i = 0, $c = count($uni); $i < $c; $i++) + { + if (isset($UTF8_UPPER_TO_LOWER[$uni[$i]])) + { + $uni[$i] = $UTF8_UPPER_TO_LOWER[$uni[$i]]; + } + } + + return utf8::from_unicode($uni); +}
\ No newline at end of file diff --git a/kohana/core/utf8/strtoupper.php b/kohana/core/utf8/strtoupper.php new file mode 100644 index 00000000..76837b36 --- /dev/null +++ b/kohana/core/utf8/strtoupper.php @@ -0,0 +1,84 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::strtoupper + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _strtoupper($str) +{ + if (SERVER_UTF8) + return mb_strtoupper($str); + + if (utf8::is_ascii($str)) + return strtoupper($str); + + static $UTF8_LOWER_TO_UPPER = NULL; + + if ($UTF8_LOWER_TO_UPPER === NULL) + { + $UTF8_LOWER_TO_UPPER = array( + 0x0061=>0x0041, 0x03C6=>0x03A6, 0x0163=>0x0162, 0x00E5=>0x00C5, 0x0062=>0x0042, + 0x013A=>0x0139, 0x00E1=>0x00C1, 0x0142=>0x0141, 0x03CD=>0x038E, 0x0101=>0x0100, + 0x0491=>0x0490, 0x03B4=>0x0394, 0x015B=>0x015A, 0x0064=>0x0044, 0x03B3=>0x0393, + 0x00F4=>0x00D4, 0x044A=>0x042A, 0x0439=>0x0419, 0x0113=>0x0112, 0x043C=>0x041C, + 0x015F=>0x015E, 0x0144=>0x0143, 0x00EE=>0x00CE, 0x045E=>0x040E, 0x044F=>0x042F, + 0x03BA=>0x039A, 0x0155=>0x0154, 0x0069=>0x0049, 0x0073=>0x0053, 0x1E1F=>0x1E1E, + 0x0135=>0x0134, 0x0447=>0x0427, 0x03C0=>0x03A0, 0x0438=>0x0418, 0x00F3=>0x00D3, + 0x0440=>0x0420, 0x0454=>0x0404, 0x0435=>0x0415, 0x0449=>0x0429, 0x014B=>0x014A, + 0x0431=>0x0411, 0x0459=>0x0409, 0x1E03=>0x1E02, 0x00F6=>0x00D6, 0x00F9=>0x00D9, + 0x006E=>0x004E, 0x0451=>0x0401, 0x03C4=>0x03A4, 0x0443=>0x0423, 0x015D=>0x015C, + 0x0453=>0x0403, 0x03C8=>0x03A8, 0x0159=>0x0158, 0x0067=>0x0047, 0x00E4=>0x00C4, + 0x03AC=>0x0386, 0x03AE=>0x0389, 0x0167=>0x0166, 0x03BE=>0x039E, 0x0165=>0x0164, + 0x0117=>0x0116, 0x0109=>0x0108, 0x0076=>0x0056, 0x00FE=>0x00DE, 0x0157=>0x0156, + 0x00FA=>0x00DA, 0x1E61=>0x1E60, 0x1E83=>0x1E82, 0x00E2=>0x00C2, 0x0119=>0x0118, + 0x0146=>0x0145, 0x0070=>0x0050, 0x0151=>0x0150, 0x044E=>0x042E, 0x0129=>0x0128, + 0x03C7=>0x03A7, 0x013E=>0x013D, 0x0442=>0x0422, 0x007A=>0x005A, 0x0448=>0x0428, + 0x03C1=>0x03A1, 0x1E81=>0x1E80, 0x016D=>0x016C, 0x00F5=>0x00D5, 0x0075=>0x0055, + 0x0177=>0x0176, 0x00FC=>0x00DC, 0x1E57=>0x1E56, 0x03C3=>0x03A3, 0x043A=>0x041A, + 0x006D=>0x004D, 0x016B=>0x016A, 0x0171=>0x0170, 0x0444=>0x0424, 0x00EC=>0x00CC, + 0x0169=>0x0168, 0x03BF=>0x039F, 0x006B=>0x004B, 0x00F2=>0x00D2, 0x00E0=>0x00C0, + 0x0434=>0x0414, 0x03C9=>0x03A9, 0x1E6B=>0x1E6A, 0x00E3=>0x00C3, 0x044D=>0x042D, + 0x0436=>0x0416, 0x01A1=>0x01A0, 0x010D=>0x010C, 0x011D=>0x011C, 0x00F0=>0x00D0, + 0x013C=>0x013B, 0x045F=>0x040F, 0x045A=>0x040A, 0x00E8=>0x00C8, 0x03C5=>0x03A5, + 0x0066=>0x0046, 0x00FD=>0x00DD, 0x0063=>0x0043, 0x021B=>0x021A, 0x00EA=>0x00CA, + 0x03B9=>0x0399, 0x017A=>0x0179, 0x00EF=>0x00CF, 0x01B0=>0x01AF, 0x0065=>0x0045, + 0x03BB=>0x039B, 0x03B8=>0x0398, 0x03BC=>0x039C, 0x045C=>0x040C, 0x043F=>0x041F, + 0x044C=>0x042C, 0x00FE=>0x00DE, 0x00F0=>0x00D0, 0x1EF3=>0x1EF2, 0x0068=>0x0048, + 0x00EB=>0x00CB, 0x0111=>0x0110, 0x0433=>0x0413, 0x012F=>0x012E, 0x00E6=>0x00C6, + 0x0078=>0x0058, 0x0161=>0x0160, 0x016F=>0x016E, 0x03B1=>0x0391, 0x0457=>0x0407, + 0x0173=>0x0172, 0x00FF=>0x0178, 0x006F=>0x004F, 0x043B=>0x041B, 0x03B5=>0x0395, + 0x0445=>0x0425, 0x0121=>0x0120, 0x017E=>0x017D, 0x017C=>0x017B, 0x03B6=>0x0396, + 0x03B2=>0x0392, 0x03AD=>0x0388, 0x1E85=>0x1E84, 0x0175=>0x0174, 0x0071=>0x0051, + 0x0437=>0x0417, 0x1E0B=>0x1E0A, 0x0148=>0x0147, 0x0105=>0x0104, 0x0458=>0x0408, + 0x014D=>0x014C, 0x00ED=>0x00CD, 0x0079=>0x0059, 0x010B=>0x010A, 0x03CE=>0x038F, + 0x0072=>0x0052, 0x0430=>0x0410, 0x0455=>0x0405, 0x0452=>0x0402, 0x0127=>0x0126, + 0x0137=>0x0136, 0x012B=>0x012A, 0x03AF=>0x038A, 0x044B=>0x042B, 0x006C=>0x004C, + 0x03B7=>0x0397, 0x0125=>0x0124, 0x0219=>0x0218, 0x00FB=>0x00DB, 0x011F=>0x011E, + 0x043E=>0x041E, 0x1E41=>0x1E40, 0x03BD=>0x039D, 0x0107=>0x0106, 0x03CB=>0x03AB, + 0x0446=>0x0426, 0x00FE=>0x00DE, 0x00E7=>0x00C7, 0x03CA=>0x03AA, 0x0441=>0x0421, + 0x0432=>0x0412, 0x010F=>0x010E, 0x00F8=>0x00D8, 0x0077=>0x0057, 0x011B=>0x011A, + 0x0074=>0x0054, 0x006A=>0x004A, 0x045B=>0x040B, 0x0456=>0x0406, 0x0103=>0x0102, + 0x03BB=>0x039B, 0x00F1=>0x00D1, 0x043D=>0x041D, 0x03CC=>0x038C, 0x00E9=>0x00C9, + 0x00F0=>0x00D0, 0x0457=>0x0407, 0x0123=>0x0122, + ); + } + + $uni = utf8::to_unicode($str); + + if ($uni === FALSE) + return FALSE; + + for ($i = 0, $c = count($uni); $i < $c; $i++) + { + if (isset($UTF8_LOWER_TO_UPPER[$uni[$i]])) + { + $uni[$i] = $UTF8_LOWER_TO_UPPER[$uni[$i]]; + } + } + + return utf8::from_unicode($uni); +}
\ No newline at end of file diff --git a/kohana/core/utf8/substr.php b/kohana/core/utf8/substr.php new file mode 100644 index 00000000..3151ca21 --- /dev/null +++ b/kohana/core/utf8/substr.php @@ -0,0 +1,75 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::substr + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _substr($str, $offset, $length = NULL) +{ + if (SERVER_UTF8) + return ($length === NULL) ? mb_substr($str, $offset) : mb_substr($str, $offset, $length); + + if (utf8::is_ascii($str)) + return ($length === NULL) ? substr($str, $offset) : substr($str, $offset, $length); + + // Normalize params + $str = (string) $str; + $strlen = utf8::strlen($str); + $offset = (int) ($offset < 0) ? max(0, $strlen + $offset) : $offset; // Normalize to positive offset + $length = ($length === NULL) ? NULL : (int) $length; + + // Impossible + if ($length === 0 OR $offset >= $strlen OR ($length < 0 AND $length <= $offset - $strlen)) + return ''; + + // Whole string + if ($offset == 0 AND ($length === NULL OR $length >= $strlen)) + return $str; + + // Build regex + $regex = '^'; + + // Create an offset expression + if ($offset > 0) + { + // PCRE repeating quantifiers must be less than 65536, so repeat when necessary + $x = (int) ($offset / 65535); + $y = (int) ($offset % 65535); + $regex .= ($x == 0) ? '' : '(?:.{65535}){'.$x.'}'; + $regex .= ($y == 0) ? '' : '.{'.$y.'}'; + } + + // Create a length expression + if ($length === NULL) + { + $regex .= '(.*)'; // No length set, grab it all + } + // Find length from the left (positive length) + elseif ($length > 0) + { + // Reduce length so that it can't go beyond the end of the string + $length = min($strlen - $offset, $length); + + $x = (int) ($length / 65535); + $y = (int) ($length % 65535); + $regex .= '('; + $regex .= ($x == 0) ? '' : '(?:.{65535}){'.$x.'}'; + $regex .= '.{'.$y.'})'; + } + // Find length from the right (negative length) + else + { + $x = (int) (-$length / 65535); + $y = (int) (-$length % 65535); + $regex .= '(.*)'; + $regex .= ($x == 0) ? '' : '(?:.{65535}){'.$x.'}'; + $regex .= '.{'.$y.'}'; + } + + preg_match('/'.$regex.'/us', $str, $matches); + return $matches[1]; +}
\ No newline at end of file diff --git a/kohana/core/utf8/substr_replace.php b/kohana/core/utf8/substr_replace.php new file mode 100644 index 00000000..061e8834 --- /dev/null +++ b/kohana/core/utf8/substr_replace.php @@ -0,0 +1,22 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::substr_replace + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _substr_replace($str, $replacement, $offset, $length = NULL) +{ + if (utf8::is_ascii($str)) + return ($length === NULL) ? substr_replace($str, $replacement, $offset) : substr_replace($str, $replacement, $offset, $length); + + $length = ($length === NULL) ? utf8::strlen($str) : (int) $length; + preg_match_all('/./us', $str, $str_array); + preg_match_all('/./us', $replacement, $replacement_array); + + array_splice($str_array[0], $offset, $length, $replacement_array[0]); + return implode('', $str_array[0]); +}
\ No newline at end of file diff --git a/kohana/core/utf8/to_unicode.php b/kohana/core/utf8/to_unicode.php new file mode 100644 index 00000000..93dccb19 --- /dev/null +++ b/kohana/core/utf8/to_unicode.php @@ -0,0 +1,141 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::to_unicode + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _to_unicode($str) +{ + $mState = 0; // cached expected number of octets after the current octet until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + + $out = array(); + + $len = strlen($str); + + for ($i = 0; $i < $len; $i++) + { + $in = ord($str[$i]); + + if ($mState == 0) + { + // When mState is zero we expect either a US-ASCII character or a + // multi-octet sequence. + if (0 == (0x80 & $in)) + { + // US-ASCII, pass straight through. + $out[] = $in; + $mBytes = 1; + } + elseif (0xC0 == (0xE0 & $in)) + { + // First octet of 2 octet sequence + $mUcs4 = $in; + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } + elseif (0xE0 == (0xF0 & $in)) + { + // First octet of 3 octet sequence + $mUcs4 = $in; + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } + elseif (0xF0 == (0xF8 & $in)) + { + // First octet of 4 octet sequence + $mUcs4 = $in; + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } + elseif (0xF8 == (0xFC & $in)) + { + // First octet of 5 octet sequence. + // + // This is illegal because the encoded codepoint must be either + // (a) not the shortest form or + // (b) outside the Unicode range of 0-0x10FFFF. + // Rather than trying to resynchronize, we will carry on until the end + // of the sequence and let the later error handling code catch it. + $mUcs4 = $in; + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } + elseif (0xFC == (0xFE & $in)) + { + // First octet of 6 octet sequence, see comments for 5 octet sequence. + $mUcs4 = $in; + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } + else + { + // Current octet is neither in the US-ASCII range nor a legal first octet of a multi-octet sequence. + trigger_error('utf8::to_unicode: Illegal sequence identifier in UTF-8 at byte '.$i, E_USER_WARNING); + return FALSE; + } + } + else + { + // When mState is non-zero, we expect a continuation of the multi-octet sequence + if (0x80 == (0xC0 & $in)) + { + // Legal continuation + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + // End of the multi-octet sequence. mUcs4 now contains the final Unicode codepoint to be output + if (0 == --$mState) + { + // Check for illegal sequences and codepoints + + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) AND ($mUcs4 < 0x0080)) OR + ((3 == $mBytes) AND ($mUcs4 < 0x0800)) OR + ((4 == $mBytes) AND ($mUcs4 < 0x10000)) OR + (4 < $mBytes) OR + // From Unicode 3.2, surrogate characters are illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) OR + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF)) + { + trigger_error('utf8::to_unicode: Illegal sequence or codepoint in UTF-8 at byte '.$i, E_USER_WARNING); + return FALSE; + } + + if (0xFEFF != $mUcs4) + { + // BOM is legal but we don't want to output it + $out[] = $mUcs4; + } + + // Initialize UTF-8 cache + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + } + } + else + { + // ((0xC0 & (*in) != 0x80) AND (mState != 0)) + // Incomplete multi-octet sequence + trigger_error('utf8::to_unicode: Incomplete multi-octet sequence in UTF-8 at byte '.$i, E_USER_WARNING); + return FALSE; + } + } + } + + return $out; +}
\ No newline at end of file diff --git a/kohana/core/utf8/transliterate_to_ascii.php b/kohana/core/utf8/transliterate_to_ascii.php new file mode 100644 index 00000000..65860217 --- /dev/null +++ b/kohana/core/utf8/transliterate_to_ascii.php @@ -0,0 +1,77 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::transliterate_to_ascii + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _transliterate_to_ascii($str, $case = 0) +{ + static $UTF8_LOWER_ACCENTS = NULL; + static $UTF8_UPPER_ACCENTS = NULL; + + if ($case <= 0) + { + if ($UTF8_LOWER_ACCENTS === NULL) + { + $UTF8_LOWER_ACCENTS = array( + 'à' => 'a', 'ô' => 'o', 'ď' => 'd', 'ḟ' => 'f', 'ë' => 'e', 'š' => 's', 'ơ' => 'o', + 'ß' => 'ss', 'ă' => 'a', 'ř' => 'r', 'ț' => 't', 'ň' => 'n', 'ā' => 'a', 'ķ' => 'k', + 'ŝ' => 's', 'ỳ' => 'y', 'ņ' => 'n', 'ĺ' => 'l', 'ħ' => 'h', 'ṗ' => 'p', 'ó' => 'o', + 'ú' => 'u', 'ě' => 'e', 'é' => 'e', 'ç' => 'c', 'ẁ' => 'w', 'ċ' => 'c', 'õ' => 'o', + 'ṡ' => 's', 'ø' => 'o', 'ģ' => 'g', 'ŧ' => 't', 'ș' => 's', 'ė' => 'e', 'ĉ' => 'c', + 'ś' => 's', 'î' => 'i', 'ű' => 'u', 'ć' => 'c', 'ę' => 'e', 'ŵ' => 'w', 'ṫ' => 't', + 'ū' => 'u', 'č' => 'c', 'ö' => 'o', 'è' => 'e', 'ŷ' => 'y', 'ą' => 'a', 'ł' => 'l', + 'ų' => 'u', 'ů' => 'u', 'ş' => 's', 'ğ' => 'g', 'ļ' => 'l', 'ƒ' => 'f', 'ž' => 'z', + 'ẃ' => 'w', 'ḃ' => 'b', 'å' => 'a', 'ì' => 'i', 'ï' => 'i', 'ḋ' => 'd', 'ť' => 't', + 'ŗ' => 'r', 'ä' => 'a', 'í' => 'i', 'ŕ' => 'r', 'ê' => 'e', 'ü' => 'u', 'ò' => 'o', + 'ē' => 'e', 'ñ' => 'n', 'ń' => 'n', 'ĥ' => 'h', 'ĝ' => 'g', 'đ' => 'd', 'ĵ' => 'j', + 'ÿ' => 'y', 'ũ' => 'u', 'ŭ' => 'u', 'ư' => 'u', 'ţ' => 't', 'ý' => 'y', 'ő' => 'o', + 'â' => 'a', 'ľ' => 'l', 'ẅ' => 'w', 'ż' => 'z', 'ī' => 'i', 'ã' => 'a', 'ġ' => 'g', + 'ṁ' => 'm', 'ō' => 'o', 'ĩ' => 'i', 'ù' => 'u', 'į' => 'i', 'ź' => 'z', 'á' => 'a', + 'û' => 'u', 'þ' => 'th', 'ð' => 'dh', 'æ' => 'ae', 'µ' => 'u', 'ĕ' => 'e', + ); + } + + $str = str_replace( + array_keys($UTF8_LOWER_ACCENTS), + array_values($UTF8_LOWER_ACCENTS), + $str + ); + } + + if ($case >= 0) + { + if ($UTF8_UPPER_ACCENTS === NULL) + { + $UTF8_UPPER_ACCENTS = array( + 'À' => 'A', 'Ô' => 'O', 'Ď' => 'D', 'Ḟ' => 'F', 'Ë' => 'E', 'Š' => 'S', 'Ơ' => 'O', + 'Ă' => 'A', 'Ř' => 'R', 'Ț' => 'T', 'Ň' => 'N', 'Ā' => 'A', 'Ķ' => 'K', 'Ĕ' => 'E', + 'Ŝ' => 'S', 'Ỳ' => 'Y', 'Ņ' => 'N', 'Ĺ' => 'L', 'Ħ' => 'H', 'Ṗ' => 'P', 'Ó' => 'O', + 'Ú' => 'U', 'Ě' => 'E', 'É' => 'E', 'Ç' => 'C', 'Ẁ' => 'W', 'Ċ' => 'C', 'Õ' => 'O', + 'Ṡ' => 'S', 'Ø' => 'O', 'Ģ' => 'G', 'Ŧ' => 'T', 'Ș' => 'S', 'Ė' => 'E', 'Ĉ' => 'C', + 'Ś' => 'S', 'Î' => 'I', 'Ű' => 'U', 'Ć' => 'C', 'Ę' => 'E', 'Ŵ' => 'W', 'Ṫ' => 'T', + 'Ū' => 'U', 'Č' => 'C', 'Ö' => 'O', 'È' => 'E', 'Ŷ' => 'Y', 'Ą' => 'A', 'Ł' => 'L', + 'Ų' => 'U', 'Ů' => 'U', 'Ş' => 'S', 'Ğ' => 'G', 'Ļ' => 'L', 'Ƒ' => 'F', 'Ž' => 'Z', + 'Ẃ' => 'W', 'Ḃ' => 'B', 'Å' => 'A', 'Ì' => 'I', 'Ï' => 'I', 'Ḋ' => 'D', 'Ť' => 'T', + 'Ŗ' => 'R', 'Ä' => 'A', 'Í' => 'I', 'Ŕ' => 'R', 'Ê' => 'E', 'Ü' => 'U', 'Ò' => 'O', + 'Ē' => 'E', 'Ñ' => 'N', 'Ń' => 'N', 'Ĥ' => 'H', 'Ĝ' => 'G', 'Đ' => 'D', 'Ĵ' => 'J', + 'Ÿ' => 'Y', 'Ũ' => 'U', 'Ŭ' => 'U', 'Ư' => 'U', 'Ţ' => 'T', 'Ý' => 'Y', 'Ő' => 'O', + 'Â' => 'A', 'Ľ' => 'L', 'Ẅ' => 'W', 'Ż' => 'Z', 'Ī' => 'I', 'Ã' => 'A', 'Ġ' => 'G', + 'Ṁ' => 'M', 'Ō' => 'O', 'Ĩ' => 'I', 'Ù' => 'U', 'Į' => 'I', 'Ź' => 'Z', 'Á' => 'A', + 'Û' => 'U', 'Þ' => 'Th', 'Ð' => 'Dh', 'Æ' => 'Ae', + ); + } + + $str = str_replace( + array_keys($UTF8_UPPER_ACCENTS), + array_values($UTF8_UPPER_ACCENTS), + $str + ); + } + + return $str; +}
\ No newline at end of file diff --git a/kohana/core/utf8/trim.php b/kohana/core/utf8/trim.php new file mode 100644 index 00000000..2bfbb385 --- /dev/null +++ b/kohana/core/utf8/trim.php @@ -0,0 +1,17 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::trim + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _trim($str, $charlist = NULL) +{ + if ($charlist === NULL) + return trim($str); + + return utf8::ltrim(utf8::rtrim($str, $charlist), $charlist); +}
\ No newline at end of file diff --git a/kohana/core/utf8/ucfirst.php b/kohana/core/utf8/ucfirst.php new file mode 100644 index 00000000..63dbe80c --- /dev/null +++ b/kohana/core/utf8/ucfirst.php @@ -0,0 +1,18 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::ucfirst + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _ucfirst($str) +{ + if (utf8::is_ascii($str)) + return ucfirst($str); + + preg_match('/^(.?)(.*)$/us', $str, $matches); + return utf8::strtoupper($matches[1]).$matches[2]; +}
\ No newline at end of file diff --git a/kohana/core/utf8/ucwords.php b/kohana/core/utf8/ucwords.php new file mode 100644 index 00000000..2ba57573 --- /dev/null +++ b/kohana/core/utf8/ucwords.php @@ -0,0 +1,26 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * utf8::ucwords + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007 Kohana Team + * @copyright (c) 2005 Harry Fuecks + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +function _ucwords($str) +{ + if (SERVER_UTF8) + return mb_convert_case($str, MB_CASE_TITLE); + + if (utf8::is_ascii($str)) + return ucwords($str); + + // [\x0c\x09\x0b\x0a\x0d\x20] matches form feeds, horizontal tabs, vertical tabs, linefeeds and carriage returns. + // This corresponds to the definition of a 'word' defined at http://php.net/ucwords + return preg_replace( + '/(?<=^|[\x0c\x09\x0b\x0a\x0d\x20])[^\x0c\x09\x0b\x0a\x0d\x20]/ue', + 'utf8::strtoupper(\'$0\')', + $str + ); +}
\ No newline at end of file diff --git a/kohana/fonts/DejaVuSerif.ttf b/kohana/fonts/DejaVuSerif.ttf Binary files differnew file mode 100644 index 00000000..b7f4482d --- /dev/null +++ b/kohana/fonts/DejaVuSerif.ttf diff --git a/kohana/fonts/LICENSE b/kohana/fonts/LICENSE new file mode 100644 index 00000000..254e2cc4 --- /dev/null +++ b/kohana/fonts/LICENSE @@ -0,0 +1,99 @@ +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ diff --git a/kohana/helpers/arr.php b/kohana/helpers/arr.php new file mode 100644 index 00000000..b0592347 --- /dev/null +++ b/kohana/helpers/arr.php @@ -0,0 +1,291 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Array helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class arr_Core { + + /** + * Return a callback array from a string, eg: limit[10,20] would become + * array('limit', array('10', '20')) + * + * @param string callback string + * @return array + */ + public static function callback_string($str) + { + // command[param,param] + if (preg_match('/([^\[]*+)\[(.+)\]/', (string) $str, $match)) + { + // command + $command = $match[1]; + + // param,param + $params = preg_split('/(?<!\\\\),/', $match[2]); + $params = str_replace('\,', ',', $params); + } + else + { + // command + $command = $str; + + // No params + $params = NULL; + } + + return array($command, $params); + } + + /** + * Rotates a 2D array clockwise. + * Example, turns a 2x3 array into a 3x2 array. + * + * @param array array to rotate + * @param boolean keep the keys in the final rotated array. the sub arrays of the source array need to have the same key values. + * if your subkeys might not match, you need to pass FALSE here! + * @return array + */ + public static function rotate($source_array, $keep_keys = TRUE) + { + $new_array = array(); + foreach ($source_array as $key => $value) + { + $value = ($keep_keys === TRUE) ? $value : array_values($value); + foreach ($value as $k => $v) + { + $new_array[$k][$key] = $v; + } + } + + return $new_array; + } + + /** + * Removes a key from an array and returns the value. + * + * @param string key to return + * @param array array to work on + * @return mixed value of the requested array key + */ + public static function remove($key, & $array) + { + if ( ! array_key_exists($key, $array)) + return NULL; + + $val = $array[$key]; + unset($array[$key]); + + return $val; + } + + + /** + * Extract one or more keys from an array. Each key given after the first + * argument (the array) will be extracted. Keys that do not exist in the + * search array will be NULL in the extracted data. + * + * @param array array to search + * @param string key name + * @return array + */ + public static function extract(array $search, $keys) + { + // Get the keys, removing the $search array + $keys = array_slice(func_get_args(), 1); + + $found = array(); + foreach ($keys as $key) + { + if (isset($search[$key])) + { + $found[$key] = $search[$key]; + } + else + { + $found[$key] = NULL; + } + } + + return $found; + } + + /** + * Because PHP does not have this function. + * + * @param array array to unshift + * @param string key to unshift + * @param mixed value to unshift + * @return array + */ + public static function unshift_assoc( array & $array, $key, $val) + { + $array = array_reverse($array, TRUE); + $array[$key] = $val; + $array = array_reverse($array, TRUE); + + return $array; + } + + /** + * Because PHP does not have this function, and array_walk_recursive creates + * references in arrays and is not truly recursive. + * + * @param mixed callback to apply to each member of the array + * @param array array to map to + * @return array + */ + public static function map_recursive($callback, array $array) + { + foreach ($array as $key => $val) + { + // Map the callback to the key + $array[$key] = is_array($val) ? arr::map_recursive($callback, $val) : call_user_func($callback, $val); + } + + return $array; + } + + /** + * Binary search algorithm. + * + * @param mixed the value to search for + * @param array an array of values to search in + * @param boolean return false, or the nearest value + * @param mixed sort the array before searching it + * @return integer + */ + public static function binary_search($needle, $haystack, $nearest = FALSE, $sort = FALSE) + { + if ($sort === TRUE) + { + sort($haystack); + } + + $high = count($haystack); + $low = 0; + + while ($high - $low > 1) + { + $probe = ($high + $low) / 2; + if ($haystack[$probe] < $needle) + { + $low = $probe; + } + else + { + $high = $probe; + } + } + + if ($high == count($haystack) OR $haystack[$high] != $needle) + { + if ($nearest === FALSE) + return FALSE; + + // return the nearest value + $high_distance = $haystack[ceil($low)] - $needle; + $low_distance = $needle - $haystack[floor($low)]; + + return ($high_distance >= $low_distance) ? $haystack[ceil($low)] : $haystack[floor($low)]; + } + + return $high; + } + + /** + * Emulates array_merge_recursive, but appends numeric keys and replaces + * associative keys, instead of appending all keys. + * + * @param array any number of arrays + * @return array + */ + public static function merge() + { + $total = func_num_args(); + + $result = array(); + for ($i = 0; $i < $total; $i++) + { + foreach (func_get_arg($i) as $key => $val) + { + if (isset($result[$key])) + { + if (is_array($val)) + { + // Arrays are merged recursively + $result[$key] = arr::merge($result[$key], $val); + } + elseif (is_int($key)) + { + // Indexed arrays are appended + array_push($result, $val); + } + else + { + // Associative arrays are replaced + $result[$key] = $val; + } + } + else + { + // New values are added + $result[$key] = $val; + } + } + } + + return $result; + } + + /** + * Overwrites an array with values from input array(s). + * Non-existing keys will not be appended! + * + * @param array key array + * @param array input array(s) that will overwrite key array values + * @return array + */ + public static function overwrite($array1) + { + foreach (array_slice(func_get_args(), 1) as $array2) + { + foreach ($array2 as $key => $value) + { + if (array_key_exists($key, $array1)) + { + $array1[$key] = $value; + } + } + } + + return $array1; + } + + /** + * Fill an array with a range of numbers. + * + * @param integer stepping + * @param integer ending number + * @return array + */ + public static function range($step = 10, $max = 100) + { + if ($step < 1) + return array(); + + $array = array(); + for ($i = $step; $i <= $max; $i += $step) + { + $array[$i] = $i; + } + + return $array; + } + +} // End arr
\ No newline at end of file diff --git a/kohana/helpers/cookie.php b/kohana/helpers/cookie.php new file mode 100644 index 00000000..d10c6966 --- /dev/null +++ b/kohana/helpers/cookie.php @@ -0,0 +1,84 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Cookie helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class cookie_Core { + + /** + * Sets a cookie with the given parameters. + * + * @param string cookie name or array of config options + * @param string cookie value + * @param integer number of seconds before the cookie expires + * @param string URL path to allow + * @param string URL domain to allow + * @param boolean HTTPS only + * @param boolean HTTP only (requires PHP 5.2 or higher) + * @return boolean + */ + public static function set($name, $value = NULL, $expire = NULL, $path = NULL, $domain = NULL, $secure = NULL, $httponly = NULL) + { + if (headers_sent()) + return FALSE; + + // If the name param is an array, we import it + is_array($name) and extract($name, EXTR_OVERWRITE); + + // Fetch default options + $config = Kohana::config('cookie'); + + foreach (array('value', 'expire', 'domain', 'path', 'secure', 'httponly') as $item) + { + if ($$item === NULL AND isset($config[$item])) + { + $$item = $config[$item]; + } + } + + // Expiration timestamp + $expire = ($expire == 0) ? 0 : time() + (int) $expire; + + return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly); + } + + /** + * Fetch a cookie value, using the Input library. + * + * @param string cookie name + * @param mixed default value + * @param boolean use XSS cleaning on the value + * @return string + */ + public static function get($name, $default = NULL, $xss_clean = FALSE) + { + return Input::instance()->cookie($name, $default, $xss_clean); + } + + /** + * Nullify and unset a cookie. + * + * @param string cookie name + * @param string URL path + * @param string URL domain + * @return boolean + */ + public static function delete($name, $path = NULL, $domain = NULL) + { + if ( ! isset($_COOKIE[$name])) + return FALSE; + + // Delete the cookie from globals + unset($_COOKIE[$name]); + + // Sets the cookie value to an empty string, and the expiration to 24 hours ago + return cookie::set($name, '', -86400, $path, $domain, FALSE, FALSE); + } + +} // End cookie
\ No newline at end of file diff --git a/kohana/helpers/date.php b/kohana/helpers/date.php new file mode 100644 index 00000000..c6607009 --- /dev/null +++ b/kohana/helpers/date.php @@ -0,0 +1,399 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Date helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class date_Core { + + /** + * Converts a UNIX timestamp to DOS format. + * + * @param integer UNIX timestamp + * @return integer + */ + public static function unix2dos($timestamp = FALSE) + { + $timestamp = ($timestamp === FALSE) ? getdate() : getdate($timestamp); + + if ($timestamp['year'] < 1980) + { + return (1 << 21 | 1 << 16); + } + + $timestamp['year'] -= 1980; + + // What voodoo is this? I have no idea... Geert can explain it though, + // and that's good enough for me. + return ($timestamp['year'] << 25 | $timestamp['mon'] << 21 | + $timestamp['mday'] << 16 | $timestamp['hours'] << 11 | + $timestamp['minutes'] << 5 | $timestamp['seconds'] >> 1); + } + + /** + * Converts a DOS timestamp to UNIX format. + * + * @param integer DOS timestamp + * @return integer + */ + public static function dos2unix($timestamp = FALSE) + { + $sec = 2 * ($timestamp & 0x1f); + $min = ($timestamp >> 5) & 0x3f; + $hrs = ($timestamp >> 11) & 0x1f; + $day = ($timestamp >> 16) & 0x1f; + $mon = ($timestamp >> 21) & 0x0f; + $year = ($timestamp >> 25) & 0x7f; + + return mktime($hrs, $min, $sec, $mon, $day, $year + 1980); + } + + /** + * Returns the offset (in seconds) between two time zones. + * @see http://php.net/timezones + * + * @param string timezone that to find the offset of + * @param string|boolean timezone used as the baseline + * @return integer + */ + public static function offset($remote, $local = TRUE) + { + static $offsets; + + // Default values + $remote = (string) $remote; + $local = ($local === TRUE) ? date_default_timezone_get() : (string) $local; + + // Cache key name + $cache = $remote.$local; + + if (empty($offsets[$cache])) + { + // Create timezone objects + $remote = new DateTimeZone($remote); + $local = new DateTimeZone($local); + + // Create date objects from timezones + $time_there = new DateTime('now', $remote); + $time_here = new DateTime('now', $local); + + // Find the offset + $offsets[$cache] = $remote->getOffset($time_there) - $local->getOffset($time_here); + } + + return $offsets[$cache]; + } + + /** + * Number of seconds in a minute, incrementing by a step. + * + * @param integer amount to increment each step by, 1 to 30 + * @param integer start value + * @param integer end value + * @return array A mirrored (foo => foo) array from 1-60. + */ + public static function seconds($step = 1, $start = 0, $end = 60) + { + // Always integer + $step = (int) $step; + + $seconds = array(); + + for ($i = $start; $i < $end; $i += $step) + { + $seconds[$i] = ($i < 10) ? '0'.$i : $i; + } + + return $seconds; + } + + /** + * Number of minutes in an hour, incrementing by a step. + * + * @param integer amount to increment each step by, 1 to 30 + * @return array A mirrored (foo => foo) array from 1-60. + */ + public static function minutes($step = 5) + { + // Because there are the same number of minutes as seconds in this set, + // we choose to re-use seconds(), rather than creating an entirely new + // function. Shhhh, it's cheating! ;) There are several more of these + // in the following methods. + return date::seconds($step); + } + + /** + * Number of hours in a day. + * + * @param integer amount to increment each step by + * @param boolean use 24-hour time + * @param integer the hour to start at + * @return array A mirrored (foo => foo) array from start-12 or start-23. + */ + public static function hours($step = 1, $long = FALSE, $start = NULL) + { + // Default values + $step = (int) $step; + $long = (bool) $long; + $hours = array(); + + // Set the default start if none was specified. + if ($start === NULL) + { + $start = ($long === FALSE) ? 1 : 0; + } + + $hours = array(); + + // 24-hour time has 24 hours, instead of 12 + $size = ($long === TRUE) ? 23 : 12; + + for ($i = $start; $i <= $size; $i += $step) + { + $hours[$i] = $i; + } + + return $hours; + } + + /** + * Returns AM or PM, based on a given hour. + * + * @param integer number of the hour + * @return string + */ + public static function ampm($hour) + { + // Always integer + $hour = (int) $hour; + + return ($hour > 11) ? 'PM' : 'AM'; + } + + /** + * Adjusts a non-24-hour number into a 24-hour number. + * + * @param integer hour to adjust + * @param string AM or PM + * @return string + */ + public static function adjust($hour, $ampm) + { + $hour = (int) $hour; + $ampm = strtolower($ampm); + + switch ($ampm) + { + case 'am': + if ($hour == 12) + $hour = 0; + break; + case 'pm': + if ($hour < 12) + $hour += 12; + break; + } + + return sprintf('%02s', $hour); + } + + /** + * Number of days in month. + * + * @param integer number of month + * @param integer number of year to check month, defaults to the current year + * @return array A mirrored (foo => foo) array of the days. + */ + public static function days($month, $year = FALSE) + { + static $months; + + // Always integers + $month = (int) $month; + $year = (int) $year; + + // Use the current year by default + $year = ($year == FALSE) ? date('Y') : $year; + + // We use caching for months, because time functions are used + if (empty($months[$year][$month])) + { + $months[$year][$month] = array(); + + // Use date to find the number of days in the given month + $total = date('t', mktime(1, 0, 0, $month, 1, $year)) + 1; + + for ($i = 1; $i < $total; $i++) + { + $months[$year][$month][$i] = $i; + } + } + + return $months[$year][$month]; + } + + /** + * Number of months in a year + * + * @return array A mirrored (foo => foo) array from 1-12. + */ + public static function months() + { + return date::hours(); + } + + /** + * Returns an array of years between a starting and ending year. + * Uses the current year +/- 5 as the max/min. + * + * @param integer starting year + * @param integer ending year + * @return array + */ + public static function years($start = FALSE, $end = FALSE) + { + // Default values + $start = ($start === FALSE) ? date('Y') - 5 : (int) $start; + $end = ($end === FALSE) ? date('Y') + 5 : (int) $end; + + $years = array(); + + // Add one, so that "less than" works + $end += 1; + + for ($i = $start; $i < $end; $i++) + { + $years[$i] = $i; + } + + return $years; + } + + /** + * Returns time difference between two timestamps, in human readable format. + * + * @param integer timestamp + * @param integer timestamp, defaults to the current time + * @param string formatting string + * @return string|array + */ + public static function timespan($time1, $time2 = NULL, $output = 'years,months,weeks,days,hours,minutes,seconds') + { + // Array with the output formats + $output = preg_split('/[^a-z]+/', strtolower((string) $output)); + + // Invalid output + if (empty($output)) + return FALSE; + + // Make the output values into keys + extract(array_flip($output), EXTR_SKIP); + + // Default values + $time1 = max(0, (int) $time1); + $time2 = empty($time2) ? time() : max(0, (int) $time2); + + // Calculate timespan (seconds) + $timespan = abs($time1 - $time2); + + // All values found using Google Calculator. + // Years and months do not match the formula exactly, due to leap years. + + // Years ago, 60 * 60 * 24 * 365 + isset($years) and $timespan -= 31556926 * ($years = (int) floor($timespan / 31556926)); + + // Months ago, 60 * 60 * 24 * 30 + isset($months) and $timespan -= 2629744 * ($months = (int) floor($timespan / 2629743.83)); + + // Weeks ago, 60 * 60 * 24 * 7 + isset($weeks) and $timespan -= 604800 * ($weeks = (int) floor($timespan / 604800)); + + // Days ago, 60 * 60 * 24 + isset($days) and $timespan -= 86400 * ($days = (int) floor($timespan / 86400)); + + // Hours ago, 60 * 60 + isset($hours) and $timespan -= 3600 * ($hours = (int) floor($timespan / 3600)); + + // Minutes ago, 60 + isset($minutes) and $timespan -= 60 * ($minutes = (int) floor($timespan / 60)); + + // Seconds ago, 1 + isset($seconds) and $seconds = $timespan; + + // Remove the variables that cannot be accessed + unset($timespan, $time1, $time2); + + // Deny access to these variables + $deny = array_flip(array('deny', 'key', 'difference', 'output')); + + // Return the difference + $difference = array(); + foreach ($output as $key) + { + if (isset($$key) AND ! isset($deny[$key])) + { + // Add requested key to the output + $difference[$key] = $$key; + } + } + + // Invalid output formats string + if (empty($difference)) + return FALSE; + + // If only one output format was asked, don't put it in an array + if (count($difference) === 1) + return current($difference); + + // Return array + return $difference; + } + + /** + * Returns time difference between two timestamps, in the format: + * N year, N months, N weeks, N days, N hours, N minutes, and N seconds ago + * + * @param integer timestamp + * @param integer timestamp, defaults to the current time + * @param string formatting string + * @return string + */ + public static function timespan_string($time1, $time2 = NULL, $output = 'years,months,weeks,days,hours,minutes,seconds') + { + if ($difference = date::timespan($time1, $time2, $output) AND is_array($difference)) + { + // Determine the key of the last item in the array + $last = end($difference); + $last = key($difference); + + $span = array(); + foreach ($difference as $name => $amount) + { + if ($name !== $last AND $amount === 0) + { + // Skip empty amounts + continue; + } + + // Add the amount to the span + $span[] = ($name === $last ? ' and ' : ', ').$amount.' '.($amount === 1 ? inflector::singular($name) : $name); + } + + // Replace difference by making the span into a string + $difference = trim(implode('', $span), ','); + } + elseif (is_int($difference)) + { + // Single-value return + $difference = $difference.' '.($difference === 1 ? inflector::singular($output) : $output); + } + + return $difference; + } + +} // End date
\ No newline at end of file diff --git a/kohana/helpers/download.php b/kohana/helpers/download.php new file mode 100644 index 00000000..9151208f --- /dev/null +++ b/kohana/helpers/download.php @@ -0,0 +1,105 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Download helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class download_Core { + + /** + * Force a download of a file to the user's browser. This function is + * binary-safe and will work with any MIME type that Kohana is aware of. + * + * @param string a file path or file name + * @param mixed data to be sent if the filename does not exist + * @param string suggested filename to display in the download + * @return void + */ + public static function force($filename = NULL, $data = NULL, $nicename = NULL) + { + if (empty($filename)) + return FALSE; + + if (is_file($filename)) + { + // Get the real path + $filepath = str_replace('\\', '/', realpath($filename)); + + // Set filesize + $filesize = filesize($filepath); + + // Get filename + $filename = substr(strrchr('/'.$filepath, '/'), 1); + + // Get extension + $extension = strtolower(substr(strrchr($filepath, '.'), 1)); + } + else + { + // Get filesize + $filesize = strlen($data); + + // Make sure the filename does not have directory info + $filename = substr(strrchr('/'.$filename, '/'), 1); + + // Get extension + $extension = strtolower(substr(strrchr($filename, '.'), 1)); + } + + // Get the mime type of the file + $mime = Kohana::config('mimes.'.$extension); + + if (empty($mime)) + { + // Set a default mime if none was found + $mime = array('application/octet-stream'); + } + + // Generate the server headers + header('Content-Type: '.$mime[0]); + header('Content-Disposition: attachment; filename="'.(empty($nicename) ? $filename : $nicename).'"'); + header('Content-Transfer-Encoding: binary'); + header('Content-Length: '.sprintf('%d', $filesize)); + + // More caching prevention + header('Expires: 0'); + + if (Kohana::user_agent('browser') === 'Internet Explorer') + { + // Send IE headers + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + } + else + { + // Send normal headers + header('Pragma: no-cache'); + } + + // Clear the output buffer + Kohana::close_buffers(FALSE); + + if (isset($filepath)) + { + // Open the file + $handle = fopen($filepath, 'rb'); + + // Send the file data + fpassthru($handle); + + // Close the file + fclose($handle); + } + else + { + // Send the file data + echo $data; + } + } + +} // End download
\ No newline at end of file diff --git a/kohana/helpers/email.php b/kohana/helpers/email.php new file mode 100644 index 00000000..89f29096 --- /dev/null +++ b/kohana/helpers/email.php @@ -0,0 +1,181 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Email helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class email_Core { + + // SwiftMailer instance + protected static $mail; + + /** + * Creates a SwiftMailer instance. + * + * @param string DSN connection string + * @return object Swift object + */ + public static function connect($config = NULL) + { + if ( ! class_exists('Swift', FALSE)) + { + // Load SwiftMailer + require Kohana::find_file('vendor', 'swift/Swift'); + + // Register the Swift ClassLoader as an autoload + spl_autoload_register(array('Swift_ClassLoader', 'load')); + } + + // Load default configuration + ($config === NULL) and $config = Kohana::config('email'); + + switch ($config['driver']) + { + case 'smtp': + // Set port + $port = empty($config['options']['port']) ? NULL : (int) $config['options']['port']; + + if (empty($config['options']['encryption'])) + { + // No encryption + $encryption = Swift_Connection_SMTP::ENC_OFF; + } + else + { + // Set encryption + switch (strtolower($config['options']['encryption'])) + { + case 'tls': $encryption = Swift_Connection_SMTP::ENC_TLS; break; + case 'ssl': $encryption = Swift_Connection_SMTP::ENC_SSL; break; + } + } + + // Create a SMTP connection + $connection = new Swift_Connection_SMTP($config['options']['hostname'], $port, $encryption); + + // Do authentication, if part of the DSN + empty($config['options']['username']) or $connection->setUsername($config['options']['username']); + empty($config['options']['password']) or $connection->setPassword($config['options']['password']); + + if ( ! empty($config['options']['auth'])) + { + // Get the class name and params + list ($class, $params) = arr::callback_string($config['options']['auth']); + + if ($class === 'PopB4Smtp') + { + // Load the PopB4Smtp class manually, due to its odd filename + require Kohana::find_file('vendor', 'swift/Swift/Authenticator/$PopB4Smtp$'); + } + + // Prepare the class name for auto-loading + $class = 'Swift_Authenticator_'.$class; + + // Attach the authenticator + $connection->attachAuthenticator(($params === NULL) ? new $class : new $class($params[0])); + } + + // Set the timeout to 5 seconds + $connection->setTimeout(empty($config['options']['timeout']) ? 5 : (int) $config['options']['timeout']); + break; + case 'sendmail': + // Create a sendmail connection + $connection = new Swift_Connection_Sendmail + ( + empty($config['options']) ? Swift_Connection_Sendmail::AUTO_DETECT : $config['options'] + ); + + // Set the timeout to 5 seconds + $connection->setTimeout(5); + break; + default: + // Use the native connection + $connection = new Swift_Connection_NativeMail; + break; + } + + // Create the SwiftMailer instance + return email::$mail = new Swift($connection); + } + + /** + * Send an email message. + * + * @param string|array recipient email (and name), or an array of To, Cc, Bcc names + * @param string|array sender email (and name) + * @param string message subject + * @param string message body + * @param boolean send email as HTML + * @return integer number of emails sent + */ + public static function send($to, $from, $subject, $message, $html = FALSE) + { + // Connect to SwiftMailer + (email::$mail === NULL) and email::connect(); + + // Determine the message type + $html = ($html === TRUE) ? 'text/html' : 'text/plain'; + + // Create the message + $message = new Swift_Message($subject, $message, $html, '8bit', 'utf-8'); + + if (is_string($to)) + { + // Single recipient + $recipients = new Swift_Address($to); + } + elseif (is_array($to)) + { + if (isset($to[0]) AND isset($to[1])) + { + // Create To: address set + $to = array('to' => $to); + } + + // Create a list of recipients + $recipients = new Swift_RecipientList; + + foreach ($to as $method => $set) + { + if ( ! in_array($method, array('to', 'cc', 'bcc'))) + { + // Use To: by default + $method = 'to'; + } + + // Create method name + $method = 'add'.ucfirst($method); + + if (is_array($set)) + { + // Add a recipient with name + $recipients->$method($set[0], $set[1]); + } + else + { + // Add a recipient without name + $recipients->$method($set); + } + } + } + + if (is_string($from)) + { + // From without a name + $from = new Swift_Address($from); + } + elseif (is_array($from)) + { + // From with a name + $from = new Swift_Address($from[0], $from[1]); + } + + return email::$mail->send($message, $recipients, $from); + } + +} // End email
\ No newline at end of file diff --git a/kohana/helpers/expires.php b/kohana/helpers/expires.php new file mode 100644 index 00000000..72bfd79b --- /dev/null +++ b/kohana/helpers/expires.php @@ -0,0 +1,110 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Controls headers that effect client caching of pages + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class expires_Core { + + /** + * Sets the amount of time before a page expires + * + * @param integer Seconds before the page expires + * @return boolean + */ + public static function set($seconds = 60) + { + if (expires::check_headers()) + { + $now = $expires = time(); + + // Set the expiration timestamp + $expires += $seconds; + + // Send headers + header('Last-Modified: '.gmdate('D, d M Y H:i:s T', $now)); + header('Expires: '.gmdate('D, d M Y H:i:s T', $expires)); + header('Cache-Control: max-age='.$seconds); + + return $expires; + } + + return FALSE; + } + + /** + * Checks to see if a page should be updated or send Not Modified status + * + * @param integer Seconds added to the modified time received to calculate what should be sent + * @return bool FALSE when the request needs to be updated + */ + public static function check($seconds = 60) + { + if ( ! empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) AND expires::check_headers()) + { + if (($strpos = strpos($_SERVER['HTTP_IF_MODIFIED_SINCE'], ';')) !== FALSE) + { + // IE6 and perhaps other IE versions send length too, compensate here + $mod_time = substr($_SERVER['HTTP_IF_MODIFIED_SINCE'], 0, $strpos); + } + else + { + $mod_time = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + } + + $mod_time = strtotime($mod_time); + $mod_time_diff = $mod_time + $seconds - time(); + + if ($mod_time_diff > 0) + { + // Re-send headers + header('Last-Modified: '.gmdate('D, d M Y H:i:s T', $mod_time)); + header('Expires: '.gmdate('D, d M Y H:i:s T', time() + $mod_time_diff)); + header('Cache-Control: max-age='.$mod_time_diff); + header('Status: 304 Not Modified', TRUE, 304); + + // Prevent any output + Event::add('system.display', array('expires', 'prevent_output')); + + // Exit to prevent other output + exit; + } + } + + return FALSE; + } + + /** + * Check headers already created to not step on download or Img_lib's feet + * + * @return boolean + */ + public static function check_headers() + { + foreach (headers_list() as $header) + { + if (stripos($header, 'Last-Modified:') === 0 OR stripos($header, 'Expires:') === 0) + { + return FALSE; + } + } + + return TRUE; + } + + /** + * Prevent any output from being displayed. Executed during system.display. + * + * @return void + */ + public static function prevent_output() + { + Kohana::$output = ''; + } + +} // End expires
\ No newline at end of file diff --git a/kohana/helpers/feed.php b/kohana/helpers/feed.php new file mode 100644 index 00000000..2049c7bd --- /dev/null +++ b/kohana/helpers/feed.php @@ -0,0 +1,116 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Feed helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class feed_Core { + + /** + * Parses a remote feed into an array. + * + * @param string remote feed URL + * @param integer item limit to fetch + * @return array + */ + public static function parse($feed, $limit = 0) + { + // Make limit an integer + $limit = (int) $limit; + + // Disable error reporting while opening the feed + $ER = error_reporting(0); + + // Allow loading by filename or raw XML string + $load = (is_file($feed) OR valid::url($feed)) ? 'simplexml_load_file' : 'simplexml_load_string'; + + // Load the feed + $feed = $load($feed, 'SimpleXMLElement', LIBXML_NOCDATA); + + // Restore error reporting + error_reporting($ER); + + // Feed could not be loaded + if ($feed === FALSE) + return array(); + + // Detect the feed type. RSS 1.0/2.0 and Atom 1.0 are supported. + $feed = isset($feed->channel) ? $feed->xpath('//item') : $feed->entry; + + $i = 0; + $items = array(); + + foreach ($feed as $item) + { + if ($limit > 0 AND $i++ === $limit) + break; + + $items[] = (array) $item; + } + + return $items; + } + + /** + * Creates a feed from the given parameters. + * + * @param array feed information + * @param array items to add to the feed + * @return string + */ + public static function create($info, $items, $format = 'rss2') + { + $info += array('title' => 'Generated Feed', 'link' => '', 'generator' => 'KohanaPHP'); + + $feed = '<?xml version="1.0"?><rss version="2.0"><channel></channel></rss>'; + $feed = simplexml_load_string($feed); + + foreach ($info as $name => $value) + { + if (($name === 'pubDate' OR $name === 'lastBuildDate') AND (is_int($value) OR ctype_digit($value))) + { + // Convert timestamps to RFC 822 formatted dates + $value = date(DATE_RFC822, $value); + } + elseif (($name === 'link' OR $name === 'docs') AND strpos($value, '://') === FALSE) + { + // Convert URIs to URLs + $value = url::site($value, 'http'); + } + + // Add the info to the channel + $feed->channel->addChild($name, $value); + } + + foreach ($items as $item) + { + // Add the item to the channel + $row = $feed->channel->addChild('item'); + + foreach ($item as $name => $value) + { + if ($name === 'pubDate' AND (is_int($value) OR ctype_digit($value))) + { + // Convert timestamps to RFC 822 formatted dates + $value = date(DATE_RFC822, $value); + } + elseif (($name === 'link' OR $name === 'guid') AND strpos($value, '://') === FALSE) + { + // Convert URIs to URLs + $value = url::site($value, 'http'); + } + + // Add the info to the row + $row->addChild($name, $value); + } + } + + return $feed->asXML(); + } + +} // End feed
\ No newline at end of file diff --git a/kohana/helpers/file.php b/kohana/helpers/file.php new file mode 100644 index 00000000..65268e08 --- /dev/null +++ b/kohana/helpers/file.php @@ -0,0 +1,177 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * File helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class file_Core { + + /** + * Attempt to get the mime type from a file. This method is horribly + * unreliable, due to PHP being horribly unreliable when it comes to + * determining the mime-type of a file. + * + * @param string filename + * @return string mime-type, if found + * @return boolean FALSE, if not found + */ + public static function mime($filename) + { + // Make sure the file is readable + if ( ! (is_file($filename) AND is_readable($filename))) + return FALSE; + + // Get the extension from the filename + $extension = strtolower(substr(strrchr($filename, '.'), 1)); + + if (preg_match('/^(?:jpe?g|png|[gt]if|bmp|swf)$/', $extension)) + { + // Disable error reporting + $ER = error_reporting(0); + + // Use getimagesize() to find the mime type on images + $mime = getimagesize($filename); + + // Turn error reporting back on + error_reporting($ER); + + // Return the mime type + if (isset($mime['mime'])) + return $mime['mime']; + } + + if (function_exists('finfo_open')) + { + // Use the fileinfo extension + $finfo = finfo_open(FILEINFO_MIME); + $mime = finfo_file($finfo, $filename); + finfo_close($finfo); + + // Return the mime type + return $mime; + } + + if (ini_get('mime_magic.magicfile') AND function_exists('mime_content_type')) + { + // Return the mime type using mime_content_type + return mime_content_type($filename); + } + + if ( ! empty($extension) AND is_array($mime = Kohana::config('mimes.'.$extension))) + { + // Return the mime-type guess, based on the extension + return $mime[0]; + } + + // Unable to find the mime-type + return FALSE; + } + + /** + * Split a file into pieces matching a specific size. + * + * @param string file to be split + * @param string directory to output to, defaults to the same directory as the file + * @param integer size, in MB, for each chunk to be + * @return integer The number of pieces that were created. + */ + public static function split($filename, $output_dir = FALSE, $piece_size = 10) + { + // Find output dir + $output_dir = ($output_dir == FALSE) ? pathinfo(str_replace('\\', '/', realpath($filename)), PATHINFO_DIRNAME) : str_replace('\\', '/', realpath($output_dir)); + $output_dir = rtrim($output_dir, '/').'/'; + + // Open files for writing + $input_file = fopen($filename, 'rb'); + + // Change the piece size to bytes + $piece_size = 1024 * 1024 * (int) $piece_size; // Size in bytes + + // Set up reading variables + $read = 0; // Number of bytes read + $piece = 1; // Current piece + $chunk = 1024 * 8; // Chunk size to read + + // Split the file + while ( ! feof($input_file)) + { + // Open a new piece + $piece_name = $filename.'.'.str_pad($piece, 3, '0', STR_PAD_LEFT); + $piece_open = @fopen($piece_name, 'wb+') or die('Could not write piece '.$piece_name); + + // Fill the current piece + while ($read < $piece_size AND $data = fread($input_file, $chunk)) + { + fwrite($piece_open, $data) or die('Could not write to open piece '.$piece_name); + $read += $chunk; + } + + // Close the current piece + fclose($piece_open); + + // Prepare to open a new piece + $read = 0; + $piece++; + + // Make sure that piece is valid + ($piece < 999) or die('Maximum of 999 pieces exceeded, try a larger piece size'); + } + + // Close input file + fclose($input_file); + + // Returns the number of pieces that were created + return ($piece - 1); + } + + /** + * Join a split file into a whole file. + * + * @param string split filename, without .000 extension + * @param string output filename, if different then an the filename + * @return integer The number of pieces that were joined. + */ + public static function join($filename, $output = FALSE) + { + if ($output == FALSE) + $output = $filename; + + // Set up reading variables + $piece = 1; // Current piece + $chunk = 1024 * 8; // Chunk size to read + + // Open output file + $output_file = @fopen($output, 'wb+') or die('Could not open output file '.$output); + + // Read each piece + while ($piece_open = @fopen(($piece_name = $filename.'.'.str_pad($piece, 3, '0', STR_PAD_LEFT)), 'rb')) + { + // Write the piece into the output file + while ( ! feof($piece_open)) + { + fwrite($output_file, fread($piece_open, $chunk)); + } + + // Close the current piece + fclose($piece_open); + + // Prepare for a new piece + $piece++; + + // Make sure piece is valid + ($piece < 999) or die('Maximum of 999 pieces exceeded'); + } + + // Close the output file + fclose($output_file); + + // Return the number of pieces joined + return ($piece - 1); + } + +} // End file
\ No newline at end of file diff --git a/kohana/helpers/form.php b/kohana/helpers/form.php new file mode 100644 index 00000000..b432fc78 --- /dev/null +++ b/kohana/helpers/form.php @@ -0,0 +1,526 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Form helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class form_Core { + + /** + * Generates an opening HTML form tag. + * + * @param string form action attribute + * @param array extra attributes + * @param array hidden fields to be created immediately after the form tag + * @return string + */ + public static function open($action = NULL, $attr = array(), $hidden = NULL) + { + // Make sure that the method is always set + empty($attr['method']) and $attr['method'] = 'post'; + + if ($attr['method'] !== 'post' AND $attr['method'] !== 'get') + { + // If the method is invalid, use post + $attr['method'] = 'post'; + } + + if ($action === NULL) + { + // Use the current URL as the default action + $action = url::site(Router::$complete_uri); + } + elseif (strpos($action, '://') === FALSE) + { + // Make the action URI into a URL + $action = url::site($action); + } + + // Set action + $attr['action'] = $action; + + // Form opening tag + $form = '<form'.form::attributes($attr).'>'."\n"; + + // Add hidden fields immediate after opening tag + empty($hidden) or $form .= form::hidden($hidden); + + return $form; + } + + /** + * Generates an opening HTML form tag that can be used for uploading files. + * + * @param string form action attribute + * @param array extra attributes + * @param array hidden fields to be created immediately after the form tag + * @return string + */ + public static function open_multipart($action = NULL, $attr = array(), $hidden = array()) + { + // Set multi-part form type + $attr['enctype'] = 'multipart/form-data'; + + return form::open($action, $attr, $hidden); + } + + /** + * Generates a fieldset opening tag. + * + * @param array html attributes + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function open_fieldset($data = NULL, $extra = '') + { + return '<fieldset'.html::attributes((array) $data).' '.$extra.'>'."\n"; + } + + /** + * Generates a fieldset closing tag. + * + * @return string + */ + public static function close_fieldset() + { + return '</fieldset>'."\n"; + } + + /** + * Generates a legend tag for use with a fieldset. + * + * @param string legend text + * @param array HTML attributes + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function legend($text = '', $data = NULL, $extra = '') + { + return '<legend'.form::attributes((array) $data).' '.$extra.'>'.$text.'</legend>'."\n"; + } + + /** + * Generates hidden form fields. + * You can pass a simple key/value string or an associative array with multiple values. + * + * @param string|array input name (string) or key/value pairs (array) + * @param string input value, if using an input name + * @return string + */ + public static function hidden($data, $value = '') + { + if ( ! is_array($data)) + { + $data = array + ( + $data => $value + ); + } + + $input = ''; + foreach ($data as $name => $value) + { + $attr = array + ( + 'type' => 'hidden', + 'name' => $name, + 'value' => $value + ); + + $input .= form::input($attr)."\n"; + } + + return $input; + } + + /** + * Creates an HTML form input tag. Defaults to a text type. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function input($data, $value = '', $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + // Type and value are required attributes + $data += array + ( + 'type' => 'text', + 'value' => $value + ); + + // For safe form data + $data['value'] = html::specialchars($data['value']); + + return '<input'.form::attributes($data).' '.$extra.' />'; + } + + /** + * Creates a HTML form password input tag. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function password($data, $value = '', $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + $data['type'] = 'password'; + + return form::input($data, $value, $extra); + } + + /** + * Creates an HTML form upload input tag. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function upload($data, $value = '', $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + $data['type'] = 'file'; + + return form::input($data, $value, $extra); + } + + /** + * Creates an HTML form textarea tag. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function textarea($data, $value = '', $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + // Use the value from $data if possible, or use $value + $value = isset($data['value']) ? $data['value'] : $value; + + // Value is not part of the attributes + unset($data['value']); + + return '<textarea'.form::attributes($data, 'textarea').' '.$extra.'>'.html::specialchars($value).'</textarea>'; + } + + /** + * Creates an HTML form select tag, or "dropdown menu". + * + * @param string|array input name or an array of HTML attributes + * @param array select options, when using a name + * @param string option key that should be selected by default + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function dropdown($data, $options = NULL, $selected = NULL, $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + else + { + if (isset($data['options'])) + { + // Use data options + $options = $data['options']; + } + + if (isset($data['selected'])) + { + // Use data selected + $selected = $data['selected']; + } + } + + // Selected value should always be a string + $selected = (string) $selected; + + $input = '<select'.form::attributes($data, 'select').' '.$extra.'>'."\n"; + foreach ((array) $options as $key => $val) + { + // Key should always be a string + $key = (string) $key; + + if (is_array($val)) + { + $input .= '<optgroup label="'.$key.'">'."\n"; + foreach ($val as $inner_key => $inner_val) + { + // Inner key should always be a string + $inner_key = (string) $inner_key; + + $sel = ($selected === $inner_key) ? ' selected="selected"' : ''; + $input .= '<option value="'.$inner_key.'"'.$sel.'>'.$inner_val.'</option>'."\n"; + } + $input .= '</optgroup>'."\n"; + } + else + { + $sel = ($selected === $key) ? ' selected="selected"' : ''; + $input .= '<option value="'.$key.'"'.$sel.'>'.$val.'</option>'."\n"; + } + } + $input .= '</select>'; + + return $input; + } + + /** + * Creates an HTML form checkbox input tag. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param boolean make the checkbox checked by default + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function checkbox($data, $value = '', $checked = FALSE, $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + $data['type'] = 'checkbox'; + + if ($checked == TRUE OR (isset($data['checked']) AND $data['checked'] == TRUE)) + { + $data['checked'] = 'checked'; + } + else + { + unset($data['checked']); + } + + return form::input($data, $value, $extra); + } + + /** + * Creates an HTML form radio input tag. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param boolean make the radio selected by default + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function radio($data = '', $value = '', $checked = FALSE, $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + $data['type'] = 'radio'; + + if ($checked == TRUE OR (isset($data['checked']) AND $data['checked'] == TRUE)) + { + $data['checked'] = 'checked'; + } + else + { + unset($data['checked']); + } + + return form::input($data, $value, $extra); + } + + /** + * Creates an HTML form submit input tag. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function submit($data = '', $value = '', $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + if (empty($data['name'])) + { + // Remove the name if it is empty + unset($data['name']); + } + + $data['type'] = 'submit'; + + return form::input($data, $value, $extra); + } + + /** + * Creates an HTML form button input tag. + * + * @param string|array input name or an array of HTML attributes + * @param string input value, when using a name + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function button($data = '', $value = '', $extra = '') + { + if ( ! is_array($data)) + { + $data = array('name' => $data); + } + + if (empty($data['name'])) + { + // Remove the name if it is empty + unset($data['name']); + } + + if (isset($data['value']) AND empty($value)) + { + $value = arr::remove('value', $data); + } + + return '<button'.form::attributes($data, 'button').' '.$extra.'>'.$value.'</button>'; + } + + /** + * Closes an open form tag. + * + * @param string string to be attached after the closing tag + * @return string + */ + public static function close($extra = '') + { + return '</form>'."\n".$extra; + } + + /** + * Creates an HTML form label tag. + * + * @param string|array label "for" name or an array of HTML attributes + * @param string label text or HTML + * @param string a string to be attached to the end of the attributes + * @return string + */ + public static function label($data = '', $text = '', $extra = '') + { + if ( ! is_array($data)) + { + if (strpos($data, '[') !== FALSE) + { + $data = preg_replace('/\[.*\]/', '', $data); + } + + $data = empty($data) ? array() : array('for' => $data); + } + + return '<label'.form::attributes($data).' '.$extra.'>'.$text.'</label>'; + } + + /** + * Sorts a key/value array of HTML attributes, putting form attributes first, + * and returns an attribute string. + * + * @param array HTML attributes array + * @return string + */ + public static function attributes($attr, $type = NULL) + { + if (empty($attr)) + return ''; + + if (isset($attr['name']) AND empty($attr['id']) AND strpos($attr['name'], '[') === FALSE) + { + if ($type === NULL AND ! empty($attr['type'])) + { + // Set the type by the attributes + $type = $attr['type']; + } + + switch ($type) + { + case 'text': + case 'textarea': + case 'password': + case 'select': + case 'checkbox': + case 'file': + case 'image': + case 'button': + case 'submit': + // Only specific types of inputs use name to id matching + $attr['id'] = $attr['name']; + break; + } + } + + $order = array + ( + 'action', + 'method', + 'type', + 'id', + 'name', + 'value', + 'src', + 'size', + 'maxlength', + 'rows', + 'cols', + 'accept', + 'tabindex', + 'accesskey', + 'align', + 'alt', + 'title', + 'class', + 'style', + 'selected', + 'checked', + 'readonly', + 'disabled' + ); + + $sorted = array(); + foreach ($order as $key) + { + if (isset($attr[$key])) + { + // Move the attribute to the sorted array + $sorted[$key] = $attr[$key]; + + // Remove the attribute from unsorted array + unset($attr[$key]); + } + } + + // Combine the sorted and unsorted attributes and create an HTML string + return html::attributes(array_merge($sorted, $attr)); + } + +} // End form
\ No newline at end of file diff --git a/kohana/helpers/format.php b/kohana/helpers/format.php new file mode 100644 index 00000000..dd37be11 --- /dev/null +++ b/kohana/helpers/format.php @@ -0,0 +1,66 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Format helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class format_Core { + + /** + * Formats a phone number according to the specified format. + * + * @param string phone number + * @param string format string + * @return string + */ + public static function phone($number, $format = '3-3-4') + { + // Get rid of all non-digit characters in number string + $number_clean = preg_replace('/\D+/', '', (string) $number); + + // Array of digits we need for a valid format + $format_parts = preg_split('/[^1-9][^0-9]*/', $format, -1, PREG_SPLIT_NO_EMPTY); + + // Number must match digit count of a valid format + if (strlen($number_clean) !== array_sum($format_parts)) + return $number; + + // Build regex + $regex = '(\d{'.implode('})(\d{', $format_parts).'})'; + + // Build replace string + for ($i = 1, $c = count($format_parts); $i <= $c; $i++) + { + $format = preg_replace('/(?<!\$)[1-9][0-9]*/', '\$'.$i, $format, 1); + } + + // Hocus pocus! + return preg_replace('/^'.$regex.'$/', $format, $number_clean); + } + + /** + * Formats a URL to contain a protocol at the beginning. + * + * @param string possibly incomplete URL + * @return string + */ + public static function url($str = '') + { + // Clear protocol-only strings like "http://" + if ($str === '' OR substr($str, -3) === '://') + return ''; + + // If no protocol given, prepend "http://" by default + if (strpos($str, '://') === FALSE) + return 'http://'.$str; + + // Return the original URL + return $str; + } + +} // End format
\ No newline at end of file diff --git a/kohana/helpers/html.php b/kohana/helpers/html.php new file mode 100644 index 00000000..3cb8d5a4 --- /dev/null +++ b/kohana/helpers/html.php @@ -0,0 +1,400 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * HTML helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class html_Core { + + // Enable or disable automatic setting of target="_blank" + public static $windowed_urls = FALSE; + + /** + * Convert special characters to HTML entities + * + * @param string string to convert + * @param boolean encode existing entities + * @return string + */ + public static function specialchars($str, $double_encode = TRUE) + { + // Force the string to be a string + $str = (string) $str; + + // Do encode existing HTML entities (default) + if ($double_encode === TRUE) + { + $str = htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); + } + else + { + // Do not encode existing HTML entities + // From PHP 5.2.3 this functionality is built-in, otherwise use a regex + if (version_compare(PHP_VERSION, '5.2.3', '>=')) + { + $str = htmlspecialchars($str, ENT_QUOTES, 'UTF-8', FALSE); + } + else + { + $str = preg_replace('/&(?!(?:#\d++|[a-z]++);)/ui', '&', $str); + $str = str_replace(array('<', '>', '\'', '"'), array('<', '>', ''', '"'), $str); + } + } + + return $str; + } + + /** + * Create HTML link anchors. + * + * @param string URL or URI string + * @param string link text + * @param array HTML anchor attributes + * @param string non-default protocol, eg: https + * @return string + */ + public static function anchor($uri, $title = NULL, $attributes = NULL, $protocol = NULL) + { + if ($uri === '') + { + $site_url = url::base(FALSE); + } + elseif (strpos($uri, '://') === FALSE AND strpos($uri, '#') !== 0) + { + $site_url = url::site($uri, $protocol); + } + else + { + if (html::$windowed_urls === TRUE AND empty($attributes['target'])) + { + $attributes['target'] = '_blank'; + } + + $site_url = $uri; + } + + return + // Parsed URL + '<a href="'.html::specialchars($site_url, FALSE).'"' + // Attributes empty? Use an empty string + .(is_array($attributes) ? html::attributes($attributes) : '').'>' + // Title empty? Use the parsed URL + .(($title === NULL) ? $site_url : $title).'</a>'; + } + + /** + * Creates an HTML anchor to a file. + * + * @param string name of file to link to + * @param string link text + * @param array HTML anchor attributes + * @param string non-default protocol, eg: ftp + * @return string + */ + public static function file_anchor($file, $title = NULL, $attributes = NULL, $protocol = NULL) + { + return + // Base URL + URI = full URL + '<a href="'.html::specialchars(url::base(FALSE, $protocol).$file, FALSE).'"' + // Attributes empty? Use an empty string + .(is_array($attributes) ? html::attributes($attributes) : '').'>' + // Title empty? Use the filename part of the URI + .(($title === NULL) ? end(explode('/', $file)) : $title) .'</a>'; + } + + /** + * Similar to anchor, but with the protocol parameter first. + * + * @param string link protocol + * @param string URI or URL to link to + * @param string link text + * @param array HTML anchor attributes + * @return string + */ + public static function panchor($protocol, $uri, $title = FALSE, $attributes = FALSE) + { + return html::anchor($uri, $title, $attributes, $protocol); + } + + /** + * Create an array of anchors from an array of link/title pairs. + * + * @param array link/title pairs + * @return array + */ + public static function anchor_array(array $array) + { + $anchors = array(); + foreach ($array as $link => $title) + { + // Create list of anchors + $anchors[] = html::anchor($link, $title); + } + return $anchors; + } + + /** + * Generates an obfuscated version of an email address. + * + * @param string email address + * @return string + */ + public static function email($email) + { + $safe = ''; + foreach (str_split($email) as $letter) + { + switch (($letter === '@') ? rand(1, 2) : rand(1, 3)) + { + // HTML entity code + case 1: $safe .= '&#'.ord($letter).';'; break; + // Hex character code + case 2: $safe .= '&#x'.dechex(ord($letter)).';'; break; + // Raw (no) encoding + case 3: $safe .= $letter; + } + } + + return $safe; + } + + /** + * Creates an email anchor. + * + * @param string email address to send to + * @param string link text + * @param array HTML anchor attributes + * @return string + */ + public static function mailto($email, $title = NULL, $attributes = NULL) + { + if (empty($email)) + return $title; + + // Remove the subject or other parameters that do not need to be encoded + if (strpos($email, '?') !== FALSE) + { + // Extract the parameters from the email address + list ($email, $params) = explode('?', $email, 2); + + // Make the params into a query string, replacing spaces + $params = '?'.str_replace(' ', '%20', $params); + } + else + { + // No parameters + $params = ''; + } + + // Obfuscate email address + $safe = html::email($email); + + // Title defaults to the encoded email address + empty($title) and $title = $safe; + + // Parse attributes + empty($attributes) or $attributes = html::attributes($attributes); + + // Encoded start of the href="" is a static encoded version of 'mailto:' + return '<a href="mailto:'.$safe.$params.'"'.$attributes.'>'.$title.'</a>'; + } + + /** + * Generate a "breadcrumb" list of anchors representing the URI. + * + * @param array segments to use as breadcrumbs, defaults to using Router::$segments + * @return string + */ + public static function breadcrumb($segments = NULL) + { + empty($segments) and $segments = Router::$segments; + + $array = array(); + while ($segment = array_pop($segments)) + { + $array[] = html::anchor + ( + // Complete URI for the URL + implode('/', $segments).'/'.$segment, + // Title for the current segment + ucwords(inflector::humanize($segment)) + ); + } + + // Retrun the array of all the segments + return array_reverse($array); + } + + /** + * Creates a meta tag. + * + * @param string|array tag name, or an array of tags + * @param string tag "content" value + * @return string + */ + public static function meta($tag, $value = NULL) + { + if (is_array($tag)) + { + $tags = array(); + foreach ($tag as $t => $v) + { + // Build each tag and add it to the array + $tags[] = html::meta($t, $v); + } + + // Return all of the tags as a string + return implode("\n", $tags); + } + + // Set the meta attribute value + $attr = in_array(strtolower($tag), Kohana::config('http.meta_equiv')) ? 'http-equiv' : 'name'; + + return '<meta '.$attr.'="'.$tag.'" content="'.$value.'" />'; + } + + /** + * Creates a stylesheet link. + * + * @param string|array filename, or array of filenames to match to array of medias + * @param string|array media type of stylesheet, or array to match filenames + * @param boolean include the index_page in the link + * @return string + */ + public static function stylesheet($style, $media = FALSE, $index = FALSE) + { + return html::link($style, 'stylesheet', 'text/css', '.css', $media, $index); + } + + /** + * Creates a link tag. + * + * @param string|array filename + * @param string|array relationship + * @param string|array mimetype + * @param string specifies suffix of the file + * @param string|array specifies on what device the document will be displayed + * @param boolean include the index_page in the link + * @return string + */ + public static function link($href, $rel, $type, $suffix = FALSE, $media = FALSE, $index = FALSE) + { + $compiled = ''; + + if (is_array($href)) + { + foreach ($href as $_href) + { + $_rel = is_array($rel) ? array_shift($rel) : $rel; + $_type = is_array($type) ? array_shift($type) : $type; + $_media = is_array($media) ? array_shift($media) : $media; + + $compiled .= html::link($_href, $_rel, $_type, $suffix, $_media, $index); + } + } + else + { + // Add the suffix only when it's not already present + $suffix = ( ! empty($suffix) AND strpos($href, $suffix) === FALSE) ? $suffix : ''; + $media = empty($media) ? '' : ' media="'.$media.'"'; + $compiled = '<link rel="'.$rel.'" type="'.$type.'" href="'.url::base((bool) $index).$href.$suffix.'"'.$media.' />'; + } + + return $compiled."\n"; + } + + /** + * Creates a script link. + * + * @param string|array filename + * @param boolean include the index_page in the link + * @return string + */ + public static function script($script, $index = FALSE) + { + $compiled = ''; + + if (is_array($script)) + { + foreach ($script as $name) + { + $compiled .= html::script($name, $index); + } + } + else + { + // Do not touch full URLs + if (strpos($script, '://') === FALSE) + { + // Add the suffix only when it's not already present + $suffix = (substr($script, -3) !== '.js') ? '.js' : ''; + $script = url::base((bool) $index).$script.$suffix; + } + + $compiled = '<script type="text/javascript" src="'.$script.'"></script>'; + } + + return $compiled."\n"; + } + + /** + * Creates a image link. + * + * @param string image source, or an array of attributes + * @param string|array image alt attribute, or an array of attributes + * @param boolean include the index_page in the link + * @return string + */ + public static function image($src = NULL, $alt = NULL, $index = FALSE) + { + // Create attribute list + $attributes = is_array($src) ? $src : array('src' => $src); + + if (is_array($alt)) + { + $attributes += $alt; + } + elseif ( ! empty($alt)) + { + // Add alt to attributes + $attributes['alt'] = $alt; + } + + if (strpos($attributes['src'], '://') === FALSE) + { + // Make the src attribute into an absolute URL + $attributes['src'] = url::base($index).$attributes['src']; + } + + return '<img'.html::attributes($attributes).' />'; + } + + /** + * Compiles an array of HTML attributes into an attribute string. + * + * @param string|array array of attributes + * @return string + */ + public static function attributes($attrs) + { + if (empty($attrs)) + return ''; + + if (is_string($attrs)) + return ' '.$attrs; + + $compiled = ''; + foreach ($attrs as $key => $val) + { + $compiled .= ' '.$key.'="'.$val.'"'; + } + + return $compiled; + } + +} // End html diff --git a/kohana/helpers/inflector.php b/kohana/helpers/inflector.php new file mode 100644 index 00000000..0e980390 --- /dev/null +++ b/kohana/helpers/inflector.php @@ -0,0 +1,193 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Inflector helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class inflector_Core { + + // Cached inflections + protected static $cache = array(); + + // Uncountable and irregular words + protected static $uncountable; + protected static $irregular; + + /** + * Checks if a word is defined as uncountable. + * + * @param string word to check + * @return boolean + */ + public static function uncountable($str) + { + if (self::$uncountable === NULL) + { + // Cache uncountables + self::$uncountable = Kohana::config('inflector.uncountable'); + + // Make uncountables mirroed + self::$uncountable = array_combine(self::$uncountable, self::$uncountable); + } + + return isset(self::$uncountable[strtolower($str)]); + } + + /** + * Makes a plural word singular. + * + * @param string word to singularize + * @param integer number of things + * @return string + */ + public static function singular($str, $count = NULL) + { + // Remove garbage + $str = strtolower(trim($str)); + + if (is_string($count)) + { + // Convert to integer when using a digit string + $count = (int) $count; + } + + // Do nothing with a single count + if ($count === 0 OR $count > 1) + return $str; + + // Cache key name + $key = 'singular_'.$str.$count; + + if (isset(self::$cache[$key])) + return self::$cache[$key]; + + if (inflector::uncountable($str)) + return self::$cache[$key] = $str; + + if (empty(self::$irregular)) + { + // Cache irregular words + self::$irregular = Kohana::config('inflector.irregular'); + } + + if ($irregular = array_search($str, self::$irregular)) + { + $str = $irregular; + } + elseif (preg_match('/[sxz]es$/', $str) OR preg_match('/[^aeioudgkprt]hes$/', $str)) + { + // Remove "es" + $str = substr($str, 0, -2); + } + elseif (preg_match('/[^aeiou]ies$/', $str)) + { + $str = substr($str, 0, -3).'y'; + } + elseif (substr($str, -1) === 's' AND substr($str, -2) !== 'ss') + { + $str = substr($str, 0, -1); + } + + return self::$cache[$key] = $str; + } + + /** + * Makes a singular word plural. + * + * @param string word to pluralize + * @return string + */ + public static function plural($str, $count = NULL) + { + // Remove garbage + $str = strtolower(trim($str)); + + if (is_string($count)) + { + // Convert to integer when using a digit string + $count = (int) $count; + } + + // Do nothing with singular + if ($count === 1) + return $str; + + // Cache key name + $key = 'plural_'.$str.$count; + + if (isset(self::$cache[$key])) + return self::$cache[$key]; + + if (inflector::uncountable($str)) + return self::$cache[$key] = $str; + + if (empty(self::$irregular)) + { + // Cache irregular words + self::$irregular = Kohana::config('inflector.irregular'); + } + + if (isset(self::$irregular[$str])) + { + $str = self::$irregular[$str]; + } + elseif (preg_match('/[sxz]$/', $str) OR preg_match('/[^aeioudgkprt]h$/', $str)) + { + $str .= 'es'; + } + elseif (preg_match('/[^aeiou]y$/', $str)) + { + // Change "y" to "ies" + $str = substr_replace($str, 'ies', -1); + } + else + { + $str .= 's'; + } + + // Set the cache and return + return self::$cache[$key] = $str; + } + + /** + * Makes a phrase camel case. + * + * @param string phrase to camelize + * @return string + */ + public static function camelize($str) + { + $str = 'x'.strtolower(trim($str)); + $str = ucwords(preg_replace('/[\s_]+/', ' ', $str)); + + return substr(str_replace(' ', '', $str), 1); + } + + /** + * Makes a phrase underscored instead of spaced. + * + * @param string phrase to underscore + * @return string + */ + public static function underscore($str) + { + return preg_replace('/\s+/', '_', trim($str)); + } + + /** + * Makes an underscored or dashed phrase human-reable. + * + * @param string phrase to make human-reable + * @return string + */ + public static function humanize($str) + { + return preg_replace('/[_-]+/', ' ', trim($str)); + } + +} // End inflector
\ No newline at end of file diff --git a/kohana/helpers/num.php b/kohana/helpers/num.php new file mode 100644 index 00000000..62516e81 --- /dev/null +++ b/kohana/helpers/num.php @@ -0,0 +1,26 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Number helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class num_Core { + + /** + * Round a number to the nearest nth + * + * @param integer number to round + * @param integer number to round to + * @return integer + */ + public static function round($number, $nearest = 5) + { + return round($number / $nearest) * $nearest; + } + +} // End num
\ No newline at end of file diff --git a/kohana/helpers/remote.php b/kohana/helpers/remote.php new file mode 100644 index 00000000..f37d156e --- /dev/null +++ b/kohana/helpers/remote.php @@ -0,0 +1,66 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Remote url/file helper. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class remote_Core { + + public static function status($url) + { + if ( ! valid::url($url, 'http')) + return FALSE; + + // Get the hostname and path + $url = parse_url($url); + + if (empty($url['path'])) + { + // Request the root document + $url['path'] = '/'; + } + + // Open a remote connection + $remote = fsockopen($url['host'], 80, $errno, $errstr, 5); + + if ( ! is_resource($remote)) + return FALSE; + + // Set CRLF + $CRLF = "\r\n"; + + // Send request + fwrite($remote, 'HEAD '.$url['path'].' HTTP/1.0'.$CRLF); + fwrite($remote, 'Host: '.$url['host'].$CRLF); + fwrite($remote, 'Connection: close'.$CRLF); + fwrite($remote, 'User-Agent: Kohana Framework (+http://kohanaphp.com/)'.$CRLF); + + // Send one more CRLF to terminate the headers + fwrite($remote, $CRLF); + + while ( ! feof($remote)) + { + // Get the line + $line = trim(fgets($remote, 512)); + + if ($line !== '' AND preg_match('#^HTTP/1\.[01] (\d{3})#', $line, $matches)) + { + // Response code found + $response = (int) $matches[1]; + + break; + } + } + + // Close the connection + fclose($remote); + + return isset($response) ? $response : FALSE; + } + +} // End remote
\ No newline at end of file diff --git a/kohana/helpers/request.php b/kohana/helpers/request.php new file mode 100644 index 00000000..17408643 --- /dev/null +++ b/kohana/helpers/request.php @@ -0,0 +1,217 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Request helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class request_Core { + + // Possible HTTP methods + protected static $http_methods = array('get', 'head', 'options', 'post', 'put', 'delete'); + + // Content types from client's HTTP Accept request header (array) + protected static $accept_types; + + /** + * Returns the HTTP referrer, or the default if the referrer is not set. + * + * @param mixed default to return + * @return string + */ + public static function referrer($default = FALSE) + { + if ( ! empty($_SERVER['HTTP_REFERER'])) + { + // Set referrer + $ref = $_SERVER['HTTP_REFERER']; + + if (strpos($ref, url::base(FALSE)) === 0) + { + // Remove the base URL from the referrer + $ref = substr($ref, strlen(url::base(TRUE))); + } + } + + return isset($ref) ? $ref : $default; + } + + /** + * Tests if the current request is an AJAX request by checking the X-Requested-With HTTP + * request header that most popular JS frameworks now set for AJAX calls. + * + * @return boolean + */ + public static function is_ajax() + { + return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) AND strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + } + + /** + * Returns current request method. + * + * @throws Kohana_Exception in case of an unknown request method + * @return string + */ + public static function method() + { + $method = strtolower($_SERVER['REQUEST_METHOD']); + + if ( ! in_array($method, self::$http_methods)) + throw new Kohana_Exception('request.unknown_method', $method); + + return $method; + } + + /** + * Returns boolean of whether client accepts content type. + * + * @param string content type + * @param boolean set to TRUE to disable wildcard checking + * @return boolean + */ + public static function accepts($type = NULL, $explicit_check = FALSE) + { + request::parse_accept_header(); + + if ($type === NULL) + return self::$accept_types; + + return (request::accepts_at_quality($type, $explicit_check) > 0); + } + + /** + * Compare the q values for given array of content types and return the one with the highest value. + * If items are found to have the same q value, the first one encountered in the given array wins. + * If all items in the given array have a q value of 0, FALSE is returned. + * + * @param array content types + * @param boolean set to TRUE to disable wildcard checking + * @return mixed string mime type with highest q value, FALSE if none of the given types are accepted + */ + public static function preferred_accept($types, $explicit_check = FALSE) + { + // Initialize + $mime_types = array(); + $max_q = 0; + $preferred = FALSE; + + // Load q values for all given content types + foreach (array_unique($types) as $type) + { + $mime_types[$type] = request::accepts_at_quality($type, $explicit_check); + } + + // Look for the highest q value + foreach ($mime_types as $type => $q) + { + if ($q > $max_q) + { + $max_q = $q; + $preferred = $type; + } + } + + return $preferred; + } + + /** + * Returns quality factor at which the client accepts content type. + * + * @param string content type (e.g. "image/jpg", "jpg") + * @param boolean set to TRUE to disable wildcard checking + * @return integer|float + */ + public static function accepts_at_quality($type = NULL, $explicit_check = FALSE) + { + request::parse_accept_header(); + + // Normalize type + $type = strtolower((string) $type); + + // General content type (e.g. "jpg") + if (strpos($type, '/') === FALSE) + { + // Don't accept anything by default + $q = 0; + + // Look up relevant mime types + foreach ((array) Kohana::config('mimes.'.$type) as $type) + { + $q2 = request::accepts_at_quality($type, $explicit_check); + $q = ($q2 > $q) ? $q2 : $q; + } + + return $q; + } + + // Content type with subtype given (e.g. "image/jpg") + $type = explode('/', $type, 2); + + // Exact match + if (isset(self::$accept_types[$type[0]][$type[1]])) + return self::$accept_types[$type[0]][$type[1]]; + + // Wildcard match (if not checking explicitly) + if ($explicit_check === FALSE AND isset(self::$accept_types[$type[0]]['*'])) + return self::$accept_types[$type[0]]['*']; + + // Catch-all wildcard match (if not checking explicitly) + if ($explicit_check === FALSE AND isset(self::$accept_types['*']['*'])) + return self::$accept_types['*']['*']; + + // Content type not accepted + return 0; + } + + /** + * Parses client's HTTP Accept request header, and builds array structure representing it. + * + * @return void + */ + protected static function parse_accept_header() + { + // Run this function just once + if (self::$accept_types !== NULL) + return; + + // Initialize accept_types array + self::$accept_types = array(); + + // No HTTP Accept header found + if (empty($_SERVER['HTTP_ACCEPT'])) + { + // Accept everything + self::$accept_types['*']['*'] = 1; + return; + } + + // Remove linebreaks and parse the HTTP Accept header + foreach (explode(',', str_replace(array("\r", "\n"), '', $_SERVER['HTTP_ACCEPT'])) as $accept_entry) + { + // Explode each entry in content type and possible quality factor + $accept_entry = explode(';', trim($accept_entry), 2); + + // Explode each content type (e.g. "text/html") + $type = explode('/', $accept_entry[0], 2); + + // Skip invalid content types + if ( ! isset($type[1])) + continue; + + // Assume a default quality factor of 1 if no custom q value found + $q = (isset($accept_entry[1]) AND preg_match('~\bq\s*+=\s*+([.0-9]+)~', $accept_entry[1], $match)) ? (float) $match[1] : 1; + + // Populate accept_types array + if ( ! isset(self::$accept_types[$type[0]][$type[1]]) OR $q > self::$accept_types[$type[0]][$type[1]]) + { + self::$accept_types[$type[0]][$type[1]] = $q; + } + } + } + +} // End request
\ No newline at end of file diff --git a/kohana/helpers/security.php b/kohana/helpers/security.php new file mode 100644 index 00000000..de723d76 --- /dev/null +++ b/kohana/helpers/security.php @@ -0,0 +1,47 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Security helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class security_Core { + + /** + * Sanitize a string with the xss_clean method. + * + * @param string string to sanitize + * @return string + */ + public static function xss_clean($str) + { + return Input::instance()->xss_clean($str); + } + + /** + * Remove image tags from a string. + * + * @param string string to sanitize + * @return string + */ + public static function strip_image_tags($str) + { + return preg_replace('#<img\s.*?(?:src\s*=\s*["\']?([^"\'<>\s]*)["\']?[^>]*)?>#is', '$1', $str); + } + + /** + * Remove PHP tags from a string. + * + * @param string string to sanitize + * @return string + */ + public static function encode_php_tags($str) + { + return str_replace(array('<?', '?>'), array('<?', '?>'), $str); + } + +} // End security
\ No newline at end of file diff --git a/kohana/helpers/text.php b/kohana/helpers/text.php new file mode 100644 index 00000000..ea648a19 --- /dev/null +++ b/kohana/helpers/text.php @@ -0,0 +1,389 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Text helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class text_Core { + + /** + * Limits a phrase to a given number of words. + * + * @param string phrase to limit words of + * @param integer number of words to limit to + * @param string end character or entity + * @return string + */ + public static function limit_words($str, $limit = 100, $end_char = NULL) + { + $limit = (int) $limit; + $end_char = ($end_char === NULL) ? '…' : $end_char; + + if (trim($str) === '') + return $str; + + if ($limit <= 0) + return $end_char; + + preg_match('/^\s*+(?:\S++\s*+){1,'.$limit.'}/u', $str, $matches); + + // Only attach the end character if the matched string is shorter + // than the starting string. + return rtrim($matches[0]).(strlen($matches[0]) === strlen($str) ? '' : $end_char); + } + + /** + * Limits a phrase to a given number of characters. + * + * @param string phrase to limit characters of + * @param integer number of characters to limit to + * @param string end character or entity + * @param boolean enable or disable the preservation of words while limiting + * @return string + */ + public static function limit_chars($str, $limit = 100, $end_char = NULL, $preserve_words = FALSE) + { + $end_char = ($end_char === NULL) ? '…' : $end_char; + + $limit = (int) $limit; + + if (trim($str) === '' OR utf8::strlen($str) <= $limit) + return $str; + + if ($limit <= 0) + return $end_char; + + if ($preserve_words == FALSE) + { + return rtrim(utf8::substr($str, 0, $limit)).$end_char; + } + + preg_match('/^.{'.($limit - 1).'}\S*/us', $str, $matches); + + return rtrim($matches[0]).(strlen($matches[0]) == strlen($str) ? '' : $end_char); + } + + /** + * Alternates between two or more strings. + * + * @param string strings to alternate between + * @return string + */ + public static function alternate() + { + static $i; + + if (func_num_args() === 0) + { + $i = 0; + return ''; + } + + $args = func_get_args(); + return $args[($i++ % count($args))]; + } + + /** + * Generates a random string of a given type and length. + * + * @param string a type of pool, or a string of characters to use as the pool + * @param integer length of string to return + * @return string + * + * @tutorial alnum - alpha-numeric characters + * @tutorial alpha - alphabetical characters + * @tutorial numeric - digit characters, 0-9 + * @tutorial nozero - digit characters, 1-9 + * @tutorial distinct - clearly distinct alpha-numeric characters + */ + public static function random($type = 'alnum', $length = 8) + { + $utf8 = FALSE; + + switch ($type) + { + case 'alnum': + $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + break; + case 'alpha': + $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + break; + case 'numeric': + $pool = '0123456789'; + break; + case 'nozero': + $pool = '123456789'; + break; + case 'distinct': + $pool = '2345679ACDEFHJKLMNPRSTUVWXYZ'; + break; + default: + $pool = (string) $type; + $utf8 = ! utf8::is_ascii($pool); + break; + } + + $str = ''; + + $pool_size = ($utf8 === TRUE) ? utf8::strlen($pool) : strlen($pool); + + for ($i = 0; $i < $length; $i++) + { + $str .= ($utf8 === TRUE) + ? utf8::substr($pool, mt_rand(0, $pool_size - 1), 1) + : substr($pool, mt_rand(0, $pool_size - 1), 1); + } + + return $str; + } + + /** + * Reduces multiple slashes in a string to single slashes. + * + * @param string string to reduce slashes of + * @return string + */ + public static function reduce_slashes($str) + { + return preg_replace('#(?<!:)//+#', '/', $str); + } + + /** + * Replaces the given words with a string. + * + * @param string phrase to replace words in + * @param array words to replace + * @param string replacement string + * @param boolean replace words across word boundries (space, period, etc) + * @return string + */ + public static function censor($str, $badwords, $replacement = '#', $replace_partial_words = FALSE) + { + foreach ((array) $badwords as $key => $badword) + { + $badwords[$key] = str_replace('\*', '\S*?', preg_quote((string) $badword)); + } + + $regex = '('.implode('|', $badwords).')'; + + if ($replace_partial_words == TRUE) + { + // Just using \b isn't sufficient when we need to replace a badword that already contains word boundaries itself + $regex = '(?<=\b|\s|^)'.$regex.'(?=\b|\s|$)'; + } + + $regex = '!'.$regex.'!ui'; + + if (utf8::strlen($replacement) == 1) + { + $regex .= 'e'; + return preg_replace($regex, 'str_repeat($replacement, utf8::strlen(\'$1\')', $str); + } + + return preg_replace($regex, $replacement, $str); + } + + /** + * Finds the text that is similar between a set of words. + * + * @param array words to find similar text of + * @return string + */ + public static function similar(array $words) + { + // First word is the word to match against + $word = current($words); + + for ($i = 0, $max = strlen($word); $i < $max; ++$i) + { + foreach ($words as $w) + { + // Once a difference is found, break out of the loops + if ( ! isset($w[$i]) OR $w[$i] !== $word[$i]) + break 2; + } + } + + // Return the similar text + return substr($word, 0, $i); + } + + /** + * Converts text email addresses and anchors into links. + * + * @param string text to auto link + * @return string + */ + public static function auto_link($text) + { + // Auto link emails first to prevent problems with "www.domain.com@example.com" + return text::auto_link_urls(text::auto_link_emails($text)); + } + + /** + * Converts text anchors into links. + * + * @param string text to auto link + * @return string + */ + public static function auto_link_urls($text) + { + // Finds all http/https/ftp/ftps links that are not part of an existing html anchor + if (preg_match_all('~\b(?<!href="|">)(?:ht|f)tps?://\S+(?:/|\b)~i', $text, $matches)) + { + foreach ($matches[0] as $match) + { + // Replace each link with an anchor + $text = str_replace($match, html::anchor($match), $text); + } + } + + // Find all naked www.links.com (without http://) + if (preg_match_all('~\b(?<!://)www(?:\.[a-z0-9][-a-z0-9]*+)+\.[a-z]{2,6}\b~i', $text, $matches)) + { + foreach ($matches[0] as $match) + { + // Replace each link with an anchor + $text = str_replace($match, html::anchor('http://'.$match, $match), $text); + } + } + + return $text; + } + + /** + * Converts text email addresses into links. + * + * @param string text to auto link + * @return string + */ + public static function auto_link_emails($text) + { + // Finds all email addresses that are not part of an existing html mailto anchor + // Note: The "58;" negative lookbehind prevents matching of existing encoded html mailto anchors + // The html entity for a colon (:) is : or : or : etc. + if (preg_match_all('~\b(?<!href="mailto:|">|58;)(?!\.)[-+_a-z0-9.]++(?<!\.)@(?![-.])[-a-z0-9.]+(?<!\.)\.[a-z]{2,6}\b~i', $text, $matches)) + { + foreach ($matches[0] as $match) + { + // Replace each email with an encoded mailto + $text = str_replace($match, html::mailto($match), $text); + } + } + + return $text; + } + + /** + * Automatically applies <p> and <br /> markup to text. Basically nl2br() on steroids. + * + * @param string subject + * @return string + */ + public static function auto_p($str) + { + // Trim whitespace + if (($str = trim($str)) === '') + return ''; + + // Standardize newlines + $str = str_replace(array("\r\n", "\r"), "\n", $str); + + // Trim whitespace on each line + $str = preg_replace('~^[ \t]+~m', '', $str); + $str = preg_replace('~[ \t]+$~m', '', $str); + + // The following regexes only need to be executed if the string contains html + if ($html_found = (strpos($str, '<') !== FALSE)) + { + // Elements that should not be surrounded by p tags + $no_p = '(?:p|div|h[1-6r]|ul|ol|li|blockquote|d[dlt]|pre|t[dhr]|t(?:able|body|foot|head)|c(?:aption|olgroup)|form|s(?:elect|tyle)|a(?:ddress|rea)|ma(?:p|th))'; + + // Put at least two linebreaks before and after $no_p elements + $str = preg_replace('~^<'.$no_p.'[^>]*+>~im', "\n$0", $str); + $str = preg_replace('~</'.$no_p.'\s*+>$~im', "$0\n", $str); + } + + // Do the <p> magic! + $str = '<p>'.trim($str).'</p>'; + $str = preg_replace('~\n{2,}~', "</p>\n\n<p>", $str); + + // The following regexes only need to be executed if the string contains html + if ($html_found !== FALSE) + { + // Remove p tags around $no_p elements + $str = preg_replace('~<p>(?=</?'.$no_p.'[^>]*+>)~i', '', $str); + $str = preg_replace('~(</?'.$no_p.'[^>]*+>)</p>~i', '$1', $str); + } + + // Convert single linebreaks to <br /> + $str = preg_replace('~(?<!\n)\n(?!\n)~', "<br />\n", $str); + + return $str; + } + + /** + * Returns human readable sizes. + * @see Based on original functions written by: + * @see Aidan Lister: http://aidanlister.com/repos/v/function.size_readable.php + * @see Quentin Zervaas: http://www.phpriot.com/d/code/strings/filesize-format/ + * + * @param integer size in bytes + * @param string a definitive unit + * @param string the return string format + * @param boolean whether to use SI prefixes or IEC + * @return string + */ + public static function bytes($bytes, $force_unit = NULL, $format = NULL, $si = TRUE) + { + // Format string + $format = ($format === NULL) ? '%01.2f %s' : (string) $format; + + // IEC prefixes (binary) + if ($si == FALSE OR strpos($force_unit, 'i') !== FALSE) + { + $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'); + $mod = 1024; + } + // SI prefixes (decimal) + else + { + $units = array('B', 'kB', 'MB', 'GB', 'TB', 'PB'); + $mod = 1000; + } + + // Determine unit to use + if (($power = array_search((string) $force_unit, $units)) === FALSE) + { + $power = ($bytes > 0) ? floor(log($bytes, $mod)) : 0; + } + + return sprintf($format, $bytes / pow($mod, $power), $units[$power]); + } + + /** + * Prevents widow words by inserting a non-breaking space between the last two words. + * @see http://www.shauninman.com/archive/2006/08/22/widont_wordpress_plugin + * + * @param string string to remove widows from + * @return string + */ + public static function widont($str) + { + $str = rtrim($str); + $space = strrpos($str, ' '); + + if ($space !== FALSE) + { + $str = substr($str, 0, $space).' '.substr($str, $space + 1); + } + + return $str; + } + +} // End text
\ No newline at end of file diff --git a/kohana/helpers/upload.php b/kohana/helpers/upload.php new file mode 100644 index 00000000..15839640 --- /dev/null +++ b/kohana/helpers/upload.php @@ -0,0 +1,162 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Upload helper class for working with the global $_FILES + * array and Validation library. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class upload_Core { + + /** + * Save an uploaded file to a new location. + * + * @param mixed name of $_FILE input or array of upload data + * @param string new filename + * @param string new directory + * @param integer chmod mask + * @return string full path to new file + */ + public static function save($file, $filename = NULL, $directory = NULL, $chmod = 0644) + { + // Load file data from FILES if not passed as array + $file = is_array($file) ? $file : $_FILES[$file]; + + if ($filename === NULL) + { + // Use the default filename, with a timestamp pre-pended + $filename = time().$file['name']; + } + + if (Kohana::config('upload.remove_spaces') === TRUE) + { + // Remove spaces from the filename + $filename = preg_replace('/\s+/', '_', $filename); + } + + if ($directory === NULL) + { + // Use the pre-configured upload directory + $directory = Kohana::config('upload.directory', TRUE); + } + + // Make sure the directory ends with a slash + $directory = rtrim($directory, '/').'/'; + + if ( ! is_dir($directory) AND Kohana::config('upload.create_directories') === TRUE) + { + // Create the upload directory + mkdir($directory, 0777, TRUE); + } + + if ( ! is_writable($directory)) + throw new Kohana_Exception('upload.not_writable', $directory); + + if (is_uploaded_file($file['tmp_name']) AND move_uploaded_file($file['tmp_name'], $filename = $directory.$filename)) + { + if ($chmod !== FALSE) + { + // Set permissions on filename + chmod($filename, $chmod); + } + + // Return new file path + return $filename; + } + + return FALSE; + } + + /* Validation Rules */ + + /** + * Tests if input data is valid file type, even if no upload is present. + * + * @param array $_FILES item + * @return bool + */ + public static function valid($file) + { + return (is_array($file) + AND isset($file['error']) + AND isset($file['name']) + AND isset($file['type']) + AND isset($file['tmp_name']) + AND isset($file['size'])); + } + + /** + * Tests if input data has valid upload data. + * + * @param array $_FILES item + * @return bool + */ + public static function required(array $file) + { + return (isset($file['tmp_name']) + AND isset($file['error']) + AND is_uploaded_file($file['tmp_name']) + AND (int) $file['error'] === UPLOAD_ERR_OK); + } + + /** + * Validation rule to test if an uploaded file is allowed by extension. + * + * @param array $_FILES item + * @param array allowed file extensions + * @return bool + */ + public static function type(array $file, array $allowed_types) + { + if ((int) $file['error'] !== UPLOAD_ERR_OK) + return TRUE; + + // Get the default extension of the file + $extension = strtolower(substr(strrchr($file['name'], '.'), 1)); + + // Get the mime types for the extension + $mime_types = Kohana::config('mimes.'.$extension); + + // Make sure there is an extension, that the extension is allowed, and that mime types exist + return ( ! empty($extension) AND in_array($extension, $allowed_types) AND is_array($mime_types)); + } + + /** + * Validation rule to test if an uploaded file is allowed by file size. + * File sizes are defined as: SB, where S is the size (1, 15, 300, etc) and + * B is the byte modifier: (B)ytes, (K)ilobytes, (M)egabytes, (G)igabytes. + * Eg: to limit the size to 1MB or less, you would use "1M". + * + * @param array $_FILES item + * @param array maximum file size + * @return bool + */ + public static function size(array $file, array $size) + { + if ((int) $file['error'] !== UPLOAD_ERR_OK) + return TRUE; + + // Only one size is allowed + $size = strtoupper($size[0]); + + if ( ! preg_match('/[0-9]++[BKMG]/', $size)) + return FALSE; + + // Make the size into a power of 1024 + switch (substr($size, -1)) + { + case 'G': $size = intval($size) * pow(1024, 3); break; + case 'M': $size = intval($size) * pow(1024, 2); break; + case 'K': $size = intval($size) * pow(1024, 1); break; + default: $size = intval($size); break; + } + + // Test that the file is under or equal to the max size + return ($file['size'] <= $size); + } + +} // End upload
\ No newline at end of file diff --git a/kohana/helpers/url.php b/kohana/helpers/url.php new file mode 100644 index 00000000..12453936 --- /dev/null +++ b/kohana/helpers/url.php @@ -0,0 +1,247 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * URL helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class url_Core { + + /** + * Fetches the current URI. + * + * @param boolean include the query string + * @return string + */ + public static function current($qs = FALSE) + { + return ($qs === TRUE) ? Router::$complete_uri : Router::$current_uri; + } + + /** + * Base URL, with or without the index page. + * + * If protocol (and core.site_protocol) and core.site_domain are both empty, + * then + * + * @param boolean include the index page + * @param boolean non-default protocol + * @return string + */ + public static function base($index = FALSE, $protocol = FALSE) + { + if ($protocol == FALSE) + { + // Use the default configured protocol + $protocol = Kohana::config('core.site_protocol'); + } + + // Load the site domain + $site_domain = (string) Kohana::config('core.site_domain', TRUE); + + if ($protocol == FALSE) + { + if ($site_domain === '' OR $site_domain[0] === '/') + { + // Use the configured site domain + $base_url = $site_domain; + } + else + { + // Guess the protocol to provide full http://domain/path URL + $base_url = ((empty($_SERVER['HTTPS']) OR $_SERVER['HTTPS'] === 'off') ? 'http' : 'https').'://'.$site_domain; + } + } + else + { + if ($site_domain === '' OR $site_domain[0] === '/') + { + // Guess the server name if the domain starts with slash + $base_url = $protocol.'://'.$_SERVER['HTTP_HOST'].$site_domain; + } + else + { + // Use the configured site domain + $base_url = $protocol.'://'.$site_domain; + } + } + + if ($index === TRUE AND $index = Kohana::config('core.index_page')) + { + // Append the index page + $base_url = $base_url.$index; + } + + // Force a slash on the end of the URL + return rtrim($base_url, '/').'/'; + } + + /** + * Fetches an absolute site URL based on a URI segment. + * + * @param string site URI to convert + * @param string non-default protocol + * @return string + */ + public static function site($uri = '', $protocol = FALSE) + { + if ($path = trim(parse_url($uri, PHP_URL_PATH), '/')) + { + // Add path suffix + $path .= Kohana::config('core.url_suffix'); + } + + if ($query = parse_url($uri, PHP_URL_QUERY)) + { + // ?query=string + $query = '?'.$query; + } + + if ($fragment = parse_url($uri, PHP_URL_FRAGMENT)) + { + // #fragment + $fragment = '#'.$fragment; + } + + // Concat the URL + return url::base(TRUE, $protocol).$path.$query.$fragment; + } + + /** + * Return the URL to a file. Absolute filenames and relative filenames + * are allowed. + * + * @param string filename + * @param boolean include the index page + * @return string + */ + public static function file($file, $index = FALSE) + { + if (strpos($file, '://') === FALSE) + { + // Add the base URL to the filename + $file = url::base($index).$file; + } + + return $file; + } + + /** + * Merges an array of arguments with the current URI and query string to + * overload, instead of replace, the current query string. + * + * @param array associative array of arguments + * @return string + */ + public static function merge(array $arguments) + { + if ($_GET === $arguments) + { + $query = Router::$query_string; + } + elseif ($query = http_build_query(array_merge($_GET, $arguments))) + { + $query = '?'.$query; + } + + // Return the current URI with the arguments merged into the query string + return Router::$current_uri.$query; + } + + /** + * Convert a phrase to a URL-safe title. + * + * @param string phrase to convert + * @param string word separator (- or _) + * @return string + */ + public static function title($title, $separator = '-') + { + $separator = ($separator === '-') ? '-' : '_'; + + // Replace accented characters by their unaccented equivalents + $title = utf8::transliterate_to_ascii($title); + + // Remove all characters that are not the separator, a-z, 0-9, or whitespace + $title = preg_replace('/[^'.$separator.'a-z0-9\s]+/', '', strtolower($title)); + + // Replace all separator characters and whitespace by a single separator + $title = preg_replace('/['.$separator.'\s]+/', $separator, $title); + + // Trim separators from the beginning and end + return trim($title, $separator); + } + + /** + * Sends a page redirect header. + * + * @param mixed string site URI or URL to redirect to, or array of strings if method is 300 + * @param string HTTP method of redirect + * @return void + */ + public static function redirect($uri = '', $method = '302') + { + if (Event::has_run('system.send_headers')) + return; + + $uri = (array) $uri; + + for ($i = 0, $count_uri = count($uri); $i < $count_uri; $i++) + { + if (strpos($uri[$i], '://') === FALSE) + { + $uri[$i] = url::site($uri[$i]); + } + } + + if ($method == '300') + { + if ($count_uri > 0) + { + header('HTTP/1.1 300 Multiple Choices'); + header('Location: '.$uri[0]); + + $choices = ''; + foreach ($uri as $href) + { + $choices .= '<li><a href="'.$href.'">'.$href.'</a></li>'; + } + + exit('<h1>301 - Multiple Choices:</h1><ul>'.$choices.'</ul>'); + } + } + else + { + $uri = $uri[0]; + + if ($method == 'refresh') + { + header('Refresh: 0; url='.$uri); + } + else + { + $codes = array + ( + '301' => 'Moved Permanently', + '302' => 'Found', + '303' => 'See Other', + '304' => 'Not Modified', + '305' => 'Use Proxy', + '307' => 'Temporary Redirect' + ); + + $method = isset($codes[$method]) ? $method : '302'; + + header('HTTP/1.1 '.$method.' '.$codes[$method]); + header('Location: '.$uri); + } + + exit('<h1>'.$method.' - '.$codes[$method].'</h1><p><a href="'.$uri.'">'.$uri.'</a></p>'); + } + } + +} // End url
\ No newline at end of file diff --git a/kohana/helpers/valid.php b/kohana/helpers/valid.php new file mode 100644 index 00000000..cee303fd --- /dev/null +++ b/kohana/helpers/valid.php @@ -0,0 +1,313 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Validation helper class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class valid_Core { + + /** + * Validate email, commonly used characters only + * + * @param string email address + * @return boolean + */ + public static function email($email) + { + return (bool) preg_match('/^[-_a-z0-9\'+*$^&%=~!?{}]++(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*+@(?:(?![-.])[-a-z0-9.]+(?<![-.])\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d++)?$/iD', (string) $email); + } + + /** + * Validate the domain of an email address by checking if the domain has a + * valid MX record. + * + * @param string email address + * @return boolean + */ + public static function email_domain($email) + { + // If we can't prove the domain is invalid, consider it valid + // Note: checkdnsrr() is not implemented on Windows platforms + if ( ! function_exists('checkdnsrr')) + return TRUE; + + // Check if the email domain has a valid MX record + return (bool) checkdnsrr(preg_replace('/^[^@]+@/', '', $email), 'MX'); + } + + /** + * Validate email, RFC compliant version + * Note: This function is LESS strict than valid_email. Choose carefully. + * + * @see Originally by Cal Henderson, modified to fit Kohana syntax standards: + * @see http://www.iamcal.com/publish/articles/php/parsing_email/ + * @see http://www.w3.org/Protocols/rfc822/ + * + * @param string email address + * @return boolean + */ + public static function email_rfc($email) + { + $qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'; + $dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'; + $atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'; + $pair = '\\x5c[\\x00-\\x7f]'; + + $domain_literal = "\\x5b($dtext|$pair)*\\x5d"; + $quoted_string = "\\x22($qtext|$pair)*\\x22"; + $sub_domain = "($atom|$domain_literal)"; + $word = "($atom|$quoted_string)"; + $domain = "$sub_domain(\\x2e$sub_domain)*"; + $local_part = "$word(\\x2e$word)*"; + $addr_spec = "$local_part\\x40$domain"; + + return (bool) preg_match('/^'.$addr_spec.'$/D', (string) $email); + } + + /** + * Validate URL + * + * @param string URL + * @return boolean + */ + public static function url($url) + { + return (bool) filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED); + } + + /** + * Validate IP + * + * @param string IP address + * @param boolean allow IPv6 addresses + * @return boolean + */ + public static function ip($ip, $ipv6 = FALSE) + { + // Do not allow private and reserved range IPs + $flags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE; + + if ($ipv6 === TRUE) + return (bool) filter_var($ip, FILTER_VALIDATE_IP, $flags); + + return (bool) filter_var($ip, FILTER_VALIDATE_IP, $flags | FILTER_FLAG_IPV4); + } + + /** + * Validates a credit card number using the Luhn (mod10) formula. + * @see http://en.wikipedia.org/wiki/Luhn_algorithm + * + * @param integer credit card number + * @param string|array card type, or an array of card types + * @return boolean + */ + public static function credit_card($number, $type = NULL) + { + // Remove all non-digit characters from the number + if (($number = preg_replace('/\D+/', '', $number)) === '') + return FALSE; + + if ($type == NULL) + { + // Use the default type + $type = 'default'; + } + elseif (is_array($type)) + { + foreach ($type as $t) + { + // Test each type for validity + if (valid::credit_card($number, $t)) + return TRUE; + } + + return FALSE; + } + + $cards = Kohana::config('credit_cards'); + + // Check card type + $type = strtolower($type); + + if ( ! isset($cards[$type])) + return FALSE; + + // Check card number length + $length = strlen($number); + + // Validate the card length by the card type + if ( ! in_array($length, preg_split('/\D+/', $cards[$type]['length']))) + return FALSE; + + // Check card number prefix + if ( ! preg_match('/^'.$cards[$type]['prefix'].'/', $number)) + return FALSE; + + // No Luhn check required + if ($cards[$type]['luhn'] == FALSE) + return TRUE; + + // Checksum of the card number + $checksum = 0; + + for ($i = $length - 1; $i >= 0; $i -= 2) + { + // Add up every 2nd digit, starting from the right + $checksum += substr($number, $i, 1); + } + + for ($i = $length - 2; $i >= 0; $i -= 2) + { + // Add up every 2nd digit doubled, starting from the right + $double = substr($number, $i, 1) * 2; + + // Subtract 9 from the double where value is greater than 10 + $checksum += ($double >= 10) ? $double - 9 : $double; + } + + // If the checksum is a multiple of 10, the number is valid + return ($checksum % 10 === 0); + } + + /** + * Checks if a phone number is valid. + * + * @param string phone number to check + * @return boolean + */ + public static function phone($number, $lengths = NULL) + { + if ( ! is_array($lengths)) + { + $lengths = array(7,10,11); + } + + // Remove all non-digit characters from the number + $number = preg_replace('/\D+/', '', $number); + + // Check if the number is within range + return in_array(strlen($number), $lengths); + } + + /** + * Checks whether a string consists of alphabetical characters only. + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function alpha($str, $utf8 = FALSE) + { + return ($utf8 === TRUE) + ? (bool) preg_match('/^\pL++$/uD', (string) $str) + : ctype_alpha((string) $str); + } + + /** + * Checks whether a string consists of alphabetical characters and numbers only. + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function alpha_numeric($str, $utf8 = FALSE) + { + return ($utf8 === TRUE) + ? (bool) preg_match('/^[\pL\pN]++$/uD', (string) $str) + : ctype_alnum((string) $str); + } + + /** + * Checks whether a string consists of alphabetical characters, numbers, underscores and dashes only. + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function alpha_dash($str, $utf8 = FALSE) + { + return ($utf8 === TRUE) + ? (bool) preg_match('/^[-\pL\pN_]++$/uD', (string) $str) + : (bool) preg_match('/^[-a-z0-9_]++$/iD', (string) $str); + } + + /** + * Checks whether a string consists of digits only (no dots or dashes). + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function digit($str, $utf8 = FALSE) + { + return ($utf8 === TRUE) + ? (bool) preg_match('/^\pN++$/uD', (string) $str) + : ctype_digit((string) $str); + } + + /** + * Checks whether a string is a valid number (negative and decimal numbers allowed). + * + * @param string input string + * @return boolean + */ + public static function numeric($str) + { + return (is_numeric($str) AND preg_match('/^[-0-9.]++$/D', (string) $str)); + } + + /** + * Checks whether a string is a valid text. Letters, numbers, whitespace, + * dashes, periods, and underscores are allowed. + * + * @param string $str + * @return boolean + */ + public static function standard_text($str) + { + return (bool) preg_match('/^[-\pL\pN\pZ_.]++$/uD', (string) $str); + } + + /** + * Checks if a string is a proper decimal format. The format array can be + * used to specify a decimal length, or a number and decimal length, eg: + * array(2) would force the number to have 2 decimal places, array(4,2) + * would force the number to have 4 digits and 2 decimal places. + * + * @param string input string + * @param array decimal format: y or x,y + * @return boolean + */ + public static function decimal($str, $format = NULL) + { + // Create the pattern + $pattern = '/^[0-9]%s\.[0-9]%s$/'; + + if ( ! empty($format)) + { + if (count($format) > 1) + { + // Use the format for number and decimal length + $pattern = sprintf($pattern, '{'.$format[0].'}', '{'.$format[1].'}'); + } + elseif (count($format) > 0) + { + // Use the format as decimal length + $pattern = sprintf($pattern, '+', '{'.$format[0].'}'); + } + } + else + { + // No format + $pattern = sprintf($pattern, '+', '+'); + } + + return (bool) preg_match($pattern, (string) $str); + } + +} // End valid
\ No newline at end of file diff --git a/kohana/i18n/en_US/cache.php b/kohana/i18n/en_US/cache.php new file mode 100644 index 00000000..40f15e71 --- /dev/null +++ b/kohana/i18n/en_US/cache.php @@ -0,0 +1,10 @@ +<?php defined('SYSPATH') or die('No direct script access.'); + +$lang = array +( + 'undefined_group' => 'The %s group is not defined in your configuration.', + 'extension_not_loaded' => 'The %s PHP extension must be loaded to use this driver.', + 'unwritable' => 'The configured storage location, %s, is not writable.', + 'resources' => 'Caching of resources is impossible, because resources cannot be serialized.', + 'driver_error' => '%s', +);
\ No newline at end of file diff --git a/kohana/i18n/en_US/calendar.php b/kohana/i18n/en_US/calendar.php new file mode 100644 index 00000000..3fcba613 --- /dev/null +++ b/kohana/i18n/en_US/calendar.php @@ -0,0 +1,59 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + // Two letter days + 'su' => 'Su', + 'mo' => 'Mo', + 'tu' => 'Tu', + 'we' => 'We', + 'th' => 'Th', + 'fr' => 'Fr', + 'sa' => 'Sa', + + // Short day names + 'sun' => 'Sun', + 'mon' => 'Mon', + 'tue' => 'Tue', + 'wed' => 'Wed', + 'thu' => 'Thu', + 'fri' => 'Fri', + 'sat' => 'Sat', + + // Long day names + 'sunday' => 'Sunday', + 'monday' => 'Monday', + 'tuesday' => 'Tuesday', + 'wednesday' => 'Wednesday', + 'thursday' => 'Thursday', + 'friday' => 'Friday', + 'saturday' => 'Saturday', + + // Short month names + 'jan' => 'Jan', + 'feb' => 'Feb', + 'mar' => 'Mar', + 'apr' => 'Apr', + 'may' => 'May', + 'jun' => 'Jun', + 'jul' => 'Jul', + 'aug' => 'Aug', + 'sep' => 'Sep', + 'oct' => 'Oct', + 'nov' => 'Nov', + 'dec' => 'Dec', + + // Long month names + 'january' => 'January', + 'february' => 'February', + 'march' => 'March', + 'april' => 'April', + 'mayl' => 'May', + 'june' => 'June', + 'july' => 'July', + 'august' => 'August', + 'september' => 'September', + 'october' => 'October', + 'november' => 'November', + 'december' => 'December' +);
\ No newline at end of file diff --git a/kohana/i18n/en_US/captcha.php b/kohana/i18n/en_US/captcha.php new file mode 100644 index 00000000..7aed9c4f --- /dev/null +++ b/kohana/i18n/en_US/captcha.php @@ -0,0 +1,33 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'file_not_found' => 'The specified file, %s, was not found. Please verify that files exist by using file_exists() before using them.', + 'requires_GD2' => 'The Captcha library requires GD2 with FreeType support. Please see http://php.net/gd_info for more information.', + + // Words of varying length for the Captcha_Word_Driver to pick from + // Note: use only alphanumeric characters + 'words' => array + ( + 'cd', 'tv', 'it', 'to', 'be', 'or', + 'sun', 'car', 'dog', 'bed', 'kid', 'egg', + 'bike', 'tree', 'bath', 'roof', 'road', 'hair', + 'hello', 'world', 'earth', 'beard', 'chess', 'water', + 'barber', 'bakery', 'banana', 'market', 'purple', 'writer', + 'america', 'release', 'playing', 'working', 'foreign', 'general', + 'aircraft', 'computer', 'laughter', 'alphabet', 'kangaroo', 'spelling', + 'architect', 'president', 'cockroach', 'encounter', 'terrorism', 'cylinders', + ), + + // Riddles for the Captcha_Riddle_Driver to pick from + // Note: use only alphanumeric characters + 'riddles' => array + ( + array('Do you hate spam? (yes or no)', 'yes'), + array('Are you a robot? (yes or no)', 'no'), + array('Fire is... (hot or cold)', 'hot'), + array('The season after fall is...', 'winter'), + array('Which day of the week is it today?', strftime('%A')), + array('Which month of the year are we in?', strftime('%B')), + ), +); diff --git a/kohana/i18n/en_US/core.php b/kohana/i18n/en_US/core.php new file mode 100644 index 00000000..9f09c64c --- /dev/null +++ b/kohana/i18n/en_US/core.php @@ -0,0 +1,34 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'there_can_be_only_one' => 'There can be only one instance of Kohana per page request', + 'uncaught_exception' => 'Uncaught %s: %s in file %s on line %s', + 'invalid_method' => 'Invalid method %s called in %s', + 'invalid_property' => 'The %s property does not exist in the %s class.', + 'log_dir_unwritable' => 'The log directory is not writable: %s', + 'resource_not_found' => 'The requested %s, %s, could not be found', + 'invalid_filetype' => 'The requested filetype, .%s, is not allowed in your view configuration file', + 'view_set_filename' => 'You must set the the view filename before calling render', + 'no_default_route' => 'Please set a default route in config/routes.php', + 'no_controller' => 'Kohana was not able to determine a controller to process this request: %s', + 'page_not_found' => 'The page you requested, %s, could not be found.', + 'stats_footer' => 'Loaded in {execution_time} seconds, using {memory_usage} of memory. Generated by Kohana v{kohana_version}.', + 'error_file_line' => '<tt>%s <strong>[%s]:</strong></tt>', + 'stack_trace' => 'Stack Trace', + 'generic_error' => 'Unable to Complete Request', + 'errors_disabled' => 'You can go to the <a href="%s">home page</a> or <a href="%s">try again</a>.', + + // Drivers + 'driver_implements' => 'The %s driver for the %s library must implement the %s interface', + 'driver_not_found' => 'The %s driver for the %s library could not be found', + + // Resource names + 'config' => 'config file', + 'controller' => 'controller', + 'helper' => 'helper', + 'library' => 'library', + 'driver' => 'driver', + 'model' => 'model', + 'view' => 'view', +); diff --git a/kohana/i18n/en_US/database.php b/kohana/i18n/en_US/database.php new file mode 100644 index 00000000..9f4624a6 --- /dev/null +++ b/kohana/i18n/en_US/database.php @@ -0,0 +1,15 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'undefined_group' => 'The %s group is not defined in your configuration.', + 'error' => 'There was an SQL error: %s', + 'connection' => 'There was an error connecting to the database: %s', + 'invalid_dsn' => 'The DSN you supplied is not valid: %s', + 'must_use_set' => 'You must set a SET clause for your query.', + 'must_use_where' => 'You must set a WHERE clause for your query.', + 'must_use_table' => 'You must set a database table for your query.', + 'table_not_found' => 'Table %s does not exist in your database.', + 'not_implemented' => 'The method you called, %s, is not supported by this driver.', + 'result_read_only' => 'Query results are read only.' +);
\ No newline at end of file diff --git a/kohana/i18n/en_US/encrypt.php b/kohana/i18n/en_US/encrypt.php new file mode 100644 index 00000000..7c1f1c6d --- /dev/null +++ b/kohana/i18n/en_US/encrypt.php @@ -0,0 +1,8 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'undefined_group' => 'The %s group is not defined in your configuration.', + 'requires_mcrypt' => 'To use the Encrypt library, mcrypt must be enabled in your PHP installation', + 'no_encryption_key' => 'To use the Encrypt library, you must set an encryption key in your config file' +); diff --git a/kohana/i18n/en_US/errors.php b/kohana/i18n/en_US/errors.php new file mode 100644 index 00000000..a218c2da --- /dev/null +++ b/kohana/i18n/en_US/errors.php @@ -0,0 +1,16 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + E_KOHANA => array( 1, 'Framework Error', 'Please check the Kohana documentation for information about the following error.'), + E_PAGE_NOT_FOUND => array( 1, 'Page Not Found', 'The requested page was not found. It may have moved, been deleted, or archived.'), + E_DATABASE_ERROR => array( 1, 'Database Error', 'A database error occurred while performing the requested procedure. Please review the database error below for more information.'), + E_RECOVERABLE_ERROR => array( 1, 'Recoverable Error', 'An error was detected which prevented the loading of this page. If this problem persists, please contact the website administrator.'), + E_ERROR => array( 1, 'Fatal Error', ''), + E_USER_ERROR => array( 1, 'Fatal Error', ''), + E_PARSE => array( 1, 'Syntax Error', ''), + E_WARNING => array( 1, 'Warning Message', ''), + E_USER_WARNING => array( 1, 'Warning Message', ''), + E_STRICT => array( 2, 'Strict Mode Error', ''), + E_NOTICE => array( 2, 'Runtime Message', ''), +);
\ No newline at end of file diff --git a/kohana/i18n/en_US/event.php b/kohana/i18n/en_US/event.php new file mode 100644 index 00000000..4118def1 --- /dev/null +++ b/kohana/i18n/en_US/event.php @@ -0,0 +1,7 @@ +<?php defined('SYSPATH') or die('No direct script access.'); + +$lang = array +( + 'invalid_subject' => 'Attempt to attach invalid subject %s to %s failed: Subjects must extend the Event_Subject class', + 'invalid_observer' => 'Attempt to attach invalid observer %s to %s failed: Observers must extend the Event_Observer class', +); diff --git a/kohana/i18n/en_US/image.php b/kohana/i18n/en_US/image.php new file mode 100644 index 00000000..07e030f9 --- /dev/null +++ b/kohana/i18n/en_US/image.php @@ -0,0 +1,27 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'getimagesize_missing' => 'The Image library requires the getimagesize() PHP function, which is not available in your installation.', + 'unsupported_method' => 'Your configured driver does not support the %s image transformation.', + 'file_not_found' => 'The specified image, %s, was not found. Please verify that images exist by using file_exists() before manipulating them.', + 'type_not_allowed' => 'The specified image, %s, is not an allowed image type.', + 'invalid_width' => 'The width you specified, %s, is not valid.', + 'invalid_height' => 'The height you specified, %s, is not valid.', + 'invalid_dimensions' => 'The dimensions specified for %s are not valid.', + 'invalid_master' => 'The master dimension specified is not valid.', + 'invalid_flip' => 'The flip direction specified is not valid.', + 'directory_unwritable' => 'The specified directory, %s, is not writable.', + + // ImageMagick specific messages + 'imagemagick' => array + ( + 'not_found' => 'The ImageMagick directory specified does not contain a required program, %s.', + ), + + // GD specific messages + 'gd' => array + ( + 'requires_v2' => 'The Image library requires GD2. Please see http://php.net/gd_info for more information.', + ), +); diff --git a/kohana/i18n/en_US/orm.php b/kohana/i18n/en_US/orm.php new file mode 100644 index 00000000..b3f71ac0 --- /dev/null +++ b/kohana/i18n/en_US/orm.php @@ -0,0 +1,3 @@ +<?php defined('SYSPATH') or die('No direct script access.'); + +$lang['query_methods_not_allowed'] = 'Query methods cannot be used through ORM';
\ No newline at end of file diff --git a/kohana/i18n/en_US/pagination.php b/kohana/i18n/en_US/pagination.php new file mode 100644 index 00000000..4ce2c196 --- /dev/null +++ b/kohana/i18n/en_US/pagination.php @@ -0,0 +1,15 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'undefined_group' => 'The %s group is not defined in your pagination configuration.', + 'page' => 'page', + 'pages' => 'pages', + 'item' => 'item', + 'items' => 'items', + 'of' => 'of', + 'first' => 'first', + 'last' => 'last', + 'previous' => 'previous', + 'next' => 'next', +); diff --git a/kohana/i18n/en_US/profiler.php b/kohana/i18n/en_US/profiler.php new file mode 100644 index 00000000..c372ea0a --- /dev/null +++ b/kohana/i18n/en_US/profiler.php @@ -0,0 +1,15 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'benchmarks' => 'Benchmarks', + 'post_data' => 'Post Data', + 'no_post' => 'No post data', + 'session_data' => 'Session Data', + 'no_session' => 'No session data', + 'queries' => 'Database Queries', + 'no_queries' => 'No queries', + 'no_database' => 'Database not loaded', + 'cookie_data' => 'Cookie Data', + 'no_cookie' => 'No cookie data', +); diff --git a/kohana/i18n/en_US/session.php b/kohana/i18n/en_US/session.php new file mode 100644 index 00000000..2eb21222 --- /dev/null +++ b/kohana/i18n/en_US/session.php @@ -0,0 +1,6 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'invalid_session_name' => 'The session_name, %s, is invalid. It must contain only alphanumeric characters and underscores. Also at least one letter must be present.', +);
\ No newline at end of file diff --git a/kohana/i18n/en_US/swift.php b/kohana/i18n/en_US/swift.php new file mode 100644 index 00000000..c2b00aef --- /dev/null +++ b/kohana/i18n/en_US/swift.php @@ -0,0 +1,6 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'general_error' => 'An error occurred while sending the email message.' +);
\ No newline at end of file diff --git a/kohana/i18n/en_US/upload.php b/kohana/i18n/en_US/upload.php new file mode 100644 index 00000000..153c5577 --- /dev/null +++ b/kohana/i18n/en_US/upload.php @@ -0,0 +1,6 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + 'not_writable' => 'The upload destination folder, %s, does not appear to be writable.', +);
\ No newline at end of file diff --git a/kohana/i18n/en_US/validation.php b/kohana/i18n/en_US/validation.php new file mode 100644 index 00000000..b970f500 --- /dev/null +++ b/kohana/i18n/en_US/validation.php @@ -0,0 +1,39 @@ +<?php defined('SYSPATH') or die('No direct access allowed.'); + +$lang = array +( + // Class errors + 'invalid_rule' => 'Invalid validation rule used: %s', + + // General errors + 'unknown_error' => 'Unknown validation error while validating the %s field.', + 'required' => 'The %s field is required.', + 'min_length' => 'The %s field must be at least %d characters long.', + 'max_length' => 'The %s field must be %d characters or fewer.', + 'exact_length' => 'The %s field must be exactly %d characters.', + 'in_array' => 'The %s field must be selected from the options listed.', + 'matches' => 'The %s field must match the %s field.', + 'valid_url' => 'The %s field must contain a valid URL.', + 'valid_email' => 'The %s field must contain a valid email address.', + 'valid_ip' => 'The %s field must contain a valid IP address.', + 'valid_type' => 'The %s field must only contain %s characters.', + 'range' => 'The %s field must be between specified ranges.', + 'regex' => 'The %s field does not match accepted input.', + 'depends_on' => 'The %s field depends on the %s field.', + + // Upload errors + 'user_aborted' => 'The %s file was aborted during upload.', + 'invalid_type' => 'The %s file is not an allowed file type.', + 'max_size' => 'The %s file you uploaded was too large. The maximum size allowed is %s.', + 'max_width' => 'The %s file you uploaded was too big. The maximum allowed width is %spx.', + 'max_height' => 'The %s file you uploaded was too big. The maximum allowed height is %spx.', + 'min_width' => 'The %s file you uploaded was too small. The minimum allowed width is %spx.', + 'min_height' => 'The %s file you uploaded was too small. The minimum allowed height is %spx.', + + // Field types + 'alpha' => 'alphabetical', + 'alpha_numeric' => 'alphabetical and numeric', + 'alpha_dash' => 'alphabetical, dash, and underscore', + 'digit' => 'digit', + 'numeric' => 'numeric', +); diff --git a/kohana/libraries/Cache.php b/kohana/libraries/Cache.php new file mode 100644 index 00000000..4ad19cdc --- /dev/null +++ b/kohana/libraries/Cache.php @@ -0,0 +1,228 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Provides a driver-based interface for finding, creating, and deleting cached + * resources. Caches are identified by a unique string. Tagging of caches is + * also supported, and caches can be found and deleted by id or tag. + * + * $Id$ + * + * @package Cache + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Cache_Core { + + // For garbage collection + protected static $loaded; + + // Configuration + protected $config; + + // Driver object + protected $driver; + + /** + * Returns a singleton instance of Cache. + * + * @param array configuration + * @return Cache_Core + */ + public static function instance($config = array()) + { + static $obj; + + // Create the Cache instance + ($obj === NULL) and $obj = new Cache($config); + + return $obj; + } + + /** + * Loads the configured driver and validates it. + * + * @param array|string custom configuration or config group name + * @return void + */ + public function __construct($config = FALSE) + { + if (is_string($config)) + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('cache.'.$config)) === NULL) + throw new Kohana_Exception('cache.undefined_group', $name); + } + + if (is_array($config)) + { + // Append the default configuration options + $config += Kohana::config('cache.default'); + } + else + { + // Load the default group + $config = Kohana::config('cache.default'); + } + + // Cache the config in the object + $this->config = $config; + + // Set driver name + $driver = 'Cache_'.ucfirst($this->config['driver']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('core.driver_not_found', $this->config['driver'], get_class($this)); + + // Initialize the driver + $this->driver = new $driver($this->config['params']); + + // Validate the driver + if ( ! ($this->driver instanceof Cache_Driver)) + throw new Kohana_Exception('core.driver_implements', $this->config['driver'], get_class($this), 'Cache_Driver'); + + Kohana::log('debug', 'Cache Library initialized'); + + if (self::$loaded !== TRUE) + { + $this->config['requests'] = (int) $this->config['requests']; + + if ($this->config['requests'] > 0 AND mt_rand(1, $this->config['requests']) === 1) + { + // Do garbage collection + $this->driver->delete_expired(); + + Kohana::log('debug', 'Cache: Expired caches deleted.'); + } + + // Cache has been loaded once + self::$loaded = TRUE; + } + } + + /** + * Fetches a cache by id. Non-string cache items are automatically + * unserialized before the cache is returned. NULL is returned when + * a cache item is not found. + * + * @param string cache id + * @return mixed cached data or NULL + */ + public function get($id) + { + // Change slashes to colons + $id = str_replace(array('/', '\\'), '=', $id); + + if ($data = $this->driver->get($id)) + { + if (substr($data, 0, 14) === '<{serialized}>') + { + // Data has been serialized, unserialize now + $data = unserialize(substr($data, 14)); + } + } + + return $data; + } + + /** + * Fetches all of the caches for a given tag. An empty array will be + * returned when no matching caches are found. + * + * @param string cache tag + * @return array all cache items matching the tag + */ + public function find($tag) + { + if ($ids = $this->driver->find($tag)) + { + $data = array(); + foreach ($ids as $id) + { + // Load each cache item and add it to the array + if (($cache = $this->get($id)) !== NULL) + { + $data[$id] = $cache; + } + } + + return $data; + } + + return array(); + } + + /** + * Set a cache item by id. Tags may also be added and a custom lifetime + * can be set. Non-string data is automatically serialized. + * + * @param string unique cache id + * @param mixed data to cache + * @param array tags for this item + * @param integer number of seconds until the cache expires + * @return boolean + */ + function set($id, $data, $tags = NULL, $lifetime = NULL) + { + if (is_resource($data)) + throw new Kohana_Exception('cache.resources'); + + // Change slashes to colons + $id = str_replace(array('/', '\\'), '=', $id); + + if ( ! is_string($data)) + { + // Serialize all non-string data, so that types can be preserved + $data = '<{serialized}>'.serialize($data); + } + + // Make sure that tags is an array + $tags = empty($tags) ? array() : (array) $tags; + + if ($lifetime === NULL) + { + // Get the default lifetime + $lifetime = $this->config['lifetime']; + } + + return $this->driver->set($id, $data, $tags, $lifetime); + } + + /** + * Delete a cache item by id. + * + * @param string cache id + * @return boolean + */ + public function delete($id) + { + // Change slashes to colons + $id = str_replace(array('/', '\\'), '=', $id); + + return $this->driver->delete($id); + } + + /** + * Delete all cache items with a given tag. + * + * @param string cache tag name + * @return boolean + */ + public function delete_tag($tag) + { + return $this->driver->delete(FALSE, $tag); + } + + /** + * Delete ALL cache items items. + * + * @return boolean + */ + public function delete_all() + { + return $this->driver->delete(TRUE); + } + +} // End Cache diff --git a/kohana/libraries/Calendar.php b/kohana/libraries/Calendar.php new file mode 100644 index 00000000..61b57366 --- /dev/null +++ b/kohana/libraries/Calendar.php @@ -0,0 +1,361 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Calendar creation library. + * + * $Id$ + * + * @package Calendar + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Calendar_Core extends Event_Subject { + + // Start the calendar on Sunday by default + public static $start_monday = FALSE; + + // Month and year to use for calendaring + protected $month; + protected $year; + + // Week starts on Sunday + protected $week_start = 0; + + // Observed data + protected $observed_data; + + /** + * Returns an array of the names of the days, using the current locale. + * + * @param boolean return short names + * @return array + */ + public static function days($short = FALSE) + { + // strftime day format + $format = ($short == TRUE) ? '%a' : '%A'; + + // Days of the week + $days = array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'); + + if (Calendar::$start_monday === TRUE) + { + // Push Sunday to the end of the days + array_push($days, array_shift($days)); + } + + if (strpos(Kohana::config('locale.language.0'), 'en') !== 0) + { + // This is a bit awkward, but it works properly and is reliable + foreach ($days as $i => $day) + { + // Convert the English names to i18n names + $days[$i] = strftime($format, strtotime($day)); + } + } + elseif ($short == TRUE) + { + foreach ($days as $i => $day) + { + // Shorten the day names to 3 letters + $days[$i] = substr($day, 0, 3); + } + } + + return $days; + } + + /** + * Create a new Calendar instance. A month and year can be specified. + * By default, the current month and year are used. + * + * @param integer month number + * @param integer year number + * @return object + */ + public static function factory($month = NULL, $year = NULL) + { + return new Calendar($month, $year); + } + + /** + * Create a new Calendar instance. A month and year can be specified. + * By default, the current month and year are used. + * + * @param integer month number + * @param integer year number + * @return void + */ + public function __construct($month = NULL, $year = NULL) + { + empty($month) and $month = date('n'); // Current month + empty($year) and $year = date('Y'); // Current year + + // Set the month and year + $this->month = (int) $month; + $this->year = (int) $year; + + if (Calendar::$start_monday === TRUE) + { + // Week starts on Monday + $this->week_start = 1; + } + } + + /** + * Allows fetching the current month and year. + * + * @param string key to get + * @return mixed + */ + public function __get($key) + { + if ($key === 'month' OR $key === 'year') + { + return $this->$key; + } + } + + /** + * Calendar_Event factory method. + * + * @param string unique name for the event + * @return object Calendar_Event + */ + public function event($name = NULL) + { + return new Calendar_Event($this); + } + + /** + * Calendar_Event factory method. + * + * @chainable + * @param string standard event type + * @return object + */ + public function standard($name) + { + switch ($name) + { + case 'today': + // Add an event for the current day + $this->attach($this->event()->condition('timestamp', strtotime('today'))->add_class('today')); + break; + case 'prev-next': + // Add an event for padding days + $this->attach($this->event()->condition('current', FALSE)->add_class('prev-next')); + break; + case 'holidays': + // Base event + $event = $this->event()->condition('current', TRUE)->add_class('holiday'); + + // Attach New Years + $holiday = clone $event; + $this->attach($holiday->condition('month', 1)->condition('day', 1)); + + // Attach Valentine's Day + $holiday = clone $event; + $this->attach($holiday->condition('month', 2)->condition('day', 14)); + + // Attach St. Patrick's Day + $holiday = clone $event; + $this->attach($holiday->condition('month', 3)->condition('day', 17)); + + // Attach Easter + $holiday = clone $event; + $this->attach($holiday->condition('easter', TRUE)); + + // Attach Memorial Day + $holiday = clone $event; + $this->attach($holiday->condition('month', 5)->condition('day_of_week', 1)->condition('last_occurrence', TRUE)); + + // Attach Independance Day + $holiday = clone $event; + $this->attach($holiday->condition('month', 7)->condition('day', 4)); + + // Attach Labor Day + $holiday = clone $event; + $this->attach($holiday->condition('month', 9)->condition('day_of_week', 1)->condition('occurrence', 1)); + + // Attach Halloween + $holiday = clone $event; + $this->attach($holiday->condition('month', 10)->condition('day', 31)); + + // Attach Thanksgiving + $holiday = clone $event; + $this->attach($holiday->condition('month', 11)->condition('day_of_week', 4)->condition('occurrence', 4)); + + // Attach Christmas + $holiday = clone $event; + $this->attach($holiday->condition('month', 12)->condition('day', 25)); + break; + case 'weekends': + // Weekend events + $this->attach($this->event()->condition('weekend', TRUE)->add_class('weekend')); + break; + } + + return $this; + } + + /** + * Returns an array for use with a view. The array contains an array for + * each week. Each week contains 7 arrays, with a day number and status: + * TRUE if the day is in the month, FALSE if it is padding. + * + * @return array + */ + public function weeks() + { + // First day of the month as a timestamp + $first = mktime(1, 0, 0, $this->month, 1, $this->year); + + // Total number of days in this month + $total = (int) date('t', $first); + + // Last day of the month as a timestamp + $last = mktime(1, 0, 0, $this->month, $total, $this->year); + + // Make the month and week empty arrays + $month = $week = array(); + + // Number of days added. When this reaches 7, start a new week + $days = 0; + $week_number = 1; + + if (($w = (int) date('w', $first) - $this->week_start) < 0) + { + $w = 6; + } + + if ($w > 0) + { + // Number of days in the previous month + $n = (int) date('t', mktime(1, 0, 0, $this->month - 1, 1, $this->year)); + + // i = number of day, t = number of days to pad + for ($i = $n - $w + 1, $t = $w; $t > 0; $t--, $i++) + { + // Notify the listeners + $this->notify(array($this->month - 1, $i, $this->year, $week_number, FALSE)); + + // Add previous month padding days + $week[] = array($i, FALSE, $this->observed_data); + $days++; + } + } + + // i = number of day + for ($i = 1; $i <= $total; $i++) + { + if ($days % 7 === 0) + { + // Start a new week + $month[] = $week; + $week = array(); + + $week_number++; + } + + // Notify the listeners + $this->notify(array($this->month, $i, $this->year, $week_number, TRUE)); + + // Add days to this month + $week[] = array($i, TRUE, $this->observed_data); + $days++; + } + + if (($w = (int) date('w', $last) - $this->week_start) < 0) + { + $w = 6; + } + + if ($w >= 0) + { + // i = number of day, t = number of days to pad + for ($i = 1, $t = 6 - $w; $t > 0; $t--, $i++) + { + // Notify the listeners + $this->notify(array($this->month + 1, $i, $this->year, $week_number, FALSE)); + + // Add next month padding days + $week[] = array($i, FALSE, $this->observed_data); + } + } + + if ( ! empty($week)) + { + // Append the remaining days + $month[] = $week; + } + + return $month; + } + + /** + * Adds new data from an observer. All event data contains and array of CSS + * classes and an array of output messages. + * + * @param array observer data. + * @return void + */ + public function add_data(array $data) + { + // Add new classes + $this->observed_data['classes'] += $data['classes']; + + if ( ! empty($data['output'])) + { + // Only add output if it's not empty + $this->observed_data['output'][] = $data['output']; + } + } + + /** + * Resets the observed data and sends a notify to all attached events. + * + * @param array UNIX timestamp + * @return void + */ + public function notify($data) + { + // Reset observed data + $this->observed_data = array + ( + 'classes' => array(), + 'output' => array(), + ); + + // Send a notify + parent::notify($data); + } + + /** + * Convert the calendar to HTML using the kohana_calendar view. + * + * @return string + */ + public function render() + { + $view = new View('kohana_calendar', array + ( + 'month' => $this->month, + 'year' => $this->year, + 'weeks' => $this->weeks(), + )); + + return $view->render(); + } + + /** + * Magically convert this object to a string, the rendered calendar. + * + * @return string + */ + public function __toString() + { + return $this->render(); + } + +} // End Calendar
\ No newline at end of file diff --git a/kohana/libraries/Calendar_Event.php b/kohana/libraries/Calendar_Event.php new file mode 100644 index 00000000..9878924e --- /dev/null +++ b/kohana/libraries/Calendar_Event.php @@ -0,0 +1,297 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Calendar event observer class. + * + * $Id$ + * + * @package Calendar + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Calendar_Event_Core extends Event_Observer { + + // Boolean conditions + protected $booleans = array + ( + 'current', + 'weekend', + 'first_day', + 'last_day', + 'last_occurrence', + 'easter', + ); + + // Rendering conditions + protected $conditions = array(); + + // Cell classes + protected $classes = array(); + + // Cell output + protected $output = ''; + + /** + * Adds a condition to the event. The condition can be one of the following: + * + * timestamp - UNIX timestamp + * day - day number (1-31) + * week - week number (1-5) + * month - month number (1-12) + * year - year number (4 digits) + * day_of_week - day of week (1-7) + * current - active month (boolean) (only show data for the month being rendered) + * weekend - weekend day (boolean) + * first_day - first day of month (boolean) + * last_day - last day of month (boolean) + * occurrence - occurrence of the week day (1-5) (use with "day_of_week") + * last_occurrence - last occurrence of week day (boolean) (use with "day_of_week") + * easter - Easter day (boolean) + * callback - callback test (boolean) + * + * To unset a condition, call condition with a value of NULL. + * + * @chainable + * @param string condition key + * @param mixed condition value + * @return object + */ + public function condition($key, $value) + { + if ($value === NULL) + { + unset($this->conditions[$key]); + } + else + { + if ($key === 'callback') + { + // Do nothing + } + elseif (in_array($key, $this->booleans)) + { + // Make the value boolean + $value = (bool) $value; + } + else + { + // Make the value an int + $value = (int) $value; + } + + $this->conditions[$key] = $value; + } + + return $this; + } + + /** + * Add a CSS class for this event. This can be called multiple times. + * + * @chainable + * @param string CSS class name + * @return object + */ + public function add_class($class) + { + $this->classes[$class] = $class; + + return $this; + } + + /** + * Remove a CSS class for this event. This can be called multiple times. + * + * @chainable + * @param string CSS class name + * @return object + */ + public function remove_class($class) + { + unset($this->classes[$class]); + + return $this; + } + + /** + * Set HTML output for this event. + * + * @chainable + * @param string HTML output + * @return object + */ + public function output($str) + { + $this->output = $str; + + return $this; + } + + /** + * Add a CSS class for this event. This can be called multiple times. + * + * @chainable + * @param string CSS class name + * @return object + */ + public function notify($data) + { + // Split the date and current status + list ($month, $day, $year, $week, $current) = $data; + + // Get a timestamp for the day + $timestamp = mktime(0, 0, 0, $month, $day, $year); + + // Date conditionals + $condition = array + ( + 'timestamp' => (int) $timestamp, + 'day' => (int) date('j', $timestamp), + 'week' => (int) $week, + 'month' => (int) date('n', $timestamp), + 'year' => (int) date('Y', $timestamp), + 'day_of_week' => (int) date('w', $timestamp), + 'current' => (bool) $current, + ); + + // Tested conditions + $tested = array(); + + foreach ($condition as $key => $value) + { + // Test basic conditions first + if (isset($this->conditions[$key]) AND $this->conditions[$key] !== $value) + return FALSE; + + // Condition has been tested + $tested[$key] = TRUE; + } + + if (isset($this->conditions['weekend'])) + { + // Weekday vs Weekend + $condition['weekend'] = ($condition['day_of_week'] === 0 OR $condition['day_of_week'] === 6); + } + + if (isset($this->conditions['first_day'])) + { + // First day of month + $condition['first_day'] = ($condition['day'] === 1); + } + + if (isset($this->conditions['last_day'])) + { + // Last day of month + $condition['last_day'] = ($condition['day'] === (int) date('t', $timestamp)); + } + + if (isset($this->conditions['occurrence'])) + { + // Get the occurance of the current day + $condition['occurrence'] = $this->day_occurrence($timestamp); + } + + if (isset($this->conditions['last_occurrence'])) + { + // Test if the next occurance of this date is next month + $condition['last_occurrence'] = ((int) date('n', $timestamp + 604800) !== $condition['month']); + } + + if (isset($this->conditions['easter'])) + { + if ($condition['month'] === 3 OR $condition['month'] === 4) + { + // This algorithm is from Practical Astronomy With Your Calculator, 2nd Edition by Peter + // Duffett-Smith. It was originally from Butcher's Ecclesiastical Calendar, published in + // 1876. This algorithm has also been published in the 1922 book General Astronomy by + // Spencer Jones; in The Journal of the British Astronomical Association (Vol.88, page + // 91, December 1977); and in Astronomical Algorithms (1991) by Jean Meeus. + + $a = $condition['year'] % 19; + $b = (int) ($condition['year'] / 100); + $c = $condition['year'] % 100; + $d = (int) ($b / 4); + $e = $b % 4; + $f = (int) (($b + 8) / 25); + $g = (int) (($b - $f + 1) / 3); + $h = (19 * $a + $b - $d - $g + 15) % 30; + $i = (int) ($c / 4); + $k = $c % 4; + $l = (32 + 2 * $e + 2 * $i - $h - $k) % 7; + $m = (int) (($a + 11 * $h + 22 * $l) / 451); + $p = ($h + $l - 7 * $m + 114) % 31; + + $month = (int) (($h + $l - 7 * $m + 114) / 31); + $day = $p + 1; + + $condition['easter'] = ($condition['month'] === $month AND $condition['day'] === $day); + } + else + { + // Easter can only happen in March or April + $condition['easter'] = FALSE; + } + } + + if (isset($this->conditions['callback'])) + { + // Use a callback to determine validity + $condition['callback'] = call_user_func($this->conditions['callback'], $condition, $this); + } + + $conditions = array_diff_key($this->conditions, $tested); + + foreach ($conditions as $key => $value) + { + if ($key === 'callback') + { + // Callbacks are tested on a TRUE/FALSE basis + $value = TRUE; + } + + // Test advanced conditions + if ($condition[$key] !== $value) + return FALSE; + } + + $this->caller->add_data(array + ( + 'classes' => $this->classes, + 'output' => $this->output, + )); + } + + /** + * Find the week day occurrence for a specific timestamp. The occurrence is + * relative to the current month. For example, the second Saturday of any + * given month will return "2" as the occurrence. This is used in combination + * with the "occurrence" condition. + * + * @param integer UNIX timestamp + * @return integer + */ + protected function day_occurrence($timestamp) + { + // Get the current month for the timestamp + $month = date('m', $timestamp); + + // Default occurrence is one + $occurrence = 1; + + // Reduce the timestamp by one week for each loop. This has the added + // benefit of preventing an infinite loop. + while ($timestamp -= 604800) + { + if (date('m', $timestamp) !== $month) + { + // Once the timestamp has gone into the previous month, the + // proper occurrence has been found. + return $occurrence; + } + + // Increment the occurrence + $occurrence++; + } + } + +} // End Calendar Event diff --git a/kohana/libraries/Captcha.php b/kohana/libraries/Captcha.php new file mode 100644 index 00000000..bac4e132 --- /dev/null +++ b/kohana/libraries/Captcha.php @@ -0,0 +1,279 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Captcha library. + * + * $Id$ + * + * @package Captcha + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Captcha_Core { + + // Captcha singleton + protected static $instance; + + // Style-dependent Captcha driver + protected $driver; + + // Config values + public static $config = array + ( + 'style' => 'basic', + 'width' => 150, + 'height' => 50, + 'complexity' => 4, + 'background' => '', + 'fontpath' => '', + 'fonts' => array(), + 'promote' => FALSE, + ); + + /** + * Singleton instance of Captcha. + * + * @return object + */ + public static function instance() + { + // Create the instance if it does not exist + empty(self::$instance) and new Captcha; + + return self::$instance; + } + + /** + * Constructs and returns a new Captcha object. + * + * @param string config group name + * @return object + */ + public function factory($group = NULL) + { + return new Captcha($group); + } + + /** + * Constructs a new Captcha object. + * + * @throws Kohana_Exception + * @param string config group name + * @return void + */ + public function __construct($group = NULL) + { + // Create a singleton instance once + empty(self::$instance) and self::$instance = $this; + + // No config group name given + if ( ! is_string($group)) + { + $group = 'default'; + } + + // Load and validate config group + if ( ! is_array($config = Kohana::config('captcha.'.$group))) + throw new Kohana_Exception('captcha.undefined_group', $group); + + // All captcha config groups inherit default config group + if ($group !== 'default') + { + // Load and validate default config group + if ( ! is_array($default = Kohana::config('captcha.default'))) + throw new Kohana_Exception('captcha.undefined_group', 'default'); + + // Merge config group with default config group + $config += $default; + } + + // Assign config values to the object + foreach ($config as $key => $value) + { + if (array_key_exists($key, self::$config)) + { + self::$config[$key] = $value; + } + } + + // Store the config group name as well, so the drivers can access it + self::$config['group'] = $group; + + // If using a background image, check if it exists + if ( ! empty($config['background'])) + { + self::$config['background'] = str_replace('\\', '/', realpath($config['background'])); + + if ( ! is_file(self::$config['background'])) + throw new Kohana_Exception('captcha.file_not_found', self::$config['background']); + } + + // If using any fonts, check if they exist + if ( ! empty($config['fonts'])) + { + self::$config['fontpath'] = str_replace('\\', '/', realpath($config['fontpath'])).'/'; + + foreach ($config['fonts'] as $font) + { + if ( ! is_file(self::$config['fontpath'].$font)) + throw new Kohana_Exception('captcha.file_not_found', self::$config['fontpath'].$font); + } + } + + // Set driver name + $driver = 'Captcha_'.ucfirst($config['style']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('core.driver_not_found', $config['style'], get_class($this)); + + // Initialize the driver + $this->driver = new $driver; + + // Validate the driver + if ( ! ($this->driver instanceof Captcha_Driver)) + throw new Kohana_Exception('core.driver_implements', $config['style'], get_class($this), 'Captcha_Driver'); + + Kohana::log('debug', 'Captcha Library initialized'); + } + + /** + * Validates a Captcha response and updates response counter. + * + * @param string captcha response + * @return boolean + */ + public static function valid($response) + { + // Maximum one count per page load + static $counted; + + // User has been promoted, always TRUE and don't count anymore + if (self::instance()->promoted()) + return TRUE; + + // Challenge result + $result = (bool) self::instance()->driver->valid($response); + + // Increment response counter + if ($counted !== TRUE) + { + $counted = TRUE; + + // Valid response + if ($result === TRUE) + { + self::instance()->valid_count(Session::instance()->get('captcha_valid_count') + 1); + } + // Invalid response + else + { + self::instance()->invalid_count(Session::instance()->get('captcha_invalid_count') + 1); + } + } + + return $result; + } + + /** + * Gets or sets the number of valid Captcha responses for this session. + * + * @param integer new counter value + * @param boolean trigger invalid counter (for internal use only) + * @return integer counter value + */ + public function valid_count($new_count = NULL, $invalid = FALSE) + { + // Pick the right session to use + $session = ($invalid === TRUE) ? 'captcha_invalid_count' : 'captcha_valid_count'; + + // Update counter + if ($new_count !== NULL) + { + $new_count = (int) $new_count; + + // Reset counter = delete session + if ($new_count < 1) + { + Session::instance()->delete($session); + } + // Set counter to new value + else + { + Session::instance()->set($session, (int) $new_count); + } + + // Return new count + return (int) $new_count; + } + + // Return current count + return (int) Session::instance()->get($session); + } + + /** + * Gets or sets the number of invalid Captcha responses for this session. + * + * @param integer new counter value + * @return integer counter value + */ + public function invalid_count($new_count = NULL) + { + return $this->valid_count($new_count, TRUE); + } + + /** + * Resets the Captcha response counters and removes the count sessions. + * + * @return void + */ + public function reset_count() + { + $this->valid_count(0); + $this->valid_count(0, TRUE); + } + + /** + * Checks whether user has been promoted after having given enough valid responses. + * + * @param integer valid response count threshold + * @return boolean + */ + public function promoted($threshold = NULL) + { + // Promotion has been disabled + if (self::$config['promote'] === FALSE) + return FALSE; + + // Use the config threshold + if ($threshold === NULL) + { + $threshold = self::$config['promote']; + } + + // Compare the valid response count to the threshold + return ($this->valid_count() >= $threshold); + } + + /** + * Returns or outputs the Captcha challenge. + * + * @param boolean TRUE to output html, e.g. <img src="#" /> + * @return mixed html string or void + */ + public function render($html = TRUE) + { + return $this->driver->render($html); + } + + /** + * Magically outputs the Captcha challenge. + * + * @return mixed + */ + public function __toString() + { + return $this->render(); + } + +} // End Captcha Class
\ No newline at end of file diff --git a/kohana/libraries/Controller.php b/kohana/libraries/Controller.php new file mode 100644 index 00000000..8572e77f --- /dev/null +++ b/kohana/libraries/Controller.php @@ -0,0 +1,78 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Kohana Controller class. The controller class must be extended to work + * properly, so this class is defined as abstract. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Controller_Core { + + // Allow all controllers to run in production by default + const ALLOW_PRODUCTION = TRUE; + + /** + * Loads URI, and Input into this controller. + * + * @return void + */ + public function __construct() + { + if (Kohana::$instance == NULL) + { + // Set the instance to the first controller loaded + Kohana::$instance = $this; + } + + // URI should always be available + $this->uri = URI::instance(); + + // Input should always be available + $this->input = Input::instance(); + } + + /** + * Handles methods that do not exist. + * + * @param string method name + * @param array arguments + * @return void + */ + public function __call($method, $args) + { + // Default to showing a 404 page + Event::run('system.404'); + } + + /** + * Includes a View within the controller scope. + * + * @param string view filename + * @param array array of view variables + * @return string + */ + public function _kohana_load_view($kohana_view_filename, $kohana_input_data) + { + if ($kohana_view_filename == '') + return; + + // Buffering on + ob_start(); + + // Import the view variables to local namespace + extract($kohana_input_data, EXTR_SKIP); + + // Views are straight HTML pages with embedded PHP, so importing them + // this way insures that $this can be accessed as if the user was in + // the controller, which gives the easiest access to libraries in views + include $kohana_view_filename; + + // Fetch the output and close the buffer + return ob_get_clean(); + } + +} // End Controller Class
\ No newline at end of file diff --git a/kohana/libraries/Database.php b/kohana/libraries/Database.php new file mode 100644 index 00000000..612fb6c9 --- /dev/null +++ b/kohana/libraries/Database.php @@ -0,0 +1,1266 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Provides database access in a platform agnostic way, using simple query building blocks. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Database_Core { + + // Database instances + public static $instances = array(); + + // Global benchmark + public static $benchmarks = array(); + + // Configuration + protected $config = array + ( + 'benchmark' => TRUE, + 'persistent' => FALSE, + 'connection' => '', + 'character_set' => 'utf8', + 'table_prefix' => '', + 'object' => TRUE, + 'cache' => FALSE, + 'escape' => TRUE, + ); + + // Database driver object + protected $driver; + protected $link; + + // Un-compiled parts of the SQL query + protected $select = array(); + protected $set = array(); + protected $from = array(); + protected $join = array(); + protected $where = array(); + protected $orderby = array(); + protected $order = array(); + protected $groupby = array(); + protected $having = array(); + protected $distinct = FALSE; + protected $limit = FALSE; + protected $offset = FALSE; + protected $last_query = ''; + + /** + * Returns a singleton instance of Database. + * + * @param mixed configuration array or DSN + * @return Database_Core + */ + public static function & instance($name = 'default', $config = NULL) + { + if ( ! isset(Database::$instances[$name])) + { + // Create a new instance + Database::$instances[$name] = new Database($config === NULL ? $name : $config); + } + + return Database::$instances[$name]; + } + + /** + * Returns the name of a given database instance. + * + * @param Database instance of Database + * @return string + */ + public static function instance_name(Database $db) + { + return array_search($db, Database::$instances, TRUE); + } + + /** + * Sets up the database configuration, loads the Database_Driver. + * + * @throws Kohana_Database_Exception + */ + public function __construct($config = array()) + { + if (empty($config)) + { + // Load the default group + $config = Kohana::config('database.default'); + } + elseif (is_array($config) AND count($config) > 0) + { + if ( ! array_key_exists('connection', $config)) + { + $config = array('connection' => $config); + } + } + elseif (is_string($config)) + { + // The config is a DSN string + if (strpos($config, '://') !== FALSE) + { + $config = array('connection' => $config); + } + // The config is a group name + else + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('database.'.$config)) === NULL) + throw new Kohana_Database_Exception('database.undefined_group', $name); + } + } + + // Merge the default config with the passed config + $this->config = array_merge($this->config, $config); + + if (is_string($this->config['connection'])) + { + // Make sure the connection is valid + if (strpos($this->config['connection'], '://') === FALSE) + throw new Kohana_Database_Exception('database.invalid_dsn', $this->config['connection']); + + // Parse the DSN, creating an array to hold the connection parameters + $db = array + ( + 'type' => FALSE, + 'user' => FALSE, + 'pass' => FALSE, + 'host' => FALSE, + 'port' => FALSE, + 'socket' => FALSE, + 'database' => FALSE + ); + + // Get the protocol and arguments + list ($db['type'], $connection) = explode('://', $this->config['connection'], 2); + + if (strpos($connection, '@') !== FALSE) + { + // Get the username and password + list ($db['pass'], $connection) = explode('@', $connection, 2); + // Check if a password is supplied + $logindata = explode(':', $db['pass'], 2); + $db['pass'] = (count($logindata) > 1) ? $logindata[1] : ''; + $db['user'] = $logindata[0]; + + // Prepare for finding the database + $connection = explode('/', $connection); + + // Find the database name + $db['database'] = array_pop($connection); + + // Reset connection string + $connection = implode('/', $connection); + + // Find the socket + if (preg_match('/^unix\([^)]++\)/', $connection)) + { + // This one is a little hairy: we explode based on the end of + // the socket, removing the 'unix(' from the connection string + list ($db['socket'], $connection) = explode(')', substr($connection, 5), 2); + } + elseif (strpos($connection, ':') !== FALSE) + { + // Fetch the host and port name + list ($db['host'], $db['port']) = explode(':', $connection, 2); + } + else + { + $db['host'] = $connection; + } + } + else + { + // File connection + $connection = explode('/', $connection); + + // Find database file name + $db['database'] = array_pop($connection); + + // Find database directory name + $db['socket'] = implode('/', $connection).'/'; + } + + // Reset the connection array to the database config + $this->config['connection'] = $db; + } + // Set driver name + $driver = 'Database_'.ucfirst($this->config['connection']['type']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Database_Exception('core.driver_not_found', $this->config['connection']['type'], get_class($this)); + + // Initialize the driver + $this->driver = new $driver($this->config); + + // Validate the driver + if ( ! ($this->driver instanceof Database_Driver)) + throw new Kohana_Database_Exception('core.driver_implements', $this->config['connection']['type'], get_class($this), 'Database_Driver'); + + Kohana::log('debug', 'Database Library initialized'); + } + + /** + * Simple connect method to get the database queries up and running. + * + * @return void + */ + public function connect() + { + // A link can be a resource or an object + if ( ! is_resource($this->link) AND ! is_object($this->link)) + { + $this->link = $this->driver->connect(); + if ( ! is_resource($this->link) AND ! is_object($this->link)) + throw new Kohana_Database_Exception('database.connection', $this->driver->show_error()); + + // Clear password after successful connect + $this->config['connection']['pass'] = NULL; + } + } + + /** + * Runs a query into the driver and returns the result. + * + * @param string SQL query to execute + * @return Database_Result + */ + public function query($sql = '') + { + if ($sql == '') return FALSE; + + // No link? Connect! + $this->link or $this->connect(); + + // Start the benchmark + $start = microtime(TRUE); + + if (func_num_args() > 1) //if we have more than one argument ($sql) + { + $argv = func_get_args(); + $binds = (is_array(next($argv))) ? current($argv) : array_slice($argv, 1); + } + + // Compile binds if needed + if (isset($binds)) + { + $sql = $this->compile_binds($sql, $binds); + } + + // Fetch the result + $result = $this->driver->query($this->last_query = $sql); + + // Stop the benchmark + $stop = microtime(TRUE); + + if ($this->config['benchmark'] == TRUE) + { + // Benchmark the query + self::$benchmarks[] = array('query' => $sql, 'time' => $stop - $start, 'rows' => count($result)); + } + + return $result; + } + + /** + * Selects the column names for a database query. + * + * @param string string or array of column names to select + * @return Database_Core This Database object. + */ + public function select($sql = '*') + { + if (func_num_args() > 1) + { + $sql = func_get_args(); + } + elseif (is_string($sql)) + { + $sql = explode(',', $sql); + } + else + { + $sql = (array) $sql; + } + + foreach ($sql as $val) + { + if (($val = trim($val)) === '') continue; + + if (strpos($val, '(') === FALSE AND $val !== '*') + { + if (preg_match('/^DISTINCT\s++(.+)$/i', $val, $matches)) + { + $val = $this->config['table_prefix'].$matches[1]; + $this->distinct = TRUE; + } + else + { + $val = (strpos($val, '.') !== FALSE) ? $this->config['table_prefix'].$val : $val; + } + + $val = $this->driver->escape_column($val); + } + + $this->select[] = $val; + } + + return $this; + } + + /** + * Selects the from table(s) for a database query. + * + * @param string string or array of tables to select + * @return Database_Core This Database object. + */ + public function from($sql) + { + if (func_num_args() > 1) + { + $sql = func_get_args(); + } + elseif (is_string($sql)) + { + $sql = explode(',', $sql); + } + else + { + $sql = (array) $sql; + } + + foreach ($sql as $val) + { + if (($val = trim($val)) === '') continue; + + $this->from[] = $this->config['table_prefix'].$val; + } + + return $this; + } + + /** + * Generates the JOIN portion of the query. + * + * @param string table name + * @param string|array where key or array of key => value pairs + * @param string where value + * @param string type of join + * @return Database_Core This Database object. + */ + public function join($table, $key, $value = NULL, $type = '') + { + if ($type != '') + { + $type = strtoupper(trim($type)); + + if ( ! in_array($type, array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER'), TRUE)) + { + $type = ''; + } + else + { + $type .= ' '; + } + } + + $cond = array(); + $keys = is_array($key) ? $key : array($key => $value); + foreach ($keys as $key => $value) + { + $key = (strpos($key, '.') !== FALSE) ? $this->config['table_prefix'].$key : $key; + $cond[] = $this->driver->where($key, $this->driver->escape_column($this->config['table_prefix'].$value), 'AND ', count($cond), FALSE); + } + + if( ! isset($this->join['tables']) OR ! isset($this->join['conditions'])) + { + $this->join['tables'] = array(); + $this->join['conditions'] = array(); + } + + foreach ((array) $table as $t) + { + $this->join['tables'][] = $this->driver->escape_column($this->config['table_prefix'].$t); + } + + $this->join['conditions'][] = '('.trim(implode(' ', $cond)).')'; + $this->join['type'] = $type; + + return $this; + } + + /** + * Selects the where(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function where($key, $value = NULL, $quote = TRUE) + { + $quote = (func_num_args() < 2 AND ! is_array($key)) ? -1 : $quote; + $keys = is_array($key) ? $key : array($key => $value); + + foreach ($keys as $key => $value) + { + $key = (strpos($key, '.') !== FALSE) ? $this->config['table_prefix'].$key : $key; + $this->where[] = $this->driver->where($key, $value, 'AND ', count($this->where), $quote); + } + + return $this; + } + + /** + * Selects the or where(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function orwhere($key, $value = NULL, $quote = TRUE) + { + $quote = (func_num_args() < 2 AND ! is_array($key)) ? -1 : $quote; + $keys = is_array($key) ? $key : array($key => $value); + + foreach ($keys as $key => $value) + { + $key = (strpos($key, '.') !== FALSE) ? $this->config['table_prefix'].$key : $key; + $this->where[] = $this->driver->where($key, $value, 'OR ', count($this->where), $quote); + } + + return $this; + } + + /** + * Selects the like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @param boolean automatically add starting and ending wildcards + * @return Database_Core This Database object. + */ + public function like($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->like($field, $match, $auto, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @param boolean automatically add starting and ending wildcards + * @return Database_Core This Database object. + */ + public function orlike($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->like($field, $match, $auto, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Selects the not like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @param boolean automatically add starting and ending wildcards + * @return Database_Core This Database object. + */ + public function notlike($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notlike($field, $match, $auto, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or not like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @return Database_Core This Database object. + */ + public function ornotlike($field, $match = '', $auto = TRUE) + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notlike($field, $match, $auto, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Selects the like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @return Database_Core This Database object. + */ + public function regex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->regex($field, $match, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or like(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string like value to match with field + * @return Database_Core This Database object. + */ + public function orregex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->regex($field, $match, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Selects the not regex(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string regex value to match with field + * @return Database_Core This Database object. + */ + public function notregex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notregex($field, $match, 'AND ', count($this->where)); + } + + return $this; + } + + /** + * Selects the or not regex(s) for a database query. + * + * @param string|array field name or array of field => match pairs + * @param string regex value to match with field + * @return Database_Core This Database object. + */ + public function ornotregex($field, $match = '') + { + $fields = is_array($field) ? $field : array($field => $match); + + foreach ($fields as $field => $match) + { + $field = (strpos($field, '.') !== FALSE) ? $this->config['table_prefix'].$field : $field; + $this->where[] = $this->driver->notregex($field, $match, 'OR ', count($this->where)); + } + + return $this; + } + + /** + * Chooses the column to group by in a select query. + * + * @param string column name to group by + * @return Database_Core This Database object. + */ + public function groupby($by) + { + if ( ! is_array($by)) + { + $by = explode(',', (string) $by); + } + + foreach ($by as $val) + { + $val = trim($val); + + if ($val != '') + { + $this->groupby[] = $this->driver->escape_column($val); + } + } + + return $this; + } + + /** + * Selects the having(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function having($key, $value = '', $quote = TRUE) + { + $this->having[] = $this->driver->where($key, $value, 'AND', count($this->having), TRUE); + return $this; + } + + /** + * Selects the or having(s) for a database query. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @param boolean disable quoting of WHERE clause + * @return Database_Core This Database object. + */ + public function orhaving($key, $value = '', $quote = TRUE) + { + $this->having[] = $this->driver->where($key, $value, 'OR', count($this->having), TRUE); + return $this; + } + + /** + * Chooses which column(s) to order the select query by. + * + * @param string|array column(s) to order on, can be an array, single column, or comma seperated list of columns + * @param string direction of the order + * @return Database_Core This Database object. + */ + public function orderby($orderby, $direction = NULL) + { + if ( ! is_array($orderby)) + { + $orderby = array($orderby => $direction); + } + + foreach ($orderby as $column => $direction) + { + $direction = strtoupper(trim($direction)); + + if ( ! in_array($direction, array('ASC', 'DESC', 'RAND()', 'RANDOM()', 'NULL'))) + { + $direction = 'ASC'; + } + + $this->orderby[] = $this->driver->escape_column($column).' '.$direction; + } + + return $this; + } + + /** + * Selects the limit section of a query. + * + * @param integer number of rows to limit result to + * @param integer offset in result to start returning rows from + * @return Database_Core This Database object. + */ + public function limit($limit, $offset = NULL) + { + $this->limit = (int) $limit; + + if ($offset !== NULL OR ! is_int($this->offset)) + { + $this->offset($offset); + } + + return $this; + } + + /** + * Sets the offset portion of a query. + * + * @param integer offset value + * @return Database_Core This Database object. + */ + public function offset($value) + { + $this->offset = (int) $value; + + return $this; + } + + /** + * Allows key/value pairs to be set for inserting or updating. + * + * @param string|array key name or array of key => value pairs + * @param string value to match with key + * @return Database_Core This Database object. + */ + public function set($key, $value = '') + { + if ( ! is_array($key)) + { + $key = array($key => $value); + } + + foreach ($key as $k => $v) + { + // Add a table prefix if the column includes the table. + if (strpos($k, '.')) + $k = $this->config['table_prefix'].$k; + + $this->set[$k] = $this->driver->escape($v); + } + + return $this; + } + + /** + * Compiles the select statement based on the other functions called and runs the query. + * + * @param string table name + * @param string limit clause + * @param string offset clause + * @return Database_Result + */ + public function get($table = '', $limit = NULL, $offset = NULL) + { + if ($table != '') + { + $this->from($table); + } + + if ( ! is_null($limit)) + { + $this->limit($limit, $offset); + } + + $sql = $this->driver->compile_select(get_object_vars($this)); + + $this->reset_select(); + + $result = $this->query($sql); + + $this->last_query = $sql; + + return $result; + } + + /** + * Compiles the select statement based on the other functions called and runs the query. + * + * @param string table name + * @param array where clause + * @param string limit clause + * @param string offset clause + * @return Database_Core This Database object. + */ + public function getwhere($table = '', $where = NULL, $limit = NULL, $offset = NULL) + { + if ($table != '') + { + $this->from($table); + } + + if ( ! is_null($where)) + { + $this->where($where); + } + + if ( ! is_null($limit)) + { + $this->limit($limit, $offset); + } + + $sql = $this->driver->compile_select(get_object_vars($this)); + + $this->reset_select(); + + $result = $this->query($sql); + + return $result; + } + + /** + * Compiles the select statement based on the other functions called and returns the query string. + * + * @param string table name + * @param string limit clause + * @param string offset clause + * @return string sql string + */ + public function compile($table = '', $limit = NULL, $offset = NULL) + { + if ($table != '') + { + $this->from($table); + } + + if ( ! is_null($limit)) + { + $this->limit($limit, $offset); + } + + $sql = $this->driver->compile_select(get_object_vars($this)); + + $this->reset_select(); + + return $sql; + } + + /** + * Compiles an insert string and runs the query. + * + * @param string table name + * @param array array of key/value pairs to insert + * @return Database_Result Query result + */ + public function insert($table = '', $set = NULL) + { + if ( ! is_null($set)) + { + $this->set($set); + } + + if ($this->set == NULL) + throw new Kohana_Database_Exception('database.must_use_set'); + + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + + // If caching is enabled, clear the cache before inserting + ($this->config['cache'] === TRUE) and $this->clear_cache(); + + $sql = $this->driver->insert($this->config['table_prefix'].$table, array_keys($this->set), array_values($this->set)); + + $this->reset_write(); + + return $this->query($sql); + } + + /** + * Adds an "IN" condition to the where clause + * + * @param string Name of the column being examined + * @param mixed An array or string to match against + * @param bool Generate a NOT IN clause instead + * @return Database_Core This Database object. + */ + public function in($field, $values, $not = FALSE) + { + if (is_array($values)) + { + $escaped_values = array(); + foreach ($values as $v) + { + if (is_numeric($v)) + { + $escaped_values[] = $v; + } + else + { + $escaped_values[] = "'".$this->driver->escape_str($v)."'"; + } + } + $values = implode(",", $escaped_values); + } + $this->where($this->driver->escape_column($field).' '.($not === TRUE ? 'NOT ' : '').'IN ('.$values.')'); + + return $this; + } + + /** + * Adds a "NOT IN" condition to the where clause + * + * @param string Name of the column being examined + * @param mixed An array or string to match against + * @return Database_Core This Database object. + */ + public function notin($field, $values) + { + return $this->in($field, $values, TRUE); + } + + /** + * Compiles a merge string and runs the query. + * + * @param string table name + * @param array array of key/value pairs to merge + * @return Database_Result Query result + */ + public function merge($table = '', $set = NULL) + { + if ( ! is_null($set)) + { + $this->set($set); + } + + if ($this->set == NULL) + throw new Kohana_Database_Exception('database.must_use_set'); + + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + + $sql = $this->driver->merge($this->config['table_prefix'].$table, array_keys($this->set), array_values($this->set)); + + $this->reset_write(); + return $this->query($sql); + } + + /** + * Compiles an update string and runs the query. + * + * @param string table name + * @param array associative array of update values + * @param array where clause + * @return Database_Result Query result + */ + public function update($table = '', $set = NULL, $where = NULL) + { + if ( is_array($set)) + { + $this->set($set); + } + + if ( ! is_null($where)) + { + $this->where($where); + } + + if ($this->set == FALSE) + throw new Kohana_Database_Exception('database.must_use_set'); + + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + + $sql = $this->driver->update($this->config['table_prefix'].$table, $this->set, $this->where); + + $this->reset_write(); + return $this->query($sql); + } + + /** + * Compiles a delete string and runs the query. + * + * @param string table name + * @param array where clause + * @return Database_Result Query result + */ + public function delete($table = '', $where = NULL) + { + if ($table == '') + { + if ( ! isset($this->from[0])) + throw new Kohana_Database_Exception('database.must_use_table'); + + $table = $this->from[0]; + } + else + { + $table = $this->config['table_prefix'].$table; + } + + if (! is_null($where)) + { + $this->where($where); + } + + if (count($this->where) < 1) + throw new Kohana_Database_Exception('database.must_use_where'); + + $sql = $this->driver->delete($table, $this->where); + + $this->reset_write(); + return $this->query($sql); + } + + /** + * Returns the last query run. + * + * @return string SQL + */ + public function last_query() + { + return $this->last_query; + } + + /** + * Count query records. + * + * @param string table name + * @param array where clause + * @return integer + */ + public function count_records($table = FALSE, $where = NULL) + { + if (count($this->from) < 1) + { + if ($table == FALSE) + throw new Kohana_Database_Exception('database.must_use_table'); + + $this->from($table); + } + + if ($where !== NULL) + { + $this->where($where); + } + + $query = $this->select('COUNT(*) AS '.$this->escape_column('records_found'))->get()->result(TRUE); + + return (int) $query->current()->records_found; + } + + /** + * Resets all private select variables. + * + * @return void + */ + protected function reset_select() + { + $this->select = array(); + $this->from = array(); + $this->join = array(); + $this->where = array(); + $this->orderby = array(); + $this->groupby = array(); + $this->having = array(); + $this->distinct = FALSE; + $this->limit = FALSE; + $this->offset = FALSE; + } + + /** + * Resets all private insert and update variables. + * + * @return void + */ + protected function reset_write() + { + $this->set = array(); + $this->from = array(); + $this->where = array(); + } + + /** + * Lists all the tables in the current database. + * + * @return array + */ + public function list_tables() + { + $this->link or $this->connect(); + + $this->reset_select(); + + return $this->driver->list_tables(); + } + + /** + * See if a table exists in the database. + * + * @param string table name + * @return boolean + */ + public function table_exists($table_name) + { + return in_array($this->config['table_prefix'].$table_name, $this->list_tables()); + } + + /** + * Combine a SQL statement with the bind values. Used for safe queries. + * + * @param string query to bind to the values + * @param array array of values to bind to the query + * @return string + */ + public function compile_binds($sql, $binds) + { + foreach ((array) $binds as $val) + { + // If the SQL contains no more bind marks ("?"), we're done. + if (($next_bind_pos = strpos($sql, '?')) === FALSE) + break; + + // Properly escape the bind value. + $val = $this->driver->escape($val); + + // Temporarily replace possible bind marks ("?"), in the bind value itself, with a placeholder. + $val = str_replace('?', '{%B%}', $val); + + // Replace the first bind mark ("?") with its corresponding value. + $sql = substr($sql, 0, $next_bind_pos).$val.substr($sql, $next_bind_pos + 1); + } + + // Restore placeholders. + return str_replace('{%B%}', '?', $sql); + } + + /** + * Get the field data for a database table, along with the field's attributes. + * + * @param string table name + * @return array + */ + public function field_data($table = '') + { + $this->link or $this->connect(); + + return $this->driver->field_data($this->config['table_prefix'].$table); + } + + /** + * Get the field data for a database table, along with the field's attributes. + * + * @param string table name + * @return array + */ + public function list_fields($table = '') + { + $this->link or $this->connect(); + + return $this->driver->list_fields($this->config['table_prefix'].$table); + } + + /** + * Escapes a value for a query. + * + * @param mixed value to escape + * @return string + */ + public function escape($value) + { + return $this->driver->escape($value); + } + + /** + * Escapes a string for a query. + * + * @param string string to escape + * @return string + */ + public function escape_str($str) + { + return $this->driver->escape_str($str); + } + + /** + * Escapes a table name for a query. + * + * @param string string to escape + * @return string + */ + public function escape_table($table) + { + return $this->driver->escape_table($table); + } + + /** + * Escapes a column name for a query. + * + * @param string string to escape + * @return string + */ + public function escape_column($table) + { + return $this->driver->escape_column($table); + } + + /** + * Returns table prefix of current configuration. + * + * @return string + */ + public function table_prefix() + { + return $this->config['table_prefix']; + } + + /** + * Clears the query cache. + * + * @param string|TRUE clear cache by SQL statement or TRUE for last query + * @return Database_Core This Database object. + */ + public function clear_cache($sql = NULL) + { + if ($sql === TRUE) + { + $this->driver->clear_cache($this->last_query); + } + elseif (is_string($sql)) + { + $this->driver->clear_cache($sql); + } + else + { + $this->driver->clear_cache(); + } + + return $this; + } + + /** + * Create a prepared statement (experimental). + * + * @param string SQL query + * @return object + */ + public function stmt_prepare($sql) + { + return $this->driver->stmt_prepare($sql, $this->config); + } + +} // End Database Class + + +/** + * Sets the code for a Database exception. + */ +class Kohana_Database_Exception extends Kohana_Exception { + + protected $code = E_DATABASE_ERROR; + +} // End Kohana Database Exception diff --git a/kohana/libraries/Encrypt.php b/kohana/libraries/Encrypt.php new file mode 100644 index 00000000..38c094f0 --- /dev/null +++ b/kohana/libraries/Encrypt.php @@ -0,0 +1,164 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * The Encrypt library provides two-way encryption of text and binary strings + * using the MCrypt extension. + * @see http://php.net/mcrypt + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Encrypt_Core { + + // OS-dependant RAND type to use + protected static $rand; + + // Configuration + protected $config; + + /** + * Returns a singleton instance of Encrypt. + * + * @param array configuration options + * @return Encrypt_Core + */ + public static function instance($config = NULL) + { + static $instance; + + // Create the singleton + empty($instance) and $instance = new Encrypt((array) $config); + + return $instance; + } + + /** + * Loads encryption configuration and validates the data. + * + * @param array|string custom configuration or config group name + * @throws Kohana_Exception + */ + public function __construct($config = FALSE) + { + if ( ! defined('MCRYPT_ENCRYPT')) + throw new Kohana_Exception('encrypt.requires_mcrypt'); + + if (is_string($config)) + { + $name = $config; + + // Test the config group name + if (($config = Kohana::config('encryption.'.$config)) === NULL) + throw new Kohana_Exception('encrypt.undefined_group', $name); + } + + if (is_array($config)) + { + // Append the default configuration options + $config += Kohana::config('encryption.default'); + } + else + { + // Load the default group + $config = Kohana::config('encryption.default'); + } + + if (empty($config['key'])) + throw new Kohana_Exception('encrypt.no_encryption_key'); + + // Find the max length of the key, based on cipher and mode + $size = mcrypt_get_key_size($config['cipher'], $config['mode']); + + if (strlen($config['key']) > $size) + { + // Shorten the key to the maximum size + $config['key'] = substr($config['key'], 0, $size); + } + + // Find the initialization vector size + $config['iv_size'] = mcrypt_get_iv_size($config['cipher'], $config['mode']); + + // Cache the config in the object + $this->config = $config; + + Kohana::log('debug', 'Encrypt Library initialized'); + } + + /** + * Encrypts a string and returns an encrypted string that can be decoded. + * + * @param string data to be encrypted + * @return string encrypted data + */ + public function encode($data) + { + // Set the rand type if it has not already been set + if (self::$rand === NULL) + { + if (KOHANA_IS_WIN) + { + // Windows only supports the system random number generator + self::$rand = MCRYPT_RAND; + } + else + { + if (defined('MCRYPT_DEV_URANDOM')) + { + // Use /dev/urandom + self::$rand = MCRYPT_DEV_URANDOM; + } + elseif (defined('MCRYPT_DEV_RANDOM')) + { + // Use /dev/random + self::$rand = MCRYPT_DEV_RANDOM; + } + else + { + // Use the system random number generator + self::$rand = MCRYPT_RAND; + } + } + } + + if (self::$rand === MCRYPT_RAND) + { + // The system random number generator must always be seeded each + // time it is used, or it will not produce true random results + mt_srand(); + } + + // Create a random initialization vector of the proper size for the current cipher + $iv = mcrypt_create_iv($this->config['iv_size'], self::$rand); + + // Encrypt the data using the configured options and generated iv + $data = mcrypt_encrypt($this->config['cipher'], $this->config['key'], $data, $this->config['mode'], $iv); + + // Use base64 encoding to convert to a string + return base64_encode($iv.$data); + } + + /** + * Decrypts an encoded string back to its original value. + * + * @param string encoded string to be decrypted + * @return string decrypted data + */ + public function decode($data) + { + // Convert the data back to binary + $data = base64_decode($data); + + // Extract the initialization vector from the data + $iv = substr($data, 0, $this->config['iv_size']); + + // Remove the iv from the data + $data = substr($data, $this->config['iv_size']); + + // Return the decrypted data, trimming the \0 padding bytes from the end of the data + return rtrim(mcrypt_decrypt($this->config['cipher'], $this->config['key'], $data, $this->config['mode'], $iv), "\0"); + } + +} // End Encrypt diff --git a/kohana/libraries/Event_Observer.php b/kohana/libraries/Event_Observer.php new file mode 100644 index 00000000..8bfa766b --- /dev/null +++ b/kohana/libraries/Event_Observer.php @@ -0,0 +1,70 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Kohana event observer. Uses the SPL observer pattern. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Event_Observer implements SplObserver { + + // Calling object + protected $caller; + + /** + * Initializes a new observer and attaches the subject as the caller. + * + * @param object Event_Subject + * @return void + */ + public function __construct(SplSubject $caller) + { + // Update the caller + $this->update($caller); + } + + /** + * Updates the observer subject with a new caller. + * + * @chainable + * @param object Event_Subject + * @return object + */ + public function update(SplSubject $caller) + { + if ( ! ($caller instanceof Event_Subject)) + throw new Kohana_Exception('event.invalid_subject', get_class($caller), get_class($this)); + + // Update the caller + $this->caller = $caller; + + return $this; + } + + /** + * Detaches this observer from the subject. + * + * @chainable + * @return object + */ + public function remove() + { + // Detach this observer from the caller + $this->caller->detach($this); + + return $this; + } + + /** + * Notify the observer of a new message. This function must be defined in + * all observers and must take exactly one parameter of any type. + * + * @param mixed message string, object, or array + * @return void + */ + abstract public function notify($message); + +} // End Event Observer
\ No newline at end of file diff --git a/kohana/libraries/Event_Subject.php b/kohana/libraries/Event_Subject.php new file mode 100644 index 00000000..35b0990d --- /dev/null +++ b/kohana/libraries/Event_Subject.php @@ -0,0 +1,67 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Kohana event subject. Uses the SPL observer pattern. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +abstract class Event_Subject implements SplSubject { + + // Attached subject listeners + protected $listeners = array(); + + /** + * Attach an observer to the object. + * + * @chainable + * @param object Event_Observer + * @return object + */ + public function attach(SplObserver $obj) + { + if ( ! ($obj instanceof Event_Observer)) + throw new Kohana_Exception('eventable.invalid_observer', get_class($obj), get_class($this)); + + // Add a new listener + $this->listeners[spl_object_hash($obj)] = $obj; + + return $this; + } + + /** + * Detach an observer from the object. + * + * @chainable + * @param object Event_Observer + * @return object + */ + public function detach(SplObserver $obj) + { + // Remove the listener + unset($this->listeners[spl_object_hash($obj)]); + + return $this; + } + + /** + * Notify all attached observers of a new message. + * + * @chainable + * @param mixed message string, object, or array + * @return object + */ + public function notify($message) + { + foreach ($this->listeners as $obj) + { + $obj->notify($message); + } + + return $this; + } + +} // End Event Subject
\ No newline at end of file diff --git a/kohana/libraries/Image.php b/kohana/libraries/Image.php new file mode 100644 index 00000000..a70cb1fb --- /dev/null +++ b/kohana/libraries/Image.php @@ -0,0 +1,427 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Manipulate images using standard methods such as resize, crop, rotate, etc. + * This class must be re-initialized for every image you wish to manipulate. + * + * $Id$ + * + * @package Image + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Image_Core { + + // Master Dimension + const NONE = 1; + const AUTO = 2; + const HEIGHT = 3; + const WIDTH = 4; + // Flip Directions + const HORIZONTAL = 5; + const VERTICAL = 6; + + // Allowed image types + public static $allowed_types = array + ( + IMAGETYPE_GIF => 'gif', + IMAGETYPE_JPEG => 'jpg', + IMAGETYPE_PNG => 'png', + IMAGETYPE_TIFF_II => 'tiff', + IMAGETYPE_TIFF_MM => 'tiff', + ); + + // Driver instance + protected $driver; + + // Driver actions + protected $actions = array(); + + // Reference to the current image filename + protected $image = ''; + + /** + * Creates a new Image instance and returns it. + * + * @param string filename of image + * @param array non-default configurations + * @return object + */ + public static function factory($image, $config = NULL) + { + return new Image($image, $config); + } + + /** + * Creates a new image editor instance. + * + * @throws Kohana_Exception + * @param string filename of image + * @param array non-default configurations + * @return void + */ + public function __construct($image, $config = NULL) + { + static $check; + + // Make the check exactly once + ($check === NULL) and $check = function_exists('getimagesize'); + + if ($check === FALSE) + throw new Kohana_Exception('image.getimagesize_missing'); + + // Check to make sure the image exists + if ( ! is_file($image)) + throw new Kohana_Exception('image.file_not_found', $image); + + // Disable error reporting, to prevent PHP warnings + $ER = error_reporting(0); + + // Fetch the image size and mime type + $image_info = getimagesize($image); + + // Turn on error reporting again + error_reporting($ER); + + // Make sure that the image is readable and valid + if ( ! is_array($image_info) OR count($image_info) < 3) + throw new Kohana_Exception('image.file_unreadable', $image); + + // Check to make sure the image type is allowed + if ( ! isset(Image::$allowed_types[$image_info[2]])) + throw new Kohana_Exception('image.type_not_allowed', $image); + + // Image has been validated, load it + $this->image = array + ( + 'file' => str_replace('\\', '/', realpath($image)), + 'width' => $image_info[0], + 'height' => $image_info[1], + 'type' => $image_info[2], + 'ext' => Image::$allowed_types[$image_info[2]], + 'mime' => $image_info['mime'] + ); + + // Load configuration + $this->config = (array) $config + Kohana::config('image'); + + // Set driver class name + $driver = 'Image_'.ucfirst($this->config['driver']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('core.driver_not_found', $this->config['driver'], get_class($this)); + + // Initialize the driver + $this->driver = new $driver($this->config['params']); + + // Validate the driver + if ( ! ($this->driver instanceof Image_Driver)) + throw new Kohana_Exception('core.driver_implements', $this->config['driver'], get_class($this), 'Image_Driver'); + } + + /** + * Handles retrieval of pre-save image properties + * + * @param string property name + * @return mixed + */ + public function __get($property) + { + if (isset($this->image[$property])) + { + return $this->image[$property]; + } + else + { + throw new Kohana_Exception('core.invalid_property', $column, get_class($this)); + } + } + + /** + * Resize an image to a specific width and height. By default, Kohana will + * maintain the aspect ratio using the width as the master dimension. If you + * wish to use height as master dim, set $image->master_dim = Image::HEIGHT + * This method is chainable. + * + * @throws Kohana_Exception + * @param integer width + * @param integer height + * @param integer one of: Image::NONE, Image::AUTO, Image::WIDTH, Image::HEIGHT + * @return object + */ + public function resize($width, $height, $master = NULL) + { + if ( ! $this->valid_size('width', $width)) + throw new Kohana_Exception('image.invalid_width', $width); + + if ( ! $this->valid_size('height', $height)) + throw new Kohana_Exception('image.invalid_height', $height); + + if (empty($width) AND empty($height)) + throw new Kohana_Exception('image.invalid_dimensions', __FUNCTION__); + + if ($master === NULL) + { + // Maintain the aspect ratio by default + $master = Image::AUTO; + } + elseif ( ! $this->valid_size('master', $master)) + throw new Kohana_Exception('image.invalid_master'); + + $this->actions['resize'] = array + ( + 'width' => $width, + 'height' => $height, + 'master' => $master, + ); + + return $this; + } + + /** + * Crop an image to a specific width and height. You may also set the top + * and left offset. + * This method is chainable. + * + * @throws Kohana_Exception + * @param integer width + * @param integer height + * @param integer top offset, pixel value or one of: top, center, bottom + * @param integer left offset, pixel value or one of: left, center, right + * @return object + */ + public function crop($width, $height, $top = 'center', $left = 'center') + { + if ( ! $this->valid_size('width', $width)) + throw new Kohana_Exception('image.invalid_width', $width); + + if ( ! $this->valid_size('height', $height)) + throw new Kohana_Exception('image.invalid_height', $height); + + if ( ! $this->valid_size('top', $top)) + throw new Kohana_Exception('image.invalid_top', $top); + + if ( ! $this->valid_size('left', $left)) + throw new Kohana_Exception('image.invalid_left', $left); + + if (empty($width) AND empty($height)) + throw new Kohana_Exception('image.invalid_dimensions', __FUNCTION__); + + $this->actions['crop'] = array + ( + 'width' => $width, + 'height' => $height, + 'top' => $top, + 'left' => $left, + ); + + return $this; + } + + /** + * Allows rotation of an image by 180 degrees clockwise or counter clockwise. + * + * @param integer degrees + * @return object + */ + public function rotate($degrees) + { + $degrees = (int) $degrees; + + if ($degrees > 180) + { + do + { + // Keep subtracting full circles until the degrees have normalized + $degrees -= 360; + } + while($degrees > 180); + } + + if ($degrees < -180) + { + do + { + // Keep adding full circles until the degrees have normalized + $degrees += 360; + } + while($degrees < -180); + } + + $this->actions['rotate'] = $degrees; + + return $this; + } + + /** + * Flip an image horizontally or vertically. + * + * @throws Kohana_Exception + * @param integer direction + * @return object + */ + public function flip($direction) + { + if ($direction !== self::HORIZONTAL AND $direction !== self::VERTICAL) + throw new Kohana_Exception('image.invalid_flip'); + + $this->actions['flip'] = $direction; + + return $this; + } + + /** + * Change the quality of an image. + * + * @param integer quality as a percentage + * @return object + */ + public function quality($amount) + { + $this->actions['quality'] = max(1, min($amount, 100)); + + return $this; + } + + /** + * Sharpen an image. + * + * @param integer amount to sharpen, usually ~20 is ideal + * @return object + */ + public function sharpen($amount) + { + $this->actions['sharpen'] = max(1, min($amount, 100)); + + return $this; + } + + /** + * Save the image to a new image or overwrite this image. + * + * @throws Kohana_Exception + * @param string new image filename + * @param integer permissions for new image + * @return object + */ + public function save($new_image = FALSE, $chmod = 0644) + { + // If no new image is defined, use the current image + empty($new_image) and $new_image = $this->image['file']; + + // Separate the directory and filename + $dir = pathinfo($new_image, PATHINFO_DIRNAME); + $file = pathinfo($new_image, PATHINFO_BASENAME); + + // Normalize the path + $dir = str_replace('\\', '/', realpath($dir)).'/'; + + if ( ! is_writable($dir)) + throw new Kohana_Exception('image.directory_unwritable', $dir); + + if ($status = $this->driver->process($this->image, $this->actions, $dir, $file)) + { + if ($chmod !== FALSE) + { + // Set permissions + chmod($new_image, $chmod); + } + } + + // Reset the actions + $this->actions = array(); + + return $status; + } + + /** + * Output the image to the browser. + * + * @return object + */ + public function render() + { + $new_image = $this->image['file']; + + // Separate the directory and filename + $dir = pathinfo($new_image, PATHINFO_DIRNAME); + $file = pathinfo($new_image, PATHINFO_BASENAME); + + // Normalize the path + $dir = str_replace('\\', '/', realpath($dir)).'/'; + + // Process the image with the driver + $status = $this->driver->process($this->image, $this->actions, $dir, $file, $render = TRUE); + + // Reset the actions + $this->actions = array(); + + return $status; + } + + /** + * Sanitize a given value type. + * + * @param string type of property + * @param mixed property value + * @return boolean + */ + protected function valid_size($type, & $value) + { + if (is_null($value)) + return TRUE; + + if ( ! is_scalar($value)) + return FALSE; + + switch ($type) + { + case 'width': + case 'height': + if (is_string($value) AND ! ctype_digit($value)) + { + // Only numbers and percent signs + if ( ! preg_match('/^[0-9]++%$/D', $value)) + return FALSE; + } + else + { + $value = (int) $value; + } + break; + case 'top': + if (is_string($value) AND ! ctype_digit($value)) + { + if ( ! in_array($value, array('top', 'bottom', 'center'))) + return FALSE; + } + else + { + $value = (int) $value; + } + break; + case 'left': + if (is_string($value) AND ! ctype_digit($value)) + { + if ( ! in_array($value, array('left', 'right', 'center'))) + return FALSE; + } + else + { + $value = (int) $value; + } + break; + case 'master': + if ($value !== Image::NONE AND + $value !== Image::AUTO AND + $value !== Image::WIDTH AND + $value !== Image::HEIGHT) + return FALSE; + break; + } + + return TRUE; + } + +} // End Image
\ No newline at end of file diff --git a/kohana/libraries/Input.php b/kohana/libraries/Input.php new file mode 100644 index 00000000..12da494a --- /dev/null +++ b/kohana/libraries/Input.php @@ -0,0 +1,450 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Input library. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Input_Core { + + // Enable or disable automatic XSS cleaning + protected $use_xss_clean = FALSE; + + // Are magic quotes enabled? + protected $magic_quotes_gpc = FALSE; + + // IP address of current user + public $ip_address; + + // Input singleton + protected static $instance; + + /** + * Retrieve a singleton instance of Input. This will always be the first + * created instance of this class. + * + * @return object + */ + public static function instance() + { + if (self::$instance === NULL) + { + // Create a new instance + return new Input; + } + + return self::$instance; + } + + /** + * Sanitizes global GET, POST and COOKIE data. Also takes care of + * magic_quotes and register_globals, if they have been enabled. + * + * @return void + */ + public function __construct() + { + // Use XSS clean? + $this->use_xss_clean = (bool) Kohana::config('core.global_xss_filtering'); + + if (self::$instance === NULL) + { + // magic_quotes_runtime is enabled + if (get_magic_quotes_runtime()) + { + set_magic_quotes_runtime(0); + Kohana::log('debug', 'Disable magic_quotes_runtime! It is evil and deprecated: http://php.net/magic_quotes'); + } + + // magic_quotes_gpc is enabled + if (get_magic_quotes_gpc()) + { + $this->magic_quotes_gpc = TRUE; + Kohana::log('debug', 'Disable magic_quotes_gpc! It is evil and deprecated: http://php.net/magic_quotes'); + } + + // register_globals is enabled + if (ini_get('register_globals')) + { + if (isset($_REQUEST['GLOBALS'])) + { + // Prevent GLOBALS override attacks + exit('Global variable overload attack.'); + } + + // Destroy the REQUEST global + $_REQUEST = array(); + + // These globals are standard and should not be removed + $preserve = array('GLOBALS', '_REQUEST', '_GET', '_POST', '_FILES', '_COOKIE', '_SERVER', '_ENV', '_SESSION'); + + // This loop has the same effect as disabling register_globals + foreach ($GLOBALS as $key => $val) + { + if ( ! in_array($key, $preserve)) + { + global $$key; + $$key = NULL; + + // Unset the global variable + unset($GLOBALS[$key], $$key); + } + } + + // Warn the developer about register globals + Kohana::log('debug', 'Disable register_globals! It is evil and deprecated: http://php.net/register_globals'); + } + + if (is_array($_GET)) + { + foreach ($_GET as $key => $val) + { + // Sanitize $_GET + $_GET[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + } + else + { + $_GET = array(); + } + + if (is_array($_POST)) + { + foreach ($_POST as $key => $val) + { + // Sanitize $_POST + $_POST[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + } + else + { + $_POST = array(); + } + + if (is_array($_COOKIE)) + { + foreach ($_COOKIE as $key => $val) + { + // Sanitize $_COOKIE + $_COOKIE[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + } + else + { + $_COOKIE = array(); + } + + // Create a singleton + self::$instance = $this; + + Kohana::log('debug', 'Global GET, POST and COOKIE data sanitized'); + } + } + + /** + * Fetch an item from the $_GET array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function get($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_GET, $key, $default, $xss_clean); + } + + /** + * Fetch an item from the $_POST array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function post($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_POST, $key, $default, $xss_clean); + } + + /** + * Fetch an item from the $_COOKIE array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function cookie($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_COOKIE, $key, $default, $xss_clean); + } + + /** + * Fetch an item from the $_SERVER array. + * + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + public function server($key = array(), $default = NULL, $xss_clean = FALSE) + { + return $this->search_array($_SERVER, $key, $default, $xss_clean); + } + + /** + * Fetch an item from a global array. + * + * @param array array to search + * @param string key to find + * @param mixed default value + * @param boolean XSS clean the value + * @return mixed + */ + protected function search_array($array, $key, $default = NULL, $xss_clean = FALSE) + { + if ($key === array()) + return $array; + + if ( ! isset($array[$key])) + return $default; + + // Get the value + $value = $array[$key]; + + if ($this->use_xss_clean === FALSE AND $xss_clean === TRUE) + { + // XSS clean the value + $value = $this->xss_clean($value); + } + + return $value; + } + + /** + * Fetch the IP Address. + * + * @return string + */ + public function ip_address() + { + if ($this->ip_address !== NULL) + return $this->ip_address; + + if ($ip = $this->server('HTTP_CLIENT_IP')) + { + $this->ip_address = $ip; + } + elseif ($ip = $this->server('REMOTE_ADDR')) + { + $this->ip_address = $ip; + } + elseif ($ip = $this->server('HTTP_X_FORWARDED_FOR')) + { + $this->ip_address = $ip; + } + + if ($comma = strrpos($this->ip_address, ',') !== FALSE) + { + $this->ip_address = substr($this->ip_address, $comma + 1); + } + + if ( ! valid::ip($this->ip_address)) + { + // Use an empty IP + $this->ip_address = '0.0.0.0'; + } + + return $this->ip_address; + } + + /** + * Clean cross site scripting exploits from string. + * HTMLPurifier may be used if installed, otherwise defaults to built in method. + * Note - This function should only be used to deal with data upon submission. + * It's not something that should be used for general runtime processing + * since it requires a fair amount of processing overhead. + * + * @param string data to clean + * @param string xss_clean method to use ('htmlpurifier' or defaults to built-in method) + * @return string + */ + public function xss_clean($data, $tool = NULL) + { + if ($tool === NULL) + { + // Use the default tool + $tool = Kohana::config('core.global_xss_filtering'); + } + + if (is_array($data)) + { + foreach ($data as $key => $val) + { + $data[$key] = $this->xss_clean($val, $tool); + } + + return $data; + } + + // Do not clean empty strings + if (trim($data) === '') + return $data; + + if ($tool === TRUE) + { + // NOTE: This is necessary because switch is NOT type-sensative! + $tool = 'default'; + } + + switch ($tool) + { + case 'htmlpurifier': + /** + * @todo License should go here, http://htmlpurifier.org/ + */ + if ( ! class_exists('HTMLPurifier_Config', FALSE)) + { + // Load HTMLPurifier + require Kohana::find_file('vendor', 'htmlpurifier/HTMLPurifier.auto', TRUE); + require 'HTMLPurifier.func.php'; + } + + // Set configuration + $config = HTMLPurifier_Config::createDefault(); + $config->set('HTML', 'TidyLevel', 'none'); // Only XSS cleaning now + + // Run HTMLPurifier + $data = HTMLPurifier($data, $config); + break; + default: + // http://svn.bitflux.ch/repos/public/popoon/trunk/classes/externalinput.php + // +----------------------------------------------------------------------+ + // | Copyright (c) 2001-2006 Bitflux GmbH | + // +----------------------------------------------------------------------+ + // | Licensed under the Apache License, Version 2.0 (the "License"); | + // | you may not use this file except in compliance with the License. | + // | You may obtain a copy of the License at | + // | http://www.apache.org/licenses/LICENSE-2.0 | + // | Unless required by applicable law or agreed to in writing, software | + // | distributed under the License is distributed on an "AS IS" BASIS, | + // | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | + // | implied. See the License for the specific language governing | + // | permissions and limitations under the License. | + // +----------------------------------------------------------------------+ + // | Author: Christian Stocker <chregu@bitflux.ch> | + // +----------------------------------------------------------------------+ + // + // Kohana Modifications: + // * Changed double quotes to single quotes, changed indenting and spacing + // * Removed magic_quotes stuff + // * Increased regex readability: + // * Used delimeters that aren't found in the pattern + // * Removed all unneeded escapes + // * Deleted U modifiers and swapped greediness where needed + // * Increased regex speed: + // * Made capturing parentheses non-capturing where possible + // * Removed parentheses where possible + // * Split up alternation alternatives + // * Made some quantifiers possessive + + // Fix &entity\n; + $data = str_replace(array('&','<','>'), array('&amp;','&lt;','&gt;'), $data); + $data = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $data); + $data = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $data); + $data = html_entity_decode($data, ENT_COMPAT, 'UTF-8'); + + // Remove any attribute starting with "on" or xmlns + $data = preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*+>#iu', '$1>', $data); + + // Remove javascript: and vbscript: protocols + $data = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $data); + $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $data); + $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $data); + + // Only works in IE: <span style="width: expression(alert('Ping!'));"></span> + $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i', '$1>', $data); + $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i', '$1>', $data); + $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu', '$1>', $data); + + // Remove namespaced elements (we do not need them) + $data = preg_replace('#</*\w+:\w[^>]*+>#i', '', $data); + + do + { + // Remove really unwanted tags + $old_data = $data; + $data = preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^>]*+>#i', '', $data); + } + while ($old_data !== $data); + break; + } + + return $data; + } + + /** + * This is a helper method. It enforces W3C specifications for allowed + * key name strings, to prevent malicious exploitation. + * + * @param string string to clean + * @return string + */ + public function clean_input_keys($str) + { + $chars = PCRE_UNICODE_PROPERTIES ? '\pL' : 'a-zA-Z'; + + if ( ! preg_match('#^['.$chars.'0-9:_.-]++$#uD', $str)) + { + exit('Disallowed key characters in global data.'); + } + + return $str; + } + + /** + * This is a helper method. It escapes data and forces all newline + * characters to "\n". + * + * @param unknown_type string to clean + * @return string + */ + public function clean_input_data($str) + { + if (is_array($str)) + { + $new_array = array(); + foreach ($str as $key => $val) + { + // Recursion! + $new_array[$this->clean_input_keys($key)] = $this->clean_input_data($val); + } + return $new_array; + } + + if ($this->magic_quotes_gpc === TRUE) + { + // Remove annoying magic quotes + $str = stripslashes($str); + } + + if ($this->use_xss_clean === TRUE) + { + $str = $this->xss_clean($str); + } + + if (strpos($str, "\r") !== FALSE) + { + // Standardize newlines + $str = str_replace(array("\r\n", "\r"), "\n", $str); + } + + return $str; + } + +} // End Input Class diff --git a/kohana/libraries/Model.php b/kohana/libraries/Model.php new file mode 100644 index 00000000..d6130896 --- /dev/null +++ b/kohana/libraries/Model.php @@ -0,0 +1,30 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Model base class. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Model_Core { + + protected $db; + + /** + * Loads the database instance, if the database is not already loaded. + * + * @return void + */ + public function __construct() + { + if ( ! is_object($this->db)) + { + // Load the default database + $this->db = Database::instance('default'); + } + } + +} // End Model
\ No newline at end of file diff --git a/kohana/libraries/ORM.php b/kohana/libraries/ORM.php new file mode 100644 index 00000000..ace7a4d8 --- /dev/null +++ b/kohana/libraries/ORM.php @@ -0,0 +1,1095 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Object Relational Mapping (ORM) is a method of abstracting database + * access to standard PHP calls. All table rows are represented as a model. + * + * @see http://en.wikipedia.org/wiki/Active_record + * @see http://en.wikipedia.org/wiki/Object-relational_mapping + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class ORM_Core { + + // Current relationships + protected $has_one = array(); + protected $belongs_to = array(); + protected $has_many = array(); + protected $has_and_belongs_to_many = array(); + + // Current object + protected $object = array(); + protected $changed = array(); + protected $loaded = FALSE; + protected $saved = FALSE; + protected $sorting = array('id' => 'asc'); + + // Related objects + protected $related = array(); + + // Model table information + protected $object_name; + protected $table_name; + protected $table_columns; + protected $ignored_columns; + + // Table primary key and value + protected $primary_key = 'id'; + protected $primary_val = 'name'; + + // Model configuration + protected $table_names_plural = TRUE; + protected $reload_on_wakeup = TRUE; + + // Database configuration + protected $db = 'default'; + protected $db_applied = array(); + + /** + * Creates and returns a new model. + * + * @chainable + * @param string model name + * @param mixed parameter for find() + * @return ORM + */ + public static function factory($model, $id = NULL) + { + // Set class name + $model = ucfirst($model).'_Model'; + + return new $model($id); + } + + /** + * Prepares the model database connection and loads the object. + * + * @param mixed parameter for find or object to load + * @return void + */ + public function __construct($id = NULL) + { + // Set the object name + $this->object_name = strtolower(substr(get_class($this), 0, -6)); + + // Initialize database + $this->__initialize(); + + if ($id === NULL OR $id === '') + { + // Clear the object + $this->clear(); + } + elseif (is_object($id)) + { + // Object is loaded and saved + $this->loaded = $this->saved = TRUE; + + // Load an object + $this->load_values((array) $id); + } + else + { + // Find an object + $this->find($id); + } + } + + /** + * Prepares the model database connection, determines the table name, + * and loads column information. + * + * @return void + */ + public function __initialize() + { + if ( ! is_object($this->db)) + { + // Get database instance + $this->db = Database::instance($this->db); + } + + if (empty($this->table_name)) + { + // Table name is the same as the object name + $this->table_name = $this->object_name; + + if ($this->table_names_plural === TRUE) + { + // Make the table name plural + $this->table_name = inflector::plural($this->table_name); + } + } + + if (is_array($this->ignored_columns)) + { + // Make the ignored columns mirrored = mirrored + $this->ignored_columns = array_combine($this->ignored_columns, $this->ignored_columns); + } + + // Load column information + $this->reload_columns(); + } + + /** + * Allows serialization of only the object data and state, to prevent + * "stale" objects being unserialized, which also requires less memory. + * + * @return array + */ + public function __sleep() + { + // Store only information about the object + return array('object_name', 'object', 'changed', 'loaded', 'saved', 'sorting'); + } + + /** + * Prepares the database connection and reloads the object. + * + * @return void + */ + public function __wakeup() + { + // Initialize database + $this->__initialize(); + + if ($this->reload_on_wakeup === TRUE) + { + // Reload the object + $this->reload(); + } + } + + /** + * Handles pass-through to database methods. Calls to query methods + * (query, get, insert, update) are not allowed. Query builder methods + * are chainable. + * + * @param string method name + * @param array method arguments + * @return mixed + */ + public function __call($method, array $args) + { + if (method_exists($this->db, $method)) + { + if (in_array($method, array('query', 'get', 'insert', 'update', 'delete'))) + throw new Kohana_Exception('orm.query_methods_not_allowed'); + + // Method has been applied to the database + $this->db_applied[$method] = $method; + + // Number of arguments passed + $num_args = count($args); + + if ($method === 'select' AND $num_args > 3) + { + // Call select() manually to avoid call_user_func_array + $this->db->select($args); + } + else + { + // We use switch here to manually call the database methods. This is + // done for speed: call_user_func_array can take over 300% longer to + // make calls. Mose database methods are 4 arguments or less, so this + // avoids almost any calls to call_user_func_array. + + switch ($num_args) + { + case 0: + // Support for things like reset_select, reset_write, list_tables + return $this->db->$method(); + break; + case 1: + $this->db->$method($args[0]); + break; + case 2: + $this->db->$method($args[0], $args[1]); + break; + case 3: + $this->db->$method($args[0], $args[1], $args[2]); + break; + case 4: + $this->db->$method($args[0], $args[1], $args[2], $args[3]); + break; + default: + // Here comes the snail... + call_user_func_array(array($this->db, $method), $args); + break; + } + } + + return $this; + } + else + { + throw new Kohana_Exception('core.invalid_method', $method, get_class($this)); + } + } + + /** + * Handles retrieval of all model values, relationships, and metadata. + * + * @param string column name + * @return mixed + */ + public function __get($column) + { + if (isset($this->ignored_columns[$column])) + { + return NULL; + } + elseif (isset($this->object[$column]) OR array_key_exists($column, $this->object)) + { + return $this->object[$column]; + } + elseif (isset($this->related[$column])) + { + return $this->related[$column]; + } + elseif ($column === 'primary_key_value') + { + return $this->object[$this->primary_key]; + } + elseif (($owner = isset($this->has_one[$column])) OR isset($this->belongs_to[$column])) + { + // Determine the model name + $model = ($owner === TRUE) ? $this->has_one[$column] : $this->belongs_to[$column]; + + // Load model + $model = ORM::factory($model); + + if (isset($this->object[$column.'_'.$model->primary_key])) + { + // Use the FK that exists in this model as the PK + $where = array($model->primary_key => $this->object[$column.'_'.$model->primary_key]); + } + else + { + // Use this model PK as the FK + $where = array($this->foreign_key() => $this->object[$this->primary_key]); + } + + // one<>alias:one relationship + return $this->related[$column] = $model->find($where); + } + elseif (in_array($column, $this->has_one) OR in_array($column, $this->belongs_to)) + { + $model = ORM::factory($column); + + if (isset($this->object[$column.'_'.$model->primary_key])) + { + // Use the FK that exists in this model as the PK + $where = array($model->primary_key => $this->object[$column.'_'.$model->primary_key]); + } + else + { + // Use this model PK as the FK + $where = array($this->foreign_key() => $this->object[$this->primary_key]); + } + + // one<>one relationship + return $this->related[$column] = ORM::factory($column, $where); + } + elseif (isset($this->has_many[$column])) + { + // Load the "middle" model + $through = ORM::factory(inflector::singular($this->has_many[$column])); + + // Load the "end" model + $model = ORM::factory(inflector::singular($column)); + + // Load JOIN info + $join_table = $through->table_name; + $join_col1 = $model->foreign_key(NULL, $join_table); + $join_col2 = $model->foreign_key(TRUE); + + // one<>alias:many relationship + return $this->related[$column] = $model + ->join($join_table, $join_col1, $join_col2) + ->where($this->foreign_key(NULL, $join_table), $this->object[$this->primary_key]) + ->find_all(); + } + elseif (in_array($column, $this->has_many)) + { + // one<>many relationship + return $this->related[$column] = ORM::factory(inflector::singular($column)) + ->where($this->foreign_key($column), $this->object[$this->primary_key]) + ->find_all(); + } + elseif (in_array($column, $this->has_and_belongs_to_many)) + { + // Load the remote model, always singular + $model = ORM::factory(inflector::singular($column)); + + // Load JOIN info + $join_table = $model->join_table($this->table_name); + $join_col1 = $model->foreign_key(NULL, $join_table); + $join_col2 = $model->foreign_key(TRUE); + + // many<>many relationship + return $this->related[$column] = $model + ->join($join_table, $join_col1, $join_col2) + ->where($this->foreign_key(NULL, $join_table), $this->object[$this->primary_key]) + ->find_all(); + } + elseif (in_array($column, array + ( + 'object_name', // Object + 'primary_key', 'primary_val', 'table_name', 'table_columns', // Table + 'loaded', 'saved', // Status + 'has_one', 'belongs_to', 'has_many', 'has_and_belongs_to_many', // Relationships + ))) + { + // Model meta information + return $this->$column; + } + else + { + throw new Kohana_Exception('core.invalid_property', $column, get_class($this)); + } + } + + /** + * Handles setting of all model values, and tracks changes between values. + * + * @param string column name + * @param mixed column value + * @return void + */ + public function __set($column, $value) + { + if (isset($this->ignored_columns[$column])) + { + return NULL; + } + elseif (isset($this->object[$column]) OR array_key_exists($column, $this->object)) + { + if (isset($this->table_columns[$column])) + { + // Data has changed + $this->changed[$column] = $column; + + // Object is no longer saved + $this->saved = FALSE; + } + + $this->object[$column] = $this->load_type($column, $value); + } + else + { + throw new Kohana_Exception('core.invalid_property', $column, get_class($this)); + } + } + + /** + * Checks if object data is set. + * + * @param string column name + * @return boolean + */ + public function __isset($column) + { + return (isset($this->object[$column]) OR isset($this->related[$column])); + } + + /** + * Unsets object data. + * + * @param string column name + * @return void + */ + public function __unset($column) + { + unset($this->object[$column], $this->changed[$column], $this->related[$column]); + } + + /** + * Displays the primary key of a model when it is converted to a string. + * + * @return string + */ + public function __toString() + { + return (string) $this->object[$this->primary_key]; + } + + /** + * Returns the values of this object as an array. + * + * @return array + */ + public function as_array() + { + return $this->object; + } + + /** + * Finds and loads a single database row into the object. + * + * @chainable + * @param mixed primary key or an array of clauses + * @return ORM + */ + public function find($id = NULL) + { + if ($id !== NULL) + { + if (is_array($id)) + { + // Search for all clauses + $this->db->where($id); + } + else + { + // Search for a specific column + $this->db->where($this->unique_key($id), $id); + } + } + + return $this->load_result(); + } + + /** + * Finds multiple database rows and returns an iterator of the rows found. + * + * @chainable + * @param integer SQL limit + * @param integer SQL offset + * @return ORM_Iterator + */ + public function find_all($limit = NULL, $offset = 0) + { + if ($limit !== NULL) + { + // Set limit + $this->db->limit($limit); + } + + if ($offset !== NULL) + { + // Set offset + $this->db->offset($offset); + } + + if ( ! isset($this->db_applied['orderby'])) + { + // Apply sorting + $this->db->orderby($this->sorting); + } + + return $this->load_result(TRUE); + } + + /** + * Creates a key/value array from all of the objects available. Uses find_all + * to find the objects. + * + * @param string key column + * @param string value column + * @return array + */ + public function select_list($key, $val) + { + // Return a select list from the results + return $this->select($key, $val)->find_all()->select_list($key, $val); + } + + /** + * Validates the current object. This method should generally be called + * via the model, after the $_POST Validation object has been created. + * + * @param object Validation array + * @return boolean + */ + public function validate(Validation $array, $save = FALSE) + { + if ( ! $array->submitted()) + { + $safe_array = $array->safe_array(); + + foreach ($safe_array as $key => $val) + { + // Pre-fill data + $array[$key] = $this->$key; + } + } + + // Validate the array + if ($status = $array->validate()) + { + $safe_array = $array->safe_array(); + + foreach ($safe_array as $key => $val) + { + // Set new data + $this->$key = $val; + } + + if ($save === TRUE OR is_string($save)) + { + // Save this object + $this->save(); + + if (is_string($save)) + { + // Redirect to the saved page + url::redirect($save); + } + } + } + + // Return validation status + return $status; + } + + /** + * Saves the current object. If the object is new, it will be reloaded + * after being saved. + * + * @chainable + * @return ORM + */ + public function save() + { + if (empty($this->changed)) + return $this; + + $data = array(); + foreach ($this->changed as $column) + { + // Compile changed data + $data[$column] = $this->object[$column]; + } + + if ($this->loaded === TRUE) + { + $query = $this->db + ->where($this->primary_key, $this->object[$this->primary_key]) + ->update($this->table_name, $data); + + // Object has been saved + $this->saved = TRUE; + + // Nothing has been changed + $this->changed = array(); + } + else + { + $query = $this->db + ->insert($this->table_name, $data); + + if ($query->count() > 0) + { + if (empty($this->object[$this->primary_key])) + { + // Load the insert id as the primary key + $this->object[$this->primary_key] = $query->insert_id(); + } + + // Reload the object + $this->reload(); + } + } + + return $this; + } + + /** + * Deletes the current object from the database. This does NOT destroy + * relationships that have been created with other objects. + * + * @chainable + * @return ORM + */ + public function delete($id = NULL) + { + if ($id === NULL AND $this->loaded) + { + // Use the the primary key value + $id = $this->object[$this->primary_key]; + } + + // Delete this object + $this->db->where($this->primary_key, $id)->delete($this->table_name); + + return $this->clear(); + } + + /** + * Delete all objects in the associated table. This does NOT destroy + * relationships that have been created with other objects. + * + * @chainable + * @param array ids to delete + * @return ORM + */ + public function delete_all($ids = NULL) + { + if (is_array($ids)) + { + // Delete only given ids + $this->db->in($this->primary_key, $ids); + } + else + { + // Delete all records + $this->db->where(TRUE); + } + + // Delete all objects + $this->db->delete($this->table_name); + + return $this->clear(); + } + + /** + * Unloads the current object and clears the status. + * + * @chainable + * @return ORM + */ + public function clear() + { + // Object is no longer loaded or saved + $this->loaded = $this->saved = FALSE; + + // Nothing has been changed + $this->changed = array(); + + // Replace the current object with an empty one + $this->load_values(array()); + + return $this; + } + + /** + * Reloads the current object from the database. + * + * @chainable + * @return ORM + */ + public function reload() + { + return $this->find($this->object[$this->primary_key]); + } + + /** + * Reload column definitions. + * + * @chainable + * @param boolean force reloading + * @return ORM + */ + public function reload_columns($force = FALSE) + { + if ($force === TRUE OR empty($this->table_columns)) + { + // Load table columns + $this->table_columns = $this->db->list_fields($this->table_name); + + if (empty($this->table_columns)) + throw new Kohana_Exception('database.table_not_found', $this->table); + } + + return $this; + } + + /** + * Tests if this object has a relationship to a different model. + * + * @param object related ORM model + * @return boolean + */ + public function has(ORM $model) + { + if ( ! $this->loaded) + return FALSE; + + if (($join_table = array_search(inflector::plural($model->object_name), $this->has_and_belongs_to_many)) === FALSE) + return FALSE; + + if (is_int($join_table)) + { + // No "through" table, load the default JOIN table + $join_table = $model->join_table($this->table_name); + } + + if ($model->loaded) + { + // Select only objects of a specific id + $this->db->where($model->foreign_key(NULL, $join_table), $model->primary_key_value); + } + + // Return the number of rows that exist + return $this->db + ->where($this->foreign_key(NULL, $join_table), $this->object[$this->primary_key]) + ->count_records($join_table); + } + + /** + * Adds a new relationship to between this model and another. + * + * @param object related ORM model + * @return boolean + */ + public function add(ORM $model) + { + if ( ! $this->loaded) + return FALSE; + + if ($this->has($model)) + return TRUE; + + if (($join_table = array_search(inflector::plural($model->object_name), $this->has_and_belongs_to_many)) === FALSE) + return FALSE; + + if (is_int($join_table)) + { + // No "through" table, load the default JOIN table + $join_table = $model->join_table($this->table_name); + } + + // Insert the new relationship + $this->db->insert($join_table, array + ( + $this->foreign_key(NULL, $join_table) => $this->object[$this->primary_key], + $model->foreign_key(NULL, $join_table) => $model->primary_key_value, + )); + + return TRUE; + } + + /** + * Adds a new relationship to between this model and another. + * + * @param object related ORM model + * @return boolean + */ + public function remove(ORM $model) + { + if ( ! $this->has($model)) + return FALSE; + + if (($join_table = array_search(inflector::plural($model->object_name), $this->has_and_belongs_to_many)) === FALSE) + return FALSE; + + if (is_int($join_table)) + { + // No "through" table, load the default JOIN table + $join_table = $model->join_table($this->table_name); + } + + if ($model->loaded) + { + // Delete only a specific object + $this->db->where($model->foreign_key(NULL, $join_table), $model->primary_key_value); + } + + // Return the number of rows deleted + return $this->db + ->where($this->foreign_key(NULL, $join_table), $this->object[$this->primary_key]) + ->delete($join_table) + ->count(); + } + + /** + * Count the number of records in the table. + * + * @return integer + */ + public function count_all() + { + // Return the total number of records in a table + return $this->db->count_records($this->table_name); + } + + /** + * Count the number of records in the last query, without LIMIT or OFFSET applied. + * + * @return integer + */ + public function count_last_query() + { + if ($sql = $this->db->last_query()) + { + if (stripos($sql, 'LIMIT') !== FALSE) + { + // Remove LIMIT from the SQL + $sql = preg_replace('/\bLIMIT\s+[^a-z]+/i', '', $sql); + } + + if (stripos($sql, 'OFFSET') !== FALSE) + { + // Remove OFFSET from the SQL + $sql = preg_replace('/\bOFFSET\s+\d+/i', '', $sql); + } + + // Get the total rows from the last query executed + $result = $this->db->query + ( + 'SELECT COUNT(*) AS '.$this->db->escape_column('total_rows').' '. + 'FROM ('.trim($sql).') AS '.$this->db->escape_table('counted_results') + ); + + if ($result->count()) + { + // Return the total number of rows from the query + return (int) $result->current()->total_rows; + } + } + + return FALSE; + } + + /** + * Proxy method to Database list_fields. + * + * @param string table name + * @return array + */ + public function list_fields($table) + { + // Proxy to database + return $this->db->list_fields($table); + } + + /** + * Proxy method to Database field_data. + * + * @param string table name + * @return array + */ + public function field_data($table) + { + // Proxy to database + return $this->db->field_data($table); + } + + /** + * Proxy method to Database last_query. + * + * @return string + */ + public function last_query() + { + // Proxy to database + return $this->db->last_query(); + } + + /** + * Proxy method to Database field_data. + * + * @chainable + * @param string SQL query to clear + * @return ORM + */ + public function clear_cache($sql = NULL) + { + // Proxy to database + $this->db->clear_cache($sql); + + return $this; + } + + /** + * Returns the unique key for a specific value. This method is expected + * to be overloaded in models if the model has other unique columns. + * + * @param mixed unique value + * @return string + */ + public function unique_key($id) + { + return $this->primary_key; + } + + /** + * Determines the name of a foreign key for a specific table. + * + * @param string related table name + * @param string prefix table name (used for JOINs) + * @return string + */ + public function foreign_key($table = NULL, $prefix_table = NULL) + { + if ($table === TRUE) + { + // Return the name of this tables PK + return $this->table_name.'.'.$this->primary_key; + } + + if (is_string($prefix_table)) + { + // Add a period for prefix_table.column support + $prefix_table .= '.'; + } + + if ( ! is_string($table) OR ! isset($this->object[$table.'_'.$this->primary_key])) + { + // Use this table + $table = $this->table_name; + + if ($this->table_names_plural === TRUE) + { + // Make the key name singular + $table = inflector::singular($table); + } + } + + return $prefix_table.$table.'_'.$this->primary_key; + } + + /** + * This uses alphabetical comparison to choose the name of the table. + * + * Example: The joining table of users and roles would be roles_users, + * because "r" comes before "u". Joining products and categories would + * result in categories_prouducts, because "c" comes before "p". + * + * Example: zoo > zebra > robber > ocean > angel > aardvark + * + * @param string table name + * @return string + */ + public function join_table($table) + { + if ($this->table_name > $table) + { + $table = $table.'_'.$this->table_name; + } + else + { + $table = $this->table_name.'_'.$table; + } + + return $table; + } + + /** + * Loads a value according to the types defined by the column metadata. + * + * @param string column name + * @param mixed value to load + * @return mixed + */ + protected function load_type($column, $value) + { + if (is_object($value) OR is_array($value) OR ! isset($this->table_columns[$column])) + return $value; + + // Load column data + $column = $this->table_columns[$column]; + + if ($value === NULL AND ! empty($column['null'])) + return $value; + + if ( ! empty($column['binary']) AND ! empty($column['exact']) AND (int) $column['length'] === 1) + { + // Use boolean for BINARY(1) fields + $column['type'] = 'boolean'; + } + + switch ($column['type']) + { + case 'int': + $value = ($value === '' AND ! empty($data['null'])) ? NULL : (int) $value; + break; + case 'float': + $value = (float) $value; + break; + case 'boolean': + $value = (bool) $value; + break; + case 'string': + $value = (string) $value; + break; + } + + return $value; + } + + /** + * Loads an array of values into into the current object. + * + * @chainable + * @param array values to load + * @return ORM + */ + protected function load_values(array $values) + { + // Get the table columns + $columns = array_keys($this->table_columns); + + // Make sure all the columns are defined + $this->object += array_combine($columns, array_fill(0, count($columns), NULL)); + + foreach ($columns as $column) + { + // Value for this column + $value = isset($values[$column]) ? $values[$column] : NULL; + + // Set value manually, to avoid triggering changes + $this->object[$column] = $this->load_type($column, $value); + } + + return $this; + } + + /** + * Loads a database result, either as a new object for this model, or as + * an iterator for multiple rows. + * + * @chainable + * @param boolean return an iterator or load a single row + * @return ORM for single rows + * @return ORM_Iterator for multiple rows + */ + protected function load_result($array = FALSE) + { + if ($array === FALSE) + { + // Only fetch 1 record + $this->db->limit(1); + } + + if ( ! isset($this->db_applied['select'])) + { + // Selete all columns by default + $this->db->select($this->table_name.'.*'); + } + + // Load the result + $result = $this->db->get($this->table_name); + + if ($array === TRUE) + { + // Return an iterated result + return new ORM_Iterator($this, $result); + } + + if ($result->count() === 1) + { + // Model is loaded and saved + $this->loaded = $this->saved = TRUE; + + // Clear relationships and changed values + $this->related = $this->changed = array(); + + // Load object values + $this->load_values($result->result(FALSE)->current()); + } + else + { + // Clear the object, nothing was found + $this->clear(); + } + + return $this; + } + +} // End ORM
\ No newline at end of file diff --git a/kohana/libraries/ORM_Iterator.php b/kohana/libraries/ORM_Iterator.php new file mode 100644 index 00000000..ae25ce7a --- /dev/null +++ b/kohana/libraries/ORM_Iterator.php @@ -0,0 +1,213 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** +* Object Relational Mapping (ORM) result iterator. +* +* $Id$ +* +* @package Core +* @author Kohana Team +* @copyright (c) 2007-2008 Kohana Team +* @license http://kohanaphp.com/license.html +*/ +class ORM_Iterator_Core implements Iterator, ArrayAccess, Countable { + + // Class attributes + protected $class_name; + protected $primary_key; + protected $primary_val; + + // Database result object + protected $result; + + public function __construct(ORM $model, $result) + { + // Class attributes + $this->class_name = get_class($model); + $this->primary_key = $model->primary_key; + $this->primary_val = $model->primary_val; + + // Database result + $this->result = $result->result(TRUE); + } + + /** + * Returns an array of the results as ORM objects. + * + * @return array + */ + public function as_array() + { + $array = array(); + + if ($results = $this->result->result_array()) + { + // Import class name + $class = $this->class_name; + + foreach ($results as $obj) + { + $array[] = new $class($obj); + } + } + + return $array; + } + + /** + * Create a key/value array from the results. + * + * @param string key column + * @param string value column + * @return array + */ + public function select_list($key = NULL, $val = NULL) + { + if ($key === NULL) + { + // Use the default key + $key = $this->primary_key; + } + + if ($val === NULL) + { + // Use the default value + $val = $this->primary_val; + } + + $array = array(); + foreach ($this->result->result_array() as $row) + { + $array[$row->$key] = $row->$val; + } + return $array; + } + + /** + * Return a range of offsets. + * + * @param integer start + * @param integer end + * @return array + */ + public function range($start, $end) + { + // Array of objects + $array = array(); + + if ($this->result->offsetExists($start)) + { + // Import the class name + $class = $this->class_name; + + // Set the end offset + $end = $this->result->offsetExists($end) ? $end : $this->count(); + + for ($i = $start; $i < $end; $i++) + { + // Insert each object in the range + $array[] = new $class($this->result->offsetGet($i)); + } + } + + return $array; + } + + /** + * Countable: count + */ + public function count() + { + return $this->result->count(); + } + + /** + * Iterator: current + */ + public function current() + { + if ($row = $this->result->current()) + { + // Import class name + $class = $this->class_name; + + $row = new $class($row); + } + + return $row; + } + + /** + * Iterator: key + */ + public function key() + { + return $this->result->key(); + } + + /** + * Iterator: next + */ + public function next() + { + return $this->result->next(); + } + + /** + * Iterator: rewind + */ + public function rewind() + { + $this->result->rewind(); + } + + /** + * Iterator: valid + */ + public function valid() + { + return $this->result->valid(); + } + + /** + * ArrayAccess: offsetExists + */ + public function offsetExists($offset) + { + return $this->result->offsetExists($offset); + } + + /** + * ArrayAccess: offsetGet + */ + public function offsetGet($offset) + { + if ($this->result->offsetExists($offset)) + { + // Import class name + $class = $this->class_name; + + return new $class($this->result->offsetGet($offset)); + } + } + + /** + * ArrayAccess: offsetSet + * + * @throws Kohana_Database_Exception + */ + public function offsetSet($offset, $value) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + + /** + * ArrayAccess: offsetUnset + * + * @throws Kohana_Database_Exception + */ + public function offsetUnset($offset) + { + throw new Kohana_Database_Exception('database.result_read_only'); + } + +} // End ORM Iterator
\ No newline at end of file diff --git a/kohana/libraries/ORM_Tree.php b/kohana/libraries/ORM_Tree.php new file mode 100644 index 00000000..ff5f62f1 --- /dev/null +++ b/kohana/libraries/ORM_Tree.php @@ -0,0 +1,76 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Object Relational Mapping (ORM) "tree" extension. Allows ORM objects to act + * as trees, with parents and children. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class ORM_Tree_Core extends ORM { + + // Name of the child + protected $children; + + // Parent keyword name + protected $parent_key = 'parent_id'; + + /** + * Overload ORM::__get to support "parent" and "children" properties. + * + * @param string column name + * @return mixed + */ + public function __get($column) + { + if ($column === 'parent') + { + if (empty($this->related[$column])) + { + // Load child model + $model = ORM::factory(inflector::singular($this->children)); + + if (isset($this->object[$this->parent_key])) + { + // Find children of this parent + $model->where($this->parent_key, $this->object[$this->parent_key])->find(); + } + + $this->related[$column] = $model; + } + + return $this->related[$column]; + } + elseif ($column === 'children') + { + if (empty($this->related[$column])) + { + $model = ORM::factory(inflector::singular($this->children)); + + if ($this->children === $this->table_name) + { + // Load children within this table + $this->related[$column] = $model + ->where($this->parent_key, $this->object[$this->primary_key]) + ->find_all(); + } + else + { + // Find first selection of children + $this->related[$column] = $model + ->where($this->foreign_key(), $this->object[$this->primary_key]) + ->where($this->parent_key, NULL) + ->find_all(); + } + } + + return $this->related[$column]; + } + + return parent::__get($column); + } + +} // End ORM Tree
\ No newline at end of file diff --git a/kohana/libraries/Pagination.php b/kohana/libraries/Pagination.php new file mode 100644 index 00000000..42c7c511 --- /dev/null +++ b/kohana/libraries/Pagination.php @@ -0,0 +1,235 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Pagination library. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Pagination_Core { + + // Config values + protected $base_url = ''; + protected $directory = 'pagination'; + protected $style = 'classic'; + protected $uri_segment = 3; + protected $query_string = ''; + protected $items_per_page = 20; + protected $total_items = 0; + protected $auto_hide = FALSE; + + // Autogenerated values + protected $url; + protected $current_page; + protected $total_pages; + protected $current_first_item; + protected $current_last_item; + protected $first_page; + protected $last_page; + protected $previous_page; + protected $next_page; + protected $sql_offset; + protected $sql_limit; + + /** + * Constructs and returns a new Pagination object. + * + * @param array configuration settings + * @return object + */ + public function factory($config = array()) + { + return new Pagination($config); + } + + /** + * Constructs a new Pagination object. + * + * @param array configuration settings + * @return void + */ + public function __construct($config = array()) + { + // No custom group name given + if ( ! isset($config['group'])) + { + $config['group'] = 'default'; + } + + // Pagination setup + $this->initialize($config); + + Kohana::log('debug', 'Pagination Library initialized'); + } + + /** + * Sets config values. + * + * @throws Kohana_Exception + * @param array configuration settings + * @return void + */ + public function initialize($config = array()) + { + // Load config group + if (isset($config['group'])) + { + // Load and validate config group + if ( ! is_array($group_config = Kohana::config('pagination.'.$config['group']))) + throw new Kohana_Exception('pagination.undefined_group', $config['group']); + + // All pagination config groups inherit default config group + if ($config['group'] !== 'default') + { + // Load and validate default config group + if ( ! is_array($default_config = Kohana::config('pagination.default'))) + throw new Kohana_Exception('pagination.undefined_group', 'default'); + + // Merge config group with default config group + $group_config += $default_config; + } + + // Merge custom config items with config group + $config += $group_config; + } + + // Assign config values to the object + foreach ($config as $key => $value) + { + if (property_exists($this, $key)) + { + $this->$key = $value; + } + } + + // Clean view directory + $this->directory = trim($this->directory, '/').'/'; + + // Build generic URL with page in query string + if ($this->query_string !== '') + { + // Extract current page + $this->current_page = isset($_GET[$this->query_string]) ? (int) $_GET[$this->query_string] : 1; + + // Insert {page} placeholder + $_GET[$this->query_string] = '{page}'; + + // Create full URL + $this->url = url::site(Router::$current_uri).'?'.str_replace('%7Bpage%7D', '{page}', http_build_query($_GET)); + + // Reset page number + $_GET[$this->query_string] = $this->current_page; + } + + // Build generic URL with page as URI segment + else + { + // Use current URI if no base_url set + $this->url = ($this->base_url === '') ? Router::$segments : explode('/', trim($this->base_url, '/')); + + // Convert uri 'label' to corresponding integer if needed + if (is_string($this->uri_segment)) + { + if (($key = array_search($this->uri_segment, $this->url)) === FALSE) + { + // If uri 'label' is not found, auto add it to base_url + $this->url[] = $this->uri_segment; + $this->uri_segment = count($this->url) + 1; + } + else + { + $this->uri_segment = $key + 2; + } + } + + // Insert {page} placeholder + $this->url[$this->uri_segment - 1] = '{page}'; + + // Create full URL + $this->url = url::site(implode('/', $this->url)).Router::$query_string; + + // Extract current page + $this->current_page = URI::instance()->segment($this->uri_segment); + } + + // Core pagination values + $this->total_items = (int) max(0, $this->total_items); + $this->items_per_page = (int) max(1, $this->items_per_page); + $this->total_pages = (int) ceil($this->total_items / $this->items_per_page); + $this->current_page = (int) min(max(1, $this->current_page), max(1, $this->total_pages)); + $this->current_first_item = (int) min((($this->current_page - 1) * $this->items_per_page) + 1, $this->total_items); + $this->current_last_item = (int) min($this->current_first_item + $this->items_per_page - 1, $this->total_items); + + // If there is no first/last/previous/next page, relative to the + // current page, value is set to FALSE. Valid page number otherwise. + $this->first_page = ($this->current_page === 1) ? FALSE : 1; + $this->last_page = ($this->current_page >= $this->total_pages) ? FALSE : $this->total_pages; + $this->previous_page = ($this->current_page > 1) ? $this->current_page - 1 : FALSE; + $this->next_page = ($this->current_page < $this->total_pages) ? $this->current_page + 1 : FALSE; + + // SQL values + $this->sql_offset = (int) ($this->current_page - 1) * $this->items_per_page; + $this->sql_limit = sprintf(' LIMIT %d OFFSET %d ', $this->items_per_page, $this->sql_offset); + } + + /** + * Generates the HTML for the chosen pagination style. + * + * @param string pagination style + * @return string pagination html + */ + public function render($style = NULL) + { + // Hide single page pagination + if ($this->auto_hide === TRUE AND $this->total_pages <= 1) + return ''; + + if ($style === NULL) + { + // Use default style + $style = $this->style; + } + + // Return rendered pagination view + return View::factory($this->directory.$style, get_object_vars($this))->render(); + } + + /** + * Magically converts Pagination object to string. + * + * @return string pagination html + */ + public function __toString() + { + return $this->render(); + } + + /** + * Magically gets a pagination variable. + * + * @param string variable key + * @return mixed variable value if the key is found + * @return void if the key is not found + */ + public function __get($key) + { + if (isset($this->$key)) + return $this->$key; + } + + /** + * Adds a secondary interface for accessing properties, e.g. $pagination->total_pages(). + * Note that $pagination->total_pages is the recommended way to access properties. + * + * @param string function name + * @return string + */ + public function __call($func, $args = NULL) + { + return $this->__get($func); + } + +} // End Pagination Class
\ No newline at end of file diff --git a/kohana/libraries/Profiler.php b/kohana/libraries/Profiler.php new file mode 100644 index 00000000..9b42739c --- /dev/null +++ b/kohana/libraries/Profiler.php @@ -0,0 +1,270 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Adds useful information to the bottom of the current page for debugging and optimization purposes. + * + * Benchmarks - The times and memory usage of benchmarks run by the Benchmark library. + * Database - The raw SQL and number of affected rows of Database queries. + * Session Data - Data stored in the current session if using the Session library. + * POST Data - The name and values of any POST data submitted to the current page. + * Cookie Data - All cookies sent for the current request. + * + * $Id$ + * + * @package Profiler + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Profiler_Core { + + protected $profiles = array(); + protected $show; + + public function __construct() + { + // Add all built in profiles to event + Event::add('profiler.run', array($this, 'benchmarks')); + Event::add('profiler.run', array($this, 'database')); + Event::add('profiler.run', array($this, 'session')); + Event::add('profiler.run', array($this, 'post')); + Event::add('profiler.run', array($this, 'cookies')); + + // Add profiler to page output automatically + Event::add('system.display', array($this, 'render')); + + Kohana::log('debug', 'Profiler Library initialized'); + } + + /** + * Magic __call method. Creates a new profiler section object. + * + * @param string input type + * @param string input name + * @return object + */ + public function __call($method, $args) + { + if ( ! $this->show OR (is_array($this->show) AND ! in_array($args[0], $this->show))) + return FALSE; + + // Class name + $class = 'Profiler_'.ucfirst($method); + + $class = new $class(); + + $this->profiles[$args[0]] = $class; + + return $class; + } + + /** + * Disables the profiler for this page only. + * Best used when profiler is autoloaded. + * + * @return void + */ + public function disable() + { + // Removes itself from the event queue + Event::clear('system.display', array($this, 'render')); + } + + /** + * Render the profiler. Output is added to the bottom of the page by default. + * + * @param boolean return the output if TRUE + * @return void|string + */ + public function render($return = FALSE) + { + $start = microtime(TRUE); + + $get = isset($_GET['profiler']) ? explode(',', $_GET['profiler']) : array(); + $this->show = empty($get) ? Kohana::config('profiler.show') : $get; + + Event::run('profiler.run', $this); + + $styles = ''; + foreach ($this->profiles as $profile) + { + $styles .= $profile->styles(); + } + + // Don't display if there's no profiles + if (empty($this->profiles)) + return; + + // Load the profiler view + $data = array + ( + 'profiles' => $this->profiles, + 'styles' => $styles, + 'execution_time' => microtime(TRUE) - $start + ); + $view = new View('kohana_profiler', $data); + + // Return rendered view if $return is TRUE + if ($return == TRUE) + return $view->render(); + + // Add profiler data to the output + if (stripos(Kohana::$output, '</body>') !== FALSE) + { + // Closing body tag was found, insert the profiler data before it + Kohana::$output = str_ireplace('</body>', $view->render().'</body>', Kohana::$output); + } + else + { + // Append the profiler data to the output + Kohana::$output .= $view->render(); + } + } + + /** + * Benchmark times and memory usage from the Benchmark library. + * + * @return void + */ + public function benchmarks() + { + if ( ! $table = $this->table('benchmarks')) + return; + + $table->add_column(); + $table->add_column('kp-column kp-data'); + $table->add_column('kp-column kp-data'); + $table->add_row(array('Benchmarks', 'Time', 'Memory'), 'kp-title', 'background-color: #FFE0E0'); + + $benchmarks = Benchmark::get(TRUE); + + // Moves the first benchmark (total execution time) to the end of the array + $benchmarks = array_slice($benchmarks, 1) + array_slice($benchmarks, 0, 1); + + text::alternate(); + foreach ($benchmarks as $name => $benchmark) + { + // Clean unique id from system benchmark names + $name = ucwords(str_replace(array('_', '-'), ' ', str_replace(SYSTEM_BENCHMARK.'_', '', $name))); + + $data = array($name, number_format($benchmark['time'], 3), number_format($benchmark['memory'] / 1024 / 1024, 2).'MB'); + $class = text::alternate('', 'kp-altrow'); + + if ($name == 'Total Execution') + $class = 'kp-totalrow'; + + $table->add_row($data, $class); + } + } + + /** + * Database query benchmarks. + * + * @return void + */ + public function database() + { + if ( ! $table = $this->table('database')) + return; + + $table->add_column(); + $table->add_column('kp-column kp-data'); + $table->add_column('kp-column kp-data'); + $table->add_row(array('Queries', 'Time', 'Rows'), 'kp-title', 'background-color: #E0FFE0'); + + $queries = Database::$benchmarks; + + text::alternate(); + $total_time = $total_rows = 0; + foreach ($queries as $query) + { + $data = array($query['query'], number_format($query['time'], 3), $query['rows']); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + $total_time += $query['time']; + $total_rows += $query['rows']; + } + + $data = array('Total: ' . count($queries), number_format($total_time, 3), $total_rows); + $table->add_row($data, 'kp-totalrow'); + } + + /** + * Session data. + * + * @return void + */ + public function session() + { + if (empty($_SESSION)) return; + + if ( ! $table = $this->table('session')) + return; + + $table->add_column('kp-name'); + $table->add_column(); + $table->add_row(array('Session', 'Value'), 'kp-title', 'background-color: #CCE8FB'); + + text::alternate(); + foreach($_SESSION as $name => $value) + { + if (is_object($value)) + { + $value = get_class($value).' [object]'; + } + + $data = array($name, $value); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + } + } + + /** + * POST data. + * + * @return void + */ + public function post() + { + if (empty($_POST)) return; + + if ( ! $table = $this->table('post')) + return; + + $table->add_column('kp-name'); + $table->add_column(); + $table->add_row(array('POST', 'Value'), 'kp-title', 'background-color: #E0E0FF'); + + text::alternate(); + foreach($_POST as $name => $value) + { + $data = array($name, $value); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + } + } + + /** + * Cookie data. + * + * @return void + */ + public function cookies() + { + if (empty($_COOKIE)) return; + + if ( ! $table = $this->table('cookies')) + return; + + $table->add_column('kp-name'); + $table->add_column(); + $table->add_row(array('Cookies', 'Value'), 'kp-title', 'background-color: #FFF4D7'); + + text::alternate(); + foreach($_COOKIE as $name => $value) + { + $data = array($name, $value); + $class = text::alternate('', 'kp-altrow'); + $table->add_row($data, $class); + } + } +}
\ No newline at end of file diff --git a/kohana/libraries/Profiler_Table.php b/kohana/libraries/Profiler_Table.php new file mode 100644 index 00000000..16d93cda --- /dev/null +++ b/kohana/libraries/Profiler_Table.php @@ -0,0 +1,69 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Provides a table layout for sections in the Profiler library. + * + * $Id$ + * + * @package Profiler + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Profiler_Table_Core { + + protected $columns = array(); + protected $rows = array(); + + /** + * Get styles for table. + * + * @return string + */ + public function styles() + { + static $styles_output; + + if ( ! $styles_output) + { + $styles_output = TRUE; + return file_get_contents(Kohana::find_file('views', 'kohana_profiler_table', FALSE, 'css')); + } + + return ''; + } + + /** + * Add column to table. + * + * @param string CSS class + * @param string CSS style + */ + public function add_column($class = '', $style = '') + { + $this->columns[] = array('class' => $class, 'style' => $style); + } + + /** + * Add row to table. + * + * @param array data to go in table cells + * @param string CSS class + * @param string CSS style + */ + public function add_row($data, $class = '', $style = '') + { + $this->rows[] = array('data' => $data, 'class' => $class, 'style' => $style); + } + + /** + * Render table. + * + * @return string + */ + public function render() + { + $data['rows'] = $this->rows; + $data['columns'] = $this->columns; + return View::factory('kohana_profiler_table', $data)->render(); + } +}
\ No newline at end of file diff --git a/kohana/libraries/Router.php b/kohana/libraries/Router.php new file mode 100644 index 00000000..09e45c03 --- /dev/null +++ b/kohana/libraries/Router.php @@ -0,0 +1,313 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Router + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Router_Core { + + protected static $routes; + + public static $current_uri = ''; + public static $query_string = ''; + public static $complete_uri = ''; + public static $routed_uri = ''; + public static $url_suffix = ''; + + public static $segments; + public static $rsegments; + + public static $controller; + public static $controller_path; + + public static $method = 'index'; + public static $arguments = array(); + + /** + * Router setup routine. Automatically called during Kohana setup process. + * + * @return void + */ + public static function setup() + { + if ( ! empty($_SERVER['QUERY_STRING'])) + { + // Set the query string to the current query string + self::$query_string = '?'.trim($_SERVER['QUERY_STRING'], '&/'); + } + + if (self::$routes === NULL) + { + // Load routes + self::$routes = Kohana::config('routes'); + } + + // Default route status + $default_route = FALSE; + + if (self::$current_uri === '') + { + // Make sure the default route is set + if ( ! isset(self::$routes['_default'])) + throw new Kohana_Exception('core.no_default_route'); + + // Use the default route when no segments exist + self::$current_uri = self::$routes['_default']; + + // Default route is in use + $default_route = TRUE; + } + + // Make sure the URL is not tainted with HTML characters + self::$current_uri = html::specialchars(self::$current_uri, FALSE); + + // Remove all dot-paths from the URI, they are not valid + self::$current_uri = preg_replace('#\.[\s./]*/#', '', self::$current_uri); + + // At this point segments, rsegments, and current URI are all the same + self::$segments = self::$rsegments = self::$current_uri = trim(self::$current_uri, '/'); + + // Set the complete URI + self::$complete_uri = self::$current_uri.self::$query_string; + + // Explode the segments by slashes + self::$segments = ($default_route === TRUE OR self::$segments === '') ? array() : explode('/', self::$segments); + + if ($default_route === FALSE AND count(self::$routes) > 1) + { + // Custom routing + self::$rsegments = self::routed_uri(self::$current_uri); + } + + // The routed URI is now complete + self::$routed_uri = self::$rsegments; + + // Routed segments will never be empty + self::$rsegments = explode('/', self::$rsegments); + + // Prepare to find the controller + $controller_path = ''; + $method_segment = NULL; + + // Paths to search + $paths = Kohana::include_paths(); + + foreach (self::$rsegments as $key => $segment) + { + // Add the segment to the search path + $controller_path .= $segment; + + $found = FALSE; + foreach ($paths as $dir) + { + // Search within controllers only + $dir .= 'controllers/'; + + if (is_dir($dir.$controller_path) OR is_file($dir.$controller_path.EXT)) + { + // Valid path + $found = TRUE; + + // The controller must be a file that exists with the search path + if ($c = str_replace('\\', '/', realpath($dir.$controller_path.EXT)) + AND is_file($c) AND strpos($c, $dir) === 0) + { + // Set controller name + self::$controller = $segment; + + // Change controller path + self::$controller_path = $c; + + // Set the method segment + $method_segment = $key + 1; + + // Stop searching + break; + } + } + } + + if ($found === FALSE) + { + // Maximum depth has been reached, stop searching + break; + } + + // Add another slash + $controller_path .= '/'; + } + + if ($method_segment !== NULL AND isset(self::$rsegments[$method_segment])) + { + // Set method + self::$method = self::$rsegments[$method_segment]; + + if (isset(self::$rsegments[$method_segment + 1])) + { + // Set arguments + self::$arguments = array_slice(self::$rsegments, $method_segment + 1); + } + } + + // Last chance to set routing before a 404 is triggered + Event::run('system.post_routing'); + + if (self::$controller === NULL) + { + // No controller was found, so no page can be rendered + Event::run('system.404'); + } + } + + /** + * Attempts to determine the current URI using CLI, GET, PATH_INFO, ORIG_PATH_INFO, or PHP_SELF. + * + * @return void + */ + public static function find_uri() + { + if (PHP_SAPI === 'cli') + { + // Command line requires a bit of hacking + if (isset($_SERVER['argv'][1])) + { + self::$current_uri = $_SERVER['argv'][1]; + + // Remove GET string from segments + if (($query = strpos(self::$current_uri, '?')) !== FALSE) + { + list (self::$current_uri, $query) = explode('?', self::$current_uri, 2); + + // Parse the query string into $_GET + parse_str($query, $_GET); + + // Convert $_GET to UTF-8 + $_GET = utf8::clean($_GET); + } + } + } + elseif (current($_GET) === '' AND substr($_SERVER['QUERY_STRING'], -1) !== '=') + { + // The URI is the array key, eg: ?this/is/the/uri + self::$current_uri = key($_GET); + + // Remove the URI from $_GET + unset($_GET[self::$current_uri]); + + // Remove the URI from $_SERVER['QUERY_STRING'] + $_SERVER['QUERY_STRING'] = ltrim(substr($_SERVER['QUERY_STRING'], strlen(self::$current_uri)), '/&'); + + // Fixes really strange handling of a suffix in a GET string + if ($suffix = Kohana::config('core.url_suffix') AND substr(self::$current_uri, -(strlen($suffix))) === '_'.substr($suffix, 1)) + { + self::$current_uri = substr(self::$current_uri, 0, -(strlen($suffix))); + } + } + elseif (isset($_SERVER['PATH_INFO']) AND $_SERVER['PATH_INFO']) + { + self::$current_uri = $_SERVER['PATH_INFO']; + } + elseif (isset($_SERVER['ORIG_PATH_INFO']) AND $_SERVER['ORIG_PATH_INFO']) + { + self::$current_uri = $_SERVER['ORIG_PATH_INFO']; + } + elseif (isset($_SERVER['PHP_SELF']) AND $_SERVER['PHP_SELF']) + { + self::$current_uri = $_SERVER['PHP_SELF']; + } + + // The front controller directory and filename + $fc = substr(realpath($_SERVER['SCRIPT_FILENAME']), strlen(DOCROOT)); + + if (($strpos_fc = strpos(self::$current_uri, $fc)) !== FALSE) + { + // Remove the front controller from the current uri + self::$current_uri = substr(self::$current_uri, $strpos_fc + strlen($fc)); + } + + // Remove slashes from the start and end of the URI + self::$current_uri = trim(self::$current_uri, '/'); + + if (self::$current_uri !== '') + { + if ($suffix = Kohana::config('core.url_suffix') AND strpos(self::$current_uri, $suffix) !== FALSE) + { + // Remove the URL suffix + self::$current_uri = preg_replace('#'.preg_quote($suffix).'$#u', '', self::$current_uri); + + // Set the URL suffix + self::$url_suffix = $suffix; + } + + // Reduce multiple slashes into single slashes + self::$current_uri = preg_replace('#//+#', '/', self::$current_uri); + } + } + + /** + * Generates routed URI from given URI. + * + * @param string URI to convert + * @return string Routed uri + */ + public static function routed_uri($uri) + { + if (self::$routes === NULL) + { + // Load routes + self::$routes = Kohana::config('routes'); + } + + // Prepare variables + $routed_uri = $uri = trim($uri, '/'); + + if (isset(self::$routes[$uri])) + { + // Literal match, no need for regex + $routed_uri = self::$routes[$uri]; + } + else + { + // Loop through the routes and see if anything matches + foreach (self::$routes as $key => $val) + { + if ($key === '_default') continue; + + // Trim slashes + $key = trim($key, '/'); + $val = trim($val, '/'); + + if (preg_match('#^'.$key.'$#u', $uri)) + { + if (strpos($val, '$') !== FALSE) + { + // Use regex routing + $routed_uri = preg_replace('#^'.$key.'$#u', $val, $uri); + } + else + { + // Standard routing + $routed_uri = $val; + } + + // A valid route has been found + break; + } + } + } + + if (isset(self::$routes[$routed_uri])) + { + // Check for double routing (without regex) + $routed_uri = self::$routes[$routed_uri]; + } + + return trim($routed_uri, '/'); + } + +} // End Router
\ No newline at end of file diff --git a/kohana/libraries/Session.php b/kohana/libraries/Session.php new file mode 100644 index 00000000..cb85e61e --- /dev/null +++ b/kohana/libraries/Session.php @@ -0,0 +1,458 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Session library. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Session_Core { + + // Session singleton + private static $instance; + + // Protected key names (cannot be set by the user) + protected static $protect = array('session_id', 'user_agent', 'last_activity', 'ip_address', 'total_hits', '_kf_flash_'); + + // Configuration and driver + protected static $config; + protected static $driver; + + // Flash variables + protected static $flash; + + // Input library + protected $input; + + /** + * Singleton instance of Session. + */ + public static function instance() + { + if (self::$instance == NULL) + { + // Create a new instance + new Session; + } + + return self::$instance; + } + + /** + * On first session instance creation, sets up the driver and creates session. + */ + public function __construct() + { + $this->input = Input::instance(); + + // This part only needs to be run once + if (self::$instance === NULL) + { + // Load config + self::$config = Kohana::config('session'); + + // Makes a mirrored array, eg: foo=foo + self::$protect = array_combine(self::$protect, self::$protect); + + // Configure garbage collection + ini_set('session.gc_probability', (int) self::$config['gc_probability']); + ini_set('session.gc_divisor', 100); + ini_set('session.gc_maxlifetime', (self::$config['expiration'] == 0) ? 86400 : self::$config['expiration']); + + // Create a new session + $this->create(); + + if (self::$config['regenerate'] > 0 AND ($_SESSION['total_hits'] % self::$config['regenerate']) === 0) + { + // Regenerate session id and update session cookie + $this->regenerate(); + } + else + { + // Always update session cookie to keep the session alive + cookie::set(self::$config['name'], $_SESSION['session_id'], self::$config['expiration']); + } + + // Close the session just before sending the headers, so that + // the session cookie(s) can be written. + Event::add('system.send_headers', array($this, 'write_close')); + + // Make sure that sessions are closed before exiting + register_shutdown_function(array($this, 'write_close')); + + // Singleton instance + self::$instance = $this; + } + + Kohana::log('debug', 'Session Library initialized'); + } + + /** + * Get the session id. + * + * @return string + */ + public function id() + { + return $_SESSION['session_id']; + } + + /** + * Create a new session. + * + * @param array variables to set after creation + * @return void + */ + public function create($vars = NULL) + { + // Destroy any current sessions + $this->destroy(); + + if (self::$config['driver'] !== 'native') + { + // Set driver name + $driver = 'Session_'.ucfirst(self::$config['driver']).'_Driver'; + + // Load the driver + if ( ! Kohana::auto_load($driver)) + throw new Kohana_Exception('core.driver_not_found', self::$config['driver'], get_class($this)); + + // Initialize the driver + self::$driver = new $driver(); + + // Validate the driver + if ( ! (self::$driver instanceof Session_Driver)) + throw new Kohana_Exception('core.driver_implements', self::$config['driver'], get_class($this), 'Session_Driver'); + + // Register non-native driver as the session handler + session_set_save_handler + ( + array(self::$driver, 'open'), + array(self::$driver, 'close'), + array(self::$driver, 'read'), + array(self::$driver, 'write'), + array(self::$driver, 'destroy'), + array(self::$driver, 'gc') + ); + } + + // Validate the session name + if ( ! preg_match('~^(?=.*[a-z])[a-z0-9_]++$~iD', self::$config['name'])) + throw new Kohana_Exception('session.invalid_session_name', self::$config['name']); + + // Name the session, this will also be the name of the cookie + session_name(self::$config['name']); + + // Set the session cookie parameters + session_set_cookie_params + ( + self::$config['expiration'], + Kohana::config('cookie.path'), + Kohana::config('cookie.domain'), + Kohana::config('cookie.secure'), + Kohana::config('cookie.httponly') + ); + + // Start the session! + session_start(); + + // Put session_id in the session variable + $_SESSION['session_id'] = session_id(); + + // Set defaults + if ( ! isset($_SESSION['_kf_flash_'])) + { + $_SESSION['total_hits'] = 0; + $_SESSION['_kf_flash_'] = array(); + + $_SESSION['user_agent'] = Kohana::$user_agent; + $_SESSION['ip_address'] = $this->input->ip_address(); + } + + // Set up flash variables + self::$flash =& $_SESSION['_kf_flash_']; + + // Increase total hits + $_SESSION['total_hits'] += 1; + + // Validate data only on hits after one + if ($_SESSION['total_hits'] > 1) + { + // Validate the session + foreach (self::$config['validate'] as $valid) + { + switch ($valid) + { + // Check user agent for consistency + case 'user_agent': + if ($_SESSION[$valid] !== Kohana::$user_agent) + return $this->create(); + break; + + // Check ip address for consistency + case 'ip_address': + if ($_SESSION[$valid] !== $this->input->$valid()) + return $this->create(); + break; + + // Check expiration time to prevent users from manually modifying it + case 'expiration': + if (time() - $_SESSION['last_activity'] > ini_get('session.gc_maxlifetime')) + return $this->create(); + break; + } + } + + // Expire flash keys + $this->expire_flash(); + } + + // Update last activity + $_SESSION['last_activity'] = time(); + + // Set the new data + self::set($vars); + } + + /** + * Regenerates the global session id. + * + * @return void + */ + public function regenerate() + { + if (self::$config['driver'] === 'native') + { + // Generate a new session id + // Note: also sets a new session cookie with the updated id + session_regenerate_id(TRUE); + + // Update session with new id + $_SESSION['session_id'] = session_id(); + } + else + { + // Pass the regenerating off to the driver in case it wants to do anything special + $_SESSION['session_id'] = self::$driver->regenerate(); + } + + // Get the session name + $name = session_name(); + + if (isset($_COOKIE[$name])) + { + // Change the cookie value to match the new session id to prevent "lag" + $_COOKIE[$name] = $_SESSION['session_id']; + } + } + + /** + * Destroys the current session. + * + * @return void + */ + public function destroy() + { + if (session_id() !== '') + { + // Get the session name + $name = session_name(); + + // Destroy the session + session_destroy(); + + // Re-initialize the array + $_SESSION = array(); + + // Delete the session cookie + cookie::delete($name); + } + } + + /** + * Runs the system.session_write event, then calls session_write_close. + * + * @return void + */ + public function write_close() + { + static $run; + + if ($run === NULL) + { + $run = TRUE; + + // Run the events that depend on the session being open + Event::run('system.session_write'); + + // Expire flash keys + $this->expire_flash(); + + // Close the session + session_write_close(); + } + } + + /** + * Set a session variable. + * + * @param string|array key, or array of values + * @param mixed value (if keys is not an array) + * @return void + */ + public function set($keys, $val = FALSE) + { + if (empty($keys)) + return FALSE; + + if ( ! is_array($keys)) + { + $keys = array($keys => $val); + } + + foreach ($keys as $key => $val) + { + if (isset(self::$protect[$key])) + continue; + + // Set the key + $_SESSION[$key] = $val; + } + } + + /** + * Set a flash variable. + * + * @param string|array key, or array of values + * @param mixed value (if keys is not an array) + * @return void + */ + public function set_flash($keys, $val = FALSE) + { + if (empty($keys)) + return FALSE; + + if ( ! is_array($keys)) + { + $keys = array($keys => $val); + } + + foreach ($keys as $key => $val) + { + if ($key == FALSE) + continue; + + self::$flash[$key] = 'new'; + self::set($key, $val); + } + } + + /** + * Freshen one, multiple or all flash variables. + * + * @param string variable key(s) + * @return void + */ + public function keep_flash($keys = NULL) + { + $keys = ($keys === NULL) ? array_keys(self::$flash) : func_get_args(); + + foreach ($keys as $key) + { + if (isset(self::$flash[$key])) + { + self::$flash[$key] = 'new'; + } + } + } + + /** + * Expires old flash data and removes it from the session. + * + * @return void + */ + public function expire_flash() + { + static $run; + + // Method can only be run once + if ($run === TRUE) + return; + + if ( ! empty(self::$flash)) + { + foreach (self::$flash as $key => $state) + { + if ($state === 'old') + { + // Flash has expired + unset(self::$flash[$key], $_SESSION[$key]); + } + else + { + // Flash will expire + self::$flash[$key] = 'old'; + } + } + } + + // Method has been run + $run = TRUE; + } + + /** + * Get a variable. Access to sub-arrays is supported with key.subkey. + * + * @param string variable key + * @param mixed default value returned if variable does not exist + * @return mixed Variable data if key specified, otherwise array containing all session data. + */ + public function get($key = FALSE, $default = FALSE) + { + if (empty($key)) + return $_SESSION; + + $result = isset($_SESSION[$key]) ? $_SESSION[$key] : Kohana::key_string($_SESSION, $key); + + return ($result === NULL) ? $default : $result; + } + + /** + * Get a variable, and delete it. + * + * @param string variable key + * @param mixed default value returned if variable does not exist + * @return mixed + */ + public function get_once($key, $default = FALSE) + { + $return = self::get($key, $default); + self::delete($key); + + return $return; + } + + /** + * Delete one or more variables. + * + * @param string variable key(s) + * @return void + */ + public function delete($keys) + { + $args = func_get_args(); + + foreach ($args as $key) + { + if (isset(self::$protect[$key])) + continue; + + // Unset the key + unset($_SESSION[$key]); + } + } + +} // End Session Class diff --git a/kohana/libraries/URI.php b/kohana/libraries/URI.php new file mode 100644 index 00000000..30753b4b --- /dev/null +++ b/kohana/libraries/URI.php @@ -0,0 +1,279 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * URI library. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class URI_Core extends Router { + + /** + * Returns a singleton instance of URI. + * + * @return object + */ + public static function instance() + { + static $instance; + + if ($instance == NULL) + { + // Initialize the URI instance + $instance = new URI; + } + + return $instance; + } + + /** + * Retrieve a specific URI segment. + * + * @param integer|string segment number or label + * @param mixed default value returned if segment does not exist + * @return string + */ + public function segment($index = 1, $default = FALSE) + { + if (is_string($index)) + { + if (($key = array_search($index, self::$segments)) === FALSE) + return $default; + + $index = $key + 2; + } + + $index = (int) $index - 1; + + return isset(self::$segments[$index]) ? self::$segments[$index] : $default; + } + + /** + * Retrieve a specific routed URI segment. + * + * @param integer|string rsegment number or label + * @param mixed default value returned if segment does not exist + * @return string + */ + public function rsegment($index = 1, $default = FALSE) + { + if (is_string($index)) + { + if (($key = array_search($index, self::$rsegments)) === FALSE) + return $default; + + $index = $key + 2; + } + + $index = (int) $index - 1; + + return isset(self::$rsegments[$index]) ? self::$rsegments[$index] : $default; + } + + /** + * Retrieve a specific URI argument. + * This is the part of the segments that does not indicate controller or method + * + * @param integer|string argument number or label + * @param mixed default value returned if segment does not exist + * @return string + */ + public function argument($index = 1, $default = FALSE) + { + if (is_string($index)) + { + if (($key = array_search($index, self::$arguments)) === FALSE) + return $default; + + $index = $key + 2; + } + + $index = (int) $index - 1; + + return isset(self::$arguments[$index]) ? self::$arguments[$index] : $default; + } + + /** + * Returns an array containing all the URI segments. + * + * @param integer segment offset + * @param boolean return an associative array + * @return array + */ + public function segment_array($offset = 0, $associative = FALSE) + { + return $this->build_array(self::$segments, $offset, $associative); + } + + /** + * Returns an array containing all the re-routed URI segments. + * + * @param integer rsegment offset + * @param boolean return an associative array + * @return array + */ + public function rsegment_array($offset = 0, $associative = FALSE) + { + return $this->build_array(self::$rsegments, $offset, $associative); + } + + /** + * Returns an array containing all the URI arguments. + * + * @param integer segment offset + * @param boolean return an associative array + * @return array + */ + public function argument_array($offset = 0, $associative = FALSE) + { + return $this->build_array(self::$arguments, $offset, $associative); + } + + /** + * Creates a simple or associative array from an array and an offset. + * Used as a helper for (r)segment_array and argument_array. + * + * @param array array to rebuild + * @param integer offset to start from + * @param boolean create an associative array + * @return array + */ + public function build_array($array, $offset = 0, $associative = FALSE) + { + // Prevent the keys from being improperly indexed + array_unshift($array, 0); + + // Slice the array, preserving the keys + $array = array_slice($array, $offset + 1, count($array) - 1, TRUE); + + if ($associative === FALSE) + return $array; + + $associative = array(); + $pairs = array_chunk($array, 2); + + foreach ($pairs as $pair) + { + // Add the key/value pair to the associative array + $associative[$pair[0]] = isset($pair[1]) ? $pair[1] : ''; + } + + return $associative; + } + + /** + * Returns the complete URI as a string. + * + * @return string + */ + public function string() + { + return self::$current_uri; + } + + /** + * Magic method for converting an object to a string. + * + * @return string + */ + public function __toString() + { + return self::$current_uri; + } + + /** + * Returns the total number of URI segments. + * + * @return integer + */ + public function total_segments() + { + return count(self::$segments); + } + + /** + * Returns the total number of re-routed URI segments. + * + * @return integer + */ + public function total_rsegments() + { + return count(self::$rsegments); + } + + /** + * Returns the total number of URI arguments. + * + * @return integer + */ + public function total_arguments() + { + return count(self::$arguments); + } + + /** + * Returns the last URI segment. + * + * @param mixed default value returned if segment does not exist + * @return string + */ + public function last_segment($default = FALSE) + { + if (($end = $this->total_segments()) < 1) + return $default; + + return self::$segments[$end - 1]; + } + + /** + * Returns the last re-routed URI segment. + * + * @param mixed default value returned if segment does not exist + * @return string + */ + public function last_rsegment($default = FALSE) + { + if (($end = $this->total_segments()) < 1) + return $default; + + return self::$rsegments[$end - 1]; + } + + /** + * Returns the path to the current controller (not including the actual + * controller), as a web path. + * + * @param boolean return a full url, or only the path specifically + * @return string + */ + public function controller_path($full = TRUE) + { + return ($full) ? url::site(self::$controller_path) : self::$controller_path; + } + + /** + * Returns the current controller, as a web path. + * + * @param boolean return a full url, or only the controller specifically + * @return string + */ + public function controller($full = TRUE) + { + return ($full) ? url::site(self::$controller_path.self::$controller) : self::$controller; + } + + /** + * Returns the current method, as a web path. + * + * @param boolean return a full url, or only the method specifically + * @return string + */ + public function method($full = TRUE) + { + return ($full) ? url::site(self::$controller_path.self::$controller.'/'.self::$method) : self::$method; + } + +} // End URI Class diff --git a/kohana/libraries/Validation.php b/kohana/libraries/Validation.php new file mode 100644 index 00000000..172ff66e --- /dev/null +++ b/kohana/libraries/Validation.php @@ -0,0 +1,773 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Validation library. + * + * $Id$ + * + * @package Validation + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class Validation_Core extends ArrayObject { + + // Unique "any field" key + protected $any_field; + + // Array fields + protected $array_fields = array(); + + // Filters + protected $pre_filters = array(); + protected $post_filters = array(); + + // Rules and callbacks + protected $rules = array(); + protected $callbacks = array(); + + // Rules that are allowed to run on empty fields + protected $empty_rules = array('required', 'matches'); + + // Errors + protected $errors = array(); + protected $messages = array(); + + // Checks if there is data to validate. + protected $submitted; + + /** + * Creates a new Validation instance. + * + * @param array array to use for validation + * @return object + */ + public static function factory($array = NULL) + { + return new Validation( ! is_array($array) ? $_POST : $array); + } + + /** + * Sets the unique "any field" key and creates an ArrayObject from the + * passed array. + * + * @param array array to validate + * @return void + */ + public function __construct(array $array) + { + // Set a dynamic, unique "any field" key + $this->any_field = uniqid(NULL, TRUE); + + // Test if there is any actual data + $this->submitted = (count($array) > 0); + + parent::__construct($array, ArrayObject::ARRAY_AS_PROPS | ArrayObject::STD_PROP_LIST); + } + + /** + * Test if the data has been submitted. + * + * @return boolean + */ + public function submitted($value = NULL) + { + if (is_bool($value)) + { + $this->submitted = $value; + } + + return $this->submitted; + } + + /** + * Returns the ArrayObject values. + * + * @return array + */ + public function as_array() + { + return $this->getArrayCopy(); + } + + /** + * Returns the ArrayObject values, removing all inputs without rules. + * To choose specific inputs, list the field name as arguments. + * + * @return array + */ + public function safe_array() + { + // All the fields that are being validated + $all_fields = array_unique(array_merge + ( + array_keys($this->pre_filters), + array_keys($this->rules), + array_keys($this->callbacks), + array_keys($this->post_filters) + )); + + // Load choices + $choices = func_get_args(); + $choices = empty($choices) ? NULL : array_combine($choices, $choices); + + $safe = array(); + foreach ($all_fields as $i => $field) + { + // Ignore "any field" key + if ($field === $this->any_field) continue; + + if (isset($this->array_fields[$field])) + { + // Use the key field + $field = $this->array_fields[$field]; + } + + if ($choices === NULL OR isset($choices[$field])) + { + // Make sure all fields are defined + $safe[$field] = isset($this[$field]) ? $this[$field] : NULL; + } + } + + return $safe; + } + + /** + * Add additional rules that will forced, even for empty fields. All arguments + * passed will be appended to the list. + * + * @chainable + * @param string rule name + * @return object + */ + public function allow_empty_rules($rules) + { + // Any number of args are supported + $rules = func_get_args(); + + // Merge the allowed rules + $this->empty_rules = array_merge($this->empty_rules, $rules); + + return $this; + } + + /** + * Add a pre-filter to one or more inputs. + * + * @chainable + * @param callback filter + * @param string fields to apply filter to, use TRUE for all fields + * @return object + */ + public function pre_filter($filter, $field = TRUE) + { + if ( ! is_callable($filter)) + throw new Kohana_Exception('validation.filter_not_callable'); + + $filter = (is_string($filter) AND strpos($filter, '::') !== FALSE) ? explode('::', $filter) : $filter; + + if ($field === TRUE) + { + // Handle "any field" filters + $fields = array($this->any_field); + } + else + { + // Add the filter to specific inputs + $fields = func_get_args(); + $fields = array_slice($fields, 1); + } + + foreach ($fields as $field) + { + if (strpos($field, '.') > 0) + { + // Field keys + $keys = explode('.', $field); + + // Add to array fields + $this->array_fields[$field] = $keys[0]; + } + + // Add the filter to specified field + $this->pre_filters[$field][] = $filter; + } + + return $this; + } + + /** + * Add a post-filter to one or more inputs. + * + * @chainable + * @param callback filter + * @param string fields to apply filter to, use TRUE for all fields + * @return object + */ + public function post_filter($filter, $field = TRUE) + { + if ( ! is_callable($filter, TRUE)) + throw new Kohana_Exception('validation.filter_not_callable'); + + $filter = (is_string($filter) AND strpos($filter, '::') !== FALSE) ? explode('::', $filter) : $filter; + + if ($field === TRUE) + { + // Handle "any field" filters + $fields = array($this->any_field); + } + else + { + // Add the filter to specific inputs + $fields = func_get_args(); + $fields = array_slice($fields, 1); + } + + foreach ($fields as $field) + { + if (strpos($field, '.') > 0) + { + // Field keys + $keys = explode('.', $field); + + // Add to array fields + $this->array_fields[$field] = $keys[0]; + } + + // Add the filter to specified field + $this->post_filters[$field][] = $filter; + } + + return $this; + } + + /** + * Add rules to a field. Rules are callbacks or validation methods. Rules can + * only return TRUE or FALSE. + * + * @chainable + * @param string field name + * @param callback rules (unlimited number) + * @return object + */ + public function add_rules($field, $rules) + { + // Handle "any field" filters + ($field === TRUE) and $field = $this->any_field; + + // Get the rules + $rules = func_get_args(); + $rules = array_slice($rules, 1); + + foreach ($rules as $rule) + { + // Rule arguments + $args = NULL; + + if (is_string($rule)) + { + if (preg_match('/^([^\[]++)\[(.+)\]$/', $rule, $matches)) + { + // Split the rule into the function and args + $rule = $matches[1]; + $args = preg_split('/(?<!\\\\),\s*/', $matches[2]); + + // Replace escaped comma with comma + $args = str_replace('\,', ',', $args); + } + + if (method_exists($this, $rule)) + { + // Make the rule a valid callback + $rule = array($this, $rule); + } + elseif (method_exists('valid', $rule)) + { + // Make the rule a callback for the valid:: helper + $rule = array('valid', $rule); + } + } + + if ( ! is_callable($rule, TRUE)) + throw new Kohana_Exception('validation.rule_not_callable'); + + $rule = (is_string($rule) AND strpos($rule, '::') !== FALSE) ? explode('::', $rule) : $rule; + + if (strpos($field, '.') > 0) + { + // Field keys + $keys = explode('.', $field); + + // Add to array fields + $this->array_fields[$field] = $keys[0]; + } + + // Add the rule to specified field + $this->rules[$field][] = array($rule, $args); + } + + return $this; + } + + /** + * Add callbacks to a field. Callbacks must accept the Validation object + * and the input name. Callback returns are not processed. + * + * @chainable + * @param string field name + * @param callbacks callbacks (unlimited number) + * @return object + */ + public function add_callbacks($field, $callbacks) + { + // Handle "any field" filters + ($field === TRUE) and $field = $this->any_field; + + if (func_get_args() > 2) + { + // Multiple callback + $callbacks = array_slice(func_get_args(), 1); + } + else + { + // Only one callback + $callbacks = array($callbacks); + } + + foreach ($callbacks as $callback) + { + if ( ! is_callable($callback, TRUE)) + throw new Kohana_Exception('validation.callback_not_callable'); + + $callback = (is_string($callback) AND strpos($callback, '::') !== FALSE) ? explode('::', $callback) : $callback; + + if (strpos($field, '.') > 0) + { + // Field keys + $keys = explode('.', $field); + + // Add to array fields + $this->array_fields[$field] = $keys[0]; + } + + // Add the callback to specified field + $this->callbacks[$field][] = $callback; + } + + return $this; + } + + /** + * Validate by processing pre-filters, rules, callbacks, and post-filters. + * All fields that have filters, rules, or callbacks will be initialized if + * they are undefined. Validation will only be run if there is data already + * in the array. + * + * @return bool + */ + public function validate() + { + // All the fields that are being validated + $all_fields = array_unique(array_merge + ( + array_keys($this->pre_filters), + array_keys($this->rules), + array_keys($this->callbacks), + array_keys($this->post_filters) + )); + + // Copy the array from the object, to optimize multiple sets + $object_array = $this->getArrayCopy(); + + foreach ($all_fields as $i => $field) + { + if ($field === $this->any_field) + { + // Remove "any field" from the list of fields + unset($all_fields[$i]); + continue; + } + + if (substr($field, -2) === '.*') + { + // Set the key to be an array + Kohana::key_string_set($object_array, substr($field, 0, -2), array()); + } + else + { + // Set the key to be NULL + Kohana::key_string_set($object_array, $field, NULL); + } + } + + // Swap the array back into the object + $this->exchangeArray($object_array); + + // Reset all fields to ALL defined fields + $all_fields = array_keys($this->getArrayCopy()); + + foreach ($this->pre_filters as $field => $calls) + { + foreach ($calls as $func) + { + if ($field === $this->any_field) + { + foreach ($all_fields as $f) + { + // Process each filter + $this[$f] = is_array($this[$f]) ? arr::map_recursive($func, $this[$f]) : call_user_func($func, $this[$f]); + } + } + else + { + // Process each filter + $this[$field] = is_array($this[$field]) ? arr::map_recursive($func, $this[$field]) : call_user_func($func, $this[$field]); + } + } + } + + if ($this->submitted === FALSE) + return FALSE; + + foreach ($this->rules as $field => $calls) + { + foreach ($calls as $call) + { + // Split the rule into function and args + list($func, $args) = $call; + + if ($field === $this->any_field) + { + foreach ($all_fields as $f) + { + if (isset($this->array_fields[$f])) + { + // Use the field key + $f_key = $this->array_fields[$f]; + + // Prevent other rules from running when this field already has errors + if ( ! empty($this->errors[$f_key])) break; + + // Don't process rules on empty fields + if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$f_key] == NULL) + continue; + + foreach ($this[$f_key] as $k => $v) + { + if ( ! call_user_func($func, $this[$f_key][$k], $args)) + { + // Run each rule + $this->errors[$f_key] = is_array($func) ? $func[1] : $func; + } + } + } + else + { + // Prevent other rules from running when this field already has errors + if ( ! empty($this->errors[$f])) break; + + // Don't process rules on empty fields + if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$f] == NULL) + continue; + + if ( ! call_user_func($func, $this[$f], $args)) + { + // Run each rule + $this->errors[$f] = is_array($func) ? $func[1] : $func; + } + } + } + } + else + { + if (isset($this->array_fields[$field])) + { + // Use the field key + $field_key = $this->array_fields[$field]; + + // Prevent other rules from running when this field already has errors + if ( ! empty($this->errors[$field_key])) break; + + // Don't process rules on empty fields + if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$field_key] == NULL) + continue; + + foreach ($this[$field_key] as $k => $val) + { + if ( ! call_user_func($func, $this[$field_key][$k], $args)) + { + // Run each rule + $this->errors[$field_key] = is_array($func) ? $func[1] : $func; + + // Stop after an error is found + break 2; + } + } + } + else + { + // Prevent other rules from running when this field already has errors + if ( ! empty($this->errors[$field])) break; + + // Don't process rules on empty fields + if ( ! in_array($func[1], $this->empty_rules, TRUE) AND $this[$field] == NULL) + continue; + + if ( ! call_user_func($func, $this[$field], $args)) + { + // Run each rule + $this->errors[$field] = is_array($func) ? $func[1] : $func; + + // Stop after an error is found + break; + } + } + } + } + } + + foreach ($this->callbacks as $field => $calls) + { + foreach ($calls as $func) + { + if ($field === $this->any_field) + { + foreach ($all_fields as $f) + { + // Execute the callback + call_user_func($func, $this, $f); + + // Stop after an error is found + if ( ! empty($errors[$f])) break 2; + } + } + else + { + // Execute the callback + call_user_func($func, $this, $field); + + // Stop after an error is found + if ( ! empty($errors[$field])) break; + } + } + } + + foreach ($this->post_filters as $field => $calls) + { + foreach ($calls as $func) + { + if ($field === $this->any_field) + { + foreach ($all_fields as $f) + { + if (isset($this->array_fields[$f])) + { + // Use the field key + $f = $this->array_fields[$f]; + } + + // Process each filter + $this[$f] = is_array($this[$f]) ? array_map($func, $this[$f]) : call_user_func($func, $this[$f]); + } + } + else + { + if (isset($this->array_fields[$field])) + { + // Use the field key + $field = $this->array_fields[$field]; + } + + // Process each filter + $this[$field] = is_array($this[$field]) ? array_map($func, $this[$field]) : call_user_func($func, $this[$field]); + } + } + } + + // Return TRUE if there are no errors + return (count($this->errors) === 0); + } + + /** + * Add an error to an input. + * + * @chainable + * @param string input name + * @param string unique error name + * @return object + */ + public function add_error($field, $name) + { + if (isset($this[$field])) + { + $this->errors[$field] = $name; + } + + return $this; + } + + /** + * Sets or returns the message for an input. + * + * @chainable + * @param string input key + * @param string message to set + * @return string|object + */ + public function message($input = NULL, $message = NULL) + { + if ($message === NULL) + { + if ($input === NULL) + { + $messages = array(); + $keys = array_keys($this->messages); + + foreach ($keys as $input) + { + $messages[] = $this->message($input); + } + + return implode("\n", $messages); + } + + // Return nothing if no message exists + if (empty($this->messages[$input])) + return ''; + + // Return the HTML message string + return $this->messages[$input]; + } + else + { + $this->messages[$input] = $message; + } + + return $this; + } + + /** + * Return the errors array. + * + * @param boolean load errors from a lang file + * @return array + */ + public function errors($file = NULL) + { + if ($file === NULL) + { + return $this->errors; + } + else + { + $errors = array(); + foreach ($this->errors as $input => $error) + { + // Key for this input error + $key = "$file.$input.$error"; + + if (($errors[$input] = Kohana::lang($key)) === $key) + { + // Get the default error message + $errors[$input] = Kohana::lang("$file.$input.default"); + } + } + + return $errors; + } + } + + /** + * Rule: required. Generates an error if the field has an empty value. + * + * @param mixed input value + * @return bool + */ + public function required($str) + { + return ! ($str === '' OR $str === NULL OR $str === FALSE OR (is_array($str) AND empty($str))); + } + + /** + * Rule: matches. Generates an error if the field does not match one or more + * other fields. + * + * @param mixed input value + * @param array input names to match against + * @return bool + */ + public function matches($str, array $inputs) + { + foreach ($inputs as $key) + { + if ($str !== (isset($this[$key]) ? $this[$key] : NULL)) + return FALSE; + } + + return TRUE; + } + + /** + * Rule: length. Generates an error if the field is too long or too short. + * + * @param mixed input value + * @param array minimum, maximum, or exact length to match + * @return bool + */ + public function length($str, array $length) + { + if ( ! is_string($str)) + return FALSE; + + $size = utf8::strlen($str); + $status = FALSE; + + if (count($length) > 1) + { + list ($min, $max) = $length; + + if ($size >= $min AND $size <= $max) + { + $status = TRUE; + } + } + else + { + $status = ($size === (int) $length[0]); + } + + return $status; + } + + /** + * Rule: depends_on. Generates an error if the field does not depend on one + * or more other fields. + * + * @param mixed field name + * @param array field names to check dependency + * @return bool + */ + public function depends_on($field, array $fields) + { + foreach ($fields as $depends_on) + { + if ( ! isset($this[$depends_on]) OR $this[$depends_on] == NULL) + return FALSE; + } + + return TRUE; + } + + /** + * Rule: chars. Generates an error if the field contains characters outside of the list. + * + * @param string field value + * @param array allowed characters + * @return bool + */ + public function chars($value, array $chars) + { + return ! preg_match('![^'.preg_quote(implode(',', $chars)).']!', $value); + } + +} // End Validation diff --git a/kohana/libraries/View.php b/kohana/libraries/View.php new file mode 100644 index 00000000..6e3bc4d9 --- /dev/null +++ b/kohana/libraries/View.php @@ -0,0 +1,259 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Loads and displays Kohana view files. Can also handle output of some binary + * files, such as image, Javascript, and CSS files. + * + * $Id$ + * + * @package Core + * @author Kohana Team + * @copyright (c) 2007-2008 Kohana Team + * @license http://kohanaphp.com/license.html + */ +class View_Core { + + // The view file name and type + protected $kohana_filename = FALSE; + protected $kohana_filetype = FALSE; + + // View variable storage + protected $kohana_local_data = array(); + protected static $kohana_global_data = array(); + + /** + * Creates a new View using the given parameters. + * + * @param string view name + * @param array pre-load data + * @param string type of file: html, css, js, etc. + * @return object + */ + public static function factory($name = NULL, $data = NULL, $type = NULL) + { + return new View($name, $data, $type); + } + + /** + * Attempts to load a view and pre-load view data. + * + * @throws Kohana_Exception if the requested view cannot be found + * @param string view name + * @param array pre-load data + * @param string type of file: html, css, js, etc. + * @return void + */ + public function __construct($name = NULL, $data = NULL, $type = NULL) + { + if (is_string($name) AND $name !== '') + { + // Set the filename + $this->set_filename($name, $type); + } + + if (is_array($data) AND ! empty($data)) + { + // Preload data using array_merge, to allow user extensions + $this->kohana_local_data = array_merge($this->kohana_local_data, $data); + } + } + + /** + * Sets the view filename. + * + * @chainable + * @param string view filename + * @param string view file type + * @return object + */ + public function set_filename($name, $type = NULL) + { + if ($type == NULL) + { + // Load the filename and set the content type + $this->kohana_filename = Kohana::find_file('views', $name, TRUE); + $this->kohana_filetype = EXT; + } + else + { + // Check if the filetype is allowed by the configuration + if ( ! in_array($type, Kohana::config('view.allowed_filetypes'))) + throw new Kohana_Exception('core.invalid_filetype', $type); + + // Load the filename and set the content type + $this->kohana_filename = Kohana::find_file('views', $name, TRUE, $type); + $this->kohana_filetype = Kohana::config('mimes.'.$type); + + if ($this->kohana_filetype == NULL) + { + // Use the specified type + $this->kohana_filetype = $type; + } + } + + return $this; + } + + /** + * Sets a view variable. + * + * @param string|array name of variable or an array of variables + * @param mixed value when using a named variable + * @return object + */ + public function set($name, $value = NULL) + { + if (is_array($name)) + { + foreach ($name as $key => $value) + { + $this->__set($key, $value); + } + } + else + { + $this->__set($name, $value); + } + + return $this; + } + + /** + * Sets a bound variable by reference. + * + * @param string name of variable + * @param mixed variable to assign by reference + * @return object + */ + public function bind($name, & $var) + { + $this->kohana_local_data[$name] =& $var; + + return $this; + } + + /** + * Sets a view global variable. + * + * @param string|array name of variable or an array of variables + * @param mixed value when using a named variable + * @return object + */ + public function set_global($name, $value = NULL) + { + if (is_array($name)) + { + foreach ($name as $key => $value) + { + self::$kohana_global_data[$key] = $value; + } + } + else + { + self::$kohana_global_data[$name] = $value; + } + + return $this; + } + + /** + * Magically sets a view variable. + * + * @param string variable key + * @param string variable value + * @return void + */ + public function __set($key, $value) + { + if ( ! isset($this->$key)) + { + $this->kohana_local_data[$key] = $value; + } + } + + /** + * Magically gets a view variable. + * + * @param string variable key + * @return mixed variable value if the key is found + * @return void if the key is not found + */ + public function __get($key) + { + if (isset($this->kohana_local_data[$key])) + return $this->kohana_local_data[$key]; + + if (isset(self::$kohana_global_data[$key])) + return self::$kohana_global_data[$key]; + + if (isset($this->$key)) + return $this->$key; + } + + /** + * Magically converts view object to string. + * + * @return string + */ + public function __toString() + { + return $this->render(); + } + + /** + * Renders a view. + * + * @param boolean set to TRUE to echo the output instead of returning it + * @param callback special renderer to pass the output through + * @return string if print is FALSE + * @return void if print is TRUE + */ + public function render($print = FALSE, $renderer = FALSE) + { + if (empty($this->kohana_filename)) + throw new Kohana_Exception('core.view_set_filename'); + + if (is_string($this->kohana_filetype)) + { + // Merge global and local data, local overrides global with the same name + $data = array_merge(self::$kohana_global_data, $this->kohana_local_data); + + // Load the view in the controller for access to $this + $output = Kohana::$instance->_kohana_load_view($this->kohana_filename, $data); + + if ($renderer !== FALSE AND is_callable($renderer, TRUE)) + { + // Pass the output through the user defined renderer + $output = call_user_func($renderer, $output); + } + + if ($print === TRUE) + { + // Display the output + echo $output; + return; + } + } + else + { + // Set the content type and size + header('Content-Type: '.$this->kohana_filetype[0]); + + if ($print === TRUE) + { + if ($file = fopen($this->kohana_filename, 'rb')) + { + // Display the output + fpassthru($file); + fclose($file); + } + return; + } + + // Fetch the file contents + $output = file_get_contents($this->kohana_filename); + } + + return $output; + } + +} // End View
\ No newline at end of file diff --git a/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 diff --git a/kohana/views/kohana/template.php b/kohana/views/kohana/template.php new file mode 100644 index 00000000..97c458a3 --- /dev/null +++ b/kohana/views/kohana/template.php @@ -0,0 +1,35 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <title><?php echo html::specialchars($title) ?></title> + + <style type="text/css"> + html { background: #83c018 url(<?php echo url::base(FALSE) ?>kohana.png) 50% 0 no-repeat; } + body { width: 52em; margin: 200px auto 2em; font-size: 76%; font-family: Arial, sans-serif; color: #273907; line-height: 1.5; text-align: center; } + h1 { font-size: 3em; font-weight: normal; text-transform: uppercase; color: #fff; } + a { color: inherit; } + code { font-size: 1.3em; } + ul { list-style: none; padding: 2em 0; } + ul li { display: inline; padding-right: 1em; text-transform: uppercase; } + ul li a { padding: 0.5em 1em; background: #69ad0f; border: 1px solid #569f09; color: #fff; text-decoration: none; } + ul li a:hover { background: #569f09; } + .box { padding: 2em; background: #98cc2b; border: 1px solid #569f09; } + .copyright { font-size: 0.9em; text-transform: uppercase; color: #557d10; } + </style> + +</head> +<body> + + <h1><?php echo html::specialchars($title) ?></h1> + <?php echo $content ?> + + <p class="copyright"> + Rendered in {execution_time} seconds, using {memory_usage} of memory<br /> + Copyright ©2007–2008 Kohana Team + </p> + +</body> +</html>
\ No newline at end of file diff --git a/kohana/views/kohana_calendar.php b/kohana/views/kohana_calendar.php new file mode 100644 index 00000000..581c7da7 --- /dev/null +++ b/kohana/views/kohana_calendar.php @@ -0,0 +1,52 @@ +<?php + +// Get the day names +$days = Calendar::days(TRUE); + +// Previous and next month timestamps +$next = mktime(0, 0, 0, $month + 1, 1, $year); +$prev = mktime(0, 0, 0, $month - 1, 1, $year); + +// Import the GET query array locally and remove the day +$qs = $_GET; +unset($qs['day']); + +// Previous and next month query URIs +$prev = Router::$current_uri.'?'.http_build_query(array_merge($qs, array('month' => date('n', $prev), 'year' => date('Y', $prev)))); +$next = Router::$current_uri.'?'.http_build_query(array_merge($qs, array('month' => date('n', $next), 'year' => date('Y', $next)))); + +?> +<table class="calendar"> +<tr class="controls"> +<td class="prev"><?php echo html::anchor($prev, '«') ?></td> +<td class="title" colspan="5"><?php echo strftime('%B %Y', mktime(0, 0, 0, $month, 1, $year)) ?></td> +<td class="next"><?php echo html::anchor($next, '»') ?></td> +</tr> +<tr> +<?php foreach ($days as $day): ?> +<th><?php echo $day ?></th> +<?php endforeach ?> +</tr> +<?php foreach ($weeks as $week): ?> +<tr> +<?php foreach ($week as $day): + +list ($number, $current, $data) = $day; + +if (is_array($data)) +{ + $classes = $data['classes']; + $output = empty($data['output']) ? '' : '<ul class="output"><li>'.implode('</li><li>', $data['output']).'</li></ul>'; +} +else +{ + $classes = array(); + $output = ''; +} + +?> +<td class="<?php echo implode(' ', $classes) ?>"><span class="day"><?php echo $day[0] ?></span><?php echo $output ?></td> +<?php endforeach ?> +</tr> +<?php endforeach ?> +</table> diff --git a/kohana/views/kohana_error_disabled.php b/kohana/views/kohana_error_disabled.php new file mode 100644 index 00000000..e32e7396 --- /dev/null +++ b/kohana/views/kohana_error_disabled.php @@ -0,0 +1,16 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> +<title><?php echo $error ?></title> +</head> +<body> +<style type="text/css"> +<?php include Kohana::find_file('views', 'kohana_errors', FALSE, 'css') ?> +</style> +<div id="framework_error" style="width:24em;margin:50px auto;"> +<h3><?php echo html::specialchars($error) ?></h3> +<p style="text-align:center"><?php echo $message ?></p> +</div> +</body> +</html>
\ No newline at end of file diff --git a/kohana/views/kohana_error_page.php b/kohana/views/kohana_error_page.php new file mode 100644 index 00000000..9e4bba88 --- /dev/null +++ b/kohana/views/kohana_error_page.php @@ -0,0 +1,26 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> +<title><?php echo $error ?></title> +<base href="http://php.net/" /> +</head> +<body> +<style type="text/css"> +<?php include Kohana::find_file('views', 'kohana_errors', FALSE, 'css') ?> +</style> +<div id="framework_error" style="width:42em;margin:20px auto;"> +<h3><?php echo html::specialchars($error) ?></h3> +<p><?php echo html::specialchars($description) ?></p> +<?php if ( ! empty($line) AND ! empty($file)): ?> +<p><?php echo Kohana::lang('core.error_file_line', $file, $line) ?></p> +<?php endif ?> +<p><code class="block"><?php echo $message ?></code></p> +<?php if ( ! empty($trace)): ?> +<h3><?php echo Kohana::lang('core.stack_trace') ?></h3> +<?php echo $trace ?> +<?php endif ?> +<p class="stats"><?php echo Kohana::lang('core.stats_footer') ?></p> +</div> +</body> +</html>
\ No newline at end of file diff --git a/kohana/views/kohana_errors.css b/kohana/views/kohana_errors.css new file mode 100644 index 00000000..1341f57d --- /dev/null +++ b/kohana/views/kohana_errors.css @@ -0,0 +1,21 @@ +div#framework_error { background:#fff; border:solid 1px #ccc; font-family:sans-serif; color:#111; font-size:14px; line-height:130%; } +div#framework_error h3 { color:#fff; font-size:16px; padding:8px 6px; margin:0 0 8px; background:#f15a00; text-align:center; } +div#framework_error a { color:#228; text-decoration:none; } +div#framework_error a:hover { text-decoration:underline; } +div#framework_error strong { color:#900; } +div#framework_error p { margin:0; padding:4px 6px 10px; } +div#framework_error tt, +div#framework_error pre, +div#framework_error code { font-family:monospace; padding:2px 4px; font-size:12px; color:#333; + white-space:pre-wrap; /* CSS 2.1 */ + white-space:-moz-pre-wrap; /* For Mozilla */ + word-wrap:break-word; /* For IE5.5+ */ +} +div#framework_error tt { font-style:italic; } +div#framework_error tt:before { content:">"; color:#aaa; } +div#framework_error code tt:before { content:""; } +div#framework_error pre, +div#framework_error code { background:#eaeee5; border:solid 0 #D6D8D1; border-width:0 1px 1px 0; } +div#framework_error .block { display:block; text-align:left; } +div#framework_error .stats { padding:4px; background: #eee; border-top:solid 1px #ccc; text-align:center; font-size:10px; color:#888; } +div#framework_error .backtrace { margin:0; padding:0 6px; list-style:none; line-height:12px; }
\ No newline at end of file diff --git a/kohana/views/kohana_profiler.php b/kohana/views/kohana_profiler.php new file mode 100644 index 00000000..a16c018b --- /dev/null +++ b/kohana/views/kohana_profiler.php @@ -0,0 +1,36 @@ +<style type="text/css"> +#kohana-profiler +{ + font-family: Monaco, 'Courier New'; + background-color: #F8FFF8; + margin-top: 20px; + clear: both; + padding: 10px 10px 0; + border: 1px solid #E5EFF8; + text-align: left; +} +#kohana-profiler pre +{ + margin: 0; + font: inherit; +} +#kohana-profiler .kp-meta +{ + margin: 0 0 10px; + padding: 4px; + background: #FFF; + border: 1px solid #E5EFF8; + color: #A6B0B8; + text-align: center; +} +<?php echo $styles ?> +</style> +<div id="kohana-profiler"> +<?php +foreach ($profiles as $profile) +{ + echo $profile->render(); +} +?> +<p class="kp-meta">Profiler executed in <?php echo number_format($execution_time, 3) ?>s</p> +</div>
\ No newline at end of file diff --git a/kohana/views/kohana_profiler_table.css b/kohana/views/kohana_profiler_table.css new file mode 100644 index 00000000..6e7601c9 --- /dev/null +++ b/kohana/views/kohana_profiler_table.css @@ -0,0 +1,53 @@ +#kohana-profiler .kp-table
+{
+ font-size: 1.0em;
+ color: #4D6171;
+ width: 100%;
+ border-collapse: collapse;
+ border-top: 1px solid #E5EFF8;
+ border-right: 1px solid #E5EFF8;
+ border-left: 1px solid #E5EFF8;
+ margin-bottom: 10px;
+}
+#kohana-profiler .kp-table td
+{
+ background-color: #FFFFFF;
+ border-bottom: 1px solid #E5EFF8;
+ padding: 3px;
+ vertical-align: top;
+}
+#kohana-profiler .kp-table .kp-title td
+{
+ font-weight: bold;
+ background-color: inherit;
+}
+#kohana-profiler .kp-table .kp-altrow td
+{
+ background-color: #F7FBFF;
+}
+#kohana-profiler .kp-table .kp-totalrow td
+{
+ background-color: #FAFAFA;
+ border-top: 1px solid #D2DCE5;
+ font-weight: bold;
+}
+#kohana-profiler .kp-table .kp-column
+{
+ width: 100px;
+ border-left: 1px solid #E5EFF8;
+ text-align: center;
+}
+#kohana-profiler .kp-table .kp-data, #kohana-profiler .kp-table .kp-name
+{
+ background-color: #FAFAFB;
+ vertical-align: top;
+}
+#kohana-profiler .kp-table .kp-name
+{
+ width: 200px;
+ border-right: 1px solid #E5EFF8;
+}
+#kohana-profiler .kp-table .kp-altrow .kp-data, #kohana-profiler .kp-table .kp-altrow .kp-name
+{
+ background-color: #F6F8FB;
+}
\ No newline at end of file diff --git a/kohana/views/kohana_profiler_table.php b/kohana/views/kohana_profiler_table.php new file mode 100644 index 00000000..aed6d094 --- /dev/null +++ b/kohana/views/kohana_profiler_table.php @@ -0,0 +1,24 @@ +<table class="kp-table"> +<?php +foreach ($rows as $row): + +$class = empty($row['class']) ? '' : ' class="'.$row['class'].'"'; +$style = empty($row['style']) ? '' : ' style="'.$row['style'].'"'; +?> + <tr<?php echo $class; echo $style; ?>> + <?php + foreach ($columns as $index => $column) + { + $class = empty($column['class']) ? '' : ' class="'.$column['class'].'"'; + $style = empty($column['style']) ? '' : ' style="'.$column['style'].'"'; + $value = $row['data'][$index]; + $value = (is_array($value) OR is_object($value)) ? '<pre>'.html::specialchars(print_r($value, TRUE)).'</pre>' : html::specialchars($value); + echo '<td', $style, $class, '>', $value, '</td>'; + } + ?> + </tr> +<?php + +endforeach; +?> +</table>
\ No newline at end of file diff --git a/kohana/views/pagination/classic.php b/kohana/views/pagination/classic.php new file mode 100644 index 00000000..79299211 --- /dev/null +++ b/kohana/views/pagination/classic.php @@ -0,0 +1,39 @@ +<?php +/** + * Classic pagination style + * + * @preview ‹ First < 1 2 3 > Last › + */ +?> + +<p class="pagination"> + + <?php if ($first_page): ?> + <a href="<?php echo str_replace('{page}', 1, $url) ?>">‹ <?php echo Kohana::lang('pagination.first') ?></a> + <?php endif ?> + + <?php if ($previous_page): ?> + <a href="<?php echo str_replace('{page}', $previous_page, $url) ?>"><</a> + <?php endif ?> + + + <?php for ($i = 1; $i <= $total_pages; $i++): ?> + + <?php if ($i == $current_page): ?> + <strong><?php echo $i ?></strong> + <?php else: ?> + <a href="<?php echo str_replace('{page}', $i, $url) ?>"><?php echo $i ?></a> + <?php endif ?> + + <?php endfor ?> + + + <?php if ($next_page): ?> + <a href="<?php echo str_replace('{page}', $next_page, $url) ?>">></a> + <?php endif ?> + + <?php if ($last_page): ?> + <a href="<?php echo str_replace('{page}', $last_page, $url) ?>"><?php echo Kohana::lang('pagination.last') ?> ›</a> + <?php endif ?> + +</p>
\ No newline at end of file diff --git a/kohana/views/pagination/digg.php b/kohana/views/pagination/digg.php new file mode 100644 index 00000000..888da48f --- /dev/null +++ b/kohana/views/pagination/digg.php @@ -0,0 +1,83 @@ +<?php +/** + * Digg pagination style + * + * @preview « Previous 1 2 … 5 6 7 8 9 10 11 12 13 14 … 25 26 Next » + */ +?> + +<p class="pagination"> + + <?php if ($previous_page): ?> + <a href="<?php echo str_replace('{page}', $previous_page, $url) ?>">« <?php echo Kohana::lang('pagination.previous') ?></a> + <?php else: ?> + « <?php echo Kohana::lang('pagination.previous') ?> + <?php endif ?> + + + <?php if ($total_pages < 13): /* « Previous 1 2 3 4 5 6 7 8 9 10 11 12 Next » */ ?> + + <?php for ($i = 1; $i <= $total_pages; $i++): ?> + <?php if ($i == $current_page): ?> + <strong><?php echo $i ?></strong> + <?php else: ?> + <a href="<?php echo str_replace('{page}', $i, $url) ?>"><?php echo $i ?></a> + <?php endif ?> + <?php endfor ?> + + <?php elseif ($current_page < 9): /* « Previous 1 2 3 4 5 6 7 8 9 10 … 25 26 Next » */ ?> + + <?php for ($i = 1; $i <= 10; $i++): ?> + <?php if ($i == $current_page): ?> + <strong><?php echo $i ?></strong> + <?php else: ?> + <a href="<?php echo str_replace('{page}', $i, $url) ?>"><?php echo $i ?></a> + <?php endif ?> + <?php endfor ?> + + … + <a href="<?php echo str_replace('{page}', $total_pages - 1, $url) ?>"><?php echo $total_pages - 1 ?></a> + <a href="<?php echo str_replace('{page}', $total_pages, $url) ?>"><?php echo $total_pages ?></a> + + <?php elseif ($current_page > $total_pages - 8): /* « Previous 1 2 … 17 18 19 20 21 22 23 24 25 26 Next » */ ?> + + <a href="<?php echo str_replace('{page}', 1, $url) ?>">1</a> + <a href="<?php echo str_replace('{page}', 2, $url) ?>">2</a> + … + + <?php for ($i = $total_pages - 9; $i <= $total_pages; $i++): ?> + <?php if ($i == $current_page): ?> + <strong><?php echo $i ?></strong> + <?php else: ?> + <a href="<?php echo str_replace('{page}', $i, $url) ?>"><?php echo $i ?></a> + <?php endif ?> + <?php endfor ?> + + <?php else: /* « Previous 1 2 … 5 6 7 8 9 10 11 12 13 14 … 25 26 Next » */ ?> + + <a href="<?php echo str_replace('{page}', 1, $url) ?>">1</a> + <a href="<?php echo str_replace('{page}', 2, $url) ?>">2</a> + … + + <?php for ($i = $current_page - 5; $i <= $current_page + 5; $i++): ?> + <?php if ($i == $current_page): ?> + <strong><?php echo $i ?></strong> + <?php else: ?> + <a href="<?php echo str_replace('{page}', $i, $url) ?>"><?php echo $i ?></a> + <?php endif ?> + <?php endfor ?> + + … + <a href="<?php echo str_replace('{page}', $total_pages - 1, $url) ?>"><?php echo $total_pages - 1 ?></a> + <a href="<?php echo str_replace('{page}', $total_pages, $url) ?>"><?php echo $total_pages ?></a> + + <?php endif ?> + + + <?php if ($next_page): ?> + <a href="<?php echo str_replace('{page}', $next_page, $url) ?>"><?php echo Kohana::lang('pagination.next') ?> »</a> + <?php else: ?> + <?php echo Kohana::lang('pagination.next') ?> » + <?php endif ?> + +</p>
\ No newline at end of file diff --git a/kohana/views/pagination/extended.php b/kohana/views/pagination/extended.php new file mode 100644 index 00000000..7e4fa389 --- /dev/null +++ b/kohana/views/pagination/extended.php @@ -0,0 +1,27 @@ +<?php +/** + * Extended pagination style + * + * @preview « Previous | Page 2 of 11 | Showing items 6-10 of 52 | Next » + */ +?> + +<p class="pagination"> + + <?php if ($previous_page): ?> + <a href="<?php echo str_replace('{page}', $previous_page, $url) ?>">« <?php echo Kohana::lang('pagination.previous') ?></a> + <?php else: ?> + « <?php echo Kohana::lang('pagination.previous') ?> + <?php endif ?> + + | <?php echo Kohana::lang('pagination.page') ?> <?php echo $current_page ?> <?php echo Kohana::lang('pagination.of') ?> <?php echo $total_pages ?> + + | <?php echo Kohana::lang('pagination.items') ?> <?php echo $current_first_item ?>–<?php echo $current_last_item ?> <?php echo Kohana::lang('pagination.of') ?> <?php echo $total_items ?> + + | <?php if ($next_page): ?> + <a href="<?php echo str_replace('{page}', $next_page, $url) ?>"><?php echo Kohana::lang('pagination.next') ?> »</a> + <?php else: ?> + <?php echo Kohana::lang('pagination.next') ?> » + <?php endif ?> + +</p>
\ No newline at end of file diff --git a/kohana/views/pagination/punbb.php b/kohana/views/pagination/punbb.php new file mode 100644 index 00000000..3bb62676 --- /dev/null +++ b/kohana/views/pagination/punbb.php @@ -0,0 +1,37 @@ +<?php +/** + * PunBB pagination style + * + * @preview Pages: 1 … 4 5 6 7 8 … 15 + */ +?> + +<p class="pagination"> + + <?php echo Kohana::lang('pagination.pages') ?>: + + <?php if ($current_page > 3): ?> + <a href="<?php echo str_replace('{page}', 1, $url) ?>">1</a> + <?php if ($current_page != 4) echo '…' ?> + <?php endif ?> + + + <?php for ($i = $current_page - 2, $stop = $current_page + 3; $i < $stop; ++$i): ?> + + <?php if ($i < 1 OR $i > $total_pages) continue ?> + + <?php if ($current_page == $i): ?> + <strong><?php echo $i ?></strong> + <?php else: ?> + <a href="<?php echo str_replace('{page}', $i, $url) ?>"><?php echo $i ?></a> + <?php endif ?> + + <?php endfor ?> + + + <?php if ($current_page <= $total_pages - 3): ?> + <?php if ($current_page != $total_pages - 3) echo '…' ?> + <a href="<?php echo str_replace('{page}', $total_pages, $url) ?>"><?php echo $total_pages ?></a> + <?php endif ?> + +</p>
\ No newline at end of file |