diff --git a/application/bootstrap.php b/application/bootstrap.php index 101af67..86e6486 100644 --- a/application/bootstrap.php +++ b/application/bootstrap.php @@ -3,48 +3,56 @@ // -- Environment setup -------------------------------------------------------- // Load the core Kohana class -require SYSPATH.'classes/kohana/core'.EXT; +require SYSPATH.'classes/Kohana/Core'.EXT; -if (is_file(APPPATH.'classes/kohana'.EXT)) +if (is_file(APPPATH.'classes/Kohana'.EXT)) { // Application extends the core - require APPPATH.'classes/kohana'.EXT; + require APPPATH.'classes/Kohana'.EXT; } else { // Load empty core extension - require SYSPATH.'classes/kohana'.EXT; + require SYSPATH.'classes/Kohana'.EXT; } /** * Set the default time zone. * - * @see http://kohanaframework.org/guide/using.configuration - * @see http://php.net/timezones + * @link http://kohanaframework.org/guide/using.configuration + * @link http://www.php.net/manual/timezones */ date_default_timezone_set('Australia/Melbourne'); /** * Set the default locale. * - * @see http://kohanaframework.org/guide/using.configuration - * @see http://php.net/setlocale + * @link http://kohanaframework.org/guide/using.configuration + * @link http://www.php.net/manual/function.setlocale */ setlocale(LC_ALL, 'en_US.utf-8'); /** * Enable the Kohana auto-loader. * - * @see http://kohanaframework.org/guide/using.autoloading - * @see http://php.net/spl_autoload_register + * @link http://kohanaframework.org/guide/using.autoloading + * @link http://www.php.net/manual/function.spl-autoload-register */ spl_autoload_register(array('Kohana', 'auto_load')); +/** + * Optionally, you can enable a compatibility auto-loader for use with + * older modules that have not been updated for PSR-0. + * + * It is recommended to not enable this unless absolutely necessary. + */ +//spl_autoload_register(array('Kohana', 'auto_load_lowercase')); + /** * Enable the Kohana auto-loader for unserialization. * - * @see http://php.net/spl_autoload_call - * @see http://php.net/manual/var.configuration.php#unserialize-callback-func + * @link http://www.php.net/manual/function.spl-autoload-call + * @link http://www.php.net/manual/var.configuration#unserialize-callback-func */ ini_set('unserialize_callback_func', 'spl_autoload_call'); @@ -75,15 +83,16 @@ if (isset($_SERVER['KOHANA_ENV'])) * - string index_file name of your index file, usually "index.php" index.php * - string charset internal character set used for input and output utf-8 * - string cache_dir set the internal cache directory APPPATH/cache + * - integer cache_life lifetime, in seconds, of items cached 60 * - boolean errors enable or disable error handling TRUE * - boolean profile enable or disable internal profiling TRUE * - boolean caching enable or disable internal caching FALSE + * - boolean expose set the X-Powered-By header FALSE */ Kohana::init(array( - 'base_url' => '/pta', - 'index_file' => '', + 'base_url' => '/pta/', 'caching' => TRUE, - 'cache_dir' => '/dev/shm/lnapp', + 'index_file' => '', )); /** @@ -102,10 +111,15 @@ Kohana::$config->attach(new Config_File); Kohana::modules(array( 'auth' => SMDPATH.'auth', // Basic authentication 'cache' => SMDPATH.'cache', // Caching with multiple backends + 'cron' => SMDPATH.'cron', // Kohana Cron Module // 'codebench' => SMDPATH.'codebench', // Benchmarking tool 'database' => SMDPATH.'database', // Database access + 'gchart' => MODPATH.'gchart', // Google Chart Module // 'image' => SMDPATH.'image', // Image manipulation + 'khemail' => SMDPATH.'khemail', // Email module for Kohana 3 PHP Framework + 'minion' => SMDPATH.'minion', // CLI Tasks 'orm' => SMDPATH.'orm', // Object Relationship Mapping + 'pagination' => SMDPATH.'pagination', // Kohana Pagination module for Kohana 3 PHP Framework // 'unittest' => SMDPATH.'unittest', // Unit testing 'userguide' => SMDPATH.'userguide', // User guide and API documentation )); @@ -115,24 +129,33 @@ Kohana::modules(array( */ Route::set('sections', '/(/(/(/)))', array( - 'directory' => '('.implode('|',Kohana::config('config.method_directory')).')' + 'directory' => '('.implode('|',Kohana::$config->load('config')->method_directory).')' + )) + ->defaults(array( + 'action' => 'index', )); // Static file serving (CSS, JS, images) Route::set('default/media', 'media(/)', array('file' => '.+')) ->defaults(array( - 'controller' => 'welcome', - 'action' => 'media', - 'file' => NULL, + 'controller' => 'media', + 'action' => 'get', )); /** * Set the routes. Each route must have a minimum of a name, a URI and a set of * defaults for the URI. */ -Route::set('default', '((/(/)))', array('id' => '[a-zA-Z0-9_.-]+')) +Route::set('default', '((/(/)))', array('id'=>'[a-zA-Z0-9_.-]+')) ->defaults(array( 'controller' => 'welcome', 'action' => 'index', )); -?> + +/** + * If APC is enabled, and we need to clear the cache + */ +if (file_exists(APPPATH.'cache/CLEAR_APC_CACHE') AND function_exists('apc_clear_cache') AND ! Kohana::$is_cli) { + if (! apc_clear_cache() OR ! unlink(APPPATH.'cache/CLEAR_APC_CACHE')) + throw new Kohana_Exception('Unable to clear the APC cache.'); +} diff --git a/includes/kohana/.travis.yml b/includes/kohana/.travis.yml new file mode 100644 index 0000000..ba7adb4 --- /dev/null +++ b/includes/kohana/.travis.yml @@ -0,0 +1,24 @@ +language: php + +php: + - 5.3 + +before_install: + - "git submodule update --init --recursive" + +before_script: + - "pear channel-discover pear.phing.info" + - "pear install phing/phing" + - "phpenv rehash" + - "composer install" + +script: "phing test" + +notifications: + irc: + channels: + - "irc.freenode.org#kohana" + template: + - "%{repository}/%{branch} (%{commit}) - %{author}: %{message}" + - "Build details: %{build_url}" + email: false diff --git a/includes/kohana/LICENSE.md b/includes/kohana/LICENSE.md index 87af9ad..a36fb76 100644 --- a/includes/kohana/LICENSE.md +++ b/includes/kohana/LICENSE.md @@ -2,7 +2,7 @@ This license is a legal agreement between you and the Kohana Team for the use of Kohana Framework (the "Software"). By obtaining the Software you agree to comply with the terms and conditions of this license. -Copyright (c) 2007-2010 Kohana Team +Copyright (c) 2007-2011 Kohana Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/includes/kohana/README.md b/includes/kohana/README.md index e19aba8..220a934 100644 --- a/includes/kohana/README.md +++ b/includes/kohana/README.md @@ -1,3 +1,19 @@ -# Kohana PHP Framework, version 3.1 (release) +# Kohana PHP Framework -This is the current release version of [Kohana](http://kohanaframework.org/). +[Kohana](http://kohanaframework.org/) is an elegant, open source, and object oriented HMVC framework built using PHP5, by a team of volunteers. It aims to be swift, secure, and small. + +Released under a [BSD license](http://kohanaframework.org/license), Kohana can be used legally for any open source, commercial, or personal project. + +## Documentation +Kohana's documentation can be found at which also contains an API browser. + +The `userguide` module included in all Kohana releases also allows you to view the documentation locally. Once the `userguide` module is enabled in the bootstrap, it is accessible from your site via `/index.php/guide` (or just `/guide` if you are rewriting your URLs). + +## Reporting bugs +If you've stumbled across a bug, please help us out by [reporting the bug](http://dev.kohanaframework.org/projects/kohana3/) you have found. Simply log in or register and submit a new issue, leaving as much information about the bug as possible, e.g. + +* Steps to reproduce +* Expected result +* Actual result + +This will help us to fix the bug as quickly as possible, and if you'd like to fix it yourself feel free to [fork us on GitHub](https://github.com/kohana) and submit a pull request! diff --git a/includes/kohana/application/classes/controller/welcome.php b/includes/kohana/application/classes/controller/welcome.php deleted file mode 100644 index 0984eff..0000000 --- a/includes/kohana/application/classes/controller/welcome.php +++ /dev/null @@ -1,10 +0,0 @@ -response->body('hello, world!'); - } - -} // End Welcome diff --git a/includes/kohana/composer.json b/includes/kohana/composer.json new file mode 100644 index 0000000..a473df1 --- /dev/null +++ b/includes/kohana/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "phpunit/phpunit": "3.7.*" + } +} diff --git a/includes/kohana/composer.lock b/includes/kohana/composer.lock new file mode 100644 index 0000000..3fe1de8 --- /dev/null +++ b/includes/kohana/composer.lock @@ -0,0 +1,413 @@ +{ + "hash": "11774bed2716724738d66bb56a8a1508", + "packages": [ + { + "name": "phpunit/php-code-coverage", + "version": "1.2.6", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "1.2.6" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-code-coverage/zipball/1.2.6", + "reference": "1.2.6", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": ">=1.3.0@stable", + "phpunit/php-token-stream": ">=1.1.3@stable", + "phpunit/php-text-template": ">=1.1.1@stable" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.0.5" + }, + "time": "2012-10-16 22:34:13", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "testing", + "coverage", + "xunit" + ] + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.3.3", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "1.3.3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-file-iterator/zipball/1.3.3", + "reference": "1.3.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "time": "2012-10-11 04:44:38", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "File/" + ] + }, + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "filesystem", + "iterator" + ] + }, + { + "name": "phpunit/php-text-template", + "version": "1.1.3", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-text-template.git", + "reference": "1.1.3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.3", + "reference": "1.1.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "time": "2012-10-11 04:48:39", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "Text/" + ] + }, + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "template" + ] + }, + { + "name": "phpunit/php-timer", + "version": "1.0.4", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-timer.git", + "reference": "1.0.4" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-timer/zipball/1.0.4", + "reference": "1.0.4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "time": "2012-10-11 04:45:58", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "timer" + ] + }, + { + "name": "phpunit/php-token-stream", + "version": "1.1.5", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-token-stream.git", + "reference": "1.1.5" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-token-stream/zipball/1.1.5", + "reference": "1.1.5", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "time": "2012-10-11 04:47:14", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "tokenizer" + ] + }, + { + "name": "phpunit/phpunit", + "version": "3.7.8", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/phpunit.git", + "reference": "3.7.8" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/phpunit/zipball/3.7.8", + "reference": "3.7.8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": ">=1.3.1", + "phpunit/php-text-template": ">=1.1.1", + "phpunit/php-code-coverage": ">=1.2.1", + "phpunit/php-timer": ">=1.0.2", + "phpunit/phpunit-mock-objects": ">=1.2.0", + "symfony/yaml": ">=2.1.0", + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*" + }, + "suggest": { + "phpunit/php-invoker": ">=1.1.0", + "ext-json": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*" + }, + "time": "2012-10-16 22:37:08", + "bin": [ + "composer/bin/phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "include-path": [ + "", + "../../symfony/yaml/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "testing", + "phpunit", + "xunit" + ] + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "1.2.1", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "1.2.1" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/zipball/1.2.1", + "reference": "1.2.1", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-text-template": ">=1.1.1@stable", + "ext-reflection": "*", + "ext-spl": "*" + }, + "suggest": { + "ext-soap": "*" + }, + "time": "2012-10-05 00:00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "mock", + "xunit" + ] + }, + { + "name": "symfony/yaml", + "version": "v2.1.2", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml", + "reference": "v2.1.0-RC2" + }, + "dist": { + "type": "zip", + "url": "https://github.com/symfony/Yaml/zipball/v2.1.0-RC2", + "reference": "v2.1.0-RC2", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "time": "2012-08-22 06:48:41", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml": "" + } + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "http://symfony.com" + } + ], + "packages-dev": null, + "aliases": [ + + ], + "minimum-stability": "stable", + "stability-flags": [ + + ] +} diff --git a/includes/kohana/composer.phar b/includes/kohana/composer.phar new file mode 100644 index 0000000..7c83809 Binary files /dev/null and b/includes/kohana/composer.phar differ diff --git a/includes/kohana/install.php b/includes/kohana/install.php index cbb0a4c..f654177 100644 --- a/includes/kohana/install.php +++ b/includes/kohana/install.php @@ -1,4 +1,20 @@ - + @@ -41,15 +57,15 @@ - =')): ?> + =')): ?> - + - + diff --git a/includes/kohana/modules/auth/classes/Auth.php b/includes/kohana/modules/auth/classes/Auth.php new file mode 100644 index 0000000..8d31fad --- /dev/null +++ b/includes/kohana/modules/auth/classes/Auth.php @@ -0,0 +1,3 @@ +load('auth'); if ( ! $type = $config->get('driver')) { @@ -47,6 +47,7 @@ abstract class Kohana_Auth { /** * Loads Session and configuration options. * + * @param array $config Config Options * @return void */ public function __construct($config = array()) @@ -54,7 +55,7 @@ abstract class Kohana_Auth { // Save the config in the object $this->_config = $config; - $this->_session = Session::instance(); + $this->_session = Session::instance($this->_config['session_type']); } abstract protected function _login($username, $password, $remember); @@ -67,6 +68,7 @@ abstract class Kohana_Auth { * Gets the currently logged in user from the session. * Returns NULL if no user is currently logged in. * + * @param mixed $default Default value to return if the user is currently not logged in. * @return mixed */ public function get_user($default = NULL) @@ -77,9 +79,9 @@ abstract class Kohana_Auth { /** * Attempt to log in a user by using an ORM object and plain-text password. * - * @param string username to log in - * @param string password to check against - * @param boolean enable autologin + * @param string $username Username to log in + * @param string $password Password to check against + * @param boolean $remember Enable autologin * @return boolean */ public function login($username, $password, $remember = FALSE) @@ -87,20 +89,14 @@ abstract class Kohana_Auth { if (empty($password)) return FALSE; - if (is_string($password)) - { - // Create a hashed password - $password = $this->hash($password); - } - return $this->_login($username, $password, $remember); } /** * Log out a user by removing the related session variables. * - * @param boolean completely destroy the session - * @param boolean remove all tokens for user + * @param boolean $destroy Completely destroy the session + * @param boolean $logout_all Remove all tokens for user * @return boolean */ public function logout($destroy = FALSE, $logout_all = FALSE) @@ -127,7 +123,7 @@ abstract class Kohana_Auth { * Check if there is an active session. Optionally allows checking for a * specific role. * - * @param string role name + * @param string $role role name * @return mixed */ public function logged_in($role = NULL) @@ -140,7 +136,7 @@ abstract class Kohana_Auth { * method is deprecated, [Auth::hash] should be used instead. * * @deprecated - * @param string plaintext password + * @param string $password Plaintext password */ public function hash_password($password) { @@ -150,7 +146,7 @@ abstract class Kohana_Auth { /** * Perform a hmac hash, using the configured method. * - * @param string string to hash + * @param string $str string to hash * @return string */ public function hash($str) diff --git a/includes/kohana/modules/auth/classes/kohana/auth/file.php b/includes/kohana/modules/auth/classes/Kohana/Auth/File.php similarity index 75% rename from includes/kohana/modules/auth/classes/kohana/auth/file.php rename to includes/kohana/modules/auth/classes/Kohana/Auth/File.php index 31ca0c3..8640fde 100644 --- a/includes/kohana/modules/auth/classes/kohana/auth/file.php +++ b/includes/kohana/modules/auth/classes/Kohana/Auth/File.php @@ -1,11 +1,11 @@ -hash($password); + } + if (isset($this->_users[$username]) AND $this->_users[$username] === $password) { // Complete the login @@ -47,7 +53,7 @@ class Kohana_Auth_File extends Auth { /** * Forces a user to be logged in, without specifying a password. * - * @param mixed username + * @param mixed $username Username * @return boolean */ public function force_login($username) @@ -59,7 +65,7 @@ class Kohana_Auth_File extends Auth { /** * Get the stored password for a username. * - * @param mixed username + * @param mixed $username Username * @return string */ public function password($username) @@ -70,7 +76,7 @@ class Kohana_Auth_File extends Auth { /** * Compare password with original (plain text). Works for current (logged in) user * - * @param string $password + * @param string $password Password * @return boolean */ public function check_password($password) @@ -85,4 +91,4 @@ class Kohana_Auth_File extends Auth { return ($password === $this->password($username)); } -} // End Auth File \ No newline at end of file +} // End Auth File diff --git a/includes/kohana/modules/auth/classes/auth.php b/includes/kohana/modules/auth/classes/auth.php deleted file mode 100644 index a02b1e5..0000000 --- a/includes/kohana/modules/auth/classes/auth.php +++ /dev/null @@ -1,3 +0,0 @@ - 'file', + 'driver' => 'File', 'hash_method' => 'sha256', 'hash_key' => NULL, 'lifetime' => 1209600, + 'session_type' => Session::$default, 'session_key' => 'auth_user', // Username/password combinations for the Auth File driver diff --git a/includes/kohana/modules/auth/config/userguide.php b/includes/kohana/modules/auth/config/userguide.php new file mode 100644 index 0000000..425e27c --- /dev/null +++ b/includes/kohana/modules/auth/config/userguide.php @@ -0,0 +1,23 @@ + array( + + // This should be the path to this modules userguide pages, without the 'guide/'. Ex: '/guide/modulename/' would be 'modulename' + 'auth' => array( + + // Whether this modules userguide pages should be shown + 'enabled' => TRUE, + + // The name that should show up on the userguide index page + 'name' => 'Auth', + + // A short description of this module, shown on the index page + 'description' => 'User authentication and authorization.', + + // Copyright message, shown in the footer for this module + 'copyright' => '© 2008–2012 Kohana Team', + ) + ) +); \ No newline at end of file diff --git a/includes/kohana/modules/auth/guide/auth/config.md b/includes/kohana/modules/auth/guide/auth/config.md index e69de29..e411fa0 100644 --- a/includes/kohana/modules/auth/guide/auth/config.md +++ b/includes/kohana/modules/auth/guide/auth/config.md @@ -0,0 +1,13 @@ +# Configuration + +The default configuration file is located in `MODPATH/auth/config/auth.php`. You should copy this file to `APPPATH/config/auth.php` and make changes there, in keeping with the [cascading filesystem](../kohana/files). + +[Config merging](../kohana/config#config-merging) allows these default configuration settings to apply if you don't overwrite them in your application configuration file. + +Name | Type | Default | Description +-----|------|---------|------------ +driver | `string` | file | The name of the auth driver to use. +hash_method | `string` | sha256 | The hashing function to use on the passwords. +hash_key | `string` | NULL | The key to use when hashing the password. +session_type | `string` | [Session::$default] | The type of session to use when storing the auth user. +session_key | `string` | auth_user | The name of the session variable used to save the user. diff --git a/includes/kohana/modules/auth/guide/auth/driver/develop.md b/includes/kohana/modules/auth/guide/auth/driver/develop.md new file mode 100644 index 0000000..a69a08c --- /dev/null +++ b/includes/kohana/modules/auth/guide/auth/driver/develop.md @@ -0,0 +1,79 @@ +# Developing Drivers + +## Real World Example + +Sometimes the best way to learn is to jump right in and read the code from another module. The [ORM](https://github.com/kohana/orm/blob/3.2/develop/classes/kohana/auth/orm.php) module comes with an auth driver you can learn from. + +[!!] We will be developing an `example` driver. In your own driver you will substitute `example` with your driver name. + +This example file would be saved at `APPPATH/classes/auth/example.php` (or `MODPATH` if you are creating a module). + +--- + +## Quick Example + +First we will show you a quick example and then break down what is going on. + +~~~ +class Auth_Example extends Auth +{ + protected function _login($username, $password, $remember) + { + // Do username/password check here + } + + public function password($username) + { + // Return the password for the username + } + + public function check_password($password) + { + // Check to see if the logged in user has the given password + } + + public function logged_in($role = NULL) + { + // Check to see if the user is logged in, and if $role is set, has all roles + } + + public function get_user($default = NULL) + { + // Get the logged in user, or return the $default if a user is not found + } +} +~~~ + +## Extending Auth + +All drivers must extend the [Auth] class. + + class Auth_Example extends Auth + +## Abstract Methods + +The `Auth` class has 3 abstract methods that must be defined in your new driver. + +~~~ +abstract protected function _login($username, $password, $remember); + +abstract public function password($username); + +abstract public function check_password($user); +~~~ + +## Extending Functionality + +Given that every auth system is going to check if users exist and if they have roles or not you will more than likely have to change some default functionality. + +Here are a few functions that you should pay attention to. + +~~~ +public function logged_in($role = NULL) + +public function get_user($default = NULL) +~~~ + +## Activating the Driver + +After you create your driver you will want to use it. It is a easy as setting the `driver` [configuration](config) option to the name of your driver (in our case `example`). diff --git a/includes/kohana/modules/auth/guide/auth/driver/file.md b/includes/kohana/modules/auth/guide/auth/driver/file.md new file mode 100644 index 0000000..7a6fa09 --- /dev/null +++ b/includes/kohana/modules/auth/guide/auth/driver/file.md @@ -0,0 +1,19 @@ +# File Driver + +The [Auth::File] driver is included with the auth module. + +Below are additional configuration options that can be set for this driver. + +Name | Type | Default | Description +-----|------|---------|------------- +users | `array` | array() | A user => password (_hashed_) array of all the users in your application + +## Forcing Login + +[Auth_File::force_login] allows you to force a user login without a password. + +~~~ +// Force the user with a username of admin to be logged into your application +Auth::instance()->force_login('admin'); +$user = Auth::instance()->get_user(); +~~~ diff --git a/includes/kohana/modules/auth/guide/auth/index.md b/includes/kohana/modules/auth/guide/auth/index.md index e69de29..18127e6 100644 --- a/includes/kohana/modules/auth/guide/auth/index.md +++ b/includes/kohana/modules/auth/guide/auth/index.md @@ -0,0 +1,19 @@ +# Auth + +User authentication and authorization is provided by the auth module. + +The auth module is included with Kohana, but needs to be enabled before you can use it. To enable, open your `application/bootstrap.php` file and modify the call to [Kohana::modules] by including the auth module like so: + +~~~ +Kohana::modules(array( + ... + 'auth' => MODPATH.'auth', + ... +)); +~~~ + +Next, you will then need to [configure](config) the auth module. + +The auth module provides the [Auth::File] driver for you. There is also an auth driver included with the ORM module. + +As your application needs change you may need to find another driver or [develop](driver/develop) your own. diff --git a/includes/kohana/modules/auth/guide/auth/login.md b/includes/kohana/modules/auth/guide/auth/login.md index e69de29..1207b5d 100644 --- a/includes/kohana/modules/auth/guide/auth/login.md +++ b/includes/kohana/modules/auth/guide/auth/login.md @@ -0,0 +1,62 @@ +# Log in and out + +The auth module provides methods to help you log users in and out of your application. + +## Log in + +The [Auth::login] method handles the login. + +~~~ +// Handled from a form with inputs with names email / password +$post = $this->request->post(); +$success = Auth::instance()->login($post['email'], $post['password']); + +if ($success) +{ + // Login successful, send to app +} +else +{ + // Login failed, send back to form with error message +} +~~~ + +## Logged in User + +There are two ways to check if a user is logged in. If you just need to check if the user is logged in use [Auth::logged_in]. + +~~~ +if (Auth::instance()->logged_in()) +{ + // User is logged in, continue on +} +else +{ + // User isn't logged in, redirect to the login form. +} +~~~ + +You can also get the logged in user object by using [Auth::get_user]. If the user is null, then no user was found. + +~~~ +$user = Auth::instance()->get_user(); + +// Check for a user (NULL if not user is found) +if ($user !== null) +{ + // User is found, continue on +} +else +{ + // User was not found, redirect to the login form +} +~~~ + +## Log out + +The [Auth::logout] method will take care of logging out a user. + +~~~ +Auth::instance()->logout(); +// Redirect the user back to login page +~~~ diff --git a/includes/kohana/modules/auth/guide/auth/menu.md b/includes/kohana/modules/auth/guide/auth/menu.md index 1708caa..23fc0ee 100644 --- a/includes/kohana/modules/auth/guide/auth/menu.md +++ b/includes/kohana/modules/auth/guide/auth/menu.md @@ -1,7 +1,6 @@ ## [Auth]() -- [Config](config) -- [User Model](user) -- [Register Users](register) +- [Configuration](config) - [Log in and out](login) -- [Edit User](edit) -- [Using Roles](roles) +- Drivers + - [File](driver/file) + - [Developing](driver/develop) diff --git a/includes/kohana/modules/auth/guide/auth/user.md b/includes/kohana/modules/auth/guide/auth/user.md deleted file mode 100644 index e69de29..0000000 diff --git a/includes/kohana/modules/cache/README.md b/includes/kohana/modules/cache/README.md index cd04fbd..7acff9c 100644 --- a/includes/kohana/modules/cache/README.md +++ b/includes/kohana/modules/cache/README.md @@ -9,13 +9,11 @@ Supported cache solutions Currently this module supports the following cache methods. 1. APC -2. eAccelerator -3. Memcache -4. Memcached-tags (Supports tags) -5. SQLite (Supports tags) -6. File -7. Xcache -8. Wincache +2. Memcache +3. Memcached-tags (Supports tags) +4. SQLite (Supports tags) +5. File +6. Wincache Planned support --------------- diff --git a/includes/kohana/modules/cache/classes/cache.php b/includes/kohana/modules/cache/classes/Cache.php similarity index 100% rename from includes/kohana/modules/cache/classes/cache.php rename to includes/kohana/modules/cache/classes/Cache.php diff --git a/includes/kohana/modules/cache/classes/cache/apc.php b/includes/kohana/modules/cache/classes/Cache/Apc.php similarity index 100% rename from includes/kohana/modules/cache/classes/cache/apc.php rename to includes/kohana/modules/cache/classes/Cache/Apc.php diff --git a/includes/kohana/modules/cache/classes/cache/eaccelerator.php b/includes/kohana/modules/cache/classes/Cache/Arithmetic.php similarity index 50% rename from includes/kohana/modules/cache/classes/cache/eaccelerator.php rename to includes/kohana/modules/cache/classes/Cache/Arithmetic.php index 0aa168d..ab832c3 100644 --- a/includes/kohana/modules/cache/classes/cache/eaccelerator.php +++ b/includes/kohana/modules/cache/classes/Cache/Arithmetic.php @@ -1,3 +1,3 @@ array( // Default group * 'driver' => 'memcache', // using Memcache driver @@ -48,30 +48,30 @@ * 'compression' => FALSE, // Use compression? * ), * ) - * + * * In cases where only one cache group is required, if the group is named `default` there is * no need to pass the group name when instantiating a cache instance. - * + * * #### General cache group configuration settings - * + * * Below are the settings available to all types of cache driver. - * + * * Name | Required | Description * -------------- | -------- | --------------------------------------------------------------- * driver | __YES__ | (_string_) The driver type to use - * + * * Details of the settings specific to each driver are available within the drivers documentation. - * + * * ### System requirements - * + * * * Kohana 3.0.x * * PHP 5.2.4 or greater - * + * * @package Kohana/Cache * @category Base * @version 2.0 * @author Kohana Team - * @copyright (c) 2009-2010 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ abstract class Kohana_Cache { @@ -91,19 +91,19 @@ abstract class Kohana_Cache { /** * Creates a singleton of a Kohana Cache group. If no group is supplied * the __default__ cache group is used. - * + * * // Create an instance of the default group * $default_group = Cache::instance(); - * + * * // Create an instance of a group * $foo_group = Cache::instance('foo'); - * + * * // Access an instantiated group directly * $foo_group = Cache::$instances['default']; * - * @param string the name of the cache group to use [Optional] - * @return Kohana_Cache - * @throws Kohana_Cache_Exception + * @param string $group the name of the cache group to use [Optional] + * @return Cache + * @throws Cache_Exception */ public static function instance($group = NULL) { @@ -120,11 +120,14 @@ abstract class Kohana_Cache { return Cache::$instances[$group]; } - $config = Kohana::config('cache'); + $config = Kohana::$config->load('cache'); if ( ! $config->offsetExists($group)) { - throw new Kohana_Cache_Exception('Failed to load Kohana Cache group: :group', array(':group' => $group)); + throw new Cache_Exception( + 'Failed to load Kohana Cache group: :group', + array(':group' => $group) + ); } $config = $config->get($group); @@ -140,59 +143,100 @@ abstract class Kohana_Cache { /** * @var Config */ - protected $_config; + protected $_config = array(); /** * Ensures singleton pattern is observed, loads the default expiry - * - * @param array configuration + * + * @param array $config configuration */ protected function __construct(array $config) { - $this->_config = $config; + $this->config($config); + } + + /** + * Getter and setter for the configuration. If no argument provided, the + * current configuration is returned. Otherwise the configuration is set + * to this class. + * + * // Overwrite all configuration + * $cache->config(array('driver' => 'memcache', '...')); + * + * // Set a new configuration setting + * $cache->config('servers', array( + * 'foo' => 'bar', + * '...' + * )); + * + * // Get a configuration setting + * $servers = $cache->config('servers); + * + * @param mixed key to set to array, either array or config path + * @param mixed value to associate with key + * @return mixed + */ + public function config($key = NULL, $value = NULL) + { + if ($key === NULL) + return $this->_config; + + if (is_array($key)) + { + $this->_config = $key; + } + else + { + if ($value === NULL) + return Arr::get($this->_config, $key); + + $this->_config[$key] = $value; + } + + return $this; } /** * Overload the __clone() method to prevent cloning * * @return void - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ - public function __clone() + final public function __clone() { - throw new Kohana_Cache_Exception('Cloning of Kohana_Cache objects is forbidden'); + throw new Cache_Exception('Cloning of Kohana_Cache objects is forbidden'); } /** * Retrieve a cached value entry by id. - * + * * // Retrieve cache entry from default group * $data = Cache::instance()->get('foo'); - * + * * // Retrieve cache entry from default group and return 'bar' if miss * $data = Cache::instance()->get('foo', 'bar'); - * + * * // Retrieve cache entry from memcache group * $data = Cache::instance('memcache')->get('foo'); * - * @param string id of cache to entry - * @param string default value to return if cache miss + * @param string $id id of cache to entry + * @param string $default default value to return if cache miss * @return mixed - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ abstract public function get($id, $default = NULL); /** * Set a value to cache with id and lifetime - * + * * $data = 'bar'; - * + * * // Set 'bar' to 'foo' in default group, using default expiry * Cache::instance()->set('foo', $data); - * + * * // Set 'bar' to 'foo' in default group for 30 seconds * Cache::instance()->set('foo', $data, 30); - * + * * // Set 'bar' to 'foo' in memcache group for 10 minutes * if (Cache::instance('memcache')->set('foo', $data, 600)) * { @@ -200,37 +244,37 @@ abstract class Kohana_Cache { * return * } * - * @param string id of cache entry - * @param string data to set to cache - * @param integer lifetime in seconds + * @param string $id id of cache entry + * @param string $data data to set to cache + * @param integer $lifetime lifetime in seconds * @return boolean */ abstract public function set($id, $data, $lifetime = 3600); /** * Delete a cache entry based on id - * + * * // Delete 'foo' entry from the default group * Cache::instance()->delete('foo'); - * + * * // Delete 'foo' entry from the memcache group * Cache::instance('memcache')->delete('foo') * - * @param string id to remove from cache + * @param string $id id to remove from cache * @return boolean */ abstract public function delete($id); /** * Delete all cache entries. - * + * * Beware of using this method when * using shared memory cache systems, as it will wipe every * entry within the system for all clients. - * + * * // Delete all cache entries in the default group * Cache::instance()->delete_all(); - * + * * // Delete all cache entries in the memcache group * Cache::instance('memcache')->delete_all(); * @@ -243,8 +287,8 @@ abstract class Kohana_Cache { * * // Sanitize a cache id * $id = $this->_sanitize_id($id); - * - * @param string id of cache to sanitize + * + * @param string $id id of cache to sanitize * @return string */ protected function _sanitize_id($id) diff --git a/includes/kohana/modules/cache/classes/kohana/cache/apc.php b/includes/kohana/modules/cache/classes/Kohana/Cache/Apc.php similarity index 68% rename from includes/kohana/modules/cache/classes/kohana/cache/apc.php rename to includes/kohana/modules/cache/classes/Kohana/Cache/Apc.php index 78765fe..acefcc8 100644 --- a/includes/kohana/modules/cache/classes/kohana/cache/apc.php +++ b/includes/kohana/modules/cache/classes/Kohana/Cache/Apc.php @@ -2,54 +2,54 @@ /** * [Kohana Cache](api/Kohana_Cache) APC driver. Provides an opcode based * driver for the Kohana Cache library. - * + * * ### Configuration example - * + * * Below is an example of an _apc_ server configuration. - * + * * return array( * 'apc' => array( // Driver group * 'driver' => 'apc', // using APC driver * ), * ) - * + * * In cases where only one cache group is required, if the group is named `default` there is * no need to pass the group name when instantiating a cache instance. - * + * * #### General cache group configuration settings - * + * * Below are the settings available to all types of cache driver. - * + * * Name | Required | Description * -------------- | -------- | --------------------------------------------------------------- * driver | __YES__ | (_string_) The driver type to use - * + * * ### System requirements - * + * * * Kohana 3.0.x * * PHP 5.2.4 or greater * * APC PHP extension - * + * * @package Kohana/Cache * @category Base * @author Kohana Team - * @copyright (c) 2009-2010 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ -class Kohana_Cache_Apc extends Cache { +class Kohana_Cache_Apc extends Cache implements Cache_Arithmetic { /** * Check for existence of the APC extension This method cannot be invoked externally. The driver must * be instantiated using the `Cache::instance()` method. * - * @param array configuration - * @throws Kohana_Cache_Exception + * @param array $config configuration + * @throws Cache_Exception */ protected function __construct(array $config) { if ( ! extension_loaded('apc')) { - throw new Kohana_Cache_Exception('PHP APC extension is not available.'); + throw new Cache_Exception('PHP APC extension is not available.'); } parent::__construct($config); @@ -57,17 +57,17 @@ class Kohana_Cache_Apc extends Cache { /** * Retrieve a cached value entry by id. - * + * * // Retrieve cache entry from apc group * $data = Cache::instance('apc')->get('foo'); - * + * * // Retrieve cache entry from apc group and return 'bar' if miss * $data = Cache::instance('apc')->get('foo', 'bar'); * - * @param string id of cache to entry - * @param string default value to return if cache miss + * @param string $id id of cache to entry + * @param string $default default value to return if cache miss * @return mixed - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function get($id, $default = NULL) { @@ -78,18 +78,18 @@ class Kohana_Cache_Apc extends Cache { /** * Set a value to cache with id and lifetime - * + * * $data = 'bar'; - * + * * // Set 'bar' to 'foo' in apc group, using default expiry * Cache::instance('apc')->set('foo', $data); - * + * * // Set 'bar' to 'foo' in apc group for 30 seconds * Cache::instance('apc')->set('foo', $data, 30); * - * @param string id of cache entry - * @param string data to set to cache - * @param integer lifetime in seconds + * @param string $id id of cache entry + * @param string $data data to set to cache + * @param integer $lifetime lifetime in seconds * @return boolean */ public function set($id, $data, $lifetime = NULL) @@ -104,11 +104,11 @@ class Kohana_Cache_Apc extends Cache { /** * Delete a cache entry based on id - * + * * // Delete 'foo' entry from the apc group * Cache::instance('apc')->delete('foo'); * - * @param string id to remove from cache + * @param string $id id to remove from cache * @return boolean */ public function delete($id) @@ -118,11 +118,11 @@ class Kohana_Cache_Apc extends Cache { /** * Delete all cache entries. - * + * * Beware of using this method when * using shared memory cache systems, as it will wipe every * entry within the system for all clients. - * + * * // Delete all cache entries in the apc group * Cache::instance('apc')->delete_all(); * @@ -132,4 +132,35 @@ class Kohana_Cache_Apc extends Cache { { return apc_clear_cache('user'); } -} + + /** + * Increments a given value by the step value supplied. + * Useful for shared counters and other persistent integer based + * tracking. + * + * @param string id of cache entry to increment + * @param int step value to increment by + * @return integer + * @return boolean + */ + public function increment($id, $step = 1) + { + return apc_inc($id, $step); + } + + /** + * Decrements a given value by the step value supplied. + * Useful for shared counters and other persistent integer based + * tracking. + * + * @param string id of cache entry to decrement + * @param int step value to decrement by + * @return integer + * @return boolean + */ + public function decrement($id, $step = 1) + { + return apc_dec($id, $step); + } + +} // End Kohana_Cache_Apc diff --git a/includes/kohana/modules/cache/classes/Kohana/Cache/Arithmetic.php b/includes/kohana/modules/cache/classes/Kohana/Cache/Arithmetic.php new file mode 100644 index 0000000..1bdfb31 --- /dev/null +++ b/includes/kohana/modules/cache/classes/Kohana/Cache/Arithmetic.php @@ -0,0 +1,39 @@ + array( // File driver group * 'driver' => 'file', // using File driver * 'cache_dir' => APPPATH.'cache/.kohana_cache', // Cache location * ), * ) - * + * * In cases where only one cache group is required, if the group is named `default` there is * no need to pass the group name when instantiating a cache instance. - * + * * #### General cache group configuration settings - * + * * Below are the settings available to all types of cache driver. - * + * * Name | Required | Description * -------------- | -------- | --------------------------------------------------------------- * driver | __YES__ | (_string_) The driver type to use * cache_dir | __NO__ | (_string_) The cache directory to use for this cache instance - * + * * ### System requirements - * + * * * Kohana 3.0.x * * PHP 5.2.4 or greater - * + * * @package Kohana/Cache * @category Base * @author Kohana Team - * @copyright (c) 2009-2010 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ -class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { +class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { /** * Creates a hashed filename based on the string. This is used * to create shorter unique IDs for each cache filename. - * + * * // Create the cache filename * $filename = Cache_File::filename($this->_sanitize_id($id)); * - * @param string string to hash into filename + * @param string $string string to hash into filename * @return string */ protected static function filename($string) { - return sha1($string).'.json'; + return sha1($string).'.cache'; } /** @@ -64,8 +64,8 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { * Constructs the file cache driver. This method cannot be invoked externally. The file cache driver must * be instantiated using the `Cache::instance()` method. * - * @param array config - * @throws Kohana_Cache_Exception + * @param array $config config + * @throws Cache_Exception */ protected function __construct(array $config) { @@ -91,35 +91,35 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { // If the defined directory is a file, get outta here if ($this->_cache_dir->isFile()) { - throw new Kohana_Cache_Exception('Unable to create cache directory as a file already exists : :resource', array(':resource' => $this->_cache_dir->getRealPath())); + throw new Cache_Exception('Unable to create cache directory as a file already exists : :resource', array(':resource' => $this->_cache_dir->getRealPath())); } // Check the read status of the directory if ( ! $this->_cache_dir->isReadable()) { - throw new Kohana_Cache_Exception('Unable to read from the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath())); + throw new Cache_Exception('Unable to read from the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath())); } // Check the write status of the directory if ( ! $this->_cache_dir->isWritable()) { - throw new Kohana_Cache_Exception('Unable to write to the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath())); + throw new Cache_Exception('Unable to write to the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath())); } } /** * Retrieve a cached value entry by id. - * + * * // Retrieve cache entry from file group * $data = Cache::instance('file')->get('foo'); - * + * * // Retrieve cache entry from file group and return 'bar' if miss * $data = Cache::instance('file')->get('foo', 'bar'); * - * @param string id of cache to entry - * @param string default value to return if cache miss + * @param string $id id of cache to entry + * @param string $default default value to return if cache miss * @return mixed - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function get($id, $default = NULL) { @@ -140,34 +140,44 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { } else { - // Open the file and extract the json - $json = $file->openFile()->current(); + // Open the file and parse data + $created = $file->getMTime(); + $data = $file->openFile(); + $lifetime = $data->fgets(); - // Decode the json into PHP object - $data = json_decode($json); + // If we're at the EOF at this point, corrupted! + if ($data->eof()) + { + throw new Cache_Exception(__METHOD__.' corrupted cache file!'); + } + + $cache = ''; + + while ($data->eof() === FALSE) + { + $cache .= $data->fgets(); + } // Test the expiry - if ($data->expiry < time()) + if (($created + (int) $lifetime) < time()) { // Delete the file $this->_delete_file($file, NULL, TRUE); - - // Return default value return $default; } else { - return ($data->type === 'string') ? $data->payload : unserialize($data->payload); + return unserialize($cache); } } - + } catch (ErrorException $e) { // Handle ErrorException caused by failed unserialization if ($e->getCode() === E_NOTICE) { - throw new Kohana_Cache_Exception(__METHOD__.' failed to unserialize cached object with message : '.$e->getMessage()); + throw new Cache_Exception(__METHOD__.' failed to unserialize cached object with message : '.$e->getMessage()); } // Otherwise throw the exception @@ -177,18 +187,18 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { /** * Set a value to cache with id and lifetime - * + * * $data = 'bar'; - * + * * // Set 'bar' to 'foo' in file group, using default expiry * Cache::instance('file')->set('foo', $data); - * + * * // Set 'bar' to 'foo' in file group for 30 seconds * Cache::instance('file')->set('foo', $data, 30); * - * @param string id of cache entry - * @param string data to set to cache - * @param integer lifetime in seconds + * @param string $id id of cache entry + * @param string $data data to set to cache + * @param integer $lifetime lifetime in seconds * @return boolean */ public function set($id, $data, $lifetime = NULL) @@ -209,10 +219,10 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { // If the directory path is not a directory if ( ! $dir->isDir()) { - // Create the directory + // Create the directory if ( ! mkdir($directory, 0777, TRUE)) { - throw new Kohana_Cache_Exception(__METHOD__.' unable to create directory : :directory', array(':directory' => $directory)); + throw new Cache_Exception(__METHOD__.' unable to create directory : :directory', array(':directory' => $directory)); } // chmod to solve potential umask issues @@ -225,16 +235,9 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { try { - $type = gettype($data); - - // Serialize the data - $data = json_encode( (object) array( - 'payload' => ($type === 'string') ? $data : serialize($data), - 'expiry' => time() + $lifetime, - 'type' => $type - )); - - $size = strlen($data); + $data = $lifetime."\n".serialize($data); + $file->fwrite($data, strlen($data)); + return (bool) $file->fflush(); } catch (ErrorException $e) { @@ -242,31 +245,21 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { if ($e->getCode() === E_NOTICE) { // Throw a caching error - throw new Kohana_Cache_Exception(__METHOD__.' failed to serialize data for caching with message : '.$e->getMessage()); + throw new Cache_Exception(__METHOD__.' failed to serialize data for caching with message : '.$e->getMessage()); } // Else rethrow the error exception throw $e; } - - try - { - $file->fwrite($data, $size); - return (bool) $file->fflush(); - } - catch (Exception $e) - { - throw $e; - } } /** * Delete a cache entry based on id - * + * * // Delete 'foo' entry from the file group * Cache::instance('file')->delete('foo'); * - * @param string id to remove from cache + * @param string $id id to remove from cache * @return boolean */ public function delete($id) @@ -279,11 +272,11 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { /** * Delete all cache entries. - * + * * Beware of using this method when * using shared memory cache systems, as it will wipe every * entry within the system for all clients. - * + * * // Delete all cache entries in the file group * Cache::instance('file')->delete_all(); * @@ -308,16 +301,16 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { /** * Deletes files recursively and returns FALSE on any errors - * + * * // Delete a file or folder whilst retaining parent directory and ignore all errors * $this->_delete_file($folder, TRUE, TRUE); * - * @param SplFileInfo file - * @param boolean retain the parent directory - * @param boolean ignore_errors to prevent all exceptions interrupting exec - * @param boolean only expired files + * @param SplFileInfo $file file + * @param boolean $retain_parent_directory retain the parent directory + * @param boolean $ignore_errors ignore_errors to prevent all exceptions interrupting exec + * @param boolean $only_expired only expired files * @return boolean - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ protected function _delete_file(SplFileInfo $file, $retain_parent_directory = FALSE, $ignore_errors = FALSE, $only_expired = FALSE) { @@ -329,8 +322,13 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { { try { + // Handle ignore files + if (in_array($file->getFilename(), $this->config('ignore_on_delete'))) + { + $delete = FALSE; + } // If only expired is not set - if ($only_expired === FALSE) + elseif ($only_expired === FALSE) { // We want to delete the file $delete = TRUE; @@ -344,19 +342,18 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { $delete = $data->expiry < time(); } - // If the delete flag is set + // If the delete flag is set delete file if ($delete === TRUE) - { - // Try to delete - unlink($file->getRealPath()); - } + return unlink($file->getRealPath()); + else + return FALSE; } catch (ErrorException $e) { // Catch any delete file warnings if ($e->getCode() === E_WARNING) { - throw new Kohana_Cache_Exception(__METHOD__.' failed to delete file : :file', array(':file' => $file->getRealPath())); + throw new Cache_Exception(__METHOD__.' failed to delete file : :file', array(':file' => $file->getRealPath())); } } } @@ -373,7 +370,7 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { $name = $files->getFilename(); // If the name is not a dot - if ($name != '.' AND $name != '..' AND substr($file->getFilename(), 0, 1) == '.') + if ($name != '.' AND $name != '..') { // Create new file resource $fp = new SplFileInfo($files->getRealPath()); @@ -405,10 +402,16 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { // Catch any delete directory warnings if ($e->getCode() === E_WARNING) { - throw new Kohana_Cache_Exception(__METHOD__.' failed to delete directory : :directory', array(':directory' => $file->getRealPath())); + throw new Cache_Exception(__METHOD__.' failed to delete directory : :directory', array(':directory' => $file->getRealPath())); } + throw $e; } } + else + { + // We get here if a file has already been deleted + return FALSE; + } } // Catch all exceptions catch (Exception $e) @@ -426,11 +429,11 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { /** * Resolves the cache directory real path from the filename - * + * * // Get the realpath of the cache folder * $realpath = $this->_resolve_directory($filename); * - * @param string filename to resolve + * @param string $filename filename to resolve * @return string */ protected function _resolve_directory($filename) @@ -442,22 +445,22 @@ class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { * Makes the cache directory if it doesn't exist. Simply a wrapper for * `mkdir` to ensure DRY principles * - * @see http://php.net/manual/en/function.mkdir.php - * @param string directory - * @param string mode - * @param string recursive - * @param string context + * @link http://php.net/manual/en/function.mkdir.php + * @param string $directory + * @param integer $mode + * @param boolean $recursive + * @param resource $context * @return SplFileInfo - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ protected function _make_directory($directory, $mode = 0777, $recursive = FALSE, $context = NULL) { if ( ! mkdir($directory, $mode, $recursive, $context)) { - throw new Kohana_Cache_Exception('Failed to create the defined cache directory : :directory', array(':directory' => $directory)); + throw new Cache_Exception('Failed to create the defined cache directory : :directory', array(':directory' => $directory)); } chmod($directory, $mode); - return new SplFileInfo($directory);; + return new SplFileInfo($directory); } } diff --git a/includes/kohana/modules/cache/classes/kohana/cache/garbagecollect.php b/includes/kohana/modules/cache/classes/Kohana/Cache/GarbageCollect.php similarity index 93% rename from includes/kohana/modules/cache/classes/kohana/cache/garbagecollect.php rename to includes/kohana/modules/cache/classes/Kohana/Cache/GarbageCollect.php index 62c3148..c0bc519 100644 --- a/includes/kohana/modules/cache/classes/kohana/cache/garbagecollect.php +++ b/includes/kohana/modules/cache/classes/Kohana/Cache/GarbageCollect.php @@ -8,7 +8,7 @@ * @category Base * @version 2.0 * @author Kohana Team - * @copyright (c) 2009-2010 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license * @since 3.0.8 */ diff --git a/includes/kohana/modules/cache/classes/kohana/cache/memcache.php b/includes/kohana/modules/cache/classes/Kohana/Cache/Memcache.php similarity index 83% rename from includes/kohana/modules/cache/classes/kohana/cache/memcache.php rename to includes/kohana/modules/cache/classes/Kohana/Cache/Memcache.php index c377dc4..b6a1cdb 100644 --- a/includes/kohana/modules/cache/classes/kohana/cache/memcache.php +++ b/includes/kohana/modules/cache/classes/Kohana/Cache/Memcache.php @@ -1,16 +1,16 @@ array( // Default group * 'driver' => 'memcache', // using Memcache driver @@ -37,24 +37,24 @@ * 'compression' => FALSE, // Use compression? * ), * ) - * + * * In cases where only one cache group is required, if the group is named `default` there is * no need to pass the group name when instantiating a cache instance. - * + * * #### General cache group configuration settings - * + * * Below are the settings available to all types of cache driver. - * + * * Name | Required | Description * -------------- | -------- | --------------------------------------------------------------- * driver | __YES__ | (_string_) The driver type to use * servers | __YES__ | (_array_) Associative array of server details, must include a __host__ key. (see _Memcache server configuration_ below) * compression | __NO__ | (_boolean_) Use data compression when caching - * + * * #### Memcache server configuration - * + * * The following settings should be used when defining each memcache server - * + * * Name | Required | Description * ---------------- | -------- | --------------------------------------------------------------- * host | __YES__ | (_string_) The host of the memcache server, i.e. __localhost__; or __127.0.0.1__; or __memcache.domain.tld__ @@ -65,22 +65,22 @@ * retry_interval | __NO__ | (_integer_) Controls how often a failed server will be retried, the default value is 15 seconds. Setting this parameter to -1 disables automatic retry. Default __15__ * status | __NO__ | (_boolean_) Controls if the server should be flagged as online. Default __TRUE__ * failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language.pseudo-types.php#language.types.callback)_) Allows the user to specify a callback function to run upon encountering an error. The callback is run before failover is attempted. The function takes two parameters, the hostname and port of the failed server. Default __NULL__ - * + * * ### System requirements - * + * * * Kohana 3.0.x * * PHP 5.2.4 or greater * * Memcache (plus Memcached-tags for native tagging support) * * Zlib - * + * * @package Kohana/Cache * @category Base * @version 2.0 * @author Kohana Team - * @copyright (c) 2009-2010 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ -class Kohana_Cache_Memcache extends Cache { +class Kohana_Cache_Memcache extends Cache implements Cache_Arithmetic { // Memcache has a maximum cache lifetime of 30 days const CACHE_CEILING = 2592000; @@ -109,15 +109,15 @@ class Kohana_Cache_Memcache extends Cache { /** * Constructs the memcache Kohana_Cache object * - * @param array configuration - * @throws Kohana_Cache_Exception + * @param array $config configuration + * @throws Cache_Exception */ protected function __construct(array $config) { // Check for the memcache extention if ( ! extension_loaded('memcache')) { - throw new Kohana_Cache_Exception('Memcache PHP extention not loaded'); + throw new Cache_Exception('Memcache PHP extention not loaded'); } parent::__construct($config); @@ -131,7 +131,7 @@ class Kohana_Cache_Memcache extends Cache { if ( ! $servers) { // Throw an exception if no server found - throw new Kohana_Cache_Exception('No Memcache servers defined in configuration'); + throw new Cache_Exception('No Memcache servers defined in configuration'); } // Setup default server configuration @@ -155,7 +155,7 @@ class Kohana_Cache_Memcache extends Cache { if ( ! $this->_memcache->addServer($server['host'], $server['port'], $server['persistent'], $server['weight'], $server['timeout'], $server['retry_interval'], $server['status'], $server['failure_callback'])) { - throw new Kohana_Cache_Exception('Memcache could not connect to host \':host\' using port \':port\'', array(':host' => $server['host'], ':port' => $server['port'])); + throw new Cache_Exception('Memcache could not connect to host \':host\' using port \':port\'', array(':host' => $server['host'], ':port' => $server['port'])); } } @@ -165,17 +165,17 @@ class Kohana_Cache_Memcache extends Cache { /** * Retrieve a cached value entry by id. - * + * * // Retrieve cache entry from memcache group * $data = Cache::instance('memcache')->get('foo'); - * + * * // Retrieve cache entry from memcache group and return 'bar' if miss * $data = Cache::instance('memcache')->get('foo', 'bar'); * - * @param string id of cache to entry - * @param string default value to return if cache miss + * @param string $id id of cache to entry + * @param string $default default value to return if cache miss * @return mixed - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function get($id, $default = NULL) { @@ -194,9 +194,9 @@ class Kohana_Cache_Memcache extends Cache { /** * Set a value to cache with id and lifetime - * + * * $data = 'bar'; - * + * * // Set 'bar' to 'foo' in memcache group for 10 minutes * if (Cache::instance('memcache')->set('foo', $data, 600)) * { @@ -204,9 +204,9 @@ class Kohana_Cache_Memcache extends Cache { * return * } * - * @param string id of cache entry - * @param mixed data to set to cache - * @param integer lifetime in seconds, maximum value 2592000 + * @param string $id id of cache entry + * @param mixed $data data to set to cache + * @param integer $lifetime lifetime in seconds, maximum value 2592000 * @return boolean */ public function set($id, $data, $lifetime = 3600) @@ -235,15 +235,15 @@ class Kohana_Cache_Memcache extends Cache { /** * Delete a cache entry based on id - * + * * // Delete the 'foo' cache entry immediately * Cache::instance('memcache')->delete('foo'); - * + * * // Delete the 'bar' cache entry after 30 seconds * Cache::instance('memcache')->delete('bar', 30); * - * @param string id of entry to delete - * @param integer timeout of entry, if zero item is deleted immediately, otherwise the item will delete after the specified value in seconds + * @param string $id id of entry to delete + * @param integer $timeout timeout of entry, if zero item is deleted immediately, otherwise the item will delete after the specified value in seconds * @return boolean */ public function delete($id, $timeout = 0) @@ -254,11 +254,11 @@ class Kohana_Cache_Memcache extends Cache { /** * Delete all cache entries. - * + * * Beware of using this method when * using shared memory cache systems, as it will wipe every * entry within the system for all clients. - * + * * // Delete all cache entries in the default group * Cache::instance('memcache')->delete_all(); * @@ -280,15 +280,15 @@ class Kohana_Cache_Memcache extends Cache { * on a particular server fails. This method switches off that instance of the * server if the configuration setting `instant_death` is set to `TRUE`. * - * @param string hostname - * @param integer port + * @param string $hostname + * @param integer $port * @return void|boolean * @since 3.0.8 */ public function _failed_request($hostname, $port) { if ( ! $this->_config['instant_death']) - return; + return; // Setup non-existent host $host = FALSE; @@ -321,4 +321,34 @@ class Kohana_Cache_Memcache extends Cache { )); } } + + /** + * Increments a given value by the step value supplied. + * Useful for shared counters and other persistent integer based + * tracking. + * + * @param string id of cache entry to increment + * @param int step value to increment by + * @return integer + * @return boolean + */ + public function increment($id, $step = 1) + { + return $this->_memcache->increment($id, $step); + } + + /** + * Decrements a given value by the step value supplied. + * Useful for shared counters and other persistent integer based + * tracking. + * + * @param string id of cache entry to decrement + * @param int step value to decrement by + * @return integer + * @return boolean + */ + public function decrement($id, $step = 1) + { + return $this->_memcache->decrement($id, $step); + } } \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/memcachetag.php b/includes/kohana/modules/cache/classes/Kohana/Cache/MemcacheTag.php similarity index 62% rename from includes/kohana/modules/cache/classes/kohana/cache/memcachetag.php rename to includes/kohana/modules/cache/classes/Kohana/Cache/MemcacheTag.php index 866ab9b..644e543 100644 --- a/includes/kohana/modules/cache/classes/kohana/cache/memcachetag.php +++ b/includes/kohana/modules/cache/classes/Kohana/Cache/MemcacheTag.php @@ -1,21 +1,21 @@ _memcache, 'tag_add')) { - throw new Kohana_Cache_Exception('Memcached-tags PHP plugin not present. Please see http://code.google.com/p/memcached-tags/ for more information'); + throw new Cache_Exception('Memcached-tags PHP plugin not present. Please see http://code.google.com/p/memcached-tags/ for more information'); } } /** * Set a value based on an id with tags - * - * @param string id - * @param mixed data - * @param integer lifetime [Optional] - * @param array tags [Optional] + * + * @param string $id id + * @param mixed $data data + * @param integer $lifetime lifetime [Optional] + * @param array $tags tags [Optional] * @return boolean */ public function set_with_tags($id, $data, $lifetime = NULL, array $tags = NULL) { + $id = $this->_sanitize_id($id); + $result = $this->set($id, $data, $lifetime); if ($result and $tags) @@ -54,7 +56,7 @@ class Kohana_Cache_MemcacheTag extends Cache_Memcache implements Kohana_Cache_Ta /** * Delete cache entries based on a tag * - * @param string tag + * @param string $tag tag * @return boolean */ public function delete_tag($tag) @@ -65,12 +67,12 @@ class Kohana_Cache_MemcacheTag extends Cache_Memcache implements Kohana_Cache_Ta /** * Find cache entries based on a tag * - * @param string tag + * @param string $tag tag * @return void - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function find($tag) { - throw new Kohana_Cache_Exception('Memcached-tags does not support finding by tag'); - } + throw new Cache_Exception('Memcached-tags does not support finding by tag'); + } } diff --git a/includes/kohana/modules/cache/classes/kohana/cache/sqlite.php b/includes/kohana/modules/cache/classes/Kohana/Cache/Sqlite.php similarity index 71% rename from includes/kohana/modules/cache/classes/kohana/cache/sqlite.php rename to includes/kohana/modules/cache/classes/Kohana/Cache/Sqlite.php index 70045e5..932704a 100644 --- a/includes/kohana/modules/cache/classes/kohana/cache/sqlite.php +++ b/includes/kohana/modules/cache/classes/Kohana/Cache/Sqlite.php @@ -1,16 +1,16 @@ $e->getMessage())); + throw new Cache_Exception('Failed to create new SQLite caches table with the following error : :error', array(':error' => $e->getMessage())); } } } @@ -68,10 +68,10 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ /** * Retrieve a value based on an id * - * @param string id - * @param string default [Optional] Default value to return if id not found + * @param string $id id + * @param string $default default [Optional] Default value to return if id not found * @return mixed - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function get($id, $default = NULL) { @@ -85,7 +85,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ } catch (PDOException $e) { - throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); } if ( ! $result = $statement->fetch(PDO::FETCH_OBJ)) @@ -105,7 +105,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ { // Disable notices for unserializing $ER = error_reporting(~E_NOTICE); - + // Return the valid cache data $data = unserialize($result->cache); @@ -120,9 +120,9 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ /** * Set a value based on an id. Optionally add tags. * - * @param string id - * @param mixed data - * @param integer lifetime [Optional] + * @param string $id id + * @param mixed $data data + * @param integer $lifetime lifetime [Optional] * @return boolean */ public function set($id, $data, $lifetime = NULL) @@ -133,10 +133,9 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ /** * Delete a cache entry based on id * - * @param string id - * @param integer timeout [Optional] + * @param string $id id * @return boolean - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function delete($id) { @@ -150,7 +149,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ } catch (PDOException $e) { - throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); } return (bool) $statement->rowCount(); @@ -181,13 +180,13 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ /** * Set a value based on an id. Optionally add tags. - * - * @param string id - * @param mixed data - * @param integer lifetime [Optional] - * @param array tags [Optional] + * + * @param string $id id + * @param mixed $data data + * @param integer $lifetime lifetime [Optional] + * @param array $tags tags [Optional] * @return boolean - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function set_with_tags($id, $data, $lifetime = NULL, array $tags = NULL) { @@ -200,7 +199,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ // Setup lifetime if ($lifetime === NULL) { - $lifetime = (0 === Arr::get('default_expire', NULL)) ? 0 : (Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE) + time()); + $lifetime = (0 === Arr::get($this->_config, 'default_expire', NULL)) ? 0 : (Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE) + time()); } else { @@ -208,7 +207,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ } // Prepare statement - // $this->exists() may throw Kohana_Cache_Exception, no need to catch/rethrow + // $this->exists() may throw Cache_Exception, no need to catch/rethrow $statement = $this->exists($id) ? $this->_db->prepare('UPDATE caches SET expiration = :expiration, cache = :cache, tags = :tags WHERE id = :id') : $this->_db->prepare('INSERT INTO caches (id, cache, expiration, tags) VALUES (:id, :cache, :expiration, :tags)'); // Try to insert @@ -218,7 +217,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ } catch (PDOException $e) { - throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); } return (bool) $statement->rowCount(); @@ -227,10 +226,9 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ /** * Delete cache entries based on a tag * - * @param string tag - * @param integer timeout [Optional] + * @param string $tag tag * @return boolean - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function delete_tag($tag) { @@ -253,9 +251,9 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ /** * Find cache entries based on a tag * - * @param string tag + * @param string $tag tag * @return array - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function find($tag) { @@ -272,7 +270,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ } catch (PDOException $e) { - throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); } $result = array(); @@ -308,16 +306,16 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ } catch (PDOException $e) { - throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); } } /** * Tests whether an id exists or not * - * @param string id + * @param string $id id * @return boolean - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ protected function exists($id) { @@ -328,7 +326,7 @@ class Kohana_Cache_Sqlite extends Cache implements Kohana_Cache_Tagging, Kohana_ } catch (PDOExeption $e) { - throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); } return (bool) $statement->fetchAll(); diff --git a/includes/kohana/modules/cache/classes/kohana/cache/tagging.php b/includes/kohana/modules/cache/classes/Kohana/Cache/Tagging.php similarity index 70% rename from includes/kohana/modules/cache/classes/kohana/cache/tagging.php rename to includes/kohana/modules/cache/classes/Kohana/Cache/Tagging.php index 001a224..70d4d63 100644 --- a/includes/kohana/modules/cache/classes/kohana/cache/tagging.php +++ b/includes/kohana/modules/cache/classes/Kohana/Cache/Tagging.php @@ -1,25 +1,25 @@ array( // Driver group * 'driver' => 'wincache', // using wincache driver * ), * ) - * + * * In cases where only one cache group is required, if the group is named `default` there is * no need to pass the group name when instantiating a cache instance. - * + * * #### General cache group configuration settings - * + * * Below are the settings available to all types of cache driver. - * + * * Name | Required | Description * -------------- | -------- | --------------------------------------------------------------- * driver | __YES__ | (_string_) The driver type to use - * + * * ### System requirements - * + * * * Windows XP SP3 with IIS 5.1 and » FastCGI Extension * * Windows Server 2003 with IIS 6.0 and » FastCGI Extension * * Windows Vista SP1 with IIS 7.0 and FastCGI Module @@ -34,11 +34,11 @@ * * Windows Server 2008 R2 with IIS 7.5 and FastCGI Module * * PHP 5.2.X, Non-thread-safe build * * PHP 5.3 X86, Non-thread-safe VC9 build - * + * * @package Kohana/Cache * @category Base * @author Kohana Team - * @copyright (c) 2009-2010 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ class Kohana_Cache_Wincache extends Cache { @@ -47,14 +47,14 @@ class Kohana_Cache_Wincache extends Cache { * Check for existence of the wincache extension This method cannot be invoked externally. The driver must * be instantiated using the `Cache::instance()` method. * - * @param array configuration - * @throws Kohana_Cache_Exception + * @param array $config configuration + * @throws Cache_Exception */ protected function __construct(array $config) { if ( ! extension_loaded('wincache')) { - throw new Kohana_Cache_Exception('PHP wincache extension is not available.'); + throw new Cache_Exception('PHP wincache extension is not available.'); } parent::__construct($config); @@ -62,17 +62,17 @@ class Kohana_Cache_Wincache extends Cache { /** * Retrieve a cached value entry by id. - * + * * // Retrieve cache entry from wincache group * $data = Cache::instance('wincache')->get('foo'); - * + * * // Retrieve cache entry from wincache group and return 'bar' if miss * $data = Cache::instance('wincache')->get('foo', 'bar'); * - * @param string id of cache to entry - * @param string default value to return if cache miss + * @param string $id id of cache to entry + * @param string $default default value to return if cache miss * @return mixed - * @throws Kohana_Cache_Exception + * @throws Cache_Exception */ public function get($id, $default = NULL) { @@ -83,18 +83,18 @@ class Kohana_Cache_Wincache extends Cache { /** * Set a value to cache with id and lifetime - * + * * $data = 'bar'; - * + * * // Set 'bar' to 'foo' in wincache group, using default expiry * Cache::instance('wincache')->set('foo', $data); - * + * * // Set 'bar' to 'foo' in wincache group for 30 seconds * Cache::instance('wincache')->set('foo', $data, 30); * - * @param string id of cache entry - * @param string data to set to cache - * @param integer lifetime in seconds + * @param string $id id of cache entry + * @param string $data data to set to cache + * @param integer $lifetime lifetime in seconds * @return boolean */ public function set($id, $data, $lifetime = NULL) @@ -109,11 +109,11 @@ class Kohana_Cache_Wincache extends Cache { /** * Delete a cache entry based on id - * + * * // Delete 'foo' entry from the wincache group * Cache::instance('wincache')->delete('foo'); * - * @param string id to remove from cache + * @param string $id id to remove from cache * @return boolean */ public function delete($id) @@ -123,11 +123,11 @@ class Kohana_Cache_Wincache extends Cache { /** * Delete all cache entries. - * + * * Beware of using this method when * using shared memory cache systems, as it will wipe every * entry within the system for all clients. - * + * * // Delete all cache entries in the wincache group * Cache::instance('wincache')->delete_all(); * diff --git a/includes/kohana/modules/cache/classes/Kohana/HTTP/Cache.php b/includes/kohana/modules/cache/classes/Kohana/HTTP/Cache.php new file mode 100644 index 0000000..2507f81 --- /dev/null +++ b/includes/kohana/modules/cache/classes/Kohana/HTTP/Cache.php @@ -0,0 +1,503 @@ + FALSE + * ) + * ); + * + * // Create HTTP_Cache with supplied cache engine + * $http_cache = HTTP_Cache::factory(Cache::instance('memcache'), + * array( + * 'allow_private_cache' => FALSE + * ) + * ); + * + * @uses [Cache] + * @param mixed $cache cache engine to use + * @param array $options options to set to this class + * @return HTTP_Cache + */ + public static function factory($cache, array $options = array()) + { + if ( ! $cache instanceof Cache) + { + $cache = Cache::instance($cache); + } + + $options['cache'] = $cache; + + return new HTTP_Cache($options); + } + + /** + * Basic cache key generator that hashes the entire request and returns + * it. This is fine for static content, or dynamic content where user + * specific information is encoded into the request. + * + * // Generate cache key + * $cache_key = HTTP_Cache::basic_cache_key_generator($request); + * + * @param Request $request + * @return string + */ + public static function basic_cache_key_generator(Request $request) + { + $uri = $request->uri(); + $query = $request->query(); + $headers = $request->headers()->getArrayCopy(); + $body = $request->body(); + + return sha1($uri.'?'.http_build_query($query, NULL, '&').'~'.implode('~', $headers).'~'.$body); + } + + /** + * @var Cache cache driver to use for HTTP caching + */ + protected $_cache; + + /** + * @var callback Cache key generator callback + */ + protected $_cache_key_callback; + + /** + * @var boolean Defines whether this client should cache `private` cache directives + * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + */ + protected $_allow_private_cache = FALSE; + + /** + * @var int The timestamp of the request + */ + protected $_request_time; + + /** + * @var int The timestamp of the response + */ + protected $_response_time; + + /** + * Constructor method for this class. Allows dependency injection of the + * required components such as `Cache` and the cache key generator. + * + * @param array $options + */ + public function __construct(array $options = array()) + { + foreach ($options as $key => $value) + { + if (method_exists($this, $key)) + { + $this->$key($value); + } + } + + if ($this->_cache_key_callback === NULL) + { + $this->cache_key_callback('HTTP_Cache::basic_cache_key_generator'); + } + } + + /** + * Executes the supplied [Request] with the supplied [Request_Client]. + * Before execution, the HTTP_Cache adapter checks the request type, + * destructive requests such as `POST`, `PUT` and `DELETE` will bypass + * cache completely and ensure the response is not cached. All other + * Request methods will allow caching, if the rules are met. + * + * @param Request_Client $client client to execute with Cache-Control + * @param Request $request request to execute with client + * @return [Response] + */ + public function execute(Request_Client $client, Request $request, Response $response) + { + if ( ! $this->_cache instanceof Cache) + return $client->execute_request($request, $response); + + // If this is a destructive request, by-pass cache completely + if (in_array($request->method(), array( + HTTP_Request::POST, + HTTP_Request::PUT, + HTTP_Request::DELETE))) + { + // Kill existing caches for this request + $this->invalidate_cache($request); + + $response = $client->execute_request($request, $response); + + $cache_control = HTTP_Header::create_cache_control(array( + 'no-cache', + 'must-revalidate' + )); + + // Ensure client respects destructive action + return $response->headers('cache-control', $cache_control); + } + + // Create the cache key + $cache_key = $this->create_cache_key($request, $this->_cache_key_callback); + + // Try and return cached version + if (($cached_response = $this->cache_response($cache_key, $request)) instanceof Response) + return $cached_response; + + // Start request time + $this->_request_time = time(); + + // Execute the request with the Request client + $response = $client->execute_request($request, $response); + + // Stop response time + $this->_response_time = (time() - $this->_request_time); + + // Cache the response + $this->cache_response($cache_key, $request, $response); + + $response->headers(HTTP_Cache::CACHE_STATUS_KEY, + HTTP_Cache::CACHE_STATUS_MISS); + + return $response; + } + + /** + * Invalidate a cached response for the [Request] supplied. + * This has the effect of deleting the response from the + * [Cache] entry. + * + * @param Request $request Response to remove from cache + * @return void + */ + public function invalidate_cache(Request $request) + { + if (($cache = $this->cache()) instanceof Cache) + { + $cache->delete($this->create_cache_key($request, $this->_cache_key_callback)); + } + + return; + } + + /** + * Getter and setter for the internal caching engine, + * used to cache responses if available and valid. + * + * @param Kohana_Cache $cache engine to use for caching + * @return Kohana_Cache + * @return Kohana_Request_Client + */ + public function cache(Cache $cache = NULL) + { + if ($cache === NULL) + return $this->_cache; + + $this->_cache = $cache; + return $this; + } + + /** + * Gets or sets the [Request_Client::allow_private_cache] setting. + * If set to `TRUE`, the client will also cache cache-control directives + * that have the `private` setting. + * + * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + * @param boolean $setting allow caching of privately marked responses + * @return boolean + * @return [Request_Client] + */ + public function allow_private_cache($setting = NULL) + { + if ($setting === NULL) + return $this->_allow_private_cache; + + $this->_allow_private_cache = (bool) $setting; + return $this; + } + + /** + * Sets or gets the cache key generator callback for this caching + * class. The cache key generator provides a unique hash based on the + * `Request` object passed to it. + * + * The default generator is [HTTP_Cache::basic_cache_key_generator()], which + * serializes the entire `HTTP_Request` into a unique sha1 hash. This will + * provide basic caching for static and simple dynamic pages. More complex + * algorithms can be defined and then passed into `HTTP_Cache` using this + * method. + * + * // Get the cache key callback + * $callback = $http_cache->cache_key_callback(); + * + * // Set the cache key callback + * $http_cache->cache_key_callback('Foo::cache_key'); + * + * // Alternatively, in PHP 5.3 use a closure + * $http_cache->cache_key_callback(function (Request $request) { + * return sha1($request->render()); + * }); + * + * @param callback $callback + * @return mixed + * @throws HTTP_Exception + */ + public function cache_key_callback($callback = NULL) + { + if ($callback === NULL) + return $this->_cache_key_callback; + + if ( ! is_callable($callback)) + throw new Kohana_Exception('cache_key_callback must be callable!'); + + $this->_cache_key_callback = $callback; + return $this; + } + + /** + * Creates a cache key for the request to use for caching + * [Kohana_Response] returned by [Request::execute]. + * + * This is the default cache key generating logic, but can be overridden + * by setting [HTTP_Cache::cache_key_callback()]. + * + * @param Request $request request to create key for + * @param callback $callback optional callback to use instead of built-in method + * @return string + */ + public function create_cache_key(Request $request, $callback = FALSE) + { + if (is_callable($callback)) + return call_user_func($callback, $request); + else + return HTTP_Cache::basic_cache_key_generator($request); + } + + /** + * Controls whether the response can be cached. Uses HTTP + * protocol to determine whether the response can be cached. + * + * @link RFC 2616 http://www.w3.org/Protocols/rfc2616/ + * @param Response $response The Response + * @return boolean + */ + public function set_cache(Response $response) + { + $headers = $response->headers()->getArrayCopy(); + + if ($cache_control = Arr::get($headers, 'cache-control')) + { + // Parse the cache control + $cache_control = HTTP_Header::parse_cache_control($cache_control); + + // If the no-cache or no-store directive is set, return + if (array_intersect($cache_control, array('no-cache', 'no-store'))) + return FALSE; + + // Check for private cache and get out of here if invalid + if ( ! $this->_allow_private_cache AND in_array('private', $cache_control)) + { + if ( ! isset($cache_control['s-maxage'])) + return FALSE; + + // If there is a s-maxage directive we can use that + $cache_control['max-age'] = $cache_control['s-maxage']; + } + + // Check that max-age has been set and if it is valid for caching + if (isset($cache_control['max-age']) AND $cache_control['max-age'] < 1) + return FALSE; + } + + if ($expires = Arr::get($headers, 'expires') AND ! isset($cache_control['max-age'])) + { + // Can't cache things that have expired already + if (strtotime($expires) <= time()) + return FALSE; + } + + return TRUE; + } + + /** + * Caches a [Response] using the supplied [Cache] + * and the key generated by [Request_Client::_create_cache_key]. + * + * If not response is supplied, the cache will be checked for an existing + * one that is available. + * + * @param string $key the cache key to use + * @param Request $request the HTTP Request + * @param Response $response the HTTP Response + * @return mixed + */ + public function cache_response($key, Request $request, Response $response = NULL) + { + if ( ! $this->_cache instanceof Cache) + return FALSE; + + // Check for Pragma: no-cache + if ($pragma = $request->headers('pragma')) + { + if ($pragma == 'no-cache') + return FALSE; + elseif (is_array($pragma) AND in_array('no-cache', $pragma)) + return FALSE; + } + + // If there is no response, lookup an existing cached response + if ($response === NULL) + { + $response = $this->_cache->get($key); + + if ( ! $response instanceof Response) + return FALSE; + + // Do cache hit arithmetic, using fast arithmetic if available + if ($this->_cache instanceof Cache_Arithmetic) + { + $hit_count = $this->_cache->increment(HTTP_Cache::CACHE_HIT_KEY.$key); + } + else + { + $hit_count = $this->_cache->get(HTTP_Cache::CACHE_HIT_KEY.$key); + $this->_cache->set(HTTP_Cache::CACHE_HIT_KEY.$key, ++$hit_count); + } + + // Update the header to have correct HIT status and count + $response->headers(HTTP_Cache::CACHE_STATUS_KEY, + HTTP_Cache::CACHE_STATUS_HIT) + ->headers(HTTP_Cache::CACHE_HIT_KEY, $hit_count); + + return $response; + } + else + { + if (($ttl = $this->cache_lifetime($response)) === FALSE) + return FALSE; + + $response->headers(HTTP_Cache::CACHE_STATUS_KEY, + HTTP_Cache::CACHE_STATUS_SAVED); + + // Set the hit count to zero + $this->_cache->set(HTTP_Cache::CACHE_HIT_KEY.$key, 0); + + return $this->_cache->set($key, $response, $ttl); + } + } + + /** + * Calculates the total Time To Live based on the specification + * RFC 2616 cache lifetime rules. + * + * @param Response $response Response to evaluate + * @return mixed TTL value or false if the response should not be cached + */ + public function cache_lifetime(Response $response) + { + // Get out of here if this cannot be cached + if ( ! $this->set_cache($response)) + return FALSE; + + // Calculate apparent age + if ($date = $response->headers('date')) + { + $apparent_age = max(0, $this->_response_time - strtotime($date)); + } + else + { + $apparent_age = max(0, $this->_response_time); + } + + // Calculate corrected received age + if ($age = $response->headers('age')) + { + $corrected_received_age = max($apparent_age, intval($age)); + } + else + { + $corrected_received_age = $apparent_age; + } + + // Corrected initial age + $corrected_initial_age = $corrected_received_age + $this->request_execution_time(); + + // Resident time + $resident_time = time() - $this->_response_time; + + // Current age + $current_age = $corrected_initial_age + $resident_time; + + // Prepare the cache freshness lifetime + $ttl = NULL; + + // Cache control overrides + if ($cache_control = $response->headers('cache-control')) + { + // Parse the cache control header + $cache_control = HTTP_Header::parse_cache_control($cache_control); + + if (isset($cache_control['max-age'])) + { + $ttl = $cache_control['max-age']; + } + + if (isset($cache_control['s-maxage']) AND isset($cache_control['private']) AND $this->_allow_private_cache) + { + $ttl = $cache_control['s-maxage']; + } + + if (isset($cache_control['max-stale']) AND ! isset($cache_control['must-revalidate'])) + { + $ttl = $current_age + $cache_control['max-stale']; + } + } + + // If we have a TTL at this point, return + if ($ttl !== NULL) + return $ttl; + + if ($expires = $response->headers('expires')) + return strtotime($expires) - $current_age; + + return FALSE; + } + + /** + * Returns the duration of the last request execution. + * Either returns the time of completed requests or + * `FALSE` if the request hasn't finished executing, or + * is yet to be run. + * + * @return mixed + */ + public function request_execution_time() + { + if ($this->_request_time === NULL OR $this->_response_time === NULL) + return FALSE; + + return $this->_response_time - $this->_request_time; + } + +} // End Kohana_HTTP_Cache \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/eaccelerator.php b/includes/kohana/modules/cache/classes/kohana/cache/eaccelerator.php deleted file mode 100644 index 96c97f5..0000000 --- a/includes/kohana/modules/cache/classes/kohana/cache/eaccelerator.php +++ /dev/null @@ -1,133 +0,0 @@ - array( // Driver group - * 'driver' => 'eaccelerator', // using Eaccelerator driver - * ), - * ) - * - * In cases where only one cache group is required, if the group is named `default` there is - * no need to pass the group name when instantiating a cache instance. - * - * #### General cache group configuration settings - * - * Below are the settings available to all types of cache driver. - * - * Name | Required | Description - * -------------- | -------- | --------------------------------------------------------------- - * driver | __YES__ | (_string_) The driver type to use - * - * ### System requirements - * - * * Kohana 3.0.x - * * PHP 5.2.4 or greater - * * Eaccelerator PHP extension - * - * @package Kohana/Cache - * @category Base - * @author Kohana Team - * @copyright (c) 2009-2010 Kohana Team - * @license http://kohanaphp.com/license - */ -class Kohana_Cache_Eaccelerator extends Cache { - - /** - * Check for existence of the eAccelerator extension This method cannot be invoked externally. The driver must - * be instantiated using the `Cache::instance()` method. - * - * @param array configuration - * @throws Kohana_Cache_Exception - */ - protected function __construct(array $config) - { - if ( ! extension_loaded('eaccelerator')) - { - throw new Kohana_Cache_Exception('PHP eAccelerator extension is not available.'); - } - - parent::__construct($config); - } - - /** - * Retrieve a cached value entry by id. - * - * // Retrieve cache entry from eaccelerator group - * $data = Cache::instance('eaccelerator')->get('foo'); - * - * // Retrieve cache entry from eaccelerator group and return 'bar' if miss - * $data = Cache::instance('eaccelerator')->get('foo', 'bar'); - * - * @param string id of cache to entry - * @param string default value to return if cache miss - * @return mixed - * @throws Kohana_Cache_Exception - */ - public function get($id, $default = NULL) - { - return (($data = eaccelerator_get($this->_sanitize_id($id))) === FALSE) ? $default : $data; - } - - /** - * Set a value to cache with id and lifetime - * - * $data = 'bar'; - * - * // Set 'bar' to 'foo' in eaccelerator group, using default expiry - * Cache::instance('eaccelerator')->set('foo', $data); - * - * // Set 'bar' to 'foo' in eaccelerator group for 30 seconds - * Cache::instance('eaccelerator')->set('foo', $data, 30); - * - * @param string id of cache entry - * @param string data to set to cache - * @param integer lifetime in seconds - * @return boolean - */ - public function set($id, $data, $lifetime = NULL) - { - if ($lifetime === NULL) - { - $lifetime = time() + Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE); - } - - return eaccelerator_put($this->_sanitize_id($id), $data, $lifetime); - } - - /** - * Delete a cache entry based on id - * - * // Delete 'foo' entry from the eaccelerator group - * Cache::instance('eaccelerator')->delete('foo'); - * - * @param string id to remove from cache - * @return boolean - */ - public function delete($id) - { - return eaccelerator_rm($this->_sanitize_id($id)); - } - - /** - * Delete all cache entries. - * - * Beware of using this method when - * using shared memory cache systems, as it will wipe every - * entry within the system for all clients. - * - * // Delete all cache entries in the eaccelerator group - * Cache::instance('eaccelerator')->delete_all(); - * - * @return boolean - */ - public function delete_all() - { - return eaccelerator_clean(); - } -} diff --git a/includes/kohana/modules/cache/classes/kohana/cache/xcache.php b/includes/kohana/modules/cache/classes/kohana/cache/xcache.php deleted file mode 100644 index 133f18b..0000000 --- a/includes/kohana/modules/cache/classes/kohana/cache/xcache.php +++ /dev/null @@ -1,84 +0,0 @@ -_sanitize_id($id))) === NULL) ? $default : $data; - } - - /** - * Set a value based on an id. Optionally add tags. - * - * @param string id - * @param string data - * @param integer lifetime [Optional] - * @return boolean - */ - public function set($id, $data, $lifetime = NULL) - { - if (NULL === $lifetime) - { - $lifetime = Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE); - } - - return xcache_set($this->_sanitize_id($id), $data, $lifetime); - } - - /** - * Delete a cache entry based on id - * - * @param string id - * @param integer timeout [Optional] - * @return boolean - */ - public function delete($id) - { - return xcache_unset($this->_sanitize_id($id)); - } - - /** - * Delete all cache entries - * To use this method xcache.admin.enable_auth has to be Off in xcache.ini - * - * @return void - */ - public function delete_all() - { - xcache_clear_cache(XC_TYPE_PHP, 0); - } -} diff --git a/includes/kohana/modules/cache/config/cache.php b/includes/kohana/modules/cache/config/cache.php index 9ec311c..acc567e 100644 --- a/includes/kohana/modules/cache/config/cache.php +++ b/includes/kohana/modules/cache/config/cache.php @@ -1,15 +1,12 @@ array - ( +/* 'memcache' => array( 'driver' => 'memcache', 'default_expire' => 3600, 'compression' => FALSE, // Use Zlib compression (can cause issues with integers) - 'servers' => array - ( - array - ( + 'servers' => array( + 'local' => array( 'host' => 'localhost', // Memcache Server 'port' => 11211, // Memcache port number 'persistent' => FALSE, // Persistent connection @@ -21,15 +18,12 @@ return array ), 'instant_death' => TRUE, // Take server offline immediately on first fail (no retry) ), - 'memcachetag' => array - ( + 'memcachetag' => array( 'driver' => 'memcachetag', 'default_expire' => 3600, 'compression' => FALSE, // Use Zlib compression (can cause issues with integers) - 'servers' => array - ( - array - ( + 'servers' => array( + 'local' => array( 'host' => 'localhost', // Memcache Server 'port' => 11211, // Memcache port number 'persistent' => FALSE, // Persistent connection @@ -41,36 +35,36 @@ return array ), 'instant_death' => TRUE, ), - 'apc' => array - ( + 'apc' => array( 'driver' => 'apc', 'default_expire' => 3600, ), - 'wincache' => array - ( + 'wincache' => array( 'driver' => 'wincache', 'default_expire' => 3600, ), - 'sqlite' => array - ( + 'sqlite' => array( 'driver' => 'sqlite', 'default_expire' => 3600, 'database' => APPPATH.'cache/kohana-cache.sql3', 'schema' => 'CREATE TABLE caches(id VARCHAR(127) PRIMARY KEY, tags VARCHAR(255), expiration INTEGER, cache TEXT)', ), - 'eaccelerator' => array - ( + 'eaccelerator' => array( 'driver' => 'eaccelerator', ), - 'xcache' => array - ( + 'xcache' => array( 'driver' => 'xcache', 'default_expire' => 3600, ), - 'file' => array - ( + 'file' => array( 'driver' => 'file', 'cache_dir' => APPPATH.'cache', 'default_expire' => 3600, + 'ignore_on_delete' => array( + '.gitignore', + '.git', + '.svn' + ) ) -); \ No newline at end of file +*/ +); diff --git a/includes/kohana/modules/cache/config/userguide.php b/includes/kohana/modules/cache/config/userguide.php index 524afc4..0756600 100644 --- a/includes/kohana/modules/cache/config/userguide.php +++ b/includes/kohana/modules/cache/config/userguide.php @@ -17,7 +17,7 @@ return array( 'description' => 'Common interface for caching engines.', // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2010 Kohana Team', + 'copyright' => '© 2008–2012 Kohana Team', ) ) ); \ No newline at end of file diff --git a/includes/kohana/modules/cache/guide/cache.usage.md b/includes/kohana/modules/cache/guide/cache.usage.md index 8a8239b..15d7c52 100644 --- a/includes/kohana/modules/cache/guide/cache.usage.md +++ b/includes/kohana/modules/cache/guide/cache.usage.md @@ -67,7 +67,7 @@ Certain cache drivers support setting values with tags. To set a value to cache $memcache = Cache::instance('memcachetag'); // Test for tagging interface - if ($memcache instanceof Kohana_Cache_Tagging) + if ($memcache instanceof Cache_Tagging) { // Set a value with some tags for 30 seconds $memcache->set('foo', $object, 30, array('snafu', 'stfu', 'fubar')); @@ -79,7 +79,7 @@ Certain cache drivers support setting values with tags. To set a value to cache $memcache->set('foo', $object, 30); } -It is possible to implement custom tagging solutions onto existing or new cache drivers by implementing the [Kohana_Cache_Tagging] interface. Kohana_Cache only applies the interface to drivers that support tagging natively as standard. +It is possible to implement custom tagging solutions onto existing or new cache drivers by implementing the [Cache_Tagging] interface. Kohana_Cache only applies the interface to drivers that support tagging natively as standard. ### Getting a value from cache @@ -115,7 +115,7 @@ It is possible to retrieve values from cache grouped by tag, using the [Cache::f // Find values based on tag return $cache->find('snafu'); } - catch (Kohana_Cache_Exception $e) + catch (Cache_Exception $e) { // Handle gracefully return FALSE; @@ -170,7 +170,7 @@ Some of the caching drivers support deleting by tag. This will remove all the ca $cache = Cache::instance(); // Check for tagging interface - if ($cache instanceof Kohana_Cache_Tagging) + if ($cache instanceof Cache_Tagging) { // Delete all entries by the tag 'snafu' $cache->delete_tag('snafu'); @@ -189,7 +189,7 @@ When not automated, garbage collection is the responsibility of the developer. I $gc = 10; // If the GC probability is a hit - if (rand(0,99) <= $gc and $cache_file instanceof Kohana_Cache_GarbageCollect) + if (rand(0,99) <= $gc and $cache_file instanceof Cache_GarbageCollect) { // Garbage Collect $cache_file->garbage_collect(); @@ -199,10 +199,10 @@ When not automated, garbage collection is the responsibility of the developer. I Kohana Cache comes with two interfaces that are implemented where the drivers support them: - - __[Kohana_Cache_Tagging] for tagging support on cache entries__ + - __[Cache_Tagging] for tagging support on cache entries__ - [Cache_MemcacheTag] - [Cache_Sqlite] - - __[Kohana_Cache_GarbageCollect] for garbage collection with drivers without native support__ + - __[Cache_GarbageCollect] for garbage collection with drivers without native support__ - [Cache_File] - [Cache_Sqlite] @@ -212,7 +212,7 @@ When using interface specific caching features, ensure that code checks for the $cache = Cache::instance(); // Test for Garbage Collection - if ($cache instanceof Kohana_Cache_GarbageCollect) + if ($cache instanceof Cache_GarbageCollect) { // Collect garbage $cache->garbage_collect(); diff --git a/includes/kohana/modules/cache/guide/cache/config.md b/includes/kohana/modules/cache/guide/cache/config.md index a6d428f..450bea8 100644 --- a/includes/kohana/modules/cache/guide/cache/config.md +++ b/includes/kohana/modules/cache/guide/cache/config.md @@ -58,7 +58,7 @@ failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language (can cause issues with integers) 'servers' => array ( - array + 'local' => array ( 'host' => 'localhost', // Memcache Server 'port' => 11211, // Memcache port number @@ -74,7 +74,7 @@ failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language (can cause issues with integers) 'servers' => array ( - array + 'local' => array ( 'host' => 'localhost', // Memcache Server 'port' => 11211, // Memcache port number @@ -102,21 +102,6 @@ failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language tags VARCHAR(255), expiration INTEGER, cache TEXT)', ), -## Eaccelerator settings - - 'eaccelerator' array - ( - 'driver' => 'eaccelerator', - ), - -## Xcache settings - - 'xcache' => array - ( - 'driver' => 'xcache', - 'default_expire' => 3600, - ), - ## File settings 'file' => array @@ -126,6 +111,15 @@ failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language 'default_expire' => 3600, ) +## Wincache settings + + 'wincache' => array + ( + 'driver' => 'wincache', + 'default_expire' => 3600, + ), + + ## Override existing configuration group The following example demonstrates how to override an existing configuration setting, using the config file in `/application/config/cache.php`. @@ -165,4 +159,4 @@ The following example demonstrates how to add a new configuration setting, using 'driver' => 'apc', // Use Memcached as the default driver 'default_expire' => 1000, // Overide default expiry ) - ); \ No newline at end of file + ); diff --git a/includes/kohana/modules/cache/guide/cache/index.md b/includes/kohana/modules/cache/guide/cache/index.md index b93b11d..0df10c9 100644 --- a/includes/kohana/modules/cache/guide/cache/index.md +++ b/includes/kohana/modules/cache/guide/cache/index.md @@ -1,18 +1,17 @@ # About Kohana Cache -[Kohana_Cache] provides a common interface to a variety of caching engines. [Kohana_Cache_Tagging] is +[Kohana_Cache] provides a common interface to a variety of caching engines. [Cache_Tagging] is supported where available natively to the cache system. Kohana Cache supports multiple instances of cache engines through a grouped singleton pattern. ## Supported cache engines * APC ([Cache_Apc]) - * eAccelerator ([Cache_Eaccelerator]) * File ([Cache_File]) * Memcached ([Cache_Memcache]) * Memcached-tags ([Cache_Memcachetag]) * SQLite ([Cache_Sqlite]) - * Xcache ([Cache_Xcache]) + * Wincache ## Introduction to caching @@ -45,13 +44,12 @@ Getting and setting values to cache is very simple when using the _Kohana Cache_ Driver | Storage | Speed | Tags | Distributed | Automatic Garbage Collection | Notes ---------------- | ------------ | --------- | -------- | ----------- | ---------------------------- | ----------------------- APC | __Memory__ | Excellent | No | No | Yes | Widely available PHP opcode caching solution, improves php execution performance -eAccelerator | __Memory__ | Excellent | No | No | Yes | Limited support and no longer developed. Included for legacy systems +Wincache | __Memory__ | Excellent | No | No | Yes | Windows variant of APC File | __Disk__ | Poor | No | No | No | Marginally faster than execution -Memcache (tag) | __Memory__ | Good | No (yes) | Yes | Yes | Generally fast distributed solution, but has a speed hit due to variable network latency +Memcache (tag) | __Memory__ | Good | No (yes) | Yes | Yes | Generally fast distributed solution, but has a speed hit due to variable network latency and serialization Sqlite | __Disk__ | Poor | Yes | No | No | Marginally faster than execution -Xcache | __Memory__ | Excellent | Yes | No | Yes | Very fast memory solution and alternative to APC -It is possible to have hybrid cache solutions that use a combination of the engines above in different contexts. This is supported with _Kohana Cache_ as well. +It is possible to have hybrid cache solutions that use a combination of the engines above in different contexts. This is supported with _Kohana Cache_ as well ## Minimum requirements diff --git a/includes/kohana/modules/cache/guide/cache/usage.md b/includes/kohana/modules/cache/guide/cache/usage.md index 8a8239b..15d7c52 100644 --- a/includes/kohana/modules/cache/guide/cache/usage.md +++ b/includes/kohana/modules/cache/guide/cache/usage.md @@ -67,7 +67,7 @@ Certain cache drivers support setting values with tags. To set a value to cache $memcache = Cache::instance('memcachetag'); // Test for tagging interface - if ($memcache instanceof Kohana_Cache_Tagging) + if ($memcache instanceof Cache_Tagging) { // Set a value with some tags for 30 seconds $memcache->set('foo', $object, 30, array('snafu', 'stfu', 'fubar')); @@ -79,7 +79,7 @@ Certain cache drivers support setting values with tags. To set a value to cache $memcache->set('foo', $object, 30); } -It is possible to implement custom tagging solutions onto existing or new cache drivers by implementing the [Kohana_Cache_Tagging] interface. Kohana_Cache only applies the interface to drivers that support tagging natively as standard. +It is possible to implement custom tagging solutions onto existing or new cache drivers by implementing the [Cache_Tagging] interface. Kohana_Cache only applies the interface to drivers that support tagging natively as standard. ### Getting a value from cache @@ -115,7 +115,7 @@ It is possible to retrieve values from cache grouped by tag, using the [Cache::f // Find values based on tag return $cache->find('snafu'); } - catch (Kohana_Cache_Exception $e) + catch (Cache_Exception $e) { // Handle gracefully return FALSE; @@ -170,7 +170,7 @@ Some of the caching drivers support deleting by tag. This will remove all the ca $cache = Cache::instance(); // Check for tagging interface - if ($cache instanceof Kohana_Cache_Tagging) + if ($cache instanceof Cache_Tagging) { // Delete all entries by the tag 'snafu' $cache->delete_tag('snafu'); @@ -189,7 +189,7 @@ When not automated, garbage collection is the responsibility of the developer. I $gc = 10; // If the GC probability is a hit - if (rand(0,99) <= $gc and $cache_file instanceof Kohana_Cache_GarbageCollect) + if (rand(0,99) <= $gc and $cache_file instanceof Cache_GarbageCollect) { // Garbage Collect $cache_file->garbage_collect(); @@ -199,10 +199,10 @@ When not automated, garbage collection is the responsibility of the developer. I Kohana Cache comes with two interfaces that are implemented where the drivers support them: - - __[Kohana_Cache_Tagging] for tagging support on cache entries__ + - __[Cache_Tagging] for tagging support on cache entries__ - [Cache_MemcacheTag] - [Cache_Sqlite] - - __[Kohana_Cache_GarbageCollect] for garbage collection with drivers without native support__ + - __[Cache_GarbageCollect] for garbage collection with drivers without native support__ - [Cache_File] - [Cache_Sqlite] @@ -212,7 +212,7 @@ When using interface specific caching features, ensure that code checks for the $cache = Cache::instance(); // Test for Garbage Collection - if ($cache instanceof Kohana_Cache_GarbageCollect) + if ($cache instanceof Cache_GarbageCollect) { // Collect garbage $cache->garbage_collect(); diff --git a/includes/kohana/modules/cache/tests/cache/CacheBasicMethodsTest.php b/includes/kohana/modules/cache/tests/cache/CacheBasicMethodsTest.php new file mode 100644 index 0000000..5fdae60 --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/CacheBasicMethodsTest.php @@ -0,0 +1,299 @@ +_cache_driver; + + $this->_cache_driver = $cache; + return $this; + } + + /** + * Data provider for test_set_get() + * + * @return array + */ + public function provider_set_get() + { + $object = new StdClass; + $object->foo = 'foo'; + $object->bar = 'bar'; + + $html_text = << + + + + + + +TESTTEXT; + + return array( + array( + array( + 'id' => 'string', // Key to set to cache + 'value' => 'foobar', // Value to set to key + 'ttl' => 0, // Time to live + 'wait' => FALSE, // Test wait time to let cache expire + 'type' => 'string', // Type test + 'default' => NULL // Default value get should return + ), + 'foobar' + ), + array( + array( + 'id' => 'integer', + 'value' => 101010, + 'ttl' => 0, + 'wait' => FALSE, + 'type' => 'integer', + 'default' => NULL + ), + 101010 + ), + array( + array( + 'id' => 'float', + 'value' => 10.00, + 'ttl' => 0, + 'wait' => FALSE, + 'type' => 'float', + 'default' => NULL + ), + 10.00 + ), + array( + array( + 'id' => 'array', + 'value' => array( + 'key' => 'foo', + 'value' => 'bar' + ), + 'ttl' => 0, + 'wait' => FALSE, + 'type' => 'array', + 'default' => NULL + ), + array( + 'key' => 'foo', + 'value' => 'bar' + ) + ), + array( + array( + 'id' => 'boolean', + 'value' => TRUE, + 'ttl' => 0, + 'wait' => FALSE, + 'type' => 'boolean', + 'default' => NULL + ), + TRUE + ), + array( + array( + 'id' => 'null', + 'value' => NULL, + 'ttl' => 0, + 'wait' => FALSE, + 'type' => 'null', + 'default' => NULL + ), + NULL + ), + array( + array( + 'id' => 'object', + 'value' => $object, + 'ttl' => 0, + 'wait' => FALSE, + 'type' => 'object', + 'default' => NULL + ), + $object + ), + array( + array( + 'id' => 'bar\\ with / troublesome key', + 'value' => 'foo bar snafu', + 'ttl' => 0, + 'wait' => FALSE, + 'type' => 'string', + 'default' => NULL + ), + 'foo bar snafu' + ), + array( + array( + 'id' => 'bar', + 'value' => 'foo', + 'ttl' => 3, + 'wait' => 5, + 'type' => 'null', + 'default' => NULL + ), + NULL + ), + array( + array( + 'id' => 'snafu', + 'value' => 'fubar', + 'ttl' => 3, + 'wait' => 5, + 'type' => 'string', + 'default' => 'something completely different!' + ), + 'something completely different!' + ), + array( + array( + 'id' => 'new line test with HTML', + 'value' => $html_text, + 'ttl' => 10, + 'wait' => FALSE, + 'type' => 'string', + 'default' => NULL, + ), + $html_text + ) + ); + } + + /** + * Tests the [Cache::set()] method, testing; + * + * - The value is cached + * - The lifetime is respected + * - The returned value type is as expected + * - The default not-found value is respected + * + * @dataProvider provider_set_get + * + * @param array data + * @param mixed expected + * @return void + */ + public function test_set_get(array $data, $expected) + { + $cache = $this->cache(); + extract($data); + + $this->assertTrue($cache->set($id, $value, $ttl)); + + if ($wait !== FALSE) + { + // Lets let the cache expire + sleep($wait); + } + + $result = $cache->get($id, $default); + $this->assertEquals($expected, $result); + $this->assertInternalType($type, $result); + + unset($id, $value, $ttl, $wait, $type, $default); + } + + /** + * Tests the [Cache::delete()] method, testing; + * + * - The a cached value is deleted from cache + * - The cache returns a TRUE value upon deletion + * - The cache returns a FALSE value if no value exists to delete + * + * @return void + */ + public function test_delete() + { + // Init + $cache = $this->cache(); + $cache->delete_all(); + + // Test deletion if real cached value + if ( ! $cache->set('test_delete_1', 'This should not be here!', 0)) + { + $this->fail('Unable to set cache value to delete!'); + } + + // Test delete returns TRUE and check the value is gone + $this->assertTrue($cache->delete('test_delete_1')); + $this->assertNull($cache->get('test_delete_1')); + + // Test non-existant cache value returns FALSE if no error + $this->assertFalse($cache->delete('test_delete_1')); + } + + /** + * Tests [Cache::delete_all()] works as specified + * + * @return void + * @uses Kohana_CacheBasicMethodsTest::provider_set_get() + */ + public function test_delete_all() + { + // Init + $cache = $this->cache(); + $data = $this->provider_set_get(); + + foreach ($data as $key => $values) + { + extract($values[0]); + if ( ! $cache->set($id, $value)) + { + $this->fail('Unable to set: '.$key.' => '.$value.' to cache'); + } + unset($id, $value, $ttl, $wait, $type, $default); + } + + // Test delete_all is successful + $this->assertTrue($cache->delete_all()); + + foreach ($data as $key => $values) + { + // Verify data has been purged + $this->assertSame('Cache Deleted!', $cache->get($values[0]['id'], + 'Cache Deleted!')); + } + } + +} // End Kohana_CacheBasicMethodsTest diff --git a/includes/kohana/modules/cache/tests/cache/CacheTest.php b/includes/kohana/modules/cache/tests/cache/CacheTest.php new file mode 100644 index 0000000..a5c7564 --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/CacheTest.php @@ -0,0 +1,242 @@ +load('cache.file')) + { + $base = array( + // Test default group + array( + NULL, + Cache::instance('file') + ), + // Test defined group + array( + 'file', + Cache::instance('file') + ), + ); + } + + + return array( + // Test bad group definition + $base+array( + Kohana_CacheTest::BAD_GROUP_DEFINITION, + 'Failed to load Kohana Cache group: 1010' + ), + ); + } + + /** + * Tests the [Cache::factory()] method behaves as expected + * + * @dataProvider provider_instance + * + * @return void + */ + public function test_instance($group, $expected) + { + if (in_array($group, array( + Kohana_CacheTest::BAD_GROUP_DEFINITION, + ) + )) + { + $this->setExpectedException('Cache_Exception'); + } + + try + { + $cache = Cache::instance($group); + } + catch (Cache_Exception $e) + { + $this->assertSame($expected, $e->getMessage()); + throw $e; + } + + $this->assertInstanceOf(get_class($expected), $cache); + $this->assertSame($expected->config(), $cache->config()); + } + + /** + * Tests that `clone($cache)` will be prevented to maintain singleton + * + * @return void + * @expectedException Cache_Exception + */ + public function test_cloning_fails() + { + if ( ! Kohana::$config->load('cache.file')) + { + $this->markTestSkipped('Unable to load File configuration'); + } + + try + { + $cache_clone = clone(Cache::instance('file')); + } + catch (Cache_Exception $e) + { + $this->assertSame('Cloning of Kohana_Cache objects is forbidden', + $e->getMessage()); + throw $e; + } + } + + /** + * Data provider for test_config + * + * @return array + */ + public function provider_config() + { + return array( + array( + array( + 'server' => 'otherhost', + 'port' => 5555, + 'persistent' => TRUE, + ), + NULL, + Kohana_CacheTest::EXPECT_SELF, + array( + 'server' => 'otherhost', + 'port' => 5555, + 'persistent' => TRUE, + ), + ), + array( + 'foo', + 'bar', + Kohana_CacheTest::EXPECT_SELF, + array( + 'foo' => 'bar' + ) + ), + array( + 'server', + NULL, + NULL, + array() + ), + array( + NULL, + NULL, + array(), + array() + ) + ); + } + + /** + * Tests the config method behaviour + * + * @dataProvider provider_config + * + * @param mixed key value to set or get + * @param mixed value to set to key + * @param mixed expected result from [Cache::config()] + * @param array expected config within cache + * @return void + */ + public function test_config($key, $value, $expected_result, array $expected_config) + { + $cache = $this->getMock('Cache_File', NULL, array(), '', FALSE); + + if ($expected_result === Kohana_CacheTest::EXPECT_SELF) + { + $expected_result = $cache; + } + + $this->assertSame($expected_result, $cache->config($key, $value)); + $this->assertSame($expected_config, $cache->config()); + } + + /** + * Data provider for test_sanitize_id + * + * @return array + */ + public function provider_sanitize_id() + { + return array( + array( + 'foo', + 'foo' + ), + array( + 'foo+-!@', + 'foo+-!@' + ), + array( + 'foo/bar', + 'foo_bar', + ), + array( + 'foo\\bar', + 'foo_bar' + ), + array( + 'foo bar', + 'foo_bar' + ), + array( + 'foo\\bar snafu/stfu', + 'foo_bar_snafu_stfu' + ) + ); + } + + /** + * Tests the [Cache::_sanitize_id()] method works as expected. + * This uses some nasty reflection techniques to access a protected + * method. + * + * @dataProvider provider_sanitize_id + * + * @param string id + * @param string expected + * @return void + */ + public function test_sanitize_id($id, $expected) + { + $cache = $this->getMock('Cache', array( + 'get', + 'set', + 'delete', + 'delete_all' + ), array(array()), + '', FALSE + ); + + $cache_reflection = new ReflectionClass($cache); + $sanitize_id = $cache_reflection->getMethod('_sanitize_id'); + $sanitize_id->setAccessible(TRUE); + + $this->assertSame($expected, $sanitize_id->invoke($cache, $id)); + } +} // End Kohana_CacheTest diff --git a/includes/kohana/modules/cache/tests/cache/FileTest.php b/includes/kohana/modules/cache/tests/cache/FileTest.php new file mode 100644 index 0000000..803258f --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/FileTest.php @@ -0,0 +1,98 @@ +load('cache.file')) + { + $this->markTestSkipped('Unable to load File configuration'); + } + + $this->cache(Cache::instance('file')); + } + + /** + * Tests that ignored files are not removed from file cache + * + * @return void + */ + public function test_ignore_delete_file() + { + $cache = $this->cache(); + $config = Kohana::$config->load('cache')->file; + $file = $config['cache_dir'].'/.gitignore'; + + // Lets pollute the cache folder + file_put_contents($file, 'foobar'); + + $this->assertTrue($cache->delete_all()); + $this->assertTrue(file_exists($file)); + $this->assertEquals('foobar', file_get_contents($file)); + + unlink($file); + } + + /** + * Provider for test_utf8 + * + * @return array + */ + public function provider_utf8() + { + return array( + array( + 'This is â ütf-8 Ӝ☃ string', + 'This is â ütf-8 Ӝ☃ string' + ), + array( + '㆓㆕㆙㆛', + '㆓㆕㆙㆛' + ), + array( + 'அஆஇஈஊ', + 'அஆஇஈஊ' + ) + ); + } + + /** + * Tests the file driver supports utf-8 strings + * + * @dataProvider provider_utf8 + * + * @return void + */ + public function test_utf8($input, $expected) + { + $cache = $this->cache(); + $cache->set('utf8', $input); + + $this->assertSame($expected, $cache->get('utf8')); + } + +} // End Kohana_SqliteTest diff --git a/includes/kohana/modules/cache/tests/cache/KohanaCacheTest.php b/includes/kohana/modules/cache/tests/cache/KohanaCacheTest.php deleted file mode 100644 index 229e7d3..0000000 --- a/includes/kohana/modules/cache/tests/cache/KohanaCacheTest.php +++ /dev/null @@ -1,91 +0,0 @@ -delete_all(); - - self::$test_instance->set('testGet1', 'foo', 3600); - } - - public function tearDown() - { - self::$test_instance->delete_all(); - self::$test_instance = NULL; - } - - /** - * Tests the cache static instance method - */ - public function testInstance() - { - $file_instance = Cache::instance('file'); - $file_instance2 = Cache::instance('file'); - - // Try and load a Cache instance - $this->assertType('Kohana_Cache', Cache::instance()); - $this->assertType('Kohana_Cache_File', $file_instance); - - // Test instances are only initialised once - $this->assertTrue(spl_object_hash($file_instance) == spl_object_hash($file_instance2)); - - // Test the publically accessible Cache instance store - $this->assertTrue(spl_object_hash(Cache::$instances['file']) == spl_object_hash($file_instance)); - - // Get the constructor method - $constructorMethod = new ReflectionMethod($file_instance, '__construct'); - - // Test the constructor for hidden visibility - $this->assertTrue($constructorMethod->isProtected(), '__construct is does not have protected visibility'); - } - - public function testGet() - { - // Try and get a non property - $this->assertNull(self::$test_instance->get('testGet0')); - - // Try and get a non property with default return value - $this->assertEquals('bar', self::$test_instance->get('testGet0', 'bar')); - - // Try and get a real cached property - $this->assertEquals('foo', self::$test_instance->get('testGet1')); - } - - public function testSet() - { - $value = 'foobar'; - $value2 = 'snafu'; - - // Set a new property - $this->assertTrue(self::$test_instance->set('testSet1', $value)); - - // Test the property exists - $this->assertEquals(self::$test_instance->get('testSet1'), $value); - - // Test short set - $this->assertTrue(self::$test_instance->set('testSet2', $value2, 3)); - - // Test the property exists - $this->assertEquals(self::$test_instance->get('testSet2'), $value2); - - // Allow test2 to expire - sleep(4); - - // Test the property has expired - $this->assertNull(self::$test_instance->get('testSet2')); - } - - public function testDelete() - { - - } - - public function testDeleteAll() - { - - } -} \ No newline at end of file diff --git a/includes/kohana/modules/cache/tests/cache/SqliteTest.php b/includes/kohana/modules/cache/tests/cache/SqliteTest.php new file mode 100644 index 0000000..4a9c2ea --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/SqliteTest.php @@ -0,0 +1,44 @@ +markTestSkipped('SQLite PDO PHP Extension is not available'); + } + + if ( ! Kohana::$config->load('cache.sqlite')) + { + $this->markTestIncomplete('Unable to load sqlite configuration'); + } + + $this->cache(Cache::instance('sqlite')); + } + +} // End Kohana_SqliteTest diff --git a/includes/kohana/modules/cache/tests/cache/WincacheTest.php b/includes/kohana/modules/cache/tests/cache/WincacheTest.php new file mode 100644 index 0000000..70e6f79 --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/WincacheTest.php @@ -0,0 +1,39 @@ +markTestSkipped('Wincache PHP Extension is not available'); + } + + $this->cache(Cache::instance('wincache')); + } + +} // End Kohana_WincacheTest diff --git a/includes/kohana/modules/cache/tests/cache/arithmetic/ApcTest.php b/includes/kohana/modules/cache/tests/cache/arithmetic/ApcTest.php new file mode 100644 index 0000000..e1597cd --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/arithmetic/ApcTest.php @@ -0,0 +1,75 @@ +markTestSkipped('APC PHP Extension is not available'); + } + + if (ini_get('apc.enable_cli') != '1') + { + $this->markTestSkipped('Unable to test APC in CLI mode. To fix '. + 'place "apc.enable_cli=1" in your php.ini file'); + } + + $this->cache(Cache::instance('apc')); + } + + /** + * Tests the [Cache::set()] method, testing; + * + * - The value is cached + * - The lifetime is respected + * - The returned value type is as expected + * - The default not-found value is respected + * + * This test doesn't test the TTL as there is a known bug/feature + * in APC that prevents the same request from killing cache on timeout. + * + * @link http://pecl.php.net/bugs/bug.php?id=16814 + * + * @dataProvider provider_set_get + * + * @param array data + * @param mixed expected + * @return void + */ + public function test_set_get(array $data, $expected) + { + if ($data['wait'] !== FALSE) + { + $this->markTestSkipped('Unable to perform TTL test in CLI, see: '. + 'http://pecl.php.net/bugs/bug.php?id=16814 for more info!'); + } + + parent::test_set_get($data, $expected); + } + +} // End Kohana_ApcTest diff --git a/includes/kohana/modules/cache/tests/cache/arithmetic/CacheArithmeticMethods.php b/includes/kohana/modules/cache/tests/cache/arithmetic/CacheArithmeticMethods.php new file mode 100644 index 0000000..1dcc7c7 --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/arithmetic/CacheArithmeticMethods.php @@ -0,0 +1,173 @@ +cache(); + + if ($cache instanceof Cache) + { + $cache->delete_all(); + } + } + + /** + * Provider for test_increment + * + * @return array + */ + public function provider_increment() + { + return array( + array( + 0, + array( + 'id' => 'increment_test_1', + 'step' => 1 + ), + 1 + ), + array( + 1, + array( + 'id' => 'increment_test_2', + 'step' => 1 + ), + 2 + ), + array( + 5, + array( + 'id' => 'increment_test_3', + 'step' => 5 + ), + 10 + ), + array( + NULL, + array( + 'id' => 'increment_test_4', + 'step' => 1 + ), + FALSE + ), + ); + } + + /** + * Test for [Cache_Arithmetic::increment()] + * + * @dataProvider provider_increment + * + * @param integer start state + * @param array increment arguments + * @return void + */ + public function test_increment( + $start_state = NULL, + array $inc_args, + $expected) + { + $cache = $this->cache(); + + if ($start_state !== NULL) + { + $cache->set($inc_args['id'], $start_state, 0); + } + + $this->assertSame( + $expected, + $cache->increment( + $inc_args['id'], + $inc_args['step'] + ) + ); + } + + /** + * Provider for test_decrement + * + * @return array + */ + public function provider_decrement() + { + return array( + array( + 10, + array( + 'id' => 'decrement_test_1', + 'step' => 1 + ), + 9 + ), + array( + 10, + array( + 'id' => 'decrement_test_2', + 'step' => 2 + ), + 8 + ), + array( + 50, + array( + 'id' => 'decrement_test_3', + 'step' => 5 + ), + 45 + ), + array( + NULL, + array( + 'id' => 'decrement_test_4', + 'step' => 1 + ), + FALSE + ), + ); } + + /** + * Test for [Cache_Arithmetic::decrement()] + * + * @dataProvider provider_decrement + * + * @param integer start state + * @param array decrement arguments + * @return void + */ + public function test_decrement( + $start_state = NULL, + array $dec_args, + $expected) + { + $cache = $this->cache(); + + if ($start_state !== NULL) + { + $cache->set($dec_args['id'], $start_state, 0); + } + + $this->assertSame( + $expected, + $cache->decrement( + $dec_args['id'], + $dec_args['step'] + ) + ); + } + +} // End Kohana_CacheArithmeticMethodsTest diff --git a/includes/kohana/modules/cache/tests/cache/arithmetic/MemcacheTest.php b/includes/kohana/modules/cache/tests/cache/arithmetic/MemcacheTest.php new file mode 100644 index 0000000..07cb9ef --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/arithmetic/MemcacheTest.php @@ -0,0 +1,103 @@ +markTestSkipped('Memcache PHP Extension is not available'); + } + if ( ! $config = Kohana::$config->load('cache.memcache')) + { + $this->markTestSkipped('Unable to load Memcache configuration'); + } + + $memcache = new Memcache; + if ( ! $memcache->connect($config['servers']['local']['host'], + $config['servers']['local']['port'])) + { + $this->markTestSkipped('Unable to connect to memcache server @ '. + $config['servers']['local']['host'].':'. + $config['servers']['local']['port']); + } + + if ($memcache->getVersion() === FALSE) + { + $this->markTestSkipped('Memcache server @ '. + $config['servers']['local']['host'].':'. + $config['servers']['local']['port']. + ' not responding!'); + } + + unset($memcache); + + $this->cache(Cache::instance('memcache')); + } + + /** + * Tests that multiple values set with Memcache do not cause unexpected + * results. For accurate results, this should be run with a memcache + * configuration that includes multiple servers. + * + * This is to test #4110 + * + * @link http://dev.kohanaframework.org/issues/4110 + * @return void + */ + public function test_multiple_set() + { + $cache = $this->cache(); + $id_set = 'set_id'; + $ttl = 300; + + $data = array( + 'foobar', + 0, + 1.0, + new stdClass, + array('foo', 'bar' => 1), + TRUE, + NULL, + FALSE + ); + + $previous_set = $cache->get($id_set, NULL); + + foreach ($data as $value) + { + // Use Equals over Sames as Objects will not be equal + $this->assertEquals($previous_set, $cache->get($id_set, NULL)); + $cache->set($id_set, $value, $ttl); + + $previous_set = $value; + } + } + + +} // End Kohana_CacheArithmeticMemcacheTest diff --git a/includes/kohana/modules/cache/tests/cache/request/client/CacheTest.php b/includes/kohana/modules/cache/tests/cache/request/client/CacheTest.php new file mode 100644 index 0000000..11f9a3e --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/request/client/CacheTest.php @@ -0,0 +1,265 @@ +defaults(array( + 'controller' => 'welcome', + 'action' => 'index' + )); + + parent::setUp(); + } + + /** + * Tests the Client does not attempt to load cache if no Cache library + * is present + * + * @return void + */ + public function test_cache_not_called_with_no_cache() + { + $request = new Request('welcome/index'); + $response = new Response; + + $client_mock = $this->getMock('Request_Client_Internal'); + + $request->client($client_mock); + $client_mock->expects($this->exactly(0)) + ->method('execute_request'); + $client_mock->expects($this->once()) + ->method('execute') + ->will($this->returnValue($response)); + + $this->assertSame($response, $request->execute()); + } + + /** + * Tests that the client attempts to load a cached response from the + * cache library, but fails. + * + * @return void + */ + public function test_cache_miss() + { + $route = new Route('welcome/index'); + $route->defaults(array( + 'controller' => 'Kohana_Request_CacheTest_Dummy', + 'action' => 'index', + )); + + $request = new Request('welcome/index', NULL, array($route)); + $cache_mock = $this->_get_cache_mock(); + + $request->client()->cache(HTTP_Cache::factory($cache_mock)); + + $cache_mock->expects($this->once()) + ->method('get') + ->with($request->client()->cache()->create_cache_key($request)) + ->will($this->returnValue(FALSE)); + + $response = $request->client()->execute($request); + + $this->assertSame(HTTP_Cache::CACHE_STATUS_MISS, + $response->headers(HTTP_Cache::CACHE_STATUS_KEY)); + } + + /** + * Tests the client saves a response if the correct headers are set + * + * @return void + */ + public function test_cache_save() + { + $lifetime = 800; + $request = new Request('welcome/index'); + $cache_mock = $this->_get_cache_mock(); + $response = Response::factory(); + + $request->client()->cache(new HTTP_Cache(array( + 'cache' => $cache_mock + ) + )); + + $response->headers('cache-control', 'max-age='.$lifetime); + + $key = $request->client()->cache()->create_cache_key($request); + + $cache_mock->expects($this->at(0)) + ->method('set') + ->with($this->stringEndsWith($key), $this->identicalTo(0)); + + $cache_mock->expects($this->at(1)) + ->method('set') + ->with($this->identicalTo($key), $this->anything(), $this->identicalTo($lifetime)) + ->will($this->returnValue(TRUE)); + + $this->assertTrue( + $request->client()->cache() + ->cache_response($key, $request, $response) + ); + + $this->assertSame(HTTP_Cache::CACHE_STATUS_SAVED, + $response->headers(HTTP_Cache::CACHE_STATUS_KEY)); + } + + /** + * Tests the client handles a cache HIT event correctly + * + * @return void + */ + public function test_cache_hit() + { + $lifetime = 800; + $request = new Request('welcome/index'); + $cache_mock = $this->_get_cache_mock(); + + $request->client()->cache(new HTTP_Cache(array( + 'cache' => $cache_mock + ) + )); + + $response = Response::factory(); + + $response->headers(array( + 'cache-control' => 'max-age='.$lifetime, + HTTP_Cache::CACHE_STATUS_KEY => + HTTP_Cache::CACHE_STATUS_HIT + )); + + $key = $request->client()->cache()->create_cache_key($request); + + $cache_mock->expects($this->exactly(2)) + ->method('get') + ->with($this->stringContains($key)) + ->will($this->returnValue($response)); + + $request->client()->cache()->cache_response($key, $request); + + $this->assertSame(HTTP_Cache::CACHE_STATUS_HIT, + $response->headers(HTTP_Cache::CACHE_STATUS_KEY)); + } + + + /** + * Data provider for test_set_cache + * + * @return array + */ + public function provider_set_cache() + { + return array( + array( + new HTTP_Header(array('cache-control' => 'no-cache')), + array('no-cache' => NULL), + FALSE, + ), + array( + new HTTP_Header(array('cache-control' => 'no-store')), + array('no-store' => NULL), + FALSE, + ), + array( + new HTTP_Header(array('cache-control' => 'max-age=100')), + array('max-age' => '100'), + TRUE + ), + array( + new HTTP_Header(array('cache-control' => 'private')), + array('private' => NULL), + FALSE + ), + array( + new HTTP_Header(array('cache-control' => 'private, max-age=100')), + array('private' => NULL, 'max-age' => '100'), + FALSE + ), + array( + new HTTP_Header(array('cache-control' => 'private, s-maxage=100')), + array('private' => NULL, 's-maxage' => '100'), + TRUE + ), + array( + new HTTP_Header(array( + 'expires' => date('m/d/Y', strtotime('-1 day')), + )), + array(), + FALSE + ), + array( + new HTTP_Header(array( + 'expires' => date('m/d/Y', strtotime('+1 day')), + )), + array(), + TRUE + ), + array( + new HTTP_Header(array()), + array(), + TRUE + ), + ); + } + + /** + * Tests the set_cache() method + * + * @test + * @dataProvider provider_set_cache + * + * @return null + */ + public function test_set_cache($headers, $cache_control, $expected) + { + /** + * Set up a mock response object to test with + */ + $response = $this->getMock('Response'); + + $response->expects($this->any()) + ->method('headers') + ->will($this->returnValue($headers)); + + $request = new Request_Client_Internal; + $request->cache(new HTTP_Cache); + $this->assertEquals($request->cache()->set_cache($response), $expected); + } + + /** + * Returns a mock object for Cache + * + * @return Cache + */ + protected function _get_cache_mock() + { + return $this->getMock('Cache_File', array(), array(), '', FALSE); + } +} // End Kohana_Request_Client_CacheTest + +class Controller_Kohana_Request_CacheTest_Dummy extends Controller +{ + public function action_index() + { + + } +} \ No newline at end of file diff --git a/includes/kohana/modules/cache/tests/phpunit.xml b/includes/kohana/modules/cache/tests/phpunit.xml index 5e3b9c7..c6590be 100644 --- a/includes/kohana/modules/cache/tests/phpunit.xml +++ b/includes/kohana/modules/cache/tests/phpunit.xml @@ -7,7 +7,10 @@ Any options you specify when calling phpunit will override the ones in here --> - + + ./cache + + cache/ diff --git a/includes/kohana/modules/codebench/classes/bench/arrcallback.php b/includes/kohana/modules/codebench/classes/Bench/ArrCallback.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/arrcallback.php rename to includes/kohana/modules/codebench/classes/Bench/ArrCallback.php diff --git a/includes/kohana/modules/codebench/classes/bench/autolinkemails.php b/includes/kohana/modules/codebench/classes/Bench/AutoLinkEmails.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/autolinkemails.php rename to includes/kohana/modules/codebench/classes/Bench/AutoLinkEmails.php diff --git a/includes/kohana/modules/codebench/classes/bench/datespan.php b/includes/kohana/modules/codebench/classes/Bench/DateSpan.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/datespan.php rename to includes/kohana/modules/codebench/classes/Bench/DateSpan.php diff --git a/includes/kohana/modules/codebench/classes/bench/explodelimit.php b/includes/kohana/modules/codebench/classes/Bench/ExplodeLimit.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/explodelimit.php rename to includes/kohana/modules/codebench/classes/Bench/ExplodeLimit.php diff --git a/includes/kohana/modules/codebench/classes/bench/gruberurl.php b/includes/kohana/modules/codebench/classes/Bench/GruberURL.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/gruberurl.php rename to includes/kohana/modules/codebench/classes/Bench/GruberURL.php diff --git a/includes/kohana/modules/codebench/classes/bench/ltrimdigits.php b/includes/kohana/modules/codebench/classes/Bench/LtrimDigits.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/ltrimdigits.php rename to includes/kohana/modules/codebench/classes/Bench/LtrimDigits.php diff --git a/includes/kohana/modules/codebench/classes/bench/mddobaseurl.php b/includes/kohana/modules/codebench/classes/Bench/MDDoBaseURL.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/mddobaseurl.php rename to includes/kohana/modules/codebench/classes/Bench/MDDoBaseURL.php diff --git a/includes/kohana/modules/codebench/classes/bench/mddoimageurl.php b/includes/kohana/modules/codebench/classes/Bench/MDDoImageURL.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/mddoimageurl.php rename to includes/kohana/modules/codebench/classes/Bench/MDDoImageURL.php diff --git a/includes/kohana/modules/codebench/classes/bench/mddoincludeviews.php b/includes/kohana/modules/codebench/classes/Bench/MDDoIncludeViews.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/mddoincludeviews.php rename to includes/kohana/modules/codebench/classes/Bench/MDDoIncludeViews.php diff --git a/includes/kohana/modules/codebench/classes/bench/stripnullbytes.php b/includes/kohana/modules/codebench/classes/Bench/StripNullBytes.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/stripnullbytes.php rename to includes/kohana/modules/codebench/classes/Bench/StripNullBytes.php diff --git a/includes/kohana/modules/codebench/classes/bench/transliterate.php b/includes/kohana/modules/codebench/classes/Bench/Transliterate.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/transliterate.php rename to includes/kohana/modules/codebench/classes/Bench/Transliterate.php diff --git a/includes/kohana/modules/codebench/classes/bench/urlsite.php b/includes/kohana/modules/codebench/classes/Bench/URLSite.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/urlsite.php rename to includes/kohana/modules/codebench/classes/Bench/URLSite.php diff --git a/includes/kohana/modules/codebench/classes/bench/userfuncarray.php b/includes/kohana/modules/codebench/classes/Bench/UserFuncArray.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/userfuncarray.php rename to includes/kohana/modules/codebench/classes/Bench/UserFuncArray.php diff --git a/includes/kohana/modules/codebench/classes/bench/validcolor.php b/includes/kohana/modules/codebench/classes/Bench/ValidColor.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/validcolor.php rename to includes/kohana/modules/codebench/classes/Bench/ValidColor.php diff --git a/includes/kohana/modules/codebench/classes/bench/validurl.php b/includes/kohana/modules/codebench/classes/Bench/ValidURL.php similarity index 100% rename from includes/kohana/modules/codebench/classes/bench/validurl.php rename to includes/kohana/modules/codebench/classes/Bench/ValidURL.php diff --git a/includes/kohana/modules/codebench/classes/codebench.php b/includes/kohana/modules/codebench/classes/Codebench.php similarity index 100% rename from includes/kohana/modules/codebench/classes/codebench.php rename to includes/kohana/modules/codebench/classes/Codebench.php diff --git a/includes/kohana/modules/codebench/classes/controller/codebench.php b/includes/kohana/modules/codebench/classes/Controller/Codebench.php similarity index 82% rename from includes/kohana/modules/codebench/classes/controller/codebench.php rename to includes/kohana/modules/codebench/classes/Controller/Codebench.php index cdd9a70..c940a3c 100644 --- a/includes/kohana/modules/codebench/classes/controller/codebench.php +++ b/includes/kohana/modules/codebench/classes/Controller/Codebench.php @@ -13,12 +13,14 @@ class Controller_Codebench extends Kohana_Controller_Template { // The codebench view public $template = 'codebench'; - public function action_index($class) + public function action_index() { + $class = $this->request->param('class'); + // Convert submitted class name to URI segment if (isset($_POST['class'])) { - $this->request->redirect('codebench/'.trim($_POST['class'])); + throw HTTP_Exception::factory(302)->location('codebench/'.trim($_POST['class'])); } // Pass the class name on to the view diff --git a/includes/kohana/modules/codebench/classes/kohana/codebench.php b/includes/kohana/modules/codebench/classes/Kohana/Codebench.php similarity index 99% rename from includes/kohana/modules/codebench/classes/kohana/codebench.php rename to includes/kohana/modules/codebench/classes/Kohana/Codebench.php index 68601bd..0acfa5c 100644 --- a/includes/kohana/modules/codebench/classes/kohana/codebench.php +++ b/includes/kohana/modules/codebench/classes/Kohana/Codebench.php @@ -47,7 +47,7 @@ abstract class Kohana_Codebench { public function __construct() { // Set the maximum execution time - set_time_limit(Kohana::config('codebench')->max_execution_time); + set_time_limit(Kohana::$config->load('codebench')->max_execution_time); } /** diff --git a/includes/kohana/modules/codebench/config/userguide.php b/includes/kohana/modules/codebench/config/userguide.php index f9e6a14..e6943ac 100644 --- a/includes/kohana/modules/codebench/config/userguide.php +++ b/includes/kohana/modules/codebench/config/userguide.php @@ -17,7 +17,7 @@ return array( 'description' => 'Code benchmarking tool.', // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2010 Kohana Team', + 'copyright' => '© 2008–2012 Kohana Team', ) ) ); \ No newline at end of file diff --git a/includes/kohana/modules/codebench/init.php b/includes/kohana/modules/codebench/init.php index 866cc34..f238cb0 100644 --- a/includes/kohana/modules/codebench/init.php +++ b/includes/kohana/modules/codebench/init.php @@ -3,6 +3,6 @@ // Catch-all route for Codebench classes to run Route::set('codebench', 'codebench(/)') ->defaults(array( - 'controller' => 'codebench', + 'controller' => 'Codebench', 'action' => 'index', 'class' => NULL)); diff --git a/includes/kohana/modules/codebench/views/codebench.php b/includes/kohana/modules/codebench/views/codebench.php index 7b6ef2a..6482340 100644 --- a/includes/kohana/modules/codebench/views/codebench.php +++ b/includes/kohana/modules/codebench/views/codebench.php @@ -111,7 +111,7 @@ } }); - expand_all) { ?> + load('codebench')->expand_all) { ?> // Expand all benchmark details by default $toggle_all.click(); @@ -245,7 +245,7 @@ - Raw output:', Kohana::debug($codebench) ?> + Raw output:', Debug::vars($codebench) ?> diff --git a/includes/kohana/modules/cron/README.markdown b/includes/kohana/modules/cron/README.markdown new file mode 100644 index 0000000..b08b42f --- /dev/null +++ b/includes/kohana/modules/cron/README.markdown @@ -0,0 +1,86 @@ +# Kohana-Cron + +This module provides a way to schedule tasks (jobs) within your Kohana application. + + +## Installation + +Step 1: Download the module into your modules subdirectory. + +Step 2: Enable the module in your bootstrap file: + + /** + * Enable modules. Modules are referenced by a relative or absolute path. + */ + Kohana::modules(array( + 'cron' => MODPATH.'cron', + // 'auth' => MODPATH.'auth', // Basic authentication + // 'codebench' => MODPATH.'codebench', // Benchmarking tool + // 'database' => MODPATH.'database', // Database access + // 'image' => MODPATH.'image', // Image manipulation + // 'orm' => MODPATH.'orm', // Object Relationship Mapping + // 'pagination' => MODPATH.'pagination', // Paging of results + // 'userguide' => MODPATH.'userguide', // User guide and API documentation + )); + + +Step 3: Make sure the settings in `config/cron.php` are correct for your environment. +If not, copy the file to `application/config/cron.php` and change the values accordingly. + + +## Usage + +In its simplest form, a task is a [PHP callback][1] and times at which it should run. +To configure a task call `Cron::set($name, array($frequency, $callback))` where +`$frequency` is a string of date and time fields identical to those found in [crontab][2]. +For example, + + Cron::set('reindex_catalog', array('@daily', 'Catalog::regenerate_index')); + Cron::set('calendar_notifications', array('*/5 * * * *', 'Calendar::send_emails')); + +Configured tasks are run with their appropriate frequency by calling `Cron::run()`. Call +this method in your bootstrap file, and you're done! + + +## Advanced Usage + +A task can also be an instance of `Cron` that extends `next()` and/or `execute()` as +needed. Such a task is configured by calling `Cron::set($name, $instance)`. + +If you have access to the system crontab, you can run Cron less (or more) than once +every request. You will need to modify the lines where the request is handled in your +bootstrap file to prevent extraneous output. The default is: + + /** + * Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO']. + * If no source is specified, the URI will be automatically detected. + */ + echo Request::instance() + ->execute() + ->send_headers() + ->response; + +Change it to: + + if ( ! defined('SUPPRESS_REQUEST')) + { + /** + * Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO']. + * If no source is specified, the URI will be automatically detected. + */ + echo Request::instance() + ->execute() + ->send_headers() + ->response; + } + +Then set up a system cron job to run your application's Cron once a minute: + + * * * * * /usr/bin/php -f /path/to/kohana/modules/cron/run.php + +The included `run.php` should work for most cases, but you are free to call `Cron::run()` +in any way you see fit. + + + [1]: http://php.net/manual/language.pseudo-types.php#language.types.callback + [2]: http://linux.die.net/man/5/crontab diff --git a/includes/kohana/modules/cron/classes/cron.php b/includes/kohana/modules/cron/classes/cron.php new file mode 100644 index 0000000..7f3d275 --- /dev/null +++ b/includes/kohana/modules/cron/classes/cron.php @@ -0,0 +1,10 @@ +load('cron'); + $result = FALSE; + + if (file_exists($config->lock) AND ($stat = @stat($config->lock)) AND time() - $config->window < $stat['mtime']) + { + // Lock exists and has not expired + return $result; + } + + $fh = fopen($config->lock, 'a'); + + if (flock($fh, LOCK_EX)) + { + fseek($fh, 0, SEEK_END); + + if (ftell($fh) === (empty($stat) ? 0 : $stat['size'])) + { + // Current size matches expected size + // Claim the file by changing the size + fwrite($fh, '.'); + + $result = TRUE; + } + + // else, Another process acquired during flock() + } + + fclose($fh); + + return $result; + } + + /** + * Store the timestamps of when jobs should run next + */ + protected static function _save() + { + Kohana::cache("Cron::run()", Cron::$_times, Kohana::$config->load('cron')->window * 2); + } + + /** + * Release the Cron mutex + */ + protected static function _unlock() + { + return @unlink(Kohana::$config->load('cron')->lock); + } + + /** + * @return boolean FALSE when another instance is running + */ + public static function run() + { + if (empty(Cron::$_jobs)) + return TRUE; + + if ( ! Cron::_lock()) + return FALSE; + + try + { + Cron::_load(); + + $now = time(); + $threshold = $now - Kohana::$config->load('cron')->window; + + foreach (Cron::$_jobs as $name => $job) + { + if (empty(Cron::$_times[$name]) OR Cron::$_times[$name] < $threshold) + { + // Expired + + Cron::$_times[$name] = $job->next($now); + + if ($job->next($threshold) < $now) + { + // Within the window + + $job->execute(); + } + } + elseif (Cron::$_times[$name] < $now) + { + // Within the window + + Cron::$_times[$name] = $job->next($now); + + $job->execute(); + } + } + } + catch (Exception $e) {} + + Cron::_save(); + Cron::_unlock(); + + if (isset($e)) + throw $e; + + return TRUE; + } + + protected $_callback; + protected $_period; + + public function __construct($period, $callback) + { + $this->_period = $period; + $this->_callback = $callback; + } + + /** + * Execute this job + */ + public function execute() + { + call_user_func($this->_callback); + } + + /** + * Calculates the next timestamp in this period + * + * @param integer Timestamp from which to calculate + * @return integer Next timestamp in this period + */ + public function next($from) + { + // PHP >= 5.3.0 + //if ($this->_period instanceof DatePeriod) { return; } + //if (is_string($this->_period) AND preg_match('/^P[\dDHMSTWY]+$/', $period)) { $this->_period = new DateInterval($this->_period); } + //if ($this->_period instanceof DateInterval) { return; } + + return $this->_next_crontab($from); + } + + /** + * Calculates the next timestamp of this crontab period + * + * @param integer Timestamp from which to calculate + * @return integer Next timestamp in this period + */ + protected function _next_crontab($from) + { + if (is_string($this->_period)) + { + // Convert string to lists of valid values + + if ($this->_period[0] === '@') + { + switch (substr($this->_period, 1)) + { + case 'annually': + case 'yearly': + // '0 0 1 1 *' + $this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => array(1), 'months' => array(1), 'weekdays' => range(0,6)); + break; + + case 'daily': + case 'midnight': + // '0 0 * * *' + $this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => range(1,31), 'months' => range(1,12), 'weekdays' => range(0,6)); + break; + + case 'hourly': + // '0 * * * *' + $this->_period = array('minutes' => array(0), 'hours' => range(0,23), 'monthdays' => range(1,31), 'months' => range(1,12), 'weekdays' => range(0,6)); + break; + + case 'monthly': + // '0 0 1 * *' + $this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => array(1), 'months' => range(1,12), 'weekdays' => range(0,6)); + break; + + case 'weekly': + // '0 0 * * 0' + $this->_period = array('minutes' => array(0), 'hours' => array(0), 'monthdays' => range(1,31), 'months' => range(1,12), 'weekdays' => array(0)); + break; + } + } + else + { + list($minutes, $hours, $monthdays, $months, $weekdays) = explode(' ', $this->_period); + + $months = strtr(strtolower($months), array( + 'jan' => 1, + 'feb' => 2, + 'mar' => 3, + 'apr' => 4, + 'may' => 5, + 'jun' => 6, + 'jul' => 7, + 'aug' => 8, + 'sep' => 9, + 'oct' => 10, + 'nov' => 11, + 'dec' => 12, + )); + + $weekdays = strtr(strtolower($weekdays), array( + 'sun' => 0, + 'mon' => 1, + 'tue' => 2, + 'wed' => 3, + 'thu' => 4, + 'fri' => 5, + 'sat' => 6, + )); + + $this->_period = array( + 'minutes' => $this->_parse_crontab_field($minutes, 0, 59), + 'hours' => $this->_parse_crontab_field($hours, 0, 23), + 'monthdays' => $this->_parse_crontab_field($monthdays, 1, 31), + 'months' => $this->_parse_crontab_field($months, 1, 12), + 'weekdays' => $this->_parse_crontab_field($weekdays, 0, 7) + ); + + // Ensure Sunday is zero + if (end($this->_period['weekdays']) === 7) + { + array_pop($this->_period['weekdays']); + + if (reset($this->_period['weekdays']) !== 0) + { + array_unshift($this->_period['weekdays'], 0); + } + } + } + } + + $from = getdate($from); + + if ( ! in_array($from['mon'], $this->_period['months'])) + return $this->_next_crontab_month($from); + + if (count($this->_period['weekdays']) === 7) + { + // Day of Week is unrestricted, defer to Day of Month + if ( ! in_array($from['mday'], $this->_period['monthdays'])) + return $this->_next_crontab_monthday($from); + } + elseif (count($this->_period['monthdays']) === 31) + { + // Day of Month is unrestricted, use Day of Week + if ( ! in_array($from['wday'], $this->_period['weekdays'])) + return $this->_next_crontab_weekday($from); + } + else + { + // Both Day of Week and Day of Month are restricted + if ( ! in_array($from['mday'], $this->_period['monthdays']) AND ! in_array($from['wday'], $this->_period['weekdays'])) + return $this->_next_crontab_day($from); + } + + if ( ! in_array($from['hours'], $this->_period['hours'])) + return $this->_next_crontab_hour($from); + + return $this->_next_crontab_minute($from); + } + + /** + * Calculates the first timestamp in the next day of this period when both + * Day of Week and Day of Month are restricted + * + * @uses _next_crontab_month() + * + * @param array Date array from getdate() + * @return integer Timestamp of next restricted Day + */ + protected function _next_crontab_day(array $from) + { + // Calculate effective Day of Month for next Day of Week + + if ($from['wday'] >= end($this->_period['weekdays'])) + { + $next = reset($this->_period['weekdays']) + 7; + } + else + { + foreach ($this->_period['weekdays'] as $next) + { + if ($from['wday'] < $next) + break; + } + } + + $monthday = $from['mday'] + $next - $from['wday']; + + if ($monthday <= (int) date('t', mktime(0, 0, 0, $from['mon'], 1, $from['year']))) + { + // Next Day of Week is in this Month + + if ($from['mday'] >= end($this->_period['monthdays'])) + { + // No next Day of Month, use next Day of Week + $from['mday'] = $monthday; + } + else + { + // Calculate next Day of Month + foreach ($this->_period['monthdays'] as $next) + { + if ($from['mday'] < $next) + break; + } + + // Use earliest day + $from['mday'] = min($monthday, $next); + } + } + else + { + if ($from['mday'] >= end($this->_period['monthdays'])) + { + // No next Day of Month, use next Month + return $this->_next_crontab_month($from); + } + + // Calculate next Day of Month + foreach ($this->_period['monthdays'] as $next) + { + if ($from['mday'] < $next) + break; + } + + // Use next Day of Month + $from['mday'] = $next; + } + + // Use first Hour and first Minute + return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $from['mday'], $from['year']); + } + + /** + * Calculates the first timestamp in the next hour of this period + * + * @uses _next_crontab_day() + * @uses _next_crontab_monthday() + * @uses _next_crontab_weekday() + * + * @param array Date array from getdate() + * @return integer Timestamp of next Hour + */ + protected function _next_crontab_hour(array $from) + { + if ($from['hours'] >= end($this->_period['hours'])) + { + // No next Hour + + if (count($this->_period['weekdays']) === 7) + { + // Day of Week is unrestricted, defer to Day of Month + return $this->_next_crontab_monthday($from); + } + + if (count($this->_period['monthdays']) === 31) + { + // Day of Month is unrestricted, use Day of Week + return $this->_next_crontab_weekday($from); + } + + // Both Day of Week and Day of Month are restricted + return $this->_next_crontab_day($from); + } + + // Calculate next Hour + foreach ($this->_period['hours'] as $next) + { + if ($from['hours'] < $next) + break; + } + + // Use next Hour and first Minute + return mktime($next, reset($this->_period['minutes']), 0, $from['mon'], $from['mday'], $from['year']); + } + + /** + * Calculates the timestamp of the next minute in this period + * + * @uses _next_crontab_hour() + * + * @param array Date array from getdate() + * @return integer Timestamp of next Minute + */ + protected function _next_crontab_minute(array $from) + { + if ($from['minutes'] >= end($this->_period['minutes'])) + { + // No next Minute, use next Hour + return $this->_next_crontab_hour($from); + } + + // Calculate next Minute + foreach ($this->_period['minutes'] as $next) + { + if ($from['minutes'] < $next) + break; + } + + // Use next Minute + return mktime($from['hours'], $next, 0, $from['mon'], $from['mday'], $from['year']); + } + + /** + * Calculates the first timestamp in the next month of this period + * + * @param array Date array from getdate() + * @return integer Timestamp of next Month + */ + protected function _next_crontab_month(array $from) + { + if ($from['mon'] >= end($this->_period['months'])) + { + // No next Month, increment Year and use first Month + ++$from['year']; + $from['mon'] = reset($this->_period['months']); + } + else + { + // Calculate next Month + foreach ($this->_period['months'] as $next) + { + if ($from['mon'] < $next) + break; + } + + // Use next Month + $from['mon'] = $next; + } + + if (count($this->_period['weekdays']) === 7) + { + // Day of Week is unrestricted, use first Day of Month + $from['mday'] = reset($this->_period['monthdays']); + } + else + { + // Calculate Day of Month for the first Day of Week + $indices = array_flip($this->_period['weekdays']); + + $monthday = 1; + $weekday = (int) date('w', mktime(0, 0, 0, $from['mon'], 1, $from['year'])); + + while ( ! isset($indices[$weekday % 7]) AND $monthday < 7) + { + ++$monthday; + ++$weekday; + } + + if (count($this->_period['monthdays']) === 31) + { + // Day of Month is unrestricted, use first Day of Week + $from['mday'] = $monthday; + } + else + { + // Both Day of Month and Day of Week are restricted, use earliest one + $from['mday'] = min($monthday, reset($this->_period['monthdays'])); + } + } + + // Use first Hour and first Minute + return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $from['mday'], $from['year']); + } + + /** + * Calculates the first timestamp in the next day of this period when only + * Day of Month is restricted + * + * @uses _next_crontab_month() + * + * @param array Date array from getdate() + * @return integer Timestamp of next Day of Month + */ + protected function _next_crontab_monthday(array $from) + { + if ($from['mday'] >= end($this->_period['monthdays'])) + { + // No next Day of Month, use next Month + return $this->_next_crontab_month($from); + } + + // Calculate next Day of Month + foreach ($this->_period['monthdays'] as $next) + { + if ($from['mday'] < $next) + break; + } + + // Use next Day of Month, first Hour, and first Minute + return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $next, $from['year']); + } + + /** + * Calculates the first timestamp in the next day of this period when only + * Day of Week is restricted + * + * @uses _next_crontab_month() + * + * @param array Date array from getdate() + * @return integer Timestamp of next Day of Week + */ + protected function _next_crontab_weekday(array $from) + { + // Calculate effective Day of Month for next Day of Week + + if ($from['wday'] >= end($this->_period['weekdays'])) + { + $next = reset($this->_period['weekdays']) + 7; + } + else + { + foreach ($this->_period['weekdays'] as $next) + { + if ($from['wday'] < $next) + break; + } + } + + $monthday = $from['mday'] + $next - $from['wday']; + + if ($monthday > (int) date('t', mktime(0, 0, 0, $from['mon'], 1, $from['year']))) + { + // Next Day of Week is not in this Month, use next Month + return $this->_next_crontab_month($from); + } + + // Use next Day of Week, first Hour, and first Minute + return mktime(reset($this->_period['hours']), reset($this->_period['minutes']), 0, $from['mon'], $monthday, $from['year']); + } + + /** + * Returns a sorted array of all the values indicated in a Crontab field + * @link http://linux.die.net/man/5/crontab + * + * @param string Crontab field + * @param integer Minimum value for this field + * @param integer Maximum value for this field + * @return array + */ + protected function _parse_crontab_field($value, $min, $max) + { + $result = array(); + + foreach (explode(',', $value) as $value) + { + if ($slash = strrpos($value, '/')) + { + $step = (int) substr($value, $slash + 1); + $value = substr($value, 0, $slash); + } + + if ($value === '*') + { + $result = array_merge($result, range($min, $max, $slash ? $step : 1)); + } + elseif ($dash = strpos($value, '-')) + { + $result = array_merge($result, range(max($min, (int) substr($value, 0, $dash)), min($max, (int) substr($value, $dash + 1)), $slash ? $step : 1)); + } + else + { + $value = (int) $value; + + if ($min <= $value AND $value <= $max) + { + $result[] = $value; + } + } + } + + sort($result); + + return array_unique($result); + } + +} diff --git a/includes/kohana/modules/cron/config/cron.php b/includes/kohana/modules/cron/config/cron.php new file mode 100644 index 0000000..80cd3d3 --- /dev/null +++ b/includes/kohana/modules/cron/config/cron.php @@ -0,0 +1,28 @@ + Kohana::$cache_dir.DIRECTORY_SEPARATOR.'cron.lck', + + /** + * Cron does not run EXACTLY when tasks are scheduled. + * A task can be executed up to this many seconds AFTER its scheduled time. + * + * For example, Cron is run at 10:48 and a task was scheduled to execute at + * 10:45, 180 seconds ago. If window is greater than 180, the task will be + * executed. + * + * This value should always be larger than the time it takes to run all + * your tasks. + */ + 'window' => 300, +); diff --git a/includes/kohana/modules/cron/run.php b/includes/kohana/modules/cron/run.php new file mode 100644 index 0000000..6cafdbf --- /dev/null +++ b/includes/kohana/modules/cron/run.php @@ -0,0 +1,22 @@ +next($from); + + $this->assertSame($expected_result, $result); + } + + public function provider_next() + { + return array + ( + array('@annually', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 1, 1, 2010)), + array('@monthly', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 12, 1, 2009)), + array('@weekly', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 11, 22, 2009)), + array('@daily', mktime(8, 45, 0, 11, 19, 2009), mktime(0, 0, 0, 11, 20, 2009)), + array('@hourly', mktime(8, 45, 0, 11, 19, 2009), mktime(9, 0, 0, 11, 19, 2009)), + + array('* * * * *', mktime(8, 45, 0, 11, 19, 2009), mktime(8, 46, 0, 11, 19, 2009)), + + array( + '* * * * 0', // Sundays + mktime(0, 0, 0, 11, 30, 2009), // Monday, Nov 30, 2009 + mktime(0, 0, 0, 12, 6, 2009) // Sunday, Dec 6, 2009 + ), + + array( + '* * 15 * 6', // 15th and Saturdays + mktime(0, 0, 0, 11, 29, 2009), // Sunday, Nov 29, 2009 + mktime(0, 0, 0, 12, 5, 2009) // Saturday, Dec 5, 2009 + ), + + array( + '* * * * 1,5', // Mondays and Fridays + mktime(0, 0, 0, 11, 24, 2009), // Tuesday, Nov 24, 2009 + mktime(0, 0, 0, 11, 27, 2009) // Friday, Nov 27, 2009 + ), + + array( + '* * 15 * 6-7', // 15th, Saturdays, and Sundays + mktime(0, 0, 0, 11, 23, 2009), // Monday, Nov 23, 2009 + mktime(0, 0, 0, 11, 28, 2009) // Saturday, Nov 28, 2009 + ), + + array( + '* * 15,30 * 2', // 15th, 30th, and Tuesdays + mktime(0, 0, 0, 11, 29, 2009), // Sunday, Nov 29, 2009 + mktime(0, 0, 0, 11, 30, 2009) // Monday, Nov 30, 2009 + ), + + array( + '0 0 * * 4', // Midnight on Thursdays + mktime(1, 0, 0, 11, 19, 2009), // 01:00 Thursday, Nov 19, 2009 + mktime(0, 0, 0, 11, 26, 2009) // 00:00 Thursday, Nov 26, 2009 + ), + + array( + '0 0 */2 * 4', // Midnight on odd days and Thursdays + mktime(1, 0, 0, 11, 19, 2009), // 01:00 Thursday, Nov 19, 2009 + mktime(0, 0, 0, 11, 21, 2009) // 00:00 Saturday, Nov 21, 2009 + ), + ); + } +} diff --git a/includes/kohana/modules/database/classes/Config/Database.php b/includes/kohana/modules/database/classes/Config/Database.php new file mode 100644 index 0000000..7acb31b --- /dev/null +++ b/includes/kohana/modules/database/classes/Config/Database.php @@ -0,0 +1,12 @@ +_db_instance = $config['instance']; + } + elseif ($this->_db_instance === NULL) + { + $this->_db_instance = Database::$default; + } + + if (isset($config['table_name'])) + { + $this->_table_name = $config['table_name']; + } + } + + /** + * Tries to load the specificed configuration group + * + * Returns FALSE if group does not exist or an array if it does + * + * @param string $group Configuration group + * @return boolean|array + */ + public function load($group) + { + /** + * Prevents the catch-22 scenario where the database config reader attempts to load the + * database connections details from the database. + * + * @link http://dev.kohanaframework.org/issues/4316 + */ + if ($group === 'database') + return FALSE; + + $query = DB::select('config_key', 'config_value') + ->from($this->_table_name) + ->where('group_name', '=', $group) + ->execute($this->_db_instance); + + return count($query) ? array_map('unserialize', $query->as_array('config_key', 'config_value')) : FALSE; + } +} diff --git a/includes/kohana/modules/database/classes/Kohana/Config/Database/Writer.php b/includes/kohana/modules/database/classes/Kohana/Config/Database/Writer.php new file mode 100644 index 0000000..f6b6738 --- /dev/null +++ b/includes/kohana/modules/database/classes/Kohana/Config/Database/Writer.php @@ -0,0 +1,110 @@ +_loaded_keys[$group] = array_combine(array_keys($config), array_keys($config)); + } + + return $config; + } + + /** + * Writes the passed config for $group + * + * Returns chainable instance on success or throws + * Kohana_Config_Exception on failure + * + * @param string $group The config group + * @param string $key The config key to write to + * @param array $config The configuration to write + * @return boolean + */ + public function write($group, $key, $config) + { + $config = serialize($config); + + // Check to see if we've loaded the config from the table already + if (isset($this->_loaded_keys[$group][$key])) + { + $this->_update($group, $key, $config); + } + else + { + // Attempt to run an insert query + // This may fail if the config key already exists in the table + // and we don't know about it + try + { + $this->_insert($group, $key, $config); + } + catch (Database_Exception $e) + { + // Attempt to run an update instead + $this->_update($group, $key, $config); + } + } + + return TRUE; + } + + /** + * Insert the config values into the table + * + * @param string $group The config group + * @param string $key The config key to write to + * @param array $config The serialized configuration to write + * @return boolean + */ + protected function _insert($group, $key, $config) + { + DB::insert($this->_table_name, array('group_name', 'config_key', 'config_value')) + ->values(array($group, $key, $config)) + ->execute($this->_db_instance); + + return $this; + } + + /** + * Update the config values in the table + * + * @param string $group The config group + * @param string $key The config key to write to + * @param array $config The serialized configuration to write + * @return boolean + */ + protected function _update($group, $key, $config) + { + DB::update($this->_table_name) + ->set(array('config_value' => $config)) + ->where('group_name', '=', $group) + ->where('config_key', '=', $key) + ->execute($this->_db_instance); + + return $this; + } +} diff --git a/includes/kohana/modules/database/classes/kohana/db.php b/includes/kohana/modules/database/classes/Kohana/DB.php similarity index 83% rename from includes/kohana/modules/database/classes/kohana/db.php rename to includes/kohana/modules/database/classes/Kohana/DB.php index b6e513f..77388a9 100644 --- a/includes/kohana/modules/database/classes/kohana/db.php +++ b/includes/kohana/modules/database/classes/Kohana/DB.php @@ -1,4 +1,4 @@ -set(array('login_count' => DB::expr('login_count + 1')))->where('id', '=', $id); * $users = ORM::factory('user')->where(DB::expr("BINARY `hash`"), '=', $hash)->find(); * - * @param string expression + * @param string $string expression + * @param array parameters * @return Database_Expression */ - public static function expr($string) + public static function expr($string, $parameters = array()) { - return new Database_Expression($string); + return new Database_Expression($string, $parameters); } } // End DB diff --git a/includes/kohana/modules/database/classes/kohana/database.php b/includes/kohana/modules/database/classes/Kohana/Database.php similarity index 82% rename from includes/kohana/modules/database/classes/kohana/database.php rename to includes/kohana/modules/database/classes/Kohana/Database.php index d84420a..267d938 100644 --- a/includes/kohana/modules/database/classes/kohana/database.php +++ b/includes/kohana/modules/database/classes/Kohana/Database.php @@ -1,9 +1,9 @@ -$name; + $config = Kohana::$config->load('database')->$name; } if ( ! isset($config['type'])) @@ -75,7 +75,10 @@ abstract class Kohana_Database { $driver = 'Database_'.ucfirst($config['type']); // Create the database connection instance - new $driver($name, $config); + $driver = new $driver($name, $config); + + // Store the database instance + Database::$instances[$name] = $driver; } return Database::$instances[$name]; @@ -105,7 +108,7 @@ abstract class Kohana_Database { * * @return void */ - protected function __construct($name, array $config) + public function __construct($name, array $config) { // Set the instance name $this->_instance = $name; @@ -113,8 +116,10 @@ abstract class Kohana_Database { // Store the config locally $this->_config = $config; - // Store the database instance - Database::$instances[$name] = $this; + if (empty($this->_config['table_prefix'])) + { + $this->_config['table_prefix'] = ''; + } } /** @@ -128,7 +133,7 @@ abstract class Kohana_Database { * * @return void */ - final public function __destruct() + public function __destruct() { $this->disconnect(); } @@ -140,7 +145,7 @@ abstract class Kohana_Database { * * @return string */ - final public function __toString() + public function __toString() { return $this->_instance; } @@ -177,7 +182,7 @@ abstract class Kohana_Database { * $db->set_charset('utf8'); * * @throws Database_Exception - * @param string character set name + * @param string $charset character set name * @return void */ abstract public function set_charset($charset); @@ -191,10 +196,10 @@ abstract class Kohana_Database { * // Make a SELECT query and use "Model_User" for the results * $db->query(Database::SELECT, 'SELECT * FROM users LIMIT 1', 'Model_User'); * - * @param integer Database::SELECT, Database::INSERT, etc - * @param string SQL query - * @param mixed result object class string, TRUE for stdClass, FALSE for assoc array - * @param array object construct parameters for result class + * @param integer $type Database::SELECT, Database::INSERT, etc + * @param string $sql SQL query + * @param mixed $as_object result object class string, TRUE for stdClass, FALSE for assoc array + * @param array $params object construct parameters for result class * @return object Database_Result for SELECT queries * @return array list (insert id, row count) for INSERT queries * @return integer number of affected rows for all other queries @@ -219,7 +224,7 @@ abstract class Kohana_Database { * $db->rollback(); * } * - * @param string transaction mode + * @param string $mode transaction mode * @return boolean */ abstract public function begin($mode = NULL); @@ -250,7 +255,7 @@ abstract class Kohana_Database { * // Get the total number of records in the "users" table * $count = $db->count_records('users'); * - * @param mixed table name string or array(query, alias) + * @param mixed $table table name string or array(query, alias) * @return integer */ public function count_records($table) @@ -267,7 +272,7 @@ abstract class Kohana_Database { * * $db->datatype('char'); * - * @param string SQL data type + * @param string $type SQL data type * @return array */ public function datatype($type) @@ -342,7 +347,7 @@ abstract class Kohana_Database { * // Get all user-related tables * $tables = $db->list_tables('user%'); * - * @param string table to search for + * @param string $like table to search for * @return array */ abstract public function list_tables($like = NULL); @@ -360,9 +365,9 @@ abstract class Kohana_Database { * // Get the columns from a table that doesn't use the table prefix * $columns = $db->list_columns('users', NULL, FALSE); * - * @param string table to get columns from - * @param string column to search for - * @param boolean whether to add the table prefix automatically or not + * @param string $table table to get columns from + * @param string $like column to search for + * @param boolean $add_prefix whether to add the table prefix automatically or not * @return array */ abstract public function list_columns($table, $like = NULL, $add_prefix = TRUE); @@ -373,7 +378,7 @@ abstract class Kohana_Database { * // Returns: array('CHAR', '6') * list($type, $length) = $db->_parse_type('CHAR(6)'); * - * @param string + * @param string $type * @return array list containing the type and length, if any */ protected function _parse_type($type) @@ -385,7 +390,7 @@ abstract class Kohana_Database { } // Closing parenthesis - $close = strpos($type, ')', $open); + $close = strrpos($type, ')', $open); // Length without parentheses $length = substr($type, $open + 1, $close - 1 - $open); @@ -416,11 +421,11 @@ abstract class Kohana_Database { * $db->quote('fred'); // 'fred' * * Objects passed to this function will be converted to strings. - * [Database_Expression] objects will use the value of the expression. + * [Database_Expression] objects will be compiled. * [Database_Query] objects will be compiled and converted to a sub-query. * All other objects will be converted using the `__toString` method. * - * @param mixed any value to quote + * @param mixed $value any value to quote * @return string * @uses Database::escape */ @@ -447,8 +452,8 @@ abstract class Kohana_Database { } elseif ($value instanceof Database_Expression) { - // Use a raw expression - return $value->value(); + // Compile the expression + return $value->compile($this); } else { @@ -480,19 +485,27 @@ abstract class Kohana_Database { * * You can also use SQL methods within identifiers. * - * // The value of "column" will be quoted - * $column = $db->quote_column('COUNT("column")'); + * $column = $db->quote_column(DB::expr('COUNT(`column`)')); * - * @param mixed column name or array(column, alias) + * Objects passed to this function will be converted to strings. + * [Database_Expression] objects will be compiled. + * [Database_Query] objects will be compiled and converted to a sub-query. + * All other objects will be converted using the `__toString` method. + * + * @param mixed $column column name or array(column, alias) * @return string * @uses Database::quote_identifier * @uses Database::table_prefix */ public function quote_column($column) { + // Identifiers are escaped by repeating them + $escaped_identifier = $this->_identifier.$this->_identifier; + if (is_array($column)) { list($column, $alias) = $column; + $alias = str_replace($this->_identifier, $escaped_identifier, $alias); } if ($column instanceof Database_Query) @@ -502,23 +515,20 @@ abstract class Kohana_Database { } elseif ($column instanceof Database_Expression) { - // Use a raw expression - $column = $column->value(); + // Compile the expression + $column = $column->compile($this); } else { // Convert to a string $column = (string) $column; + $column = str_replace($this->_identifier, $escaped_identifier, $column); + if ($column === '*') { return $column; } - elseif (strpos($column, '"') !== FALSE) - { - // Quote the column in FUNC("column") identifiers - $column = preg_replace('/"(.+?)"/e', '$this->quote_column("$1")', $column); - } elseif (strpos($column, '.') !== FALSE) { $parts = explode('.', $column); @@ -562,16 +572,25 @@ abstract class Kohana_Database { * * $table = $db->quote_table($table); * - * @param mixed table name or array(table, alias) + * Objects passed to this function will be converted to strings. + * [Database_Expression] objects will be compiled. + * [Database_Query] objects will be compiled and converted to a sub-query. + * All other objects will be converted using the `__toString` method. + * + * @param mixed $table table name or array(table, alias) * @return string * @uses Database::quote_identifier * @uses Database::table_prefix */ public function quote_table($table) { + // Identifiers are escaped by repeating them + $escaped_identifier = $this->_identifier.$this->_identifier; + if (is_array($table)) { list($table, $alias) = $table; + $alias = str_replace($this->_identifier, $escaped_identifier, $alias); } if ($table instanceof Database_Query) @@ -581,14 +600,16 @@ abstract class Kohana_Database { } elseif ($table instanceof Database_Expression) { - // Use a raw expression - $table = $table->value(); + // Compile the expression + $table = $table->compile($this); } else { // Convert to a string $table = (string) $table; + $table = str_replace($this->_identifier, $escaped_identifier, $table); + if (strpos($table, '.') !== FALSE) { $parts = explode('.', $table); @@ -630,18 +651,22 @@ abstract class Kohana_Database { * Quote a database identifier * * Objects passed to this function will be converted to strings. - * [Database_Expression] objects will use the value of the expression. + * [Database_Expression] objects will be compiled. * [Database_Query] objects will be compiled and converted to a sub-query. * All other objects will be converted using the `__toString` method. * - * @param mixed any identifier + * @param mixed $value any identifier * @return string */ public function quote_identifier($value) { + // Identifiers are escaped by repeating them + $escaped_identifier = $this->_identifier.$this->_identifier; + if (is_array($value)) { list($value, $alias) = $value; + $alias = str_replace($this->_identifier, $escaped_identifier, $alias); } if ($value instanceof Database_Query) @@ -651,14 +676,16 @@ abstract class Kohana_Database { } elseif ($value instanceof Database_Expression) { - // Use a raw expression - $value = $value->value(); + // Compile the expression + $value = $value->compile($this); } else { // Convert to a string $value = (string) $value; + $value = str_replace($this->_identifier, $escaped_identifier, $value); + if (strpos($value, '.') !== FALSE) { $parts = explode('.', $value); @@ -691,7 +718,7 @@ abstract class Kohana_Database { * * $value = $db->escape('any string'); * - * @param string value to quote + * @param string $value value to quote * @return string */ abstract public function escape($value); diff --git a/includes/kohana/modules/database/classes/kohana/database/exception.php b/includes/kohana/modules/database/classes/Kohana/Database/Exception.php similarity index 80% rename from includes/kohana/modules/database/classes/kohana/database/exception.php rename to includes/kohana/modules/database/classes/Kohana/Database/Exception.php index ea2630e..68f709e 100644 --- a/includes/kohana/modules/database/classes/kohana/database/exception.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Exception.php @@ -1,4 +1,4 @@ -_value = $value; + $this->_parameters = $parameters; + } + + /** + * Bind a variable to a parameter. + * + * @param string $param parameter key to replace + * @param mixed $var variable to use + * @return $this + */ + public function bind($param, & $var) + { + $this->_parameters[$param] =& $var; + + return $this; + } + + /** + * Set the value of a parameter. + * + * @param string $param parameter key to replace + * @param mixed $value value to use + * @return $this + */ + public function param($param, $value) + { + $this->_parameters[$param] = $value; + + return $this; + } + + /** + * Add multiple parameter values. + * + * @param array $params list of parameter values + * @return $this + */ + public function parameters(array $params) + { + $this->_parameters = $params + $this->_parameters; + + return $this; + } + + /** + * Get the expression value as a string. + * + * $sql = $expression->value(); + * + * @return string + */ + public function value() + { + return (string) $this->_value; + } + + /** + * Return the value of the expression as a string. + * + * echo $expression; + * + * @return string + * @uses Database_Expression::value + */ + public function __toString() + { + return $this->value(); + } + + /** + * Compile the SQL expression and return it. Replaces any parameters with + * their given values. + * + * @param mixed Database instance or name of instance + * @return string + */ + public function compile($db = NULL) + { + if ( ! is_object($db)) + { + // Get the database instance + $db = Database::instance($db); + } + + $value = $this->value(); + + if ( ! empty($this->_parameters)) + { + // Quote all of the parameter values + $params = array_map(array($db, 'quote'), $this->_parameters); + + // Replace the values in the expression + $value = strtr($value, $params); + } + + return $value; + } + +} // End Database_Expression diff --git a/includes/kohana/modules/database/classes/kohana/database/mysql.php b/includes/kohana/modules/database/classes/Kohana/Database/MySQL.php similarity index 95% rename from includes/kohana/modules/database/classes/kohana/database/mysql.php rename to includes/kohana/modules/database/classes/Kohana/Database/MySQL.php index 4fdbf01..8700904 100644 --- a/includes/kohana/modules/database/classes/kohana/database/mysql.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/MySQL.php @@ -1,4 +1,4 @@ -_connection = mysql_connect($hostname, $username, $password, TRUE); } } - catch (ErrorException $e) + catch (Exception $e) { // No connection exists $this->_connection = NULL; throw new Database_Exception(':error', - array(':error' => mysql_error()), - mysql_errno()); + array(':error' => $e->getMessage()), + $e->getCode()); } // \xFF is a better delimiter, but the PHP driver uses underscore @@ -79,12 +79,25 @@ class Kohana_Database_MySQL extends Database { // Set the character set $this->set_charset($this->_config['charset']); } + + if ( ! empty($this->_config['connection']['variables'])) + { + // Set session variables + $variables = array(); + + foreach ($this->_config['connection']['variables'] as $var => $val) + { + $variables[] = 'SESSION '.$var.' = '.$this->quote($val); + } + + mysql_query('SET '.implode(', ', $variables), $this->_connection); + } } /** * Select the database * - * @param string Database + * @param string $database Database * @return void */ protected function _select_db($database) @@ -157,7 +170,7 @@ class Kohana_Database_MySQL extends Database { // Make sure the database is connected $this->_connection or $this->connect(); - if ( ! empty($this->_config['profiling'])) + if (Kohana::$profiling) { // Benchmark this query for the current instance $benchmark = Profiler::start("Database ({$this->_instance})", $sql); @@ -263,7 +276,7 @@ class Kohana_Database_MySQL extends Database { * * @link http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html * - * @param string Isolation level + * @param string $mode Isolation level * @return boolean */ public function begin($mode = NULL) @@ -284,7 +297,6 @@ class Kohana_Database_MySQL extends Database { /** * Commit a SQL transaction * - * @param string Isolation level * @return boolean */ public function commit() @@ -298,7 +310,6 @@ class Kohana_Database_MySQL extends Database { /** * Rollback a SQL transaction * - * @param string Isolation level * @return boolean */ public function rollback() diff --git a/includes/kohana/modules/database/classes/kohana/database/mysql/result.php b/includes/kohana/modules/database/classes/Kohana/Database/MySQL/Result.php similarity index 96% rename from includes/kohana/modules/database/classes/kohana/database/mysql/result.php rename to includes/kohana/modules/database/classes/Kohana/Database/MySQL/Result.php index bbfa66e..22a5d14 100644 --- a/includes/kohana/modules/database/classes/kohana/database/mysql/result.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/MySQL/Result.php @@ -1,4 +1,4 @@ -_config['connection']); // Force PDO to use exceptions for all errors - $attrs = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; if ( ! empty($persistent)) { // Make the connection persistent - $attrs[PDO::ATTR_PERSISTENT] = TRUE; + $options[PDO::ATTR_PERSISTENT] = TRUE; } try { // Create a new PDO connection - $this->_connection = new PDO($dsn, $username, $password, $attrs); + $this->_connection = new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { @@ -60,12 +60,51 @@ class Kohana_Database_PDO extends Database { array(':error' => $e->getMessage()), $e->getCode()); } + } - if ( ! empty($this->_config['charset'])) - { - // Set the character set - $this->set_charset($this->_config['charset']); - } + /** + * Create or redefine a SQL aggregate function. + * + * [!!] Works only with SQLite + * + * @link http://php.net/manual/function.pdo-sqlitecreateaggregate + * + * @param string $name Name of the SQL function to be created or redefined + * @param callback $step Called for each row of a result set + * @param callback $final Called after all rows of a result set have been processed + * @param integer $arguments Number of arguments that the SQL function takes + * + * @return boolean + */ + public function create_aggregate($name, $step, $final, $arguments = -1) + { + $this->_connection or $this->connect(); + + return $this->_connection->sqliteCreateAggregate( + $name, $step, $final, $arguments + ); + } + + /** + * Create or redefine a SQL function. + * + * [!!] Works only with SQLite + * + * @link http://php.net/manual/function.pdo-sqlitecreatefunction + * + * @param string $name Name of the SQL function to be created or redefined + * @param callback $callback Callback which implements the SQL function + * @param integer $arguments Number of arguments that the SQL function takes + * + * @return boolean + */ + public function create_function($name, $callback, $arguments = -1) + { + $this->_connection or $this->connect(); + + return $this->_connection->sqliteCreateFunction( + $name, $callback, $arguments + ); } public function disconnect() @@ -79,9 +118,9 @@ class Kohana_Database_PDO extends Database { public function set_charset($charset) { // Make sure the database is connected - $this->_connection or $this->connect(); + $this->_connection OR $this->connect(); - // Execute a raw SET NAMES query + // This SQL-92 syntax is not supported by all drivers $this->_connection->exec('SET NAMES '.$this->quote($charset)); } @@ -90,7 +129,7 @@ class Kohana_Database_PDO extends Database { // Make sure the database is connected $this->_connection or $this->connect(); - if ( ! empty($this->_config['profiling'])) + if (Kohana::$profiling) { // Benchmark this query for the current instance $benchmark = Profiler::start("Database ({$this->_instance})", $sql); diff --git a/includes/kohana/modules/database/classes/kohana/database/query.php b/includes/kohana/modules/database/classes/Kohana/Database/Query.php similarity index 65% rename from includes/kohana/modules/database/classes/kohana/database/query.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query.php index f08f3c1..480e41b 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query.php @@ -1,6 +1,6 @@ -_force_execute = $force; $this->_lifetime = $lifetime; return $this; @@ -106,7 +111,8 @@ class Kohana_Database_Query { /** * Returns results as objects * - * @param string classname or TRUE for stdClass + * @param string $class classname or TRUE for stdClass + * @param array $params * @return $this */ public function as_object($class = TRUE, array $params = NULL) @@ -125,8 +131,8 @@ class Kohana_Database_Query { /** * Set the value of a parameter in the query. * - * @param string parameter key to replace - * @param mixed value to use + * @param string $param parameter key to replace + * @param mixed $value value to use * @return $this */ public function param($param, $value) @@ -140,8 +146,8 @@ class Kohana_Database_Query { /** * Bind a variable to a parameter in the query. * - * @param string parameter key to replace - * @param mixed variable to use + * @param string $param parameter key to replace + * @param mixed $var variable to use * @return $this */ public function bind($param, & $var) @@ -155,7 +161,7 @@ class Kohana_Database_Query { /** * Add multiple parameters to the query. * - * @param array list of parameters + * @param array $params list of parameters * @return $this */ public function parameters(array $params) @@ -170,11 +176,17 @@ class Kohana_Database_Query { * Compile the SQL query and return it. Replaces any parameters with their * given values. * - * @param object Database instance + * @param mixed $db Database instance or name of instance * @return string */ - public function compile(Database $db) + public function compile($db = NULL) { + if ( ! is_object($db)) + { + // Get the database instance + $db = Database::instance($db); + } + // Import the SQL locally $sql = $this->_sql; @@ -193,12 +205,14 @@ class Kohana_Database_Query { /** * Execute the current query on the given database. * - * @param mixed Database instance or name of instance + * @param mixed $db Database instance or name of instance + * @param string result object classname, TRUE for stdClass or FALSE for array + * @param array result object constructor arguments * @return object Database_Result for SELECT queries * @return mixed the insert id for INSERT queries * @return integer number of affected rows for all other queries */ - public function execute($db = NULL) + public function execute($db = NULL, $as_object = NULL, $object_params = NULL) { if ( ! is_object($db)) { @@ -206,6 +220,16 @@ class Kohana_Database_Query { $db = Database::instance($db); } + if ($as_object === NULL) + { + $as_object = $this->_as_object; + } + + if ($object_params === NULL) + { + $object_params = $this->_object_params; + } + // Compile the SQL query $sql = $this->compile($db); @@ -214,17 +238,19 @@ class Kohana_Database_Query { // Set the cache key based on the database instance name and SQL $cache_key = 'Database::query("'.$db.'", "'.$sql.'")'; - if (! is_null($result = Kohana::cache($cache_key, NULL, $this->_lifetime))) + // Read the cache first to delete a possible hit with lifetime <= 0 + if (($result = Kohana::cache($cache_key, NULL, $this->_lifetime)) !== NULL + AND ! $this->_force_execute) { // Return a cached result - return new Database_Result_Cached($result, $sql, $this->_as_object, $this->_object_params); + return new Database_Result_Cached($result, $sql, $as_object, $object_params); } } // Execute the query - $result = $db->query($this->_type, $sql, $this->_as_object, $this->_object_params); + $result = $db->query($this->_type, $sql, $as_object, $object_params); - if (isset($cache_key)) + if (isset($cache_key) AND $this->_lifetime > 0) { // Cache the result array Kohana::cache($cache_key, $result->as_array(), $this->_lifetime); diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder.php b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder.php similarity index 72% rename from includes/kohana/modules/database/classes/kohana/database/query/builder.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query/Builder.php index 8d36e79..3e1bdb5 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query/builder.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder.php @@ -1,4 +1,4 @@ -quote($value); - } elseif ((is_string($value) AND array_key_exists($value, $this->_parameters)) === FALSE) { // Quote the value, it is not a parameter @@ -122,8 +118,16 @@ abstract class Kohana_Database_Query_Builder extends Database_Query { if ($column) { - // Apply proper quoting to the column - $column = $db->quote_column($column); + if (is_array($column)) + { + // Use the column name + $column = $db->quote_identifier(reset($column)); + } + else + { + // Apply proper quoting to the column + $column = $db->quote_column($column); + } } // Append the statement to the query @@ -140,8 +144,8 @@ abstract class Kohana_Database_Query_Builder extends Database_Query { /** * Compiles an array of set values into an SQL partial. Used for UPDATE. * - * @param object Database instance - * @param array updated values + * @param object $db Database instance + * @param array $values updated values * @return string */ protected function _compile_set(Database $db, array $values) @@ -167,11 +171,41 @@ abstract class Kohana_Database_Query_Builder extends Database_Query { return implode(', ', $set); } + /** + * Compiles an array of GROUP BY columns into an SQL partial. + * + * @param object $db Database instance + * @param array $columns + * @return string + */ + protected function _compile_group_by(Database $db, array $columns) + { + $group = array(); + + foreach ($columns as $column) + { + if (is_array($column)) + { + // Use the column alias + $column = $db->quote_identifier(end($column)); + } + else + { + // Apply proper quoting to the column + $column = $db->quote_column($column); + } + + $group[] = $column; + } + + return 'GROUP BY '.implode(', ', $group); + } + /** * Compiles an array of ORDER BY statements into an SQL partial. * - * @param object Database instance - * @param array sorting columns + * @param object $db Database instance + * @param array $columns sorting columns * @return string */ protected function _compile_order_by(Database $db, array $columns) @@ -181,19 +215,24 @@ abstract class Kohana_Database_Query_Builder extends Database_Query { { list ($column, $direction) = $group; - if ($direction) + if (is_array($column)) { - // Make the direction uppercase - $direction = strtoupper($direction); + // Use the column alias + $column = $db->quote_identifier(end($column)); } - - if ($column) + else { - // Quote the column, if it has a value + // Apply proper quoting to the column $column = $db->quote_column($column); } - $sort[] = trim($column.' '.$direction); + if ($direction) + { + // Make the direction uppercase + $direction = ' '.strtoupper($direction); + } + + $sort[] = $column.$direction; } return 'ORDER BY '.implode(', ', $sort); diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/delete.php b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Delete.php similarity index 79% rename from includes/kohana/modules/database/classes/kohana/database/query/builder/delete.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Delete.php index b7dec29..0bc4761 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query/builder/delete.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Delete.php @@ -1,4 +1,4 @@ -quote_table($this->_table); diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/insert.php b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Insert.php similarity index 84% rename from includes/kohana/modules/database/classes/kohana/database/query/builder/insert.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Insert.php index 525fbfe..aa3c807 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query/builder/insert.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Insert.php @@ -1,4 +1,4 @@ -quote_table($this->_table); diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/join.php b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Join.php similarity index 80% rename from includes/kohana/modules/database/classes/kohana/database/query/builder/join.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Join.php index 03448c7..10402f6 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query/builder/join.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Join.php @@ -1,4 +1,4 @@ -_type) { $sql = strtoupper($this->_type).' JOIN'; diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/select.php b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Select.php similarity index 79% rename from includes/kohana/modules/database/classes/kohana/database/query/builder/select.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Select.php index 4c45624..3492a71 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query/builder/select.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Select.php @@ -1,4 +1,4 @@ -_union []= array('select' => $select, 'all' => $all); return $this; - } + } /** * Start returning results after "OFFSET ..." * - * @param integer starting result number + * @param integer $number starting result number or NULL to reset * @return $this */ public function offset($number) { - $this->_offset = (int) $number; + $this->_offset = $number; return $this; } @@ -321,11 +317,17 @@ class Kohana_Database_Query_Builder_Select extends Database_Query_Builder_Where /** * Compile the SQL query and return it. * - * @param object Database instance + * @param mixed $db Database instance or name of instance * @return string */ - public function compile(Database $db) + public function compile($db = NULL) { + if ( ! is_object($db)) + { + // Get the database instance + $db = Database::instance($db); + } + // Callback to quote columns $quote_column = array($db, 'quote_column'); @@ -372,8 +374,8 @@ class Kohana_Database_Query_Builder_Select extends Database_Query_Builder_Where if ( ! empty($this->_group_by)) { - // Add sorting - $query .= ' GROUP BY '.implode(', ', array_map($quote_column, $this->_group_by)); + // Add grouping + $query .= ' '.$this->_compile_group_by($db, $this->_group_by); } if ( ! empty($this->_having)) @@ -399,7 +401,7 @@ class Kohana_Database_Query_Builder_Select extends Database_Query_Builder_Where // Add offsets $query .= ' OFFSET '.$this->_offset; } - + if ( ! empty($this->_union)) { foreach ($this->_union as $u) { @@ -442,4 +444,3 @@ class Kohana_Database_Query_Builder_Select extends Database_Query_Builder_Where } } // End Database_Query_Select - diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/update.php b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Update.php similarity index 78% rename from includes/kohana/modules/database/classes/kohana/database/query/builder/update.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Update.php index 4658e6a..e6e3da5 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query/builder/update.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Update.php @@ -1,4 +1,4 @@ - value) list + * @param array $pairs associative (column => value) list * @return $this */ public function set(array $pairs) @@ -66,8 +66,8 @@ class Kohana_Database_Query_Builder_Update extends Database_Query_Builder_Where /** * Set the value of a single column. * - * @param mixed table name or array($table, $alias) or object - * @param mixed column value + * @param mixed $column table name or array($table, $alias) or object + * @param mixed $value column value * @return $this */ public function value($column, $value) @@ -80,11 +80,17 @@ class Kohana_Database_Query_Builder_Update extends Database_Query_Builder_Where /** * Compile the SQL query and return it. * - * @param object Database instance + * @param mixed $db Database instance or name of instance * @return string */ - public function compile(Database $db) + public function compile($db = NULL) { + if ( ! is_object($db)) + { + // Get the database instance + $db = Database::instance($db); + } + // Start an update query $query = 'UPDATE '.$db->quote_table($this->_table); diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/where.php b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Where.php similarity index 65% rename from includes/kohana/modules/database/classes/kohana/database/query/builder/where.php rename to includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Where.php index aeece75..58f6b5d 100644 --- a/includes/kohana/modules/database/classes/kohana/database/query/builder/where.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Query/Builder/Where.php @@ -1,4 +1,4 @@ -_where); + + if ($group AND reset($group) === '(') + { + array_pop($this->_where); + + return $this; + } + + return $this->where_close(); + } + + /** + * Closes an open "WHERE (...)" grouping. * * @return $this */ @@ -119,7 +139,7 @@ abstract class Kohana_Database_Query_Builder_Where extends Database_Query_Builde } /** - * Closes an open "OR WHERE (...)" grouping. + * Closes an open "WHERE (...)" grouping. * * @return $this */ @@ -133,8 +153,8 @@ abstract class Kohana_Database_Query_Builder_Where extends Database_Query_Builde /** * Applies sorting with "ORDER BY ..." * - * @param mixed column name or array($column, $alias) or object - * @param string direction of sorting + * @param mixed $column column name or array($column, $alias) or object + * @param string $direction direction of sorting * @return $this */ public function order_by($column, $direction = NULL) @@ -147,12 +167,12 @@ abstract class Kohana_Database_Query_Builder_Where extends Database_Query_Builde /** * Return up to "LIMIT ..." results * - * @param integer maximum results to return + * @param integer $number maximum results to return or NULL to reset * @return $this */ public function limit($number) { - $this->_limit = (int) $number; + $this->_limit = $number; return $this; } diff --git a/includes/kohana/modules/database/classes/kohana/database/result.php b/includes/kohana/modules/database/classes/Kohana/Database/Result.php similarity index 91% rename from includes/kohana/modules/database/classes/kohana/database/result.php rename to includes/kohana/modules/database/classes/Kohana/Database/Result.php index 6ddd458..3c3284f 100644 --- a/includes/kohana/modules/database/classes/kohana/database/result.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Result.php @@ -1,4 +1,4 @@ - "name" * $rows = $result->as_array('id', 'name'); * - * @param string column for associative keys - * @param string column for values + * @param string $key column for associative keys + * @param string $value column for values * @return array */ public function as_array($key = NULL, $value = NULL) @@ -175,8 +177,8 @@ abstract class Kohana_Database_Result implements Countable, Iterator, SeekableIt * // Get the "id" value * $id = $result->get('id'); * - * @param string column to get - * @param mixed default value if the column does not exist + * @param string $name column to get + * @param mixed $default default value if the column does not exist * @return mixed */ public function get($name, $default = NULL) @@ -217,6 +219,7 @@ abstract class Kohana_Database_Result implements Countable, Iterator, SeekableIt * // Row 10 exists * } * + * @param int $offset * @return boolean */ public function offsetExists($offset) @@ -229,6 +232,7 @@ abstract class Kohana_Database_Result implements Countable, Iterator, SeekableIt * * $row = $result[10]; * + * @param int $offset * @return mixed */ public function offsetGet($offset) @@ -244,6 +248,8 @@ abstract class Kohana_Database_Result implements Countable, Iterator, SeekableIt * * [!!] You cannot modify a database result. * + * @param int $offset + * @param mixed $value * @return void * @throws Kohana_Exception */ @@ -257,6 +263,7 @@ abstract class Kohana_Database_Result implements Countable, Iterator, SeekableIt * * [!!] You cannot modify a database result. * + * @param int $offset * @return void * @throws Kohana_Exception */ diff --git a/includes/kohana/modules/database/classes/kohana/database/result/cached.php b/includes/kohana/modules/database/classes/Kohana/Database/Result/Cached.php similarity index 94% rename from includes/kohana/modules/database/classes/kohana/database/result/cached.php rename to includes/kohana/modules/database/classes/Kohana/Database/Result/Cached.php index ee8659a..8af0854 100644 --- a/includes/kohana/modules/database/classes/kohana/database/result/cached.php +++ b/includes/kohana/modules/database/classes/Kohana/Database/Result/Cached.php @@ -1,4 +1,4 @@ -_db = $db; } + elseif ( ! $this->_db) + { + // Use the default name + $this->_db = Database::$default; + } if (is_string($this->_db)) { diff --git a/includes/kohana/modules/database/classes/kohana/session/database.php b/includes/kohana/modules/database/classes/Kohana/Session/Database.php similarity index 95% rename from includes/kohana/modules/database/classes/kohana/session/database.php rename to includes/kohana/modules/database/classes/Kohana/Session/Database.php index 1a381cd..34abbbd 100644 --- a/includes/kohana/modules/database/classes/kohana/session/database.php +++ b/includes/kohana/modules/database/classes/Kohana/Session/Database.php @@ -1,4 +1,4 @@ -_regenerate(); + + return TRUE; + } + protected function _destroy() { if ($this->_update_id === NULL) diff --git a/includes/kohana/modules/database/classes/model/database.php b/includes/kohana/modules/database/classes/Model/Database.php similarity index 51% rename from includes/kohana/modules/database/classes/model/database.php rename to includes/kohana/modules/database/classes/Model/Database.php index e26ea12..3b6e609 100644 --- a/includes/kohana/modules/database/classes/model/database.php +++ b/includes/kohana/modules/database/classes/Model/Database.php @@ -1,3 +1,3 @@ -_database_instance = $config['instance']; - } - - if (isset($config['table'])) - { - $this->_database_table = $config['table']; - } - - parent::__construct(); - } - - /** - * Query the configuration table for all values for this group and - * unserialize each of the values. - * - * @param string group name - * @param array configuration array - * @return $this clone of the current object - */ - public function load($group, array $config = NULL) - { - if ($config === NULL AND $group !== 'database') - { - // Load all of the configuration values for this group - $query = DB::select('config_key', 'config_value') - ->from($this->_database_table) - ->where('group_name', '=', $group) - ->execute($this->_database_instance); - - if (count($query) > 0) - { - // Unserialize the configuration values - $config = array_map('unserialize', $query->as_array('config_key', 'config_value')); - } - } - - return parent::load($group, $config); - } - - /** - * Overload setting offsets to insert or update the database values as - * changes occur. - * - * @param string array key - * @param mixed new value - * @return mixed - */ - public function offsetSet($key, $value) - { - if ( ! $this->offsetExists($key)) - { - // Insert a new value - DB::insert($this->_database_table, array('group_name', 'config_key', 'config_value')) - ->values(array($this->_configuration_group, $key, serialize($value))) - ->execute($this->_database_instance); - } - elseif ($this->offsetGet($key) !== $value) - { - // Update the value - DB::update($this->_database_table) - ->value('config_value', serialize($value)) - ->where('group_name', '=', $this->_configuration_group) - ->where('config_key', '=', $key) - ->execute($this->_database_instance); - } - - return parent::offsetSet($key, $value); - } - -} // End Kohana_Config_Database diff --git a/includes/kohana/modules/database/classes/kohana/database/expression.php b/includes/kohana/modules/database/classes/kohana/database/expression.php deleted file mode 100644 index 90b8100..0000000 --- a/includes/kohana/modules/database/classes/kohana/database/expression.php +++ /dev/null @@ -1,62 +0,0 @@ -_value = $value; - } - - /** - * Get the expression value as a string. - * - * $sql = $expression->value(); - * - * @return string - */ - public function value() - { - return (string) $this->_value; - } - - /** - * Return the value of the expression as a string. - * - * echo $expression; - * - * @return string - * @uses Database_Expression::value - */ - public function __toString() - { - return $this->value(); - } - -} // End Database_Expression diff --git a/includes/kohana/modules/database/classes/session/database.php b/includes/kohana/modules/database/classes/session/database.php deleted file mode 100644 index 1e75223..0000000 --- a/includes/kohana/modules/database/classes/session/database.php +++ /dev/null @@ -1,3 +0,0 @@ - array ( - 'type' => 'mysql', + 'type' => 'MySQL', 'connection' => array( /** * The following options are available for MySQL: @@ -14,6 +14,7 @@ return array * string username database username * string password database password * boolean persistent use persistent connections? + * array variables system variables as "key => value" pairs * * Ports and sockets may be appended to the hostname. */ @@ -26,10 +27,9 @@ return array 'table_prefix' => '', 'charset' => 'utf8', 'caching' => FALSE, - 'profiling' => TRUE, ), 'alternate' => array( - 'type' => 'pdo', + 'type' => 'PDO', 'connection' => array( /** * The following options are available for PDO: @@ -52,6 +52,5 @@ return array 'table_prefix' => '', 'charset' => 'utf8', 'caching' => FALSE, - 'profiling' => TRUE, ), -); \ No newline at end of file +); diff --git a/includes/kohana/modules/database/config/session.php b/includes/kohana/modules/database/config/session.php index a4229c7..58263ae 100644 --- a/includes/kohana/modules/database/config/session.php +++ b/includes/kohana/modules/database/config/session.php @@ -1,4 +1,4 @@ - array( diff --git a/includes/kohana/modules/database/config/userguide.php b/includes/kohana/modules/database/config/userguide.php index 36b79a0..88ff2a3 100644 --- a/includes/kohana/modules/database/config/userguide.php +++ b/includes/kohana/modules/database/config/userguide.php @@ -1,4 +1,4 @@ - 'Database agnostic querying and result management.', // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2010 Kohana Team', + 'copyright' => '© 2008–2012 Kohana Team', ) ) ); \ No newline at end of file diff --git a/includes/kohana/modules/database/guide/database/config.md b/includes/kohana/modules/database/guide/database/config.md index 6879c0a..240a6bb 100644 --- a/includes/kohana/modules/database/guide/database/config.md +++ b/includes/kohana/modules/database/guide/database/config.md @@ -9,7 +9,6 @@ The database configuration file contains an array of configuration groups. The s 'connection' => array CONNECTION_ARRAY, 'table_prefix' => string TABLE_PREFIX, 'charset' => string CHARACTER_SET, - 'profiling' => boolean QUERY_PROFILING, ), Understanding each of these settings is important. @@ -24,10 +23,8 @@ CONNECTION_ARRAY : Specific driver options for connecting to your database. (Driver options are explained [below](#connection-settings).) TABLE_PREFIX -: Prefix that will be added to all table names by the [query builder](#query_building). Prepared statements will **not** use the table prefix. +: Prefix that will be added to all table names by the [query builder](#query_building). -QUERY_PROFILING -: Enables [profiling](../kohana/profiling) of database queries. This is useful for seeing how many queries each page is using, and which are taking the longest. You must enable the profiler the view these stats. ## Example @@ -47,7 +44,6 @@ The example file below shows 2 MySQL connections, one local and one remote. ), 'table_prefix' => '', 'charset' => 'utf8', - 'profiling' => TRUE, ), 'remote' => array( 'type' => 'mysql', @@ -60,7 +56,6 @@ The example file below shows 2 MySQL connections, one local and one remote. ), 'table_prefix' => '', 'charset' => 'utf8', - 'profiling' => TRUE, ), ); @@ -111,8 +106,11 @@ A [PDO database](http://php.net/manual/en/book.pdo.php) can accept these options Type | Option | Description | Default value ----------|------------|----------------------------| ------------------------- `string` | dsn | PDO data source identifier | `localhost` +`array` | options | Driver-specific options | none `string` | username | Database username | `NULL` `string` | password | Database password | `NULL` `boolean` | persistent | Persistent connections | `FALSE` +The connection character set should be configured using the DSN string or `options` array. + [!!] If you are using PDO and are not sure what to use for the `dsn` option, review [PDO::__construct](http://php.net/pdo.construct). \ No newline at end of file diff --git a/includes/kohana/modules/database/guide/database/examples.md b/includes/kohana/modules/database/guide/database/examples.md index 4849823..6a9d1b5 100644 --- a/includes/kohana/modules/database/guide/database/examples.md +++ b/includes/kohana/modules/database/guide/database/examples.md @@ -2,9 +2,9 @@ Here are some "real world" examples of using the database library to construct your queries and use the results. -## Examples of Prepared Statements +## Examples of Parameterized Statements -TODO: 4-6 examples of prepared statements of varying complexity, including a good bind() example. +TODO: 4-6 examples of parameterized statements of varying complexity, including a good bind() example. ## Pagination and search/filter @@ -25,10 +25,10 @@ In this example, we loop through an array of whitelisted input fields and for ea //copy the query & execute it $pagination_query = clone $query; - $count = $pagination_query->select('COUNT("*") AS mycount')->execute()->get('mycount'); + $count = $pagination_query->select(DB::expr('COUNT(*)) AS mycount')->execute()->get('mycount'); //pass the total item count to Pagination - $config = Kohana::config('pagination'); + $config = Kohana::$config->load('pagination'); $pagination = Pagination::factory(array( 'total_items' => $count, 'current_page' => array('source' => 'route', 'key' => 'page'), diff --git a/includes/kohana/modules/database/guide/database/menu.md b/includes/kohana/modules/database/guide/database/menu.md index e3f2c7a..e1c06a8 100644 --- a/includes/kohana/modules/database/guide/database/menu.md +++ b/includes/kohana/modules/database/guide/database/menu.md @@ -1,7 +1,7 @@ ## [Database]() - [Configuration](config) - [Querying](query) - - [Prepared Statements](query/prepared) + - [Parameterized Statements](query/parameterized) - [Query Builder](query/builder) - [Results](results) - [Examples](examples) \ No newline at end of file diff --git a/includes/kohana/modules/database/guide/database/query.md b/includes/kohana/modules/database/guide/database/query.md index 2e9bd41..0a15bf5 100644 --- a/includes/kohana/modules/database/guide/database/query.md +++ b/includes/kohana/modules/database/guide/database/query.md @@ -1,5 +1,5 @@ # Making Queries -There are two different ways to make queries. The simplest way to make a query is to use [Database_Query], via [DB::query], to manually create queries. These queries are called [prepared statements](query/prepared) and allow you to set query parameters which are automatically escaped. The second way to make a query is by building the query using method calls. This is done using the [query builder](query/builder). +There are two different ways to make queries. The simplest way to make a query is to use [Database_Query], via [DB::query], to manually create queries. These queries are called [parameterized statements](query/parameterized) and allow you to set query parameters which are automatically escaped. The second way to make a query is by building the query using method calls. This is done using the [query builder](query/builder). [!!] All queries are run using the `execute` method, which accepts a [Database] object or instance name. See [Database_Query::execute] for more information. \ No newline at end of file diff --git a/includes/kohana/modules/database/guide/database/query/builder.md b/includes/kohana/modules/database/guide/database/query/builder.md index ec11b1a..d2fd893 100644 --- a/includes/kohana/modules/database/guide/database/query/builder.md +++ b/includes/kohana/modules/database/guide/database/query/builder.md @@ -2,8 +2,6 @@ Creating queries dynamically using objects and methods allows queries to be written very quickly in an agnostic way. Query building also adds identifier (table and column name) quoting, as well as value quoting. -[!!] At this time, it is not possible to combine query building with prepared statements. - ## Select Each type of database query is represented by a different class, each with their own methods. For instance, to create a SELECT query, we use [DB::select] which is a shortcut to return a new [Database_Query_Builder_Select] object: @@ -38,7 +36,7 @@ By default, [DB::select] will select all columns (`SELECT * ...`), but you can a Now take a minute to look at what this method chain is doing. First, we create a new selection object using the [DB::select] method. Next, we set table(s) using the `from()` method. Last, we search for a specific records using the `where()` method. We can display the SQL that will be executed by casting the query to a string: - echo Kohana::debug((string) $query); + echo Debug::vars((string) $query); // Should display: // SELECT `username`, `password` FROM `users` WHERE `username` = 'john' @@ -150,11 +148,11 @@ This query would generate the following SQL: ### Database Functions -Eventually you will probably run into a situation where you need to call `COUNT` or some other database function within your query. The query builder supports these functions in two ways. The first is by using quotes within aliases: +Eventually you will probably run into a situation where you need to call `COUNT` or some other database function within your query. The query builder supports these functions using the `Database_Expression` class: - $query = DB::select(array('COUNT("username")', 'total_users'))->from('users'); + $query = DB::select(array(DB::expr('COUNT(`username`)'), 'total_users'))->from('users'); -This looks almost exactly the same as a standard `AS` alias, but note how the column name is wrapped in double quotes. Any time a double-quoted value appears inside of a column name, **only** the part inside the double quotes will be escaped. This query would generate the following SQL: +This looks almost exactly the same as a standard `AS` alias, but note how the column name is put in a call to `DB::expr()`. Any time `DB::expr()` is used, the column name will **not** be escaped. This query would generate the following SQL: SELECT COUNT(`username`) AS `total_users` FROM `users` @@ -166,14 +164,14 @@ This looks almost exactly the same as a standard `AS` alias, but note how the co ->where('posts.created', '>=', $yesterday); $total = clone $query; - $total->select(array('COUNT( DISTINCT "username")', 'unique_users')); + $total->select(array(DB::expr('COUNT( DISTINCT `username`)'), 'unique_users')); $query->select('posts.username')->distinct(); ### Aggregate Functions Aggregate functions like `COUNT()`, `SUM()`, `AVG()`, etc. will most likely be used with the `group_by()` and possibly the `having()` methods in order to group and filter the results on a set of columns. - $query = DB::select('username', array('COUNT("id")', 'total_posts') + $query = DB::select('username', array(DB::expr('COUNT(`id`)'), 'total_posts') ->from('posts')->group_by('username')->having('total_posts', '>=', 10); This will generate the following query: @@ -184,7 +182,7 @@ This will generate the following query: Query Builder objects can be passed as parameters to many of the methods to create subqueries. Let's take the previous example query and pass it to a new query. - $sub = DB::select('username', array('COUNT("id")', 'total_posts') + $sub = DB::select('username', array(DB::expr('COUNT(`id`)'), 'total_posts') ->from('posts')->group_by('username')->having('total_posts', '>=', 10); $query = DB::select('profiles.*', 'posts.total_posts')->from('profiles') @@ -198,7 +196,7 @@ This will generate the following query: Insert queries can also use a select query for the input values - $sub = DB::select('username', array('COUNT("id")', 'total_posts') + $sub = DB::select('username', array(DB::expr('COUNT(`id`)'), 'total_posts') ->from('posts')->group_by('username')->having('total_posts', '>=', 10); $query = DB::insert('post_totals', array('username', 'posts'))->select($sub); diff --git a/includes/kohana/modules/database/guide/database/query/prepared.md b/includes/kohana/modules/database/guide/database/query/parameterized.md similarity index 89% rename from includes/kohana/modules/database/guide/database/query/prepared.md rename to includes/kohana/modules/database/guide/database/query/parameterized.md index 53611b6..5a4e537 100644 --- a/includes/kohana/modules/database/guide/database/query/prepared.md +++ b/includes/kohana/modules/database/guide/database/query/parameterized.md @@ -1,6 +1,6 @@ -# Prepared Statements +# Parameterized Statements -Using prepared statements allows you to write SQL queries manually while still escaping the query values automatically to prevent [SQL injection](http://wikipedia.org/wiki/SQL_Injection). Creating a query is simple: +Using parameterized statements allows you to write SQL queries manually while still escaping the query values automatically to prevent [SQL injection](http://wikipedia.org/wiki/SQL_Injection). Creating a query is simple: $query = DB::query(Database::SELECT, 'SELECT * FROM users WHERE username = :user'); @@ -50,9 +50,9 @@ The only difference between `param()` and `bind()` is that `bind()` passes the v ## Display the raw query -If you want to display the SQL that will be executed, simply cast the object to a string: +If you want to display the SQL that will be executed, you can simply echo the query: - echo Kohana::debug((string) $query); + echo $query; // Should display: // SELECT * FROM users WHERE username = 'john' diff --git a/includes/kohana/modules/database/guide/database/results.md b/includes/kohana/modules/database/guide/database/results.md index 653aacc..e13dfa1 100644 --- a/includes/kohana/modules/database/guide/database/results.md +++ b/includes/kohana/modules/database/guide/database/results.md @@ -2,7 +2,7 @@ ## Execute -Once you have a query object built, either through a prepared statement or through the builder, you must then `execute()` the query and retrieve the results. Depending on the query type used, the results returned will vary. +Once you have a query object built, either through a parameterized statement or through the builder, you must then `execute()` the query and retrieve the results. Depending on the query type used, the results returned will vary. ## Select @@ -70,7 +70,7 @@ To return a non-associative array, leave `$key` as NULL and just pass a `$value` Sometime you only want a single value from a query. The `get()` method returns the value of the named column from the current row. The second parameter, `$default`, is used to supply a default value when the result is NULL. - $total_users = DB::select(array('COUNT("username")', 'total_users'))->from('users')->execute()->get('total_users', 0); + $total_users = DB::select(array(DB::expr('COUNT(`username`)'), 'total_users'))->from('users')->execute()->get('total_users', 0); ### Select - `cached()` diff --git a/includes/kohana/modules/image/classes/Image.php b/includes/kohana/modules/image/classes/Image.php new file mode 100644 index 0000000..559d344 --- /dev/null +++ b/includes/kohana/modules/image/classes/Image.php @@ -0,0 +1,3 @@ +resize(200, 500, Image::NONE); * - * @param integer new width - * @param integer new height - * @param integer master dimension + * @param integer $width new width + * @param integer $height new height + * @param integer $master master dimension * @return $this * @uses Image::_do_resize */ @@ -247,6 +248,19 @@ abstract class Kohana_Image { // Recalculate the width based on the height proportions $width = $this->width * $height / $this->height; break; + case Image::PRECISE: + // Resize to precise size + $ratio = $this->width / $this->height; + + if ($width / $height > $ratio) + { + $height = $this->height * $width / $this->width; + } + else + { + $width = $this->width * $height / $this->height; + } + break; } // Convert the width and height to integers, minimum value is 1px @@ -268,10 +282,10 @@ abstract class Kohana_Image { * // Crop the image to 200x200 pixels, from the center * $image->crop(200, 200); * - * @param integer new width - * @param integer new height - * @param mixed offset from the left - * @param mixed offset from the top + * @param integer $width new width + * @param integer $height new height + * @param mixed $offset_x offset from the left + * @param mixed $offset_y offset from the top * @return $this * @uses Image::_do_crop */ @@ -351,7 +365,7 @@ abstract class Kohana_Image { * // Rotate 90% counter-clockwise * $image->rotate(-90); * - * @param integer degrees to rotate: -360-360 + * @param integer $degrees degrees to rotate: -360-360 * @return $this * @uses Image::_do_rotate */ @@ -367,7 +381,7 @@ abstract class Kohana_Image { // Keep subtracting full circles until the degrees have normalized $degrees -= 360; } - while($degrees > 180); + while ($degrees > 180); } if ($degrees < -180) @@ -377,7 +391,7 @@ abstract class Kohana_Image { // Keep adding full circles until the degrees have normalized $degrees += 360; } - while($degrees < -180); + while ($degrees < -180); } $this->_do_rotate($degrees); @@ -394,7 +408,7 @@ abstract class Kohana_Image { * // Flip the image from left to right * $image->flip(Image::VERTICAL); * - * @param integer direction: Image::HORIZONTAL, Image::VERTICAL + * @param integer $direction direction: Image::HORIZONTAL, Image::VERTICAL * @return $this * @uses Image::_do_flip */ @@ -417,7 +431,7 @@ abstract class Kohana_Image { * // Sharpen the image by 20% * $image->sharpen(20); * - * @param integer amount to sharpen: 1-100 + * @param integer $amount amount to sharpen: 1-100 * @return $this * @uses Image::_do_sharpen */ @@ -448,9 +462,9 @@ abstract class Kohana_Image { * [!!] By default, the reflection will be go from transparent at the top * to opaque at the bottom. * - * @param integer reflection height - * @param integer reflection opacity: 0-100 - * @param boolean TRUE to fade in, FALSE to fade out + * @param integer $height reflection height + * @param integer $opacity reflection opacity: 0-100 + * @param boolean $fade_in TRUE to fade in, FALSE to fade out * @return $this * @uses Image::_do_reflection */ @@ -481,10 +495,10 @@ abstract class Kohana_Image { * $mark = Image::factory('upload/watermark.png'); * $image->watermark($mark, TRUE, TRUE); * - * @param object watermark Image instance - * @param integer offset from the left - * @param integer offset from the top - * @param integer opacity of watermark: 1-100 + * @param Image $watermark watermark Image instance + * @param integer $offset_x offset from the left + * @param integer $offset_y offset from the top + * @param integer $opacity opacity of watermark: 1-100 * @return $this * @uses Image::_do_watermark */ @@ -540,8 +554,8 @@ abstract class Kohana_Image { * // Make the image background black with 50% opacity * $image->background('#000', 50); * - * @param string hexadecimal color value - * @param integer background opacity: 0-100 + * @param string $color hexadecimal color value + * @param integer $opacity background opacity: 0-100 * @return $this * @uses Image::_do_background */ @@ -585,8 +599,8 @@ abstract class Kohana_Image { * [!!] If the file does not exist, and the directory is not writable, an * exception will be thrown. * - * @param string new image path - * @param integer quality of image: 1-100 + * @param string $file new image path + * @param integer $quality quality of image: 1-100 * @return boolean * @uses Image::_save * @throws Kohana_Exception @@ -634,8 +648,8 @@ abstract class Kohana_Image { * // Render the image as a PNG * $data = $image->render('png'); * - * @param string image type to return: png, jpg, gif, etc - * @param integer quality of image: 1-100 + * @param string $type image type to return: png, jpg, gif, etc + * @param integer $quality quality of image: 1-100 * @return string * @uses Image::_do_render */ @@ -653,8 +667,8 @@ abstract class Kohana_Image { /** * Execute a resize. * - * @param integer new width - * @param integer new height + * @param integer $width new width + * @param integer $height new height * @return void */ abstract protected function _do_resize($width, $height); @@ -662,10 +676,10 @@ abstract class Kohana_Image { /** * Execute a crop. * - * @param integer new width - * @param integer new height - * @param integer offset from the left - * @param integer offset from the top + * @param integer $width new width + * @param integer $height new height + * @param integer $offset_x offset from the left + * @param integer $offset_y offset from the top * @return void */ abstract protected function _do_crop($width, $height, $offset_x, $offset_y); @@ -673,7 +687,7 @@ abstract class Kohana_Image { /** * Execute a rotation. * - * @param integer degrees to rotate + * @param integer $degrees degrees to rotate * @return void */ abstract protected function _do_rotate($degrees); @@ -681,7 +695,7 @@ abstract class Kohana_Image { /** * Execute a flip. * - * @param integer direction to flip + * @param integer $direction direction to flip * @return void */ abstract protected function _do_flip($direction); @@ -689,7 +703,7 @@ abstract class Kohana_Image { /** * Execute a sharpen. * - * @param integer amount to sharpen + * @param integer $amount amount to sharpen * @return void */ abstract protected function _do_sharpen($amount); @@ -697,9 +711,9 @@ abstract class Kohana_Image { /** * Execute a reflection. * - * @param integer reflection height - * @param integer reflection opacity - * @param boolean TRUE to fade out, FALSE to fade in + * @param integer $height reflection height + * @param integer $opacity reflection opacity + * @param boolean $fade_in TRUE to fade out, FALSE to fade in * @return void */ abstract protected function _do_reflection($height, $opacity, $fade_in); @@ -707,10 +721,10 @@ abstract class Kohana_Image { /** * Execute a watermarking. * - * @param object watermarking Image - * @param integer offset from the left - * @param integer offset from the top - * @param integer opacity of watermark + * @param Image $image watermarking Image + * @param integer $offset_x offset from the left + * @param integer $offset_y offset from the top + * @param integer $opacity opacity of watermark * @return void */ abstract protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity); @@ -718,10 +732,10 @@ abstract class Kohana_Image { /** * Execute a background. * - * @param integer red - * @param integer green - * @param integer blue - * @param integer opacity + * @param integer $r red + * @param integer $g green + * @param integer $b blue + * @param integer $opacity opacity * @return void */ abstract protected function _do_background($r, $g, $b, $opacity); @@ -729,8 +743,8 @@ abstract class Kohana_Image { /** * Execute a save. * - * @param string new image filename - * @param integer quality + * @param string $file new image filename + * @param integer $quality quality * @return boolean */ abstract protected function _do_save($file, $quality); @@ -738,8 +752,8 @@ abstract class Kohana_Image { /** * Execute a render. * - * @param string image type: png, jpg, gif, etc - * @param integer quality + * @param string $type image type: png, jpg, gif, etc + * @param integer $quality quality * @return string */ abstract protected function _do_render($type, $quality); diff --git a/includes/kohana/modules/image/classes/kohana/image/gd.php b/includes/kohana/modules/image/classes/Kohana/Image/GD.php similarity index 87% rename from includes/kohana/modules/image/classes/kohana/image/gd.php rename to includes/kohana/modules/image/classes/Kohana/Image/GD.php index 38d0e24..6f383e3 100644 --- a/includes/kohana/modules/image/classes/kohana/image/gd.php +++ b/includes/kohana/modules/image/classes/Kohana/Image/GD.php @@ -1,4 +1,4 @@ -height = imagesy($flipped); } + /** + * Execute a sharpen. + * + * @param integer $amount amount to sharpen + * @return void + */ protected function _do_sharpen($amount) { if ( ! Image_GD::$_bundled) @@ -322,6 +357,14 @@ class Kohana_Image_GD extends Image { } } + /** + * Execute a reflection. + * + * @param integer $height reflection height + * @param integer $opacity reflection opacity + * @param boolean $fade_in TRUE to fade out, FALSE to fade in + * @return void + */ protected function _do_reflection($height, $opacity, $fade_in) { if ( ! Image_GD::$_bundled) @@ -394,6 +437,15 @@ class Kohana_Image_GD extends Image { $this->height = imagesy($reflection); } + /** + * Execute a watermarking. + * + * @param Image $image watermarking Image + * @param integer $offset_x offset from the left + * @param integer $offset_y offset from the top + * @param integer $opacity opacity of watermark + * @return void + */ protected function _do_watermark(Image $watermark, $offset_x, $offset_y, $opacity) { if ( ! Image_GD::$_bundled) @@ -439,6 +491,15 @@ class Kohana_Image_GD extends Image { } } + /** + * Execute a background. + * + * @param integer $r red + * @param integer $g green + * @param integer $b blue + * @param integer $opacity opacity + * @return void + */ protected function _do_background($r, $g, $b, $opacity) { // Loads image if not yet loaded @@ -468,6 +529,13 @@ class Kohana_Image_GD extends Image { } } + /** + * Execute a save. + * + * @param string $file new image filename + * @param integer $quality quality + * @return boolean + */ protected function _do_save($file, $quality) { // Loads image if not yet loaded @@ -492,6 +560,13 @@ class Kohana_Image_GD extends Image { return TRUE; } + /** + * Execute a render. + * + * @param string $type image type: png, jpg, gif, etc + * @param integer $quality quality + * @return string + */ protected function _do_render($type, $quality) { // Loads image if not yet loaded @@ -520,13 +595,19 @@ class Kohana_Image_GD extends Image { * Get the GD saving function and image type for this extension. * Also normalizes the quality setting * - * @param string image type: png, jpg, etc - * @param integer image quality + * @param string $extension image type: png, jpg, etc + * @param integer $quality image quality * @return array save function, IMAGETYPE_* constant * @throws Kohana_Exception */ protected function _save_function($extension, & $quality) { + if ( ! $extension) + { + // Use the current image type + $extension = image_type_to_extension($this->type, FALSE); + } + switch (strtolower($extension)) { case 'jpg': @@ -563,8 +644,8 @@ class Kohana_Image_GD extends Image { /** * Create an empty image with the given width and height. * - * @param integer image width - * @param integer image height + * @param integer $width image width + * @param integer $height image height * @return resource */ protected function _create($width, $height) diff --git a/includes/kohana/modules/image/classes/Kohana/Image/Imagick.php b/includes/kohana/modules/image/classes/Kohana/Image/Imagick.php new file mode 100644 index 0000000..21adbbf --- /dev/null +++ b/includes/kohana/modules/image/classes/Kohana/Image/Imagick.php @@ -0,0 +1,334 @@ +im = new Imagick; + $this->im->readImage($file); + + if ( ! $this->im->getImageAlphaChannel()) + { + // Force the image to have an alpha channel + $this->im->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET); + } + } + + /** + * Destroys the loaded image to free up resources. + * + * @return void + */ + public function __destruct() + { + $this->im->clear(); + $this->im->destroy(); + } + + protected function _do_resize($width, $height) + { + if ($this->im->scaleImage($width, $height)) + { + // Reset the width and height + $this->width = $this->im->getImageWidth(); + $this->height = $this->im->getImageHeight(); + + return TRUE; + } + + return FALSE; + } + + protected function _do_crop($width, $height, $offset_x, $offset_y) + { + if ($this->im->cropImage($width, $height, $offset_x, $offset_y)) + { + // Reset the width and height + $this->width = $this->im->getImageWidth(); + $this->height = $this->im->getImageHeight(); + + // Trim off hidden areas + $this->im->setImagePage($this->width, $this->height, 0, 0); + + return TRUE; + } + + return FALSE; + } + + protected function _do_rotate($degrees) + { + if ($this->im->rotateImage(new ImagickPixel('transparent'), $degrees)) + { + // Reset the width and height + $this->width = $this->im->getImageWidth(); + $this->height = $this->im->getImageHeight(); + + // Trim off hidden areas + $this->im->setImagePage($this->width, $this->height, 0, 0); + + return TRUE; + } + + return FALSE; + } + + protected function _do_flip($direction) + { + if ($direction === Image::HORIZONTAL) + { + return $this->im->flopImage(); + } + else + { + return $this->im->flipImage(); + } + } + + protected function _do_sharpen($amount) + { + // IM not support $amount under 5 (0.15) + $amount = ($amount < 5) ? 5 : $amount; + + // Amount should be in the range of 0.0 to 3.0 + $amount = ($amount * 3.0) / 100; + + return $this->im->sharpenImage(0, $amount); + } + + protected function _do_reflection($height, $opacity, $fade_in) + { + // Clone the current image and flip it for reflection + $reflection = $this->im->clone(); + $reflection->flipImage(); + + // Crop the reflection to the selected height + $reflection->cropImage($this->width, $height, 0, 0); + $reflection->setImagePage($this->width, $height, 0, 0); + + // Select the fade direction + $direction = array('transparent', 'black'); + + if ($fade_in) + { + // Change the direction of the fade + $direction = array_reverse($direction); + } + + // Create a gradient for fading + $fade = new Imagick; + $fade->newPseudoImage($reflection->getImageWidth(), $reflection->getImageHeight(), vsprintf('gradient:%s-%s', $direction)); + + // Apply the fade alpha channel to the reflection + $reflection->compositeImage($fade, Imagick::COMPOSITE_DSTOUT, 0, 0); + + // NOTE: Using setImageOpacity will destroy alpha channels! + $reflection->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA); + + // Create a new container to hold the image and reflection + $image = new Imagick; + $image->newImage($this->width, $this->height + $height, new ImagickPixel); + + // Force the image to have an alpha channel + $image->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET); + + // Force the background color to be transparent + // $image->setImageBackgroundColor(new ImagickPixel('transparent')); + + // Match the colorspace between the two images before compositing + $image->setColorspace($this->im->getColorspace()); + + // Place the image and reflection into the container + if ($image->compositeImage($this->im, Imagick::COMPOSITE_SRC, 0, 0) + AND $image->compositeImage($reflection, Imagick::COMPOSITE_OVER, 0, $this->height)) + { + // Replace the current image with the reflected image + $this->im = $image; + + // Reset the width and height + $this->width = $this->im->getImageWidth(); + $this->height = $this->im->getImageHeight(); + + return TRUE; + } + + return FALSE; + } + + protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity) + { + // Convert the Image intance into an Imagick instance + $watermark = new Imagick; + $watermark->readImageBlob($image->render(), $image->file); + + if ($watermark->getImageAlphaChannel() !== Imagick::ALPHACHANNEL_ACTIVATE) + { + // Force the image to have an alpha channel + $watermark->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE); + } + + if ($opacity < 100) + { + // NOTE: Using setImageOpacity will destroy current alpha channels! + $watermark->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA); + } + + // Match the colorspace between the two images before compositing + // $watermark->setColorspace($this->im->getColorspace()); + + // Apply the watermark to the image + return $this->im->compositeImage($watermark, Imagick::COMPOSITE_DISSOLVE, $offset_x, $offset_y); + } + + protected function _do_background($r, $g, $b, $opacity) + { + // Create a RGB color for the background + $color = sprintf('rgb(%d, %d, %d)', $r, $g, $b); + + // Create a new image for the background + $background = new Imagick; + $background->newImage($this->width, $this->height, new ImagickPixel($color)); + + if ( ! $background->getImageAlphaChannel()) + { + // Force the image to have an alpha channel + $background->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET); + } + + // Clear the background image + $background->setImageBackgroundColor(new ImagickPixel('transparent')); + + // NOTE: Using setImageOpacity will destroy current alpha channels! + $background->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA); + + // Match the colorspace between the two images before compositing + $background->setColorspace($this->im->getColorspace()); + + if ($background->compositeImage($this->im, Imagick::COMPOSITE_DISSOLVE, 0, 0)) + { + // Replace the current image with the new image + $this->im = $background; + + return TRUE; + } + + return FALSE; + } + + protected function _do_save($file, $quality) + { + // Get the image format and type + list($format, $type) = $this->_get_imagetype(pathinfo($file, PATHINFO_EXTENSION)); + + // Set the output image type + $this->im->setFormat($format); + + // Set the output quality + $this->im->setImageCompressionQuality($quality); + + if ($this->im->writeImage($file)) + { + // Reset the image type and mime type + $this->type = $type; + $this->mime = image_type_to_mime_type($type); + + return TRUE; + } + + return FALSE; + } + + protected function _do_render($type, $quality) + { + // Get the image format and type + list($format, $type) = $this->_get_imagetype($type); + + // Set the output image type + $this->im->setFormat($format); + + // Set the output quality + $this->im->setImageCompressionQuality($quality); + + // Reset the image type and mime type + $this->type = $type; + $this->mime = image_type_to_mime_type($type); + + return (string) $this->im; + } + + /** + * Get the image type and format for an extension. + * + * @param string $extension image extension: png, jpg, etc + * @return string IMAGETYPE_* constant + * @throws Kohana_Exception + */ + protected function _get_imagetype($extension) + { + // Normalize the extension to a format + $format = strtolower($extension); + + switch ($format) + { + case 'jpg': + case 'jpeg': + $type = IMAGETYPE_JPEG; + break; + case 'gif': + $type = IMAGETYPE_GIF; + break; + case 'png': + $type = IMAGETYPE_PNG; + break; + default: + throw new Kohana_Exception('Installed ImageMagick does not support :type images', + array(':type' => $extension)); + break; + } + + return array($format, $type); + } +} // End Kohana_Image_Imagick \ No newline at end of file diff --git a/includes/kohana/modules/image/config/userguide.php b/includes/kohana/modules/image/config/userguide.php index 2ca21c4..c04ae71 100644 --- a/includes/kohana/modules/image/config/userguide.php +++ b/includes/kohana/modules/image/config/userguide.php @@ -1,4 +1,4 @@ - 'Image manipulation.', // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2010 Kohana Team', + 'copyright' => '© 2008–2012 Kohana Team', ) ) ); \ No newline at end of file diff --git a/includes/kohana/modules/image/guide/image/examples.md b/includes/kohana/modules/image/guide/image/examples.md index e69de29..8f59c82 100644 --- a/includes/kohana/modules/image/guide/image/examples.md +++ b/includes/kohana/modules/image/guide/image/examples.md @@ -0,0 +1,7 @@ +# Examples + +The following are mini applications that uses the [Image] module. They are very straight forward and did not include additional code such as validations and the like. They are designed to be simple and should work out of the box. + +* [Uploading image](examples/upload) +* [Cropping profile images](examples/crop) +* [Serving images with dynamic dimension](examples/dynamic) \ No newline at end of file diff --git a/includes/kohana/modules/image/guide/image/examples/crop.md b/includes/kohana/modules/image/guide/image/examples/crop.md new file mode 100644 index 0000000..fe79657 --- /dev/null +++ b/includes/kohana/modules/image/guide/image/examples/crop.md @@ -0,0 +1,141 @@ +# Crop Profile Image + +This example is very similar to our previous example and even uses the same upload logics. The only difference is that the uploaded image is cropped to square from the center whose dimension is half the original height of the image. + +## Controller + +We name our new controller as `Controller_Crop` and accessible through `/crop` URL. Assuming that your project is located at [http://localhost/kohana](http://localhost/kohana), then our crop controller is at [http://localhost/kohana/crop](http://localhost/kohana/crop). + +~~~ +response->body($view); + } + + public function action_do() + { + $view = View::factory('crop/do'); + $error_message = NULL; + $filename = NULL; + + if ($this->request->method() == Request::POST) + { + if (isset($_FILES['avatar'])) + { + $filename = $this->_save_image($_FILES['avatar']); + } + } + + if ( ! $filename) + { + $error_message = 'There was a problem while uploading the image. + Make sure it is uploaded and must be JPG/PNG/GIF file.'; + } + + $view->uploaded_file = $filename; + $view->error_message = $error_message; + $this->response->body($view); + } + + protected function _save_image($image) + { + if ( + ! Upload::valid($image) OR + ! Upload::not_empty($image) OR + ! Upload::type($image, array('jpg', 'jpeg', 'png', 'gif'))) + { + return FALSE; + } + + $directory = DOCROOT.'uploads/'; + + if ($file = Upload::save($image, NULL, $directory)) + { + $filename = strtolower(Text::random('alnum', 20)).'.jpg'; + + $img = Image::factory($file); + + // Crop the image square half the height and crop from center + $new_height = (int) $img->height / 2; + + $img->crop($new_height, $new_height) + ->save($directory.$filename); + + // Delete the temporary file + unlink($file); + + return $filename; + } + + return FALSE; + } + +} +~~~ + +The `index` action displays the upload form whereas the `do` action will process the uploaded image and provides feedback to the user. + +In `do` action, it checks if the request method was `POST`, then delegates the process to `_save_image()` method which in turn performs various checks and finally crops and saves the image to the `uploads` directory. + +## Views + +For the upload form (the `index` action), the view is located at `views/crop/index.php`. + +~~~ + + + Upload Profile Image + + +

Upload your profile image

+
+

Choose file:

+

+

+ + + +~~~ + +View for `crop/do` action goes to `views/crop/do.php`. + +~~~ + + + Upload Profile Image Result + + + +

Upload success

+

+ Here is your uploaded and cropped avatar: + " alt="Uploaded avatar" /> +

+ +

Something went wrong with the upload

+

+ + + +~~~ + +## Screenshots + +Below are screenshots for this example. + +![Original image](crop_orig.jpg) + +_Original image to upload_ + +![Upload image form](crop_form.jpg) + +_Upload image form_ + +![Upload result page](crop_result.jpg) + +_Upload result form_ \ No newline at end of file diff --git a/includes/kohana/modules/image/guide/image/examples/dynamic.md b/includes/kohana/modules/image/guide/image/examples/dynamic.md new file mode 100644 index 0000000..2d70260 --- /dev/null +++ b/includes/kohana/modules/image/guide/image/examples/dynamic.md @@ -0,0 +1,108 @@ +# Dynamic Image Controller + +In this example, we have images under `/uploads` under the webroot directory. We allow the user to render any image with dynamic dimension and is resized on the fly. It also caches the response for 1 hour to show basic caching mechanism. + +## Route + +First, we need a [Route]. This [Route] is based on this URL pattern: + +`/imagefly/filename/width/height` - where filename is the name of the image without the extension. This assumes that all images are in `jpg` and all filenames uses numbers, letters, dash and underscores only. + +This is our [Route] definition: + +~~~ +/** + * Set route for image fly + */ +Route::set('imagefly', 'imagefly///', array('image' => '[-09a-zA-Z_]+', 'width' => '[0-9]+', 'height' => '[0-9]+')) + ->defaults(array( + 'controller' => 'imagefly', + 'action' => 'index' + )); +~~~ + +We ensure that the filename is only composed of letters, numbers and underscores, width and height must be numeric. + +## Controller + +Our controller simply accepts the request and capture the following parameters as defined by the [Route]: + +* `filename` - without the filename extension (and without dot) +* `width` +* `height` + +Then it finds the image file and when found, render it on the browser. Additional features added are browser caching. + +~~~ +request->param('image'); + $width = (int) $this->request->param('width'); + $height = (int) $this->request->param('height'); + + $rendered = FALSE; + if ($file AND $width AND $height) + { + $filename = DOCROOT.'uploads/'.$file.'.jpg'; + + if (is_file($filename)) + { + $this->_render_image($filename, $width, $height); + $rendered = TRUE; + } + } + + if ( ! $rendered) + { + $this->response->status(404); + } + } + + protected function _render_image($filename, $width, $height) + { + // Calculate ETag from original file padded with the dimension specs + $etag_sum = md5(base64_encode(file_get_contents($filename)).$width.','.$height); + + // Render as image and cache for 1 hour + $this->response->headers('Content-Type', 'image/jpeg') + ->headers('Cache-Control', 'max-age='.Date::HOUR.', public, must-revalidate') + ->headers('Expires', gmdate('D, d M Y H:i:s', time() + Date::HOUR).' GMT') + ->headers('Last-Modified', date('r', filemtime($filename))) + ->headers('ETag', $etag_sum); + + if ( + $this->request->headers('if-none-match') AND + (string) $this->request->headers('if-none-match') === $etag_sum) + { + $this->response->status(304) + ->headers('Content-Length', '0'); + } + else + { + $result = Image::factory($filename) + ->resize($width, $height) + ->render('jpg'); + + $this->response->body($result); + } + } +} +~~~ + +When the parameters are invalid or the filename does not exists, it simply returns 404 not found error. + +The rendering of image uses some caching mechanism. One by setting the max age and expire headers and second by using etags. + +## Screenshots + +Visiting [http://localhost/kohana/imagefly/kitteh/400/400](http://localhost/kohana/imagefly/kitteh/400/400) yields: + +![Kitten 400x400](dynamic-400.jpg) + +Visiting [http://localhost/kohana/imagefly/kitteh/600/500](http://localhost/kohana/imagefly/kitteh/600/500) yields: + +![Kitten 400x400](dynamic-600.jpg) \ No newline at end of file diff --git a/includes/kohana/modules/image/guide/image/examples/upload.md b/includes/kohana/modules/image/guide/image/examples/upload.md new file mode 100644 index 0000000..e08b780 --- /dev/null +++ b/includes/kohana/modules/image/guide/image/examples/upload.md @@ -0,0 +1,139 @@ +# Upload Image + +The following example shows how to handle uploading of an image, resize it and save it to a file. Be sure you have enabled the [Image] module as discussed in getting started guide. + +Assuming you are creating a web application that allows your members to upload their profile picture (avatar), the steps below explains it how. + +## Controller + +First we need to create a controller that handles the requests for uploading an image. We will name it `Controller_Avatar` and accessible through `/avatar` URL. Assuming that your project is located at [http://localhost/kohana](http://localhost/kohana), then our avatar controller is at [http://localhost/kohana/avatar](http://localhost/kohana/avatar). + +For simplicity, the upload form will be on `index` action and `upload` action will process the uploaded file. This is what our controller now looks like. Please note that we are not using [Controller_Template], just [Controller]. + +~~~ +response->body($view); + } + + public function action_upload() + { + $view = View::factory('avatar/upload'); + $error_message = NULL; + $filename = NULL; + + if ($this->request->method() == Request::POST) + { + if (isset($_FILES['avatar'])) + { + $filename = $this->_save_image($_FILES['avatar']); + } + } + + if ( ! $filename) + { + $error_message = 'There was a problem while uploading the image. + Make sure it is uploaded and must be JPG/PNG/GIF file.'; + } + + $view->uploaded_file = $filename; + $view->error_message = $error_message; + $this->response->body($view); + } + + protected function _save_image($image) + { + if ( + ! Upload::valid($image) OR + ! Upload::not_empty($image) OR + ! Upload::type($image, array('jpg', 'jpeg', 'png', 'gif'))) + { + return FALSE; + } + + $directory = DOCROOT.'uploads/'; + + if ($file = Upload::save($image, NULL, $directory)) + { + $filename = strtolower(Text::random('alnum', 20)).'.jpg'; + + Image::factory($file) + ->resize(200, 200, Image::AUTO) + ->save($directory.$filename); + + // Delete the temporary file + unlink($file); + + return $filename; + } + + return FALSE; + } + +} +~~~ + +We have `index` and `upload` actions. `index` action will display the upload form and `upload` action will process the uploaded image and provides feedback to the user. + +In `upload` action, it checks if the request method was `POST`, then delegates the process to `_save_image()` method which in turn performs various checks and finally resize and save the image to the `uploads` directory. + +## Views + +For the upload form (the `index` action), the view is located at `views/avatar/index.php`. + +~~~ + + + Upload Avatar + + +

Upload your avatar

+
+

Choose file:

+

+

+ + + +~~~ + +Take note of the action attribute. It points to our `avatar/upload` action whose view code goes to `views/avatar/upload.php`. + +~~~ + + + Upload Avatar Result + + + +

Upload success

+

+ Here is your uploaded avatar: + " alt="Uploaded avatar" /> +

+ +

Something went wrong with the upload

+

+ + + +~~~ + +When the upload is successfull, a success message is displayed with the uploaded image displayed. Otherwise, when it fails, it displays an error message. + +## Screenshots + +Below are screenshots for this example. + +![Upload image form](upload_form.jpg) + +_Upload image form_ + +![Upload result page](upload_result.jpg) + +_Upload result form_ \ No newline at end of file diff --git a/includes/kohana/modules/image/guide/image/index.md b/includes/kohana/modules/image/guide/image/index.md index e69de29..0225c26 100644 --- a/includes/kohana/modules/image/guide/image/index.md +++ b/includes/kohana/modules/image/guide/image/index.md @@ -0,0 +1,21 @@ +# Image + +Kohana 3.x provides a simple yet powerful image manipulation module. The [Image] module provides features that allows your application to resize images, crop, rotate, flip and many more. + +## Drivers + +[Image] module ships with [Image_GD] driver which requires `GD` extension enabled in your PHP installation. This is the default driver. Additional drivers can be created by extending the [Image] class. + +## Getting Started + +Before using the image module, we must enable it first on `APPPATH/bootstrap.php`: + +~~~ +Kohana::modules(array( + ... + 'image' => MODPATH.'image', // Image manipulation + ... +)); +~~~ + +Next: [Using the image module](using). \ No newline at end of file diff --git a/includes/kohana/modules/image/guide/image/menu.md b/includes/kohana/modules/image/guide/image/menu.md index b8e8cb3..8c87743 100644 --- a/includes/kohana/modules/image/guide/image/menu.md +++ b/includes/kohana/modules/image/guide/image/menu.md @@ -1,3 +1,6 @@ ## [Image]() - [Using](using) -- [Examples](examples) \ No newline at end of file +- [Examples](examples) + - [Upload Image](examples/upload) + - [Crop Profile Image](examples/crop) + - [Dynamic Image Controller](examples/dynamic) \ No newline at end of file diff --git a/includes/kohana/modules/image/guide/image/using.md b/includes/kohana/modules/image/guide/image/using.md index e69de29..fd1859b 100644 --- a/includes/kohana/modules/image/guide/image/using.md +++ b/includes/kohana/modules/image/guide/image/using.md @@ -0,0 +1,112 @@ +# Basic Usage + +Shown here are the basic usage of this module. For full documentation about the image module usage, visit the [Image] api browser. + +## Creating Instance + +[Image::factory()] creates an instance of the image object and prepares it for manipulation. It accepts the `filename` as an arguement and an optional `driver` parameter. When `driver` is not specified, the default driver `GD` is used. + +~~~ +// Uses the image from upload directory +$img = Image::factory(DOCROOT.'uploads/sample-image.jpg'); +~~~ + +Once an instance is created, you can now manipulate the image by using the following instance methods. + +## Resize + +Resize the image to the given size. Either the width or the height can be omitted and the image will be resized proportionally. + +Using the image object above, we can resize our image to say 150x150 pixels with automatic scaling using the code below: + +~~~ +$img->resize(150, 150, Image::AUTO); +~~~ + +The parameters are `width`, `height` and `master` dimension respectively. With `AUTO` master dimension, the image is resized by either width or height depending on which is closer to the specified dimension. + +Other examples: + +~~~ +// Resize to 200 pixels on the shortest side +$img->resize(200, 200); + +// Resize to 200x200 pixels, keeping aspect ratio +$img->resize(200, 200, Image::INVERSE); + +// Resize to 500 pixel width, keeping aspect ratio +$img->resize(500, NULL); + +// Resize to 500 pixel height, keeping aspect ratio +$img->resize(NULL, 500); + +// Resize to 200x500 pixels, ignoring aspect ratio +$img->resize(200, 500, Image::NONE); +~~~ + +## Render + +You can render the image object directly to the browser using the [Image::render()] method. + +~~~ +$img = Image::factory(DOCROOT.'uploads/colorado-farm-1920x1200.jpg'); + +header('Content-Type: image/jpeg'); + +echo $img->resize(300, 300) + ->render(); +~~~ + +What it did is resize a 1920x1200 wallpaper image into 300x300 proportionally and render it to the browser. If you are trying to render the image in a controller action, you can do instead: + +~~~ +$img = Image::factory(DOCROOT.'uploads/colorado-farm-1920x1200.jpg'); + +$this->response->headers('Content-Type', 'image/jpg'); + +$this->response->body( + $img->resize(300, 300) + ->render() +); +~~~ + +[Image::render()] method also allows you to specify the type and quality of the rendered image. + +~~~ +// Render the image at 50% quality +$img->render(NULL, 50); + +// Render the image as a PNG +$img->render('png'); +~~~ + +## Save To File + +[Image::save()] let's you save the image object to a file. It has two parameters: `filename` and `quality`. If `filename` is omitted, the original file used will be overwritten instead. The `quality` parameter is an integer from 1-100 which indicates the quality of image to save which defaults to 100. + +On our example above, instead of rendering the file to the browser, you may want to save it somewhere instead. To do so, you may: + +~~~ +$img = Image::factory(DOCROOT.'uploads/colorado-farm-1920x1200.jpg'); + +$filename = DOCROOT.'uploads/img-'.uniqid().'.jpg'; + +$img->resize(300, 300) + ->save($filename, 80); +~~~ + +What we do is resize the image and save it to file reducing quality to 80% and save it to the upload directory using a unique filename. + +## Other Methods + +There are more methods available for the [Image] module which provides powerfull features that are best describe in the API documentation. Here are some of them: + +* [Image::background()] - Set the background color of an image. +* [Image::crop()] - Crop an image to the given size. +* [Image::flip()] - Flip the image along the horizontal or vertical axis. +* [Image::reflection()] - Add a reflection to an image. +* [Image::rotate()] - Rotate the image by a given amount. +* [Image::sharpen()] - Sharpen the image by a given amount. +* [Image::watermark()] - Add a watermark to an image with a specified opacity. + +Next: [Examples](examples) \ No newline at end of file diff --git a/includes/kohana/modules/image/media/guide/image/Thumbs.db b/includes/kohana/modules/image/media/guide/image/Thumbs.db new file mode 100644 index 0000000..a0f028f Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/Thumbs.db differ diff --git a/includes/kohana/modules/image/media/guide/image/crop_form.jpg b/includes/kohana/modules/image/media/guide/image/crop_form.jpg new file mode 100644 index 0000000..7179a2e Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/crop_form.jpg differ diff --git a/includes/kohana/modules/image/media/guide/image/crop_orig.jpg b/includes/kohana/modules/image/media/guide/image/crop_orig.jpg new file mode 100644 index 0000000..1389acc Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/crop_orig.jpg differ diff --git a/includes/kohana/modules/image/media/guide/image/crop_result.jpg b/includes/kohana/modules/image/media/guide/image/crop_result.jpg new file mode 100644 index 0000000..7cf79b0 Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/crop_result.jpg differ diff --git a/includes/kohana/modules/image/media/guide/image/dynamic-400.jpg b/includes/kohana/modules/image/media/guide/image/dynamic-400.jpg new file mode 100644 index 0000000..13b691f Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/dynamic-400.jpg differ diff --git a/includes/kohana/modules/image/media/guide/image/dynamic-600.jpg b/includes/kohana/modules/image/media/guide/image/dynamic-600.jpg new file mode 100644 index 0000000..f521b49 Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/dynamic-600.jpg differ diff --git a/includes/kohana/modules/image/media/guide/image/upload_form.jpg b/includes/kohana/modules/image/media/guide/image/upload_form.jpg new file mode 100644 index 0000000..101a7b3 Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/upload_form.jpg differ diff --git a/includes/kohana/modules/image/media/guide/image/upload_result.jpg b/includes/kohana/modules/image/media/guide/image/upload_result.jpg new file mode 100644 index 0000000..ec88621 Binary files /dev/null and b/includes/kohana/modules/image/media/guide/image/upload_result.jpg differ diff --git a/includes/kohana/modules/image/tests/kohana/ImageTest.php b/includes/kohana/modules/image/tests/kohana/ImageTest.php new file mode 100644 index 0000000..967b764 --- /dev/null +++ b/includes/kohana/modules/image/tests/kohana/ImageTest.php @@ -0,0 +1,36 @@ +markTestSkipped('The GD extension is not available.'); + } + } + + /** + * Tests the Image::save() method for files that don't have extensions + * + * @return void + */ + public function test_save_without_extension() + { + $image = Image::factory(MODPATH.'image/tests/test_data/test_image'); + $this->assertTrue($image->save(Kohana::$cache_dir.'/test_image')); + + unlink(Kohana::$cache_dir.'/test_image'); + } + +} // End Kohana_ImageTest diff --git a/includes/kohana/modules/image/tests/test_data/test_image b/includes/kohana/modules/image/tests/test_data/test_image new file mode 100644 index 0000000..683a3c3 Binary files /dev/null and b/includes/kohana/modules/image/tests/test_data/test_image differ diff --git a/includes/kohana/modules/khemail/README.markdown b/includes/kohana/modules/khemail/README.markdown new file mode 100644 index 0000000..c205338 --- /dev/null +++ b/includes/kohana/modules/khemail/README.markdown @@ -0,0 +1,39 @@ +Email Module For Kohana 3.0 +================================= + +This is a direct port of the email helper from Kohana 2.3.3 source code. + +It has been updated to work with SwiftMailer 4 and includes the libs dir from the 4.0.4 distribution. + +Usage should be exactly as with old helper. + +Methods defined: + +### Email::connect($config = NULL) + +Creates SwiftMailer object. $config is an array of configuration values and defaults to using the config file 'email'. + +Note: PopBeforeSmtp is not supported in this release as I didn't know what was required to set it up. +It IS supported in Swiftmailer through the Swift_Plugins_PopBeforeSmtpPlugin plugin class. This can be used manually if required. +If anyone can modify and test the connect() methd with this functionality I'll add it but I can't find documentation about how it used to work (i.e. is expected to work) so I have left it out for now. + +### Email::send($to, $from, $subject, $message, $html = false) + +$to can be any of the following: + +* a single string email address e.g. "test@example.com" +* an array specifying an email address and a name e.g. array('test@example.com', 'John Doe') +* an array of recipients in either above format, keyed by type e.g. array('to' => 'test@example.com', 'cc' => array('test2@example.com', 'Jane Doe'), 'bcc' => 'another@example.com') + +$from can be either a string email or array of email and name as above + +More complex email (multipart, attachments, batch mailing etc.) must be done using the native Swift_Mailer classes. The Swift Mailer autoloader is included by connect() so you can use and class in the Swift library without worrying about including files. + +The Swift_Mailer object setup by connect is returned by it so if you need to access it manually use: + + $mailer = Email::connect(); + + // Create complex Swift_Message object stored in $message + + $mailer->send($message); + diff --git a/includes/kohana/modules/khemail/classes/email.php b/includes/kohana/modules/khemail/classes/email.php new file mode 100644 index 0000000..32ddec5 --- /dev/null +++ b/includes/kohana/modules/khemail/classes/email.php @@ -0,0 +1,145 @@ +load('email'); + + switch ($config['driver']) + { + case 'smtp': + // Set port + $port = empty($config['options']['port']) ? 25 : (int) $config['options']['port']; + + // Create SMTP Transport + $transport = Swift_SmtpTransport::newInstance($config['options']['hostname'], $port); + + if ( ! empty($config['options']['encryption'])) + { + // Set encryption + $transport->setEncryption($config['options']['encryption']); + } + + // Do authentication, if part of the DSN + empty($config['options']['username']) or $transport->setUsername($config['options']['username']); + empty($config['options']['password']) or $transport->setPassword($config['options']['password']); + + // Set the timeout to 5 seconds + $transport->setTimeout(empty($config['options']['timeout']) ? 5 : (int) $config['options']['timeout']); + break; + case 'sendmail': + // Create a sendmail connection + $transport = Swift_SendmailTransport::newInstance(empty($config['options']) ? "/usr/sbin/sendmail -bs" : $config['options']); + + break; + default: + // Use the native connection + $transport = Swift_MailTransport::newInstance($config['options']); + break; + } + + // Create the SwiftMailer instance + return Email::$mail = Swift_Mailer::newInstance($transport); + } + + /** + * 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 = Swift_Message::newInstance($subject, $message, $html, 'utf-8'); + + if (is_string($to)) + { + // Single recipient + $message->setTo($to); + } + elseif (is_array($to)) + { + if (isset($to[0]) AND isset($to[1])) + { + // Create To: address set + $to = array('to' => $to); + } + + 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 + $message->$method($set[0], $set[1]); + } + else + { + // Add a recipient without name + $message->$method($set); + } + } + } + + if (is_string($from)) + { + // From without a name + $message->setFrom($from); + } + elseif (is_array($from)) + { + // From with a name + $message->setFrom($from[0], $from[1]); + } + + return Email::$mail->send($message); + } + +} // End email diff --git a/includes/kohana/modules/khemail/config/email.php b/includes/kohana/modules/khemail/config/email.php new file mode 100644 index 0000000..1dfad96 --- /dev/null +++ b/includes/kohana/modules/khemail/config/email.php @@ -0,0 +1,29 @@ + 'native', + + /** + * To use secure connections with SMTP, set "port" to 465 instead of 25. + * To enable TLS, set "encryption" to "tls". + * + * Note for SMTP, 'auth' key no longer exists as it did in 2.3.x helper + * Simply specifying a username and password is enough for all normal auth methods + * as they are autodeteccted in Swiftmailer 4 + * + * PopB4Smtp is not supported in this module as I had no way to test it but + * SwiftMailer 4 does have a PopBeforeSMTP plugin so it shouldn't be hard to implement + * + * Encryption can be one of 'ssl' or 'tls' (both require non-default PHP extensions + * + * Driver options: + * @param null native: no options + * @param string sendmail: executable path, with -bs or equivalent attached + * @param array smtp: hostname, (username), (password), (port), (encryption) + */ + 'options' => NULL +); \ No newline at end of file diff --git a/includes/kohana/modules/khemail/vendor/swift/CHANGES b/includes/kohana/modules/khemail/vendor/swift/CHANGES new file mode 100644 index 0000000..86fdb11 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/CHANGES @@ -0,0 +1,61 @@ +Changelog for Swift Mailer, since Version 4.x +--------------------------------------------- + +09 March 2009: 4.0.0 +-------------------- + + * Complete rewrite of Version 3.x with lots of breaking changes at the interface + level, but for the best in the long run. + * Changed Connections to Transports + * Made sending more robust (less error prone) + * Simplified Swift_Message interface (removed need for separate RecipientList) + * Improved Plugin API (better event management) + * Changed all MIME generated content to be full RFC 2822 (and friends) compliant + +11 March 2009: 4.0.1 +-------------------- + + * Fixed regression with cache clearing logic in setBody(), setEncoder() and + setCharset() + +13 March 2009: 4.0.2 +-------------------- + + * Added addTo(), addCc() etc methods. + * Allowed setTo(), setCc() etc to accept a $name parameters. + * Patched a bug in MailTransport where failed recipients were not being merged. + * Added Swift::VERSION constant + * Allowed custom autoloaders to be used + +20 March 2009: 4.0.3 +-------------------- + + * Fixed Bug where base64 encoded content could exceed 76 chars per line + * Allowed Decorator plugin to accept a custom Replacements object + +12 August 2009: 4.0.4 +-------------------- + + * Bugfixes for operating under safe mode and using the MailTransport + * Compatibility for PHP 5.3 + * Optimizations for addTo(), addCc() etc operations + * Bugfix for double-escaping issue in batch sending + +27 September 2009: 4.0.5 +------------------------ + + * Fixed a warning (#78) + * Clarified license and updated the file headers accordingly + * Added __toString() methods where toString() methods already exists + * Removed constants (SWIFT_LIB_DIRECTORY, SWIFT_MAP_DIRECTORY, SWIFT_CLASS_DIRECTORY) + * Simplified autoloading + * Added a setAuthMode() method to AuthHandler (#54) + +20 January 2010: 4.0.6 +---------------------- + + * added a PEAR package and a script to generate PEAR packages + * fixed Swift_Transport_TransportException for SMTP connection not thrown (#109) + * fixed Message-IDs are not updated properly (#118) + +-- End of Changes -- diff --git a/includes/kohana/modules/khemail/vendor/swift/LICENSE b/includes/kohana/modules/khemail/vendor/swift/LICENSE new file mode 100644 index 0000000..fc8a5de --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/includes/kohana/modules/khemail/vendor/swift/README b/includes/kohana/modules/khemail/vendor/swift/README new file mode 100644 index 0000000..794d832 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/README @@ -0,0 +1,30 @@ +Swift Mailer, by Chris Corbyn +----------------------------- + +Swift Mailer is a component based mailing solution for PHP 5. +It is released under the LGPL license. + +Homepage: http://swiftmailer.org +Documentation: http://swiftmailer.org/docs +Mailing List: http://groups.google.com/group/swiftmailer +Bugs: http://swiftmailer.lighthouseapp.com/ +Repository: http://github.com/swiftmailer/swiftmailer + +Swift Mailer is highly object-oriented by design and lends itself +to use in complex web application with a great deal of flexibility. + +For full details on usage, see the documentation. + +IMPORTANT: Users upgrading from version 3.x or earlier absolutely + MUST read the documentation. In short, the API is considerably + different so your old code won't "just work". + +If you'd like to make a donation, we are working on a system where +donations are taken on a per-feature-request basis via the website +with target amounts for each feature. In the meantime however you +may donate directly to the author via PayPal: + + PayPal: chris@w3style.co.uk + +Donations are certainly voluntary, but seriously, you donors are +complete legends and drive this project! :) diff --git a/includes/kohana/modules/khemail/vendor/swift/VERSION b/includes/kohana/modules/khemail/vendor/swift/VERSION new file mode 100644 index 0000000..2064f10 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/VERSION @@ -0,0 +1 @@ +Swift-4.0.6 diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift.php new file mode 100644 index 0000000..77abbbf --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift.php @@ -0,0 +1,57 @@ +createDependenciesFor('mime.attachment') + ); + + $this->setBody($data); + $this->setFilename($filename); + if ($contentType) + { + $this->setContentType($contentType); + } + } + + /** + * Create a new Attachment. + * @param string|Swift_OutputByteStream $data + * @param string $filename + * @param string $contentType + * @return Swift_Mime_Attachment + */ + public static function newInstance($data = null, $filename = null, + $contentType = null) + { + return new self($data, $filename, $contentType); + } + + /** + * Create a new Attachment from a filesystem path. + * @param string $path + * @param string $contentType optional + * @return Swift_Mime_Attachment + */ + public static function fromPath($path, $contentType = null) + { + return self::newInstance()->setFile( + new Swift_ByteStream_FileByteStream($path), + $contentType + ); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/AbstractFilterableInputStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/AbstractFilterableInputStream.php new file mode 100644 index 0000000..71bc3f1 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/AbstractFilterableInputStream.php @@ -0,0 +1,178 @@ +_filters[$key] = $filter; + } + + /** + * Remove an already present StreamFilter based on its $key. + * @param string $key + */ + public function removeFilter($key) + { + unset($this->_filters[$key]); + } + + /** + * Writes $bytes to the end of the stream. + * @param string $bytes + * @throws Swift_IoException + */ + public function write($bytes) + { + $this->_writeBuffer .= $bytes; + foreach ($this->_filters as $filter) + { + if ($filter->shouldBuffer($this->_writeBuffer)) + { + return; + } + } + $this->_doWrite($this->_writeBuffer); + return ++$this->_sequence; + } + + /** + * For any bytes that are currently buffered inside the stream, force them + * off the buffer. + * + * @throws Swift_IoException + */ + public function commit() + { + $this->_doWrite($this->_writeBuffer); + } + + /** + * Attach $is to this stream. + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + */ + public function bind(Swift_InputByteStream $is) + { + $this->_mirrors[] = $is; + } + + /** + * Remove an already bound stream. + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + */ + public function unbind(Swift_InputByteStream $is) + { + foreach ($this->_mirrors as $k => $stream) + { + if ($is === $stream) + { + if ($this->_writeBuffer !== '') + { + $stream->write($this->_filter($this->_writeBuffer)); + } + unset($this->_mirrors[$k]); + } + } + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + * @throws Swift_IoException + */ + public function flushBuffers() + { + if ($this->_writeBuffer !== '') + { + $this->_doWrite($this->_writeBuffer); + } + $this->_flush(); + + foreach ($this->_mirrors as $stream) + { + $stream->flushBuffers(); + } + } + + // -- Private methods + + /** Run $bytes through all filters */ + private function _filter($bytes) + { + foreach ($this->_filters as $filter) + { + $bytes = $filter->filter($bytes); + } + return $bytes; + } + + /** Just write the bytes to the stream */ + private function _doWrite($bytes) + { + $this->_commit($this->_filter($bytes)); + + foreach ($this->_mirrors as $stream) + { + $stream->write($bytes); + } + + $this->_writeBuffer = ''; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/ArrayByteStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/ArrayByteStream.php new file mode 100644 index 0000000..f918889 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/ArrayByteStream.php @@ -0,0 +1,190 @@ +_array = $stack; + $this->_arraySize = count($stack); + } + elseif (is_string($stack)) + { + $this->write($stack); + } + else + { + $this->_array = array(); + } + } + + /** + * Reads $length bytes from the stream into a string and moves the pointer + * through the stream by $length. If less bytes exist than are requested the + * remaining bytes are given instead. If no bytes are remaining at all, boolean + * false is returned. + * @param int $length + * @return string + */ + public function read($length) + { + if ($this->_offset == $this->_arraySize) + { + return false; + } + + // Don't use array slice + $end = $length + $this->_offset; + $end = $this->_arraySize<$end + ?$this->_arraySize + :$end; + $ret = ''; + for (; $this->_offset < $end; ++$this->_offset) + { + $ret .= $this->_array[$this->_offset]; + } + return $ret; + } + + /** + * Writes $bytes to the end of the stream. + * @param string $bytes + */ + public function write($bytes) + { + $to_add = str_split($bytes); + foreach ($to_add as $value) + { + $this->_array[] = $value; + } + $this->_arraySize = count($this->_array); + + foreach ($this->_mirrors as $stream) + { + $stream->write($bytes); + } + } + + /** + * Not used. + */ + public function commit() + { + } + + /** + * Attach $is to this stream. + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + */ + public function bind(Swift_InputByteStream $is) + { + $this->_mirrors[] = $is; + } + + /** + * Remove an already bound stream. + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + */ + public function unbind(Swift_InputByteStream $is) + { + foreach ($this->_mirrors as $k => $stream) + { + if ($is === $stream) + { + unset($this->_mirrors[$k]); + } + } + } + + /** + * Move the internal read pointer to $byteOffset in the stream. + * @param int $byteOffset + * @return boolean + */ + public function setReadPointer($byteOffset) + { + if ($byteOffset > $this->_arraySize) + { + $byteOffset = $this->_arraySize; + } + elseif ($byteOffset < 0) + { + $byteOffset = 0; + } + + $this->_offset = $byteOffset; + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + */ + public function flushBuffers() + { + $this->_offset = 0; + $this->_array = array(); + $this->_arraySize = 0; + + foreach ($this->_mirrors as $stream) + { + $stream->flushBuffers(); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/FileByteStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/FileByteStream.php new file mode 100644 index 0000000..14773c2 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ByteStream/FileByteStream.php @@ -0,0 +1,177 @@ +_path = $path; + $this->_mode = $writable ? 'w+b' : 'rb'; + $this->_quotes = get_magic_quotes_runtime(); + } + + /** + * Get the complete path to the file. + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Reads $length bytes from the stream into a string and moves the pointer + * through the stream by $length. If less bytes exist than are requested the + * remaining bytes are given instead. If no bytes are remaining at all, boolean + * false is returned. + * @param int $length + * @return string + * @throws Swift_IoException + */ + public function read($length) + { + $fp = $this->_getReadHandle(); + if (!feof($fp)) + { + if ($this->_quotes) + { + set_magic_quotes_runtime(0); + } + $bytes = fread($fp, $length); + if ($this->_quotes) + { + set_magic_quotes_runtime(1); + } + $this->_offset = ftell($fp); + return $bytes; + } + else + { + return false; + } + } + + /** + * Move the internal read pointer to $byteOffset in the stream. + * @param int $byteOffset + * @return boolean + */ + public function setReadPointer($byteOffset) + { + if (isset($this->_reader)) + { + fseek($this->_reader, $byteOffset, SEEK_SET); + } + $this->_offset = $byteOffset; + } + + // -- Private methods + + /** Just write the bytes to the file */ + protected function _commit($bytes) + { + fwrite($this->_getWriteHandle(), $bytes); + $this->_resetReadHandle(); + } + + /** Not used */ + protected function _flush() + { + } + + /** Get the resource for reading */ + private function _getReadHandle() + { + if (!isset($this->_reader)) + { + if (!$this->_reader = fopen($this->_path, 'rb')) + { + throw new Swift_IoException( + 'Unable to open file for reading [' . $this->_path . ']' + ); + } + fseek($this->_reader, $this->_offset, SEEK_SET); + } + return $this->_reader; + } + + /** Get the resource for writing */ + private function _getWriteHandle() + { + if (!isset($this->_writer)) + { + if (!$this->_writer = fopen($this->_path, $this->_mode)) + { + throw new Swift_IoException( + 'Unable to open file for writing [' . $this->_path . ']' + ); + } + } + return $this->_writer; + } + + /** Force a reload of the resource for writing */ + private function _resetWriteHandle() + { + if (isset($this->_writer)) + { + fclose($this->_writer); + $this->_writer = null; + } + } + + /** Force a reload of the resource for reading */ + private function _resetReadHandle() + { + if (isset($this->_reader)) + { + fclose($this->_reader); + $this->_reader = null; + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader.php new file mode 100644 index 0000000..53d39ec --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader.php @@ -0,0 +1,60 @@ + + */ +interface Swift_CharacterReader +{ + const MAP_TYPE_INVALID = 0x01; + const MAP_TYPE_FIXED_LEN = 0x02; + const MAP_TYPE_POSITIONS = 0x03; + + /** + * Returns the complete charactermap + * + * @param string $string + * @param int $startOffset + * @param array $currentMap + * @param mixed $ignoredChars + * @return int + */ + public function getCharPositions($string, $startOffset, &$currentMap, &$ignoredChars); + + /** + * Returns mapType + * @int mapType + */ + public function getMapType(); + + /** + * Returns an integer which specifies how many more bytes to read. + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * @param int[] $bytes + * @return int + */ + public function validateByteSequence($bytes, $size); + + /** + * Returns the number of bytes which should be read to start each character. + * For fixed width character sets this should be the number of + * octets-per-character. For multibyte character sets this will probably be 1. + * @return int + */ + public function getInitialByteSize(); + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/GenericFixedWidthReader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/GenericFixedWidthReader.php new file mode 100644 index 0000000..26b13ff --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/GenericFixedWidthReader.php @@ -0,0 +1,96 @@ + + */ +class Swift_CharacterReader_GenericFixedWidthReader + implements Swift_CharacterReader +{ + + /** + * The number of bytes in a single character. + * @var int + * @access private + */ + private $_width; + + /** + * Creates a new GenericFixedWidthReader using $width bytes per character. + * @param int $width + */ + public function __construct($width) + { + $this->_width = $width; + } + + /** + * Returns the complete charactermap + * + * @param string $string + * @param int $startOffset + * @param array $currentMap + * @param mixed $ignoredChars + * @return $int + */ + public function getCharPositions($string, $startOffset, &$currentMap, &$ignoredChars) + { + $strlen = strlen($string); + // % and / are CPU intensive, so, maybe find a better way + $ignored = $strlen%$this->_width; + $ignoredChars = substr($string, - $ignored); + $currentMap = $this->_width; + return ($strlen - $ignored)/$this->_width; + + } + + /** + * Returns mapType + * @int mapType + */ + public function getMapType() + { + return self::MAP_TYPE_FIXED_LEN; + } + + /** + * Returns an integer which specifies how many more bytes to read. + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * @param string $bytes + * @return int + */ + public function validateByteSequence($bytes, $size) + { + $needed = $this->_width - $size; + return ($needed > -1) + ? $needed + : -1 + ; + } + + /** + * Returns the number of bytes which should be read to start each character. + * @return int + */ + public function getInitialByteSize() + { + return $this->_width; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/UsAsciiReader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/UsAsciiReader.php new file mode 100644 index 0000000..3e0228a --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/UsAsciiReader.php @@ -0,0 +1,83 @@ +"\x07F") + { // Invalid char + $currentMap[$i+$startOffset]=$string[$i]; + } + } + return $strlen; + } + + /** + * Returns mapType + * @int mapType + */ + public function getMapType() + { + return self::MAP_TYPE_INVALID; + } + + /** + * Returns an integer which specifies how many more bytes to read. + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * @param string $bytes + * @return int + */ + public function validateByteSequence($bytes, $size) + { + $byte = reset($bytes); + if (1 == count($bytes) && $byte >= 0x00 && $byte <= 0x7F) + { + return 0; + } + else + { + return -1; + } + } + + /** + * Returns the number of bytes which should be read to start each character. + * @return int + */ + public function getInitialByteSize() + { + return 1; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/Utf8Reader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/Utf8Reader.php new file mode 100644 index 0000000..54ea9a4 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReader/Utf8Reader.php @@ -0,0 +1,183 @@ + + */ +class Swift_CharacterReader_Utf8Reader + implements Swift_CharacterReader +{ + + /** Pre-computed for optimization */ + private static $length_map=array( +//N=0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x0N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x1N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x2N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x3N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x4N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x5N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x6N + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, //0x7N + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x8N + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0x9N + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0xAN + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //0xBN + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, //0xCN + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, //0xDN + 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, //0xEN + 4,4,4,4,4,4,4,4,5,5,5,5,6,6,0,0 //0xFN + ); + private static $s_length_map=array( + "\x00"=>1, "\x01"=>1, "\x02"=>1, "\x03"=>1, "\x04"=>1, "\x05"=>1, "\x06"=>1, "\x07"=>1, + "\x08"=>1, "\x09"=>1, "\x0a"=>1, "\x0b"=>1, "\x0c"=>1, "\x0d"=>1, "\x0e"=>1, "\x0f"=>1, + "\x10"=>1, "\x11"=>1, "\x12"=>1, "\x13"=>1, "\x14"=>1, "\x15"=>1, "\x16"=>1, "\x17"=>1, + "\x18"=>1, "\x19"=>1, "\x1a"=>1, "\x1b"=>1, "\x1c"=>1, "\x1d"=>1, "\x1e"=>1, "\x1f"=>1, + "\x20"=>1, "\x21"=>1, "\x22"=>1, "\x23"=>1, "\x24"=>1, "\x25"=>1, "\x26"=>1, "\x27"=>1, + "\x28"=>1, "\x29"=>1, "\x2a"=>1, "\x2b"=>1, "\x2c"=>1, "\x2d"=>1, "\x2e"=>1, "\x2f"=>1, + "\x30"=>1, "\x31"=>1, "\x32"=>1, "\x33"=>1, "\x34"=>1, "\x35"=>1, "\x36"=>1, "\x37"=>1, + "\x38"=>1, "\x39"=>1, "\x3a"=>1, "\x3b"=>1, "\x3c"=>1, "\x3d"=>1, "\x3e"=>1, "\x3f"=>1, + "\x40"=>1, "\x41"=>1, "\x42"=>1, "\x43"=>1, "\x44"=>1, "\x45"=>1, "\x46"=>1, "\x47"=>1, + "\x48"=>1, "\x49"=>1, "\x4a"=>1, "\x4b"=>1, "\x4c"=>1, "\x4d"=>1, "\x4e"=>1, "\x4f"=>1, + "\x50"=>1, "\x51"=>1, "\x52"=>1, "\x53"=>1, "\x54"=>1, "\x55"=>1, "\x56"=>1, "\x57"=>1, + "\x58"=>1, "\x59"=>1, "\x5a"=>1, "\x5b"=>1, "\x5c"=>1, "\x5d"=>1, "\x5e"=>1, "\x5f"=>1, + "\x60"=>1, "\x61"=>1, "\x62"=>1, "\x63"=>1, "\x64"=>1, "\x65"=>1, "\x66"=>1, "\x67"=>1, + "\x68"=>1, "\x69"=>1, "\x6a"=>1, "\x6b"=>1, "\x6c"=>1, "\x6d"=>1, "\x6e"=>1, "\x6f"=>1, + "\x70"=>1, "\x71"=>1, "\x72"=>1, "\x73"=>1, "\x74"=>1, "\x75"=>1, "\x76"=>1, "\x77"=>1, + "\x78"=>1, "\x79"=>1, "\x7a"=>1, "\x7b"=>1, "\x7c"=>1, "\x7d"=>1, "\x7e"=>1, "\x7f"=>1, + "\x80"=>0, "\x81"=>0, "\x82"=>0, "\x83"=>0, "\x84"=>0, "\x85"=>0, "\x86"=>0, "\x87"=>0, + "\x88"=>0, "\x89"=>0, "\x8a"=>0, "\x8b"=>0, "\x8c"=>0, "\x8d"=>0, "\x8e"=>0, "\x8f"=>0, + "\x90"=>0, "\x91"=>0, "\x92"=>0, "\x93"=>0, "\x94"=>0, "\x95"=>0, "\x96"=>0, "\x97"=>0, + "\x98"=>0, "\x99"=>0, "\x9a"=>0, "\x9b"=>0, "\x9c"=>0, "\x9d"=>0, "\x9e"=>0, "\x9f"=>0, + "\xa0"=>0, "\xa1"=>0, "\xa2"=>0, "\xa3"=>0, "\xa4"=>0, "\xa5"=>0, "\xa6"=>0, "\xa7"=>0, + "\xa8"=>0, "\xa9"=>0, "\xaa"=>0, "\xab"=>0, "\xac"=>0, "\xad"=>0, "\xae"=>0, "\xaf"=>0, + "\xb0"=>0, "\xb1"=>0, "\xb2"=>0, "\xb3"=>0, "\xb4"=>0, "\xb5"=>0, "\xb6"=>0, "\xb7"=>0, + "\xb8"=>0, "\xb9"=>0, "\xba"=>0, "\xbb"=>0, "\xbc"=>0, "\xbd"=>0, "\xbe"=>0, "\xbf"=>0, + "\xc0"=>2, "\xc1"=>2, "\xc2"=>2, "\xc3"=>2, "\xc4"=>2, "\xc5"=>2, "\xc6"=>2, "\xc7"=>2, + "\xc8"=>2, "\xc9"=>2, "\xca"=>2, "\xcb"=>2, "\xcc"=>2, "\xcd"=>2, "\xce"=>2, "\xcf"=>2, + "\xd0"=>2, "\xd1"=>2, "\xd2"=>2, "\xd3"=>2, "\xd4"=>2, "\xd5"=>2, "\xd6"=>2, "\xd7"=>2, + "\xd8"=>2, "\xd9"=>2, "\xda"=>2, "\xdb"=>2, "\xdc"=>2, "\xdd"=>2, "\xde"=>2, "\xdf"=>2, + "\xe0"=>3, "\xe1"=>3, "\xe2"=>3, "\xe3"=>3, "\xe4"=>3, "\xe5"=>3, "\xe6"=>3, "\xe7"=>3, + "\xe8"=>3, "\xe9"=>3, "\xea"=>3, "\xeb"=>3, "\xec"=>3, "\xed"=>3, "\xee"=>3, "\xef"=>3, + "\xf0"=>4, "\xf1"=>4, "\xf2"=>4, "\xf3"=>4, "\xf4"=>4, "\xf5"=>4, "\xf6"=>4, "\xf7"=>4, + "\xf8"=>5, "\xf9"=>5, "\xfa"=>5, "\xfb"=>5, "\xfc"=>6, "\xfd"=>6, "\xfe"=>0, "\xff"=>0, + ); + + /** + * Returns the complete charactermap + * + * @param string $string + * @param int $startOffset + * @param array $currentMap + * @param mixed $ignoredChars + */ + public function getCharPositions($string, $startOffset, &$currentMap, &$ignoredChars) + { + if (!isset($currentMap['i']) || !isset($currentMap['p'])) + { + $currentMap['p'] = $currentMap['i'] = array(); + } + $strlen=strlen($string); + $charPos=count($currentMap['p']); + $foundChars=0; + $invalid=false; + for ($i=0; $i<$strlen; ++$i) + { + $char=$string[$i]; + $size=self::$s_length_map[$char]; + if ($size==0) + { + /* char is invalid, we must wait for a resync */ + $invalid=true; + continue; + } + else + { + if ($invalid==true) + { + /* We mark the chars as invalid and start a new char */ + $currentMap['p'][$charPos+$foundChars]=$startOffset+$i; + $currentMap['i'][$charPos+$foundChars]=true; + ++$foundChars; + $invalid=false; + } + if (($i+$size) > $strlen){ + $ignoredChars=substr($string, $i); + break; + } + for ($j=1; $j<$size; ++$j) + { + $char=$string[$i+$j]; + if ($char>"\x7F" && $char<"\xC0") + { + // Valid - continue parsing + } + else + { + /* char is invalid, we must wait for a resync */ + $invalid=true; + continue 2; + } + } + /* Ok we got a complete char here */ + $lastChar=$currentMap['p'][$charPos+$foundChars]=$startOffset+$i+$size; + $i+=$j-1; + ++$foundChars; + } + } + return $foundChars; + } + + /** + * Returns mapType + * @int mapType + */ + public function getMapType() + { + return self::MAP_TYPE_POSITIONS; + } + + /** + * Returns an integer which specifies how many more bytes to read. + * A positive integer indicates the number of more bytes to fetch before invoking + * this method again. + * A value of zero means this is already a valid character. + * A value of -1 means this cannot possibly be a valid character. + * @param string $bytes + * @return int + */ + public function validateByteSequence($bytes, $size) + { + if ($size<1){ + return -1; + } + $needed = self::$length_map[$bytes[0]] - $size; + return ($needed > -1) + ? $needed + : -1 + ; + } + + /** + * Returns the number of bytes which should be read to start each character. + * @return int + */ + public function getInitialByteSize() + { + return 1; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReaderFactory.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReaderFactory.php new file mode 100644 index 0000000..9e01de1 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterReaderFactory.php @@ -0,0 +1,29 @@ + $prefix . 'GenericFixedWidthReader', + 'constructor' => array(1) + ); + + $doubleByte = array( + 'class' => $prefix . 'GenericFixedWidthReader', + 'constructor' => array(2) + ); + + $fourBytes = array( + 'class' => $prefix . 'GenericFixedWidthReader', + 'constructor' => array(4) + ); + + //Utf-8 + $this->_map['utf-?8'] = array( + 'class' => $prefix . 'Utf8Reader', + 'constructor' => array() + ); + + //7-8 bit charsets + $this->_map['(us-)?ascii'] = $singleByte; + $this->_map['(iso|iec)-?8859-?[0-9]+'] = $singleByte; + $this->_map['windows-?125[0-9]'] = $singleByte; + $this->_map['cp-?[0-9]+'] = $singleByte; + $this->_map['ansi'] = $singleByte; + $this->_map['macintosh'] = $singleByte; + $this->_map['koi-?7'] = $singleByte; + $this->_map['koi-?8-?.+'] = $singleByte; + $this->_map['mik'] = $singleByte; + $this->_map['(cork|t1)'] = $singleByte; + $this->_map['v?iscii'] = $singleByte; + + //16 bits + $this->_map['(ucs-?2|utf-?16)'] = $doubleByte; + + //32 bits + $this->_map['(ucs-?4|utf-?32)'] = $fourBytes; + + //Fallback + $this->_map['.*'] = $singleByte; + } + + /** + * Returns a CharacterReader suitable for the charset applied. + * @param string $charset + * @return Swift_CharacterReader + */ + public function getReaderFor($charset) + { + $charset = trim(strtolower($charset)); + foreach ($this->_map as $pattern => $spec) + { + $re = '/^' . $pattern . '$/D'; + if (preg_match($re, $charset)) + { + if (!array_key_exists($pattern, $this->_loaded)) + { + $reflector = new ReflectionClass($spec['class']); + if ($reflector->getConstructor()) + { + $reader = $reflector->newInstanceArgs($spec['constructor']); + } + else + { + $reader = $reflector->newInstance(); + } + $this->_loaded[$pattern] = $reader; + } + return $this->_loaded[$pattern]; + } + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterStream.php new file mode 100644 index 0000000..bf91528 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterStream.php @@ -0,0 +1,86 @@ +setCharacterReaderFactory($factory); + $this->setCharacterSet($charset); + } + + /** + * Set the character set used in this CharacterStream. + * @param string $charset + */ + public function setCharacterSet($charset) + { + $this->_charset = $charset; + $this->_charReader = null; + } + + /** + * Set the CharacterReaderFactory for multi charset support. + * @param Swift_CharacterReaderFactory $factory + */ + public function setCharacterReaderFactory( + Swift_CharacterReaderFactory $factory) + { + $this->_charReaderFactory = $factory; + } + + /** + * Overwrite this character stream using the byte sequence in the byte stream. + * @param Swift_OutputByteStream $os output stream to read from + */ + public function importByteStream(Swift_OutputByteStream $os) + { + if (!isset($this->_charReader)) + { + $this->_charReader = $this->_charReaderFactory + ->getReaderFor($this->_charset); + } + + $startLength = $this->_charReader->getInitialByteSize(); + while (false !== $bytes = $os->read($startLength)) + { + $c = array(); + for ($i = 0, $len = strlen($bytes); $i < $len; ++$i) + { + $c[] = self::$_byteMap[$bytes[$i]]; + } + $size = count($c); + $need = $this->_charReader + ->validateByteSequence($c, $size); + if ($need > 0 && + false !== $bytes = $os->read($need)) + { + for ($i = 0, $len = strlen($bytes); $i < $len; ++$i) + { + $c[] = self::$_byteMap[$bytes[$i]]; + } + } + $this->_array[] = $c; + ++$this->_array_size; + } + } + + /** + * Import a string a bytes into this CharacterStream, overwriting any existing + * data in the stream. + * @param string $string + */ + public function importString($string) + { + $this->flushContents(); + $this->write($string); + } + + /** + * Read $length characters from the stream and move the internal pointer + * $length further into the stream. + * @param int $length + * @return string + */ + public function read($length) + { + if ($this->_offset == $this->_array_size) + { + return false; + } + + // Don't use array slice + $arrays = array(); + $end = $length + $this->_offset; + for ($i = $this->_offset; $i < $end; ++$i) + { + if (!isset($this->_array[$i])) + { + break; + } + $arrays[] = $this->_array[$i]; + } + $this->_offset += $i - $this->_offset; // Limit function calls + $chars = false; + foreach ($arrays as $array) + { + $chars .= implode('', array_map('chr', $array)); + } + return $chars; + } + + /** + * Read $length characters from the stream and return a 1-dimensional array + * containing there octet values. + * @param int $length + * @return int[] + */ + public function readBytes($length) + { + if ($this->_offset == $this->_array_size) + { + return false; + } + $arrays = array(); + $end = $length + $this->_offset; + for ($i = $this->_offset; $i < $end; ++$i) + { + if (!isset($this->_array[$i])) + { + break; + } + $arrays[] = $this->_array[$i]; + } + $this->_offset += ($i - $this->_offset); // Limit function calls + return call_user_func_array('array_merge', $arrays); + } + + /** + * Write $chars to the end of the stream. + * @param string $chars + */ + public function write($chars) + { + if (!isset($this->_charReader)) + { + $this->_charReader = $this->_charReaderFactory->getReaderFor( + $this->_charset); + } + + $startLength = $this->_charReader->getInitialByteSize(); + + $fp = fopen('php://memory', 'w+b'); + fwrite($fp, $chars); + unset($chars); + fseek($fp, 0, SEEK_SET); + + $buffer = array(0); + $buf_pos = 1; + $buf_len = 1; + $has_datas = true; + do + { + $bytes = array(); + // Buffer Filing + if ($buf_len - $buf_pos < $startLength) + { + $buf = array_splice($buffer, $buf_pos); + $new = $this->_reloadBuffer($fp, 100); + if ($new) + { + $buffer = array_merge($buf, $new); + $buf_len = count($buffer); + $buf_pos = 0; + } + else + { + $has_datas = false; + } + } + if ($buf_len - $buf_pos > 0) + { + $size = 0; + for ($i = 0; $i < $startLength && isset($buffer[$buf_pos]); ++$i) + { + ++$size; + $bytes[] = $buffer[$buf_pos++]; + } + $need = $this->_charReader->validateByteSequence( + $bytes, $size); + if ($need > 0) + { + if ($buf_len - $buf_pos < $need) + { + $new = $this->_reloadBuffer($fp, $need); + + if ($new) + { + $buffer = array_merge($buffer, $new); + $buf_len = count($buffer); + } + } + for ($i = 0; $i < $need && isset($buffer[$buf_pos]); ++$i) + { + $bytes[] = $buffer[$buf_pos++]; + } + } + $this->_array[] = $bytes; + ++$this->_array_size; + } + } + while ($has_datas); + + fclose($fp); + } + + /** + * Move the internal pointer to $charOffset in the stream. + * @param int $charOffset + */ + public function setPointer($charOffset) + { + if ($charOffset > $this->_array_size) + { + $charOffset = $this->_array_size; + } + elseif ($charOffset < 0) + { + $charOffset = 0; + } + $this->_offset = $charOffset; + } + + /** + * Empty the stream and reset the internal pointer. + */ + public function flushContents() + { + $this->_offset = 0; + $this->_array = array(); + $this->_array_size = 0; + } + + private function _reloadBuffer($fp, $len) + { + if (!feof($fp) && ($bytes = fread($fp, $len)) !== false) + { + $buf = array(); + for ($i = 0, $len = strlen($bytes); $i < $len; ++$i) + { + $buf[] = self::$_byteMap[$bytes[$i]]; + } + return $buf; + } + return false; + } + + private static function _initializeMaps() + { + if (!isset(self::$_charMap)) + { + self::$_charMap = array(); + for ($byte = 0; $byte < 256; ++$byte) + { + self::$_charMap[$byte] = chr($byte); + } + self::$_byteMap = array_flip(self::$_charMap); + } + } +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterStream/NgCharacterStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterStream/NgCharacterStream.php new file mode 100644 index 0000000..f090aa7 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/CharacterStream/NgCharacterStream.php @@ -0,0 +1,300 @@ +. + + */ + +//@require 'Swift/CharacterStream.php'; +//@require 'Swift/OutputByteStream.php'; + + +/** + * A CharacterStream implementation which stores characters in an internal array. + * @package Swift + * @subpackage CharacterStream + * @author Xavier De Cock + */ + +Class Swift_CharacterStream_NgCharacterStream + implements Swift_CharacterStream +{ + + /** + * The char reader (lazy-loaded) for the current charset. + * @var Swift_CharacterReader + * @access private + */ + private $_charReader; + + /** + * A factory for creatiing CharacterReader instances. + * @var Swift_CharacterReaderFactory + * @access private + */ + private $_charReaderFactory; + + /** + * The character set this stream is using. + * @var string + * @access private + */ + private $_charset; + + /** + * The datas stored as is + * + * @var string + */ + private $_datas = ""; + + /** + * Number of bytes in the stream + * + * @var int + */ + private $_datasSize = 0; + + /** + * Map + * + * @var mixed + */ + private $_map; + + /** + * Map Type + * + * @var int + */ + private $_mapType = 0; + + /** + * Number of characters in the stream + * + * @var int + */ + private $_charCount = 0; + + /** + * Position in the stream + * + * @var unknown_type + */ + private $_currentPos = 0; + + /** + * The constructor + * + * @param Swift_CharacterReaderFactory $factory + * @param unknown_type $charset + */ + public function __construct(Swift_CharacterReaderFactory $factory, + $charset) + { + $this->setCharacterReaderFactory($factory); + $this->setCharacterSet($charset); + } + + /* -- Changing parameters of the stream -- */ + + /** + * Set the character set used in this CharacterStream. + * @param string $charset + */ + public function setCharacterSet($charset) + { + $this->_charset = $charset; + $this->_charReader = null; + $this->_mapType = 0; + } + + /** + * Set the CharacterReaderFactory for multi charset support. + * @param Swift_CharacterReaderFactory $factory + */ + public function setCharacterReaderFactory( + Swift_CharacterReaderFactory $factory) + { + $this->_charReaderFactory = $factory; + } + + /** + * @see Swift_CharacterStream::flushContents() + * + */ + public function flushContents() + { + $this->_datas = null; + $this->_map = null; + $this->_charCount = 0; + $this->_currentPos = 0; + $this->_datasSize = 0; + } + + /** + * @see Swift_CharacterStream::importByteStream() + * + * @param Swift_OutputByteStream $os + */ + public function importByteStream(Swift_OutputByteStream $os) + { + $this->flushContents(); + $blocks=512; + $os->setReadPointer(0); + while(false!==($read = $os->read($blocks))) + $this->write($read); + } + + /** + * @see Swift_CharacterStream::importString() + * + * @param string $string + */ + public function importString($string) + { + $this->flushContents(); + $this->write($string); + } + + /** + * @see Swift_CharacterStream::read() + * + * @param int $length + * @return string + */ + public function read($length) + { + if ($this->_currentPos>=$this->_charCount) + { + return false; + } + $ret=false; + $length = ($this->_currentPos+$length > $this->_charCount) + ? $this->_charCount - $this->_currentPos + : $length; + switch ($this->_mapType) + { + case Swift_CharacterReader::MAP_TYPE_FIXED_LEN: + $len = $length*$this->_map; + $ret = substr($this->_datas, + $this->_currentPos * $this->_map, + $len); + $this->_currentPos += $length; + break; + + case Swift_CharacterReader::MAP_TYPE_INVALID: + $end = $this->_currentPos + $length; + $end = $end > $this->_charCount + ?$this->_charCount + :$end; + $ret = ''; + for (; $this->_currentPos < $length; ++$this->_currentPos) + { + if (isset ($this->_map[$this->_currentPos])) + { + $ret .= '?'; + } + else + { + $ret .= $this->_datas[$this->_currentPos]; + } + } + break; + + case Swift_CharacterReader::MAP_TYPE_POSITIONS: + $end = $this->_currentPos + $length; + $end = $end > $this->_charCount + ?$this->_charCount + :$end; + $ret = ''; + $start = 0; + if ($this->_currentPos>0) + { + $start = $this->_map['p'][$this->_currentPos-1]; + } + $to = $start; + for (; $this->_currentPos < $end; ++$this->_currentPos) + { + if (isset($this->_map['i'][$this->_currentPos])) { + $ret .= substr($this->_datas, $start, $to - $start).'?'; + $start = $this->_map['p'][$this->_currentPos]; + } else { + $to = $this->_map['p'][$this->_currentPos]; + } + } + $ret .= substr($this->_datas, $start, $to - $start); + break; + } + return $ret; + } + + /** + * @see Swift_CharacterStream::readBytes() + * + * @param int $length + * @return int[] + */ + public function readBytes($length) + { + $read=$this->read($length); + if ($read!==false) + { + $ret = array_map('ord', str_split($read, 1)); + return $ret; + } + return false; + } + + /** + * @see Swift_CharacterStream::setPointer() + * + * @param int $charOffset + */ + public function setPointer($charOffset) + { + if ($this->_charCount<$charOffset){ + $charOffset=$this->_charCount; + } + $this->_currentPos = $charOffset; + } + + /** + * @see Swift_CharacterStream::write() + * + * @param string $chars + */ + public function write($chars) + { + if (!isset($this->_charReader)) + { + $this->_charReader = $this->_charReaderFactory->getReaderFor( + $this->_charset); + $this->_map = array(); + $this->_mapType = $this->_charReader->getMapType(); + } + $ignored=''; + $this->_datas .= $chars; + $this->_charCount += $this->_charReader->getCharPositions(substr($this->_datas, $this->_datasSize), $this->_datasSize, $this->_map, $ignored); + if ($ignored!==false) { + $this->_datasSize=strlen($this->_datas)-strlen($ignored); + } + else + { + $this->_datasSize=strlen($this->_datas); + } + } +} \ No newline at end of file diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/DependencyContainer.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/DependencyContainer.php new file mode 100644 index 0000000..b6ba554 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/DependencyContainer.php @@ -0,0 +1,349 @@ +_store); + } + + /** + * Test if an item is registered in this container with the given name. + * @param string $itemName + * @return boolean + * @see register() + */ + public function has($itemName) + { + return array_key_exists($itemName, $this->_store) + && isset($this->_store[$itemName]['lookupType']); + } + + /** + * Lookup the item with the given $itemName. + * @param string $itemName + * @return mixed + * @throws Swift_DependencyException If the dependency is not found + * @see register() + */ + public function lookup($itemName) + { + if (!$this->has($itemName)) + { + throw new Swift_DependencyException( + 'Cannot lookup dependency "' . $itemName . '" since it is not registered.' + ); + } + + switch ($this->_store[$itemName]['lookupType']) + { + case self::TYPE_ALIAS: + return $this->_createAlias($itemName); + case self::TYPE_VALUE: + return $this->_getValue($itemName); + case self::TYPE_INSTANCE: + return $this->_createNewInstance($itemName); + case self::TYPE_SHARED: + return $this->_createSharedInstance($itemName); + } + } + + /** + * Create an array of arguments passed to the constructor of $itemName. + * @param string $itemName + * @return array + */ + public function createDependenciesFor($itemName) + { + $args = array(); + if (isset($this->_store[$itemName]['args'])) + { + $args = $this->_resolveArgs($this->_store[$itemName]['args']); + } + return $args; + } + + /** + * Register a new dependency with $itemName. + * This method returns the current DependencyContainer instance because it + * requires the use of the fluid interface to set the specific details for the + * dependency. + * + * @param string $itemName + * @return Swift_DependencyContainer + * @see asNewInstanceOf(), asSharedInstanceOf(), asValue() + */ + public function register($itemName) + { + $this->_store[$itemName] = array(); + $this->_endPoint =& $this->_store[$itemName]; + return $this; + } + + /** + * Specify the previously registered item as a literal value. + * {@link register()} must be called before this will work. + * + * @param mixed $value + * @return Swift_DependencyContainer + */ + public function asValue($value) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_VALUE; + $endPoint['value'] = $value; + return $this; + } + + /** + * Specify the previously registered item as an alias of another item. + * @param string $lookup + * @return Swift_DependencyContainer + */ + public function asAliasOf($lookup) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_ALIAS; + $endPoint['ref'] = $lookup; + return $this; + } + + /** + * Specify the previously registered item as a new instance of $className. + * {@link register()} must be called before this will work. + * Any arguments can be set with {@link withDependencies()}, + * {@link addConstructorValue()} or {@link addConstructorLookup()}. + * + * @param string $className + * @return Swift_DependencyContainer + * @see withDependencies(), addConstructorValue(), addConstructorLookup() + */ + public function asNewInstanceOf($className) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_INSTANCE; + $endPoint['className'] = $className; + return $this; + } + + /** + * Specify the previously registered item as a shared instance of $className. + * {@link register()} must be called before this will work. + * @param string $className + * @return Swift_DependencyContainer + */ + public function asSharedInstanceOf($className) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['lookupType'] = self::TYPE_SHARED; + $endPoint['className'] = $className; + return $this; + } + + /** + * Specify a list of injected dependencies for the previously registered item. + * This method takes an array of lookup names. + * + * @param array $lookups + * @return Swift_DependencyContainer + * @see addConstructorValue(), addConstructorLookup() + */ + public function withDependencies(array $lookups) + { + $endPoint =& $this->_getEndPoint(); + $endPoint['args'] = array(); + foreach ($lookups as $lookup) + { + $this->addConstructorLookup($lookup); + } + return $this; + } + + /** + * Specify a literal (non looked up) value for the constructor of the + * previously registered item. + * + * @param mixed $value + * @return Swift_DependencyContainer + * @see withDependencies(), addConstructorLookup() + */ + public function addConstructorValue($value) + { + $endPoint =& $this->_getEndPoint(); + if (!isset($endPoint['args'])) + { + $endPoint['args'] = array(); + } + $endPoint['args'][] = array('type' => 'value', 'item' => $value); + return $this; + } + + /** + * Specify a dependency lookup for the constructor of the previously + * registered item. + * + * @param string $lookup + * @return Swift_DependencyContainer + * @see withDependencies(), addConstructorValue() + */ + public function addConstructorLookup($lookup) + { + $endPoint =& $this->_getEndPoint(); + if (!isset($this->_endPoint['args'])) + { + $endPoint['args'] = array(); + } + $endPoint['args'][] = array('type' => 'lookup', 'item' => $lookup); + return $this; + } + + // -- Private methods + + /** Get the literal value with $itemName */ + private function _getValue($itemName) + { + return $this->_store[$itemName]['value']; + } + + /** Resolve an alias to another item */ + private function _createAlias($itemName) + { + return $this->lookup($this->_store[$itemName]['ref']); + } + + /** Create a fresh instance of $itemName */ + private function _createNewInstance($itemName) + { + $reflector = new ReflectionClass($this->_store[$itemName]['className']); + if ($reflector->getConstructor()) + { + return $reflector->newInstanceArgs( + $this->createDependenciesFor($itemName) + ); + } + else + { + return $reflector->newInstance(); + } + } + + /** Create and register a shared instance of $itemName */ + private function _createSharedInstance($itemName) + { + if (!isset($this->_store[$itemName]['instance'])) + { + $this->_store[$itemName]['instance'] = $this->_createNewInstance($itemName); + } + return $this->_store[$itemName]['instance']; + } + + /** Get the current endpoint in the store */ + private function &_getEndPoint() + { + if (!isset($this->_endPoint)) + { + throw new BadMethodCallException( + 'Component must first be registered by calling register()' + ); + } + return $this->_endPoint; + } + + /** Get an argument list with dependencies resolved */ + private function _resolveArgs(array $args) + { + $resolved = array(); + foreach ($args as $argDefinition) + { + switch ($argDefinition['type']) + { + case 'lookup': + $resolved[] = $this->_lookupRecursive($argDefinition['item']); + break; + case 'value': + $resolved[] = $argDefinition['item']; + break; + } + } + return $resolved; + } + + /** Resolve a single dependency with an collections */ + private function _lookupRecursive($item) + { + if (is_array($item)) + { + $collection = array(); + foreach ($item as $k => $v) + { + $collection[$k] = $this->_lookupRecursive($v); + } + return $collection; + } + else + { + return $this->lookup($item); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/DependencyException.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/DependencyException.php new file mode 100644 index 0000000..bb1681c --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/DependencyException.php @@ -0,0 +1,30 @@ +createDependenciesFor('mime.embeddedfile') + ); + + $this->setBody($data); + $this->setFilename($filename); + if ($contentType) + { + $this->setContentType($contentType); + } + } + + /** + * Create a new EmbeddedFile. + * @param string|Swift_OutputByteStream $data + * @param string $filename + * @param string $contentType + * @return Swift_Mime_EmbeddedFile + */ + public static function newInstance($data = null, $filename = null, + $contentType = null) + { + return new self($data, $filename, $contentType); + } + + /** + * Create a new EmbeddedFile from a filesystem path. + * @param string $path + * @return Swift_Mime_EmbeddedFile + */ + public static function fromPath($path) + { + return self::newInstance()->setFile( + new Swift_ByteStream_FileByteStream($path) + ); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder.php new file mode 100644 index 0000000..32aa96a --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder.php @@ -0,0 +1,32 @@ += $maxLineLength || 76 < $maxLineLength) + { + $maxLineLength = 76; + } + + $encodedString = base64_encode($string); + $firstLine = ''; + + if (0 != $firstLineOffset) + { + $firstLine = substr( + $encodedString, 0, $maxLineLength - $firstLineOffset + ) . "\r\n"; + $encodedString = substr( + $encodedString, $maxLineLength - $firstLineOffset + ); + } + + return $firstLine . trim(chunk_split($encodedString, $maxLineLength, "\r\n")); + } + + /** + * Does nothing. + */ + public function charsetChanged($charset) + { + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder/QpEncoder.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder/QpEncoder.php new file mode 100644 index 0000000..6914f6c --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder/QpEncoder.php @@ -0,0 +1,263 @@ + '=00', 1 => '=01', 2 => '=02', 3 => '=03', 4 => '=04', + 5 => '=05', 6 => '=06', 7 => '=07', 8 => '=08', 9 => '=09', + 10 => '=0A', 11 => '=0B', 12 => '=0C', 13 => '=0D', 14 => '=0E', + 15 => '=0F', 16 => '=10', 17 => '=11', 18 => '=12', 19 => '=13', + 20 => '=14', 21 => '=15', 22 => '=16', 23 => '=17', 24 => '=18', + 25 => '=19', 26 => '=1A', 27 => '=1B', 28 => '=1C', 29 => '=1D', + 30 => '=1E', 31 => '=1F', 32 => '=20', 33 => '=21', 34 => '=22', + 35 => '=23', 36 => '=24', 37 => '=25', 38 => '=26', 39 => '=27', + 40 => '=28', 41 => '=29', 42 => '=2A', 43 => '=2B', 44 => '=2C', + 45 => '=2D', 46 => '=2E', 47 => '=2F', 48 => '=30', 49 => '=31', + 50 => '=32', 51 => '=33', 52 => '=34', 53 => '=35', 54 => '=36', + 55 => '=37', 56 => '=38', 57 => '=39', 58 => '=3A', 59 => '=3B', + 60 => '=3C', 61 => '=3D', 62 => '=3E', 63 => '=3F', 64 => '=40', + 65 => '=41', 66 => '=42', 67 => '=43', 68 => '=44', 69 => '=45', + 70 => '=46', 71 => '=47', 72 => '=48', 73 => '=49', 74 => '=4A', + 75 => '=4B', 76 => '=4C', 77 => '=4D', 78 => '=4E', 79 => '=4F', + 80 => '=50', 81 => '=51', 82 => '=52', 83 => '=53', 84 => '=54', + 85 => '=55', 86 => '=56', 87 => '=57', 88 => '=58', 89 => '=59', + 90 => '=5A', 91 => '=5B', 92 => '=5C', 93 => '=5D', 94 => '=5E', + 95 => '=5F', 96 => '=60', 97 => '=61', 98 => '=62', 99 => '=63', + 100 => '=64', 101 => '=65', 102 => '=66', 103 => '=67', 104 => '=68', + 105 => '=69', 106 => '=6A', 107 => '=6B', 108 => '=6C', 109 => '=6D', + 110 => '=6E', 111 => '=6F', 112 => '=70', 113 => '=71', 114 => '=72', + 115 => '=73', 116 => '=74', 117 => '=75', 118 => '=76', 119 => '=77', + 120 => '=78', 121 => '=79', 122 => '=7A', 123 => '=7B', 124 => '=7C', + 125 => '=7D', 126 => '=7E', 127 => '=7F', 128 => '=80', 129 => '=81', + 130 => '=82', 131 => '=83', 132 => '=84', 133 => '=85', 134 => '=86', + 135 => '=87', 136 => '=88', 137 => '=89', 138 => '=8A', 139 => '=8B', + 140 => '=8C', 141 => '=8D', 142 => '=8E', 143 => '=8F', 144 => '=90', + 145 => '=91', 146 => '=92', 147 => '=93', 148 => '=94', 149 => '=95', + 150 => '=96', 151 => '=97', 152 => '=98', 153 => '=99', 154 => '=9A', + 155 => '=9B', 156 => '=9C', 157 => '=9D', 158 => '=9E', 159 => '=9F', + 160 => '=A0', 161 => '=A1', 162 => '=A2', 163 => '=A3', 164 => '=A4', + 165 => '=A5', 166 => '=A6', 167 => '=A7', 168 => '=A8', 169 => '=A9', + 170 => '=AA', 171 => '=AB', 172 => '=AC', 173 => '=AD', 174 => '=AE', + 175 => '=AF', 176 => '=B0', 177 => '=B1', 178 => '=B2', 179 => '=B3', + 180 => '=B4', 181 => '=B5', 182 => '=B6', 183 => '=B7', 184 => '=B8', + 185 => '=B9', 186 => '=BA', 187 => '=BB', 188 => '=BC', 189 => '=BD', + 190 => '=BE', 191 => '=BF', 192 => '=C0', 193 => '=C1', 194 => '=C2', + 195 => '=C3', 196 => '=C4', 197 => '=C5', 198 => '=C6', 199 => '=C7', + 200 => '=C8', 201 => '=C9', 202 => '=CA', 203 => '=CB', 204 => '=CC', + 205 => '=CD', 206 => '=CE', 207 => '=CF', 208 => '=D0', 209 => '=D1', + 210 => '=D2', 211 => '=D3', 212 => '=D4', 213 => '=D5', 214 => '=D6', + 215 => '=D7', 216 => '=D8', 217 => '=D9', 218 => '=DA', 219 => '=DB', + 220 => '=DC', 221 => '=DD', 222 => '=DE', 223 => '=DF', 224 => '=E0', + 225 => '=E1', 226 => '=E2', 227 => '=E3', 228 => '=E4', 229 => '=E5', + 230 => '=E6', 231 => '=E7', 232 => '=E8', 233 => '=E9', 234 => '=EA', + 235 => '=EB', 236 => '=EC', 237 => '=ED', 238 => '=EE', 239 => '=EF', + 240 => '=F0', 241 => '=F1', 242 => '=F2', 243 => '=F3', 244 => '=F4', + 245 => '=F5', 246 => '=F6', 247 => '=F7', 248 => '=F8', 249 => '=F9', + 250 => '=FA', 251 => '=FB', 252 => '=FC', 253 => '=FD', 254 => '=FE', + 255 => '=FF' + ); + + /** + * A map of non-encoded ascii characters. + * @var string[] + * @access protected + */ + protected static $_safeMap = array(); + + /** + * Creates a new QpEncoder for the given CharacterStream. + * @param Swift_CharacterStream $charStream to use for reading characters + * @param Swift_StreamFilter $filter if input should be canonicalized + */ + public function __construct(Swift_CharacterStream $charStream, + Swift_StreamFilter $filter = null) + { + $this->_charStream = $charStream; + if (empty(self::$_safeMap)) + { + foreach (array_merge( + array(0x09, 0x20), range(0x21, 0x3C), range(0x3E, 0x7E)) as $byte) + { + self::$_safeMap[$byte] = chr($byte); + } + } + $this->_filter = $filter; + } + + /** + * Takes an unencoded string and produces a QP encoded string from it. + * QP encoded strings have a maximum line length of 76 characters. + * If the first line needs to be shorter, indicate the difference with + * $firstLineOffset. + * @param string $string to encode + * @param int $firstLineOffset, optional + * @param int $maxLineLength, optional, 0 indicates the default of 76 chars + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, + $maxLineLength = 0) + { + if ($maxLineLength > 76 || $maxLineLength <= 0) + { + $maxLineLength = 76; + } + + $thisLineLength = $maxLineLength - $firstLineOffset; + + $lines = array(); + $lNo = 0; + $lines[$lNo] = ''; + $currentLine =& $lines[$lNo++]; + $size=$lineLen=0; + + $this->_charStream->flushContents(); + $this->_charStream->importString($string); + + //Fetching more than 4 chars at one is slower, as is fetching fewer bytes + // Conveniently 4 chars is the UTF-8 safe number since UTF-8 has up to 6 + // bytes per char and (6 * 4 * 3 = 72 chars per line) * =NN is 3 bytes + while (false !== $bytes = $this->_nextSequence()) + { + //If we're filtering the input + if (isset($this->_filter)) + { + //If we can't filter because we need more bytes + while ($this->_filter->shouldBuffer($bytes)) + { + //Then collect bytes into the buffer + if (false === $moreBytes = $this->_nextSequence(1)) + { + break; + } + + foreach ($moreBytes as $b) + { + $bytes[] = $b; + } + } + //And filter them + $bytes = $this->_filter->filter($bytes); + } + + $enc = $this->_encodeByteSequence($bytes, $size); + if ($currentLine && $lineLen+$size >= $thisLineLength) + { + $lines[$lNo] = ''; + $currentLine =& $lines[$lNo++]; + $thisLineLength = $maxLineLength; + $lineLen=0; + } + $lineLen+=$size; + $currentLine .= $enc; + } + + return $this->_standardize(implode("=\r\n", $lines)); + } + + /** + * Updates the charset used. + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_charStream->setCharacterSet($charset); + } + + // -- Protected methods + + /** + * Encode the given byte array into a verbatim QP form. + * @param int[] $bytes + * @return string + * @access protected + */ + protected function _encodeByteSequence(array $bytes, &$size) + { + $ret = ''; + $size=0; + foreach ($bytes as $b) + { + if (isset(self::$_safeMap[$b])) + { + $ret .= self::$_safeMap[$b]; + ++$size; + } + else + { + $ret .= self::$_qpMap[$b]; + $size+=3; + } + } + return $ret; + } + + /** + * Get the next sequence of bytes to read from the char stream. + * @param int $size number of bytes to read + * @return int[] + * @access protected + */ + protected function _nextSequence($size = 4) + { + return $this->_charStream->readBytes($size); + } + + /** + * Make sure CRLF is correct and HT/SPACE are in valid places. + * @param string $string + * @return string + * @access protected + */ + protected function _standardize($string) + { + $string = str_replace(array("\t=0D=0A", " =0D=0A", "=0D=0A"), + array("=09\r\n", "=20\r\n", "\r\n"), $string + ); + switch ($end = ord(substr($string, -1))) + { + case 0x09: + case 0x20: + $string = substr_replace($string, self::$_qpMap[$end], -1); + } + return $string; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder/Rfc2231Encoder.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder/Rfc2231Encoder.php new file mode 100644 index 0000000..febc6ba --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoder/Rfc2231Encoder.php @@ -0,0 +1,89 @@ +_charStream = $charStream; + } + + /** + * Takes an unencoded string and produces a string encoded according to + * RFC 2231 from it. + * @param string $string to encode + * @param int $firstLineOffset + * @param int $maxLineLength, optional, 0 indicates the default of 75 bytes + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, + $maxLineLength = 0) + { + $lines = array(); $lineCount = 0; + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + + if (0 >= $maxLineLength) + { + $maxLineLength = 75; + } + + $this->_charStream->flushContents(); + $this->_charStream->importString($string); + + $thisLineLength = $maxLineLength - $firstLineOffset; + + while (false !== $char = $this->_charStream->read(4)) + { + $encodedChar = rawurlencode($char); + if (0 != strlen($currentLine) + && strlen($currentLine . $encodedChar) > $thisLineLength) + { + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + $thisLineLength = $maxLineLength; + } + $currentLine .= $encodedChar; + } + + return implode("\r\n", $lines); + } + + /** + * Updates the charset used. + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_charStream->setCharacterSet($charset); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoding.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoding.php new file mode 100644 index 0000000..1849a82 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Encoding.php @@ -0,0 +1,70 @@ +lookup($key); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/CommandEvent.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/CommandEvent.php new file mode 100644 index 0000000..73eb585 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/CommandEvent.php @@ -0,0 +1,67 @@ +_command = $command; + $this->_successCodes = $successCodes; + } + + /** + * Get the command which was sent to the server. + * @return string + */ + public function getCommand() + { + return $this->_command; + } + + /** + * Get the numeric response codes which indicate success for this command. + * @return int[] + */ + public function getSuccessCodes() + { + return $this->_successCodes; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/CommandListener.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/CommandListener.php new file mode 100644 index 0000000..2fd7117 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/CommandListener.php @@ -0,0 +1,29 @@ +_source = $source; + } + + /** + * Get the source object of this event. + * @return object + */ + public function getSource() + { + return $this->_source; + } + + /** + * Prevent this Event from bubbling any further up the stack. + * @param boolean $cancel, optional + */ + public function cancelBubble($cancel = true) + { + $this->_bubbleCancelled = $cancel; + } + + /** + * Returns true if this Event will not bubble any further up the stack. + * @return boolean + */ + public function bubbleCancelled() + { + return $this->_bubbleCancelled; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/ResponseEvent.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/ResponseEvent.php new file mode 100644 index 0000000..addf9e7 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/ResponseEvent.php @@ -0,0 +1,65 @@ +_response = $response; + $this->_valid = $valid; + } + + /** + * Get the response which was received from the server. + * @return string + */ + public function getResponse() + { + return $this->_response; + } + + /** + * Get the success status of this Event. + * @return boolean + */ + public function isValid() + { + return $this->_valid; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/ResponseListener.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/ResponseListener.php new file mode 100644 index 0000000..092385b --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/ResponseListener.php @@ -0,0 +1,29 @@ +_message = $message; + $this->_result = self::RESULT_PENDING; + } + + /** + * Get the Transport used to send the Message. + * @return Swift_Transport + */ + public function getTransport() + { + return $this->getSource(); + } + + /** + * Get the Message being sent. + * @return Swift_Mime_Message + */ + public function getMessage() + { + return $this->_message; + } + + /** + * Set the array of addresses that failed in sending. + * @param array $recipients + */ + public function setFailedRecipients($recipients) + { + $this->_failedRecipients = $recipients; + } + + /** + * Get an recipient addresses which were not accepted for delivery. + * @return string[] + */ + public function getFailedRecipients() + { + return $this->_failedRecipients; + } + + /** + * Set the result of sending. + * @return int + */ + public function setResult($result) + { + $this->_result = $result; + } + + /** + * Get the result of this Event. + * The return value is a bitmask from + * {@link RESULT_PENDING, RESULT_SUCCESS, RESULT_TENTATIVE, RESULT_FAILED} + * @return int + */ + public function getResult() + { + return $this->_result; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/SendListener.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/SendListener.php new file mode 100644 index 0000000..a8f0cc3 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/SendListener.php @@ -0,0 +1,35 @@ +_eventMap = array( + 'Swift_Events_CommandEvent' => 'Swift_Events_CommandListener', + 'Swift_Events_ResponseEvent' => 'Swift_Events_ResponseListener', + 'Swift_Events_SendEvent' => 'Swift_Events_SendListener', + 'Swift_Events_TransportChangeEvent' => 'Swift_Events_TransportChangeListener', + 'Swift_Events_TransportExceptionEvent' => 'Swift_Events_TransportExceptionListener' + ); + } + + /** + * Create a new SendEvent for $source and $message. + * + * @param Swift_Transport $source + * @param Swift_Mime_Message + * @return Swift_Events_SendEvent + */ + public function createSendEvent(Swift_Transport $source, + Swift_Mime_Message $message) + { + return new Swift_Events_SendEvent($source, $message); + } + + /** + * Create a new CommandEvent for $source and $command. + * + * @param Swift_Transport $source + * @param string $command That will be executed + * @param array $successCodes That are needed + * @return Swift_Events_CommandEvent + */ + public function createCommandEvent(Swift_Transport $source, + $command, $successCodes = array()) + { + return new Swift_Events_CommandEvent($source, $command, $successCodes); + } + + /** + * Create a new ResponseEvent for $source and $response. + * + * @param Swift_Transport $source + * @param string $response + * @param boolean $valid If the response is valid + * @return Swift_Events_ResponseEvent + */ + public function createResponseEvent(Swift_Transport $source, + $response, $valid) + { + return new Swift_Events_ResponseEvent($source, $response, $valid); + } + + /** + * Create a new TransportChangeEvent for $source. + * + * @param Swift_Transport $source + * @return Swift_Events_TransportChangeEvent + */ + public function createTransportChangeEvent(Swift_Transport $source) + { + return new Swift_Events_TransportChangeEvent($source); + } + + /** + * Create a new TransportExceptionEvent for $source. + * + * @param Swift_Transport $source + * @param Swift_TransportException $ex + * @return Swift_Events_TransportExceptionEvent + */ + public function createTransportExceptionEvent(Swift_Transport $source, + Swift_TransportException $ex) + { + return new Swift_Events_TransportExceptionEvent($source, $ex); + } + + /** + * Bind an event listener to this dispatcher. + * + * @param Swift_Events_EventListener $listener + */ + public function bindEventListener(Swift_Events_EventListener $listener) + { + foreach ($this->_listeners as $l) + { + //Already loaded + if ($l === $listener) + { + return; + } + } + $this->_listeners[] = $listener; + } + + /** + * Dispatch the given Event to all suitable listeners. + * + * @param Swift_Events_EventObject $evt + * @param string $target method + */ + public function dispatchEvent(Swift_Events_EventObject $evt, $target) + { + $this->_prepareBubbleQueue($evt); + $this->_bubble($evt, $target); + } + + // -- Private methods + + /** Queue listeners on a stack ready for $evt to be bubbled up it */ + private function _prepareBubbleQueue(Swift_Events_EventObject $evt) + { + $this->_bubbleQueue = array(); + $evtClass = get_class($evt); + foreach ($this->_listeners as $listener) + { + if (array_key_exists($evtClass, $this->_eventMap) + && ($listener instanceof $this->_eventMap[$evtClass])) + { + $this->_bubbleQueue[] = $listener; + } + } + } + + /** Bubble $evt up the stack calling $target() on each listener */ + private function _bubble(Swift_Events_EventObject $evt, $target) + { + if (!$evt->bubbleCancelled() && $listener = array_shift($this->_bubbleQueue)) + { + $listener->$target($evt); + $this->_bubble($evt, $target); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportChangeEvent.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportChangeEvent.php new file mode 100644 index 0000000..f069a4c --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportChangeEvent.php @@ -0,0 +1,31 @@ +getSource(); + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportChangeListener.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportChangeListener.php new file mode 100644 index 0000000..ba729d0 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportChangeListener.php @@ -0,0 +1,53 @@ +_exception = $ex; + } + + /** + * Get the TransportException thrown. + * @return Swift_TransportException + */ + public function getException() + { + return $this->_exception; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportExceptionListener.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportExceptionListener.php new file mode 100644 index 0000000..d6dce94 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Events/TransportExceptionListener.php @@ -0,0 +1,30 @@ +createDependenciesFor('transport.failover') + ); + + $this->setTransports($transports); + } + + /** + * Create a new FailoverTransport instance. + * @param string $transports + * @return Swift_FailoverTransport + */ + public static function newInstance($transports = array()) + { + return new self($transports); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/FileStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/FileStream.php new file mode 100644 index 0000000..a7f894d --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/FileStream.php @@ -0,0 +1,28 @@ +setFile( + new Swift_ByteStream_FileByteStream($path) + ); + return $image; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/InputByteStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/InputByteStream.php new file mode 100644 index 0000000..e8f45f4 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/InputByteStream.php @@ -0,0 +1,72 @@ +_stream = $stream; + } + + /** + * Set a string into the cache under $itemKey for the namespace $nsKey. + * @param string $nsKey + * @param string $itemKey + * @param string $string + * @param int $mode + * @see MODE_WRITE, MODE_APPEND + */ + public function setString($nsKey, $itemKey, $string, $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) + { + case self::MODE_WRITE: + $this->_contents[$nsKey][$itemKey] = $string; + break; + case self::MODE_APPEND: + if (!$this->hasKey($nsKey, $itemKey)) + { + $this->_contents[$nsKey][$itemKey] = ''; + } + $this->_contents[$nsKey][$itemKey] .= $string; + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + } + } + + /** + * Set a ByteStream into the cache under $itemKey for the namespace $nsKey. + * @param string $nsKey + * @param string $itemKey + * @param Swift_OutputByteStream $os + * @param int $mode + * @see MODE_WRITE, MODE_APPEND + */ + public function importFromByteStream($nsKey, $itemKey, Swift_OutputByteStream $os, + $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) + { + case self::MODE_WRITE: + $this->clearKey($nsKey, $itemKey); + case self::MODE_APPEND: + if (!$this->hasKey($nsKey, $itemKey)) + { + $this->_contents[$nsKey][$itemKey] = ''; + } + while (false !== $bytes = $os->read(8192)) + { + $this->_contents[$nsKey][$itemKey] .= $bytes; + } + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + } + } + + /** + * Provides a ByteStream which when written to, writes data to $itemKey. + * NOTE: The stream will always write in append mode. + * @param string $nsKey + * @param string $itemKey + * @return Swift_InputByteStream + */ + public function getInputByteStream($nsKey, $itemKey, + Swift_InputByteStream $writeThrough = null) + { + $is = clone $this->_stream; + $is->setKeyCache($this); + $is->setNsKey($nsKey); + $is->setItemKey($itemKey); + if (isset($writeThrough)) + { + $is->setWriteThroughStream($writeThrough); + } + return $is; + } + + /** + * Get data back out of the cache as a string. + * @param string $nsKey + * @param string $itemKey + * @return string + */ + public function getString($nsKey, $itemKey) + { + $this->_prepareCache($nsKey); + if ($this->hasKey($nsKey, $itemKey)) + { + return $this->_contents[$nsKey][$itemKey]; + } + } + + /** + * Get data back out of the cache as a ByteStream. + * @param string $nsKey + * @param string $itemKey + * @param Swift_InputByteStream $is to write the data to + */ + public function exportToByteStream($nsKey, $itemKey, Swift_InputByteStream $is) + { + $this->_prepareCache($nsKey); + $is->write($this->getString($nsKey, $itemKey)); + } + + /** + * Check if the given $itemKey exists in the namespace $nsKey. + * @param string $nsKey + * @param string $itemKey + * @return boolean + */ + public function hasKey($nsKey, $itemKey) + { + $this->_prepareCache($nsKey); + return array_key_exists($itemKey, $this->_contents[$nsKey]); + } + + /** + * Clear data for $itemKey in the namespace $nsKey if it exists. + * @param string $nsKey + * @param string $itemKey + */ + public function clearKey($nsKey, $itemKey) + { + unset($this->_contents[$nsKey][$itemKey]); + } + + /** + * Clear all data in the namespace $nsKey if it exists. + * @param string $nsKey + */ + public function clearAll($nsKey) + { + unset($this->_contents[$nsKey]); + } + + // -- Private methods + + /** + * Initialize the namespace of $nsKey if needed. + * @param string $nsKey + * @access private + */ + private function _prepareCache($nsKey) + { + if (!array_key_exists($nsKey, $this->_contents)) + { + $this->_contents[$nsKey] = array(); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/KeyCache/DiskKeyCache.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/KeyCache/DiskKeyCache.php new file mode 100644 index 0000000..599fd6c --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/KeyCache/DiskKeyCache.php @@ -0,0 +1,316 @@ +_stream = $stream; + $this->_path = $path; + $this->_quotes = get_magic_quotes_runtime(); + } + + /** + * Set a string into the cache under $itemKey for the namespace $nsKey. + * @param string $nsKey + * @param string $itemKey + * @param string $string + * @param int $mode + * @throws Swift_IoException + * @see MODE_WRITE, MODE_APPEND + */ + public function setString($nsKey, $itemKey, $string, $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) + { + case self::MODE_WRITE: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + break; + case self::MODE_APPEND: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_END); + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + break; + } + fwrite($fp, $string); + } + + /** + * Set a ByteStream into the cache under $itemKey for the namespace $nsKey. + * @param string $nsKey + * @param string $itemKey + * @param Swift_OutputByteStream $os + * @param int $mode + * @see MODE_WRITE, MODE_APPEND + * @throws Swift_IoException + */ + public function importFromByteStream($nsKey, $itemKey, Swift_OutputByteStream $os, + $mode) + { + $this->_prepareCache($nsKey); + switch ($mode) + { + case self::MODE_WRITE: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + break; + case self::MODE_APPEND: + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_END); + break; + default: + throw new Swift_SwiftException( + 'Invalid mode [' . $mode . '] used to set nsKey='. + $nsKey . ', itemKey=' . $itemKey + ); + break; + } + while (false !== $bytes = $os->read(8192)) + { + fwrite($fp, $bytes); + } + } + + /** + * Provides a ByteStream which when written to, writes data to $itemKey. + * NOTE: The stream will always write in append mode. + * @param string $nsKey + * @param string $itemKey + * @return Swift_InputByteStream + */ + public function getInputByteStream($nsKey, $itemKey, + Swift_InputByteStream $writeThrough = null) + { + $is = clone $this->_stream; + $is->setKeyCache($this); + $is->setNsKey($nsKey); + $is->setItemKey($itemKey); + if (isset($writeThrough)) + { + $is->setWriteThroughStream($writeThrough); + } + return $is; + } + + /** + * Get data back out of the cache as a string. + * @param string $nsKey + * @param string $itemKey + * @return string + * @throws Swift_IoException + */ + public function getString($nsKey, $itemKey) + { + $this->_prepareCache($nsKey); + if ($this->hasKey($nsKey, $itemKey)) + { + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + if ($this->_quotes) + { + set_magic_quotes_runtime(0); + } + $str = ''; + while (!feof($fp) && false !== $bytes = fread($fp, 8192)) + { + $str .= $bytes; + } + if ($this->_quotes) + { + set_magic_quotes_runtime(1); + } + return $str; + } + } + + /** + * Get data back out of the cache as a ByteStream. + * @param string $nsKey + * @param string $itemKey + * @param Swift_InputByteStream $is to write the data to + */ + public function exportToByteStream($nsKey, $itemKey, Swift_InputByteStream $is) + { + if ($this->hasKey($nsKey, $itemKey)) + { + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_START); + if ($this->_quotes) + { + set_magic_quotes_runtime(0); + } + while (!feof($fp) && false !== $bytes = fread($fp, 8192)) + { + $is->write($bytes); + } + if ($this->_quotes) + { + set_magic_quotes_runtime(1); + } + } + } + + /** + * Check if the given $itemKey exists in the namespace $nsKey. + * @param string $nsKey + * @param string $itemKey + * @return boolean + */ + public function hasKey($nsKey, $itemKey) + { + return is_file($this->_path . '/' . $nsKey . '/' . $itemKey); + } + + /** + * Clear data for $itemKey in the namespace $nsKey if it exists. + * @param string $nsKey + * @param string $itemKey + */ + public function clearKey($nsKey, $itemKey) + { + if ($this->hasKey($nsKey, $itemKey)) + { + $fp = $this->_getHandle($nsKey, $itemKey, self::POSITION_END); + fclose($fp); + unlink($this->_path . '/' . $nsKey . '/' . $itemKey); + } + unset($this->_keys[$nsKey][$itemKey]); + } + + /** + * Clear all data in the namespace $nsKey if it exists. + * @param string $nsKey + */ + public function clearAll($nsKey) + { + if (array_key_exists($nsKey, $this->_keys)) + { + foreach ($this->_keys[$nsKey] as $itemKey=>$null) + { + $this->clearKey($nsKey, $itemKey); + } + rmdir($this->_path . '/' . $nsKey); + unset($this->_keys[$nsKey]); + } + } + + // -- Private methods + + /** + * Initialize the namespace of $nsKey if needed. + * @param string $nsKey + * @access private + */ + private function _prepareCache($nsKey) + { + $cacheDir = $this->_path . '/' . $nsKey; + if (!is_dir($cacheDir)) + { + if (!mkdir($cacheDir)) + { + throw new Swift_IoException('Failed to create cache directory ' . $cacheDir); + } + $this->_keys[$nsKey] = array(); + } + } + + /** + * Get a file handle on the cache item. + * @param string $nsKey + * @param string $itemKey + * @param int $position + * @return resource + * @access private + */ + private function _getHandle($nsKey, $itemKey, $position) + { + if (!isset($this->_keys[$nsKey]) || !array_key_exists($itemKey, $this->_keys[$nsKey])) + { + $fp = fopen($this->_path . '/' . $nsKey . '/' . $itemKey, 'w+b'); + $this->_keys[$nsKey][$itemKey] = $fp; + } + if (self::POSITION_START == $position) + { + fseek($this->_keys[$nsKey][$itemKey], 0, SEEK_SET); + } + else + { + fseek($this->_keys[$nsKey][$itemKey], 0, SEEK_END); + } + return $this->_keys[$nsKey][$itemKey]; + } + + /** + * Destructor. + */ + public function __destruct() + { + foreach ($this->_keys as $nsKey=>$null) + { + $this->clearAll($nsKey); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/KeyCache/KeyCacheInputStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/KeyCache/KeyCacheInputStream.php new file mode 100644 index 0000000..a1f4440 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/KeyCache/KeyCacheInputStream.php @@ -0,0 +1,53 @@ +_keyCache = $keyCache; + } + + /** + * Specify a stream to write through for each write(). + * @param Swift_InputByteStream $is + */ + public function setWriteThroughStream(Swift_InputByteStream $is) + { + $this->_writeThrough = $is; + } + + /** + * Writes $bytes to the end of the stream. + * @param string $bytes + * @param Swift_InputByteStream $is, optional + */ + public function write($bytes, Swift_InputByteStream $is = null) + { + $this->_keyCache->setString( + $this->_nsKey, $this->_itemKey, $bytes, Swift_KeyCache::MODE_APPEND + ); + if (isset($is)) + { + $is->write($bytes); + } + if (isset($this->_writeThrough)) + { + $this->_writeThrough->write($bytes); + } + } + + /** + * Not used. + */ + public function commit() + { + } + + /** + * Not used. + */ + public function bind(Swift_InputByteStream $is) + { + } + + /** + * Not used. + */ + public function unbind(Swift_InputByteStream $is) + { + } + + /** + * Flush the contents of the stream (empty it) and set the internal pointer + * to the beginning. + */ + public function flushBuffers() + { + $this->_keyCache->clearKey($this->_nsKey, $this->_itemKey); + } + + /** + * Set the nsKey which will be written to. + * @param string $nsKey + */ + public function setNsKey($nsKey) + { + $this->_nsKey = $nsKey; + } + + /** + * Set the itemKey which will be written to. + * @param string $itemKey + */ + public function setItemKey($itemKey) + { + $this->_itemKey = $itemKey; + } + + /** + * Any implementation should be cloneable, allowing the clone to access a + * separate $nsKey and $itemKey. + */ + public function __clone() + { + $this->_writeThrough = null; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/LoadBalancedTransport.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/LoadBalancedTransport.php new file mode 100644 index 0000000..14ae292 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/LoadBalancedTransport.php @@ -0,0 +1,48 @@ +createDependenciesFor('transport.loadbalanced') + ); + + $this->setTransports($transports); + } + + /** + * Create a new LoadBalancedTransport instance. + * @param string $transports + * @return Swift_LoadBalancedTransport + */ + public static function newInstance($transports = array()) + { + return new self($transports); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/MailTransport.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/MailTransport.php new file mode 100644 index 0000000..afe29c6 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/MailTransport.php @@ -0,0 +1,48 @@ +createDependenciesFor('transport.mail') + ); + + $this->setExtraParams($extraParams); + } + + /** + * Create a new MailTransport instance. + * @param string $extraParams To be passed to mail() + * @return Swift_MailTransport + */ + public static function newInstance($extraParams = '-f%s') + { + return new self($extraParams); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer.php new file mode 100644 index 0000000..c92feb4 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer.php @@ -0,0 +1,173 @@ +_transport = $transport; + } + + /** + * Create a new Mailer instance. + * + * @param Swift_Transport $transport + * @return Swift_Mailer + */ + public static function newInstance(Swift_Transport $transport) + { + return new self($transport); + } + + /** + * Send the given Message like it would be sent in a mail client. + * + * All recipients (with the exception of Bcc) will be able to see the other + * recipients this message was sent to. + * + * If you need to send to each recipient without disclosing details about the + * other recipients see {@link batchSend()}. + * + * Recipient/sender data will be retreived from the Message object. + * + * The return value is the number of recipients who were accepted for + * delivery. + * + * @param Swift_Mime_Message $message + * @param array &$failedRecipients, optional + * @return int + * @see batchSend() + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $failedRecipients = (array) $failedRecipients; + + if (!$this->_transport->isStarted()) + { + $this->_transport->start(); + } + + return $this->_transport->send($message, $failedRecipients); + } + + /** + * Send the given Message to all recipients individually. + * + * This differs from {@link send()} in the way headers are presented to the + * recipient. The only recipient in the "To:" field will be the individual + * recipient it was sent to. + * + * If an iterator is provided, recipients will be read from the iterator + * one-by-one, otherwise recipient data will be retreived from the Message + * object. + * + * Sender information is always read from the Message object. + * + * The return value is the number of recipients who were accepted for + * delivery. + * + * @param Swift_Mime_Message $message + * @param array &$failedRecipients, optional + * @param Swift_Mailer_RecipientIterator $it, optional + * @return int + * @see send() + */ + public function batchSend(Swift_Mime_Message $message, + &$failedRecipients = null, + Swift_Mailer_RecipientIterator $it = null) + { + $failedRecipients = (array) $failedRecipients; + + $sent = 0; + $to = $message->getTo(); + $cc = $message->getCc(); + $bcc = $message->getBcc(); + + if (!empty($cc)) + { + $message->setCc(array()); + } + if (!empty($bcc)) + { + $message->setBcc(array()); + } + + //Use an iterator if set + if (isset($it)) + { + while ($it->hasNext()) + { + $message->setTo($it->nextRecipient()); + $sent += $this->send($message, $failedRecipients); + } + } + else + { + foreach ($to as $address => $name) + { + $message->setTo(array($address => $name)); + $sent += $this->send($message, $failedRecipients); + } + } + + $message->setTo($to); + + if (!empty($cc)) + { + $message->setCc($cc); + } + if (!empty($bcc)) + { + $message->setBcc($bcc); + } + + return $sent; + } + + /** + * Register a plugin using a known unique key (e.g. myPlugin). + * + * @param Swift_Events_EventListener $plugin + * @param string $key + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_transport->registerPlugin($plugin); + } + + /** + * The Transport used to send messages. + * @return Swift_Transport + */ + public function getTransport() + { + return $this->_transport; + } +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer/ArrayRecipientIterator.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer/ArrayRecipientIterator.php new file mode 100644 index 0000000..65d60c1 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer/ArrayRecipientIterator.php @@ -0,0 +1,59 @@ +_recipients = $recipients; + } + + /** + * Returns true only if there are more recipients to send to. + * @return boolean + */ + public function hasNext() + { + return !empty($this->_recipients); + } + + /** + * Returns an array where the keys are the addresses of recipients and the + * values are the names. + * e.g. ('foo@bar' => 'Foo') or ('foo@bar' => NULL) + * @return array + */ + public function nextRecipient() + { + return array_splice($this->_recipients, 0, 1); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer/RecipientIterator.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer/RecipientIterator.php new file mode 100644 index 0000000..2713841 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mailer/RecipientIterator.php @@ -0,0 +1,34 @@ + 'Foo') or ('foo@bar' => NULL) + * @return array + */ + public function nextRecipient(); + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Message.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Message.php new file mode 100644 index 0000000..e8183ea --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Message.php @@ -0,0 +1,82 @@ +createDependenciesFor('mime.message') + ); + + if (!isset($charset)) + { + $charset = Swift_DependencyContainer::getInstance() + ->lookup('properties.charset'); + } + $this->setSubject($subject); + $this->setBody($body); + $this->setCharset($charset); + if ($contentType) + { + $this->setContentType($contentType); + } + } + + /** + * Create a new Message. + * @param string $subject + * @param string $body + * @param string $contentType + * @param string $charset + * @return Swift_Mime_Message + */ + public static function newInstance($subject = null, $body = null, + $contentType = null, $charset = null) + { + return new self($subject, $body, $contentType, $charset); + } + + /** + * Add a MimePart to this Message. + * @param string|Swift_OutputByteStream $body + * @param string $contentType + * @param string $charset + */ + public function addPart($body, $contentType = null, $charset = null) + { + return $this->attach(Swift_MimePart::newInstance( + $body, $contentType, $charset + )); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Attachment.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Attachment.php new file mode 100644 index 0000000..25ef68b --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Attachment.php @@ -0,0 +1,143 @@ +setDisposition('attachment'); + $this->setContentType('application/octet-stream'); + $this->_mimeTypes = $mimeTypes; + } + + /** + * Get the nesting level used for this attachment. + * Always returns {@link LEVEL_MIXED}. + * @return int + */ + public function getNestingLevel() + { + return self::LEVEL_MIXED; + } + + /** + * Get the Content-Disposition of this attachment. + * By default attachments have a disposition of "attachment". + * @return string + */ + public function getDisposition() + { + return $this->_getHeaderFieldModel('Content-Disposition'); + } + + /** + * Set the Content-Disposition of this attachment. + * @param string $disposition + */ + public function setDisposition($disposition) + { + if (!$this->_setHeaderFieldModel('Content-Disposition', $disposition)) + { + $this->getHeaders()->addParameterizedHeader( + 'Content-Disposition', $disposition + ); + } + return $this; + } + + /** + * Get the filename of this attachment when downloaded. + * @return string + */ + public function getFilename() + { + return $this->_getHeaderParameter('Content-Disposition', 'filename'); + } + + /** + * Set the filename of this attachment. + * @param string $filename + */ + public function setFilename($filename) + { + $this->_setHeaderParameter('Content-Disposition', 'filename', $filename); + $this->_setHeaderParameter('Content-Type', 'name', $filename); + return $this; + } + + /** + * Get the file size of this attachment. + * @return int + */ + public function getSize() + { + return $this->_getHeaderParameter('Content-Disposition', 'size'); + } + + /** + * Set the file size of this attachment. + * @param int $size + */ + public function setSize($size) + { + $this->_setHeaderParameter('Content-Disposition', 'size', $size); + return $this; + } + + /** + * Set the file that this attachment is for. + * @param Swift_FileStream $file + * @param string $contentType optional + */ + public function setFile(Swift_FileStream $file, $contentType = null) + { + $this->setFilename(basename($file->getPath())); + $this->setBody($file, $contentType); + if (!isset($contentType)) + { + $extension = strtolower(substr( + $file->getPath(), strrpos($file->getPath(), '.') + 1 + )); + + if (array_key_exists($extension, $this->_mimeTypes)) + { + $this->setContentType($this->_mimeTypes[$extension]); + } + } + return $this; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/CharsetObserver.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/CharsetObserver.php new file mode 100644 index 0000000..c26009f --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/CharsetObserver.php @@ -0,0 +1,26 @@ += $maxLineLength || 76 < $maxLineLength) + { + $maxLineLength = 76; + } + + $remainder = 0; + + while (false !== $bytes = $os->read(8190)) + { + $encoded = base64_encode($bytes); + $encodedTransformed = ''; + $thisMaxLineLength = $maxLineLength - $remainder - $firstLineOffset; + + while ($thisMaxLineLength < strlen($encoded)) + { + $encodedTransformed .= substr($encoded, 0, $thisMaxLineLength) . "\r\n"; + $firstLineOffset = 0; + $encoded = substr($encoded, $thisMaxLineLength); + $thisMaxLineLength = $maxLineLength; + $remainder = 0; + } + + if (0 < $remainingLength = strlen($encoded)) + { + $remainder += $remainingLength; + $encodedTransformed .= $encoded; + $encoded = null; + } + + $is->write($encodedTransformed); + } + } + + /** + * Get the name of this encoding scheme. + * Returns the string 'base64'. + * @return string + */ + public function getName() + { + return 'base64'; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ContentEncoder/PlainContentEncoder.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ContentEncoder/PlainContentEncoder.php new file mode 100644 index 0000000..4a725d8 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ContentEncoder/PlainContentEncoder.php @@ -0,0 +1,175 @@ +_name = $name; + $this->_canonical = $canonical; + } + + /** + * Encode a given string to produce an encoded string. + * @param string $string + * @param int $firstLineOffset, ignored + * @param int $maxLineLength - 0 means no wrapping will occur + * @return string + */ + public function encodeString($string, $firstLineOffset = 0, + $maxLineLength = 0) + { + if ($this->_canonical) + { + $string = $this->_canonicalize($string); + } + return $this->_safeWordWrap($string, $maxLineLength, "\r\n"); + } + + /** + * Encode stream $in to stream $out. + * @param Swift_OutputByteStream $in + * @param Swift_InputByteStream $out + * @param int $firstLineOffset, ignored + * @param int $maxLineLength, optional, 0 means no wrapping will occur + */ + public function encodeByteStream( + Swift_OutputByteStream $os, Swift_InputByteStream $is, $firstLineOffset = 0, + $maxLineLength = 0) + { + $leftOver = ''; + while (false !== $bytes = $os->read(8192)) + { + $toencode = $leftOver . $bytes; + if ($this->_canonical) + { + $toencode = $this->_canonicalize($toencode); + } + $wrapped = $this->_safeWordWrap($toencode, $maxLineLength, "\r\n"); + $lastLinePos = strrpos($wrapped, "\r\n"); + $leftOver = substr($wrapped, $lastLinePos); + $wrapped = substr($wrapped, 0, $lastLinePos); + + $is->write($wrapped); + } + if (strlen($leftOver)) + { + $is->write($leftOver); + } + } + + /** + * Get the name of this encoding scheme. + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Not used. + */ + public function charsetChanged($charset) + { + } + + // -- Private methods + + /** + * A safer (but weaker) wordwrap for unicode. + * @param string $string + * @param int $length + * @param string $le + * @return string + * @access private + */ + private function _safeWordwrap($string, $length = 75, $le = "\r\n") + { + if (0 >= $length) + { + return $string; + } + + $originalLines = explode($le, $string); + + $lines = array(); + $lineCount = 0; + + foreach ($originalLines as $originalLine) + { + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + + //$chunks = preg_split('/(?<=[\ \t,\.!\?\-&\+\/])/', $originalLine); + $chunks = preg_split('/(?<=\s)/', $originalLine); + + foreach ($chunks as $chunk) + { + if (0 != strlen($currentLine) + && strlen($currentLine . $chunk) > $length) + { + $lines[] = ''; + $currentLine =& $lines[$lineCount++]; + } + $currentLine .= $chunk; + } + } + + return implode("\r\n", $lines); + } + + /** + * Canonicalize string input (fix CRLF). + * @param string $string + * @return string + * @access private + */ + private function _canonicalize($string) + { + return str_replace( + array("\r\n", "\r", "\n"), + array("\n", "\n", "\r\n"), + $string + ); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php new file mode 100644 index 0000000..3beeb63 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php @@ -0,0 +1,117 @@ + 76 || $maxLineLength <= 0) + { + $maxLineLength = 76; + } + + $thisLineLength = $maxLineLength - $firstLineOffset; + + $this->_charStream->flushContents(); + $this->_charStream->importByteStream($os); + + $currentLine = ''; + $prepend = ''; + $size=$lineLen=0; + + while (false !== $bytes = $this->_nextSequence()) + { + //If we're filtering the input + if (isset($this->_filter)) + { + //If we can't filter because we need more bytes + while ($this->_filter->shouldBuffer($bytes)) + { + //Then collect bytes into the buffer + if (false === $moreBytes = $this->_nextSequence(1)) + { + break; + } + + foreach ($moreBytes as $b) + { + $bytes[] = $b; + } + } + //And filter them + $bytes = $this->_filter->filter($bytes); + } + + $enc = $this->_encodeByteSequence($bytes, $size); + if ($currentLine && $lineLen+$size >= $thisLineLength) + { + $is->write($prepend . $this->_standardize($currentLine)); + $currentLine = ''; + $prepend = "=\r\n"; + $thisLineLength = $maxLineLength; + $lineLen=0; + } + $lineLen+=$size; + $currentLine .= $enc; + } + if (strlen($currentLine)) + { + $is->write($prepend . $this->_standardize($currentLine)); + } + } + + /** + * Get the name of this encoding scheme. + * Returns the string 'quoted-printable'. + * @return string + */ + public function getName() + { + return 'quoted-printable'; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/EmbeddedFile.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/EmbeddedFile.php new file mode 100644 index 0000000..983b78d --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/EmbeddedFile.php @@ -0,0 +1,51 @@ +setDisposition('inline'); + $this->setId($this->getId()); + } + + /** + * Get the nesting level of this EmbeddedFile. + * Returns {@link LEVEL_RELATED}. + * @return int + */ + public function getNestingLevel() + { + return self::LEVEL_RELATED; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/EncodingObserver.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/EncodingObserver.php new file mode 100644 index 0000000..50472db --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/EncodingObserver.php @@ -0,0 +1,28 @@ +clearCachedValueIf($charset != $this->_charset); + $this->_charset = $charset; + if (isset($this->_encoder)) + { + $this->_encoder->charsetChanged($charset); + } + } + + /** + * Get the character set used in this Header. + * @return string + */ + public function getCharset() + { + return $this->_charset; + } + + /** + * Set the language used in this Header. + * For example, for US English, 'en-us'. + * This can be unspecified. + * @param string $lang + */ + public function setLanguage($lang) + { + $this->clearCachedValueIf($this->_lang != $lang); + $this->_lang = $lang; + } + + /** + * Get the language used in this Header. + * @return string + */ + public function getLanguage() + { + return $this->_lang; + } + + /** + * Set the encoder used for encoding the header. + * @param Swift_Mime_HeaderEncoder $encoder + */ + public function setEncoder(Swift_Mime_HeaderEncoder $encoder) + { + $this->_encoder = $encoder; + $this->setCachedValue(null); + } + + /** + * Get the encoder used for encoding this Header. + * @return Swift_Mime_HeaderEncoder + */ + public function getEncoder() + { + return $this->_encoder; + } + + /** + * Get the name of this header (e.g. charset). + * @return string + */ + public function getFieldName() + { + return $this->_name; + } + + /** + * Set the maximum length of lines in the header (excluding EOL). + * @param int $lineLength + */ + public function setMaxLineLength($lineLength) + { + $this->clearCachedValueIf($this->_lineLength != $lineLength); + $this->_lineLength = $lineLength; + } + + /** + * Get the maximum permitted length of lines in this Header. + * @return int + */ + public function getMaxLineLength() + { + return $this->_lineLength; + } + + /** + * Get this Header rendered as a RFC 2822 compliant string. + * @return string + * @throws Swift_RfcComplianceException + */ + public function toString() + { + return $this->_tokensToString($this->toTokens()); + } + + /** + * Returns a string representation of this object. + * + * @return string + * + * @see toString() + */ + public function __toString() + { + return $this->toString(); + } + + // -- Points of extension + + /** + * Set the name of this Header field. + * @param string $name + * @access protected + */ + protected function setFieldName($name) + { + $this->_name = $name; + } + + /** + * Initialize some RFC 2822 (and friends) ABNF grammar definitions. + * @access protected + */ + protected function initializeGrammar() + { + $this->_specials = array( + '(', ')', '<', '>', '[', ']', + ':', ';', '@', ',', '.', '"' + ); + + /*** Refer to RFC 2822 for ABNF grammar ***/ + + //All basic building blocks + $this->_grammar['NO-WS-CTL'] = '[\x01-\x08\x0B\x0C\x0E-\x19\x7F]'; + $this->_grammar['WSP'] = '[ \t]'; + $this->_grammar['CRLF'] = '(?:\r\n)'; + $this->_grammar['FWS'] = '(?:(?:' . $this->_grammar['WSP'] . '*' . + $this->_grammar['CRLF'] . ')?' . $this->_grammar['WSP'] . ')'; + $this->_grammar['text'] = '[\x00-\x08\x0B\x0C\x0E-\x7F]'; + $this->_grammar['quoted-pair'] = '(?:\\\\' . $this->_grammar['text'] . ')'; + $this->_grammar['ctext'] = '(?:' . $this->_grammar['NO-WS-CTL'] . + '|[\x21-\x27\x2A-\x5B\x5D-\x7E])'; + //Uses recursive PCRE (?1) -- could be a weak point?? + $this->_grammar['ccontent'] = '(?:' . $this->_grammar['ctext'] . '|' . + $this->_grammar['quoted-pair'] . '|(?1))'; + $this->_grammar['comment'] = '(\((?:' . $this->_grammar['FWS'] . '|' . + $this->_grammar['ccontent']. ')*' . $this->_grammar['FWS'] . '?\))'; + $this->_grammar['CFWS'] = '(?:(?:' . $this->_grammar['FWS'] . '?' . + $this->_grammar['comment'] . ')*(?:(?:' . $this->_grammar['FWS'] . '?' . + $this->_grammar['comment'] . ')|' . $this->_grammar['FWS'] . '))'; + $this->_grammar['qtext'] = '(?:' . $this->_grammar['NO-WS-CTL'] . + '|[\x21\x23-\x5B\x5D-\x7E])'; + $this->_grammar['qcontent'] = '(?:' . $this->_grammar['qtext'] . '|' . + $this->_grammar['quoted-pair'] . ')'; + $this->_grammar['quoted-string'] = '(?:' . $this->_grammar['CFWS'] . '?"' . + '(' . $this->_grammar['FWS'] . '?' . $this->_grammar['qcontent'] . ')*' . + $this->_grammar['FWS'] . '?"' . $this->_grammar['CFWS'] . '?)'; + $this->_grammar['atext'] = '[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]'; + $this->_grammar['atom'] = '(?:' . $this->_grammar['CFWS'] . '?' . + $this->_grammar['atext'] . '+' . $this->_grammar['CFWS'] . '?)'; + $this->_grammar['dot-atom-text'] = '(?:' . $this->_grammar['atext'] . '+' . + '(\.' . $this->_grammar['atext'] . '+)*)'; + $this->_grammar['dot-atom'] = '(?:' . $this->_grammar['CFWS'] . '?' . + $this->_grammar['dot-atom-text'] . '+' . $this->_grammar['CFWS'] . '?)'; + $this->_grammar['word'] = '(?:' . $this->_grammar['atom'] . '|' . + $this->_grammar['quoted-string'] . ')'; + $this->_grammar['phrase'] = '(?:' . $this->_grammar['word'] . '+?)'; + $this->_grammar['no-fold-quote'] = '(?:"(?:' . $this->_grammar['qtext'] . + '|' . $this->_grammar['quoted-pair'] . ')*")'; + $this->_grammar['dtext'] = '(?:' . $this->_grammar['NO-WS-CTL'] . + '|[\x21-\x5A\x5E-\x7E])'; + $this->_grammar['no-fold-literal'] = '(?:\[(?:' . $this->_grammar['dtext'] . + '|' . $this->_grammar['quoted-pair'] . ')*\])'; + + //Message IDs + $this->_grammar['id-left'] = '(?:' . $this->_grammar['dot-atom-text'] . '|' . + $this->_grammar['no-fold-quote'] . ')'; + $this->_grammar['id-right'] = '(?:' . $this->_grammar['dot-atom-text'] . '|' . + $this->_grammar['no-fold-literal'] . ')'; + + //Addresses, mailboxes and paths + $this->_grammar['local-part'] = '(?:' . $this->_grammar['dot-atom'] . '|' . + $this->_grammar['quoted-string'] . ')'; + $this->_grammar['dcontent'] = '(?:' . $this->_grammar['dtext'] . '|' . + $this->_grammar['quoted-pair'] . ')'; + $this->_grammar['domain-literal'] = '(?:' . $this->_grammar['CFWS'] . '?\[(' . + $this->_grammar['FWS'] . '?' . $this->_grammar['dcontent'] . ')*?' . + $this->_grammar['FWS'] . '?\]' . $this->_grammar['CFWS'] . '?)'; + $this->_grammar['domain'] = '(?:' . $this->_grammar['dot-atom'] . '|' . + $this->_grammar['domain-literal'] . ')'; + $this->_grammar['addr-spec'] = '(?:' . $this->_grammar['local-part'] . '@' . + $this->_grammar['domain'] . ')'; + } + + /** + * Get the grammar defined for $name token. + * @param string $name execatly as written in the RFC + * @return string + */ + protected function getGrammar($name) + { + if (array_key_exists($name, $this->_grammar)) + { + return $this->_grammar[$name]; + } + else + { + throw new Swift_RfcComplianceException( + "No such grammar '" . $name . "' defined." + ); + } + } + + /** + * Escape special characters in a string (convert to quoted-pairs). + * @param string $token + * @param string[] $include additonal chars to escape + * @param string[] $exclude chars from escaping + * @return string + */ + protected function escapeSpecials($token, $include = array(), + $exclude = array()) + { + foreach ( + array_merge(array('\\'), array_diff($this->_specials, $exclude), $include) as $char) + { + $token = str_replace($char, '\\' . $char, $token); + } + return $token; + } + + /** + * Produces a compliant, formatted RFC 2822 'phrase' based on the string given. + * @param Swift_Mime_Header $header + * @param string $string as displayed + * @param string $charset of the text + * @param Swift_Mime_HeaderEncoder $encoder + * @param boolean $shorten the first line to make remove for header name + * @return string + */ + protected function createPhrase(Swift_Mime_Header $header, $string, $charset, + Swift_Mime_HeaderEncoder $encoder = null, $shorten = false) + { + //Treat token as exactly what was given + $phraseStr = $string; + //If it's not valid + if (!preg_match('/^' . $this->_grammar['phrase'] . '$/D', $phraseStr)) + { + // .. but it is just ascii text, try escaping some characters + // and make it a quoted-string + if (preg_match('/^' . $this->_grammar['text'] . '*$/D', $phraseStr)) + { + $phraseStr = $this->escapeSpecials( + $phraseStr, array('"'), $this->_specials + ); + $phraseStr = '"' . $phraseStr . '"'; + } + else // ... otherwise it needs encoding + { + //Determine space remaining on line if first line + if ($shorten) + { + $usedLength = strlen($header->getFieldName() . ': '); + } + else + { + $usedLength = 0; + } + $phraseStr = $this->encodeWords($header, $string, $usedLength); + } + } + + return $phraseStr; + } + + /** + * Encode needed word tokens within a string of input. + * @param string $input + * @param string $usedLength, optional + * @return string + */ + protected function encodeWords(Swift_Mime_Header $header, $input, + $usedLength = -1) + { + $value = ''; + + $tokens = $this->getEncodableWordTokens($input); + + foreach ($tokens as $token) + { + //See RFC 2822, Sect 2.2 (really 2.2 ??) + if ($this->tokenNeedsEncoding($token)) + { + //Don't encode starting WSP + $firstChar = substr($token, 0, 1); + switch($firstChar) + { + case ' ': + case "\t": + $value .= $firstChar; + $token = substr($token, 1); + } + + if (-1 == $usedLength) + { + $usedLength = strlen($header->getFieldName() . ': ') + strlen($value); + } + $value .= $this->getTokenAsEncodedWord($token, $usedLength); + + $header->setMaxLineLength(76); //Forefully override + } + else + { + $value .= $token; + } + } + + return $value; + } + + /** + * Test if a token needs to be encoded or not. + * @param string $token + * @return boolean + */ + protected function tokenNeedsEncoding($token) + { + return preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token); + } + + /** + * Splits a string into tokens in blocks of words which can be encoded quickly. + * @param string $string + * @return string[] + */ + protected function getEncodableWordTokens($string) + { + $tokens = array(); + + $encodedToken = ''; + //Split at all whitespace boundaries + foreach (preg_split('~(?=[\t ])~', $string) as $token) + { + if ($this->tokenNeedsEncoding($token)) + { + $encodedToken .= $token; + } + else + { + if (strlen($encodedToken) > 0) + { + $tokens[] = $encodedToken; + $encodedToken = ''; + } + $tokens[] = $token; + } + } + if (strlen($encodedToken)) + { + $tokens[] = $encodedToken; + } + + return $tokens; + } + + /** + * Get a token as an encoded word for safe insertion into headers. + * @param string $token to encode + * @param int $firstLineOffset, optional + * @return string + */ + protected function getTokenAsEncodedWord($token, $firstLineOffset = 0) + { + //Adjust $firstLineOffset to account for space needed for syntax + $charsetDecl = $this->_charset; + if (isset($this->_lang)) + { + $charsetDecl .= '*' . $this->_lang; + } + $encodingWrapperLength = strlen( + '=?' . $charsetDecl . '?' . $this->_encoder->getName() . '??=' + ); + + if ($firstLineOffset >= 75) //Does this logic need to be here? + { + $firstLineOffset = 0; + } + + $encodedTextLines = explode("\r\n", + $this->_encoder->encodeString( + $token, $firstLineOffset, 75 - $encodingWrapperLength + ) + ); + + foreach ($encodedTextLines as $lineNum => $line) + { + $encodedTextLines[$lineNum] = '=?' . $charsetDecl . + '?' . $this->_encoder->getName() . + '?' . $line . '?='; + } + + return implode("\r\n ", $encodedTextLines); + } + + /** + * Generates tokens from the given string which include CRLF as individual tokens. + * @param string $token + * @return string[] + * @access protected + */ + protected function generateTokenLines($token) + { + return preg_split('~(\r\n)~', $token, -1, PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Set a value into the cache. + * @param string $value + * @access protected + */ + protected function setCachedValue($value) + { + $this->_cachedValue = $value; + } + + /** + * Get the value in the cache. + * @return string + * @access protected + */ + protected function getCachedValue() + { + return $this->_cachedValue; + } + + /** + * Clear the cached value if $condition is met. + * @param boolean $condition + * @access protected + */ + protected function clearCachedValueIf($condition) + { + if ($condition) + { + $this->setCachedValue(null); + } + } + + // -- Private methods + + /** + * Generate a list of all tokens in the final header. + * @param string $string input, optional + * @return string[] + * @access private + */ + protected function toTokens($string = null) + { + if (is_null($string)) + { + $string = $this->getFieldBody(); + } + + $tokens = array(); + + //Generate atoms; split at all invisible boundaries followed by WSP + foreach (preg_split('~(?=[ \t])~', $string) as $token) + { + $tokens = array_merge($tokens, $this->generateTokenLines($token)); + } + + return $tokens; + } + + /** + * Takes an array of tokens which appear in the header and turns them into + * an RFC 2822 compliant string, adding FWSP where needed. + * @param string[] $tokens + * @return string + * @access private + */ + private function _tokensToString(array $tokens) + { + $lineCount = 0; + $headerLines = array(); + $headerLines[] = $this->_name . ': '; + $currentLine =& $headerLines[$lineCount++]; + + //Build all tokens back into compliant header + foreach ($tokens as $i => $token) + { + //Line longer than specified maximum or token was just a new line + if (("\r\n" == $token) || + ($i > 0 && strlen($currentLine . $token) > $this->_lineLength) + && 0 < strlen($currentLine)) + { + $headerLines[] = ''; + $currentLine =& $headerLines[$lineCount++]; + } + + //Append token to the line + if ("\r\n" != $token) + { + $currentLine .= $token; + } + } + + //Implode with FWS (RFC 2822, 2.2.3) + return implode("\r\n", $headerLines) . "\r\n"; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/DateHeader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/DateHeader.php new file mode 100644 index 0000000..598c0c5 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/DateHeader.php @@ -0,0 +1,118 @@ + + * + * + * @param string $name of Header + */ + public function __construct($name) + { + $this->setFieldName($name); + } + + /** + * Get the type of Header that this instance represents. + * @return int + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + */ + public function getFieldType() + { + return self::TYPE_DATE; + } + + /** + * Set the model for the field body. + * This method takes a UNIX timestamp. + * @param int $model + */ + public function setFieldBodyModel($model) + { + $this->setTimestamp($model); + } + + /** + * Get the model for the field body. + * This method returns a UNIX timestamp. + * @return mixed + */ + public function getFieldBodyModel() + { + return $this->getTimestamp(); + } + + /** + * Get the UNIX timestamp of the Date in this Header. + * @return int + */ + public function getTimestamp() + { + return $this->_timestamp; + } + + /** + * Set the UNIX timestamp of the Date in this Header. + * @param int $timestamp + */ + public function setTimestamp($timestamp) + { + if (!is_null($timestamp)) + { + $timestamp = (int) $timestamp; + } + $this->clearCachedValueIf($this->_timestamp != $timestamp); + $this->_timestamp = $timestamp; + } + + /** + * Get the string value of the body in this Header. + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@link toString()} for that). + * @return string + * @see toString() + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) + { + if (isset($this->_timestamp)) + { + $this->setCachedValue(date('r', $this->_timestamp)); + } + } + return $this->getCachedValue(); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/IdentificationHeader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/IdentificationHeader.php new file mode 100644 index 0000000..55ff737 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/IdentificationHeader.php @@ -0,0 +1,161 @@ +setFieldName($name); + $this->initializeGrammar(); + } + + /** + * Get the type of Header that this instance represents. + * @return int + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + */ + public function getFieldType() + { + return self::TYPE_ID; + } + + /** + * Set the model for the field body. + * This method takes a string ID, or an array of IDs + * @param mixed $model + * @throws Swift_RfcComplianceException + */ + public function setFieldBodyModel($model) + { + $this->setId($model); + } + + /** + * Get the model for the field body. + * This method returns an array of IDs + * @return array + */ + public function getFieldBodyModel() + { + return $this->getIds(); + } + + /** + * Set the ID used in the value of this header. + * @param string $id + * @throws Swift_RfcComplianceException + */ + public function setId($id) + { + return $this->setIds(array($id)); + } + + /** + * Get the ID used in the value of this Header. + * If multiple IDs are set only the first is returned. + * @return string + */ + public function getId() + { + if (count($this->_ids) > 0) + { + return $this->_ids[0]; + } + } + + /** + * Set a collection of IDs to use in the value of this Header. + * @param string[] $ids + * @throws Swift_RfcComplianceException + */ + public function setIds(array $ids) + { + $actualIds = array(); + + foreach ($ids as $k => $id) + { + if (preg_match( + '/^' . $this->getGrammar('id-left') . '@' . + $this->getGrammar('id-right') . '$/D', + $id + )) + { + $actualIds[] = $id; + } + else + { + throw new Swift_RfcComplianceException( + 'Invalid ID given <' . $id . '>' + ); + } + } + + $this->clearCachedValueIf($this->_ids != $actualIds); + $this->_ids = $actualIds; + } + + /** + * Get the list of IDs used in this Header. + * @return string[] + */ + public function getIds() + { + return $this->_ids; + } + + /** + * Get the string value of the body in this Header. + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@link toString()} for that). + * @return string + * @see toString() + * @throws Swift_RfcComplianceException + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) + { + $angleAddrs = array(); + + foreach ($this->_ids as $id) + { + $angleAddrs[] = '<' . $id . '>'; + } + + $this->setCachedValue(implode(' ', $angleAddrs)); + } + return $this->getCachedValue(); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/MailboxHeader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/MailboxHeader.php new file mode 100644 index 0000000..77d3bba --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/MailboxHeader.php @@ -0,0 +1,316 @@ +setFieldName($name); + $this->setEncoder($encoder); + $this->initializeGrammar(); + } + + /** + * Get the type of Header that this instance represents. + * @return int + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + */ + public function getFieldType() + { + return self::TYPE_MAILBOX; + } + + /** + * Set the model for the field body. + * This method takes a string, or an array of addresses. + * @param mixed $model + * @throws Swift_RfcComplianceException + */ + public function setFieldBodyModel($model) + { + $this->setNameAddresses($model); + } + + /** + * Get the model for the field body. + * This method returns an associative array like {@link getNameAddresses()} + * @return array + * @throws Swift_RfcComplianceException + */ + public function getFieldBodyModel() + { + return $this->getNameAddresses(); + } + + /** + * Set a list of mailboxes to be shown in this Header. + * The mailboxes can be a simple array of addresses, or an array of + * key=>value pairs where (email => personalName). + * Example: + * + * setNameAddresses(array( + * 'chris@swiftmailer.org' => 'Chris Corbyn', + * 'mark@swiftmailer.org' //No associated personal name + * )); + * ?> + * + * @param string|string[] $mailboxes + * @throws Swift_RfcComplianceException + * @see __construct() + * @see setAddresses() + * @see setValue() + */ + public function setNameAddresses($mailboxes) + { + $this->_mailboxes = $this->normalizeMailboxes((array) $mailboxes); + $this->setCachedValue(null); //Clear any cached value + } + + /** + * Get the full mailbox list of this Header as an array of valid RFC 2822 strings. + * Example: + * + * 'Chris Corbyn', + * 'mark@swiftmailer.org' => 'Mark Corbyn') + * ); + * print_r($header->getNameAddressStrings()); + * // array ( + * // 0 => Chris Corbyn , + * // 1 => Mark Corbyn + * // ) + * ?> + * + * @return string[] + * @throws Swift_RfcComplianceException + * @see getNameAddresses() + * @see toString() + */ + public function getNameAddressStrings() + { + return $this->_createNameAddressStrings($this->getNameAddresses()); + } + + /** + * Get all mailboxes in this Header as key=>value pairs. + * The key is the address and the value is the name (or null if none set). + * Example: + * + * 'Chris Corbyn', + * 'mark@swiftmailer.org' => 'Mark Corbyn') + * ); + * print_r($header->getNameAddresses()); + * // array ( + * // chris@swiftmailer.org => Chris Corbyn, + * // mark@swiftmailer.org => Mark Corbyn + * // ) + * ?> + * + * @return string[] + * @see getAddresses() + * @see getNameAddressStrings() + */ + public function getNameAddresses() + { + return $this->_mailboxes; + } + + /** + * Makes this Header represent a list of plain email addresses with no names. + * Example: + * + * setAddresses( + * array('one@domain.tld', 'two@domain.tld', 'three@domain.tld') + * ); + * ?> + * + * @param string[] $addresses + * @throws Swift_RfcComplianceException + * @see setNameAddresses() + * @see setValue() + */ + public function setAddresses($addresses) + { + return $this->setNameAddresses(array_values((array) $addresses)); + } + + /** + * Get all email addresses in this Header. + * @return string[] + * @see getNameAddresses() + */ + public function getAddresses() + { + return array_keys($this->_mailboxes); + } + + /** + * Remove one or more addresses from this Header. + * @param string|string[] $addresses + */ + public function removeAddresses($addresses) + { + $this->setCachedValue(null); + foreach ((array) $addresses as $address) + { + unset($this->_mailboxes[$address]); + } + } + + /** + * Get the string value of the body in this Header. + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@link toString()} for that). + * @return string + * @throws Swift_RfcComplianceException + * @see toString() + */ + public function getFieldBody() + { + //Compute the string value of the header only if needed + if (is_null($this->getCachedValue())) + { + $this->setCachedValue($this->createMailboxListString($this->_mailboxes)); + } + return $this->getCachedValue(); + } + + // -- Points of extension + + /** + * Normalizes a user-input list of mailboxes into consistent key=>value pairs. + * @param string[] $mailboxes + * @return string[] + * @access protected + */ + protected function normalizeMailboxes(array $mailboxes) + { + $actualMailboxes = array(); + + foreach ($mailboxes as $key => $value) + { + if (is_string($key)) //key is email addr + { + $address = $key; + $name = $value; + } + else + { + $address = $value; + $name = null; + } + $this->_assertValidAddress($address); + $actualMailboxes[$address] = $name; + } + + return $actualMailboxes; + } + + /** + * Produces a compliant, formatted display-name based on the string given. + * @param string $displayName as displayed + * @param boolean $shorten the first line to make remove for header name + * @return string + * @access protected + */ + protected function createDisplayNameString($displayName, $shorten = false) + { + return $this->createPhrase($this, $displayName, + $this->getCharset(), $this->getEncoder(), $shorten + ); + } + + /** + * Creates a string form of all the mailboxes in the passed array. + * @param string[] $mailboxes + * @return string + * @throws Swift_RfcComplianceException + * @access protected + */ + protected function createMailboxListString(array $mailboxes) + { + return implode(', ', $this->_createNameAddressStrings($mailboxes)); + } + + // -- Private methods + + /** + * Return an array of strings conforming the the name-addr spec of RFC 2822. + * @param string[] $mailboxes + * @return string[] + * @access private + */ + private function _createNameAddressStrings(array $mailboxes) + { + $strings = array(); + + foreach ($mailboxes as $email => $name) + { + $mailboxStr = $email; + if (!is_null($name)) + { + $nameStr = $this->createDisplayNameString($name, empty($strings)); + $mailboxStr = $nameStr . ' <' . $mailboxStr . '>'; + } + $strings[] = $mailboxStr; + } + + return $strings; + } + + /** + * Throws an Exception if the address passed does not comply with RFC 2822. + * @param string $address + * @throws Exception If invalid. + * @access protected + */ + private function _assertValidAddress($address) + { + if (!preg_match('/^' . $this->getGrammar('addr-spec') . '$/D', + $address)) + { + throw new Swift_RfcComplianceException( + 'Address in mailbox given [' . $address . + '] does not comply with RFC 2822, 3.6.2.' + ); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/ParameterizedHeader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/ParameterizedHeader.php new file mode 100644 index 0000000..974b44e --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/ParameterizedHeader.php @@ -0,0 +1,274 @@ +setFieldName($name); + $this->setEncoder($encoder); + $this->_paramEncoder = $paramEncoder; + $this->initializeGrammar(); + $this->_tokenRe = '(?:[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+)'; + } + + /** + * Get the type of Header that this instance represents. + * @return int + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + */ + public function getFieldType() + { + return self::TYPE_PARAMETERIZED; + } + + /** + * Set the character set used in this Header. + * @param string $charset + */ + public function setCharset($charset) + { + parent::setCharset($charset); + if (isset($this->_paramEncoder)) + { + $this->_paramEncoder->charsetChanged($charset); + } + } + + /** + * Set the value of $parameter. + * @param string $parameter + * @param string $value + */ + public function setParameter($parameter, $value) + { + $this->setParameters(array_merge($this->getParameters(), array($parameter => $value))); + } + + /** + * Get the value of $parameter. + * @return string + */ + public function getParameter($parameter) + { + $params = $this->getParameters(); + return array_key_exists($parameter, $params) + ? $params[$parameter] + : null; + } + + /** + * Set an associative array of parameter names mapped to values. + * @param string[] + */ + public function setParameters(array $parameters) + { + $this->clearCachedValueIf($this->_params != $parameters); + $this->_params = $parameters; + } + + /** + * Returns an associative array of parameter names mapped to values. + * @return string[] + */ + public function getParameters() + { + return $this->_params; + } + + /** + * Get the value of this header prepared for rendering. + * @return string + */ + public function getFieldBody() //TODO: Check caching here + { + $body = parent::getFieldBody(); + foreach ($this->_params as $name => $value) + { + if (!is_null($value)) + { + //Add the parameter + $body .= '; ' . $this->_createParameter($name, $value); + } + } + return $body; + } + + // -- Protected methods + + /** + * Generate a list of all tokens in the final header. + * This doesn't need to be overridden in theory, but it is for implementation + * reasons to prevent potential breakage of attributes. + * @return string[] + * @access protected + */ + protected function toTokens($string = null) + { + $tokens = parent::toTokens(parent::getFieldBody()); + + //Try creating any parameters + foreach ($this->_params as $name => $value) + { + if (!is_null($value)) + { + //Add the semi-colon separator + $tokens[count($tokens)-1] .= ';'; + $tokens = array_merge($tokens, $this->generateTokenLines( + ' ' . $this->_createParameter($name, $value) + )); + } + } + + return $tokens; + } + + // -- Private methods + + /** + * Render a RFC 2047 compliant header parameter from the $name and $value. + * @param string $name + * @param string $value + * @return string + * @access private + */ + private function _createParameter($name, $value) + { + $origValue = $value; + + $encoded = false; + //Allow room for parameter name, indices, "=" and DQUOTEs + $maxValueLength = $this->getMaxLineLength() - strlen($name . '=*N"";') - 1; + $firstLineOffset = 0; + + //If it's not already a valid parameter value... + if (!preg_match('/^' . $this->_tokenRe . '$/D', $value)) + { + //TODO: text, or something else?? + //... and it's not ascii + if (!preg_match('/^' . $this->getGrammar('text') . '*$/D', $value)) + { + $encoded = true; + //Allow space for the indices, charset and language + $maxValueLength = $this->getMaxLineLength() - strlen($name . '*N*="";') - 1; + $firstLineOffset = strlen( + $this->getCharset() . "'" . $this->getLanguage() . "'" + ); + } + } + + //Encode if we need to + if ($encoded || strlen($value) > $maxValueLength) + { + if (isset($this->_paramEncoder)) + { + $value = $this->_paramEncoder->encodeString( + $origValue, $firstLineOffset, $maxValueLength + ); + } + else //We have to go against RFC 2183/2231 in some areas for interoperability + { + $value = $this->getTokenAsEncodedWord($origValue); + $encoded = false; + } + } + + $valueLines = isset($this->_paramEncoder) ? explode("\r\n", $value) : array($value); + + //Need to add indices + if (count($valueLines) > 1) + { + $paramLines = array(); + foreach ($valueLines as $i => $line) + { + $paramLines[] = $name . '*' . $i . + $this->_getEndOfParameterValue($line, $encoded, $i == 0); + } + return implode(";\r\n ", $paramLines); + } + else + { + return $name . $this->_getEndOfParameterValue( + $valueLines[0], $encoded, true + ); + } + } + + /** + * Returns the parameter value from the "=" and beyond. + * @param string $value to append + * @param boolean $encoded + * @param boolean $firstLine + * @return string + * @access private + */ + private function _getEndOfParameterValue($value, $encoded = false, $firstLine = false) + { + if (!preg_match('/^' . $this->_tokenRe . '$/D', $value)) + { + $value = '"' . $value . '"'; + } + $prepend = '='; + if ($encoded) + { + $prepend = '*='; + if ($firstLine) + { + $prepend = '*=' . $this->getCharset() . "'" . $this->getLanguage() . + "'"; + } + } + return $prepend . $value; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/PathHeader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/PathHeader.php new file mode 100644 index 0000000..0a8a100 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/PathHeader.php @@ -0,0 +1,126 @@ +setFieldName($name); + $this->initializeGrammar(); + } + + /** + * Get the type of Header that this instance represents. + * @return int + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + */ + public function getFieldType() + { + return self::TYPE_PATH; + } + + /** + * Set the model for the field body. + * This method takes a string for an address. + * @param string $model + * @throws Swift_RfcComplianceException + */ + public function setFieldBodyModel($model) + { + $this->setAddress($model); + } + + /** + * Get the model for the field body. + * This method returns a string email address. + * @return mixed + */ + public function getFieldBodyModel() + { + return $this->getAddress(); + } + + /** + * Set the Address which should appear in this Header. + * @param string $address + * @throws Swift_RfcComplianceException + */ + public function setAddress($address) + { + if (is_null($address)) + { + $this->_address = null; + } + elseif ('' == $address + || preg_match('/^' . $this->getGrammar('addr-spec') . '$/D', $address)) + { + $this->_address = $address; + } + else + { + throw new Swift_RfcComplianceException( + 'Address set in PathHeader does not comply with addr-spec of RFC 2822.' + ); + } + $this->setCachedValue(null); + } + + /** + * Get the address which is used in this Header (if any). + * Null is returned if no address is set. + * @return string + */ + public function getAddress() + { + return $this->_address; + } + + /** + * Get the string value of the body in this Header. + * This is not necessarily RFC 2822 compliant since folding white space will + * not be added at this stage (see {@link toString()} for that). + * @return string + * @see toString() + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) + { + if (isset($this->_address)) + { + $this->setCachedValue('<' . $this->_address . '>'); + } + } + return $this->getCachedValue(); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/UnstructuredHeader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/UnstructuredHeader.php new file mode 100644 index 0000000..fdcc21e --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Headers/UnstructuredHeader.php @@ -0,0 +1,108 @@ +setFieldName($name); + $this->setEncoder($encoder); + } + /** + * Get the type of Header that this instance represents. + * @return int + * @see TYPE_TEXT, TYPE_PARAMETERIZED, TYPE_MAILBOX + * @see TYPE_DATE, TYPE_ID, TYPE_PATH + */ + public function getFieldType() + { + return self::TYPE_TEXT; + } + + /** + * Set the model for the field body. + * This method takes a string for the field value. + * @param string $model + */ + public function setFieldBodyModel($model) + { + $this->setValue($model); + } + + /** + * Get the model for the field body. + * This method returns a string. + * @return string + */ + public function getFieldBodyModel() + { + return $this->getValue(); + } + + /** + * Get the (unencoded) value of this header. + * @return string + */ + public function getValue() + { + return $this->_value; + } + + /** + * Set the (unencoded) value of this header. + * @param string $value + */ + public function setValue($value) + { + $this->clearCachedValueIf($this->_value != $value); + $this->_value = $value; + } + + /** + * Get the value of this header prepared for rendering. + * @return string + */ + public function getFieldBody() + { + if (!$this->getCachedValue()) + { + $this->setCachedValue( + str_replace('\\', '\\\\', $this->encodeWords( + $this, $this->_value, -1, $this->getCharset(), $this->getEncoder() + )) + ); + } + return $this->getCachedValue(); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Message.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Message.php new file mode 100644 index 0000000..0496c08 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/Message.php @@ -0,0 +1,230 @@ + 'Real Name'). + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $address + * @param string $name optional + */ + public function setSender($address, $name = null); + + /** + * Get the sender address for this message. + * + * This has a higher significance than the From address. + * + * @return string + */ + public function getSender(); + + /** + * Set the From address of this message. + * + * It is permissible for multiple From addresses to be set using an array. + * + * If multiple From addresses are used, you SHOULD set the Sender address and + * according to RFC 2822, MUST set the sender address. + * + * An array can be used if display names are to be provided: i.e. + * array('email@address.com' => 'Real Name'). + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setFrom($addresses, $name = null); + + /** + * Get the From address(es) of this message. + * + * This method always returns an associative array where the keys are the + * addresses. + * + * @return string[] + */ + public function getFrom(); + + /** + * Set the Reply-To address(es). + * + * Any replies from the receiver will be sent to this address. + * + * It is permissible for multiple reply-to addresses to be set using an array. + * + * This method has the same synopsis as {@link setFrom()} and {@link setTo()}. + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setReplyTo($addresses, $name = null); + + /** + * Get the Reply-To addresses for this message. + * + * This method always returns an associative array where the keys provide the + * email addresses. + * + * @return string[] + */ + public function getReplyTo(); + + /** + * Set the To address(es). + * + * Recipients set in this field will receive a copy of this message. + * + * This method has the same synopsis as {@link setFrom()} and {@link setCc()}. + * + * If the second parameter is provided and the first is a string, then $name + * is associated with the address. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setTo($addresses, $name = null); + + /** + * Get the To addresses for this message. + * + * This method always returns an associative array, whereby the keys provide + * the actual email addresses. + * + * @return string[] + */ + public function getTo(); + + /** + * Set the Cc address(es). + * + * Recipients set in this field will receive a 'carbon-copy' of this message. + * + * This method has the same synopsis as {@link setFrom()} and {@link setTo()}. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setCc($addresses, $name = null); + + /** + * Get the Cc addresses for this message. + * + * This method always returns an associative array, whereby the keys provide + * the actual email addresses. + * + * @return string[] + */ + public function getCc(); + + /** + * Set the Bcc address(es). + * + * Recipients set in this field will receive a 'blind-carbon-copy' of this + * message. + * + * In other words, they will get the message, but any other recipients of the + * message will have no such knowledge of their receipt of it. + * + * This method has the same synopsis as {@link setFrom()} and {@link setTo()}. + * + * @param mixed $addresses + * @param string $name optional + */ + public function setBcc($addresses, $name = null); + + /** + * Get the Bcc addresses for this message. + * + * This method always returns an associative array, whereby the keys provide + * the actual email addresses. + * + * @return string[] + */ + public function getBcc(); + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/MimeEntity.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/MimeEntity.php new file mode 100644 index 0000000..2b08009 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/MimeEntity.php @@ -0,0 +1,108 @@ +setContentType('text/plain'); + if (!is_null($charset)) + { + $this->setCharset($charset); + } + } + + /** + * Set the body of this entity, either as a string, or as an instance of + * {@link Swift_OutputByteStream}. + * + * @param mixed $body + * @param string $contentType optional + * @param string $charset optional + */ + public function setBody($body, $contentType = null, $charset = null) + { + parent::setBody($body, $contentType); + if (isset($charset)) + { + $this->setCharset($charset); + } + return $this; + } + + /** + * Get the character set of this entity. + * + * @return string + */ + public function getCharset() + { + return $this->_getHeaderParameter('Content-Type', 'charset'); + } + + /** + * Set the character set of this entity. + * + * @param string $charset + */ + public function setCharset($charset) + { + $this->_setHeaderParameter('Content-Type', 'charset', $charset); + if ($charset !== $this->_userCharset) + { + $this->_clearCache(); + } + $this->_userCharset = $charset; + parent::charsetChanged($charset); + return $this; + } + + /** + * Get the format of this entity (i.e. flowed or fixed). + * + * @return string + */ + public function getFormat() + { + return $this->_getHeaderParameter('Content-Type', 'format'); + } + + /** + * Set the format of this entity (flowed or fixed). + * + * @param string $format + */ + public function setFormat($format) + { + $this->_setHeaderParameter('Content-Type', 'format', $format); + $this->_userFormat = $format; + return $this; + } + + /** + * Test if delsp is being used for this entity. + * + * @return boolean + */ + public function getDelSp() + { + return ($this->_getHeaderParameter('Content-Type', 'delsp') == 'yes') + ? true + : false; + } + + /** + * Turn delsp on or off for this entity. + * + * @param boolean $delsp + */ + public function setDelSp($delsp = true) + { + $this->_setHeaderParameter('Content-Type', 'delsp', $delsp ? 'yes' : null); + $this->_userDelSp = $delsp; + return $this; + } + + /** + * Get the nesting level of this entity. + * + * @return int + * @see LEVEL_TOP, LEVEL_ALTERNATIVE, LEVEL_MIXED, LEVEL_RELATED + */ + public function getNestingLevel() + { + return $this->_nestingLevel; + } + + /** + * Receive notification that the charset has changed on this document, or a + * parent document. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->setCharset($charset); + } + + // -- Protected methods + + /** Fix the content-type and encoding of this entity */ + protected function _fixHeaders() + { + parent::_fixHeaders(); + if (count($this->getChildren())) + { + $this->_setHeaderParameter('Content-Type', 'charset', null); + $this->_setHeaderParameter('Content-Type', 'format', null); + $this->_setHeaderParameter('Content-Type', 'delsp', null); + } + else + { + $this->setCharset($this->_userCharset); + $this->setFormat($this->_userFormat); + $this->setDelSp($this->_userDelSp); + } + } + + /** Set the nesting level of this entity */ + protected function _setNestingLevel($level) + { + $this->_nestingLevel = $level; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ParameterizedHeader.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ParameterizedHeader.php new file mode 100644 index 0000000..da65ca9 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/ParameterizedHeader.php @@ -0,0 +1,35 @@ +_encoder = $encoder; + $this->_paramEncoder = $paramEncoder; + $this->_charset = $charset; + } + + /** + * Create a new Mailbox Header with a list of $addresses. + * @param string $name + * @param array|string $addresses + * @return Swift_Mime_Header + */ + public function createMailboxHeader($name, $addresses = null) + { + $header = new Swift_Mime_Headers_MailboxHeader($name, $this->_encoder); + if (isset($addresses)) + { + $header->setFieldBodyModel($addresses); + } + $this->_setHeaderCharset($header); + return $header; + } + + /** + * Create a new Date header using $timestamp (UNIX time). + * @param string $name + * @param int $timestamp + * @return Swift_Mime_Header + */ + public function createDateHeader($name, $timestamp = null) + { + $header = new Swift_Mime_Headers_DateHeader($name); + if (isset($timestamp)) + { + $header->setFieldBodyModel($timestamp); + } + $this->_setHeaderCharset($header); + return $header; + } + + /** + * Create a new basic text header with $name and $value. + * @param string $name + * @param string $value + * @return Swift_Mime_Header + */ + public function createTextHeader($name, $value = null) + { + $header = new Swift_Mime_Headers_UnstructuredHeader($name, $this->_encoder); + if (isset($value)) + { + $header->setFieldBodyModel($value); + } + $this->_setHeaderCharset($header); + return $header; + } + + /** + * Create a new ParameterizedHeader with $name, $value and $params. + * @param string $name + * @param string $value + * @param array $params + * @return Swift_Mime_ParameterizedHeader + */ + public function createParameterizedHeader($name, $value = null, + $params = array()) + { + $header = new Swift_Mime_Headers_ParameterizedHeader($name, + $this->_encoder, (strtolower($name) == 'content-disposition') + ? $this->_paramEncoder + : null + ); + if (isset($value)) + { + $header->setFieldBodyModel($value); + } + foreach ($params as $k => $v) + { + $header->setParameter($k, $v); + } + $this->_setHeaderCharset($header); + return $header; + } + + /** + * Create a new ID header for Message-ID or Content-ID. + * @param string $name + * @param string|array $ids + * @return Swift_Mime_Header + */ + public function createIdHeader($name, $ids = null) + { + $header = new Swift_Mime_Headers_IdentificationHeader($name); + if (isset($ids)) + { + $header->setFieldBodyModel($ids); + } + $this->_setHeaderCharset($header); + return $header; + } + + /** + * Create a new Path header with an address (path) in it. + * @param string $name + * @param string $path + * @return Swift_Mime_Header + */ + public function createPathHeader($name, $path = null) + { + $header = new Swift_Mime_Headers_PathHeader($name); + if (isset($path)) + { + $header->setFieldBodyModel($path); + } + $this->_setHeaderCharset($header); + return $header; + } + + /** + * Notify this observer that the entity's charset has changed. + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_charset = $charset; + $this->_encoder->charsetChanged($charset); + $this->_paramEncoder->charsetChanged($charset); + } + + // -- Private methods + + /** Apply the charset to the Header */ + private function _setHeaderCharset(Swift_Mime_Header $header) + { + if (isset($this->_charset)) + { + $header->setCharset($this->_charset); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleHeaderSet.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleHeaderSet.php new file mode 100644 index 0000000..eeb0221 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleHeaderSet.php @@ -0,0 +1,396 @@ +_factory = $factory; + if (isset($charset)) + { + $this->setCharset($charset); + } + } + + /** + * Set the charset used by these headers. + * + * @param string $charset + */ + public function setCharset($charset) + { + $this->_charset = $charset; + $this->_factory->charsetChanged($charset); + $this->_notifyHeadersOfCharset($charset); + } + + /** + * Add a new Mailbox Header with a list of $addresses. + * + * @param string $name + * @param array|string $addresses + */ + public function addMailboxHeader($name, $addresses = null) + { + $this->_storeHeader($name, + $this->_factory->createMailboxHeader($name, $addresses)); + } + + /** + * Add a new Date header using $timestamp (UNIX time). + * + * @param string $name + * @param int $timestamp + */ + public function addDateHeader($name, $timestamp = null) + { + $this->_storeHeader($name, + $this->_factory->createDateHeader($name, $timestamp)); + } + + /** + * Add a new basic text header with $name and $value. + * + * @param string $name + * @param string $value + */ + public function addTextHeader($name, $value = null) + { + $this->_storeHeader($name, + $this->_factory->createTextHeader($name, $value)); + } + + /** + * Add a new ParameterizedHeader with $name, $value and $params. + * + * @param string $name + * @param string $value + * @param array $params + */ + public function addParameterizedHeader($name, $value = null, + $params = array()) + { + $this->_storeHeader($name, + $this->_factory->createParameterizedHeader($name, $value, + $params)); + } + + /** + * Add a new ID header for Message-ID or Content-ID. + * + * @param string $name + * @param string|array $ids + */ + public function addIdHeader($name, $ids = null) + { + $this->_storeHeader($name, $this->_factory->createIdHeader($name, $ids)); + } + + /** + * Add a new Path header with an address (path) in it. + * + * @param string $name + * @param string $path + */ + public function addPathHeader($name, $path = null) + { + $this->_storeHeader($name, $this->_factory->createPathHeader($name, $path)); + } + + /** + * Returns true if at least one header with the given $name exists. + * + * If multiple headers match, the actual one may be specified by $index. + * + * @param string $name + * @param int $index + * + * @return boolean + */ + public function has($name, $index = 0) + { + $lowerName = strtolower($name); + return array_key_exists($lowerName, $this->_headers) + && array_key_exists($index, $this->_headers[$lowerName]); + } + + /** + * Set a header in the HeaderSet. + * + * The header may be a previously fetched header via {@link get()} or it may + * be one that has been created separately. + * + * If $index is specified, the header will be inserted into the set at this + * offset. + * + * @param Swift_Mime_Header $header + * @param int $index + */ + public function set(Swift_Mime_Header $header, $index = 0) + { + $this->_storeHeader($header->getFieldName(), $header, $index); + } + + /** + * Get the header with the given $name. + * + * If multiple headers match, the actual one may be specified by $index. + * Returns NULL if none present. + * + * @param string $name + * @param int $index + * + * @return Swift_Mime_Header + */ + public function get($name, $index = 0) + { + if ($this->has($name, $index)) + { + $lowerName = strtolower($name); + return $this->_headers[$lowerName][$index]; + } + } + + /** + * Get all headers with the given $name. + * + * @param string $name + * + * @return array + */ + public function getAll($name = null) + { + if (!isset($name)) + { + $headers = array(); + foreach ($this->_headers as $collection) + { + $headers = array_merge($headers, $collection); + } + return $headers; + } + + $lowerName = strtolower($name); + if (!array_key_exists($lowerName, $this->_headers)) + { + return array(); + } + return $this->_headers[$lowerName]; + } + + /** + * Remove the header with the given $name if it's set. + * + * If multiple headers match, the actual one may be specified by $index. + * + * @param string $name + * @param int $index + */ + public function remove($name, $index = 0) + { + $lowerName = strtolower($name); + unset($this->_headers[$lowerName][$index]); + } + + /** + * Remove all headers with the given $name. + * + * @param string $name + */ + public function removeAll($name) + { + $lowerName = strtolower($name); + unset($this->_headers[$lowerName]); + } + + /** + * Create a new instance of this HeaderSet. + * + * @return Swift_Mime_HeaderSet + */ + public function newInstance() + { + return new self($this->_factory); + } + + /** + * Define a list of Header names as an array in the correct order. + * + * These Headers will be output in the given order where present. + * + * @param array $sequence + */ + public function defineOrdering(array $sequence) + { + $this->_order = array_flip(array_map('strtolower', $sequence)); + } + + /** + * Set a list of header names which must always be displayed when set. + * + * Usually headers without a field value won't be output unless set here. + * + * @param array $names + */ + public function setAlwaysDisplayed(array $names) + { + $this->_required = array_flip(array_map('strtolower', $names)); + } + + /** + * Notify this observer that the entity's charset has changed. + * + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->setCharset($charset); + } + + /** + * Returns a string with a representation of all headers. + * + * @return string + */ + public function toString() + { + $string = ''; + $headers = $this->_headers; + if ($this->_canSort()) + { + uksort($headers, array($this, '_sortHeaders')); + } + foreach ($headers as $collection) + { + foreach ($collection as $header) + { + if ($this->_isDisplayed($header) || $header->getFieldBody() != '') + { + $string .= $header->toString(); + } + } + } + return $string; + } + + /** + * Returns a string representation of this object. + * + * @return string + * + * @see toString() + */ + public function __toString() + { + return $this->toString(); + } + + // -- Private methods + + /** Save a Header to the internal collection */ + private function _storeHeader($name, Swift_Mime_Header $header, $offset = null) + { + if (!isset($this->_headers[strtolower($name)])) + { + $this->_headers[strtolower($name)] = array(); + } + if (!isset($offset)) + { + $this->_headers[strtolower($name)][] = $header; + } + else + { + $this->_headers[strtolower($name)][$offset] = $header; + } + } + + /** Test if the headers can be sorted */ + private function _canSort() + { + return count($this->_order) > 0; + } + + /** uksort() algorithm for Header ordering */ + private function _sortHeaders($a, $b) + { + $lowerA = strtolower($a); + $lowerB = strtolower($b); + $aPos = array_key_exists($lowerA, $this->_order) + ? $this->_order[$lowerA] + : -1; + $bPos = array_key_exists($lowerB, $this->_order) + ? $this->_order[$lowerB] + : -1; + + if ($aPos == -1) + { + return 1; + } + elseif ($bPos == -1) + { + return -1; + } + + return ($aPos < $bPos) ? -1 : 1; + } + + /** Test if the given Header is always displayed */ + private function _isDisplayed(Swift_Mime_Header $header) + { + return array_key_exists(strtolower($header->getFieldName()), $this->_required); + } + + /** Notify all Headers of the new charset */ + private function _notifyHeadersOfCharset($charset) + { + foreach ($this->_headers as $headerGroup) + { + foreach ($headerGroup as $header) + { + $header->setCharset($charset); + } + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleMessage.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleMessage.php new file mode 100644 index 0000000..bbe1e8f --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleMessage.php @@ -0,0 +1,609 @@ +getHeaders()->defineOrdering(array( + 'Return-Path', + 'Sender', + 'Message-ID', + 'Date', + 'Subject', + 'From', + 'Reply-To', + 'To', + 'Cc', + 'Bcc', + 'MIME-Version', + 'Content-Type', + 'Content-Transfer-Encoding' + )); + $this->getHeaders()->setAlwaysDisplayed( + array('Date', 'Message-ID', 'From') + ); + $this->getHeaders()->addTextHeader('MIME-Version', '1.0'); + $this->setDate(time()); + $this->setId($this->getId()); + $this->getHeaders()->addMailboxHeader('From'); + } + + /** + * Always returns {@link LEVEL_TOP} for a message instance. + * @return int + */ + public function getNestingLevel() + { + return self::LEVEL_TOP; + } + + /** + * Set the subject of this message. + * @param string $subject + */ + public function setSubject($subject) + { + if (!$this->_setHeaderFieldModel('Subject', $subject)) + { + $this->getHeaders()->addTextHeader('Subject', $subject); + } + return $this; + } + + /** + * Get the subject of this message. + * @return string + */ + public function getSubject() + { + return $this->_getHeaderFieldModel('Subject'); + } + + /** + * Set the date at which this message was created. + * @param int $date + */ + public function setDate($date) + { + if (!$this->_setHeaderFieldModel('Date', $date)) + { + $this->getHeaders()->addDateHeader('Date', $date); + } + return $this; + } + + /** + * Get the date at which this message was created. + * @return int + */ + public function getDate() + { + return $this->_getHeaderFieldModel('Date'); + } + + /** + * Set the return-path (the bounce address) of this message. + * @param string $address + */ + public function setReturnPath($address) + { + if (!$this->_setHeaderFieldModel('Return-Path', $address)) + { + $this->getHeaders()->addPathHeader('Return-Path', $address); + } + return $this; + } + + /** + * Get the return-path (bounce address) of this message. + * @return string + */ + public function getReturnPath() + { + return $this->_getHeaderFieldModel('Return-Path'); + } + + /** + * Set the sender of this message. + * This does not override the From field, but it has a higher significance. + * @param string $sender + * @param string $name optional + */ + public function setSender($address, $name = null) + { + if (!is_array($address) && isset($name)) + { + $address = array($address => $name); + } + + if (!$this->_setHeaderFieldModel('Sender', (array) $address)) + { + $this->getHeaders()->addMailboxHeader('Sender', (array) $address); + } + return $this; + } + + /** + * Get the sender of this message. + * @return string + */ + public function getSender() + { + return $this->_getHeaderFieldModel('Sender'); + } + + /** + * Add a From: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + */ + public function addFrom($address, $name = null) + { + $current = $this->getFrom(); + $current[$address] = $name; + return $this->setFrom($current); + } + + /** + * Set the from address of this message. + * + * You may pass an array of addresses if this message is from multiple people. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param string $addresses + * @param string $name optional + */ + public function setFrom($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) + { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('From', (array) $addresses)) + { + $this->getHeaders()->addMailboxHeader('From', (array) $addresses); + } + return $this; + } + + /** + * Get the from address of this message. + * + * @return string + */ + public function getFrom() + { + return $this->_getHeaderFieldModel('From'); + } + + /** + * Add a Reply-To: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + */ + public function addReplyTo($address, $name = null) + { + $current = $this->getReplyTo(); + $current[$address] = $name; + return $this->setReplyTo($current); + } + + /** + * Set the reply-to address of this message. + * + * You may pass an array of addresses if replies will go to multiple people. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param string $addresses + * @param string $name optional + */ + public function setReplyTo($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) + { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('Reply-To', (array) $addresses)) + { + $this->getHeaders()->addMailboxHeader('Reply-To', (array) $addresses); + } + return $this; + } + + /** + * Get the reply-to address of this message. + * + * @return string + */ + public function getReplyTo() + { + return $this->_getHeaderFieldModel('Reply-To'); + } + + /** + * Add a To: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + */ + public function addTo($address, $name = null) + { + $current = $this->getTo(); + $current[$address] = $name; + return $this->setTo($current); + } + + /** + * Set the to addresses of this message. + * + * If multiple recipients will receive the message and array should be used. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param array $addresses + * @param string $name optional + */ + public function setTo($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) + { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('To', (array) $addresses)) + { + $this->getHeaders()->addMailboxHeader('To', (array) $addresses); + } + return $this; + } + + /** + * Get the To addresses of this message. + * + * @return array + */ + public function getTo() + { + return $this->_getHeaderFieldModel('To'); + } + + /** + * Add a Cc: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + */ + public function addCc($address, $name = null) + { + $current = $this->getCc(); + $current[$address] = $name; + return $this->setCc($current); + } + + /** + * Set the Cc addresses of this message. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param array $addresses + * @param string $name optional + */ + public function setCc($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) + { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('Cc', (array) $addresses)) + { + $this->getHeaders()->addMailboxHeader('Cc', (array) $addresses); + } + return $this; + } + + /** + * Get the Cc address of this message. + * + * @return array + */ + public function getCc() + { + return $this->_getHeaderFieldModel('Cc'); + } + + /** + * Add a Bcc: address to this message. + * + * If $name is passed this name will be associated with the address. + * + * @param string $address + * @param string $name optional + */ + public function addBcc($address, $name = null) + { + $current = $this->getBcc(); + $current[$address] = $name; + return $this->setBcc($current); + } + + /** + * Set the Bcc addresses of this message. + * + * If $name is passed and the first parameter is a string, this name will be + * associated with the address. + * + * @param array $addresses + * @param string $name optional + */ + public function setBcc($addresses, $name = null) + { + if (!is_array($addresses) && isset($name)) + { + $addresses = array($addresses => $name); + } + + if (!$this->_setHeaderFieldModel('Bcc', (array) $addresses)) + { + $this->getHeaders()->addMailboxHeader('Bcc', (array) $addresses); + } + return $this; + } + + /** + * Get the Bcc addresses of this message. + * + * @return array + */ + public function getBcc() + { + return $this->_getHeaderFieldModel('Bcc'); + } + + /** + * Set the priority of this message. + * The value is an integer where 1 is the highest priority and 5 is the lowest. + * @param int $priority + */ + public function setPriority($priority) + { + $priorityMap = array( + 1 => 'Highest', + 2 => 'High', + 3 => 'Normal', + 4 => 'Low', + 5 => 'Lowest' + ); + $pMapKeys = array_keys($priorityMap); + if ($priority > max($pMapKeys)) + { + $priority = max($pMapKeys); + } + elseif ($priority < min($pMapKeys)) + { + $priority = min($pMapKeys); + } + if (!$this->_setHeaderFieldModel('X-Priority', + sprintf('%d (%s)', $priority, $priorityMap[$priority]))) + { + $this->getHeaders()->addTextHeader('X-Priority', + sprintf('%d (%s)', $priority, $priorityMap[$priority])); + } + return $this; + } + + /** + * Get the priority of this message. + * The returned value is an integer where 1 is the highest priority and 5 + * is the lowest. + * @return int + */ + public function getPriority() + { + list($priority) = sscanf($this->_getHeaderFieldModel('X-Priority'), + '%[1-5]' + ); + return isset($priority) ? $priority : 3; + } + + /** + * Ask for a delivery receipt from the recipient to be sent to $addresses + * @param array $addresses + */ + public function setReadReceiptTo($addresses) + { + if (!$this->_setHeaderFieldModel('Disposition-Notification-To', $addresses)) + { + $this->getHeaders() + ->addMailboxHeader('Disposition-Notification-To', $addresses); + } + return $this; + } + + /** + * Get the addresses to which a read-receipt will be sent. + * @return string + */ + public function getReadReceiptTo() + { + return $this->_getHeaderFieldModel('Disposition-Notification-To'); + } + + /** + * Attach a {@link Swift_Mime_MimeEntity} such as an Attachment or MimePart. + * @param Swift_Mime_MimeEntity $entity + */ + public function attach(Swift_Mime_MimeEntity $entity) + { + $this->setChildren(array_merge($this->getChildren(), array($entity))); + return $this; + } + + /** + * Remove an already attached entity. + * @param Swift_Mime_MimeEntity $entity + */ + public function detach(Swift_Mime_MimeEntity $entity) + { + $newChildren = array(); + foreach ($this->getChildren() as $child) + { + if ($entity !== $child) + { + $newChildren[] = $child; + } + } + $this->setChildren($newChildren); + return $this; + } + + /** + * Attach a {@link Swift_Mime_MimeEntity} and return it's CID source. + * This method should be used when embedding images or other data in a message. + * @param Swift_Mime_MimeEntity $entity + * @return string + */ + public function embed(Swift_Mime_MimeEntity $entity) + { + $this->attach($entity); + return 'cid:' . $entity->getId(); + } + + /** + * Get this message as a complete string. + * @return string + */ + public function toString() + { + if (count($children = $this->getChildren()) > 0 && $this->getBody() != '') + { + $this->setChildren(array_merge(array($this->_becomeMimePart()), $children)); + $string = parent::toString(); + $this->setChildren($children); + } + else + { + $string = parent::toString(); + } + return $string; + } + + /** + * Returns a string representation of this object. + * + * @return string + * + * @see toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Write this message to a {@link Swift_InputByteStream}. + * @param Swift_InputByteStream $is + */ + public function toByteStream(Swift_InputByteStream $is) + { + if (count($children = $this->getChildren()) > 0 && $this->getBody() != '') + { + $this->setChildren(array_merge(array($this->_becomeMimePart()), $children)); + parent::toByteStream($is); + $this->setChildren($children); + } + else + { + parent::toByteStream($is); + } + } + + // -- Protected methods + + /** @see Swift_Mime_SimpleMimeEntity::_getIdField() */ + protected function _getIdField() + { + return 'Message-ID'; + } + + // -- Private methods + + /** Turn the body of this message into a child of itself if needed */ + private function _becomeMimePart() + { + $part = new parent($this->getHeaders()->newInstance(), $this->getEncoder(), + $this->_getCache(), $this->_userCharset + ); + $part->setContentType($this->_userContentType); + $part->setBody($this->getBody()); + $part->setFormat($this->_userFormat); + $part->setDelSp($this->_userDelSp); + $part->_setNestingLevel($this->_getTopNestingLevel()); + return $part; + } + + /** Get the highest nesting level nested inside this message */ + private function _getTopNestingLevel() + { + $highestLevel = $this->getNestingLevel(); + foreach ($this->getChildren() as $child) + { + $childLevel = $child->getNestingLevel(); + if ($highestLevel < $childLevel) + { + $highestLevel = $childLevel; + } + } + return $highestLevel; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleMimeEntity.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleMimeEntity.php new file mode 100644 index 0000000..1615822 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Mime/SimpleMimeEntity.php @@ -0,0 +1,803 @@ + array(self::LEVEL_TOP, self::LEVEL_MIXED), + 'multipart/alternative' => array(self::LEVEL_MIXED, self::LEVEL_ALTERNATIVE), + 'multipart/related' => array(self::LEVEL_ALTERNATIVE, self::LEVEL_RELATED) + ); + + /** A set of filter rules to define what level an entity should be nested at */ + private $_compoundLevelFilters = array(); + + /** The nesting level of this entity */ + private $_nestingLevel = self::LEVEL_ALTERNATIVE; + + /** A KeyCache instance used during encoding and streaming */ + private $_cache; + + /** Direct descendants of this entity */ + private $_immediateChildren = array(); + + /** All descendants of this entity */ + private $_children = array(); + + /** The maximum line length of the body of this entity */ + private $_maxLineLength = 78; + + /** The order in which alternative mime types should appear */ + private $_alternativePartOrder = array( + 'text/plain' => 1, + 'text/html' => 2, + 'multipart/related' => 3 + ); + + /** The CID of this entity */ + private $_id; + + /** The key used for accessing the cache */ + private $_cacheKey; + + protected $_userContentType; + + /** + * Create a new SimpleMimeEntity with $headers, $encoder and $cache. + * @param Swift_Mime_HeaderSet $headers + * @param Swift_Mime_ContentEncoder $encoder + * @param Swift_KeyCache $cache + */ + public function __construct(Swift_Mime_HeaderSet $headers, + Swift_Mime_ContentEncoder $encoder, Swift_KeyCache $cache) + { + $this->_cacheKey = uniqid(); + $this->_cache = $cache; + $this->_headers = $headers; + $this->setEncoder($encoder); + $this->_headers->defineOrdering( + array('Content-Type', 'Content-Transfer-Encoding') + ); + + // This array specifies that, when the entire MIME document contains + // $compoundLevel, then for each child within $level, if its Content-Type + // is $contentType then it should be treated as if it's level is + // $neededLevel instead. I tried to write that unambiguously! :-\ + // Data Structure: + // array ( + // $compoundLevel => array( + // $level => array( + // $contentType => $neededLevel + // ) + // ) + // ) + + $this->_compoundLevelFilters = array( + (self::LEVEL_ALTERNATIVE + self::LEVEL_RELATED) => array( + self::LEVEL_ALTERNATIVE => array( + 'text/plain' => self::LEVEL_ALTERNATIVE, + 'text/html' => self::LEVEL_RELATED + ) + ) + ); + + $this->_id = $this->getRandomId(); + } + + /** + * Generate a new Content-ID or Message-ID for this MIME entity. + * @return string + */ + public function generateId() + { + $this->setId($this->getRandomId()); + return $this->_id; + } + + /** + * Get the {@link Swift_Mime_HeaderSet} for this entity. + * @return Swift_Mime_HeaderSet + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Get the nesting level of this entity. + * @return int + * @see LEVEL_TOP, LEVEL_MIXED, LEVEL_RELATED, LEVEL_ALTERNATIVE + */ + public function getNestingLevel() + { + return $this->_nestingLevel; + } + + /** + * Get the Content-type of this entity. + * @return string + */ + public function getContentType() + { + return $this->_getHeaderFieldModel('Content-Type'); + } + + /** + * Set the Content-type of this entity. + * @param string $type + */ + public function setContentType($type) + { + $this->_setContentTypeInHeaders($type); + // Keep track of the value so that if the content-type changes automatically + // due to added child entities, it can be restored if they are later removed + $this->_userContentType = $type; + return $this; + } + + /** + * Get the CID of this entity. + * The CID will only be present in headers if a Content-ID header is present. + * @return string + */ + public function getId() + { + return $this->_headers->has($this->_getIdField()) + ? current((array) $this->_getHeaderFieldModel($this->_getIdField())) + : $this->_id; + } + + /** + * Set the CID of this entity. + * @param string $id + */ + public function setId($id) + { + if (!$this->_setHeaderFieldModel($this->_getIdField(), $id)) + { + $this->_headers->addIdHeader($this->_getIdField(), $id); + } + $this->_id = $id; + return $this; + } + + /** + * Get the description of this entity. + * This value comes from the Content-Description header if set. + * @return string + */ + public function getDescription() + { + return $this->_getHeaderFieldModel('Content-Description'); + } + + /** + * Set the description of this entity. + * This method sets a value in the Content-ID header. + * @param string $description + */ + public function setDescription($description) + { + if (!$this->_setHeaderFieldModel('Content-Description', $description)) + { + $this->_headers->addTextHeader('Content-Description', $description); + } + return $this; + } + + /** + * Get the maximum line length of the body of this entity. + * @return int + */ + public function getMaxLineLength() + { + return $this->_maxLineLength; + } + + /** + * Set the maximum line length of lines in this body. + * Though not enforced by the library, lines should not exceed 1000 chars. + * @param int $length + */ + public function setMaxLineLength($length) + { + $this->_maxLineLength = $length; + return $this; + } + + /** + * Get all children added to this entity. + * @return array of Swift_Mime_Entity + */ + public function getChildren() + { + return $this->_children; + } + + /** + * Set all children of this entity. + * @param array $children Swiift_Mime_Entity instances + * @param int $compoundLevel For internal use only + */ + public function setChildren(array $children, $compoundLevel = null) + { + //TODO: Try to refactor this logic + + $compoundLevel = isset($compoundLevel) + ? $compoundLevel + : $this->_getCompoundLevel($children) + ; + + $immediateChildren = array(); + $grandchildren = array(); + $newContentType = $this->_userContentType; + + foreach ($children as $child) + { + $level = $this->_getNeededChildLevel($child, $compoundLevel); + if (empty($immediateChildren)) //first iteration + { + $immediateChildren = array($child); + } + else + { + $nextLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel); + if ($nextLevel == $level) + { + $immediateChildren[] = $child; + } + elseif ($level < $nextLevel) + { + //Re-assign immediateChildren to grandchilden + $grandchildren = array_merge($grandchildren, $immediateChildren); + //Set new children + $immediateChildren = array($child); + } + else + { + $grandchildren[] = $child; + } + } + } + + if (!empty($immediateChildren)) + { + $lowestLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel); + + //Determine which composite media type is needed to accomodate the + // immediate children + foreach ($this->_compositeRanges as $mediaType => $range) + { + if ($lowestLevel > $range[0] + && $lowestLevel <= $range[1]) + { + $newContentType = $mediaType; + break; + } + } + + //Put any grandchildren in a subpart + if (!empty($grandchildren)) + { + $subentity = $this->_createChild(); + $subentity->_setNestingLevel($lowestLevel); + $subentity->setChildren($grandchildren, $compoundLevel); + array_unshift($immediateChildren, $subentity); + } + } + + $this->_immediateChildren = $immediateChildren; + $this->_children = $children; + $this->_setContentTypeInHeaders($newContentType); + $this->_fixHeaders(); + $this->_sortChildren(); + + return $this; + } + + /** + * Get the body of this entity as a string. + * @return string + */ + public function getBody() + { + return ($this->_body instanceof Swift_OutputByteStream) + ? $this->_readStream($this->_body) + : $this->_body; + } + + /** + * Set the body of this entity, either as a string, or as an instance of + * {@link Swift_OutputByteStream}. + * @param mixed $body + * @param string $contentType optional + */ + public function setBody($body, $contentType = null) + { + if ($body !== $this->_body) + { + $this->_clearCache(); + } + + $this->_body = $body; + if (isset($contentType)) + { + $this->setContentType($contentType); + } + return $this; + } + + /** + * Get the encoder used for the body of this entity. + * @return Swift_Mime_ContentEncoder + */ + public function getEncoder() + { + return $this->_encoder; + } + + /** + * Set the encoder used for the body of this entity. + * @param Swift_Mime_ContentEncoder $encoder + */ + public function setEncoder(Swift_Mime_ContentEncoder $encoder) + { + if ($encoder !== $this->_encoder) + { + $this->_clearCache(); + } + + $this->_encoder = $encoder; + $this->_setEncoding($encoder->getName()); + $this->_notifyEncoderChanged($encoder); + return $this; + } + + /** + * Get the boundary used to separate children in this entity. + * @return string + */ + public function getBoundary() + { + if (!isset($this->_boundary)) + { + $this->_boundary = '_=_swift_v4_' . time() . uniqid() . '_=_'; + } + return $this->_boundary; + } + + /** + * Set the boundary used to separate children in this entity. + * @param string $boundary + * @throws Swift_RfcComplianceException + */ + public function setBoundary($boundary) + { + $this->_assertValidBoundary($boundary); + $this->_boundary = $boundary; + return $this; + } + + /** + * Receive notification that the charset of this entity, or a parent entity + * has changed. + * @param string $charset + */ + public function charsetChanged($charset) + { + $this->_notifyCharsetChanged($charset); + } + + /** + * Receive notification that the encoder of this entity or a parent entity + * has changed. + * @param Swift_Mime_ContentEncoder $encoder + */ + public function encoderChanged(Swift_Mime_ContentEncoder $encoder) + { + $this->_notifyEncoderChanged($encoder); + } + + /** + * Get this entire entity as a string. + * @return string + */ + public function toString() + { + $string = $this->_headers->toString(); + if (isset($this->_body) && empty($this->_immediateChildren)) + { + if ($this->_cache->hasKey($this->_cacheKey, 'body')) + { + $body = $this->_cache->getString($this->_cacheKey, 'body'); + } + else + { + $body = "\r\n" . $this->_encoder->encodeString($this->getBody(), 0, + $this->getMaxLineLength() + ); + $this->_cache->setString($this->_cacheKey, 'body', $body, + Swift_KeyCache::MODE_WRITE + ); + } + $string .= $body; + } + + if (!empty($this->_immediateChildren)) + { + foreach ($this->_immediateChildren as $child) + { + $string .= "\r\n\r\n--" . $this->getBoundary() . "\r\n"; + $string .= $child->toString(); + } + $string .= "\r\n\r\n--" . $this->getBoundary() . "--\r\n"; + } + + return $string; + } + + /** + * Returns a string representation of this object. + * + * @return string + * + * @see toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Write this entire entity to a {@link Swift_InputByteStream}. + * @param Swift_InputByteStream + */ + public function toByteStream(Swift_InputByteStream $is) + { + $is->write($this->_headers->toString()); + $is->commit(); + + if (empty($this->_immediateChildren)) + { + if (isset($this->_body)) + { + if ($this->_cache->hasKey($this->_cacheKey, 'body')) + { + $this->_cache->exportToByteStream($this->_cacheKey, 'body', $is); + } + else + { + $cacheIs = $this->_cache->getInputByteStream($this->_cacheKey, 'body'); + if ($cacheIs) + { + $is->bind($cacheIs); + } + + $is->write("\r\n"); + + if ($this->_body instanceof Swift_OutputByteStream) + { + $this->_body->setReadPointer(0); + + $this->_encoder->encodeByteStream($this->_body, $is, 0, + $this->getMaxLineLength() + ); + } + else + { + $is->write($this->_encoder->encodeString( + $this->getBody(), 0, $this->getMaxLineLength() + )); + } + + if ($cacheIs) + { + $is->unbind($cacheIs); + } + } + } + } + + if (!empty($this->_immediateChildren)) + { + foreach ($this->_immediateChildren as $child) + { + $is->write("\r\n\r\n--" . $this->getBoundary() . "\r\n"); + $child->toByteStream($is); + } + $is->write("\r\n\r\n--" . $this->getBoundary() . "--\r\n"); + } + } + + // -- Protected methods + + /** + * Get the name of the header that provides the ID of this entity */ + protected function _getIdField() + { + return 'Content-ID'; + } + + /** + * Get the model data (usually an array or a string) for $field. + */ + protected function _getHeaderFieldModel($field) + { + if ($this->_headers->has($field)) + { + return $this->_headers->get($field)->getFieldBodyModel(); + } + } + + /** + * Set the model data for $field. + */ + protected function _setHeaderFieldModel($field, $model) + { + if ($this->_headers->has($field)) + { + $this->_headers->get($field)->setFieldBodyModel($model); + return true; + } + else + { + return false; + } + } + + /** + * Get the parameter value of $parameter on $field header. + */ + protected function _getHeaderParameter($field, $parameter) + { + if ($this->_headers->has($field)) + { + return $this->_headers->get($field)->getParameter($parameter); + } + } + + /** + * Set the parameter value of $parameter on $field header. + */ + protected function _setHeaderParameter($field, $parameter, $value) + { + if ($this->_headers->has($field)) + { + $this->_headers->get($field)->setParameter($parameter, $value); + return true; + } + else + { + return false; + } + } + + /** + * Re-evaluate what content type and encoding should be used on this entity. + */ + protected function _fixHeaders() + { + if (count($this->_immediateChildren)) + { + $this->_setHeaderParameter('Content-Type', 'boundary', + $this->getBoundary() + ); + $this->_headers->remove('Content-Transfer-Encoding'); + } + else + { + $this->_setHeaderParameter('Content-Type', 'boundary', null); + $this->_setEncoding($this->_encoder->getName()); + } + } + + /** + * Get the KeyCache used in this entity. + */ + protected function _getCache() + { + return $this->_cache; + } + + /** + * Empty the KeyCache for this entity. + */ + protected function _clearCache() + { + $this->_cache->clearKey($this->_cacheKey, 'body'); + } + + /** + * Returns a random Content-ID or Message-ID. + * @return string + */ + protected function getRandomId() + { + $idLeft = time() . '.' . uniqid(); + $idRight = !empty($_SERVER['SERVER_NAME']) + ? $_SERVER['SERVER_NAME'] + : 'swift.generated'; + return $idLeft . '@' . $idRight; + } + + // -- Private methods + + private function _readStream(Swift_OutputByteStream $os) + { + $string = ''; + while (false !== $bytes = $os->read(8192)) + { + $string .= $bytes; + } + return $string; + } + + private function _setEncoding($encoding) + { + if (!$this->_setHeaderFieldModel('Content-Transfer-Encoding', $encoding)) + { + $this->_headers->addTextHeader('Content-Transfer-Encoding', $encoding); + } + } + + private function _assertValidBoundary($boundary) + { + if (!preg_match( + '/^[a-z0-9\'\(\)\+_\-,\.\/:=\?\ ]{0,69}[a-z0-9\'\(\)\+_\-,\.\/:=\?]$/Di', + $boundary)) + { + throw new Swift_RfcComplianceException('Mime boundary set is not RFC 2046 compliant.'); + } + } + + private function _setContentTypeInHeaders($type) + { + if (!$this->_setHeaderFieldModel('Content-Type', $type)) + { + $this->_headers->addParameterizedHeader('Content-Type', $type); + } + } + + private function _setNestingLevel($level) + { + $this->_nestingLevel = $level; + } + + private function _getCompoundLevel($children) + { + $level = 0; + foreach ($children as $child) + { + $level |= $child->getNestingLevel(); + } + return $level; + } + + private function _getNeededChildLevel($child, $compoundLevel) + { + $filter = array(); + foreach ($this->_compoundLevelFilters as $bitmask => $rules) + { + if (($compoundLevel & $bitmask) === $bitmask) + { + $filter = $rules + $filter; + } + } + + $realLevel = $child->getNestingLevel(); + $lowercaseType = strtolower($child->getContentType()); + + if (isset($filter[$realLevel]) + && isset($filter[$realLevel][$lowercaseType])) + { + return $filter[$realLevel][$lowercaseType]; + } + else + { + return $realLevel; + } + } + + private function _createChild() + { + return new self($this->_headers->newInstance(), + $this->_encoder, $this->_cache); + } + + private function _notifyEncoderChanged(Swift_Mime_ContentEncoder $encoder) + { + foreach ($this->_immediateChildren as $child) + { + $child->encoderChanged($encoder); + } + } + + private function _notifyCharsetChanged($charset) + { + $this->_encoder->charsetChanged($charset); + $this->_headers->charsetChanged($charset); + foreach ($this->_immediateChildren as $child) + { + $child->charsetChanged($charset); + } + } + + private function _sortChildren() + { + $shouldSort = false; + foreach ($this->_immediateChildren as $child) + { + //NOTE: This include alternative parts moved into a related part + if ($child->getNestingLevel() == self::LEVEL_ALTERNATIVE) + { + $shouldSort = true; + break; + } + } + + //Sort in order of preference, if there is one + if ($shouldSort) + { + usort($this->_immediateChildren, array($this, '_childSortAlgorithm')); + } + } + + private function _childSortAlgorithm($a, $b) + { + $typePrefs = array(); + $types = array( + strtolower($a->getContentType()), + strtolower($b->getContentType()) + ); + foreach ($types as $type) + { + $typePrefs[] = (array_key_exists($type, $this->_alternativePartOrder)) + ? $this->_alternativePartOrder[$type] + : (max($this->_alternativePartOrder) + 1); + } + return ($typePrefs[0] >= $typePrefs[1]) ? 1 : -1; + } + + // -- Destructor + + /** + * Empties it's own contents from the cache. + */ + public function __destruct() + { + $this->_cache->clearAll($this->_cacheKey); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/MimePart.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/MimePart.php new file mode 100644 index 0000000..60b6d56 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/MimePart.php @@ -0,0 +1,65 @@ +createDependenciesFor('mime.part') + ); + + if (!isset($charset)) + { + $charset = Swift_DependencyContainer::getInstance() + ->lookup('properties.charset'); + } + $this->setBody($body); + $this->setCharset($charset); + if ($contentType) + { + $this->setContentType($contentType); + } + } + + /** + * Create a new MimePart. + * @param string $body + * @param string $contentType + * @param string $charset + * @return Swift_Mime_MimePart + */ + public static function newInstance($body = null, $contentType = null, + $charset = null) + { + return new self($body, $contentType, $charset); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/OutputByteStream.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/OutputByteStream.php new file mode 100644 index 0000000..951b838 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/OutputByteStream.php @@ -0,0 +1,41 @@ +setThreshold($threshold); + $this->setSleepTime($sleep); + $this->_sleeper = $sleeper; + } + + /** + * Set the number of emails to send before restarting. + * @param int $threshold + */ + public function setThreshold($threshold) + { + $this->_threshold = $threshold; + } + + /** + * Get the number of emails to send before restarting. + * @return int + */ + public function getThreshold() + { + return $this->_threshold; + } + + /** + * Set the number of seconds to sleep for during a restart. + * @param int $sleep time + */ + public function setSleepTime($sleep) + { + $this->_sleep = $sleep; + } + + /** + * Get the number of seconds to sleep for during a restart. + * @return int + */ + public function getSleepTime() + { + return $this->_sleep; + } + + /** + * Invoked immediately before the Message is sent. + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + } + + /** + * Invoked immediately after the Message is sent. + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + ++$this->_counter; + if ($this->_counter >= $this->_threshold) + { + $transport = $evt->getTransport(); + $transport->stop(); + if ($this->_sleep) + { + $this->sleep($this->_sleep); + } + $transport->start(); + $this->_counter = 0; + } + } + + /** + * Sleep for $seconds. + * @param int $seconds + */ + public function sleep($seconds) + { + if (isset($this->_sleeper)) + { + $this->_sleeper->sleep($seconds); + } + else + { + sleep($seconds); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/BandwidthMonitorPlugin.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/BandwidthMonitorPlugin.php new file mode 100644 index 0000000..501cd80 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/BandwidthMonitorPlugin.php @@ -0,0 +1,173 @@ +getMessage(); + $message->toByteStream($this); + } + + /** + * Invoked immediately following a command being sent. + * @param Swift_Events_ResponseEvent $evt + */ + public function commandSent(Swift_Events_CommandEvent $evt) + { + $command = $evt->getCommand(); + $this->_out += strlen($command); + } + + /** + * Invoked immediately following a response coming back. + * @param Swift_Events_ResponseEvent $evt + */ + public function responseReceived(Swift_Events_ResponseEvent $evt) + { + $response = $evt->getResponse(); + $this->_in += strlen($response); + } + + /** + * Called when a message is sent so that the outgoing counter can be increased. + * @param string $bytes + */ + public function write($bytes) + { + $this->_out += strlen($bytes); + foreach ($this->_mirrors as $stream) + { + $stream->write($bytes); + } + } + + /** + * Not used. + */ + public function commit() + { + } + + /** + * Attach $is to this stream. + * The stream acts as an observer, receiving all data that is written. + * All {@link write()} and {@link flushBuffers()} operations will be mirrored. + * + * @param Swift_InputByteStream $is + */ + public function bind(Swift_InputByteStream $is) + { + $this->_mirrors[] = $is; + } + + /** + * Remove an already bound stream. + * If $is is not bound, no errors will be raised. + * If the stream currently has any buffered data it will be written to $is + * before unbinding occurs. + * + * @param Swift_InputByteStream $is + */ + public function unbind(Swift_InputByteStream $is) + { + foreach ($this->_mirrors as $k => $stream) + { + if ($is === $stream) + { + unset($this->_mirrors[$k]); + } + } + } + + /** + * Not used. + */ + public function flushBuffers() + { + foreach ($this->_mirrors as $stream) + { + $stream->flushBuffers(); + } + } + + /** + * Get the total number of bytes sent to the server. + * @return int + */ + public function getBytesOut() + { + return $this->_out; + } + + /** + * Get the total number of bytes received from the server. + * @return int + */ + public function getBytesIn() + { + return $this->_in; + } + + /** + * Reset the internal counters to zero. + */ + public function reset() + { + $this->_out = 0; + $this->_in = 0; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Decorator/Replacements.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Decorator/Replacements.php new file mode 100644 index 0000000..9735d0a --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Decorator/Replacements.php @@ -0,0 +1,36 @@ + + * $replacements = array( + * "address1@domain.tld" => array("{a}" => "b", "{c}" => "d"), + * "address2@domain.tld" => array("{a}" => "x", "{c}" => "y") + * ) + * + * + * When using an instance of {@link Swift_Plugins_Decorator_Replacements}, + * the object should return just the array of replacements for the address + * given to {@link Swift_Plugins_Decorator_Replacements::getReplacementsFor()}. + * + * @param mixed $replacements + */ + public function __construct($replacements) + { + if (!($replacements instanceof Swift_Plugins_Decorator_Replacements)) + { + $this->_replacements = (array) $replacements; + } + else + { + $this->_replacements = $replacements; + } + } + + /** + * Invoked immediately before the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + $message = $evt->getMessage(); + $this->_restoreMessage($message); + $to = array_keys($message->getTo()); + $address = array_shift($to); + if ($replacements = $this->getReplacementsFor($address)) + { + $body = $message->getBody(); + $search = array_keys($replacements); + $replace = array_values($replacements); + $bodyReplaced = str_replace( + $search, $replace, $body + ); + if ($body != $bodyReplaced) + { + $this->_originalBody = $body; + $message->setBody($bodyReplaced); + } + $subject = $message->getSubject(); + $subjectReplaced = str_replace( + $search, $replace, $subject + ); + if ($subject != $subjectReplaced) + { + $this->_originalSubject = $subject; + $message->setSubject($subjectReplaced); + } + $children = (array) $message->getChildren(); + foreach ($children as $child) + { + list($type, ) = sscanf($child->getContentType(), '%[^/]/%s'); + if ('text' == $type) + { + $body = $child->getBody(); + $bodyReplaced = str_replace( + $search, $replace, $body + ); + if ($body != $bodyReplaced) + { + $child->setBody($bodyReplaced); + $this->_originalChildBodies[$child->getId()] = $body; + } + } + } + $this->_lastMessage = $message; + } + } + + /** + * Find a map of replacements for the address. + * + * If this plugin was provided with a delegate instance of + * {@link Swift_Plugins_Decorator_Replacements} then the call will be + * delegated to it. Otherwise, it will attempt to find the replacements + * from the array provided in the constructor. + * + * If no replacements can be found, an empty value (NULL) is returned. + * + * @param string $address + * + * @return array + */ + public function getReplacementsFor($address) + { + if ($this->_replacements instanceof Swift_Plugins_Decorator_Replacements) + { + return $this->_replacements->getReplacementsFor($address); + } + else + { + return isset($this->_replacements[$address]) + ? $this->_replacements[$address] + : null + ; + } + } + + /** + * Invoked immediately after the Message is sent. + * + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + $this->_restoreMessage($evt->getMessage()); + } + + // -- Private methods + + /** Restore a changed message back to its original state */ + private function _restoreMessage(Swift_Mime_Message $message) + { + if ($this->_lastMessage === $message) + { + if (isset($this->_originalBody)) + { + $message->setBody($this->_originalBody); + $this->_originalBody = null; + } + if (isset($this->_originalSubject)) + { + $message->setSubject($this->_originalSubject); + $this->_originalSubject = null; + } + if (!empty($this->_originalChildBodies)) + { + $children = (array) $message->getChildren(); + foreach ($children as $child) + { + $id = $child->getId(); + if (array_key_exists($id, $this->_originalChildBodies)) + { + $child->setBody($this->_originalChildBodies[$id]); + } + } + $this->_originalChildBodies = array(); + } + $this->_lastMessage = null; + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Logger.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Logger.php new file mode 100644 index 0000000..9864da0 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Logger.php @@ -0,0 +1,37 @@ +_logger = $logger; + } + + /** + * Add a log entry. + * + * @param string $entry + */ + public function add($entry) + { + $this->_logger->add($entry); + } + + /** + * Clear the log contents. + */ + public function clear() + { + $this->_logger->clear(); + } + + /** + * Get this log as a string. + * + * @return string + */ + public function dump() + { + return $this->_logger->dump(); + } + + /** + * Invoked immediately following a command being sent. + * + * @param Swift_Events_ResponseEvent $evt + */ + public function commandSent(Swift_Events_CommandEvent $evt) + { + $command = $evt->getCommand(); + $this->_logger->add(sprintf(">> %s", $command)); + } + + /** + * Invoked immediately following a response coming back. + * + * @param Swift_Events_ResponseEvent $evt + */ + public function responseReceived(Swift_Events_ResponseEvent $evt) + { + $response = $evt->getResponse(); + $this->_logger->add(sprintf("<< %s", $response)); + } + + /** + * Invoked just before a Transport is started. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function beforeTransportStarted(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ Starting %s", $transportName)); + } + + /** + * Invoked immediately after the Transport is started. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function transportStarted(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ %s started", $transportName)); + } + + /** + * Invoked just before a Transport is stopped. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function beforeTransportStopped(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ Stopping %s", $transportName)); + } + + /** + * Invoked immediately after the Transport is stopped. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function transportStopped(Swift_Events_TransportChangeEvent $evt) + { + $transportName = get_class($evt->getSource()); + $this->_logger->add(sprintf("++ %s stopped", $transportName)); + } + + /** + * Invoked as a TransportException is thrown in the Transport system. + * + * @param Swift_Events_TransportExceptionEvent $evt + */ + public function exceptionThrown(Swift_Events_TransportExceptionEvent $evt) + { + $e = $evt->getException(); + $message = $e->getMessage(); + $this->_logger->add(sprintf("!! %s", $message)); + $message .= PHP_EOL; + $message .= 'Log data:' . PHP_EOL; + $message .= $this->_logger->dump(); + $evt->cancelBubble(); + throw new Swift_TransportException($message); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Loggers/ArrayLogger.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Loggers/ArrayLogger.php new file mode 100644 index 0000000..930eca2 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Loggers/ArrayLogger.php @@ -0,0 +1,73 @@ +_size = $size; + } + + /** + * Add a log entry. + * @param string $entry + */ + public function add($entry) + { + $this->_log[] = $entry; + while (count($this->_log) > $this->_size) + { + array_shift($this->_log); + } + } + + /** + * Clear the log contents. + */ + public function clear() + { + $this->_log = array(); + } + + /** + * Get this log as a string. + * @return string + */ + public function dump() + { + return implode(PHP_EOL, $this->_log); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Loggers/EchoLogger.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Loggers/EchoLogger.php new file mode 100644 index 0000000..83dd54b --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Loggers/EchoLogger.php @@ -0,0 +1,64 @@ +_isHtml = $isHtml; + } + + /** + * Add a log entry. + * @param string $entry + */ + public function add($entry) + { + if ($this->_isHtml) + { + printf('%s%s%s', htmlspecialchars($entry, ENT_QUOTES), '
', PHP_EOL); + } + else + { + printf('%s%s', $entry, PHP_EOL); + } + } + + /** + * Not implemented. + */ + public function clear() + { + } + + /** + * Not implemented. + */ + public function dump() + { + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Pop/Pop3Connection.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Pop/Pop3Connection.php new file mode 100644 index 0000000..1c96dcf --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Pop/Pop3Connection.php @@ -0,0 +1,36 @@ +_host = $host; + $this->_port = $port; + $this->_crypto = $crypto; + } + + /** + * Create a new PopBeforeSmtpPlugin for $host and $port. + * + * @param string $host + * @param int $port + * @param string $cypto as "tls" or "ssl" + * + * @return Swift_Plugins_PopBeforeSmtpPlugin + */ + public static function newInstance($host, $port = 110, $crypto = null) + { + return new self($host, $port, $crypto); + } + + /** + * Set a Pop3Connection to delegate to instead of connecting directly. + * + * @param Swift_Plugins_Pop_Pop3Connection $connection + */ + public function setConnection(Swift_Plugins_Pop_Pop3Connection $connection) + { + $this->_connection = $connection; + return $this; + } + + /** + * Bind this plugin to a specific SMTP transport instance. + * + * @param Swift_Transport + */ + public function bindSmtp(Swift_Transport $smtp) + { + $this->_transport = $smtp; + } + + /** + * Set the connection timeout in seconds (default 10). + * + * @param int $timeout + */ + public function setTimeout($timeout) + { + $this->_timeout = (int) $timeout; + return $this; + } + + /** + * Set the username to use when connecting (if needed). + * + * @param string $username + */ + public function setUsername($username) + { + $this->_username = $username; + return $this; + } + + /** + * Set the password to use when connecting (if needed). + * + * @param string $password + */ + public function setPassword($password) + { + $this->_password = $password; + return $this; + } + + /** + * Connect to the POP3 host and authenticate. + * + * @throws Swift_Plugins_Pop_Pop3Exception if connection fails + */ + public function connect() + { + if (isset($this->_connection)) + { + $this->_connection->connect(); + } + else + { + if (!isset($this->_socket)) + { + if (!$socket = fsockopen( + $this->_getHostString(), $this->_port, $errno, $errstr, $this->_timeout)) + { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to connect to POP3 host [%s]: %s', $this->_host, $errstr) + ); + } + $this->_socket = $socket; + + if (false === $greeting = fgets($this->_socket)) + { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to connect to POP3 host [%s]', trim($greeting)) + ); + } + + $this->_assertOk($greeting); + + if ($this->_username) + { + $this->_command(sprintf("USER %s\r\n", $this->_username)); + $this->_command(sprintf("PASS %s\r\n", $this->_password)); + } + } + } + } + + /** + * Disconnect from the POP3 host. + */ + public function disconnect() + { + if (isset($this->_connection)) + { + $this->_connection->disconnect(); + } + else + { + $this->_command("QUIT\r\n"); + if (!fclose($this->_socket)) + { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('POP3 host [%s] connection could not be stopped', $this->_host) + ); + } + $this->_socket = null; + } + } + + /** + * Invoked just before a Transport is started. + * + * @param Swift_Events_TransportChangeEvent $evt + */ + public function beforeTransportStarted(Swift_Events_TransportChangeEvent $evt) + { + if (isset($this->_transport)) + { + if ($this->_transport !== $evt->getTransport()) + { + return; + } + } + + $this->connect(); + $this->disconnect(); + } + + /** + * Not used. + */ + public function transportStarted(Swift_Events_TransportChangeEvent $evt) + { + } + + /** + * Not used. + */ + public function beforeTransportStopped(Swift_Events_TransportChangeEvent $evt) + { + } + + /** + * Not used. + */ + public function transportStopped(Swift_Events_TransportChangeEvent $evt) + { + } + + // -- Private Methods + + private function _command($command) + { + if (!fwrite($this->_socket, $command)) + { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to write command [%s] to POP3 host', trim($command)) + ); + } + + if (false === $response = fgets($this->_socket)) + { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('Failed to read from POP3 host after command [%s]', trim($command)) + ); + } + + $this->_assertOk($response); + + return $response; + } + + private function _assertOk($response) + { + if (substr($response, 0, 3) != '+OK') + { + throw new Swift_Plugins_Pop_Pop3Exception( + sprintf('POP3 command failed [%s]', trim($response)) + ); + } + } + + private function _getHostString() + { + $host = $this->_host; + switch (strtolower($this->_crypto)) + { + case 'ssl': + $host = 'ssl://' . $host; + break; + + case 'tls': + $host = 'tls://' . $host; + break; + } + return $host; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporter.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporter.php new file mode 100644 index 0000000..00d5765 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporter.php @@ -0,0 +1,36 @@ +_reporter = $reporter; + } + + /** + * Not used. + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + } + + /** + * Invoked immediately after the Message is sent. + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + $message = $evt->getMessage(); + $failures = array_flip($evt->getFailedRecipients()); + foreach ((array) $message->getTo() as $address => $null) + { + $this->_reporter->notify( + $message, $address, (array_key_exists($address, $failures) + ? Swift_Plugins_Reporter::RESULT_FAIL + : Swift_Plugins_Reporter::RESULT_PASS) + ); + } + foreach ((array) $message->getCc() as $address => $null) + { + $this->_reporter->notify( + $message, $address, (array_key_exists($address, $failures) + ? Swift_Plugins_Reporter::RESULT_FAIL + : Swift_Plugins_Reporter::RESULT_PASS) + ); + } + foreach ((array) $message->getBcc() as $address => $null) + { + $this->_reporter->notify( + $message, $address, (array_key_exists($address, $failures) + ? Swift_Plugins_Reporter::RESULT_FAIL + : Swift_Plugins_Reporter::RESULT_PASS) + ); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporters/HitReporter.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporters/HitReporter.php new file mode 100644 index 0000000..0022f5e --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporters/HitReporter.php @@ -0,0 +1,63 @@ +_failures_cache[$address])) + { + $this->_failures[] = $address; + $this->_failures_cache[$address] = true; + } + } + + /** + * Get an array of addresses for which delivery failed. + * @return array + */ + public function getFailedRecipients() + { + return $this->_failures; + } + + /** + * Clear the buffer (empty the list). + */ + public function clear() + { + $this->_failures = $this->_failures_cache = array(); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporters/HtmlReporter.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporters/HtmlReporter.php new file mode 100644 index 0000000..7370078 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Reporters/HtmlReporter.php @@ -0,0 +1,47 @@ +" . PHP_EOL; + echo "PASS " . $address . PHP_EOL; + echo "" . PHP_EOL; + flush(); + } + else + { + echo "
" . PHP_EOL; + echo "FAIL " . $address . PHP_EOL; + echo "
" . PHP_EOL; + flush(); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Sleeper.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Sleeper.php new file mode 100644 index 0000000..148cbd3 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Sleeper.php @@ -0,0 +1,26 @@ +_rate = $rate; + $this->_mode = $mode; + $this->_sleeper = $sleeper; + $this->_timer = $timer; + } + + /** + * Invoked immediately before the Message is sent. + * @param Swift_Events_SendEvent $evt + */ + public function beforeSendPerformed(Swift_Events_SendEvent $evt) + { + $time = $this->getTimestamp(); + if (!isset($this->_start)) + { + $this->_start = $time; + } + $duration = $time - $this->_start; + + if (self::BYTES_PER_MINUTE == $this->_mode) + { + $sleep = $this->_throttleBytesPerMinute($duration); + } + else + { + $sleep = $this->_throttleMessagesPerMinute($duration); + } + + if ($sleep > 0) + { + $this->sleep($sleep); + } + } + + /** + * Invoked when a Message is sent. + * @param Swift_Events_SendEvent $evt + */ + public function sendPerformed(Swift_Events_SendEvent $evt) + { + parent::sendPerformed($evt); + ++$this->_messages; + } + + /** + * Sleep for $seconds. + * @param int $seconds + */ + public function sleep($seconds) + { + if (isset($this->_sleeper)) + { + $this->_sleeper->sleep($seconds); + } + else + { + sleep($seconds); + } + } + + /** + * Get the current UNIX timestamp + * @return int + */ + public function getTimestamp() + { + if (isset($this->_timer)) + { + return $this->_timer->getTimestamp(); + } + else + { + return time(); + } + } + + // -- Private methods + + /** + * Get a number of seconds to sleep for. + * @param int $timePassed + * @return int + * @access private + */ + private function _throttleBytesPerMinute($timePassed) + { + $expectedDuration = $this->getBytesOut() / ($this->_rate / 60); + return (int) ceil($expectedDuration - $timePassed); + } + + /** + * Get a number of seconds to sleep for. + * @param int $timePassed + * @return int + * @access private + */ + private function _throttleMessagesPerMinute($timePassed) + { + $expectedDuration = $this->_messages / ($this->_rate / 60); + return (int) ceil($expectedDuration - $timePassed); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Timer.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Timer.php new file mode 100644 index 0000000..92207bf --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Plugins/Timer.php @@ -0,0 +1,26 @@ +register('properties.charset')->asValue($charset); + return $this; + } + + /** + * Set the directory where temporary files can be saved. + * @param string $dir + * @return Swift_Preferences + */ + public function setTempDir($dir) + { + Swift_DependencyContainer::getInstance() + ->register('tempdir')->asValue($dir); + return $this; + } + + /** + * Set the type of cache to use (i.e. "disk" or "array"). + * @param string $type + * @return Swift_Preferences + */ + public function setCacheType($type) + { + Swift_DependencyContainer::getInstance() + ->register('cache')->asAliasOf(sprintf('cache.%s', $type)); + return $this; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ReplacementFilterFactory.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ReplacementFilterFactory.php new file mode 100644 index 0000000..db29e6d --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/ReplacementFilterFactory.php @@ -0,0 +1,27 @@ +createDependenciesFor('transport.sendmail') + ); + + $this->setCommand($command); + } + + /** + * Create a new SendmailTransport instance. + * @param string $command + * @return Swift_SendmailTransport + */ + public static function newInstance($command = '/usr/sbin/sendmail -bs') + { + return new self($command); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/SmtpTransport.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/SmtpTransport.php new file mode 100644 index 0000000..65180d5 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/SmtpTransport.php @@ -0,0 +1,56 @@ +createDependenciesFor('transport.smtp') + ); + + $this->setHost($host); + $this->setPort($port); + $this->setEncryption($security); + } + + /** + * Create a new SmtpTransport instance. + * @param string $host + * @param int $port + * @param int $security + * @return Swift_SmtpTransport + */ + public static function newInstance($host = 'localhost', $port = 25, + $security = null) + { + return new self($host, $port, $security); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilter.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilter.php new file mode 100644 index 0000000..6c262ce --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilter.php @@ -0,0 +1,33 @@ +_search = $search; + $this->_index = array(); + $this->_tree = array(); + $this->_replace = array(); + $this->_repSize = array(); + + $tree = null; + $i = null; + $last_size = $size = 0; + foreach ($search as $i => $search_element) + { + if ($tree !== null) + { + $tree[-1] = min (count($replace) - 1, $i - 1); + $tree[-2] = $last_size; + } + $tree = &$this->_tree; + if (is_array ($search_element)) + { + foreach ($search_element as $k => $char) + { + $this->_index[$char] = true; + if (!isset($tree[$char])) + { + $tree[$char] = array(); + } + $tree = &$tree[$char]; + } + $last_size = $k+1; + $size = max($size, $last_size); + } + else + { + $last_size = 1; + if (!isset($tree[$search_element])) + { + $tree[$search_element] = array(); + } + $tree = &$tree[$search_element]; + $size = max($last_size, $size); + $this->_index[$search_element] = true; + } + } + if ($i !== null) + { + $tree[-1] = min (count ($replace) - 1, $i); + $tree[-2] = $last_size; + $this->_treeMaxLen = $size; + } + foreach ($replace as $rep) + { + if (!is_array($rep)) + { + $rep = array ($rep); + } + $this->_replace[] = $rep; + } + for ($i = count($this->_replace) - 1; $i >= 0; --$i) + { + $this->_replace[$i] = $rep = $this->filter($this->_replace[$i], $i); + $this->_repSize[$i] = count($rep); + } + } + + /** + * Returns true if based on the buffer passed more bytes should be buffered. + * @param array $buffer + * @return boolean + */ + public function shouldBuffer($buffer) + { + $endOfBuffer = end($buffer); + return isset ($this->_index[$endOfBuffer]); + } + + /** + * Perform the actual replacements on $buffer and return the result. + * @param array $buffer + * @return array + */ + public function filter($buffer, $_minReplaces = -1) + { + if ($this->_treeMaxLen == 0) + { + return $buffer; + } + + $newBuffer = array(); + $buf_size = count($buffer); + for ($i = 0; $i < $buf_size; ++$i) + { + $search_pos = $this->_tree; + $last_found = PHP_INT_MAX; + // We try to find if the next byte is part of a search pattern + for ($j = 0; $j <= $this->_treeMaxLen; ++$j) + { + // We have a new byte for a search pattern + if (isset ($buffer [$p = $i + $j]) && isset($search_pos[$buffer[$p]])) + { + $search_pos = $search_pos[$buffer[$p]]; + // We have a complete pattern, save, in case we don't find a better match later + if (isset($search_pos[- 1]) && $search_pos[-1] < $last_found + && $search_pos[-1] > $_minReplaces) + { + $last_found = $search_pos[-1]; + $last_size = $search_pos[-2]; + } + } + // We got a complete pattern + elseif ($last_found !== PHP_INT_MAX) + { + // Adding replacement datas to output buffer + $rep_size = $this->_repSize[$last_found]; + for ($j = 0; $j < $rep_size; ++$j) + { + $newBuffer[] = $this->_replace[$last_found][$j]; + } + // We Move cursor forward + $i += $last_size - 1; + // Edge Case, last position in buffer + if ($i >= $buf_size) + { + $newBuffer[] = $buffer[$i]; + } + + // We start the next loop + continue 2; + } + else + { + // this byte is not in a pattern and we haven't found another pattern + break; + } + } + // Normal byte, move it to output buffer + $newBuffer[] = $buffer[$i]; + } + + return $newBuffer; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilters/StringReplacementFilter.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilters/StringReplacementFilter.php new file mode 100644 index 0000000..9ab6c30 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilters/StringReplacementFilter.php @@ -0,0 +1,66 @@ +_search = $search; + $this->_replace = $replace; + } + + /** + * Returns true if based on the buffer passed more bytes should be buffered. + * @param string $buffer + * @return boolean + */ + public function shouldBuffer($buffer) + { + $endOfBuffer = substr($buffer, -1); + foreach ((array) $this->_search as $needle) + { + if (false !== strpos($needle, $endOfBuffer)) + { + return true; + } + } + return false; + } + + /** + * Perform the actual replacements on $buffer and return the result. + * @param string $buffer + * @return string + */ + public function filter($buffer) + { + return str_replace($this->_search, $this->_replace, $buffer); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilters/StringReplacementFilterFactory.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilters/StringReplacementFilterFactory.php new file mode 100644 index 0000000..fcd4b83 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/StreamFilters/StringReplacementFilterFactory.php @@ -0,0 +1,53 @@ +_filters[$search][$replace])) + { + if (!isset($this->_filters[$search])) + { + $this->_filters[$search] = array(); + } + + if (!isset($this->_filters[$search][$replace])) + { + $this->_filters[$search][$replace] = array(); + } + + $this->_filters[$search][$replace] + = new Swift_StreamFilters_StringReplacementFilter($search, $replace); + } + + return $this->_filters[$search][$replace]; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/SwiftException.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/SwiftException.php new file mode 100644 index 0000000..bd3b656 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/SwiftException.php @@ -0,0 +1,28 @@ +_eventDispatcher = $dispatcher; + $this->_buffer = $buf; + $this->_lookupHostname(); + } + + /** + * Set the name of the local domain which Swift will identify itself as. + * This should be a fully-qualified domain name and should be truly the domain + * you're using. If your server doesn't have a domain name, use the IP in square + * brackets (i.e. [127.0.0.1]). + * + * @param string $domain + */ + public function setLocalDomain($domain) + { + $this->_domain = $domain; + return $this; + } + + /** + * Get the name of the domain Swift will identify as. + * + * @return string + */ + public function getLocalDomain() + { + return $this->_domain; + } + + /** + * Start the SMTP connection. + */ + public function start() + { + if (!$this->_started) + { + if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStarted'); + if ($evt->bubbleCancelled()) + { + return; + } + } + + try + { + $this->_buffer->initialize($this->_getBufferParams()); + } + catch (Swift_TransportException $e) + { + $this->_throwException($e); + } + $this->_readGreeting(); + $this->_doHeloCommand(); + + if ($evt) + { + $this->_eventDispatcher->dispatchEvent($evt, 'transportStarted'); + } + + $this->_started = true; + } + } + + /** + * Test if an SMTP connection has been established. + * + * @return boolean + */ + public function isStarted() + { + return $this->_started; + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retreived from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param Swift_Mime_Message $message + * @param string[] &$failedRecipients to collect failures by-reference + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $sent = 0; + $failedRecipients = (array) $failedRecipients; + + if (!$reversePath = $this->_getReversePath($message)) + { + throw new Swift_TransportException( + 'Cannot send message without a sender address' + ); + } + + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) + { + return 0; + } + } + + $to = (array) $message->getTo(); + $cc = (array) $message->getCc(); + $bcc = (array) $message->getBcc(); + + $message->setBcc(array()); + + try + { + $sent += $this->_sendTo($message, $reversePath, $to, $failedRecipients); + $sent += $this->_sendCc($message, $reversePath, $cc, $failedRecipients); + $sent += $this->_sendBcc($message, $reversePath, $bcc, $failedRecipients); + } + catch (Exception $e) + { + $message->setBcc($bcc); + throw $e; + } + + $message->setBcc($bcc); + + if ($evt) + { + if ($sent == count($to) + count($cc) + count($bcc)) + { + $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); + } + elseif ($sent > 0) + { + $evt->setResult(Swift_Events_SendEvent::RESULT_TENTATIVE); + } + else + { + $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED); + } + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + $message->generateId(); //Make sure a new Message ID is used + + return $sent; + } + + /** + * Stop the SMTP connection. + */ + public function stop() + { + if ($this->_started) + { + if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStopped'); + if ($evt->bubbleCancelled()) + { + return; + } + } + + try + { + $this->executeCommand("QUIT\r\n", array(221)); + } + catch (Swift_TransportException $e) {} + + try + { + $this->_buffer->terminate(); + + if ($evt) + { + $this->_eventDispatcher->dispatchEvent($evt, 'transportStopped'); + } + } + catch (Swift_TransportException $e) + { + $this->_throwException($e); + } + } + $this->_started = false; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_eventDispatcher->bindEventListener($plugin); + } + + /** + * Reset the current mail transaction. + */ + public function reset() + { + $this->executeCommand("RSET\r\n", array(250)); + } + + /** + * Get the IoBuffer where read/writes are occurring. + * + * @return Swift_Transport_IoBuffer + */ + public function getBuffer() + { + return $this->_buffer; + } + + /** + * Run a command against the buffer, expecting the given response codes. + * + * If no response codes are given, the response will not be validated. + * If codes are given, an exception will be thrown on an invalid response. + * + * @param string $command + * @param int[] $codes + * @param string[] &$failures + * @return string + */ + public function executeCommand($command, $codes = array(), &$failures = null) + { + $failures = (array) $failures; + $seq = $this->_buffer->write($command); + $response = $this->_getFullResponse($seq); + if ($evt = $this->_eventDispatcher->createCommandEvent($this, $command, $codes)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'commandSent'); + } + $this->_assertResponseCode($response, $codes); + return $response; + } + + // -- Protected methods + + /** Read the opening SMTP greeting */ + protected function _readGreeting() + { + $this->_assertResponseCode($this->_getFullResponse(0), array(220)); + } + + /** Send the HELO welcome */ + protected function _doHeloCommand() + { + $this->executeCommand( + sprintf("HELO %s\r\n", $this->_domain), array(250) + ); + } + + /** Send the MAIL FROM command */ + protected function _doMailFromCommand($address) + { + $this->executeCommand( + sprintf("MAIL FROM: <%s>\r\n", $address), array(250) + ); + } + + /** Send the RCPT TO command */ + protected function _doRcptToCommand($address) + { + $this->executeCommand( + sprintf("RCPT TO: <%s>\r\n", $address), array(250, 251, 252) + ); + } + + /** Send the DATA command */ + protected function _doDataCommand() + { + $this->executeCommand("DATA\r\n", array(354)); + } + + /** Stream the contents of the message over the buffer */ + protected function _streamMessage(Swift_Mime_Message $message) + { + $this->_buffer->setWriteTranslations(array("\r\n." => "\r\n..")); + try + { + $message->toByteStream($this->_buffer); + $this->_buffer->flushBuffers(); + } + catch (Swift_TransportException $e) + { + $this->_throwException($e); + } + $this->_buffer->setWriteTranslations(array()); + $this->executeCommand("\r\n.\r\n", array(250)); + } + + /** Determine the best-use reverse path for this message */ + protected function _getReversePath(Swift_Mime_Message $message) + { + $return = $message->getReturnPath(); + $sender = $message->getSender(); + $from = $message->getFrom(); + $path = null; + if (!empty($return)) + { + $path = $return; + } + elseif (!empty($sender)) + { + // Don't use array_keys + reset($sender); // Reset Pointer to first pos + $path = key($sender); // Get key + } + elseif (!empty($from)) + { + reset($from); // Reset Pointer to first pos + $path = key($from); // Get key + } + return $path; + } + + /** Throw a TransportException, first sending it to any listeners */ + protected function _throwException(Swift_TransportException $e) + { + if ($evt = $this->_eventDispatcher->createTransportExceptionEvent($this, $e)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'exceptionThrown'); + if (!$evt->bubbleCancelled()) + { + throw $e; + } + } + else + { + throw $e; + } + } + + /** Throws an Exception if a response code is incorrect */ + protected function _assertResponseCode($response, $wanted) + { + list($code, $separator, $text) = sscanf($response, '%3d%[ -]%s'); + $valid = (empty($wanted) || in_array($code, $wanted)); + + if ($evt = $this->_eventDispatcher->createResponseEvent($this, $response, + $valid)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'responseReceived'); + } + + if (!$valid) + { + $this->_throwException( + new Swift_TransportException( + 'Expected response code ' . implode('/', $wanted) . ' but got code ' . + '"' . $code . '", with message "' . $response . '"' + ) + ); + } + } + + /** Get an entire multi-line response using its sequence number */ + protected function _getFullResponse($seq) + { + $response = ''; + try + { + do + { + $line = $this->_buffer->readLine($seq); + $response .= $line; + } + while (null !== $line && false !== $line && ' ' != $line{3}); + } + catch (Swift_TransportException $e) + { + $this->_throwException($e); + } + return $response; + } + + // -- Private methods + + /** Send an email to the given recipients from the given reverse path */ + private function _doMailTransaction($message, $reversePath, + array $recipients, array &$failedRecipients) + { + $sent = 0; + $this->_doMailFromCommand($reversePath); + foreach ($recipients as $forwardPath) + { + try + { + $this->_doRcptToCommand($forwardPath); + $sent++; + } + catch (Swift_TransportException $e) + { + $failedRecipients[] = $forwardPath; + } + } + + if ($sent != 0) + { + $this->_doDataCommand(); + $this->_streamMessage($message); + } + else + { + $this->reset(); + } + + return $sent; + } + + /** Send a message to the given To: recipients */ + private function _sendTo(Swift_Mime_Message $message, $reversePath, + array $to, array &$failedRecipients) + { + if (empty($to)) + { + return 0; + } + return $this->_doMailTransaction($message, $reversePath, array_keys($to), + $failedRecipients); + } + + /** Send a message to the given Cc: recipients */ + private function _sendCc(Swift_Mime_Message $message, $reversePath, + array $cc, array &$failedRecipients) + { + if (empty($cc)) + { + return 0; + } + return $this->_doMailTransaction($message, $reversePath, array_keys($cc), + $failedRecipients); + } + + /** Send a message to all Bcc: recipients */ + private function _sendBcc(Swift_Mime_Message $message, $reversePath, + array $bcc, array &$failedRecipients) + { + $sent = 0; + foreach ($bcc as $forwardPath => $name) + { + $message->setBcc(array($forwardPath => $name)); + $sent += $this->_doMailTransaction( + $message, $reversePath, array($forwardPath), $failedRecipients + ); + } + return $sent; + } + + /** Try to determine the hostname of the server this is run on */ + private function _lookupHostname() + { + if (!empty($_SERVER['SERVER_NAME']) + && $this->_isFqdn($_SERVER['SERVER_NAME'])) + { + $this->_domain = $_SERVER['SERVER_NAME']; + } + elseif (!empty($_SERVER['SERVER_ADDR'])) + { + $this->_domain = sprintf('[%s]', $_SERVER['SERVER_ADDR']); + } + } + + /** Determine is the $hostname is a fully-qualified name */ + private function _isFqdn($hostname) + { + //We could do a really thorough check, but there's really no point + if (false !== $dotPos = strpos($hostname, '.')) + { + return ($dotPos > 0) && ($dotPos != strlen($hostname) - 1); + } + else + { + return false; + } + } + + /** + * Destructor. + */ + public function __destruct() + { + $this->stop(); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php new file mode 100644 index 0000000..4c7e0f2 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/CramMd5Authenticator.php @@ -0,0 +1,88 @@ +executeCommand("AUTH CRAM-MD5\r\n", array(334)); + $challenge = base64_decode(substr($challenge, 4)); + $message = base64_encode( + $username . ' ' . $this->_getResponse($password, $challenge) + ); + $agent->executeCommand(sprintf("%s\r\n", $message), array(235)); + return true; + } + catch (Swift_TransportException $e) + { + $agent->executeCommand("RSET\r\n", array(250)); + return false; + } + } + + /** + * Generate a CRAM-MD5 response from a server challenge. + * @param string $secret + * @param string $challenge + * @return string + */ + private function _getResponse($secret, $challenge) + { + if (strlen($secret) > 64) + { + $secret = pack('H32', md5($secret)); + } + + if (strlen($secret) < 64) + { + $secret = str_pad($secret, 64, chr(0)); + } + + $k_ipad = substr($secret, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($secret, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H32', md5($k_ipad . $challenge)); + $digest = md5($k_opad . $inner); + + return $digest; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php new file mode 100644 index 0000000..bd22617 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/LoginAuthenticator.php @@ -0,0 +1,58 @@ +executeCommand("AUTH LOGIN\r\n", array(334)); + $agent->executeCommand(sprintf("%s\r\n", base64_encode($username)), array(334)); + $agent->executeCommand(sprintf("%s\r\n", base64_encode($password)), array(235)); + return true; + } + catch (Swift_TransportException $e) + { + $agent->executeCommand("RSET\r\n", array(250)); + return false; + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php new file mode 100644 index 0000000..ddd8094 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Auth/PlainAuthenticator.php @@ -0,0 +1,57 @@ +executeCommand(sprintf("AUTH PLAIN %s\r\n", $message), array(235)); + return true; + } + catch (Swift_TransportException $e) + { + $agent->executeCommand("RSET\r\n", array(250)); + return false; + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/AuthHandler.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/AuthHandler.php new file mode 100644 index 0000000..a223169 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/AuthHandler.php @@ -0,0 +1,262 @@ +setAuthenticators($authenticators); + } + + /** + * Set the Authenticators which can process a login request. + * @param Swift_Transport_Esmtp_Authenticator[] $authenticators + */ + public function setAuthenticators(array $authenticators) + { + $this->_authenticators = $authenticators; + } + + /** + * Get the Authenticators which can process a login request. + * @return Swift_Transport_Esmtp_Authenticator[] + */ + public function getAuthenticators() + { + return $this->_authenticators; + } + + /** + * Set the username to authenticate with. + * @param string $username + */ + public function setUsername($username) + { + $this->_username = $username; + } + + /** + * Get the username to authenticate with. + * @return string + */ + public function getUsername() + { + return $this->_username; + } + + /** + * Set the password to authenticate with. + * @param string $password + */ + public function setPassword($password) + { + $this->_password = $password; + } + + /** + * Get the password to authenticate with. + * @return string + */ + public function getPassword() + { + return $this->_password; + } + + /** + * Set the auth mode to use to authenticate. + * @param string $mode + */ + public function setAuthMode($mode) + { + $this->_auth_mode = $mode; + } + + /** + * Get the auth mode to use to authenticate. + * @return string + */ + public function getAuthMode() + { + return $this->_auth_mode; + } + + /** + * Get the name of the ESMTP extension this handles. + * @return boolean + */ + public function getHandledKeyword() + { + return 'AUTH'; + } + + /** + * Set the parameters which the EHLO greeting indicated. + * @param string[] $parameters + */ + public function setKeywordParams(array $parameters) + { + $this->_esmtpParams = $parameters; + } + + /** + * Runs immediately after a EHLO has been issued. + * @param Swift_Transport_SmtpAgent $agent to read/write + */ + public function afterEhlo(Swift_Transport_SmtpAgent $agent) + { + if ($this->_username) + { + $count = 0; + foreach ($this->_getAuthenticatorsForAgent() as $authenticator) + { + if (in_array(strtolower($authenticator->getAuthKeyword()), + array_map('strtolower', $this->_esmtpParams))) + { + $count++; + if ($authenticator->authenticate($agent, $this->_username, $this->_password)) + { + return; + } + } + } + throw new Swift_TransportException( + 'Failed to authenticate on SMTP server with username "' . + $this->_username . '" using ' . $count . ' possible authenticators' + ); + } + } + + /** + * Not used. + */ + public function getMailParams() + { + return array(); + } + + /** + * Not used. + */ + public function getRcptParams() + { + return array(); + } + + /** + * Not used. + */ + public function onCommand(Swift_Transport_SmtpAgent $agent, + $command, $codes = array(), &$failedRecipients = null, &$stop = false) + { + } + + /** + * Returns +1, -1 or 0 according to the rules for usort(). + * This method is called to ensure extensions can be execute in an appropriate order. + * @param string $esmtpKeyword to compare with + * @return int + */ + public function getPriorityOver($esmtpKeyword) + { + return 0; + } + + /** + * Returns an array of method names which are exposed to the Esmtp class. + * @return string[] + */ + public function exposeMixinMethods() + { + return array('setUsername', 'getUsername', 'setPassword', 'getPassword', 'setAuthMode', 'getAuthMode'); + } + + /** + * Not used. + */ + public function resetState() + { + } + + // -- Protected methods + + /** + * Returns the authenticator list for the given agent. + * @param Swift_Transport_SmtpAgent $agent + * @return array + * @access protected + */ + protected function _getAuthenticatorsForAgent() + { + if (!$mode = strtolower($this->_auth_mode)) + { + return $this->_authenticators; + } + + foreach ($this->_authenticators as $authenticator) + { + if (strtolower($authenticator->getAuthKeyword()) == $mode) + { + return array($authenticator); + } + } + + throw new Swift_TransportException('Auth mode '.$mode.' is invalid'); + } +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Authenticator.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Authenticator.php new file mode 100644 index 0000000..bf166d3 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/Esmtp/Authenticator.php @@ -0,0 +1,38 @@ +. + * @return string[] + */ + public function getMailParams(); + + /** + * Get params which are appended to RCPT TO:<>. + * @return string[] + */ + public function getRcptParams(); + + /** + * Runs when a command is due to be sent. + * @param Swift_Transport_SmtpAgent $agent to read/write + * @param string $command to send + * @param int[] $codes expected in response + * @param string[] &$failedRecipients + * @param boolean &$stop to be set true if the command is now sent + */ + public function onCommand(Swift_Transport_SmtpAgent $agent, + $command, $codes = array(), &$failedRecipients = null, &$stop = false); + + /** + * Returns +1, -1 or 0 according to the rules for usort(). + * This method is called to ensure extensions can be execute in an appropriate order. + * @param string $esmtpKeyword to compare with + * @return int + */ + public function getPriorityOver($esmtpKeyword); + + /** + * Returns an array of method names which are exposed to the Esmtp class. + * @return string[] + */ + public function exposeMixinMethods(); + + /** + * Tells this handler to clear any buffers and reset its state. + */ + public function resetState(); + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/EsmtpTransport.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/EsmtpTransport.php new file mode 100644 index 0000000..c7833c3 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/EsmtpTransport.php @@ -0,0 +1,340 @@ + 'tcp', + 'host' => 'localhost', + 'port' => 25, + 'timeout' => 30, + 'blocking' => 1, + 'type' => Swift_Transport_IoBuffer::TYPE_SOCKET + ); + + /** + * Creates a new EsmtpTransport using the given I/O buffer. + * @param Swift_Transport_IoBuffer $buf + * @param Swift_Transport_EsmtpHandler[] $extensionHandlers + * @param Swift_Events_EventDispatcher $dispatcher + */ + public function __construct(Swift_Transport_IoBuffer $buf, + array $extensionHandlers, Swift_Events_EventDispatcher $dispatcher) + { + parent::__construct($buf, $dispatcher); + $this->setExtensionHandlers($extensionHandlers); + } + + /** + * Set the host to connect to. + * @param string $host + */ + public function setHost($host) + { + $this->_params['host'] = $host; + return $this; + } + + /** + * Get the host to connect to. + * @return string + */ + public function getHost() + { + return $this->_params['host']; + } + + /** + * Set the port to connect to. + * @param int $port + */ + public function setPort($port) + { + $this->_params['port'] = (int) $port; + return $this; + } + + /** + * Get the port to connect to. + * @return int + */ + public function getPort() + { + return $this->_params['port']; + } + + /** + * Set the connection timeout. + * @param int $timeout seconds + */ + public function setTimeout($timeout) + { + $this->_params['timeout'] = (int) $timeout; + return $this; + } + + /** + * Get the connection timeout. + * @return int + */ + public function getTimeout() + { + return $this->_params['timeout']; + } + + /** + * Set the encryption type (tls or ssl) + * @param string $encryption + */ + public function setEncryption($enc) + { + $this->_params['protocol'] = $enc; + return $this; + } + + /** + * Get the encryption type. + * @return string + */ + public function getEncryption() + { + return $this->_params['protocol']; + } + + /** + * Set ESMTP extension handlers. + * @param Swift_Transport_EsmtpHandler[] $handlers + */ + public function setExtensionHandlers(array $handlers) + { + $assoc = array(); + foreach ($handlers as $handler) + { + $assoc[$handler->getHandledKeyword()] = $handler; + } + uasort($assoc, array($this, '_sortHandlers')); + $this->_handlers = $assoc; + $this->_setHandlerParams(); + return $this; + } + + /** + * Get ESMTP extension handlers. + * @return Swift_Transport_EsmtpHandler[] + */ + public function getExtensionHandlers() + { + return array_values($this->_handlers); + } + + /** + * Run a command against the buffer, expecting the given response codes. + * If no response codes are given, the response will not be validated. + * If codes are given, an exception will be thrown on an invalid response. + * @param string $command + * @param int[] $codes + * @param string[] &$failures + * @return string + */ + public function executeCommand($command, $codes = array(), &$failures = null) + { + $failures = (array) $failures; + $stopSignal = false; + $response = null; + foreach ($this->_getActiveHandlers() as $handler) + { + $response = $handler->onCommand( + $this, $command, $codes, $failures, $stopSignal + ); + if ($stopSignal) + { + return $response; + } + } + return parent::executeCommand($command, $codes, $failures); + } + + // -- Mixin invocation code + + /** Mixin handling method for ESMTP handlers */ + public function __call($method, $args) + { + foreach ($this->_handlers as $handler) + { + if (in_array(strtolower($method), + array_map('strtolower', (array) $handler->exposeMixinMethods()) + )) + { + $return = call_user_func_array(array($handler, $method), $args); + //Allow fluid method calls + if (is_null($return) && substr($method, 0, 3) == 'set') + { + return $this; + } + else + { + return $return; + } + } + } + trigger_error('Call to undefined method ' . $method, E_USER_ERROR); + } + + // -- Protected methods + + /** Get the params to initialize the buffer */ + protected function _getBufferParams() + { + return $this->_params; + } + + /** Overridden to perform EHLO instead */ + protected function _doHeloCommand() + { + try + { + $response = $this->executeCommand( + sprintf("EHLO %s\r\n", $this->_domain), array(250) + ); + } + catch (Swift_TransportException $e) + { + return parent::_doHeloCommand(); + } + + $this->_capabilities = $this->_getCapabilities($response); + $this->_setHandlerParams(); + foreach ($this->_getActiveHandlers() as $handler) + { + $handler->afterEhlo($this); + } + } + + /** Overridden to add Extension support */ + protected function _doMailFromCommand($address) + { + $handlers = $this->_getActiveHandlers(); + $params = array(); + foreach ($handlers as $handler) + { + $params = array_merge($params, (array) $handler->getMailParams()); + } + $paramStr = !empty($params) ? ' ' . implode(' ', $params) : ''; + $this->executeCommand( + sprintf("MAIL FROM: <%s>%s\r\n", $address, $paramStr), array(250) + ); + } + + /** Overridden to add Extension support */ + protected function _doRcptToCommand($address) + { + $handlers = $this->_getActiveHandlers(); + $params = array(); + foreach ($handlers as $handler) + { + $params = array_merge($params, (array) $handler->getRcptParams()); + } + $paramStr = !empty($params) ? ' ' . implode(' ', $params) : ''; + $this->executeCommand( + sprintf("RCPT TO: <%s>%s\r\n", $address, $paramStr), array(250, 251, 252) + ); + } + + // -- Private methods + + /** Determine ESMTP capabilities by function group */ + private function _getCapabilities($ehloResponse) + { + $capabilities = array(); + $ehloResponse = trim($ehloResponse); + $lines = explode("\r\n", $ehloResponse); + array_shift($lines); + foreach ($lines as $line) + { + if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) + { + $keyword = strtoupper($matches[1]); + $paramStr = strtoupper(ltrim($matches[2], ' =')); + $params = !empty($paramStr) ? explode(' ', $paramStr) : array(); + $capabilities[$keyword] = $params; + } + } + return $capabilities; + } + + /** Set parameters which are used by each extension handler */ + private function _setHandlerParams() + { + foreach ($this->_handlers as $keyword => $handler) + { + if (array_key_exists($keyword, $this->_capabilities)) + { + $handler->setKeywordParams($this->_capabilities[$keyword]); + } + } + } + + /** Get ESMTP handlers which are currently ok to use */ + private function _getActiveHandlers() + { + $handlers = array(); + foreach ($this->_handlers as $keyword => $handler) + { + if (array_key_exists($keyword, $this->_capabilities)) + { + $handlers[] = $handler; + } + } + return $handlers; + } + + /** Custom sort for extension handler ordering */ + private function _sortHandlers($a, $b) + { + return $a->getPriorityOver($b->getHandledKeyword()); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/FailoverTransport.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/FailoverTransport.php new file mode 100644 index 0000000..e62491c --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/FailoverTransport.php @@ -0,0 +1,97 @@ +_transports); + $sent = 0; + + for ($i = 0; $i < $maxTransports + && $transport = $this->_getNextTransport(); ++$i) + { + try + { + if (!$transport->isStarted()) + { + $transport->start(); + } + + return $transport->send($message, $failedRecipients); + } + catch (Swift_TransportException $e) + { + $this->_killCurrentTransport(); + } + } + + if (count($this->_transports) == 0) + { + throw new Swift_TransportException( + 'All Transports in FailoverTransport failed, or no Transports available' + ); + } + + return $sent; + } + + // -- Protected methods + + protected function _getNextTransport() + { + if (!isset($this->_currentTransport)) + { + $this->_currentTransport = parent::_getNextTransport(); + } + return $this->_currentTransport; + } + + protected function _killCurrentTransport() + { + $this->_currentTransport = null; + parent::_killCurrentTransport(); + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/IoBuffer.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/IoBuffer.php new file mode 100644 index 0000000..ac66ef0 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/IoBuffer.php @@ -0,0 +1,65 @@ +_transports = $transports; + $this->_deadTransports = array(); + } + + /** + * Get $transports to delegate to. + * + * @return array Swift_Transport + */ + public function getTransports(array $transports) + { + return array_merge($this->_transports, $this->_deadTransports); + } + + /** + * Test if this Transport mechanism has started. + * + * @return boolean + */ + public function isStarted() + { + return count($this->_transports) > 0; + } + + /** + * Start this Transport mechanism. + */ + public function start() + { + $this->_transports = array_merge($this->_transports, $this->_deadTransports); + } + + /** + * Stop this Transport mechanism. + */ + public function stop() + { + foreach ($this->_transports as $transport) + { + $transport->stop(); + } + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retreived from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param Swift_Mime_Message $message + * @param string[] &$failedRecipients to collect failures by-reference + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $maxTransports = count($this->_transports); + $sent = 0; + + for ($i = 0; $i < $maxTransports + && $transport = $this->_getNextTransport(); ++$i) + { + try + { + if (!$transport->isStarted()) + { + $transport->start(); + } + if ($sent = $transport->send($message, $failedRecipients)) + { + break; + } + } + catch (Swift_TransportException $e) + { + $this->_killCurrentTransport(); + } + } + + if (count($this->_transports) == 0) + { + throw new Swift_TransportException( + 'All Transports in LoadBalancedTransport failed, or no Transports available' + ); + } + + return $sent; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + foreach ($this->_transports as $transport) + { + $transport->registerPlugin($plugin); + } + } + + // -- Protected methods + + /** + * Rotates the transport list around and returns the first instance. + * + * @return Swift_Transport + * @access protected + */ + protected function _getNextTransport() + { + if ($next = array_shift($this->_transports)) + { + $this->_transports[] = $next; + } + return $next; + } + + /** + * Tag the currently used (top of stack) transport as dead/useless. + * + * @access protected + */ + protected function _killCurrentTransport() + { + if ($transport = array_pop($this->_transports)) + { + try + { + $transport->stop(); + } + catch (Exception $e) + { + } + $this->_deadTransports[] = $transport; + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/MailInvoker.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/MailInvoker.php new file mode 100644 index 0000000..dda882f --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/MailInvoker.php @@ -0,0 +1,36 @@ +_invoker = $invoker; + $this->_eventDispatcher = $eventDispatcher; + } + + /** + * Not used. + */ + public function isStarted() + { + return false; + } + + /** + * Not used. + */ + public function start() + { + } + + /** + * Not used. + */ + public function stop() + { + } + + /** + * Set the additional parameters used on the mail() function. + * + * This string is formatted for sprintf() where %s is the sender address. + * + * @param string $params + */ + public function setExtraParams($params) + { + $this->_extraParams = $params; + return $this; + } + + /** + * Get the additional parameters used on the mail() function. + * + * This string is formatted for sprintf() where %s is the sender address. + * + * @return string + */ + public function getExtraParams() + { + return $this->_extraParams; + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retreived from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param Swift_Mime_Message $message + * @param string[] &$failedRecipients to collect failures by-reference + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $failedRecipients = (array) $failedRecipients; + + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) + { + return 0; + } + } + + $count = ( + count((array) $message->getTo()) + + count((array) $message->getCc()) + + count((array) $message->getBcc()) + ); + + $toHeader = $message->getHeaders()->get('To'); + $subjectHeader = $message->getHeaders()->get('Subject'); + + $to = $toHeader->getFieldBody(); + $subject = $subjectHeader->getFieldBody(); + + $reversePath = $this->_getReversePath($message); + + //Remove headers that would otherwise be duplicated + $message->getHeaders()->remove('To'); + $message->getHeaders()->remove('Subject'); + + $messageStr = $message->toString(); + + $message->getHeaders()->set($toHeader); + $message->getHeaders()->set($subjectHeader); + + //Separate headers from body + if (false !== $endHeaders = strpos($messageStr, "\r\n\r\n")) + { + $headers = substr($messageStr, 0, $endHeaders) . "\r\n"; //Keep last EOL + $body = substr($messageStr, $endHeaders + 4); + } + else + { + $headers = $messageStr . "\r\n"; + $body = ''; + } + + unset($messageStr); + + if ("\r\n" != PHP_EOL) //Non-windows (not using SMTP) + { + $headers = str_replace("\r\n", PHP_EOL, $headers); + $body = str_replace("\r\n", PHP_EOL, $body); + } + else //Windows, using SMTP + { + $headers = str_replace("\r\n.", "\r\n..", $headers); + $body = str_replace("\r\n.", "\r\n..", $body); + } + + if ($this->_invoker->mail($to, $subject, $body, $headers, + sprintf($this->_extraParams, $reversePath))) + { + if ($evt) + { + $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + } + else + { + $failedRecipients = array_merge( + $failedRecipients, + array_keys((array) $message->getTo()), + array_keys((array) $message->getCc()), + array_keys((array) $message->getBcc()) + ); + + if ($evt) + { + $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED); + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + $message->generateId(); + + $count = 0; + } + + return $count; + } + + /** + * Register a plugin. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->_eventDispatcher->bindEventListener($plugin); + } + + // -- Private methods + + /** Determine the best-use reverse path for this message */ + private function _getReversePath(Swift_Mime_Message $message) + { + $return = $message->getReturnPath(); + $sender = $message->getSender(); + $from = $message->getFrom(); + $path = null; + if (!empty($return)) + { + $path = $return; + } + elseif (!empty($sender)) + { + $keys = array_keys($sender); + $path = array_shift($keys); + } + elseif (!empty($from)) + { + $keys = array_keys($from); + $path = array_shift($keys); + } + return $path; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SendmailTransport.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SendmailTransport.php new file mode 100644 index 0000000..aae8bde --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SendmailTransport.php @@ -0,0 +1,173 @@ + 30, + 'blocking' => 1, + 'command' => '/usr/sbin/sendmail -bs', + 'type' => Swift_Transport_IoBuffer::TYPE_PROCESS + ); + + /** + * Create a new SendmailTransport with $buf for I/O. + * @param Swift_Transport_IoBuffer $buf + * @param Swift_Events_EventDispatcher $dispatcher + */ + public function __construct(Swift_Transport_IoBuffer $buf, + Swift_Events_EventDispatcher $dispatcher) + { + parent::__construct($buf, $dispatcher); + } + + /** + * Start the standalone SMTP session if running in -bs mode. + */ + public function start() + { + if (false !== strpos($this->getCommand(), ' -bs')) + { + parent::start(); + } + } + + /** + * Set the command to invoke. + * If using -t mode you are strongly advised to include -oi or -i in the + * flags. For example: /usr/sbin/sendmail -oi -t + * Swift will append a -f flag if one is not present. + * The recommended mode is "-bs" since it is interactive and failure notifications + * are hence possible. + * @param string $command + */ + public function setCommand($command) + { + $this->_params['command'] = $command; + return $this; + } + + /** + * Get the sendmail command which will be invoked. + * @return string + */ + public function getCommand() + { + return $this->_params['command']; + } + + /** + * Send the given Message. + * Recipient/sender data will be retreived from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * NOTE: If using 'sendmail -t' you will not be aware of any failures until + * they bounce (i.e. send() will always return 100% success). + * @param Swift_Mime_Message $message + * @param string[] &$failedRecipients to collect failures by-reference + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $failedRecipients = (array) $failedRecipients; + $command = $this->getCommand(); + $buffer = $this->getBuffer(); + + if (false !== strpos($command, ' -t')) + { + if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) + { + $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); + if ($evt->bubbleCancelled()) + { + return 0; + } + } + + if (false === strpos($command, ' -f')) + { + $command .= ' -f' . $this->_getReversePath($message); + } + + $buffer->initialize(array_merge($this->_params, array('command' => $command))); + + if (false === strpos($command, ' -i') && false === strpos($command, ' -oi')) + { + $buffer->setWriteTranslations(array("\r\n" => "\n", "\n." => "\n..")); + } + else + { + $buffer->setWriteTranslations(array("\r\n"=>"\n")); + } + + $count = count((array) $message->getTo()) + + count((array) $message->getCc()) + + count((array) $message->getBcc()) + ; + $message->toByteStream($buffer); + $buffer->flushBuffers(); + $buffer->setWriteTranslations(array()); + $buffer->terminate(); + + if ($evt) + { + $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); + $evt->setFailedRecipients($failedRecipients); + $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); + } + + $message->generateId(); + } + elseif (false !== strpos($command, ' -bs')) + { + $count = parent::send($message, $failedRecipients); + } + else + { + $this->_throwException(new Swift_TransportException( + 'Unsupported sendmail command flags [' . $command . ']. ' . + 'Must be one of "-bs" or "-t" but can include additional flags.' + )); + } + + return $count; + } + + // -- Protected methods + + /** Get the params to initialize the buffer */ + protected function _getBufferParams() + { + return $this->_params; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SimpleMailInvoker.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SimpleMailInvoker.php new file mode 100644 index 0000000..271ba84 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SimpleMailInvoker.php @@ -0,0 +1,58 @@ +. + + */ + +//@require 'Swift/Transport/MailInvoker.php'; + +/** + * This is the implementation class for {@link Swift_Transport_MailInvoker}. + * + * @package Swift + * @subpackage Transport + * @author Chris Corbyn + */ +class Swift_Transport_SimpleMailInvoker implements Swift_Transport_MailInvoker +{ + + /** + * Send mail via the mail() function. + * + * This method takes the same arguments as PHP mail(). + * + * @param string $to + * @param string $subject + * @param string $body + * @param string $headers + * @param string $extraParams + * + * @return boolean + */ + public function mail($to, $subject, $body, $headers = null, $extraParams = null) + { + if (!ini_get('safe_mode')) + { + return mail($to, $subject, $body, $headers, $extraParams); + } + else + { + return mail($to, $subject, $body, $headers); + } + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SmtpAgent.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SmtpAgent.php new file mode 100644 index 0000000..ee9b8ed --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/Transport/SmtpAgent.php @@ -0,0 +1,36 @@ +_replacementFactory = $replacementFactory; + } + + /** + * Perform any initialization needed, using the given $params. + * Parameters will vary depending upon the type of IoBuffer used. + * @param array $params + */ + public function initialize(array $params) + { + $this->_params = $params; + switch ($params['type']) + { + case self::TYPE_PROCESS: + $this->_establishProcessConnection(); + break; + case self::TYPE_SOCKET: + default: + $this->_establishSocketConnection(); + break; + } + } + + /** + * Set an individual param on the buffer (e.g. switching to SSL). + * @param string $param + * @param mixed $value + */ + public function setParam($param, $value) + { + if (isset($this->_stream)) + { + switch ($param) + { + case 'protocol': + if (!array_key_exists('protocol', $this->_params) + || $value != $this->_params['protocol']) + { + if ('tls' == $value) + { + stream_socket_enable_crypto( + $this->_stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT + ); + } + } + break; + } + } + $this->_params[$param] = $value; + } + + /** + * Perform any shutdown logic needed. + */ + public function terminate() + { + if (isset($this->_stream)) + { + switch ($this->_params['type']) + { + case self::TYPE_PROCESS: + fclose($this->_in); + fclose($this->_out); + proc_close($this->_stream); + break; + case self::TYPE_SOCKET: + default: + fclose($this->_stream); + break; + } + } + $this->_stream = null; + $this->_out = null; + $this->_in = null; + } + + /** + * Set an array of string replacements which should be made on data written + * to the buffer. This could replace LF with CRLF for example. + * @param string[] $replacements + */ + public function setWriteTranslations(array $replacements) + { + foreach ($this->_translations as $search => $replace) + { + if (!isset($replacements[$search])) + { + $this->removeFilter($search); + unset($this->_translations[$search]); + } + } + + foreach ($replacements as $search => $replace) + { + if (!isset($this->_translations[$search])) + { + $this->addFilter( + $this->_replacementFactory->createFilter($search, $replace), $search + ); + $this->_translations[$search] = true; + } + } + } + + /** + * Get a line of output (including any CRLF). + * The $sequence number comes from any writes and may or may not be used + * depending upon the implementation. + * @param int $sequence of last write to scan from + * @return string + */ + public function readLine($sequence) + { + if (isset($this->_out) && !feof($this->_out)) + { + $line = fgets($this->_out); + return $line; + } + } + + /** + * Reads $length bytes from the stream into a string and moves the pointer + * through the stream by $length. If less bytes exist than are requested the + * remaining bytes are given instead. If no bytes are remaining at all, boolean + * false is returned. + * @param int $length + * @return string + */ + public function read($length) + { + if (isset($this->_out) && !feof($this->_out)) + { + $ret = fread($this->_out, $length); + return $ret; + } + } + + /** Not implemented */ + public function setReadPointer($byteOffset) + { + } + + // -- Protected methods + + /** Flush the stream contents */ + protected function _flush() + { + if (isset($this->_in)) + { + fflush($this->_in); + } + } + + /** Write this bytes to the stream */ + protected function _commit($bytes) + { + if (isset($this->_in) + && fwrite($this->_in, $bytes)) + { + return ++$this->_sequence; + } + } + + // -- Private methods + + /** + * Establishes a connection to a remote server. + * @access private + */ + private function _establishSocketConnection() + { + $host = $this->_params['host']; + if (!empty($this->_params['protocol'])) + { + $host = $this->_params['protocol'] . '://' . $host; + } + $timeout = 15; + if (!empty($this->_params['timeout'])) + { + $timeout = $this->_params['timeout']; + } + if (!$this->_stream = fsockopen($host, $this->_params['port'], $errno, $errstr, $timeout)) + { + throw new Swift_TransportException( + 'Connection could not be established with host ' . $this->_params['host'] . + ' [' . $errstr . ' #' . $errno . ']' + ); + } + if (!empty($this->_params['blocking'])) + { + stream_set_blocking($this->_stream, 1); + } + else + { + stream_set_blocking($this->_stream, 0); + } + $this->_in =& $this->_stream; + $this->_out =& $this->_stream; + } + + /** + * Opens a process for input/output. + * @access private + */ + private function _establishProcessConnection() + { + $command = $this->_params['command']; + $descriptorSpec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ); + $this->_stream = proc_open($command, $descriptorSpec, $pipes); + stream_set_blocking($pipes[2], 0); + if ($err = stream_get_contents($pipes[2])) + { + throw new Swift_TransportException( + 'Process could not be started [' . $err . ']' + ); + } + $this->_in =& $pipes[0]; + $this->_out =& $pipes[1]; + } + +} diff --git a/includes/kohana/modules/khemail/vendor/swift/classes/Swift/TransportException.php b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/TransportException.php new file mode 100644 index 0000000..b7cd658 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/classes/Swift/TransportException.php @@ -0,0 +1,31 @@ + register('cache') + -> asAliasOf('cache.array') + + -> register('tempdir') + -> asValue('/tmp') + + -> register('cache.null') + -> asSharedInstanceOf('Swift_KeyCache_NullKeyCache') + + -> register('cache.array') + -> asSharedInstanceOf('Swift_KeyCache_ArrayKeyCache') + -> withDependencies(array('cache.inputstream')) + + -> register('cache.disk') + -> asSharedInstanceOf('Swift_KeyCache_DiskKeyCache') + -> withDependencies(array('cache.inputstream', 'tempdir')) + + -> register('cache.inputstream') + -> asNewInstanceOf('Swift_KeyCache_SimpleKeyCacheInputStream') + + ; diff --git a/includes/kohana/modules/khemail/vendor/swift/dependency_maps/mime_deps.php b/includes/kohana/modules/khemail/vendor/swift/dependency_maps/mime_deps.php new file mode 100644 index 0000000..e03927a --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/dependency_maps/mime_deps.php @@ -0,0 +1,97 @@ + register('properties.charset') + -> asValue('utf-8') + + -> register('mime.message') + -> asNewInstanceOf('Swift_Mime_SimpleMessage') + -> withDependencies(array( + 'mime.headerset', + 'mime.qpcontentencoder', + 'cache', + 'properties.charset' + )) + + -> register('mime.part') + -> asNewInstanceOf('Swift_Mime_MimePart') + -> withDependencies(array( + 'mime.headerset', + 'mime.qpcontentencoder', + 'cache', + 'properties.charset' + )) + + -> register('mime.attachment') + -> asNewInstanceOf('Swift_Mime_Attachment') + -> withDependencies(array( + 'mime.headerset', + 'mime.base64contentencoder', + 'cache' + )) + -> addConstructorValue($swift_mime_types) + + -> register('mime.embeddedfile') + -> asNewInstanceOf('Swift_Mime_EmbeddedFile') + -> withDependencies(array( + 'mime.headerset', + 'mime.base64contentencoder', + 'cache' + )) + -> addConstructorValue($swift_mime_types) + + -> register('mime.headerfactory') + -> asNewInstanceOf('Swift_Mime_SimpleHeaderFactory') + -> withDependencies(array( + 'mime.qpheaderencoder', + 'mime.rfc2231encoder', + 'properties.charset' + )) + + -> register('mime.headerset') + -> asNewInstanceOf('Swift_Mime_SimpleHeaderSet') + -> withDependencies(array('mime.headerfactory', 'properties.charset')) + + -> register('mime.qpheaderencoder') + -> asNewInstanceOf('Swift_Mime_HeaderEncoder_QpHeaderEncoder') + -> withDependencies(array('mime.charstream')) + + -> register('mime.charstream') + -> asNewInstanceOf('Swift_CharacterStream_NgCharacterStream') + -> withDependencies(array('mime.characterreaderfactory', 'properties.charset')) + + -> register('mime.bytecanonicalizer') + -> asSharedInstanceOf('Swift_StreamFilters_ByteArrayReplacementFilter') + -> addConstructorValue(array(array(0x0D, 0x0A), array(0x0D), array(0x0A))) + -> addConstructorValue(array(array(0x0A), array(0x0A), array(0x0D, 0x0A))) + + -> register('mime.characterreaderfactory') + -> asSharedInstanceOf('Swift_CharacterReaderFactory_SimpleCharacterReaderFactory') + + -> register('mime.qpcontentencoder') + -> asNewInstanceOf('Swift_Mime_ContentEncoder_QpContentEncoder') + -> withDependencies(array('mime.charstream', 'mime.bytecanonicalizer')) + + -> register('mime.7bitcontentencoder') + -> asNewInstanceOf('Swift_Mime_ContentEncoder_PlainContentEncoder') + -> addConstructorValue('7bit') + -> addConstructorValue(true) + + -> register('mime.8bitcontentencoder') + -> asNewInstanceOf('Swift_Mime_ContentEncoder_PlainContentEncoder') + -> addConstructorValue('8bit') + -> addConstructorValue(true) + + -> register('mime.base64contentencoder') + -> asSharedInstanceOf('Swift_Mime_ContentEncoder_Base64ContentEncoder') + + -> register('mime.rfc2231encoder') + -> asNewInstanceOf('Swift_Encoder_Rfc2231Encoder') + -> withDependencies(array('mime.charstream')) + + ; + +unset($swift_mime_types); diff --git a/includes/kohana/modules/khemail/vendor/swift/dependency_maps/transport_deps.php b/includes/kohana/modules/khemail/vendor/swift/dependency_maps/transport_deps.php new file mode 100644 index 0000000..32881d6 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/dependency_maps/transport_deps.php @@ -0,0 +1,62 @@ + register('transport.smtp') + -> asNewInstanceOf('Swift_Transport_EsmtpTransport') + -> withDependencies(array( + 'transport.buffer', + array('transport.authhandler'), + 'transport.eventdispatcher' + )) + + -> register('transport.sendmail') + -> asNewInstanceOf('Swift_Transport_SendmailTransport') + -> withDependencies(array( + 'transport.buffer', + 'transport.eventdispatcher' + )) + + -> register('transport.mail') + -> asNewInstanceOf('Swift_Transport_MailTransport') + -> withDependencies(array('transport.mailinvoker', 'transport.eventdispatcher')) + + -> register('transport.loadbalanced') + -> asNewInstanceOf('Swift_Transport_LoadBalancedTransport') + + -> register('transport.failover') + -> asNewInstanceOf('Swift_Transport_FailoverTransport') + + -> register('transport.mailinvoker') + -> asSharedInstanceOf('Swift_Transport_SimpleMailInvoker') + + -> register('transport.buffer') + -> asNewInstanceOf('Swift_Transport_StreamBuffer') + -> withDependencies(array('transport.replacementfactory')) + + -> register('transport.authhandler') + -> asNewInstanceOf('Swift_Transport_Esmtp_AuthHandler') + -> withDependencies(array( + array( + 'transport.crammd5auth', + 'transport.loginauth', + 'transport.plainauth' + ) + )) + + -> register('transport.crammd5auth') + -> asNewInstanceOf('Swift_Transport_Esmtp_Auth_CramMd5Authenticator') + + -> register('transport.loginauth') + -> asNewInstanceOf('Swift_Transport_Esmtp_Auth_LoginAuthenticator') + + -> register('transport.plainauth') + -> asNewInstanceOf('Swift_Transport_Esmtp_Auth_PlainAuthenticator') + + -> register('transport.eventdispatcher') + -> asNewInstanceOf('Swift_Events_SimpleEventDispatcher') + + -> register('transport.replacementfactory') + -> asSharedInstanceOf('Swift_StreamFilters_StringReplacementFilterFactory') + + ; diff --git a/includes/kohana/modules/khemail/vendor/swift/mime_types.php b/includes/kohana/modules/khemail/vendor/swift/mime_types.php new file mode 100644 index 0000000..65c9aa0 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/mime_types.php @@ -0,0 +1,76 @@ + 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'avi' => 'video/avi', + 'bmp' => 'image/bmp', + 'bz2' => 'application/x-bz2', + 'csv' => 'text/csv', + 'dmg' => 'application/x-apple-diskimage', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'eml' => 'message/rfc822', + 'aps' => 'application/postscript', + 'exe' => 'application/x-ms-dos-executable', + 'flv' => 'video/x-flv', + 'gif' => 'image/gif', + 'gz' => 'application/x-gzip', + 'hqx' => 'application/stuffit', + 'htm' => 'text/html', + 'html' => 'text/html', + 'jar' => 'application/x-java-archive', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'm3u' => 'audio/x-mpegurl', + 'm4a' => 'audio/mp4', + 'mdb' => 'application/x-msaccess', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mov' => 'video/quicktime', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'odg' => 'vnd.oasis.opendocument.graphics', + 'odp' => 'vnd.oasis.opendocument.presentation', + 'odt' => 'vnd.oasis.opendocument.text', + 'ods' => 'vnd.oasis.opendocument.spreadsheet', + 'ogg' => 'audio/ogg', + 'pdf' => 'application/pdf', + 'png' => 'image/png', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'rar' => 'application/x-rar-compressed', + 'rtf' => 'application/rtf', + 'tar' => 'application/x-tar', + 'sit' => 'application/x-stuffit', + 'svg' => 'image/svg+xml', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'ttf' => 'application/x-font-truetype', + 'txt' => 'text/plain', + 'vcf' => 'text/x-vcard', + 'wav' => 'audio/wav', + 'wma' => 'audio/x-ms-wma', + 'wmv' => 'audio/x-ms-wmv', + 'xls' => 'application/excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xml' => 'application/xml', + 'zip' => 'application/zip' +); diff --git a/includes/kohana/modules/khemail/vendor/swift/preferences.php b/includes/kohana/modules/khemail/vendor/swift/preferences.php new file mode 100644 index 0000000..0b9e4b1 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/preferences.php @@ -0,0 +1,20 @@ +setCharset('utf-8'); + +// Without these lines the default caching mechanism is "array" but this uses +// a lot of memory. +// If possible, use a disk cache to enable attaching large attachments etc +if (function_exists('sys_get_temp_dir') && is_writable(sys_get_temp_dir())) +{ + Swift_Preferences::getInstance() + -> setTempDir(sys_get_temp_dir()) + -> setCacheType('disk'); +} diff --git a/includes/kohana/modules/khemail/vendor/swift/swift_init.php b/includes/kohana/modules/khemail/vendor/swift/swift_init.php new file mode 100644 index 0000000..fe624a9 --- /dev/null +++ b/includes/kohana/modules/khemail/vendor/swift/swift_init.php @@ -0,0 +1,21 @@ + '0;30', + 'dark_gray' => '1;30', + 'blue' => '0;34', + 'light_blue' => '1;34', + 'green' => '0;32', + 'light_green' => '1;32', + 'cyan' => '0;36', + 'light_cyan' => '1;36', + 'red' => '0;31', + 'light_red' => '1;31', + 'purple' => '0;35', + 'light_purple' => '1;35', + 'brown' => '0;33', + 'yellow' => '1;33', + 'light_gray' => '0;37', + 'white' => '1;37', + ); + protected static $background_colors = array( + 'black' => '40', + 'red' => '41', + 'green' => '42', + 'yellow' => '43', + 'blue' => '44', + 'magenta' => '45', + 'cyan' => '46', + 'light_gray' => '47', + ); + + /** + * Returns one or more command-line options. Options are specified using + * standard CLI syntax: + * + * php index.php --username=john.smith --password=secret --var="some value with spaces" + * + * // Get the values of "username" and "password" + * $auth = Minion_CLI::options('username', 'password'); + * + * @param string $options,... option name + * @return array + */ + public static function options($options = NULL) + { + // Get all of the requested options + $options = func_get_args(); + + // Found option values + $values = array(); + + // Skip the first option, it is always the file executed + for ($i = 1; $i < $_SERVER['argc']; $i++) + { + if ( ! isset($_SERVER['argv'][$i])) + { + // No more args left + break; + } + + // Get the option + $opt = $_SERVER['argv'][$i]; + + if (substr($opt, 0, 2) !== '--') + { + // This is a positional argument + $values[] = $opt; + continue; + } + + // Remove the "--" prefix + $opt = substr($opt, 2); + + if (strpos($opt, '=')) + { + // Separate the name and value + list ($opt, $value) = explode('=', $opt, 2); + } + else + { + $value = NULL; + } + + $values[$opt] = $value; + } + + if ($options) + { + foreach ($values as $opt => $value) + { + if ( ! in_array($opt, $options)) + { + // Set the given value + unset($values[$opt]); + } + } + } + + return count($options) == 1 ? array_pop($values) : $values; + } + + /** + * Reads input from the user. This can have either 1 or 2 arguments. + * + * Usage: + * + * // Waits for any key press + * Minion_CLI::read(); + * + * // Takes any input + * $color = Minion_CLI::read('What is your favorite color?'); + * + * // Will only accept the options in the array + * $ready = Minion_CLI::read('Are you ready?', array('y','n')); + * + * @param string $text text to show user before waiting for input + * @param array $options array of options the user is shown + * @return string the user input + */ + public static function read($text = '', array $options = NULL) + { + // If a question has been asked with the read + $options_output = ''; + if ( ! empty($options)) + { + $options_output = ' [ '.implode(', ', $options).' ]'; + } + + fwrite(STDOUT, $text.$options_output.': '); + + // Read the input from keyboard. + $input = trim(fgets(STDIN)); + + // If options are provided and the choice is not in the array, tell them to try again + if ( ! empty($options) && ! in_array($input, $options)) + { + Minion_CLI::write('This is not a valid option. Please try again.'); + + $input = Minion_CLI::read($text, $options); + } + + // Read the input + return $input; + } + + /** + * Experimental feature. + * + * Reads hidden input from the user + * + * Usage: + * + * $password = Minion_CLI::password('Enter your password'); + * + * @author Mathew Davies. + * @return string + */ + public static function password($text = '') + { + $text .= ': '; + + if (Kohana::$is_windows) + { + $vbscript = sys_get_temp_dir().'Minion_CLI_Password.vbs'; + + // Create temporary file + file_put_contents($vbscript, 'wscript.echo(InputBox("'.addslashes($text).'"))'); + + $password = shell_exec('cscript //nologo '.escapeshellarg($command)); + + // Remove temporary file. + unlink($vbscript); + } + else + { + $password = shell_exec('/usr/bin/env bash -c \'read -s -p "'.escapeshellcmd($text).'" var && echo $var\''); + } + + Minion_CLI::write(); + + return trim($password); + } + + /** + * Outputs a string to the cli. If you send an array it will implode them + * with a line break. + * + * @param string|array $text the text to output, or array of lines + */ + public static function write($text = '') + { + if (is_array($text)) + { + foreach ($text as $line) + { + Minion_CLI::write($line); + } + } + else + { + fwrite(STDOUT, $text.PHP_EOL); + } + } + + /** + * Outputs a replacable line to the cli. You can continue replacing the + * line until `TRUE` is passed as the second parameter in order to indicate + * you are done modifying the line. + * + * // Sample progress indicator + * Minion_CLI::write_replace('0%'); + * Minion_CLI::write_replace('25%'); + * Minion_CLI::write_replace('50%'); + * Minion_CLI::write_replace('75%'); + * // Done writing this line + * Minion_CLI::write_replace('100%', TRUE); + * + * @param string $text the text to output + * @param boolean $end_line whether the line is done being replaced + */ + public static function write_replace($text = '', $end_line = FALSE) + { + // Append a newline if $end_line is TRUE + $text = $end_line ? $text.PHP_EOL : $text; + fwrite(STDOUT, "\r\033[K".$text); + } + + /** + * Waits a certain number of seconds, optionally showing a wait message and + * waiting for a key press. + * + * @author Fuel Development Team + * @license MIT License + * @copyright 2010 - 2011 Fuel Development Team + * @link http://fuelphp.com + * @param int $seconds number of seconds + * @param bool $countdown show a countdown or not + */ + public static function wait($seconds = 0, $countdown = false) + { + if ($countdown === true) + { + $time = $seconds; + + while ($time > 0) + { + fwrite(STDOUT, $time.'... '); + sleep(1); + $time--; + } + + Minion_CLI::write(); + } + else + { + if ($seconds > 0) + { + sleep($seconds); + } + else + { + Minion_CLI::write(Minion_CLI::$wait_msg); + Minion_CLI::read(); + } + } + } + + /** + * Returns the given text with the correct color codes for a foreground and + * optionally a background color. + * + * @author Fuel Development Team + * @license MIT License + * @copyright 2010 - 2011 Fuel Development Team + * @link http://fuelphp.com + * @param string $text the text to color + * @param atring $foreground the foreground color + * @param string $background the background color + * @return string the color coded string + */ + public static function color($text, $foreground, $background = null) + { + + if (Kohana::$is_windows) + { + return $text; + } + + if (!array_key_exists($foreground, Minion_CLI::$foreground_colors)) + { + throw new Kohana_Exception('Invalid CLI foreground color: '.$foreground); + } + + if ($background !== null and !array_key_exists($background, Minion_CLI::$background_colors)) + { + throw new Kohana_Exception('Invalid CLI background color: '.$background); + } + + $string = "\033[".Minion_CLI::$foreground_colors[$foreground]."m"; + + if ($background !== null) + { + $string .= "\033[".Minion_CLI::$background_colors[$background]."m"; + } + + $string .= $text."\033[0m"; + + return $string; + } + +} diff --git a/includes/kohana/modules/minion/classes/Kohana/Minion/Exception.php b/includes/kohana/modules/minion/classes/Kohana/Minion/Exception.php new file mode 100644 index 0000000..61c6398 --- /dev/null +++ b/includes/kohana/modules/minion/classes/Kohana/Minion/Exception.php @@ -0,0 +1,65 @@ +format_for_cli(); + } + else + { + echo Kohana_Exception::text($e); + } + + $exit_code = $e->getCode(); + + // Never exit "0" after an exception. + if ($exit_code == 0) + { + $exit_code = 1; + } + + exit($exit_code); + } + catch (Exception $e) + { + // Clean the output buffer if one exists + ob_get_level() and ob_clean(); + + // Display the exception text + echo Kohana_Exception::text($e), "\n"; + + // Exit with an error status + exit(1); + } + } + + public function format_for_cli() + { +//@todo Log Kohana Bug, $e not defined. + return Kohana_Exception::text($e); + } +} diff --git a/includes/kohana/modules/minion/classes/Kohana/Minion/Exception/InvalidTask.php b/includes/kohana/modules/minion/classes/Kohana/Minion/Exception/InvalidTask.php new file mode 100644 index 0000000..db8ead4 --- /dev/null +++ b/includes/kohana/modules/minion/classes/Kohana/Minion/Exception/InvalidTask.php @@ -0,0 +1,18 @@ +getMessage().PHP_EOL; + } + +} diff --git a/includes/kohana/modules/minion/classes/Kohana/Minion/Task.php b/includes/kohana/modules/minion/classes/Kohana/Minion/Task.php new file mode 100644 index 0000000..be4fcc0 --- /dev/null +++ b/includes/kohana/modules/minion/classes/Kohana/Minion/Task.php @@ -0,0 +1,364 @@ + $class) + ); + } + + $class = new $class; + + if ( ! $class instanceof Minion_Task) + { + throw new Minion_Exception_InvalidTask( + "Task ':task' is not a valid minion task", + array(':task' => $class) + ); + } + + $class->set_options($options); + + // Show the help page for this task if requested + if (array_key_exists('help', $options)) + { + $class->_method = '_help'; + } + + return $class; + } + + /** + * The list of options this task accepts and their default values. + * + * protected $_options = array( + * 'limit' => 4, + * 'table' => NULL, + * ); + * + * @var array + */ + protected $_options = array(); + + /** + * Populated with the accepted options for this task. + * This array is automatically populated based on $_options. + * + * @var array + */ + protected $_accepted_options = array(); + + protected $_method = '_execute'; + + protected function __construct() + { + // Populate $_accepted_options based on keys from $_options + $this->_accepted_options = array_keys($this->_options); + } + + /** + * The file that get's passes to Validation::errors() when validation fails + * @var string|NULL + */ + protected $_errors_file = 'validation'; + + /** + * Gets the task name for the task + * + * @return string + */ + public function __toString() + { + static $task_name = NULL; + + if ($task_name === NULL) + { + $task_name = Minion_Task::convert_class_to_task($this); + } + + return $task_name; + } + + /** + * Sets options for this task + * + * $param array the array of options to set + * @return this + */ + public function set_options(array $options) + { + foreach ($options as $key => $value) + { + $this->_options[$key] = $value; + } + + return $this; + } + + /** + * Get the options that were passed into this task with their defaults + * + * @return array + */ + public function get_options() + { + return (array) $this->_options; + } + + /** + * Get a set of options that this task can accept + * + * @return array + */ + public function get_accepted_options() + { + return (array) $this->_accepted_options; + } + + /** + * Adds any validation rules/labels for validating _options + * + * public function build_validation(Validation $validation) + * { + * return parent::build_validation($validation) + * ->rule('paramname', 'not_empty'); // Require this param + * } + * + * @param Validation the validation object to add rules to + * + * @return Validation + */ + public function build_validation(Validation $validation) + { + // Add a rule to each key making sure it's in the task + foreach ($validation->as_array() as $key => $value) + { + $validation->rule($key, array($this, 'valid_option'), array(':validation', ':field')); + } + + return $validation; + } + + /** + * Returns $_errors_file + * + * @return string + */ + public function get_errors_file() + { + return $this->_errors_file; + } + + /** + * Execute the task with the specified set of options + * + * @return null + */ + public function execute() + { + $options = $this->get_options(); + + // Validate $options + $validation = Validation::factory($options); + $validation = $this->build_validation($validation); + + if ( $this->_method != '_help' AND ! $validation->check()) + { + echo View::factory('minion/error/validation') + ->set('task', Minion_Task::convert_class_to_task($this)) + ->set('errors', $validation->errors($this->get_errors_file())); + } + else + { + // Finally, run the task + $method = $this->_method; + echo $this->{$method}($options); + } + } + + abstract protected function _execute(array $params); + + /** + * Outputs help for this task + * + * @return null + */ + protected function _help(array $params) + { + $tasks = $this->_compile_task_list(Kohana::list_files('classes/task')); + + $inspector = new ReflectionClass($this); + + list($description, $tags) = $this->_parse_doccomment($inspector->getDocComment()); + + $view = View::factory('minion/help/task') + ->set('description', $description) + ->set('tags', (array) $tags) + ->set('task', Minion_Task::convert_class_to_task($this)); + + echo $view; + } + + + public function valid_option(Validation $validation, $option) + { + if ( ! in_array($option, $this->_accepted_options)) + { + $validation->error($option, 'minion_option'); + } + } + + /** + * Parses a doccomment, extracting both the comment and any tags associated + * + * Based on the code in Kodoc::parse() + * + * @param string The comment to parse + * @return array First element is the comment, second is an array of tags + */ + protected function _parse_doccomment($comment) + { + // Normalize all new lines to \n + $comment = str_replace(array("\r\n", "\n"), "\n", $comment); + + // Remove the phpdoc open/close tags and split + $comment = array_slice(explode("\n", $comment), 1, -1); + + // Tag content + $tags = array(); + + foreach ($comment as $i => $line) + { + // Remove all leading whitespace + $line = preg_replace('/^\s*\* ?/m', '', $line); + + // Search this line for a tag + if (preg_match('/^@(\S+)(?:\s*(.+))?$/', $line, $matches)) + { + // This is a tag line + unset($comment[$i]); + + $name = $matches[1]; + $text = isset($matches[2]) ? $matches[2] : ''; + + $tags[$name] = $text; + } + else + { + $comment[$i] = (string) $line; + } + } + + $comment = trim(implode("\n", $comment)); + + return array($comment, $tags); + } + + /** + * Compiles a list of available tasks from a directory structure + * + * @param array Directory structure of tasks + * @param string prefix + * @return array Compiled tasks + */ + protected function _compile_task_list(array $files, $prefix = '') + { + $output = array(); + + foreach ($files as $file => $path) + { + $file = substr($file, strrpos($file, DIRECTORY_SEPARATOR) + 1); + + if (is_array($path) AND count($path)) + { + $task = $this->_compile_task_list($path, $prefix.$file.Minion_Task::$task_separator); + + if ($task) + { + $output = array_merge($output, $task); + } + } + else + { + $output[] = strtolower($prefix.substr($file, 0, -strlen(EXT))); + } + } + + return $output; + } +} diff --git a/includes/kohana/modules/orm/classes/orm.php b/includes/kohana/modules/minion/classes/Minion/CLI.php similarity index 57% rename from includes/kohana/modules/orm/classes/orm.php rename to includes/kohana/modules/minion/classes/Minion/CLI.php index edf2fc2..d998766 100644 --- a/includes/kohana/modules/orm/classes/orm.php +++ b/includes/kohana/modules/minion/classes/Minion/CLI.php @@ -1,3 +1,3 @@ _compile_task_list(Kohana::list_files('classes/Task')); + + $view = new View('minion/help/list'); + + $view->tasks = $tasks; + + echo $view; + } +} diff --git a/includes/kohana/modules/minion/config/userguide.php b/includes/kohana/modules/minion/config/userguide.php new file mode 100644 index 0000000..1bb7093 --- /dev/null +++ b/includes/kohana/modules/minion/config/userguide.php @@ -0,0 +1,13 @@ + array( + 'minion' => array( + 'enabled' => TRUE, + 'name' => 'Minion', + 'description' => 'Minion is a simple command line task runner', + 'copyright' => '© 2009-2011 Kohana Team', + ) + ) +); \ No newline at end of file diff --git a/includes/kohana/modules/minion/guide/minion/index.md b/includes/kohana/modules/minion/guide/minion/index.md new file mode 100644 index 0000000..5c3be5b --- /dev/null +++ b/includes/kohana/modules/minion/guide/minion/index.md @@ -0,0 +1,3 @@ +# Minion + +Minion is a simple command line task runner. \ No newline at end of file diff --git a/includes/kohana/modules/minion/guide/minion/menu.md b/includes/kohana/modules/minion/guide/minion/menu.md new file mode 100644 index 0000000..9c22809 --- /dev/null +++ b/includes/kohana/modules/minion/guide/minion/menu.md @@ -0,0 +1,3 @@ +## [Minion]() + - [Setup](setup) + - [Writing a Task](tasks) \ No newline at end of file diff --git a/includes/kohana/modules/minion/guide/minion/setup.md b/includes/kohana/modules/minion/guide/minion/setup.md new file mode 100644 index 0000000..11e6256 --- /dev/null +++ b/includes/kohana/modules/minion/guide/minion/setup.md @@ -0,0 +1,32 @@ +# Minion Setup + +To use minion, you'll need to make a small change to your index.php file: + + -/** + - * Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO']. + - * If no source is specified, the URI will be automatically detected. + - */ + -echo Request::factory() + - ->execute() + - ->send_headers(TRUE) + - ->body(); + +if (PHP_SAPI == 'cli') // Try and load minion + +{ + + class_exists('Minion_Task') OR die('minion required!'); + + set_exception_handler(array('Kohana_Minion_Exception_Handler', 'handler')); + + + + Minion_Task::factory(Minion_CLI::options())->execute(); + +} + +else + +{ + + /** + + * Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO']. + + * If no source is specified, the URI will be automatically detected. + + */ + + echo Request::factory() + + ->execute() + + ->send_headers(TRUE) + + ->body(); + +} + +This will short-circuit your index file to intercept any cli calls, and route them to the minion module. \ No newline at end of file diff --git a/includes/kohana/modules/minion/guide/minion/tasks.md b/includes/kohana/modules/minion/guide/minion/tasks.md new file mode 100644 index 0000000..4bda3ef --- /dev/null +++ b/includes/kohana/modules/minion/guide/minion/tasks.md @@ -0,0 +1,71 @@ +# Writing Tasks + +Writing a task in minion is very easy. Simply create a new class called `Task_` and put it inside `classes/task/.php`. + + NULL, + ); + + /** + * This is a demo task + * + * @return null + */ + protected function _execute(array $params) + { + var_dump($params); + echo 'foobar'; + } + } + +You'll notice a few things here: + + - You need a main `_execute()` method. It should take one array parameter. + - This parameter contains any command line options passed to the task. + - For example, if you call the task above with `./minion --task=demo --foo=foobar` then `$params` will contain: `array('foo' => 'foobar', 'bar' => NULL)` + - It needs to have a `protected $_defaults` array. This is a list of parameters you want to accept for this task. Any parameters passed to the task not in this list will be rejected. + +## Namespacing Tasks + +You can "namespace" tasks by placing them all in a subdirectory: `classes/task/database/generate.php`. This task will be named `database:generate` and can be called with this task name. + +# Parameter Validations + +To add validations to your command line options, simply overload the `build_validation()` method in your task: + + public function build_validation(Validation $validation) + { + return parent::build_validation($validation) + ->rule('foo', 'not_empty') // Require this param + ->rule('bar', 'numeric'); // This param should be numeric + } + +These validations will run for every task call unless `--help` is passed to the task. + +# Task Help + +Tasks can have built-in help. Minion will read class docblocks that you specify: + + ':field is not a valid option for this task!', +); \ No newline at end of file diff --git a/includes/kohana/modules/minion/minion b/includes/kohana/modules/minion/minion new file mode 100644 index 0000000..dc4f755 --- /dev/null +++ b/includes/kohana/modules/minion/minion @@ -0,0 +1,4 @@ +#!/usr/bin/env php + /dev/null 2>&1 + then + start_daemon + fi +done \ No newline at end of file diff --git a/includes/kohana/modules/minion/tests/minion/task.php b/includes/kohana/modules/minion/tests/minion/task.php new file mode 100644 index 0000000..e7918a0 --- /dev/null +++ b/includes/kohana/modules/minion/tests/minion/task.php @@ -0,0 +1,70 @@ +assertSame($expected, Minion_Task::convert_task_to_class_name($task_name)); + } + + /** + * Provides test data for test_convert_class_to_task() + * + * @return array + */ + public function provider_convert_class_to_task() + { + return array( + array('db:migrate', 'Task_Db_Migrate'), + ); + } + + /** + * Tests that the task name can be found from a class name / object + * + * @test + * @covers Minion_Task::convert_class_to_task + * @dataProvider provider_convert_class_to_task + * @param string Expected task name + * @param mixed Input class + */ + public function test_convert_class_to_task($expected, $class) + { + $this->assertSame($expected, Minion_Task::convert_class_to_task($class)); + } +} diff --git a/includes/kohana/modules/minion/views/minion/error/validation.php b/includes/kohana/modules/minion/views/minion/error/validation.php new file mode 100644 index 0000000..a65758a --- /dev/null +++ b/includes/kohana/modules/minion/views/minion/error/validation.php @@ -0,0 +1,10 @@ +Parameter Errors: + $error): ?> + - + + +Run + + php index.php --task= --help + +for more help \ No newline at end of file diff --git a/includes/kohana/modules/minion/views/minion/help/error.php b/includes/kohana/modules/minion/views/minion/help/error.php new file mode 100644 index 0000000..504115d --- /dev/null +++ b/includes/kohana/modules/minion/views/minion/help/error.php @@ -0,0 +1,7 @@ + + +Run + + index.php --uri=minion + +for more help diff --git a/includes/kohana/modules/minion/views/minion/help/list.php b/includes/kohana/modules/minion/views/minion/help/list.php new file mode 100644 index 0000000..eeb06ef --- /dev/null +++ b/includes/kohana/modules/minion/views/minion/help/list.php @@ -0,0 +1,17 @@ +Minion is a cli tool for performing tasks + +Usage + + {task} --task=[options] + +Where {task} is one of the following: + + + * + + + +For more information on what a task does and usage details execute + + --task={task} --help + diff --git a/includes/kohana/modules/minion/views/minion/help/task.php b/includes/kohana/modules/minion/views/minion/help/task.php new file mode 100644 index 0000000..8f2fcd7 --- /dev/null +++ b/includes/kohana/modules/minion/views/minion/help/task.php @@ -0,0 +1,17 @@ + +Usage +======= +php minion.php --task= [--option1=value1] [--option2=value2] + +Details +======= + $tag_content): ?> +: + + + +Description +=========== + + + diff --git a/includes/kohana/modules/orm/auth-schema-mysql.sql b/includes/kohana/modules/orm/auth-schema-mysql.sql index ec4a4a8..61a803a 100644 --- a/includes/kohana/modules/orm/auth-schema-mysql.sql +++ b/includes/kohana/modules/orm/auth-schema-mysql.sql @@ -18,7 +18,7 @@ CREATE TABLE IF NOT EXISTS `roles_users` ( CREATE TABLE IF NOT EXISTS `users` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, - `email` varchar(127) NOT NULL, + `email` varchar(254) NOT NULL, `username` varchar(32) NOT NULL DEFAULT '', `password` varchar(64) NOT NULL, `logins` int(10) UNSIGNED NOT NULL DEFAULT '0', @@ -33,12 +33,12 @@ CREATE TABLE IF NOT EXISTS `user_tokens` ( `user_id` int(11) UNSIGNED NOT NULL, `user_agent` varchar(40) NOT NULL, `token` varchar(40) NOT NULL, - `type` varchar(100) NOT NULL, `created` int(10) UNSIGNED NOT NULL, `expires` int(10) UNSIGNED NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_token` (`token`), - KEY `fk_user_id` (`user_id`) + KEY `fk_user_id` (`user_id`), + KEY `expires` (`expires`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE `roles_users` diff --git a/includes/kohana/modules/orm/auth-schema-postgresql.sql b/includes/kohana/modules/orm/auth-schema-postgresql.sql index 72f6424..ce82ed1 100644 --- a/includes/kohana/modules/orm/auth-schema-postgresql.sql +++ b/includes/kohana/modules/orm/auth-schema-postgresql.sql @@ -16,7 +16,7 @@ CREATE TABLE roles_users CREATE TABLE users ( id serial, - email varchar(318) NOT NULL, + email varchar(254) NOT NULL, username varchar(32) NOT NULL, "password" varchar(64) NOT NULL, logins integer NOT NULL DEFAULT 0, diff --git a/includes/kohana/modules/orm/classes/Auth/ORM.php b/includes/kohana/modules/orm/classes/Auth/ORM.php new file mode 100644 index 0000000..590bef1 --- /dev/null +++ b/includes/kohana/modules/orm/classes/Auth/ORM.php @@ -0,0 +1,3 @@ +where('name', 'IN', $role) ->find_all() ->as_array(NULL, 'id'); @@ -46,7 +46,7 @@ class Kohana_Auth_ORM extends Auth { if ( ! is_object($role)) { // Load the role - $roles = ORM::factory('role', array('name' => $role)); + $roles = ORM::factory('Role', array('name' => $role)); if ( ! $roles->loaded()) return FALSE; @@ -60,9 +60,9 @@ class Kohana_Auth_ORM extends Auth { /** * Logs a user in. * - * @param string username - * @param string password - * @param boolean enable autologin + * @param string $username + * @param string $password + * @param boolean $remember enable autologin * @return boolean */ protected function _login($user, $password, $remember) @@ -72,24 +72,30 @@ class Kohana_Auth_ORM extends Auth { $username = $user; // Load the user - $user = ORM::factory('user'); + $user = ORM::factory('User'); $user->where($user->unique_key($username), '=', $username)->find(); } + if (is_string($password)) + { + // Create a hashed password + $password = $this->hash($password); + } + // If the passwords match, perform a login - if ($user->has('roles', ORM::factory('role', array('name' => 'login'))) AND $user->password === $password) + if ($user->has('roles', ORM::factory('Role', array('name' => 'login'))) AND $user->password === $password) { if ($remember === TRUE) { // Token data $data = array( - 'user_id' => $user->id, + 'user_id' => $user->pk(), 'expires' => time() + $this->_config['lifetime'], 'user_agent' => sha1(Request::$user_agent), ); // Create a new autologin token - $token = ORM::factory('user_token') + $token = ORM::factory('User_Token') ->values($data) ->create(); @@ -110,8 +116,8 @@ class Kohana_Auth_ORM extends Auth { /** * Forces a user to be logged in, without specifying a password. * - * @param mixed username string, or user ORM object - * @param boolean mark the session as forced + * @param mixed $user username string, or user ORM object + * @param boolean $mark_session_as_forced mark the session as forced * @return boolean */ public function force_login($user, $mark_session_as_forced = FALSE) @@ -121,7 +127,7 @@ class Kohana_Auth_ORM extends Auth { $username = $user; // Load the user - $user = ORM::factory('user'); + $user = ORM::factory('User'); $user->where($user->unique_key($username), '=', $username)->find(); } @@ -145,7 +151,7 @@ class Kohana_Auth_ORM extends Auth { if ($token = Cookie::get('authautologin')) { // Load the token and user - $token = ORM::factory('user_token', array('token' => $token)); + $token = ORM::factory('User_Token', array('token' => $token)); if ($token->loaded() AND $token->user->loaded()) { @@ -174,18 +180,20 @@ class Kohana_Auth_ORM extends Auth { /** * Gets the currently logged in user from the session (with auto_login check). - * Returns FALSE if no user is currently logged in. + * Returns $default if no user is currently logged in. * + * @param mixed $default to return in case user isn't logged in * @return mixed */ public function get_user($default = NULL) { $user = parent::get_user($default); - if ( ! $user) + if ($user === $default) { // check for "remembered" login - $user = $this->auto_login(); + if (($user = $this->auto_login()) === FALSE) + return $default; } return $user; @@ -194,8 +202,8 @@ class Kohana_Auth_ORM extends Auth { /** * Log a user out and remove any autologin cookies. * - * @param boolean completely destroy the session - * @param boolean remove all tokens for user + * @param boolean $destroy completely destroy the session + * @param boolean $logout_all remove all tokens for user * @return boolean */ public function logout($destroy = FALSE, $logout_all = FALSE) @@ -209,11 +217,17 @@ class Kohana_Auth_ORM extends Auth { Cookie::delete('authautologin'); // Clear the autologin token from the database - $token = ORM::factory('user_token', array('token' => $token)); + $token = ORM::factory('User_Token', array('token' => $token)); if ($token->loaded() AND $logout_all) { - ORM::factory('user_token')->where('user_id', '=', $token->user_id)->delete_all(); + // Delete all user tokens. This isn't the most elegant solution but does the job + $tokens = ORM::factory('User_Token')->where('user_id','=',$token->user_id)->find_all(); + + foreach ($tokens as $_token) + { + $_token->delete(); + } } elseif ($token->loaded()) { @@ -227,7 +241,7 @@ class Kohana_Auth_ORM extends Auth { /** * Get the stored password for a username. * - * @param mixed username string, or user ORM object + * @param mixed $user username string, or user ORM object * @return string */ public function password($user) @@ -237,7 +251,7 @@ class Kohana_Auth_ORM extends Auth { $username = $user; // Load the user - $user = ORM::factory('user'); + $user = ORM::factory('User'); $user->where($user->unique_key($username), '=', $username)->find(); } @@ -248,7 +262,7 @@ class Kohana_Auth_ORM extends Auth { * Complete the login for a user by incrementing the logins and setting * session data: user_id, username, roles. * - * @param object user ORM object + * @param object $user user ORM object * @return void */ protected function complete_login($user) @@ -274,4 +288,4 @@ class Kohana_Auth_ORM extends Auth { return ($this->hash($password) === $user->password); } -} // End Auth ORM \ No newline at end of file +} // End Auth ORM diff --git a/includes/kohana/modules/orm/classes/kohana/orm.php b/includes/kohana/modules/orm/classes/Kohana/ORM.php similarity index 58% rename from includes/kohana/modules/orm/classes/kohana/orm.php rename to includes/kohana/modules/orm/classes/Kohana/ORM.php index b5ef1e2..666af8a 100644 --- a/includes/kohana/modules/orm/classes/kohana/orm.php +++ b/includes/kohana/modules/orm/classes/Kohana/ORM.php @@ -1,4 +1,4 @@ -where(($this->_disable_join_table_name ? '' : $this->_table_name.'.').$this->_primary_key, '=', $id)->find(); + $this->where(($this->_disable_join_table_name ? '' : $this->_object_name.'.').$this->_primary_key, '=', $id)->find(); } } elseif ( ! empty($this->_cast_data)) @@ -345,53 +299,99 @@ class Kohana_ORM extends Model implements serializable { { // Set the object name and plural name $this->_object_name = strtolower(substr(get_class($this), 6)); - $this->_object_plural = Inflector::plural($this->_object_name); - - if ( ! is_object($this->_db)) + + // Check if this model has already been initialized + if ( ! $init = Arr::get(ORM::$_init_cache, $this->_object_name, FALSE)) { - // Get database instance - $this->_db = Database::instance($this->_db_group); - } - - 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) + $init = array( + '_belongs_to' => array(), + '_has_one' => array(), + '_has_many' => array(), + ); + + // Set the object plural name if none predefined + if ( ! isset($this->_object_plural)) { - // Make the table name plural - $this->_table_name = Inflector::plural($this->_table_name); + $init['_object_plural'] = Inflector::plural($this->_object_name); } - } - foreach ($this->_belongs_to as $alias => $details) + if ( ! $this->_errors_filename) + { + $init['_errors_filename'] = $this->_object_name; + } + + if ( ! is_object($this->_db)) + { + // Get database instance + $init['_db'] = Database::instance($this->_db_group); + } + + if (empty($this->_table_name)) + { + // Table name is the same as the object name + $init['_table_name'] = $this->_object_name; + + if ($this->_table_names_plural === TRUE) + { + // Make the table name plural + $init['_table_name'] = Arr::get($init, '_object_plural', $this->_object_plural); + } + } + + $defaults = array(); + + foreach ($this->_belongs_to as $alias => $details) + { + if ( ! isset($details['model'])) + { + $defaults['model'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $alias))); + } + + $defaults['foreign_key'] = $alias.$this->_foreign_key_suffix; + + $init['_belongs_to'][$alias] = array_merge($defaults, $details); + } + + foreach ($this->_has_one as $alias => $details) + { + if ( ! isset($details['model'])) + { + $defaults['model'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', $alias))); + } + + $defaults['foreign_key'] = $this->_object_name.$this->_foreign_key_suffix; + $defaults['far_key'] = Inflector::singular($alias).$this->_foreign_key_suffix; + + $init['_has_one'][$alias] = array_merge($defaults, $details); + } + + foreach ($this->_has_many as $alias => $details) + { + if ( ! isset($details['model'])) + { + $defaults['model'] = str_replace(' ', '_', ucwords(str_replace('_', ' ', ($this->_model_names_plural ? Inflector::singular($alias) : $alias)))); + } + + $defaults['foreign_key'] = $this->_object_name.$this->_foreign_key_suffix; + $defaults['through'] = NULL; + + if ( ! isset($details['far_key'])) + { + $defaults['far_key'] = Inflector::singular($alias).$this->_foreign_key_suffix; + } + + $init['_has_many'][$alias] = array_merge($defaults, $details); + } + + ORM::$_init_cache[$this->_object_name] = $init; + } + + // Assign initialized properties to the current object + foreach ($init as $property => $value) { - $defaults['model'] = $alias; - $defaults['foreign_key'] = $alias.$this->_foreign_key_suffix; - - $this->_belongs_to[$alias] = array_merge($defaults, $details); + $this->{$property} = $value; } - - foreach ($this->_has_one as $alias => $details) - { - $defaults['model'] = $alias; - $defaults['foreign_key'] = $this->_object_name.$this->_foreign_key_suffix; - $defaults['far_key'] = Inflector::singular($alias).$this->_foreign_key_suffix; - - $this->_has_one[$alias] = array_merge($defaults, $details); - } - - foreach ($this->_has_many as $alias => $details) - { - $defaults['model'] = $this->_model_names_plural ? Inflector::singular($alias) : $alias; - $defaults['foreign_key'] = $this->_object_name.$this->_foreign_key_suffix; - $defaults['through'] = NULL; - $defaults['far_key'] = Inflector::singular($alias).$this->_foreign_key_suffix; - - $this->_has_many[$alias] = array_merge($defaults, $details); - } - + // Load column information $this->reload_columns(); @@ -408,7 +408,9 @@ class Kohana_ORM extends Model implements serializable { { // Build the validation object with its rules $this->_validation = Validation::factory($this->_object) - ->bind(':model', $this); + ->bind(':model', $this) + ->bind(':original_values', $this->_original_values) + ->bind(':changed', $this->_changed); foreach ($this->rules() as $field => $rules) { @@ -446,7 +448,7 @@ class Kohana_ORM extends Model implements serializable { else { // Grab column information from database - $this->_table_columns = $this->list_columns(TRUE); + $this->_table_columns = $this->list_columns(); // Load column cache ORM::$_column_cache[$this->_object_name] = $this->_table_columns; @@ -468,13 +470,16 @@ class Kohana_ORM extends Model implements serializable { $values = array_combine(array_keys($this->_table_columns), array_fill(0, count($this->_table_columns), NULL)); // Replace the object and reset the object status - $this->_object = $this->_changed = $this->_related = array(); + $this->_object = $this->_changed = $this->_related = $this->_original_values = array(); // Replace the current object with an empty one $this->_load_values($values); // Reset primary key $this->_primary_key_value = NULL; + + // Reset the loaded state + $this->_loaded = FALSE; $this->reset(); @@ -492,12 +497,12 @@ class Kohana_ORM extends Model implements serializable { $primary_key = $this->pk(); // Replace the object and reset the object status - $this->_object = $this->_changed = $this->_related = array(); + $this->_object = $this->_changed = $this->_related = $this->_original_values = array(); // Only reload the object if we have one to reload if ($this->_loaded) return $this->clear() - ->where($this->_table_name.'.'.$this->_primary_key, '=', $primary_key) + ->where($this->_object_name.'.'.$this->_primary_key, '=', $primary_key) ->find(); else return $this->clear(); @@ -543,12 +548,12 @@ class Kohana_ORM extends Model implements serializable { * Allows serialization of only the object data and state, to prevent * "stale" objects being unserialized, which also requires less memory. * - * @return array + * @return string */ public function serialize() { // Store only information about the object - foreach (array('_primary_key_value', '_object', '_changed', '_loaded', '_saved', '_sorting') as $var) + foreach (array('_primary_key_value', '_object', '_changed', '_loaded', '_saved', '_sorting', '_original_values') as $var) { $data[$var] = $this->{$var}; } @@ -556,6 +561,20 @@ class Kohana_ORM extends Model implements serializable { return serialize($data); } + /** + * Check whether the model data has been modified. + * If $field is specified, checks whether that field was modified. + * + * @param string $field field to check for changes + * @return bool Whether or not the field has changed + */ + public function changed($field = NULL) + { + return ($field === NULL) + ? $this->_changed + : Arr::get($this->_changed, $field); + } + /** * Prepares the database connection and reloads the object. * @@ -579,56 +598,33 @@ class Kohana_ORM extends Model implements serializable { } } - /** - * 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 Method name - * @param array $args Method arguments - * @return mixed - */ - public function __call($method, array $args) - { - if (in_array($method, ORM::$_properties)) - { - if ($method === 'validation') - { - if ( ! isset($this->_validation)) - { - // Initialize the validation object - $this->_validation(); - } - } - - // Return the property - return $this->{'_'.$method}; - } - elseif (in_array($method, ORM::$_db_methods)) - { - // Add pending database call which is executed after query type is determined - $this->_db_pending[] = array('name' => $method, 'args' => $args); - - return $this; - } - else - { - throw new Kohana_Exception('Invalid method :method called in :class', - array(':method' => $method, ':class' => get_class($this))); - } - } - /** * Handles retrieval of all model values, relationships, and metadata. + * [!!] This should not be overridden. * * @param string $column Column name * @return mixed */ public function __get($column) + { + return $this->get($column); + } + + /** + * Handles getting of column + * Override this method to add custom get behavior + * + * @param string $column Column name + * @throws Kohana_Exception + * @return mixed + */ + public function get($column) { if (array_key_exists($column, $this->_object)) { - return $this->_object[$column]; + return (in_array($column, $this->_serialize_columns)) + ? $this->_unserialize_value($this->_object[$column]) + : $this->_object[$column]; } elseif (isset($this->_related[$column])) { @@ -640,10 +636,16 @@ class Kohana_ORM extends Model implements serializable { $model = $this->_related($column); // Use this model's column and foreign model's primary key - $col = ($this->_disable_join_table_name ? '' : $model->_table_name.'.').$model->_primary_key; + $col = ($this->_disable_join_table_name ? '' : $model->_object_name.'.').$model->_primary_key; $val = $this->_object[$this->_belongs_to[$column]['foreign_key']]; - $model->where($col, '=', $val)->find(); + // Make sure we don't run WHERE "AUTO_INCREMENT column" = NULL queries. This would + // return the last inserted record instead of an empty result. + // See: http://mysql.localhost.net.ar/doc/refman/5.1/en/server-session-variables.html#sysvar_sql_auto_is_null + if ($val !== NULL) + { + $model->where($col, '=', $val)->find(); + } return $this->_related[$column] = $model; } @@ -654,7 +656,7 @@ class Kohana_ORM extends Model implements serializable { if (! is_array($this->_has_one[$column]['foreign_key'])) { // Use this model's primary key value and foreign model's column - $col = ($this->_disable_join_table_name ? '' : $model->_table_name.'.').$this->_has_one[$column]['foreign_key']; + $col = ($this->_disable_join_table_name ? '' : $model->_object_name.'.').$this->_has_one[$column]['foreign_key']; $val = $this->_object[$this->_has_one[$column]['far_key']]; } else @@ -662,7 +664,7 @@ class Kohana_ORM extends Model implements serializable { foreach ($this->_has_one[$column]['foreign_key'] as $fk) { // Simple has_many relationship, search where target model's foreign key is this model's primary key - $col = ($this->_disable_join_table_name ? '' : $model->_table_name.'.').$fk; + $col = ($this->_disable_join_table_name ? '' : $model->_object_name.'.').$fk; $val = $this->_object[$fk]; $model = $model->where($col, '=', $val); @@ -686,9 +688,10 @@ class Kohana_ORM extends Model implements serializable { // Join on through model's target foreign key (far_key) and target model's primary key $join_col1 = ($this->_disable_join_table_name ? '' : $through.'.').$this->_has_many[$column]['far_key']; - $join_col2 = ($this->_disable_join_table_name ? '' : $model->_table_name.'.').$model->_primary_key; + $join_col2 = ($this->_disable_join_table_name ? '' : $model->_object_name.'.').$model->_primary_key; - $model->join($through)->on($join_col1, '=', $join_col2); + $model->join($through)->on($join_col1, '=', $join_col2) + ->on(($this->_disable_join_table_name ? '' : $through.'.').'site_id', '=', ($this->_disable_join_table_name ? '' : $model->_object_name.'.').'site_id'); // Through table's source foreign key (foreign_key) should be this model's primary key $col = ($this->_disable_join_table_name ? '' : $through.'.').$this->_has_many[$column]['foreign_key']; @@ -697,7 +700,7 @@ class Kohana_ORM extends Model implements serializable { else { // Simple has_many relationship, search where target model's foreign key is this model's primary key - $col = ($this->_disable_join_table_name ? '' : $model->_table_name.'.').$this->_has_many[$column]['foreign_key']; + $col = ($this->_disable_join_table_name ? '' : $model->_object_name.'.').$this->_has_many[$column]['foreign_key']; $val = $this->_object[$this->_has_many[$column]['far_key']]; } @@ -714,7 +717,7 @@ class Kohana_ORM extends Model implements serializable { else { // Simple has_many relationship, search where target model's foreign key is this model's primary key - $col = ($this->_disable_join_table_name ? '' : $model->_table_name.'.').$fk; + $col = ($this->_disable_join_table_name ? '' : $model->_object_name.'.').$fk; $val = $this->_object[$mk]; } @@ -732,35 +735,42 @@ class Kohana_ORM extends Model implements serializable { } /** - * Base set method - this should not be overridden. + * Base set method. + * [!!] This should not be overridden. * * @param string $column Column name * @param mixed $value Column value * @return void */ public function __set($column, $value) + { + $this->set($column, $value); + } + + /** + * Handles setting of columns + * Override this method to add custom set behavior + * + * @param string $column Column name + * @param mixed $value Column value + * @throws Kohana_Exception + * @return ORM + */ + public function set($column, $value) { if ( ! isset($this->_object_name)) { // Object not yet constructed, so we're loading data from a database call cast $this->_cast_data[$column] = $value; + + return $this; } - else + + if (in_array($column, $this->_serialize_columns)) { - // Set the model's column to given value - $this->set($column, $value); + $value = $this->_serialize_value($value); } - } - /** - * Handles setting of column - * - * @param string $column Column name - * @param mixed $value Column value - * @return void - */ - public function set($column, $value) - { if (array_key_exists($column, $this->_object)) { // Filter the data @@ -784,7 +794,9 @@ class Kohana_ORM extends Model implements serializable { $this->_related[$column] = $value; // Update the foreign key of this model - $this->_object[$this->_belongs_to[$column]['foreign_key']] = $value->pk(); + $this->_object[$this->_belongs_to[$column]['foreign_key']] = ($value instanceof ORM) + ? $value->pk() + : NULL; $this->_changed[$column] = $this->_belongs_to[$column]['foreign_key']; } @@ -871,7 +883,7 @@ class Kohana_ORM extends Model implements serializable { * can be nested using 'object1:object2' syntax * * @param string $target_path Target model to bind to - * @return void + * @return ORM */ public function with($target_path) { @@ -907,7 +919,7 @@ class Kohana_ORM extends Model implements serializable { if (empty($parent_path)) { // Use this table name itself for the parent path - $parent_path = $this->_table_name; + $parent_path = $this->_object_name; } else { @@ -965,9 +977,10 @@ class Kohana_ORM extends Model implements serializable { $this->_db_builder = DB::select(); break; case Database::UPDATE: - $this->_db_builder = DB::update($this->_table_name); + $this->_db_builder = DB::update(array($this->_table_name, $this->_object_name)); break; case Database::DELETE: + // Cannot use an alias for DELETE queries $this->_db_builder = DB::delete($this->_table_name); } @@ -989,6 +1002,7 @@ class Kohana_ORM extends Model implements serializable { * Finds and loads a single database row into the object. * * @chainable + * @throws Kohana_Exception * @return ORM */ public function find() @@ -1013,6 +1027,7 @@ class Kohana_ORM extends Model implements serializable { /** * Finds multiple database rows and returns an iterator of the rows found. * + * @throws Kohana_Exception * @return Database_Result */ public function find_all() @@ -1034,6 +1049,24 @@ class Kohana_ORM extends Model implements serializable { return $this->_load_result(TRUE); } + /** + * Returns an array of columns to include in the select query. This method + * can be overridden to change the default select behavior. + * + * @return array Columns to select + */ + protected function _build_select() + { + $columns = array(); + + foreach ($this->_table_columns as $column => $_) + { + $columns[] = array($this->_object_name.'.'.$column, $column); + } + + return $columns; + } + /** * Loads a database result, either as a new record for this model, or as * an iterator for multiple rows. @@ -1044,7 +1077,7 @@ class Kohana_ORM extends Model implements serializable { */ protected function _load_result($multiple = FALSE) { - $this->_db_builder->from($this->_table_name); + $this->_db_builder->from(array($this->_table_name, $this->_object_name)); if ($multiple === FALSE AND ! $this->_disable_limit) { @@ -1054,7 +1087,7 @@ class Kohana_ORM extends Model implements serializable { // Select all columns by default if (! $this->_disable_wild_select) - $this->_db_builder->select($this->_table_name.'.*'); + $this->_db_builder->select_array($this->_build_select()); if ( ! isset($this->_db_applied['order_by']) AND ! empty($this->_sorting)) { @@ -1063,7 +1096,7 @@ class Kohana_ORM extends Model implements serializable { if (strpos($column, '.') === FALSE) { // Sorting column for use in JOINs - $column = ($this->_disable_join_table_name ? '' : $this->_table_name.'.').$column; + $column = ($this->_disable_join_table_name ? '' : $this->_object_name.'.').$column; } $this->_db_builder->order_by($column, $direction); @@ -1114,16 +1147,16 @@ class Kohana_ORM extends Model implements serializable { { if ($values[$this->_primary_key] !== NULL) { - // Flag as loaded, saved, and valid - $this->_loaded = $this->_saved = $this->_valid = TRUE; + // Flag as loaded and valid + $this->_loaded = $this->_valid = TRUE; // Store primary key $this->_primary_key_value = $values[$this->_primary_key]; } else { - // Not loaded, saved, or valid - $this->_loaded = $this->_saved = $this->_valid = FALSE; + // Not loaded or valid + $this->_loaded = $this->_valid = FALSE; } } @@ -1155,6 +1188,12 @@ class Kohana_ORM extends Model implements serializable { } } + if ($this->_loaded) + { + // Store the object in its original state + $this->_original_values = $this->_object; + } + return $this; } @@ -1265,6 +1304,7 @@ class Kohana_ORM extends Model implements serializable { * Validates the current model's data * * @param Validation $extra_validation Validation object + * @throws ORM_Validation_Exception * @return ORM */ public function check(Validation $extra_validation = NULL) @@ -1279,7 +1319,7 @@ class Kohana_ORM extends Model implements serializable { if (($this->_valid = $array->check()) === FALSE OR $extra_errors) { - $exception = new ORM_Validation_Exception($this->_object_name, $array); + $exception = new ORM_Validation_Exception($this->errors_filename(), $array); if ($extra_errors) { @@ -1295,6 +1335,7 @@ class Kohana_ORM extends Model implements serializable { /** * Insert a new object to the database * @param Validation $validation Validation object + * @throws Kohana_Exception * @return ORM */ public function create(Validation $validation = NULL) @@ -1303,7 +1344,7 @@ class Kohana_ORM extends Model implements serializable { throw new Kohana_Exception('Cannot create :model model because it is already loaded.', array(':model' => $this->_object_name)); // Require model validation before saving - if ( ! $this->_valid) + if ( ! $this->_valid OR $validation) { $this->check($validation); } @@ -1334,12 +1375,17 @@ class Kohana_ORM extends Model implements serializable { // Load the insert id as the primary key if it was left out $this->_object[$this->_primary_key] = $this->_primary_key_value = $result[0]; } + else + { + $this->_primary_key_value = $this->_object[$this->_primary_key]; + } // Object is now loaded and saved $this->_loaded = $this->_saved = TRUE; // All changes have been saved $this->_changed = array(); + $this->_original_values = $this->_object; return $this; } @@ -1349,6 +1395,7 @@ class Kohana_ORM extends Model implements serializable { * * @chainable * @param Validation $validation Validation object + * @throws Kohana_Exception * @return ORM */ public function update(Validation $validation = NULL) @@ -1356,18 +1403,18 @@ class Kohana_ORM extends Model implements serializable { if ( ! $this->_loaded) throw new Kohana_Exception('Cannot update :model model because it is not loaded.', array(':model' => $this->_object_name)); + // Run validation if the model isn't valid or we have additional validation rules. + if ( ! $this->_valid OR $validation) + { + $this->check($validation); + } + if (empty($this->_changed)) { // Nothing to update return $this; } - // Require model validation before saving - if ( ! $this->_valid) - { - $this->check($validation); - } - $data = array(); foreach ($this->_changed as $column) { @@ -1404,6 +1451,7 @@ class Kohana_ORM extends Model implements serializable { // All changes have been saved $this->_changed = array(); + $this->_original_values = $this->_object; return $this; } @@ -1421,9 +1469,10 @@ class Kohana_ORM extends Model implements serializable { } /** - * Deletes a single record or multiple records, ignoring relationships. + * Deletes a single record while ignoring relationships. * * @chainable + * @throws Kohana_Exception * @return ORM */ public function delete() @@ -1444,7 +1493,9 @@ class Kohana_ORM extends Model implements serializable { /** * Tests if this object has a relationship to a different model, - * or an array of different models. + * or an array of different models. When providing far keys, the number + * of relations must equal the number of keys. + * * * // Check if $model has the login role * $model->has('roles', ORM::factory('role', array('name' => 'login'))); @@ -1452,13 +1503,77 @@ class Kohana_ORM extends Model implements serializable { * $model->has('roles', 5); * // Check for all of the following roles * $model->has('roles', array(1, 2, 3, 4)); - + * // Check if $model has any roles + * $model->has('roles') + * * @param string $alias Alias of the has_many "through" relationship * @param mixed $far_keys Related model, primary key, or an array of primary keys - * @return Database_Result + * @return boolean */ - public function has($alias, $far_keys) + public function has($alias, $far_keys = NULL) { + $count = $this->count_relations($alias, $far_keys); + if ($far_keys === NULL) + { + return (bool) $count; + } + else + { + return $count === count($far_keys); + } + + } + + /** + * Tests if this object has a relationship to a different model, + * or an array of different models. When providing far keys, this function + * only checks that at least one of the relationships is satisfied. + * + * // Check if $model has the login role + * $model->has('roles', ORM::factory('role', array('name' => 'login'))); + * // Check for the login role if you know the roles.id is 5 + * $model->has('roles', 5); + * // Check for any of the following roles + * $model->has('roles', array(1, 2, 3, 4)); + * // Check if $model has any roles + * $model->has('roles') + * + * @param string $alias Alias of the has_many "through" relationship + * @param mixed $far_keys Related model, primary key, or an array of primary keys + * @return boolean + */ + public function has_any($alias, $far_keys = NULL) + { + return (bool) $this->count_relations($alias, $far_keys); + } + + /** + * Returns the number of relationships + * + * // Counts the number of times the login role is attached to $model + * $model->has('roles', ORM::factory('role', array('name' => 'login'))); + * // Counts the number of times role 5 is attached to $model + * $model->has('roles', 5); + * // Counts the number of times any of roles 1, 2, 3, or 4 are attached to + * // $model + * $model->has('roles', array(1, 2, 3, 4)); + * // Counts the number roles attached to $model + * $model->has('roles') + * + * @param string $alias Alias of the has_many "through" relationship + * @param mixed $far_keys Related model, primary key, or an array of primary keys + * @return integer + */ + public function count_relations($alias, $far_keys = NULL) + { + if ($far_keys === NULL) + { + return (int) DB::select(array(DB::expr('COUNT(*)'), 'records_found')) + ->from($this->_has_many[$alias]['through']) + ->where($this->_has_many[$alias]['foreign_key'], '=', $this->pk()) + ->execute($this->_db)->get('records_found'); + } + $far_keys = ($far_keys instanceof ORM) ? $far_keys->pk() : $far_keys; // We need an array to simplify the logic @@ -1466,16 +1581,16 @@ class Kohana_ORM extends Model implements serializable { // Nothing to check if the model isn't loaded or we don't have any far_keys if ( ! $far_keys OR ! $this->_loaded) - return FALSE; + return 0; - $count = (int) DB::select(array('COUNT("*")', 'records_found')) + $count = (int) DB::select(array(DB::expr('COUNT(*)'), 'records_found')) ->from($this->_has_many[$alias]['through']) ->where($this->_has_many[$alias]['foreign_key'], '=', $this->pk()) ->where($this->_has_many[$alias]['far_key'], 'IN', $far_keys) ->execute($this->_db)->get('records_found'); // Rows found need to match the rows searched - return $count === count($far_keys); + return (int) $count; } /** @@ -1575,8 +1690,8 @@ class Kohana_ORM extends Model implements serializable { $this->_build(Database::SELECT); - $records = $this->_db_builder->from($this->_table_name) - ->select(array('COUNT("*")', 'records_found')) + $records = $this->_db_builder->from(array($this->_table_name, $this->_object_name)) + ->select(array(DB::expr('COUNT(*)'), 'records_found')) ->execute($this->_db) ->get('records_found'); @@ -1600,23 +1715,6 @@ class Kohana_ORM extends Model implements serializable { return $this->_db->list_columns($this->_table_name); } - /** - * Proxy method to Database field_data. - * - * @chainable - * @param string $sql SQL query to clear - * @return ORM - */ - public function clear_cache($sql = NULL) - { - // Proxy to database - $this->_db->clear_cache($sql); - - ORM::$_column_cache = array(); - - return $this; - } - /** * Returns an ORM model for the given one-one related alias * @@ -1685,4 +1783,622 @@ class Kohana_ORM extends Model implements serializable { return $this; } + + protected function _serialize_value($value) + { + return json_encode($value); + } + + protected function _unserialize_value($value) + { + return json_decode($value, TRUE); + } + + public function object_name() + { + return $this->_object_name; + } + + public function object_plural() + { + return $this->_object_plural; + } + + public function loaded() + { + return $this->_loaded; + } + + public function saved() + { + return $this->_saved; + } + + public function primary_key() + { + return $this->_primary_key; + } + + public function table_name() + { + return $this->_table_name; + } + + public function table_columns() + { + return $this->_table_columns; + } + + public function has_one() + { + return $this->_has_one; + } + + public function belongs_to() + { + return $this->_belongs_to; + } + + public function has_many() + { + return $this->_has_many; + } + + public function load_with() + { + return $this->_load_with; + } + + public function original_values() + { + return $this->_original_values; + } + + public function created_column() + { + return $this->_created_column; + } + + public function updated_column() + { + return $this->_updated_column; + } + + public function validation() + { + if ( ! isset($this->_validation)) + { + // Initialize the validation object + $this->_validation(); + } + + return $this->_validation; + } + + public function object() + { + return $this->_object; + } + + public function errors_filename() + { + return $this->_errors_filename; + } + + /** + * Alias of and_where() + * + * @param mixed $column column name or array($column, $alias) or object + * @param string $op logic operator + * @param mixed $value column value + * @return $this + */ + public function where($column, $op, $value) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'where', + 'args' => array($column, $op, $value), + ); + + return $this; + } + + /** + * Creates a new "AND WHERE" condition for the query. + * + * @param mixed $column column name or array($column, $alias) or object + * @param string $op logic operator + * @param mixed $value column value + * @return $this + */ + public function and_where($column, $op, $value) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'and_where', + 'args' => array($column, $op, $value), + ); + + return $this; + } + + /** + * Creates a new "OR WHERE" condition for the query. + * + * @param mixed $column column name or array($column, $alias) or object + * @param string $op logic operator + * @param mixed $value column value + * @return $this + */ + public function or_where($column, $op, $value) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'or_where', + 'args' => array($column, $op, $value), + ); + + return $this; + } + + /** + * Alias of and_where_open() + * + * @return $this + */ + public function where_open() + { + return $this->and_where_open(); + } + + /** + * Opens a new "AND WHERE (...)" grouping. + * + * @return $this + */ + public function and_where_open() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'and_where_open', + 'args' => array(), + ); + + return $this; + } + + /** + * Opens a new "OR WHERE (...)" grouping. + * + * @return $this + */ + public function or_where_open() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'or_where_open', + 'args' => array(), + ); + + return $this; + } + + /** + * Closes an open "AND WHERE (...)" grouping. + * + * @return $this + */ + public function where_close() + { + return $this->and_where_close(); + } + + /** + * Closes an open "AND WHERE (...)" grouping. + * + * @return $this + */ + public function and_where_close() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'and_where_close', + 'args' => array(), + ); + + return $this; + } + + /** + * Closes an open "OR WHERE (...)" grouping. + * + * @return $this + */ + public function or_where_close() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'or_where_close', + 'args' => array(), + ); + + return $this; + } + + /** + * Applies sorting with "ORDER BY ..." + * + * @param mixed $column column name or array($column, $alias) or object + * @param string $direction direction of sorting + * @return $this + */ + public function order_by($column, $direction = NULL) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'order_by', + 'args' => array($column, $direction), + ); + + return $this; + } + + /** + * Return up to "LIMIT ..." results + * + * @param integer $number maximum results to return + * @return $this + */ + public function limit($number) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'limit', + 'args' => array($number), + ); + + return $this; + } + + /** + * Enables or disables selecting only unique columns using "SELECT DISTINCT" + * + * @param boolean $value enable or disable distinct columns + * @return $this + */ + public function distinct($value) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'distinct', + 'args' => array($value), + ); + + return $this; + } + + /** + * Choose the columns to select from. + * + * @param mixed $columns column name or array($column, $alias) or object + * @param ... + * @return $this + */ + public function select($columns = NULL) + { + $columns = func_get_args(); + + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'select', + 'args' => $columns, + ); + + return $this; + } + + /** + * Choose the tables to select "FROM ..." + * + * @param mixed $tables table name or array($table, $alias) or object + * @param ... + * @return $this + */ + public function from($tables) + { + $tables = func_get_args(); + + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'from', + 'args' => $tables, + ); + + return $this; + } + + /** + * Adds addition tables to "JOIN ...". + * + * @param mixed $table column name or array($column, $alias) or object + * @param string $type join type (LEFT, RIGHT, INNER, etc) + * @return $this + */ + public function join($table, $type = NULL) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'join', + 'args' => array($table, $type), + ); + + return $this; + } + + /** + * Adds "ON ..." conditions for the last created JOIN statement. + * + * @param mixed $c1 column name or array($column, $alias) or object + * @param string $op logic operator + * @param mixed $c2 column name or array($column, $alias) or object + * @return $this + */ + public function on($c1, $op, $c2) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'on', + 'args' => array($c1, $op, $c2), + ); + + return $this; + } + + /** + * Creates a "GROUP BY ..." filter. + * + * @param mixed $columns column name or array($column, $alias) or object + * @param ... + * @return $this + */ + public function group_by($columns) + { + $columns = func_get_args(); + + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'group_by', + 'args' => $columns, + ); + + return $this; + } + + /** + * Alias of and_having() + * + * @param mixed $column column name or array($column, $alias) or object + * @param string $op logic operator + * @param mixed $value column value + * @return $this + */ + public function having($column, $op, $value = NULL) + { + return $this->and_having($column, $op, $value); + } + + /** + * Creates a new "AND HAVING" condition for the query. + * + * @param mixed $column column name or array($column, $alias) or object + * @param string $op logic operator + * @param mixed $value column value + * @return $this + */ + public function and_having($column, $op, $value = NULL) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'and_having', + 'args' => array($column, $op, $value), + ); + + return $this; + } + + /** + * Creates a new "OR HAVING" condition for the query. + * + * @param mixed $column column name or array($column, $alias) or object + * @param string $op logic operator + * @param mixed $value column value + * @return $this + */ + public function or_having($column, $op, $value = NULL) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'or_having', + 'args' => array($column, $op, $value), + ); + + return $this; + } + + /** + * Alias of and_having_open() + * + * @return $this + */ + public function having_open() + { + return $this->and_having_open(); + } + + /** + * Opens a new "AND HAVING (...)" grouping. + * + * @return $this + */ + public function and_having_open() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'and_having_open', + 'args' => array(), + ); + + return $this; + } + + /** + * Opens a new "OR HAVING (...)" grouping. + * + * @return $this + */ + public function or_having_open() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'or_having_open', + 'args' => array(), + ); + + return $this; + } + + /** + * Closes an open "AND HAVING (...)" grouping. + * + * @return $this + */ + public function having_close() + { + return $this->and_having_close(); + } + + /** + * Closes an open "AND HAVING (...)" grouping. + * + * @return $this + */ + public function and_having_close() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'and_having_close', + 'args' => array(), + ); + + return $this; + } + + /** + * Closes an open "OR HAVING (...)" grouping. + * + * @return $this + */ + public function or_having_close() + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'or_having_close', + 'args' => array(), + ); + + return $this; + } + + /** + * Start returning results after "OFFSET ..." + * + * @param integer $number starting result number + * @return $this + */ + public function offset($number) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'offset', + 'args' => array($number), + ); + + return $this; + } + + /** + * Enables the query to be cached for a specified amount of time. + * + * @param integer $lifetime number of seconds to cache + * @return $this + * @uses Kohana::$cache_life + */ + public function cached($lifetime = NULL) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'cached', + 'args' => array($lifetime), + ); + + return $this; + } + + /** + * Set the value of a parameter in the query. + * + * @param string $param parameter key to replace + * @param mixed $value value to use + * @return $this + */ + public function param($param, $value) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'param', + 'args' => array($param, $value), + ); + + return $this; + } + + /** + * Adds "USING ..." conditions for the last created JOIN statement. + * + * @param string $columns column name + * @return $this + */ + public function using($columns) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array( + 'name' => 'using', + 'args' => array($columns), + ); + + return $this; + } + + /** + * Checks whether a column value is unique. + * Excludes itself if loaded. + * + * @param string $field the field to check for uniqueness + * @param mixed $value the value to check for uniqueness + * @return bool whteher the value is unique + */ + public function unique($field, $value) + { + $model = ORM::factory($this->object_name()) + ->where($field, '=', $value) + ->find(); + + if ($this->loaded()) + { + return ( ! ($model->loaded() AND $model->pk() != $this->pk())); + } + + return ( ! $model->loaded()); + } } // End ORM diff --git a/includes/kohana/modules/orm/classes/kohana/orm/validation/exception.php b/includes/kohana/modules/orm/classes/Kohana/ORM/Validation/Exception.php similarity index 66% rename from includes/kohana/modules/orm/classes/kohana/orm/validation/exception.php rename to includes/kohana/modules/orm/classes/Kohana/ORM/Validation/Exception.php index f4c303b..d88fcd1 100644 --- a/includes/kohana/modules/orm/classes/kohana/orm/validation/exception.php +++ b/includes/kohana/modules/orm/classes/Kohana/ORM/Validation/Exception.php @@ -1,10 +1,10 @@ -_object_name = $object_name; + $this->_alias = $alias; $this->_objects['_object'] = $object; + $this->_objects['_has_many'] = FALSE; - parent::__construct($message, $values, $code); + parent::__construct($message, $values, $code, $previous); } /** @@ -62,6 +63,9 @@ class Kohana_ORM_Validation_Exception extends Kohana_Exception { */ public function add_object($alias, Validation $object, $has_many = FALSE) { + // We will need this when generating errors + $this->_objects[$alias]['_has_many'] = ($has_many !== FALSE); + if ($has_many === TRUE) { // This is most likely a has_many relationship @@ -84,13 +88,17 @@ class Kohana_ORM_Validation_Exception extends Kohana_Exception { * Merges an ORM_Validation_Exception object into the current exception * Useful when you want to combine errors into one array * - * @param string $alias The relationship alias from the model * @param ORM_Validation_Exception $object The exception to merge * @param mixed $has_many The array key to use if this exception can be merged multiple times * @return ORM_Validation_Exception */ - public function merge($alias, ORM_Validation_Exception $object, $has_many = FALSE) + public function merge(ORM_Validation_Exception $object, $has_many = FALSE) { + $alias = $object->alias(); + + // We will need this when generating errors + $this->_objects[$alias]['_has_many'] = ($has_many !== FALSE); + if ($has_many === TRUE) { // This is most likely a has_many relationship @@ -122,48 +130,46 @@ class Kohana_ORM_Validation_Exception extends Kohana_Exception { */ public function errors($directory = NULL, $translate = TRUE) { - if ($directory !== NULL) - { - // Everything starts at $directory/$object_name - $directory .= '/'.$this->_object_name; - } - - return $this->generate_errors($this->_objects, $directory, $translate); + return $this->generate_errors($this->_alias, $this->_objects, $directory, $translate); } /** * Recursive method to fetch all the errors in this exception * + * @param string $alias Alias to use for messages file * @param array $array Array of Validation objects to get errors from * @param string $directory Directory to load error messages from * @param mixed $translate Translate the message * @return array */ - protected function generate_errors(array $array, $directory, $translate) + protected function generate_errors($alias, array $array, $directory, $translate) { $errors = array(); - foreach ($array as $alias => $object) + foreach ($array as $key => $object) { - if ($directory === NULL) - { - // Return the raw errors - $file = NULL; - } - else - { - $file = trim($directory.'/'.$alias, '/'); - } - if (is_array($object)) { - // Recursively fill the errors array - $errors[$alias] = $this->generate_errors($object, $file, $translate); + $errors[$key] = ($key === '_external') + // Search for errors in $alias/_external.php + ? $this->generate_errors($alias.'/'.$key, $object, $directory, $translate) + // Regular models get their own file not nested within $alias + : $this->generate_errors($key, $object, $directory, $translate); } - else + elseif ($object instanceof Validation) { + if ($directory === NULL) + { + // Return the raw errors + $file = NULL; + } + else + { + $file = trim($directory.'/'.$alias, '/'); + } + // Merge in this array of errors - $errors += $object->errors($directory, $translate); + $errors += $object->errors($file, $translate); } } @@ -179,4 +185,14 @@ class Kohana_ORM_Validation_Exception extends Kohana_Exception { { return $this->_objects; } + + /** + * Returns the protected _alias property from this exception + * + * @return string + */ + public function alias() + { + return $this->_alias; + } } // End Kohana_ORM_Validation_Exception diff --git a/includes/kohana/modules/orm/classes/model/auth/role.php b/includes/kohana/modules/orm/classes/Model/Auth/Role.php similarity index 72% rename from includes/kohana/modules/orm/classes/model/auth/role.php rename to includes/kohana/modules/orm/classes/Model/Auth/Role.php index 0ad4c66..5a12764 100644 --- a/includes/kohana/modules/orm/classes/model/auth/role.php +++ b/includes/kohana/modules/orm/classes/Model/Auth/Role.php @@ -1,4 +1,4 @@ - array('through' => 'roles_users')); + protected $_has_many = array( + 'users' => array('model' => 'User','through' => 'roles_users'), + ); public function rules() { @@ -26,4 +28,4 @@ class Model_Auth_Role extends ORM { ); } -} // End Auth Role Model \ No newline at end of file +} // End Auth Role Model diff --git a/includes/kohana/modules/orm/classes/model/auth/user.php b/includes/kohana/modules/orm/classes/Model/Auth/User.php similarity index 72% rename from includes/kohana/modules/orm/classes/model/auth/user.php rename to includes/kohana/modules/orm/classes/Model/Auth/User.php index aeb1563..d86fae4 100644 --- a/includes/kohana/modules/orm/classes/model/auth/user.php +++ b/includes/kohana/modules/orm/classes/Model/Auth/User.php @@ -1,10 +1,10 @@ - array('model' => 'user_token'), - 'roles' => array('model' => 'role', 'through' => 'roles_users'), + 'user_tokens' => array('model' => 'User_Token'), + 'roles' => array('model' => 'Role', 'through' => 'roles_users'), ); /** @@ -32,20 +32,16 @@ class Model_Auth_User extends ORM { return array( 'username' => array( array('not_empty'), - array('min_length', array(':value', 4)), array('max_length', array(':value', 32)), - array('regex', array(':value', '/^[-\pL\pN_.]++$/uD')), - array(array($this, 'username_available'), array(':validation', ':field')), + array(array($this, 'unique'), array('username', ':value')), ), 'password' => array( array('not_empty'), ), 'email' => array( array('not_empty'), - array('min_length', array(':value', 4)), - array('max_length', array(':value', 127)), array('email'), - array(array($this, 'email_available'), array(':validation', ':field')), + array(array($this, 'unique'), array('email', ':value')), ), ); } @@ -99,38 +95,6 @@ class Model_Auth_User extends ORM { } } - /** - * Does the reverse of unique_key_exists() by triggering error if username exists. - * Validation callback. - * - * @param Validation Validation object - * @param string Field name - * @return void - */ - public function username_available(Validation $validation, $field) - { - if ($this->unique_key_exists($validation[$field], 'username')) - { - $validation->error($field, 'username_available', array($validation[$field])); - } - } - - /** - * Does the reverse of unique_key_exists() by triggering error if email exists. - * Validation callback. - * - * @param Validation Validation object - * @param string Field name - * @return void - */ - public function email_available(Validation $validation, $field) - { - if ($this->unique_key_exists($validation[$field], 'email')) - { - $validation->error($field, 'email_available', array($validation[$field])); - } - } - /** * Tests if a unique key value exists in the database. * @@ -146,7 +110,7 @@ class Model_Auth_User extends ORM { $field = $this->unique_key($value); } - return (bool) DB::select(array('COUNT("*")', 'total_count')) + return (bool) DB::select(array(DB::expr('COUNT(*)'), 'total_count')) ->from($this->_table_name) ->where($field, '=', $value) ->where($this->_primary_key, '!=', $this->pk()) @@ -183,7 +147,7 @@ class Model_Auth_User extends ORM { * * Example usage: * ~~~ - * $user = ORM::factory('user')->create_user($_POST, array( + * $user = ORM::factory('User')->create_user($_POST, array( * 'username', * 'password', * 'email', @@ -210,7 +174,7 @@ class Model_Auth_User extends ORM { * * Example usage: * ~~~ - * $user = ORM::factory('user') + * $user = ORM::factory('User') * ->where('username', '=', 'kiall') * ->find() * ->update_user($_POST, array( @@ -237,4 +201,4 @@ class Model_Auth_User extends ORM { return $this->values($values, $expected)->update($extra_validation); } -} // End Auth User Model \ No newline at end of file +} // End Auth User Model diff --git a/includes/kohana/modules/orm/classes/model/auth/user/token.php b/includes/kohana/modules/orm/classes/Model/Auth/User/Token.php similarity index 74% rename from includes/kohana/modules/orm/classes/model/auth/user/token.php rename to includes/kohana/modules/orm/classes/Model/Auth/User/Token.php index ede1ee8..3c69ead 100644 --- a/includes/kohana/modules/orm/classes/model/auth/user/token.php +++ b/includes/kohana/modules/orm/classes/Model/Auth/User/Token.php @@ -1,16 +1,23 @@ - array()); + protected $_belongs_to = array( + 'user' => array('model' => 'User'), + ); + + protected $_created_column = array( + 'column' => 'created', + 'format' => TRUE, + ); /** * Handles garbage collection and deleting of expired objects. @@ -62,9 +69,9 @@ class Model_Auth_User_Token extends ORM { { $token = sha1(uniqid(Text::random('alnum', 32), TRUE)); } - while(ORM::factory('user_token', array('token' => $token))->loaded()); + while (ORM::factory('User_Token', array('token' => $token))->loaded()); return $token; } -} // End Auth User Token Model \ No newline at end of file +} // End Auth User Token Model diff --git a/includes/kohana/modules/orm/classes/model/role.php b/includes/kohana/modules/orm/classes/Model/Role.php similarity index 63% rename from includes/kohana/modules/orm/classes/model/role.php rename to includes/kohana/modules/orm/classes/Model/Role.php index 983fa96..b1ead06 100644 --- a/includes/kohana/modules/orm/classes/model/role.php +++ b/includes/kohana/modules/orm/classes/Model/Role.php @@ -1,4 +1,4 @@ - 'Official ORM module, a modeling library for object relational mapping.', // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2011 Kohana Team', + 'copyright' => '© 2008–2012 Kohana Team', ) ) ); diff --git a/includes/kohana/modules/orm/guide/orm/examples/simple.md b/includes/kohana/modules/orm/guide/orm/examples/simple.md index 96a23d1..b8abe92 100644 --- a/includes/kohana/modules/orm/guide/orm/examples/simple.md +++ b/includes/kohana/modules/orm/guide/orm/examples/simple.md @@ -67,7 +67,7 @@ This is a simple example of a single ORM model, that has no relationships, but u */ // Create an instance of a model - $members = ORM::factory('member'); + $members = ORM::factory('Member'); // Get all members with the first name "Peter" find_all() // means we get all records matching the query. @@ -81,7 +81,7 @@ This is a simple example of a single ORM model, that has no relationships, but u */ // Create an instance of a model - $member = ORM::factory('member'); + $member = ORM::factory('Member'); // Get a member with the user name "bongo" find() means // we only want the first record matching the query. @@ -92,7 +92,7 @@ This is a simple example of a single ORM model, that has no relationships, but u */ // Create an instance of a model - $member = ORM::factory('member'); + $member = ORM::factory('Member'); // Do an INSERT query $member->username = 'bongo'; @@ -106,7 +106,7 @@ This is a simple example of a single ORM model, that has no relationships, but u // Create an instance of a model where the // table field "id" is "1" - $member = ORM::factory('member', 1); + $member = ORM::factory('Member', 1); // Do an UPDATE query $member->username = 'bongo'; diff --git a/includes/kohana/modules/orm/guide/orm/examples/validation.md b/includes/kohana/modules/orm/guide/orm/examples/validation.md index bdbbc0c..0510a45 100644 --- a/includes/kohana/modules/orm/guide/orm/examples/validation.md +++ b/includes/kohana/modules/orm/guide/orm/examples/validation.md @@ -44,7 +44,7 @@ This example will create user accounts and demonstrate how to handle model and c public function username_available($username) { // There are simpler ways to do this, but I will use ORM for the sake of the example - return ORM::factory('member', array('username' => $username))->loaded(); + return ORM::factory('Member', array('username' => $username))->loaded(); } public function hash_password($password) @@ -85,7 +85,7 @@ Please forgive my slightly ugly form. I am trying not to use any modules or unre if ($_POST) { - $member = ORM::factory('member') + $member = ORM::factory('Member') // The ORM::values() method is a shortcut to assign many values at once ->values($_POST, array('username', 'password')); diff --git a/includes/kohana/modules/orm/guide/orm/filters.md b/includes/kohana/modules/orm/guide/orm/filters.md index 5f20e60..218f0a4 100644 --- a/includes/kohana/modules/orm/guide/orm/filters.md +++ b/includes/kohana/modules/orm/guide/orm/filters.md @@ -1,21 +1,40 @@ # Filters -Filters in ORM work much like they used to when they were part of the Validate class in 3.0.x however they have been modified to match the flexible syntax of [Validation] rules in 3.1.x. Filters run as soon as the field is set in your model and should be used to format the data before it is inserted into the Database. +Filters in ORM work much like they used to when they were part of the Validate class in 3.0.x. However, they have been modified to match the flexible syntax of [Validation] rules in 3.1.x. -Define your filters the same way you define rules, as an array returned by the `ORM::filters()` method like the following: +Filters run as soon as the field is set in your model and should be used to format the data before it is inserted into the Database. Filters are defined the same way you define [rules](validation), as an array returned by the `ORM::filters()` method, like the following: public function filters() { return array( + // Field Filters + // $field_name => array(mixed $callback[, array $params = array(':value')]), 'username' => array( + // PHP Function Callback, default implicit param of ':value' array('trim'), ), 'password' => array( - array(array($this, 'hash_password')), + // Callback method with object context and params + array(array($this, 'hash_password'), array(':value', Model_User::salt())), ), 'created_on' => array( + // Callback static method with params array('Format::date', array(':value', 'Y-m-d H:i:s')), ), + 'other_field' => array( + // Callback static method with implicit param of ':value' + array('MyClass::static_method'), + // Callback method with object context with implicit param of ':value' + array(array($this, 'change_other_field')), + // PHP function callback with explicit params + array('str_replace', array('luango', 'thomas', ':value'), + // Function as the callback (PHP 5.3+) + array(function($value) { + // Do something to $value and return it. + return some_function($value); + }), + ), + ); } diff --git a/includes/kohana/modules/orm/guide/orm/menu.md b/includes/kohana/modules/orm/guide/orm/menu.md index 03a43c1..ac2438c 100644 --- a/includes/kohana/modules/orm/guide/orm/menu.md +++ b/includes/kohana/modules/orm/guide/orm/menu.md @@ -7,3 +7,4 @@ - [Examples](examples) - [Simple](examples/simple) - [Validation](examples/validation) +- [Upgrading](upgrading) diff --git a/includes/kohana/modules/orm/guide/orm/relationships.md b/includes/kohana/modules/orm/guide/orm/relationships.md index 074c129..fac7845 100644 --- a/includes/kohana/modules/orm/guide/orm/relationships.md +++ b/includes/kohana/modules/orm/guide/orm/relationships.md @@ -37,7 +37,7 @@ If you wanted access a post's author by using code like `$post->author` then you protected $_belongs_to = array( 'author' => array( - 'model' => 'user', + 'model' => 'User', ), ); @@ -68,7 +68,7 @@ Let's assume now you want to access the posts using the name `stories` instead, protected $_has_many = array( 'stories' => array( - 'model' => 'post', + 'model' => 'Post', 'foreign_key' => 'author_id', ), ); @@ -79,7 +79,7 @@ A `has_one` relationship is almost identical to a `has_many` relationship. In a protected $_has_one = array( 'story' => array( - 'model' => 'post', + 'model' => 'Post', 'foreign_key' => 'author_id', ), ); @@ -92,7 +92,7 @@ To define the `has_many` "through" relationship, the same syntax for standard ha protected $_has_many = array( 'categories' => array( - 'model' => 'category', + 'model' => 'Category', 'through' => 'categories_posts', ), ); @@ -101,7 +101,7 @@ In the Category model: protected $_has_many = array( 'posts' => array( - 'model' => 'post', + 'model' => 'Post', 'through' => 'categories_posts', ), ); diff --git a/includes/kohana/modules/orm/guide/orm/upgrading.md b/includes/kohana/modules/orm/guide/orm/upgrading.md new file mode 100644 index 0000000..c51f0af --- /dev/null +++ b/includes/kohana/modules/orm/guide/orm/upgrading.md @@ -0,0 +1,16 @@ +# Upgrading + +## Table aliases + +ORM [will now alias the main table](http://dev.kohanaframework.org/issues/4066) in a query to the model's singular object name. +i.e. Prior to 3.2 ORM set the from table like so: + + $this->_db_builder->from($this->_table_name); + +As of 3.2 it is now aliased like so: + + $this->_db_builder->from(array($this->_table_name, $this->_object_name)); + +If you have a model `Model_Order` then when building a query use the alias like so: + + $model->where('order.id', '=', $id); diff --git a/includes/kohana/modules/orm/guide/orm/using.md b/includes/kohana/modules/orm/guide/orm/using.md index 8d54d65..9ef03cb 100644 --- a/includes/kohana/modules/orm/guide/orm/using.md +++ b/includes/kohana/modules/orm/guide/orm/using.md @@ -4,7 +4,7 @@ To create a new `Model_User` instance, you can do one of two things: - $user = ORM::factory('user'); + $user = ORM::factory('User'); // Or $user = new Model_User(); @@ -12,7 +12,7 @@ To create a new `Model_User` instance, you can do one of two things: To insert a new record into the database, create a new instance of the model: - $user = ORM::factory('user'); + $user = ORM::factory('User'); Then, assign values for each of the properties; @@ -33,11 +33,11 @@ Insert the new record into the database by running [ORM::save]: To find an object you can call the [ORM::find] method or pass the id into the ORM constructor: // Find user with ID 20 - $user = ORM::factory('user') + $user = ORM::factory('User') ->where('id', '=', 20) ->find(); // Or - $user = ORM::factory('user', 20); + $user = ORM::factory('User', 20); ## Check that ORM loaded a record @@ -70,6 +70,25 @@ And if you want to save the changes you just made back to the database, just run To delete an object, you can call the [ORM::delete] method on a loaded ORM model. - $user = ORM::factory('user', 20); + $user = ORM::factory('User', 20); $user->delete(); + +## Mass assignment + + +To set multiple values at once, use [ORM::values] + + try + { + $user = ORM::factory('user') + ->values($this->request->post(), array('username','password')) + ->create(); + } + catch (ORM_Validation_Exception $e) + { + // Handle validation errors ... + } + +[!!] Although the second argument is optional, it is *highly recommended* to specify the list of columns you expect to change. Not doing so will leave your code _vulnerable_ in case the attacker adds fields you didn't expect. + diff --git a/includes/kohana/modules/orm/guide/orm/validation.md b/includes/kohana/modules/orm/guide/orm/validation.md index d83414f..56a46ca 100644 --- a/includes/kohana/modules/orm/guide/orm/validation.md +++ b/includes/kohana/modules/orm/guide/orm/validation.md @@ -41,7 +41,7 @@ All models automatically validate their own data when `ORM::save()`, `ORM::updat { try { - $user = ORM::factory('user'); + $user = ORM::factory('User'); $user->username = 'invalid username'; $user->save(); } @@ -61,7 +61,7 @@ In the below example, the error messages will be defined in `application/message { try { - $user = ORM::factory('user'); + $user = ORM::factory('User'); $user->username = 'invalid username'; $user->save(); } @@ -79,7 +79,7 @@ Certain forms contain information that should not be validated by the model, but { try { - $user = ORM::factory('user'); + $user = ORM::factory('User'); $user->username = $_POST['username']; $user->password = $_POST['password']; @@ -108,4 +108,4 @@ Because the validation object was passed as a parameter to the model, any errors This ensures that errors from multiple validation objects and models will never overwrite each other. -[!!] The power of the [ORM_Validation_Exception] can be leveraged in many different ways to merge errors from related models. Take a look at the list of [Examples](examples) for some great use cases. +[!!] The power of the [ORM_Validation_Exception] can be leveraged in many different ways to merge errors from related models. Take a look at the list of [Examples](examples) for some great use cases. \ No newline at end of file diff --git a/includes/kohana/modules/pagination/.gitignore b/includes/kohana/modules/pagination/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/includes/kohana/modules/pagination/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/includes/kohana/modules/pagination/classes/kohana/pagination.php b/includes/kohana/modules/pagination/classes/kohana/pagination.php new file mode 100644 index 0000000..8f5d533 --- /dev/null +++ b/includes/kohana/modules/pagination/classes/kohana/pagination.php @@ -0,0 +1,335 @@ + array('source' => 'query_string', 'key' => 'page'), + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ); + + /** + * @var array Members that have access methods + */ + protected $_properties = array( + 'current_page', 'total_items', 'items_per_page', 'total_pages', 'current_first_item', 'current_last_item', + 'previous_page', 'next_page', 'first_page', 'last_page', 'offset', + ); + + // Current page number + protected $_current_page; + + // Total item count + protected $_total_items; + + // How many items to show per page + protected $_items_per_page; + + // Total page count + protected $_total_pages; + + // Item offset for the first item displayed on the current page + protected $_current_first_item; + + // Item offset for the last item displayed on the current page + protected $_current_last_item; + + // Previous page number; FALSE if the current page is the first one + protected $_previous_page; + + // Next page number; FALSE if the current page is the last one + protected $_next_page; + + // First page number; FALSE if the current page is the first one + protected $_first_page; + + // Last page number; FALSE if the current page is the last one + protected $_last_page; + + // Query offset + protected $_offset; + + /** + * Creates a new Pagination object. + * + * @param array configuration + * @return Pagination + */ + public static function factory(array $config = array()) + { + return new Pagination($config); + } + + /** + * Creates a new Pagination object. + * + * @param array configuration + * @return void + */ + public function __construct(array $config = array()) + { + // Overwrite system defaults with application defaults + $this->config = $this->config_group() + $this->config; + + // Pagination setup + $this->setup($config); + } + + /** + * Retrieves a pagination config group from the config file. One config group can + * refer to another as its parent, which will be recursively loaded. + * + * @param string pagination config group; "default" if none given + * @return array config settings + */ + public function config_group($group = 'default') + { + // Load the pagination config file + $config_file = Kohana::$config->load('pagination'); + + // Initialize the $config array + $config['group'] = (string) $group; + + // Recursively load requested config groups + while (isset($config['group']) AND isset($config_file->$config['group'])) + { + // Temporarily store config group name + $group = $config['group']; + unset($config['group']); + + // Add config group values, not overwriting existing keys + $config += $config_file->$group; + } + + // Get rid of possible stray config group names + unset($config['group']); + + // Return the merged config group settings + return $config; + } + + /** + * Loads configuration settings into the object and (re)calculates pagination if needed. + * Allows you to update config settings after a Pagination object has been constructed. + * + * @param array configuration + * @return object Pagination + */ + public function setup(array $config = array()) + { + if (isset($config['group'])) + { + // Recursively load requested config groups + $config += $this->config_group($config['group']); + } + + // Overwrite the current config settings + $this->config = $config + $this->config; + + // Only (re)calculate pagination when needed + if ($this->_current_page === NULL + OR isset($config['current_page']) + OR isset($config['total_items']) + OR isset($config['items_per_page'])) + { + // Retrieve the current page number + if ( ! empty($this->config['current_page']['page'])) + { + // The current page number has been set manually + $this->_current_page = (int) $this->config['current_page']['page']; + } + else + { + switch ($this->config['current_page']['source']) + { + case 'query_string': + $this->_current_page = isset($_GET[$this->config['current_page']['key']]) + ? (int) $_GET[$this->config['current_page']['key']] + : 1; + break; + + case 'route': + $this->_current_page = (int) Request::current()->param($this->config['current_page']['key'], 1); + break; + } + } + + // Calculate and clean all pagination variables + $this->_total_items = (int) max(0, $this->config['total_items']); + $this->_items_per_page = (int) max(1, $this->config['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); + $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; + $this->_first_page = ($this->_current_page === 1) ? FALSE : 1; + $this->_last_page = ($this->_current_page >= $this->_total_pages) ? FALSE : $this->_total_pages; + $this->_offset = (int) (($this->_current_page - 1) * $this->_items_per_page); + } + + // Chainable method + return $this; + } + + /** + * Generates the full URL for a certain page. + * + * @param integer page number + * @return string page URL + */ + public function url($page = 1) + { + // Clean the page number + $page = max(1, (int) $page); + + // No page number in URLs to first page + if ($page === 1 AND ! $this->config['first_page_in_url']) + { + $page = NULL; + } + + switch ($this->config['current_page']['source']) + { + case 'query_string': + return URL::site(Request::current()->uri()).URL::query(array($this->config['current_page']['key'] => $page)); + + case 'route': + return URL::site(Request::current()->uri(array($this->config['current_page']['key'] => $page))).URL::query(); + } + + return '#'; + } + + /** + * Checks whether the given page number exists. + * + * @param integer page number + * @return boolean + * @since 3.0.7 + */ + public function valid_page($page) + { + // Page number has to be a clean integer + if ( ! Valid::digit($page)) + return FALSE; + + return $page > 0 AND $page <= $this->_total_pages; + } + + /** + * Renders the pagination links. + * + * @param mixed string of the view to use, or a Kohana_View object + * @return string pagination output (HTML) + */ + public function render($view = NULL) + { + // Automatically hide pagination whenever it is superfluous + if ($this->config['auto_hide'] === TRUE AND $this->_total_pages <= 1) + return ''; + + if ($view === NULL) + { + // Use the view from config + $view = $this->config['view']; + } + + if ( ! $view instanceof View) + { + // Load the view file + $view = View::factory($view); + } + + // Pass on the whole Pagination object + return $view->set(get_object_vars($this))->set('page', $this)->render(); + } + + /** + * Renders the pagination links. + * + * @return string pagination output (HTML) + */ + public function __toString() + { + try + { + return $this->render(); + } + catch(Exception $e) + { + Kohana_Exception::handler($e); + return ''; + } + } + + /** + * Handles loading and setting properties. + * + * @param string $method Method name + * @param array $args Method arguments + * @return mixed + */ + public function __call($method, array $args) + { + if (in_array($method, $this->_properties)) + { + if (!count($args)) + { + return $this->{'_'.$method}; + } + } + else + { + throw new Kohana_Exception('Invalid method :method called in :class', + array(':method' => $method, ':class' => get_class($this))); + } + } + + /** + * Handles setting of property + * + * @param string $key Property name + * @param mixed $value Property value + * @return void + */ + public function __set($key, $value) + { + if (isset($this->{'_'.$key})) + { + $this->setup(array($key => $value)); + } + else + { + throw new Kohana_Exception('The :property: property does not exist in the :class: class', + array(':property:' => $key, ':class:' => get_class($this))); + } + } + +} // End Pagination diff --git a/includes/kohana/modules/image/classes/image/gd.php b/includes/kohana/modules/pagination/classes/pagination.php similarity index 57% rename from includes/kohana/modules/image/classes/image/gd.php rename to includes/kohana/modules/pagination/classes/pagination.php index 0c30ba5..6be4b15 100644 --- a/includes/kohana/modules/image/classes/image/gd.php +++ b/includes/kohana/modules/pagination/classes/pagination.php @@ -1,3 +1,3 @@ array( + 'current_page' => array('source' => 'query_string', 'key' => 'page'), // source: "query_string" or "route" + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + +); diff --git a/includes/kohana/modules/pagination/config/userguide.php b/includes/kohana/modules/pagination/config/userguide.php new file mode 100644 index 0000000..936b209 --- /dev/null +++ b/includes/kohana/modules/pagination/config/userguide.php @@ -0,0 +1,23 @@ + array( + + // This should be the path to this modules userguide pages, without the 'guide/'. Ex: '/guide/modulename/' would be 'modulename' + 'pagination' => array( + + // Whether this modules userguide pages should be shown + 'enabled' => TRUE, + + // The name that should show up on the userguide index page + 'name' => 'Pagination', + + // A short description of this module, shown on the index page + 'description' => 'Tool for creating paginated links and viewing pages of results.', + + // Copyright message, shown in the footer for this module + 'copyright' => '© 2008–2010 Kohana Team', + ) + ) +); \ No newline at end of file diff --git a/includes/kohana/modules/pagination/guide/pagination/config.md b/includes/kohana/modules/pagination/guide/pagination/config.md new file mode 100644 index 0000000..4a5713b --- /dev/null +++ b/includes/kohana/modules/pagination/guide/pagination/config.md @@ -0,0 +1,94 @@ +# Pagination Configuration + +[Pagination] uses 6 settings: `current_page`, `total_items`, `items_per_page`, `view`, `auto_hide` and `first_page_in_url`. + +## Configuration Examples + +This example shows the default configuration: + + return array( + + // Application defaults + 'default' => array( + 'current_page' => array('source' => 'query_string', 'key' => 'page'), // source: "query_string" or "route" + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + ); + +This is an example with multiple configurations: + + return array( + + // Application defaults + 'default' => array( + 'current_page' => array('source' => 'query_string', 'key' => 'page'), + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + + // Second configuration + 'pretty' => array( + 'current_page' => array('source' => 'route', 'key' => 'page'), + 'total_items' => 0, + 'items_per_page' => 20, + 'view' => 'pagination/pretty', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + ); + + + +## Settings + +### current_page + +The `current_page` setting tells Pagination where to look to find the current page number. +There are two options for the `source` of the page number: `query_string` and `route`. +The `key` index in the configuration array tells Pagination what name to look for when it's searching in the query string or route. + +This configuration informs Pagination to look in the query string for a value named `page`: + + 'current_page' => array('source' => 'query_string', 'key' => 'page'), + +If you have a route setup with the page number in the actual URL like this: + + Route::set('city_listings', 'listings(/)', array('page_num' => '[0-9]+')) + ->defaults(array( + 'controller' => 'city', + 'action' => 'listings' + )); + +then you would use a setting like this: + + 'current_page' => array('source' => 'route', 'key' => 'page_num'), + + +### total_items + +`total_items` is a setting you will most likely pass in during runtime after figuring out exactly how many items you have. It can be set to zero in the configuration for now. + +### items_per_page + +Self explanatory. This is the maximum items to show on each page. Pagination determines the total number of pages based off of this number. + +### view + +The `view` setting should be a path to a Pagination view file. + +### auto_hide + +If `auto_hide` is set to `TRUE` then Pagination will automatically hide whenever there's only one page of items. + +### first_page_in_url + +If you want Pagination to add the page number to the first page's link then set this setting to `TRUE` otherwise leave it as `FALSE`. + + diff --git a/includes/kohana/modules/auth/guide/auth/edit.md b/includes/kohana/modules/pagination/guide/pagination/examples.md similarity index 100% rename from includes/kohana/modules/auth/guide/auth/edit.md rename to includes/kohana/modules/pagination/guide/pagination/examples.md diff --git a/includes/kohana/modules/auth/guide/auth/register.md b/includes/kohana/modules/pagination/guide/pagination/index.md similarity index 100% rename from includes/kohana/modules/auth/guide/auth/register.md rename to includes/kohana/modules/pagination/guide/pagination/index.md diff --git a/includes/kohana/modules/pagination/guide/pagination/menu.md b/includes/kohana/modules/pagination/guide/pagination/menu.md new file mode 100644 index 0000000..08c39c8 --- /dev/null +++ b/includes/kohana/modules/pagination/guide/pagination/menu.md @@ -0,0 +1,4 @@ +## [Pagination]() +- [Config](config) +- [Usage](usage) +- [Examples](examples) \ No newline at end of file diff --git a/includes/kohana/modules/auth/guide/auth/roles.md b/includes/kohana/modules/pagination/guide/pagination/usage.md similarity index 100% rename from includes/kohana/modules/auth/guide/auth/roles.md rename to includes/kohana/modules/pagination/guide/pagination/usage.md diff --git a/includes/kohana/modules/pagination/views/pagination/basic.php b/includes/kohana/modules/pagination/views/pagination/basic.php new file mode 100644 index 0000000..f0d414d --- /dev/null +++ b/includes/kohana/modules/pagination/views/pagination/basic.php @@ -0,0 +1,37 @@ +

+ + first_page() !== FALSE): ?> + + + + + + previous_page() !== FALSE): ?> + + + + + + total_pages(); $i++): ?> + + current_page()): ?> + + + + + + + + next_page() !== FALSE): ?> + + + + + + last_page() !== FALSE): ?> + + + + + +

diff --git a/includes/kohana/modules/pagination/views/pagination/floating.php b/includes/kohana/modules/pagination/views/pagination/floating.php new file mode 100644 index 0000000..52e3fc2 --- /dev/null +++ b/includes/kohana/modules/pagination/views/pagination/floating.php @@ -0,0 +1,94 @@ +total_pages()); + +// Ending group of pages: $n7...$n8 +$n7 = max(1, $page->total_pages() - $count_out + 1); +$n8 = $page->total_pages(); + +// Middle group of pages: $n4...$n5 +$n4 = max($n2 + 1, $page->current_page() - $count_in); +$n5 = min($n7 - 1, $page->current_page() + $count_in); +$use_middle = ($n5 >= $n4); + +// Point $n3 between $n2 and $n4 +$n3 = (int) (($n2 + $n4) / 2); +$use_n3 = ($use_middle && (($n4 - $n2) > 1)); + +// Point $n6 between $n5 and $n7 +$n6 = (int) (($n5 + $n7) / 2); +$use_n6 = ($use_middle && (($n7 - $n5) > 1)); + +// Links to display as array(page => content) +$links = array(); + +// Generate links data in accordance with calculated numbers +for ($i = $n1; $i <= $n2; $i++) +{ + $links[$i] = $i; +} +if ($use_n3) +{ + $links[$n3] = '…'; +} +for ($i = $n4; $i <= $n5; $i++) +{ + $links[$i] = $i; +} +if ($use_n6) +{ + $links[$n6] = '…'; +} +for ($i = $n7; $i <= $n8; $i++) +{ + $links[$i] = $i; +} + +?> +

+ + first_page() !== FALSE): ?> + + + + + + previous_page() !== FALSE): ?> + + + + + + $content): ?> + + current_page()): ?> + + + + + + + + next_page() !== FALSE): ?> + + + + + + last_page() !== FALSE): ?> + + + + + +

diff --git a/includes/kohana/modules/unittest/README.markdown b/includes/kohana/modules/unittest/README.markdown index 45e45c9..af4251a 100644 --- a/includes/kohana/modules/unittest/README.markdown +++ b/includes/kohana/modules/unittest/README.markdown @@ -9,28 +9,16 @@ I've chosen to do this because it's part of the PHPUnit coding conventions and i * [PHPUnit](http://www.phpunit.de/) >= 3.4 -### Optional extras +## Usage -* The [Archive module](http://github.com/BRMatt/kohana-archive) is required if you want to download code coverage reports from the web ui, however you can also view them without downloading. + $ phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php -## Installation +Alternatively you can use a `phpunit.xml` to have a more fine grained control +over which tests are included and which files are whitelisted. -**Step 0**: Download this module! +Make sure you only whitelist the highest files in the cascading filesystem, else +you could end up with a lot of "class cannot be redefined" errors. -To get it from git execute the following command in the root of your project: - - $ git submodule add git://github.com/kohana/unittest.git modules/unittest - -And watch the gitorious magic... - -Of course, you can always download the code from the [github project](http://github.com/kohana/unittest) as an archive. - -## Running the tests - - $ phpunit --bootstrap=modules/unittest/bootstrap.php {tests} - -Where `{tests}` can either be a path to a folder of tests, or a path to the the `tests.php` (`modules/unittest/tests.php`) - -Please see the guide pages for more info. An example of how we run the tests for the kohana project can be found in the [phing build script](https://github.com/kohana/kohana/blob/3.1/master/build.xml#L172). - -If you're looking for more info on running the core kohana tests then please see our [dev wiki](https://github.com/kohana/kohana/wiki/Unit-Testing-Kohana) \ No newline at end of file +If you use the `tests.php` testsuite loader then it will only whitelist the +highest files. see `config/unittest.php` for details on configuring the +`tests.php` whitelist. diff --git a/includes/kohana/modules/unittest/bootstrap.php b/includes/kohana/modules/unittest/bootstrap.php index bcc7679..2abe238 100644 --- a/includes/kohana/modules/unittest/bootstrap.php +++ b/includes/kohana/modules/unittest/bootstrap.php @@ -1,38 +1,50 @@ 0) + { + ob_end_flush(); + } + else + { + ob_end_clean(); + } +} + // Enable the unittest module -Kohana::modules(Kohana::modules() + array('unittest' => MODPATH.'unittest')); \ No newline at end of file +Kohana::modules(Kohana::modules() + array('unittest' => SMDPATH.'unittest')); diff --git a/includes/kohana/modules/unittest/bootstrap_all_modules.php b/includes/kohana/modules/unittest/bootstrap_all_modules.php new file mode 100644 index 0000000..9a591a2 --- /dev/null +++ b/includes/kohana/modules/unittest/bootstrap_all_modules.php @@ -0,0 +1,20 @@ +isDir()) + { + $modules[$module->getFilename()] = MODPATH.$module->getFilename(); + } +} + +Kohana::modules(Kohana::modules() + $modules); + +unset ($modules_iterator, $modules, $module); diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/database/testcase.php b/includes/kohana/modules/unittest/classes/Kohana/Unittest/Database/TestCase.php similarity index 86% rename from includes/kohana/modules/unittest/classes/kohana/unittest/database/testcase.php rename to includes/kohana/modules/unittest/classes/Kohana/Unittest/Database/TestCase.php index ae825c7..e39e366 100644 --- a/includes/kohana/modules/unittest/classes/kohana/unittest/database/testcase.php +++ b/includes/kohana/modules/unittest/classes/Kohana/Unittest/Database/TestCase.php @@ -8,8 +8,7 @@ * @copyright (c) 2008-2009 Kohana Team * @license http://kohanaphp.com/license */ -// @codingStandardsIgnoreFile -abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Database_TestCase { +abstract class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Database_TestCase { /** * Whether we should enable work arounds to make the tests compatible with phpunit 3.4 @@ -21,7 +20,7 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data * Make sure PHPUnit backs up globals * @var boolean */ - protected $backupGlobals = TRUE; + protected $backupGlobals = FALSE; /** * A set of unittest helpers that are shared between normal / database @@ -36,6 +35,12 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ protected $environmentDefault = array(); + /** + * The kohana database connection that PHPUnit should use for this test + * @var string + */ + protected $_database_connection = 'default'; + /** * Creates a predefined environment using the default environment * @@ -44,9 +49,9 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public function setUp() { - if (self::$_assert_type_compatability === NULL) + if(self::$_assert_type_compatability === NULL) { - if ( ! class_exists('PHPUnit_Runner_Version')) + if( ! class_exists('PHPUnit_Runner_Version')) { require_once 'PHPUnit/Runner/Version.php'; } @@ -82,8 +87,7 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data public function getConnection() { // Get the unittesting db connection - $config = Kohana::config('database') - ->{Kohana::config('unittest')->db_connection}; + $config = Kohana::$config->load('database.'.$this->_database_connection); if($config['type'] !== 'pdo') { @@ -101,15 +105,15 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data return $this->createDefaultDBConnection($pdo, $config['connection']['database']); } - /** - * Gets a connection to the unittest database - * - * @return Kohana_Database The database connection - */ - public function getKohanaConnection() - { - return Database::instance(Kohana::config('unittest')->db_connection); - } + /** + * Gets a connection to the unittest database + * + * @return Kohana_Database The database connection + */ + public function getKohanaConnection() + { + return Database::instance(Kohana::$config->load('unittest')->db_connection); + } /** * Removes all kohana related cache files in the cache directory @@ -167,8 +171,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertInstanceOf($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertType($expected, $actual, $message); + } return parent::assertInstanceOf($expected, $actual, $message); } @@ -184,8 +190,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); + } return parent::assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message); } @@ -200,8 +208,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertNotInstanceOf($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertNotType($expected, $actual, $message); + } return self::assertNotInstanceOf($expected, $actual, $message); } @@ -217,8 +227,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); + } return self::assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message); } @@ -233,8 +245,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertInternalType($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertType($expected, $actual, $message); + } return parent::assertInternalType($expected, $actual, $message); } @@ -250,8 +264,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertAttributeInternalType($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); + } return self::assertAttributeInternalType($expected, $attributeName, $classOrObject, $message); } @@ -266,8 +282,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertNotInternalType($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertNotType($expected, $actual, $message); + } return self::assertNotInternalType($expected, $actual, $message); } @@ -283,8 +301,10 @@ abstract Class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data */ public static function assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); + } return self::assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message); } diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/helpers.php b/includes/kohana/modules/unittest/classes/Kohana/Unittest/Helpers.php similarity index 94% rename from includes/kohana/modules/unittest/classes/kohana/unittest/helpers.php rename to includes/kohana/modules/unittest/classes/Kohana/Unittest/Helpers.php index 25ef3d6..410090e 100644 --- a/includes/kohana/modules/unittest/classes/kohana/unittest/helpers.php +++ b/includes/kohana/modules/unittest/classes/Kohana/Unittest/Helpers.php @@ -132,7 +132,7 @@ class Kohana_Unittest_Helpers { $class->setStaticPropertyValue($var, $value); } // If this is an environment variable - elseif (preg_match('/^[A-Z_-]+$/D', $option) OR isset($_SERVER[$option])) + elseif (preg_match('/^[A-Z_-]+$/', $option) OR isset($_SERVER[$option])) { if ($backup_needed) { @@ -146,12 +146,12 @@ class Kohana_Unittest_Helpers { { if ($backup_needed) { - $this->_environment_backup[$option] = Kohana::config($option); + $this->_environment_backup[$option] = Kohana::$config->load($option); } list($group, $var) = explode('.', $option, 2); - Kohana::config($group)->set($var, $value); + Kohana::$config->load($group)->set($var, $value); } } } diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/testcase.php b/includes/kohana/modules/unittest/classes/Kohana/Unittest/TestCase.php similarity index 92% rename from includes/kohana/modules/unittest/classes/kohana/unittest/testcase.php rename to includes/kohana/modules/unittest/classes/Kohana/Unittest/TestCase.php index 7d6e777..d47d69f 100644 --- a/includes/kohana/modules/unittest/classes/kohana/unittest/testcase.php +++ b/includes/kohana/modules/unittest/classes/Kohana/Unittest/TestCase.php @@ -4,7 +4,6 @@ * A version of the stock PHPUnit testcase that includes some extra helpers * and default settings */ -// @codingStandardsIgnoreFile abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { /** @@ -17,7 +16,7 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { * Make sure PHPUnit backs up globals * @var boolean */ - protected $backupGlobals = TRUE; + protected $backupGlobals = FALSE; /** * A set of unittest helpers that are shared between normal / database @@ -40,9 +39,9 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public function setUp() { - if (self::$_assert_type_compatability === NULL) + if(self::$_assert_type_compatability === NULL) { - if ( ! class_exists('PHPUnit_Runner_Version')) + if( ! class_exists('PHPUnit_Runner_Version')) { require_once 'PHPUnit/Runner/Version.php'; } @@ -122,8 +121,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertInstanceOf($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertType($expected, $actual, $message); + } return parent::assertInstanceOf($expected, $actual, $message); } @@ -139,8 +140,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); + } return parent::assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message); } @@ -155,8 +158,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertNotInstanceOf($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertNotType($expected, $actual, $message); + } return self::assertNotInstanceOf($expected, $actual, $message); } @@ -172,8 +177,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); + } return self::assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message); } @@ -188,8 +195,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertInternalType($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertType($expected, $actual, $message); + } return parent::assertInternalType($expected, $actual, $message); } @@ -205,8 +214,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertAttributeInternalType($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); + } return self::assertAttributeInternalType($expected, $attributeName, $classOrObject, $message); } @@ -221,8 +232,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertNotInternalType($expected, $actual, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertNotType($expected, $actual, $message); + } return self::assertNotInternalType($expected, $actual, $message); } @@ -238,8 +251,10 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { */ public static function assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message = '') { - if (self::$_assert_type_compatability) + if(self::$_assert_type_compatability) + { return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); + } return self::assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message); } diff --git a/includes/kohana/modules/unittest/classes/Kohana/Unittest/TestSuite.php b/includes/kohana/modules/unittest/classes/Kohana/Unittest/TestSuite.php new file mode 100644 index 0000000..c853373 --- /dev/null +++ b/includes/kohana/modules/unittest/classes/Kohana/Unittest/TestSuite.php @@ -0,0 +1,80 @@ + array(), + 'addDirectoryToBlacklist' => array(), + 'addFileToWhitelist' => array()); + + /** + * Runs the tests and collects their result in a TestResult. + * + * @param PHPUnit_Framework_TestResult $result + * @param mixed $filter + * @param array $groups + * @param array $excludeGroups + * @param boolean $processIsolation + * @return PHPUnit_Framework_TestResult + * @throws InvalidArgumentException + */ + public function run(PHPUnit_Framework_TestResult $result = NULL, $filter = FALSE, array $groups = array(), array $excludeGroups = array(), $processIsolation = FALSE) + { + + // Get the code coverage filter from the suite's result object + $coverage = $result->getCodeCoverage(); + + if ($coverage) + { + $coverage_filter = $coverage->filter(); + + // Apply the white and blacklisting + foreach ($this->_filter_calls as $method => $args) + { + foreach ($args as $arg) + { + $coverage_filter->$method($arg); + } + } + } + + return parent::run($result, $filter, $groups, $excludeGroups, $processIsolation); + } + + /** + * Queues a file to be added to the code coverage blacklist when the suite runs + * @param string $file + */ + public function addFileToBlacklist($file) + { + $this->_filter_calls['addFileToBlacklist'][] = $file; + } + + /** + * Queues a directory to be added to the code coverage blacklist when the suite runs + * @param string $dir + */ + public function addDirectoryToBlacklist($dir) + { + $this->_filter_calls['addDirectoryToBlacklist'][] = $dir; + } + + /** + * Queues a file to be added to the code coverage whitelist when the suite runs + * @param string $file + */ + public function addFileToWhitelist($file) + { + $this->_filter_calls['addFileToWhitelist'][] = $file; + } +} diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/tests.php b/includes/kohana/modules/unittest/classes/Kohana/Unittest/Tests.php similarity index 61% rename from includes/kohana/modules/unittest/classes/kohana/unittest/tests.php rename to includes/kohana/modules/unittest/classes/Kohana/Unittest/Tests.php index dda38e0..308cd4a 100644 --- a/includes/kohana/modules/unittest/classes/kohana/unittest/tests.php +++ b/includes/kohana/modules/unittest/classes/Kohana/Unittest/Tests.php @@ -13,13 +13,6 @@ class Kohana_Unittest_Tests { static protected $cache = array(); - /** - * Flag to identify whether the installed version of phpunit - * is greater than or equal to 3.5 - * @var boolean - */ - static protected $phpunit_v35 = FALSE; - /** * Loads test files if they cannot be found by kohana * @param $class @@ -45,65 +38,19 @@ class Kohana_Unittest_Tests { */ static public function configure_environment($do_whitelist = TRUE, $do_blacklist = TRUE) { - // During a webui request we need to manually load PHPUnit - if ( ! class_exists('PHPUnit_Util_Filter', FALSE) AND ! function_exists('phpunit_autoload')) - { - try - { - include_once 'PHPUnit/Autoload.php'; - } - catch (ErrorException $e) - { - include_once 'PHPUnit/Framework.php'; - } - } - - // Allow PHPUnit to handle exceptions and errors - if (Kohana::$is_cli) - { - restore_exception_handler(); - restore_error_handler(); - } + restore_exception_handler(); + restore_error_handler(); spl_autoload_register(array('Unittest_tests', 'autoload')); - // As of PHPUnit v3.5 there are slight differences in the way files are black|whitelisted - self::$phpunit_v35 = function_exists('phpunit_autoload'); - Unittest_tests::$cache = (($cache = Kohana::cache('unittest_whitelist_cache')) === NULL) ? array() : $cache; - $config = Kohana::config('unittest'); - - if ($do_whitelist AND $config->use_whitelist) - { - self::whitelist(); - } - - if ($do_blacklist AND count($config['blacklist'])) - { - Unittest_tests::blacklist($config->blacklist); - } - } - - /** - * Helper function to see if unittest is enabled in the config - * - * @return boolean - */ - static function enabled() - { - $p_environment = Kohana::config('unittest.environment'); - $k_environment = Kohana::$environment; - - return (is_array($p_environment) AND in_array($k_environment, $p_environment)) - OR - ($k_environment === $p_environment); } /** * Creates the test suite for kohana * - * @return PHPUnit_Framework_TestSuite + * @return Unittest_TestSuite */ static function suite() { @@ -114,10 +61,25 @@ class Kohana_Unittest_Tests { return $suite; } + Unittest_Tests::configure_environment(); + + $suite = new Unittest_TestSuite; + + // Load the whitelist and blacklist for code coverage + $config = Kohana::$config->load('unittest'); + + if ($config->use_whitelist) + { + Unittest_Tests::whitelist(NULL, $suite); + } + + if (count($config['blacklist'])) + { + Unittest_Tests::blacklist($config->blacklist, $suite); + } + + // Add tests $files = Kohana::list_files('tests'); - - $suite = new PHPUnit_Framework_TestSuite; - self::addTests($suite, $files); return $suite; @@ -128,23 +90,20 @@ class Kohana_Unittest_Tests { * * Uses recursion to scan subdirectories * - * @param PHPUnit_Framework_TestSuite $suite The test suite to add to + * @param Unittest_TestSuite $suite The test suite to add to * @param array $files Array of files to test */ - // @codingStandardsIgnoreStart - static function addTests(PHPUnit_Framework_TestSuite $suite, array $files) - // @codingStandardsIgnoreEnd + static function addTests(Unittest_TestSuite $suite, array $files) { - if (self::$phpunit_v35) - { - $filter = PHP_CodeCoverage_Filter::getInstance(); - } - foreach ($files as $file) + foreach ($files as $path => $file) { if (is_array($file)) { - self::addTests($suite, $file); + if ($path != 'tests'.DIRECTORY_SEPARATOR.'test_data') + { + self::addTests($suite, $file); + } } else { @@ -161,14 +120,7 @@ class Kohana_Unittest_Tests { require_once($file); } - if (isset($filter)) - { - $filter->addFileToBlacklist($file); - } - else - { - PHPUnit_Util_Filter::addFileToFilter($file); - } + $suite->addFileToBlacklist($file); } } } @@ -177,38 +129,20 @@ class Kohana_Unittest_Tests { /** * Blacklist a set of files in PHPUnit code coverage * - * @param array A set of files to blacklist + * @param array $blacklist_items A set of files to blacklist + * @param Unittest_TestSuite $suite The test suite */ - static public function blacklist(array $blacklist_items) + static public function blacklist(array $blacklist_items, Unittest_TestSuite $suite = NULL) { - if (self::$phpunit_v35) + foreach ($blacklist_items as $item) { - $filter = PHP_CodeCoverage_Filter::getInstance(); - - foreach ($blacklist_items as $item) + if (is_dir($item)) { - if (is_dir($item)) - { - $filter->addDirectoryToBlacklist($item); - } - else - { - $filter->addFileToBlacklist($item); - } + $suite->addDirectoryToBlacklist($item); } - } - else - { - foreach ($blacklist_items as $item) + else { - if (is_dir($item)) - { - PHPUnit_Util_Filter::addDirectoryToFilter($item); - } - else - { - PHPUnit_Util_Filter::addFileToFilter($item); - } + $suite->addFileToBlacklist($item); } } } @@ -220,8 +154,9 @@ class Kohana_Unittest_Tests { * set in the config file * * @param array $directories Optional directories to whitelist + * @param Unittest_Testsuite $suite Suite to load the whitelist into */ - static public function whitelist(array $directories = NULL) + static public function whitelist(array $directories = NULL, Unittest_TestSuite $suite = NULL) { if (empty($directories)) { @@ -236,7 +171,7 @@ class Kohana_Unittest_Tests { } // Only whitelist the "top" files in the cascading filesystem - self::set_whitelist(Kohana::list_files('classes', $directories)); + self::set_whitelist(Kohana::list_files('classes', $directories), $suite); } } @@ -248,7 +183,7 @@ class Kohana_Unittest_Tests { */ static protected function get_config_whitelist() { - $config = Kohana::config('unittest'); + $config = Kohana::$config->load('unittest'); $directories = array(); if ($config->whitelist['app']) @@ -292,19 +227,16 @@ class Kohana_Unittest_Tests { * Recursively whitelists an array of files * * @param array $files Array of files to whitelist + * @param Unittest_TestSuite $suite Suite to load the whitelist into */ - static protected function set_whitelist($files) + static protected function set_whitelist($files, Unittest_TestSuite $suite = NULL) { - if (self::$phpunit_v35) - { - $filter = PHP_CodeCoverage_Filter::getInstance(); - } foreach ($files as $file) { if (is_array($file)) { - self::set_whitelist($file); + self::set_whitelist($file, $suite); } else { @@ -320,9 +252,9 @@ class Kohana_Unittest_Tests { if (Unittest_tests::$cache[$file]) { - if (isset($filter)) + if (isset($suite)) { - $filter->addFileToWhitelist($file); + $suite->addFileToWhitelist($file); } else { @@ -332,5 +264,4 @@ class Kohana_Unittest_Tests { } } } - } diff --git a/includes/kohana/modules/unittest/classes/Unittest/Database/TestCase.php b/includes/kohana/modules/unittest/classes/Unittest/Database/TestCase.php new file mode 100644 index 0000000..baeaa0a --- /dev/null +++ b/includes/kohana/modules/unittest/classes/Unittest/Database/TestCase.php @@ -0,0 +1,17 @@ + - * @author Paul Banks - * @copyright (c) 2008-2009 Kohana Team - * @license http://kohanaphp.com/license - */ - -class Controller_UnitTest extends Controller_Template -{ - /** - * Whether the archive module is available - * @var boolean - */ - protected $cc_archive_available = FALSE; - - /** - * Unittest config - * @var Config - */ - protected $config = NULL; - - /** - * The uri by which the report uri will be executed - * @var string - */ - protected $report_uri = ''; - - /** - * The uri by which the run action will be executed - * @var string - */ - protected $run_uri = ''; - - /** - * Is the XDEBUG extension loaded? - * @var boolean - */ - protected $xdebug_loaded = FALSE; - - /** - * Template - * @var string - */ - public $template = 'unittest/layout'; - - /** - * Loads test suite - */ - public function before() - { - parent::before(); - - if ( ! Unittest_tests::enabled()) - { - // Pretend this is a normal 404 error... - $this->status = 404; - - throw new Kohana_Request_Exception('Unable to find a route to match the URI: :uri', - array(':uri' => $this->request->uri())); - } - - // Prevent the whitelist from being autoloaded, but allow the blacklist - // to be loaded - Unittest_Tests::configure_environment(FALSE); - - $this->config = Kohana::config('unittest'); - - // This just stops some very very long lines - $route = Route::get('unittest'); - $this->report_uri = $route->uri(array('action' => 'report')); - $this->run_uri = $route->uri(array('action' => 'run')); - - // Switch used to disable cc settings - $this->xdebug_loaded = extension_loaded('xdebug'); - $this->cc_archive_enabled = class_exists('Archive'); - - Kohana_View::set_global('xdebug_enabled', $this->xdebug_loaded); - Kohana_View::set_global('cc_archive_enabled', $this->cc_archive_enabled); - } - - /** - * Handles index page for /unittest/ and /unittest/index/ - */ - public function action_index() - { - $this->template->body = View::factory('unittest/index') - ->set('run_uri', $this->run_uri) - ->set('report_uri', $this->report_uri) - ->set('whitelistable_items', $this->get_whitelistable_items()) - ->set('groups', $this->get_groups_list(Unittest_tests::suite())); - } - - /** - * Handles report generation - */ - public function action_report() - { - // Fairly foolproof - if ( ! $this->config->cc_report_path AND ! class_exists('Archive')) - throw new Kohana_Exception('Cannot generate report'); - - // We don't want to use the HTML layout, we're sending the user 100111011100110010101100 - $this->auto_render = FALSE; - - $suite = Unittest_tests::suite(); - $temp_path = rtrim($this->config->temp_path, '/').'/'; - $group = (array) Arr::get($_GET, 'group', array()); - - // Stop unittest from interpretting "all groups" as "no groups" - if (empty($group) OR empty($group[0])) - { - $group = array(); - } - - if (Arr::get($_GET, 'use_whitelist', FALSE)) - { - $this->whitelist(Arr::get($_GET, 'whitelist', array())); - } - - $runner = new Kohana_Unittest_Runner($suite); - - // If the user wants to download a report - if ($this->cc_archive_enabled AND Arr::get($_GET, 'archive') === '1') - { - // $report is the actual directory of the report, - // $folder is the name component of directory - list($report, $folder) = $runner->generate_report($group, $temp_path); - - $archive = Archive::factory('zip'); - - // TODO: Include the test results? - $archive->add($report, 'report', TRUE); - - $filename = $folder.'.zip'; - - $archive->save($temp_path.$filename); - - // It'd be nice to clear up afterwards but by deleting the report dir we corrupt the archive - // And once the archive has been sent to the user Request stops the script so we can't delete anything - // It'll be up to the user to delete files periodically - $this->request->send_file($temp_path.$filename, $filename); - } - else - { - $folder = trim($this->config->cc_report_path, '/').'/'; - $path = DOCROOT.$folder; - - if ( ! file_exists($path)) - throw new Kohana_Exception('Report directory :dir does not exist', array(':dir' => $path)); - - if ( ! is_writable($path)) - throw new Kohana_Exception('Script doesn\'t have permission to write to report dir :dir ', array(':dir' => $path)); - - $runner->generate_report($group, $path, FALSE); - - $this->request->redirect(URL::site($folder.'index.html', $this->request)); - } - } - - /** - * Handles test running interface - */ - public function action_run() - { - $this->template->body = View::factory('unittest/results'); - - // Get the test suite and work out which groups we're testing - $suite = Unittest_tests::suite(); - $group = (array) Arr::get($_GET, 'group', array()); - - - // Stop phpunit from interpretting "all groups" as "no groups" - if (empty($group) OR empty($group[0])) - { - $group = array(); - } - - // Only collect code coverage if the user asked for it - $collect_cc = (bool) Arr::get($_GET, 'collect_cc', FALSE); - - if ($collect_cc AND Arr::get($_GET, 'use_whitelist', FALSE)) - { - $whitelist = $this->whitelist(Arr::get($_GET, 'whitelist', array())); - } - - $runner = new Kohana_Unittest_Runner($suite); - - try - { - $runner->run($group, $collect_cc); - - if ($collect_cc) - { - $this->template->body->set('coverage', $runner->calculate_cc_percentage()); - } - - if (isset($whitelist)) - { - $this->template->body->set('coverage_explanation', $this->nice_whitelist_explanation($whitelist)); - } - } - catch(Kohana_Exception $e) - { - // Code coverage is not allowed, possibly xdebug disabled? - // TODO: Tell the user this? - $runner->run($group); - } - - // Show some results - $this->template->body - ->set('results', $runner->results) - ->set('totals', $runner->totals) - ->set('time', $this->nice_time($runner->time)) - - // Sets group to the currently selected group, or default all groups - ->set('group', Arr::get($this->get_groups_list($suite), reset($group), 'All groups')) - ->set('groups', $this->get_groups_list($suite)) - - ->set('run_uri', $this->request->uri()) - ->set('report_uri', $this->report_uri.url::query()) - - // Whitelist related stuff - ->set('whitelistable_items', $this->get_whitelistable_items()) - ->set('whitelisted_items', isset($whitelist) ? array_keys($whitelist) : array()) - ->set('whitelist', ! empty($whitelist)); - } - - /** - * Get the list of groups from the test suite, sorted with 'All groups' prefixed - * - * @return array Array of groups in the test suite - */ - protected function get_groups_list($suite) - { - // Make groups aray suitable for drop down - $groups = $suite->getGroups(); - if (count($groups) > 0) - { - sort($groups); - $groups = array_combine($groups, $groups); - } - return array('' => 'All Groups') + $groups; - } - - /** - * Gets a list of items that are whitelistable - * - * @return array - */ - protected function get_whitelistable_items() - { - static $whitelist; - - if (count($whitelist)) - return $whitelist; - - $whitelist = array(); - - $whitelist['k_app'] = 'Application'; - - $k_modules = array_keys(Kohana::modules()); - - $whitelist += array_map('ucfirst', array_combine($k_modules, $k_modules)); - - $whitelist['k_sys'] = 'Kohana Core'; - - return $whitelist; - } - - /** - * Whitelists a specified set of modules specified by the user - * - * @param array $modules - */ - protected function whitelist(array $modules) - { - $k_modules = Kohana::modules(); - $whitelist = array(); - - // Make sure our whitelist is valid - foreach ($modules as $item) - { - if (isset($k_modules[$item])) - { - $whitelist[$item] = $k_modules[$item]; - } - elseif ($item === 'k_app') - { - $whitelist[$item] = APPPATH; - } - elseif ($item === 'k_sys') - { - $whitelist[$item] = SYSPATH; - } - } - - if (count($whitelist)) - { - Unittest_tests::whitelist($whitelist); - } - - return $whitelist; - } - - /** - * Prettifies the list of whitelisted modules - * - * @param array Array of whitelisted items - * @return string - */ - protected function nice_whitelist_explanation(array $whitelist) - { - $items = array_intersect_key($this->get_whitelistable_items(), $whitelist); - - return implode(', ', $items); - } - - protected function nice_time($time) - { - $parts = array(); - - if ($time > DATE::DAY) - { - $parts[] = floor($time/DATE::DAY).'d'; - $time = $time % DATE::DAY; - } - - if ($time > DATE::HOUR) - { - $parts[] = floor($time/DATE::HOUR).'h'; - $time = $time % DATE::HOUR; - } - - if ($time > DATE::MINUTE) - { - $parts[] = floor($time/DATE::MINUTE).'m'; - $time = $time % DATE::MINUTE; - } - - if ($time > 0) - { - $parts[] = round($time, 1).'s'; - } - - return implode(' ', $parts); - } -} // End Controller_PHPUnit diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/runner.php b/includes/kohana/modules/unittest/classes/kohana/unittest/runner.php deleted file mode 100644 index d85733f..0000000 --- a/includes/kohana/modules/unittest/classes/kohana/unittest/runner.php +++ /dev/null @@ -1,313 +0,0 @@ - - * @author Paul Banks - * @copyright (c) 2008-2009 Kohana Team - * @license http://kohanaphp.com/license - */ -class Kohana_Unittest_Runner implements PHPUnit_Framework_TestListener { - /** - * Results - * @var array - */ - protected $results = array( - 'errors' => array(), - 'failures' => array(), - 'skipped' => array(), - 'incomplete' => array(), - ); - - /** - * Test result totals - * @var array - */ - protected $totals = array( - 'tests' => 0, - 'passed' => 0, - 'errors' => 0, - 'failures' => 0, - 'skipped' => 0, - 'incomplete' => 0, - 'assertions' => 0, - ); - - /** - * Info about the current test running - * @var array - */ - protected $current = array(); - - /** - * Time for tests to run (seconds) - * @var float - */ - protected $time = 0; - - /** - * Result collector - * @var PHPUnit_Framework_TestResult - */ - protected $result = NULL; - - /** - * the test suite to run - * @var PHPUnit_Framework_TestSuite - */ - protected $suite = NULL; - - /** - * Constructor - * - * @param PHPUnit_Framework_TestSuite $suite The suite to test - * @param PHPUnit_Framework_TestResult $result Optional result object to use - */ - function __construct(PHPUnit_Framework_TestSuite $suite, PHPUnit_Framework_TestResult $result = NULL) - { - if ($result === NULL) - { - $result = new PHPUnit_Framework_TestResult; - } - - $result->addListener($this); - - $this->suite = $suite; - $this->result = $result; - } - - /** - * Magic getter to allow access to member variables - * - * @param string $var Variable to get - * @return mixed - */ - function __get($var) - { - return $this->$var; - } - - /** - * Calcualtes stats for each file covered by the code testing - * - * Each member of the returned array is formatted like so: - * - * - * array( - * 'coverage' => $coverage_percent_for_file, - * 'loc' => $lines_of_code, - * 'locExecutable' => $lines_of_executable_code, - * 'locExecuted' => $lines_of_code_executed - * ); - * - * - * @return array Statistics for code coverage of each file - */ - public function calculate_cc() - { - if ($this->result->getCollectCodeCoverageInformation()) - { - $coverage = $this->result->getCodeCoverageInformation(); - - $coverage_summary = PHPUnit_Util_CodeCoverage::getSummary($coverage); - - $stats = array(); - - foreach ($coverage_summary as $file => $_lines) - { - $stats[$file] = PHPUnit_Util_CodeCoverage::getStatistics($coverage_summary, $file); - } - - return $stats; - } - - return FALSE; - } - - /** - * Calculates the percentage code coverage information - * - * @return boolean|float FALSE if cc is not enabled, float for coverage percent - */ - public function calculate_cc_percentage() - { - if ($stats = $this->calculate_cc()) - { - $executable = 0; - $executed = 0; - - foreach ($stats as $stat) - { - $executable += $stat['locExecutable']; - $executed += $stat['locExecuted']; - } - - return ($executable > 0) ? ($executed * 100 / $executable) : 100; - } - - return FALSE; - } - - /** - * Generate a report using the specified $temp_path - * - * @param array $groups Groups to test - * @param string $temp_path Temporary path to use while generating report - */ - public function generate_report(array $groups, $temp_path, $create_sub_dir = TRUE) - { - if ( ! is_writable($temp_path)) - throw new Kohana_Exception('Temp path :path does not exist or is not writable by the webserver', array(':path' => $temp_path)); - - $folder_path = $temp_path; - - if ($create_sub_dir === TRUE) - { - // Icky, highly unlikely, but do it anyway - // Basically adds "(n)" to the end of the filename until there's a free file - $count = 0; - do - { - $folder_name = date('Y-m-d_H:i:s') - .(empty($groups) ? '' : ('['.implode(',', $groups).']')) - .(($count > 0) ? ('('.$count.')') : ''); - ++$count; - } - while (is_dir($folder_path.$folder_name)); - - $folder_path .= $folder_name; - - mkdir($folder_path, 0777); - } - else - { - $folder_name = basename($folder_path); - } - - $this->run($groups, TRUE); - - require_once 'PHPUnit/Runner/Version.php'; - require_once 'PHPUnit/Util/Report.php'; - - PHPUnit_Util_Report::render($this->result, $folder_path); - - return array($folder_path, $folder_name); - } - - /** - * Runs the test suite using the result specified in the constructor - * - * @param array $groups Optional array of groups to test - * @param bool $collect_cc Optional, Should code coverage be collected? - * @return Kohana_PHPUnit Instance of $this - */ - public function run(array $groups = array(), $collect_cc = FALSE) - { - if ($collect_cc AND ! extension_loaded('xdebug')) - throw new Kohana_Exception('Code coverage cannot be collected because the xdebug extension is not loaded'); - - $this->result->collectCodeCoverageInformation( (bool) $collect_cc); - - // Run the tests. - $this->suite->run($this->result, FALSE, $groups); - - return $this; - } - - // @codingStandardsIgnoreStart - public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) - // @codingStandardsIgnoreEnd - { - $this->totals['errors']++; - $this->current['result'] = 'errors'; - $this->current['message'] = $test->getStatusMessage(); - } - - // @codingStandardsIgnoreStart - public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) - // @codingStandardsIgnoreEnd - { - $this->totals['failures']++; - $this->current['result'] = 'failures'; - $this->current['message'] = $test->getStatusMessage(); - } - - // @codingStandardsIgnoreStart - public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) - // @codingStandardsIgnoreEnd - { - $this->totals['incomplete']++; - $this->current['result'] = 'incomplete'; - $this->current['message'] = $test->getStatusMessage(); - } - - // @codingStandardsIgnoreStart - public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) - // @codingStandardsIgnoreEnd - { - $this->totals['skipped']++; - $this->current['result'] = 'skipped'; - $this->current['message'] = $test->getStatusMessage(); - } - - // @codingStandardsIgnoreStart - public function startTest(PHPUnit_Framework_Test $test) - // @codingStandardsIgnoreEnd - { - $this->current['name'] = $test->getName(FALSE); - $this->current['description'] = $test->toString(); - $this->current['result'] = 'passed'; - } - - // @codingStandardsIgnoreStart - public function endTest(PHPUnit_Framework_Test $test, $time) - // @codingStandardsIgnoreEnd - { - // Add totals - $this->totals['tests']++; - $this->totals['assertions'] += $test->getNumAssertions(); - - // Handle passed tests - if ($this->current['result'] == 'passed') - { - // Add to total - $this->totals['passed']++; - } - else - { - // Add to results - $this->results[$this->current['result']][] = $this->current; - } - - $this->current = array(); - - $this->time += $time; - } - - // @codingStandardsIgnoreStart - public function startTestSuite(PHPUnit_Framework_TestSuite $suite) {} - // @codingStandardsIgnoreEnd - - // @codingStandardsIgnoreStart - public function endTestSuite(PHPUnit_Framework_TestSuite $suite) - // @codingStandardsIgnoreEnd - { - // Parse test descriptions to make them look nicer - foreach ($this->results as $case => $testresults) - { - foreach ($testresults as $type => $result) - { - preg_match('/^(?:([a-z0-9_]+?)::)?([a-z0-9_]+)(?: with data set (#\d+ \(.*?\)))?/i', $result['description'], $m); - - $this->results[$case][$type] += array( - 'class' => $m[1], - 'test' => $m[2], - 'data_set' => isset($m[3]) ? $m[3] : FALSE, - ); - } - } - } -} diff --git a/includes/kohana/modules/unittest/classes/unittest/runner.php b/includes/kohana/modules/unittest/classes/unittest/runner.php deleted file mode 100644 index 02544b2..0000000 --- a/includes/kohana/modules/unittest/classes/unittest/runner.php +++ /dev/null @@ -1,3 +0,0 @@ - Kohana::DEVELOPMENT, - - // This is the folder where we generate and zip all the reports for downloading - // Needs to be readable and writable - 'temp_path' => Kohana::$cache_dir.'/unittest', - - // Path from DOCROOT (i.e. http://yourdomain/) to the folder where HTML cc reports can be published. - // If you'd prefer not to allow users to do this then simply set the value to FALSE. - // Example value of 'cc_report_path' would allow devs to see report at http://yourdomain/report/ - 'cc_report_path' => 'report', - // If you don't use a whitelist then only files included during the request will be counted // If you do, then only whitelisted items will be counted 'use_whitelist' => TRUE, // Items to whitelist, only used in cli - // Web runner ui allows user to choose which items to whitelist 'whitelist' => array( // Should the app be whitelisted? @@ -46,8 +31,4 @@ return array( // List of individual files/folders to blacklist 'blacklist' => array( ), - - // A database connection that can be used when testing - // This doesn't overwrite anything, tests will have to use this value manually - 'db_connection' => 'unittest', ); diff --git a/includes/kohana/modules/unittest/config/userguide.php b/includes/kohana/modules/unittest/config/userguide.php index d809484..a286a26 100644 --- a/includes/kohana/modules/unittest/config/userguide.php +++ b/includes/kohana/modules/unittest/config/userguide.php @@ -1,23 +1,13 @@ - array( - - // This should be the path to this modules userguide pages, without the 'guide/'. Ex: '/guide/modulename/' would be 'modulename' 'unittest' => array( - - // Whether this modules userguide pages should be shown 'enabled' => TRUE, - - // The name that should show up on the userguide index page 'name' => 'Unittest', - - // A short description of this module, shown on the index page - 'description' => 'Kohana unit testing.', - - // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2010 Kohana Team', - ) + 'description' => 'Unit testing module', + 'copyright' => '© 2009-2011 Kohana Team', + ) ) ); \ No newline at end of file diff --git a/includes/kohana/modules/unittest/guide/unittest/index.md b/includes/kohana/modules/unittest/guide/unittest/index.md index fb594e2..9f7e314 100644 --- a/includes/kohana/modules/unittest/guide/unittest/index.md +++ b/includes/kohana/modules/unittest/guide/unittest/index.md @@ -1,2 +1,3 @@ -# Unittest +# UnitTest +Unit tests for Kohana \ No newline at end of file diff --git a/includes/kohana/modules/unittest/guide/unittest/menu.md b/includes/kohana/modules/unittest/guide/unittest/menu.md index dcc6dfb..1a707cc 100644 --- a/includes/kohana/modules/unittest/guide/unittest/menu.md +++ b/includes/kohana/modules/unittest/guide/unittest/menu.md @@ -1,5 +1,5 @@ ## [UnitTest]() - - [Testing](testing) - - [Mock Objects](mockobjects) - - [Troubleshooting](troubleshooting) - - [Testing workflows](testing_workflows) \ No newline at end of file + - [Mock Objects](mockobjects) + - [Testing](testing) + - [Testing workflows](testing_workflows) + - [Troubleshooting](troubleshooting) diff --git a/includes/kohana/modules/unittest/guide/unittest/testing.md b/includes/kohana/modules/unittest/guide/unittest/testing.md index 504478a..41682f8 100644 --- a/includes/kohana/modules/unittest/guide/unittest/testing.md +++ b/includes/kohana/modules/unittest/guide/unittest/testing.md @@ -1,12 +1,12 @@ -### From the command line +# Usage - $ phpunit --bootstrap=index.php modules/unittest/tests.php + $ phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php -Of course, you'll need to make sure the path to the tests.php file is correct. If you want you can copy it to a more accessible location +Alternatively you can use a phpunit.xml to have a more fine grained control over which tests are included and which files are whitelisted. -### From the web +Make sure you only whitelist the highest files in the cascading filesystem, else you could end up with a lot of "class cannot be redefined" errors. -Just navigate to http://example.com/unittest. You may need to use http://example.com/index.php/unittest if you have not enabled url rewriting in your .htaccess. +If you use the tests.php testsuite loader then it will only whitelist the highest files. see config/unittest.php for details on configuring the tests.php whitelist. ## Writing tests @@ -103,13 +103,13 @@ This functionality can be used to record which bug reports a test is for: To see all groups that are available in your code run: - $ phpunit --boostrap=index.php --list-groups modules/unittest/tests.php + $ phpunit --boostrap=modules/unittest/bootstrap.php --list-groups modules/unittest/tests.php *Note:* the `--list-groups` switch should appear before the path to the test suite loader You can also exclude groups while testing using the `--exclude-group` switch. This can be useful if you want to ignore all kohana tests: - $ phpunit --bootstrap=index.php --exclude-group=kohana modules/unittest/tests.php + $ phpunit --bootstrap=modules/unittest/bootstrap.php --exclude-group=kohana modules/unittest/tests.php For more info see: diff --git a/includes/kohana/modules/unittest/guide/unittest/testing_workflows.md b/includes/kohana/modules/unittest/guide/unittest/testing_workflows.md index fa98cee..893ab62 100644 --- a/includes/kohana/modules/unittest/guide/unittest/testing_workflows.md +++ b/includes/kohana/modules/unittest/guide/unittest/testing_workflows.md @@ -2,16 +2,6 @@ Having unittests for your application is a nice idea, but unless you actually use them they're about as useful as a chocolate firegaurd. There are quite a few ways of getting tests "into" your development process and this guide aims to cover a few of them. -## Testing through the webui - -The web ui is a fairly temporary solution, aimed at helping developers get into unittesting and code coverage. Eventually it's hoped that people migrate on to the termainl & CI servers. - -To access it goto - - http://example.com/unittest/ - -*Note:* Your site will need to be in the correct environment in order to use the webui. See the config file for more details. You may also need to use http://example.com/index.php/unittest/ - ## Integrating with IDEs Modern IDEs have come a long way in the last couple of years and ones like netbeans have pretty decent PHP / PHPUnit support. diff --git a/includes/kohana/modules/unittest/init.php b/includes/kohana/modules/unittest/init.php deleted file mode 100644 index c40ba42..0000000 --- a/includes/kohana/modules/unittest/init.php +++ /dev/null @@ -1,16 +0,0 @@ -)') - ->defaults(array( - 'controller' => 'unittest', - 'action' => 'index', - )); diff --git a/includes/kohana/modules/unittest/tests.php b/includes/kohana/modules/unittest/tests.php index dfc21cd..4421fc9 100644 --- a/includes/kohana/modules/unittest/tests.php +++ b/includes/kohana/modules/unittest/tests.php @@ -5,16 +5,16 @@ if ( ! class_exists('Kohana')) die('Please include the kohana bootstrap file (see README.markdown)'); } -if ($file = Kohana::find_file('classes', 'unittest/tests')) +if ($file = Kohana::find_file('classes', 'Unittest/Tests')) { require_once $file; // PHPUnit requires a test suite class to be in this file, // so we create a faux one that uses the kohana base - Class TestSuite extends Unittest_Tests + class TestSuite extends Unittest_Tests {} } else { die('Could not include the test suite'); -} \ No newline at end of file +} diff --git a/includes/kohana/modules/unittest/views/unittest/index.php b/includes/kohana/modules/unittest/views/unittest/index.php deleted file mode 100644 index b4750fd..0000000 --- a/includes/kohana/modules/unittest/views/unittest/index.php +++ /dev/null @@ -1,66 +0,0 @@ - - -
-

PHPUnit for Kohana 3

-
-
- Run Tests - 'GET'));?> - - - 'run_group'));?> - - - - 'run_collect_cc')) ?> - -
- - 'run_use_whitelist')) ?> - -
- - 'run_whitelist', 'multiple' => 'multiple')) ?> -
- -
- - - - - -
- -
- Code Coverage Reports - -

- - 'GET')) ?> - - - 'cc_group'));?> - - - 'report_archive')) ?> - - - 'report_use_whitelist')) ?> - -
- - 'run_whitelist', 'multiple' => 'multiple')) ?> -
- - - - -
-
- -

Useful links

- -
diff --git a/includes/kohana/modules/unittest/views/unittest/layout.php b/includes/kohana/modules/unittest/views/unittest/layout.php deleted file mode 100644 index 5132c70..0000000 --- a/includes/kohana/modules/unittest/views/unittest/layout.php +++ /dev/null @@ -1,255 +0,0 @@ - - - - PHPUnit for Kohana - - - - - - - - - - - - diff --git a/includes/kohana/modules/unittest/views/unittest/results.php b/includes/kohana/modules/unittest/views/unittest/results.php deleted file mode 100644 index 33b15f0..0000000 --- a/includes/kohana/modules/unittest/views/unittest/results.php +++ /dev/null @@ -1,83 +0,0 @@ - - - -
- -
No tests in group
- -
Tests Passed
- - - $tests):?> - - -
-

- - []

-
    - -
  1. - :: - - - - -
  2. - -
-
- - -
diff --git a/includes/kohana/modules/userguide/README.md b/includes/kohana/modules/userguide/README.md index 1740174..f79888d 100644 --- a/includes/kohana/modules/userguide/README.md +++ b/includes/kohana/modules/userguide/README.md @@ -30,42 +30,6 @@ Any images used in the userguide pages must be in `media/guide//`. For The API browser is generated from the actual source code. The descriptions for classes, constants, properties, and methods is extracted from the comments and parsed in Markdown. For example if you look in the comment for [Kohana_Core::init](http://github.com/kohana/core/blob/c443c44922ef13421f4a/classes/kohana/core.php#L5) you can see a markdown list and table. These are parsed and show correctly in the API browser. `@param`, `@uses`, `@throws`, `@returns` and other tags are parsed as well. -## How to Contribute - -### If you don't know git, or you don't feel like you are a good documentation writer: - -Just submit a [bug report](http://dev.kohanaframework.org/projects/userguide3/issues/new) and explain what you think can be improved. If you are a good writer but don't know git, just provide some content in your bug report and we will merge it in. - -### If you know git: - -**Short version**: Create a ticket on redmine for your changes, fork the module whose docs you wish to improve (e.g. `git://github.com/kohana/orm.git` or `git://github.com/kohana/core.git`), checkout the appropriate branch, make changes, and then send a pull request with the ticket number. - -**Long version:** (This still assumes you at least know your way around git, especially how submodules work.) - - 1. Create a ticket on redmine for your changes. - - 2. Fork the specific repo you want to contribute to on github. (For example go to http://github.com/kohana/core and click the fork button.) - - 3. Now go into the repo of the area of docs you want to contribute to and add your forked repo as a new remote, and push to it. - - cd system - - # make sure we are up to date - git checkout 3.1/develop - git pull - - # add your repository as a new remote - git remote add git@github.com:/core.git - - # (make some changes to the docs) - - # now commit the changes and push to your repo - git commit - git push 3.1/develop - - 4. Send a pull request on github containing the ticket number, and update the ticket with a link to the pull request. - - # What the userguide adds to markdown: In addition to the features and syntax of [Markdown](http://daringfireball.net/projects/markdown/) and [Markdown Extra](http://michelf.com/projects/php-markdown/extra/) the following apply to userguide pages and api documentation: @@ -96,10 +60,10 @@ You can make links to the api browser by wrapping any class name in brackets. Y If you want to have parameters, only put the brackets around the class and function (not the params), and put a backslash in front of the opening parenthesis. - [Kohana::config]\('foobar','baz') + [Kohana::$config]\('foobar','baz') ### Including Views You may include a view by putting the name of the view in double curly brackets. **If the view is not found, no exception or error will be shown!** The curly brackets and view will simply be shown an the page as is. - {{some/view}} + {{some/view}} \ No newline at end of file diff --git a/includes/kohana/modules/userguide/classes/Controller/Userguide.php b/includes/kohana/modules/userguide/classes/Controller/Userguide.php new file mode 100644 index 0000000..e319614 --- /dev/null +++ b/includes/kohana/modules/userguide/classes/Controller/Userguide.php @@ -0,0 +1,3 @@ +request->action() === 'media') { // Do not template media files @@ -28,28 +30,15 @@ class Controller_Userguide extends Controller_Template { $this->media = Route::get('docs/media'); $this->guide = Route::get('docs/guide'); - if (defined('MARKDOWN_PARSER_CLASS')) - { - throw new Kohana_Exception('Markdown parser already registered. Live documentation will not work in your environment.'); - } - - // Use customized Markdown parser - define('MARKDOWN_PARSER_CLASS', 'Kodoc_Markdown'); - - if ( ! class_exists('Markdown', FALSE)) - { - // Load Markdown support - require Kohana::find_file('vendor', 'markdown/markdown'); - } - // Set the base URL for links and images Kodoc_Markdown::$base_url = URL::site($this->guide->uri()).'/'; Kodoc_Markdown::$image_url = URL::site($this->media->uri()).'/'; } - parent::before(); + // Default show_comments to config value + $this->template->show_comments = Kohana::$config->load('userguide.show_comments'); } - + // List all modules that have userguides public function index() { @@ -57,32 +46,32 @@ class Controller_Userguide extends Controller_Template { $this->template->breadcrumb = array('User Guide'); $this->template->content = View::factory('userguide/index', array('modules' => $this->_modules())); $this->template->menu = View::factory('userguide/menu', array('modules' => $this->_modules())); - + // Don't show disqus on the index page - $this->template->hide_disqus = TRUE; + $this->template->show_comments = FALSE; } - + // Display an error if a page isn't found public function error($message) { $this->response->status(404); $this->template->title = "Userguide - Error"; $this->template->content = View::factory('userguide/error',array('message' => $message)); - + // Don't show disqus on error pages - $this->template->hide_disqus = TRUE; + $this->template->show_comments = FALSE; // If we are in a module and that module has a menu, show that - if ($module = $this->request->param('module') AND $menu = $this->file($module.'/menu') AND Kohana::config('userguide.modules.'.$module.'.enabled')) + if ($module = $this->request->param('module') AND $menu = $this->file($module.'/menu') AND Kohana::$config->load('userguide.modules.'.$module.'.enabled')) { // Namespace the markdown parser Kodoc_Markdown::$base_url = URL::site($this->guide->uri()).'/'.$module.'/'; Kodoc_Markdown::$image_url = URL::site($this->media->uri()).'/'.$module.'/'; - $this->template->menu = Markdown($this->_get_all_menu_markdown()); + $this->template->menu = Kodoc_Markdown::markdown($this->_get_all_menu_markdown()); $this->template->breadcrumb = array( $this->guide->uri() => 'User Guide', - $this->guide->uri(array('module' => $module)) => Kohana::config('userguide.modules.'.$module.'.name'), + $this->guide->uri(array('module' => $module)) => Kohana::$config->load('userguide.modules.'.$module.'.name'), 'Error' ); } @@ -119,19 +108,19 @@ class Controller_Userguide extends Controller_Template { { return $this->index(); } - + // If this module's userguide pages are disabled, show the error page - if ( ! Kohana::config('userguide.modules.'.$module.'.enabled')) + if ( ! Kohana::$config->load('userguide.modules.'.$module.'.enabled')) { - return $this->error(__('That module doesn\'t exist, or has userguide pages disabled.')); + return $this->error('That module doesn\'t exist, or has userguide pages disabled.'); } - + // Prevent "guide/module" and "guide/module/index" from having duplicate content if ( $page == 'index') { - return $this->error(__('Userguide page not found')); + return $this->error('Userguide page not found'); } - + // If a module is set, but no page was provided in the url, show the index page if ( ! $page ) { @@ -144,37 +133,37 @@ class Controller_Userguide extends Controller_Template { // If it's not found, show the error page if ( ! $file) { - return $this->error(__('Userguide page not found')); + return $this->error('Userguide page not found'); } - + // Namespace the markdown parser Kodoc_Markdown::$base_url = URL::site($this->guide->uri()).'/'.$module.'/'; Kodoc_Markdown::$image_url = URL::site($this->media->uri()).'/'.$module.'/'; // Set the page title - $this->template->title = $page == 'index' ? Kohana::config('userguide.modules.'.$module.'.name') : $this->title($page); + $this->template->title = $page == 'index' ? Kohana::$config->load('userguide.modules.'.$module.'.name') : $this->title($page); // Parse the page contents into the template Kodoc_Markdown::$show_toc = true; - $this->template->content = Markdown(file_get_contents($file)); + $this->template->content = Kodoc_Markdown::markdown(file_get_contents($file)); Kodoc_Markdown::$show_toc = false; // Attach this module's menu to the template - $this->template->menu = Markdown($this->_get_all_menu_markdown()); + $this->template->menu = Kodoc_Markdown::markdown($this->_get_all_menu_markdown()); // Bind the breadcrumb $this->template->bind('breadcrumb', $breadcrumb); - + // Bind the copyright - $this->template->copyright = Kohana::config('userguide.modules.'.$module.'.copyright'); + $this->template->copyright = Kohana::$config->load('userguide.modules.'.$module.'.copyright'); // Add the breadcrumb trail $breadcrumb = array(); - $breadcrumb[$this->guide->uri()] = __('User Guide'); - $breadcrumb[$this->guide->uri(array('module' => $module))] = Kohana::config('userguide.modules.'.$module.'.name'); - + $breadcrumb[$this->guide->uri()] = 'User Guide'; + $breadcrumb[$this->guide->uri(array('module' => $module))] = Kohana::$config->load('userguide.modules.'.$module.'.name'); + // TODO try and get parent category names (from menu). Regex magic or javascript dom stuff perhaps? - + // Only add the current page title to breadcrumbs if it isn't the index, otherwise we get repeats. if ($page != 'index') { @@ -194,7 +183,7 @@ class Controller_Userguide extends Controller_Template { // If no class was passed to the url, display the API index page if ( ! $class) { - $this->template->title = __('Table of Contents'); + $this->template->title = 'Table of Contents'; $this->template->content = View::factory('userguide/api/toc') ->set('classes', Kodoc::class_methods()) @@ -204,7 +193,7 @@ class Controller_Userguide extends Controller_Template { { // Create the Kodoc_Class version of this class. $_class = Kodoc_Class::factory($class); - + // If the class requested and the actual class name are different // (different case, orm vs ORM, auth vs Auth) redirect if ($_class->class->name != $class) @@ -219,12 +208,12 @@ class Controller_Userguide extends Controller_Template { // If this classes package has been disabled via the config, 404 if ( ! Kodoc::show_class($_class)) return $this->error('That class is in package that is hidden. Check the api_packages config setting.'); - + // Everything is fine, display the class. $this->template->title = $class; $this->template->content = View::factory('userguide/api/class') - ->set('doc', Kodoc::factory($class)) + ->set('doc', $_class) ->set('route', $this->request->route()); } @@ -234,12 +223,9 @@ class Controller_Userguide extends Controller_Template { // Bind the breadcrumb $this->template->bind('breadcrumb', $breadcrumb); - // Get the docs URI - $guide = Route::get('docs/guide'); - // Add the breadcrumb $breadcrumb = array(); - $breadcrumb[$this->guide->uri(array('page' => NULL))] = __('User Guide'); + $breadcrumb[$this->guide->uri(array('page' => NULL))] = 'User Guide'; $breadcrumb[$this->request->route()->uri()] = 'API Browser'; $breadcrumb[] = $this->template->title; } @@ -258,8 +244,8 @@ class Controller_Userguide extends Controller_Template { if ($file = Kohana::find_file('media/guide', $file, $ext)) { // Check if the browser sent an "if-none-match: " header, and tell if the file hasn't changed - $this->response->check_cache(sha1($this->request->uri()).filemtime($file), $this->request); - + $this->check_cache(sha1($this->request->uri()).filemtime($file)); + // Send the file content as the response $this->response->body(file_get_contents($file)); @@ -307,83 +293,105 @@ class Controller_Userguide extends Controller_Template { return parent::after(); } + /** + * Locates the appropriate markdown file for a given guide page. Page URLS + * can be specified in one of three forms: + * + * * userguide/adding + * * userguide/adding.md + * * userguide/adding.markdown + * + * In every case, the userguide will search the cascading file system paths + * for the file guide/userguide/adding.md. + * + * @param string $page The relative URL of the guide page + * @return string + */ public function file($page) { + + // Strip optional .md or .markdown suffix from the passed filename + $info = pathinfo($page); + if (isset($info['extension']) + AND (($info['extension'] === 'md') OR ($info['extension'] === 'markdown'))) + { + $page = $info['dirname'].DIRECTORY_SEPARATOR.$info['filename']; + } return Kohana::find_file('guide', $page, 'md'); } public function section($page) { $markdown = $this->_get_all_menu_markdown(); - + if (preg_match('~\*{2}(.+?)\*{2}[^*]+\[[^\]]+\]\('.preg_quote($page).'\)~mu', $markdown, $matches)) { return $matches[1]; } - + return $page; } public function title($page) { $markdown = $this->_get_all_menu_markdown(); - + if (preg_match('~\[([^\]]+)\]\('.preg_quote($page).'\)~mu', $markdown, $matches)) { // Found a title for this link return $matches[1]; } - + return $page; } - + protected function _get_all_menu_markdown() { // Only do this once per request... static $markdown = ''; - + if (empty($markdown)) { // Get menu items $file = $this->file($this->request->param('module').'/menu'); - + if ($file AND $text = file_get_contents($file)) { // Add spans around non-link categories. This is a terrible hack. - //echo Kohana::debug($text); - + //echo Debug::vars($text); + //$text = preg_replace('/(\s*[\-\*\+]\s*)(.*)/','$1$2',$text); $text = preg_replace('/^(\s*[\-\*\+]\s*)([^\[\]]+)$/m','$1$2',$text); - //echo Kohana::debug($text); + //echo Debug::vars($text); $markdown .= $text; } - + } - + return $markdown; } - + // Get the list of modules from the config, and reverses it so it displays in the order the modules are added, but move Kohana to the top. protected function _modules() { - $modules = array_reverse(Kohana::config('userguide.modules')); - + $modules = array_reverse(Kohana::$config->load('userguide.modules')); + if (isset($modules['kohana'])) { $kohana = $modules['kohana']; unset($modules['kohana']); $modules = array_merge(array('kohana' => $kohana), $modules); } - + // Remove modules that have been disabled via config foreach ($modules as $key => $value) { - if ( ! Kohana::config('userguide.modules.'.$key.'.enabled')) + if ( ! Kohana::$config->load('userguide.modules.'.$key.'.enabled')) { unset($modules[$key]); } } - + return $modules; } diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc.php b/includes/kohana/modules/userguide/classes/Kohana/Kodoc.php similarity index 52% rename from includes/kohana/modules/userguide/classes/kohana/kodoc.php rename to includes/kohana/modules/userguide/classes/Kohana/Kodoc.php index 8bdd9dc..acd9cbc 100644 --- a/includes/kohana/modules/userguide/classes/kohana/kodoc.php +++ b/includes/kohana/modules/userguide/classes/Kohana/Kodoc.php @@ -5,7 +5,7 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2008-2009 Kohana Team + * @copyright (c) 2008-2012 Kohana Team * @license http://kohanaphp.com/license */ class Kohana_Kodoc { @@ -57,15 +57,6 @@ class Kohana_Kodoc { { $classes = Kodoc::classes(); - foreach ($classes as $class) - { - if (isset($classes['kohana_'.$class])) - { - // Remove extended classes - unset($classes['kohana_'.$class]); - } - } - ksort($classes); $menu = array(); @@ -74,6 +65,9 @@ class Kohana_Kodoc { foreach ($classes as $class) { + if (Kodoc::is_transparent($class, $classes)) + continue; + $class = Kodoc_Class::factory($class); // Test if we should show this class @@ -113,9 +107,7 @@ class Kohana_Kodoc { } /** - * Returns an array of all the classes available, built by listing all files in the classes folder and then trying to create that class. - * - * This means any empty class files (as in complety empty) will cause an exception + * Returns an array of all the classes available, built by listing all files in the classes folder. * * @param array array of files, obtained using Kohana::list_files * @return array an array of all the class names @@ -129,19 +121,22 @@ class Kohana_Kodoc { $classes = array(); + // This will be used a lot! + $ext_length = strlen(EXT); + foreach ($list as $name => $path) { if (is_array($path)) { $classes += Kodoc::classes($path); } - else + elseif (substr($name, -$ext_length) === EXT) { // Remove "classes/" and the extension - $class = substr($name, 8, -(strlen(EXT))); + $class = substr($name, 8, -$ext_length); // Convert slashes to underscores - $class = str_replace(DIRECTORY_SEPARATOR, '_', strtolower($class)); + $class = str_replace(DIRECTORY_SEPARATOR, '_', $class); $classes[$class] = $class; } @@ -166,13 +161,11 @@ class Kohana_Kodoc { foreach ($list as $class) { - $_class = new ReflectionClass($class); - - if (stripos($_class->name, 'Kohana_') === 0) - { - // Skip transparent extension classes + // Skip transparent extension classes + if (Kodoc::is_transparent($class)) continue; - } + + $_class = new ReflectionClass($class); $methods = array(); @@ -180,10 +173,10 @@ class Kohana_Kodoc { { $declares = $_method->getDeclaringClass()->name; - if (stripos($declares, 'Kohana_') === 0) + // Remove the transparent prefix from declaring classes + if ($child = Kodoc::is_transparent($declares)) { - // Remove "Kohana_" - $declares = substr($declares, 7); + $declares = $child; } if ($declares === $_class->name OR $declares === "Core") @@ -200,93 +193,149 @@ class Kohana_Kodoc { return $classes; } + /** + * Generate HTML for the content of a tag. + * + * @param string $tag Name of the tag without @ + * @param string $text Content of the tag + * @return string HTML + */ + public static function format_tag($tag, $text) + { + if ($tag === 'license') + { + if (strpos($text, '://') !== FALSE) + return HTML::anchor($text); + } + elseif ($tag === 'link') + { + $split = preg_split('/\s+/', $text, 2); + + return HTML::anchor( + $split[0], + isset($split[1]) ? $split[1] : $split[0] + ); + } + elseif ($tag === 'copyright') + { + // Convert the copyright symbol + return str_replace('(c)', '©', $text); + } + elseif ($tag === 'throws') + { + $route = Route::get('docs/api'); + + if (preg_match('/^(\w+)\W(.*)$/D', $text, $matches)) + { + return HTML::anchor( + $route->uri(array('class' => $matches[1])), + $matches[1] + ).' '.$matches[2]; + } + + return HTML::anchor( + $route->uri(array('class' => $text)), + $text + ); + } + elseif ($tag === 'see' OR $tag === 'uses') + { + if (preg_match('/^'.Kodoc::$regex_class_member.'/', $text, $matches)) + return Kodoc::link_class_member($matches); + } + + return $text; + } + /** * Parse a comment to extract the description and the tags * - * @param string the comment retreived using ReflectionClass->getDocComment() + * [!!] Converting the output to HTML in this method is deprecated in 3.3 + * + * @param string $comment The DocBlock to parse + * @param boolean $html Whether or not to convert the return values + * to HTML (deprecated) * @return array array(string $description, array $tags) */ - public static function parse($comment) + public static function parse($comment, $html = TRUE) { // Normalize all new lines to \n $comment = str_replace(array("\r\n", "\n"), "\n", $comment); - // Remove the phpdoc open/close tags and split - $comment = array_slice(explode("\n", $comment), 1, -1); + // Split into lines while capturing without leading whitespace + preg_match_all('/^\s*\* ?(.*)\n/m', $comment, $lines); // Tag content $tags = array(); - foreach ($comment as $i => $line) + /** + * Process a tag and add it to $tags + * + * @param string $tag Name of the tag without @ + * @param string $text Content of the tag + * @return void + */ + $add_tag = function($tag, $text) use ($html, &$tags) { - // Remove all leading whitespace - $line = preg_replace('/^\s*\* ?/m', '', $line); - - // Search this line for a tag - if (preg_match('/^@(\S+)(?:\s*(.+))?$/', $line, $matches)) + // Don't show @access lines, they are shown elsewhere + if ($tag !== 'access') { - // This is a tag line - unset($comment[$i]); - - $name = $matches[1]; - $text = isset($matches[2]) ? $matches[2] : ''; - - switch ($name) + if ($html) { - case 'license': - if (strpos($text, '://') !== FALSE) - { - // Convert the lincense into a link - $text = HTML::anchor($text); - } - break; - case 'link': - $text = preg_split('/\s+/', $text, 2); - $text = HTML::anchor($text[0], isset($text[1]) ? $text[1] : $text[0]); - break; - case 'copyright': - if (strpos($text, '(c)') !== FALSE) - { - // Convert the copyright sign - $text = str_replace('(c)', '©', $text); - } - break; - case 'throws': - if (preg_match('/^(\w+)\W(.*)$/', $text, $matches)) - { - $text = HTML::anchor(Route::get('docs/api')->uri(array('class' => $matches[1])), $matches[1]).' '.$matches[2]; - } - else - { - $text = HTML::anchor(Route::get('docs/api')->uri(array('class' => $text)), $text); - } - break; - case 'uses': - if (preg_match('/^'.Kodoc::$regex_class_member.'$/i', $text, $matches)) - { - $text = Kodoc::link_class_member($matches); - } - break; - // Don't show @access lines, they are shown elsewhere - case 'access': - continue 2; + $text = Kodoc::format_tag($tag, $text); } // Add the tag - $tags[$name][] = $text; + $tags[$tag][] = $text; + } + }; + + $comment = $tag = null; + $end = count($lines[1]) - 1; + + foreach ($lines[1] as $i => $line) + { + // Search this line for a tag + if (preg_match('/^@(\S+)\s*(.+)?$/', $line, $matches)) + { + if ($tag) + { + // Previous tag is finished + $add_tag($tag, $text); + } + + $tag = $matches[1]; + $text = isset($matches[2]) ? $matches[2] : ''; + + if ($i === $end) + { + // No more lines + $add_tag($tag, $text); + } + } + elseif ($tag) + { + // This is the continuation of the previous tag + $text .= "\n".$line; + + if ($i === $end) + { + // No more lines + $add_tag($tag, $text); + } } else { - // Overwrite the comment line - $comment[$i] = (string) $line; + $comment .= "\n".$line; } } - // Concat the comment lines back to a block of text - if ($comment = trim(implode("\n", $comment))) + $comment = trim($comment, "\n"); + + if ($comment AND $html) { // Parse the comment with Markdown - $comment = Markdown($comment); + $comment = Kodoc_Markdown::markdown($comment); } return array($comment, $tags); @@ -328,7 +377,7 @@ class Kohana_Kodoc { */ public static function show_class(Kodoc_Class $class) { - $api_packages = Kohana::config('userguide.api_packages'); + $api_packages = Kohana::$config->load('userguide.api_packages'); // If api_packages is true, all packages should be shown if ($api_packages === TRUE) @@ -350,5 +399,68 @@ class Kohana_Kodoc { return $show_this; } + /** + * Checks whether a class is a transparent extension class or not. + * + * This method takes an optional $classes parameter, a list of all defined + * class names. If provided, the method will return false unless the extension + * class exists. If not, the method will only check known transparent class + * prefixes. + * + * Transparent prefixes are defined in the userguide.php config file: + * + * 'transparent_prefixes' => array( + * 'Kohana' => TRUE, + * ); + * + * Module developers can therefore add their own transparent extension + * namespaces and exclude them from the userguide. + * + * @param string $class The name of the class to check for transparency + * @param array $classes An optional list of all defined classes + * @return false If this is not a transparent extension class + * @return string The name of the class that extends this (in the case provided) + * @throws InvalidArgumentException If the $classes array is provided and the $class variable is not lowercase + */ + public static function is_transparent($class, $classes = NULL) + { + + static $transparent_prefixes = NULL; + + if ( ! $transparent_prefixes) + { + $transparent_prefixes = Kohana::$config->load('userguide.transparent_prefixes'); + } + + // Split the class name at the first underscore + $segments = explode('_',$class,2); + + if ((count($segments) == 2) AND (isset($transparent_prefixes[$segments[0]]))) + { + if ($segments[1] === 'Core') + { + // Cater for Module extends Module_Core naming + $child_class = $segments[0]; + } + else + { + // Cater for Foo extends Module_Foo naming + $child_class = $segments[1]; + } + + // It is only a transparent class if the unprefixed class also exists + if ($classes AND ! isset($classes[$child_class])) + return FALSE; + + // Return the name of the child class + return $child_class; + } + else + { + // Not a transparent class + return FALSE; + } + } + } // End Kodoc diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/class.php b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Class.php similarity index 69% rename from includes/kohana/modules/userguide/classes/kohana/kodoc/class.php rename to includes/kohana/modules/userguide/classes/Kohana/Kodoc/Class.php index c6b182d..2c2f2f2 100644 --- a/includes/kohana/modules/userguide/classes/kohana/kodoc/class.php +++ b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Class.php @@ -5,7 +5,7 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ class Kohana_Kodoc_Class extends Kodoc { @@ -35,6 +35,11 @@ class Kohana_Kodoc_Class extends Kodoc { */ public $constants = array(); + /** + * @var array Parent classes/interfaces of this class/interface + */ + public $parents = array(); + /** * Loads a class and uses [reflection](http://php.net/reflection) to parse * the class. Reads the class modifiers, constants and comment. Parses the @@ -52,43 +57,80 @@ class Kohana_Kodoc_Class extends Kodoc { $this->modifiers = ''.implode(' ', Reflection::getModifierNames($modifiers)).' '; } - if ($constants = $this->class->getConstants()) + $this->constants = $this->class->getConstants(); + + // If ReflectionClass::getParentClass() won't work if the class in + // question is an interface + if ($this->class->isInterface()) { - foreach ($constants as $name => $value) + $this->parents = $this->class->getInterfaces(); + } + else + { + $parent = $this->class; + + while ($parent = $parent->getParentClass()) { - $this->constants[$name] = Debug::vars($value); + $this->parents[] = $parent; } } - $parent = $this->class; - - do + if ( ! $comment = $this->class->getDocComment()) { - if ($comment = $parent->getDocComment()) + foreach ($this->parents as $parent) { - // Found a description for this class - break; + if ($comment = $parent->getDocComment()) + { + // Found a description for this class + break; + } } } - while ($parent = $parent->getParentClass()); - list($this->description, $this->tags) = Kodoc::parse($comment); - + list($this->description, $this->tags) = Kodoc::parse($comment, FALSE); + } + + /** + * Gets the constants of this class as HTML. + * + * @return array + */ + public function constants() + { + $result = array(); + + foreach ($this->constants as $name => $value) + { + $result[$name] = Debug::vars($value); + } + + return $result; + } + + /** + * Get the description of this class as HTML. Includes a warning when the + * class or one of its parents could not be found. + * + * @return string HTML + */ + public function description() + { + $result = $this->description; + // If this class extends Kodoc_Missing, add a warning about possible // incomplete documentation - $parent = $this->class; - - while ($parent = $parent->getParentClass()) + foreach ($this->parents as $parent) { if ($parent->name == 'Kodoc_Missing') { - $warning = "[!!] **This class, or a class parent, could not be + $result .= "[!!] **This class, or a class parent, could not be found or loaded. This could be caused by a missing - module or other dependancy. The documentation for - class may not be complete!**"; - $this->description = Markdown($warning).$this->description; + module or other dependancy. The documentation for + class may not be complete!**"; } } + + return Kodoc_Markdown::markdown($result); } /** @@ -100,12 +142,14 @@ class Kohana_Kodoc_Class extends Kodoc { { $props = $this->class->getProperties(); + $defaults = $this->class->getDefaultProperties(); + usort($props, array($this,'_prop_sort')); foreach ($props as $key => $property) { // Create Kodoc Properties for each property - $props[$key] = new Kodoc_Property($this->class->name, $property->name); + $props[$key] = new Kodoc_Property($this->class->name, $property->name, Arr::get($defaults, $property->name)); } return $props; @@ -174,7 +218,7 @@ class Kohana_Kodoc_Class extends Kodoc { /* - echo kohana::debug('a is '.$a->class.'::'.$a->name,'b is '.$b->class.'::'.$b->name, + echo Debug::vars('a is '.$a->class.'::'.$a->name,'b is '.$b->class.'::'.$b->name, 'are the classes the same?', $a->class == $b->class,'if they are, the result is:',strcmp($a->name, $b->name), 'is a this class?', $a->name == $this->class->name,-1, 'is b this class?', $b->name == $this->class->name,1, @@ -213,4 +257,23 @@ class Kohana_Kodoc_Class extends Kodoc { return $bdepth - $adepth; } -} // End Kodac_Class + /** + * Get the tags of this class as HTML. + * + * @return array + */ + public function tags() + { + $result = array(); + + foreach ($this->tags as $name => $set) + { + foreach ($set as $text) + { + $result[$name][] = Kodoc::format_tag($name, $text); + } + } + + return $result; + } +} diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/markdown.php b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Markdown.php similarity index 86% rename from includes/kohana/modules/userguide/classes/kohana/kodoc/markdown.php rename to includes/kohana/modules/userguide/classes/Kohana/Kodoc/Markdown.php index 3562126..856be95 100644 --- a/includes/kohana/modules/userguide/classes/kohana/kodoc/markdown.php +++ b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Markdown.php @@ -5,7 +5,7 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { @@ -38,6 +38,26 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { */ public static $show_toc = false; + /** + * Transform some text using [Kodoc_Markdown] + * + * @see Markdown() + * + * @param string Text to parse + * @return string Transformed text + */ + public static function markdown($text) + { + static $instance; + + if ($instance === NULL) + { + $instance = new Kodoc_Markdown; + } + + return $instance->transform($text); + } + public function __construct() { // doImage is 10, add image url just before @@ -107,7 +127,7 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { $attr = ' id="'.$this->make_heading_id($matches[2]).'"'; // Add this header to the page toc - $this->_add_to_toc($level,$matches[2],$this->make_heading_id($matches[2])); + $this->_add_to_toc($level, $matches[2], $this->make_heading_id(empty($matches[3]) ? $matches[2] : $matches[3])); $block = "".$this->runSpanGamut($matches[2]).""; return "\n" . $this->hashBlock($block) . "\n\n"; @@ -152,18 +172,23 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { { list($search, $view) = $set; - try + if (Kohana::find_file('views', $view)) { - $replace[$search] = View::factory($view)->render(); - } - catch (Exception $e) - { - ob_start(); + try + { + $replace[$search] = View::factory($view)->render(); + } + catch (Exception $e) + { + /** + * Capture the exception handler output and insert it instead. + * + * NOTE: Is this really the correct way to handle an exception? + */ + $response = Kohana_exception::_handler($e); - // Capture the exception handler output and insert it instead - Kohana_exception::handler($e); - - $replace[$search] = ob_get_clean(); + $replace[$search] = $response->body(); + } } } diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/method.php b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Method.php similarity index 98% rename from includes/kohana/modules/userguide/classes/kohana/kodoc/method.php rename to includes/kohana/modules/userguide/classes/Kohana/Kodoc/Method.php index 025055f..2e673d4 100644 --- a/includes/kohana/modules/userguide/classes/kohana/kodoc/method.php +++ b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Method.php @@ -68,7 +68,7 @@ class Kohana_Kodoc_Method extends Kodoc { if (isset($tags['param'][$i])) { - preg_match('/^(\S+)(?:\s*(?:\$'.$param->name.'\s*)?(.+))?$/', $tags['param'][$i], $matches); + preg_match('/^(\S+)(?:\s*(?:\$'.$param->name.'\s*)?(.+))?$/s', $tags['param'][$i], $matches); $param->type = $matches[1]; @@ -138,4 +138,4 @@ class Kohana_Kodoc_Method extends Kodoc { return $out; } -} // End Kodoc_Method \ No newline at end of file +} // End Kodoc_Method diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/method/param.php b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Method/Param.php similarity index 93% rename from includes/kohana/modules/userguide/classes/kohana/kodoc/method/param.php rename to includes/kohana/modules/userguide/classes/Kohana/Kodoc/Method/Param.php index 33ff592..7b5976f 100644 --- a/includes/kohana/modules/userguide/classes/kohana/kodoc/method/param.php +++ b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Method/Param.php @@ -83,7 +83,7 @@ class Kohana_Kodoc_Method_Param extends Kodoc { if ($this->description) { - $display .= '$'.$this->name.' '; + $display .= '$'.$this->name.' '; } else { diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/missing.php b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Missing.php similarity index 100% rename from includes/kohana/modules/userguide/classes/kohana/kodoc/missing.php rename to includes/kohana/modules/userguide/classes/Kohana/Kodoc/Missing.php diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/property.php b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Property.php similarity index 81% rename from includes/kohana/modules/userguide/classes/kohana/kodoc/property.php rename to includes/kohana/modules/userguide/classes/Kohana/Kodoc/Property.php index 1c87369..af2cbee 100644 --- a/includes/kohana/modules/userguide/classes/kohana/kodoc/property.php +++ b/includes/kohana/modules/userguide/classes/Kohana/Kodoc/Property.php @@ -5,7 +5,7 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009 Kohana Team + * @copyright (c) 2009-2012 Kohana Team * @license http://kohanaphp.com/license */ class Kohana_Kodoc_Property extends Kodoc { @@ -30,7 +30,12 @@ class Kohana_Kodoc_Property extends Kodoc { */ public $value; - public function __construct($class, $property) + /** + * @var string default value of the property + */ + public $default; + + public function __construct($class, $property, $default = NULL) { $property = new ReflectionProperty($class, $property); @@ -45,19 +50,19 @@ class Kohana_Kodoc_Property extends Kodoc { if (isset($tags['var'])) { - if (preg_match('/^(\S*)(?:\s*(.+?))?$/', $tags['var'][0], $matches)) + if (preg_match('/^(\S*)(?:\s*(.+?))?$/s', $tags['var'][0], $matches)) { $this->type = $matches[1]; if (isset($matches[2])) { - $this->description = Markdown($matches[2]); + $this->description = Kodoc_Markdown::markdown($matches[2]); } } } $this->property = $property; - + // Show the value of static properties, but only if they are public or we are php 5.3 or higher and can force them to be accessible if ($property->isStatic() AND ($property->isPublic() OR version_compare(PHP_VERSION, '5.3', '>='))) { @@ -66,7 +71,7 @@ class Kohana_Kodoc_Property extends Kodoc { { $property->setAccessible(TRUE); } - + // Don't debug the entire object, just say what kind of object it is if (is_object($property->getValue($class))) { @@ -77,7 +82,9 @@ class Kohana_Kodoc_Property extends Kodoc { $this->value = Debug::vars($property->getValue($class)); } } - + + // Store the defult property + $this->default = Debug::vars($default);; } } // End Kodoc_Property diff --git a/includes/kohana/modules/userguide/config/userguide.php b/includes/kohana/modules/userguide/config/userguide.php index 9b5d252..0258f69 100644 --- a/includes/kohana/modules/userguide/config/userguide.php +++ b/includes/kohana/modules/userguide/config/userguide.php @@ -4,11 +4,14 @@ return array ( // Enable the API browser. TRUE or FALSE 'api_browser' => TRUE, - + // Enable these packages in the API browser. TRUE for all packages, or a string of comma seperated packages, using 'None' for a class with no @package // Example: 'api_packages' => 'Kohana,Kohana/Database,Kohana/ORM,None', 'api_packages' => TRUE, - + + // Enables Disqus comments on the API and User Guide pages + 'show_comments' => Kohana::$environment === Kohana::PRODUCTION, + // Leave this alone 'modules' => array( @@ -25,7 +28,12 @@ return array 'description' => 'Documentation viewer and api generation.', // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2011 Kohana Team', + 'copyright' => '© 2008–2012 Kohana Team', ) + ), + + // Set transparent class name segments + 'transparent_prefixes' => array( + 'Kohana' => TRUE, ) ); diff --git a/includes/kohana/modules/userguide/guide/userguide/adding.md b/includes/kohana/modules/userguide/guide/userguide/adding.md index 7a5ec68..156eca8 100644 --- a/includes/kohana/modules/userguide/guide/userguide/adding.md +++ b/includes/kohana/modules/userguide/guide/userguide/adding.md @@ -24,6 +24,20 @@ First, copy this config and place in it `/config/userguide.php`, replaci // Copyright message, shown in the footer for this module 'copyright' => '© 2010–2011 ', ) + ), + + /* + * If you use transparent extension outside the Kohana_ namespace, + * add your class prefix here. Both common Kohana naming conventions are + * excluded: + * - Modulename extends Modulename_Core + * - Foo extends Modulename_Foo + * + * For example, if you use Modulename_ for your base classes + * then you would define: + */ + 'transparent_prefixes' => array( + 'Modulename' => TRUE, ) ); diff --git a/includes/kohana/modules/userguide/guide/userguide/config.md b/includes/kohana/modules/userguide/guide/userguide/config.md index a566040..338abc1 100644 --- a/includes/kohana/modules/userguide/guide/userguide/config.md +++ b/includes/kohana/modules/userguide/guide/userguide/config.md @@ -6,17 +6,17 @@ The userguide has the following config options, available in `config/userguide.p ( // Enable the API browser. TRUE or FALSE 'api_browser' => TRUE, - + // Enable these packages in the API browser. TRUE for all packages, or a string of comma seperated packages, using 'None' for a class with no @package // Example: 'api_packages' => 'Kohana,Kohana/Database,Kohana/ORM,None', 'api_packages' => TRUE, - + ); -You can enable or disabled the entire api browser, or limit it to only show certain packages. To disable a module from showing pages in the userguide, simply change that modules `enabled` option using the cascading filesystem. For example: +You can enable or disable the entire API browser, or limit it to only show certain packages. To disable a module from showing pages in the userguide, simply change that module's `enabled` option using the cascading filesystem. For example: `application/config/userguide.php` - + return array ( 'modules' => array @@ -31,5 +31,5 @@ You can enable or disabled the entire api browser, or limit it to only show cert ) ) ) - -Using this you could make the userguide only show your modules and your classes in the api browser, if you wanted to host your own documentation on your site. Feel free to change the styles and views as well, but be sure to give credit where credit is due! \ No newline at end of file + +Using this you could make the userguide only show your modules and classes in the API browser, if you wanted to host your own documentation on your site. Feel free to change the styles and views as well, but be sure to give credit where credit is due! \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/userguide/contributing.md b/includes/kohana/modules/userguide/guide/userguide/contributing.md index fcf4e61..b5bf255 100644 --- a/includes/kohana/modules/userguide/guide/userguide/contributing.md +++ b/includes/kohana/modules/userguide/guide/userguide/contributing.md @@ -1,5 +1,3 @@ -[!!] When the docs get merged these images/links should be update - # Contributing Kohana is community driven, and we rely on community contributions for the documentation. @@ -18,7 +16,7 @@ To quickly point out something that needs improvement, report a [bug report](htt If you want to contribute some changes, you can do so right from your browser without even knowing git! -First create an account on [Github](https://github.com/signup/free). +First create an account on [GitHub](https://github.com/signup/free). You will need to fork the module for the area you want to improve. For example, to improve the [ORM documentation](../orm) fork . To improve the [Kohana documentation](../kohana), fork , etc. So, find the module you want to improve and click on the Fork button in the top right. @@ -36,31 +34,42 @@ After you have made your changes, send a pull request so your improvements can b Once your pull request has been accepted, you can delete your repository if you want. Your commit will have been copied to the official branch. -## If you know git +## If you know Git -**Short version**: Create a ticket on redmine for your changes, fork the module whose docs you wish to improve (e.g. `git://github.com/kohana/orm.git` or `git://github.com/kohana/core.git`), checkout the appropriate branch, make changes, and then send a pull request with the ticket number. +### Short version -**Long version:** (This still assumes you at least know your way around git, especially how submodules work.) +Fork the module whose docs you wish to improve (e.g. `git://github.com/kohana/orm.git` or `git://github.com/kohana/core.git`), checkout the `3.2/develop` branch (for the 3.2 docs), make changes, and then send a pull request. - 1. Create a ticket on redmine for your changes. +### Long version - 2. Fork the specific repo you want to contribute to on github. (For example go to http://github.com/kohana/core and click the fork button.) +(This still assumes you at least know your way around Git, especially how submodules work.) + + 1. Fork the specific repo you want to contribute to on GitHub. (For example, go to http://github.com/kohana/core and click the fork button.) + + 1. Now you need to add your fork as a "git remote" to your application and ensure you are on the right branch. An example for the [ORM](../orm) module and 3.2 docs: + + cd my-kohana-app/modules/orm - 3. Now go into the repo of the area of docs you want to contribute to and add your forked repo as a new remote, and push to it. - - cd system - - # make sure we are up to date - git checkout 3.1/develop - git pull - # add your repository as a new remote - git remote add git@github.com:/core.git - - # (make some changes to the docs) - - # now commit the changes and push to your repo - git commit - git push 3.1/develop + git remote add git://github.com//orm.git - 4. Send a pull request on github containing the ticket number, and update the ticket with a link to the pull request. + # Get the correct branch + git checkout 3.2/develop + + 1. Now go into the repo of the area of docs you want to contribute to and add your forked repo as a new remote, and push to it. + + cd my-kohana-app/modules/orm + + # Make some changes to the docs + nano file.md + + # Commit your changes - Use a descriptive commit message! If there is a redmine ticket for the changes you are making include "Fixes #XXXXX" in the commit message so its tracked. + git commit -a -m "Corrected a typo in the ORM docs. Fixes #12345." + + # make sure we are up to date with the latest changes + git merge origin/3.2/develop + + # Now push your changes to your fork. + git push 3.2/develop + + 1. Finally, send a pull request on GitHub. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/userguide/markdown.md b/includes/kohana/modules/userguide/guide/userguide/markdown.md index 3367fbd..d4c1d70 100644 --- a/includes/kohana/modules/userguide/guide/userguide/markdown.md +++ b/includes/kohana/modules/userguide/guide/userguide/markdown.md @@ -192,6 +192,8 @@ To link to page in a different module, prefix your url with `../` and the module **Images are also namespaced**, using `![Alt Text](imagename.jpg)` would look for `media/guide//imagename.jpg`. +[!!] If you want your userguide pages to be browsable on github or similar sites outside Kohana's own userguide module, specify the optional .md file extension in your links + ## API Links You can make links to the api browser by wrapping any class name in brackets. You may also include a function name, or propery name to link to that specifically. All of the following will link to the API browser: @@ -208,9 +210,9 @@ You can make links to the api browser by wrapping any class name in brackets. Y If you want to have parameters and have the function be clickable, only put the brackets around the class and function (not the params), and put a backslash in front of the opening parenthesis. - [Kohana::config]\('foobar','baz') + [Kohana::$config]\('foobar','baz') -[Kohana::config]\('foobar','baz') +[Kohana::$config]\('foobar','baz') ## Notes diff --git a/includes/kohana/modules/userguide/i18n/de.php b/includes/kohana/modules/userguide/i18n/de.php deleted file mode 100644 index 836f663..0000000 --- a/includes/kohana/modules/userguide/i18n/de.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Handbuch' -); diff --git a/includes/kohana/modules/userguide/i18n/es.php b/includes/kohana/modules/userguide/i18n/es.php deleted file mode 100644 index 22175dc..0000000 --- a/includes/kohana/modules/userguide/i18n/es.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Guía de Usuario' -); diff --git a/includes/kohana/modules/userguide/i18n/fr.php b/includes/kohana/modules/userguide/i18n/fr.php deleted file mode 100644 index 017d6a3..0000000 --- a/includes/kohana/modules/userguide/i18n/fr.php +++ /dev/null @@ -1,7 +0,0 @@ - 'Guide Utilisateur' -); - diff --git a/includes/kohana/modules/userguide/i18n/he.php b/includes/kohana/modules/userguide/i18n/he.php deleted file mode 100644 index 0d392ed..0000000 --- a/includes/kohana/modules/userguide/i18n/he.php +++ /dev/null @@ -1,6 +0,0 @@ - 'מדריך למשתמש' -); diff --git a/includes/kohana/modules/userguide/i18n/nl.php b/includes/kohana/modules/userguide/i18n/nl.php deleted file mode 100644 index c93ebf5..0000000 --- a/includes/kohana/modules/userguide/i18n/nl.php +++ /dev/null @@ -1,6 +0,0 @@ - 'Gebruiksaanwijzing' -); \ No newline at end of file diff --git a/includes/kohana/modules/userguide/i18n/ru.php b/includes/kohana/modules/userguide/i18n/ru.php deleted file mode 100644 index ecf4ce0..0000000 --- a/includes/kohana/modules/userguide/i18n/ru.php +++ /dev/null @@ -1,7 +0,0 @@ - 'Руководство пользователя' -); - diff --git a/includes/kohana/modules/userguide/i18n/zh.php b/includes/kohana/modules/userguide/i18n/zh.php deleted file mode 100644 index 6025fdd..0000000 --- a/includes/kohana/modules/userguide/i18n/zh.php +++ /dev/null @@ -1,28 +0,0 @@ - '用户手册', - - // Errors - 'Error' => '错误', - 'Userguide page not found' => '用户手册页面无法找到', - 'API Reference: Class not found.' => 'API 参考: 没有找到此类。', - 'That class is hidden' => '那个是隐藏类', - - // API - 'Table of Contents' => '目录', - 'Available Classes' => '可用的类', - 'Class Contents' => '类列表', - 'Constants' => '常量', - 'Properties' => '属性', - 'Methods' => '方法', - 'None' => '无', - 'Parameters' => '参数', - 'Parameter' => '参数', - 'Type' => '类型', - 'Description' => '描述', - 'Default' => '默认', - 'Return Values' => '返回值', - 'Source Code' => '源代码', -); diff --git a/includes/kohana/modules/userguide/init.php b/includes/kohana/modules/userguide/init.php index a812316..fc083e4 100644 --- a/includes/kohana/modules/userguide/init.php +++ b/includes/kohana/modules/userguide/init.php @@ -1,19 +1,19 @@ )', array('file' => '.+')) +Route::set('docs/media', 'guide-media(/)', array('file' => '.+')) ->defaults(array( - 'controller' => 'userguide', + 'controller' => 'Userguide', 'action' => 'media', 'file' => NULL, )); // API Browser, if enabled -if (Kohana::config('userguide.api_browser') === TRUE) +if (Kohana::$config->load('userguide.api_browser') === TRUE) { - Route::set('docs/api', 'guide/api(/)', array('class' => '[a-zA-Z0-9_]+')) + Route::set('docs/api', 'guide-api(/)', array('class' => '[a-zA-Z0-9_]+')) ->defaults(array( - 'controller' => 'userguide', + 'controller' => 'Userguide', 'action' => 'api', 'class' => NULL, )); @@ -24,7 +24,21 @@ Route::set('docs/guide', 'guide(/(/))', array( 'page' => '.+', )) ->defaults(array( - 'controller' => 'userguide', + 'controller' => 'Userguide', 'action' => 'docs', 'module' => '', - )); \ No newline at end of file + )); + +// Simple autoloader used to encourage PHPUnit to behave itself. +class Markdown_Autoloader { + public static function autoload($class) + { + if ($class == 'Markdown_Parser' OR $class == 'MarkdownExtra_Parser') + { + include_once Kohana::find_file('vendor', 'markdown/markdown'); + } + } +} + +// Register the autoloader +spl_autoload_register(array('Markdown_Autoloader', 'autoload')); diff --git a/includes/kohana/modules/userguide/media/guide/css/kodoc.css b/includes/kohana/modules/userguide/media/guide/css/kodoc.css index 391c2d7..0807bcc 100644 --- a/includes/kohana/modules/userguide/media/guide/css/kodoc.css +++ b/includes/kohana/modules/userguide/media/guide/css/kodoc.css @@ -95,48 +95,48 @@ h6:hover a.permalink { } -#header, -#content, -#footer { float: left; clear: both; width: 100%; } +#kodoc-header, +#kodoc-content, +#kodoc-footer { float: left; clear: both; width: 100%; } -#header { padding: 58px 0 2em; background: #77c244 url(../img/header.png) center top repeat-x; } - #logo { display: block; float: left; } - #menu { float: right; margin-top: 12px; background: #113c32; -moz-border-radius: 5px; -webkit-border-radius: 5px; } - #menu ul { float: left; margin: 0; padding: 0 0.5em 0 0; } - #menu li { display: block; float: left; margin: 0; padding: 0; } - #menu li.first { padding-left: 0.5em; } - #menu li a { display: block; height: 32px; line-height: 32px; padding: 0 0.8em; border-right: solid 1px #0f362d; border-left: solid 1px #144539; letter-spacing: 0.05em; text-decoration: none; text-transform: uppercase; color: #efefef; font-size: 90%; } - #menu li.first a { border-left: 0; } - #menu li.last a { border-right: 0; } - #menu li a:hover { background: #164e41; border-left-color: #195a4b; color: #fff; text-shadow: #fff 0 0 1px; } +#kodoc-header { padding: 58px 0 2em; background: #77c244 url(../img/header.png) center top repeat-x; } + #kodoc-logo { display: block; float: left; } + #kodoc-menu { float: right; margin-top: 12px; background: #113c32; -moz-border-radius: 5px; -webkit-border-radius: 5px; } + #kodoc-menu ul { float: left; margin: 0; padding: 0 0.5em 0 0; } + #kodoc-menu li { display: block; float: left; margin: 0; padding: 0; } + #kodoc-menu li.first { padding-left: 0.5em; } + #kodoc-menu li a { display: block; height: 32px; line-height: 32px; padding: 0 0.8em; border-right: solid 1px #0f362d; border-left: solid 1px #144539; letter-spacing: 0.05em; text-decoration: none; text-transform: uppercase; color: #efefef; font-size: 90%; } + #kodoc-menu li.first a { border-left: 0; } + #kodoc-menu li.last a { border-right: 0; } + #kodoc-menu li a:hover { background: #164e41; border-left-color: #195a4b; color: #fff; text-shadow: #fff 0 0 1px; } -#content { background: #f1f8db url(../img/content.png) center top repeat-x; } - #content .wrapper { min-height: 390px; padding: 1em 0; background: transparent url(../img/wrapper.png) center top no-repeat; } - #content div.page-toc { float: right; margin: 1em; margin-top: 0; padding: 1em; background: #fff; border: solid 0.1em #e8efcf; border-radius: 0.6em; } - #content p.intro { padding: 1em 20px; padding-left: 20px; margin: 0 -20px; font-size: 1.2em; } - #content a { color: #004352; } - #content a:hover { color: #00758f; } - #content a:active { text-decoration: none; } +#kodoc-content { background: #f1f8db url(../img/content.png) center top repeat-x; } + #kodoc-content .wrapper { min-height: 390px; padding: 1em 0; background: transparent url(../img/wrapper.png) center top no-repeat; } + #kodoc-content div.page-toc { float: right; margin: 1em; margin-top: 0; padding: 1em; background: #fff; border: solid 0.1em #e8efcf; border-radius: 0.6em; } + #kodoc-content p.intro { padding: 1em 20px; padding-left: 20px; margin: 0 -20px; font-size: 1.2em; } + #kodoc-content a { color: #004352; } + #kodoc-content a:hover { color: #00758f; } + #kodoc-content a:active { text-decoration: none; } -#breadcrumb { margin: 0 0 1em; padding: 0 0 0.5em; list-style: none; border-bottom: solid 1px #e8efcf; } - #breadcrumb li { display: inline-block; margin: 0; padding: 0 0.4em 0 0; text-transform: uppercase; font-size: 11px; } - #breadcrumb li:before { content: '»'; padding-right: 0.4em; } - #breadcrumb li a { color: #999; text-decoration: none; } +#kodoc-breadcrumb { margin: 0 0 1em; padding: 0 0 0.5em; list-style: none; border-bottom: solid 1px #e8efcf; } + #kodoc-breadcrumb li { display: inline-block; margin: 0; padding: 0 0.4em 0 0; text-transform: uppercase; font-size: 11px; } + #kodoc-breadcrumb li:before { content: '»'; padding-right: 0.4em; } + #kodoc-breadcrumb li a { color: #999; text-decoration: none; } -#topics { } - #topics ul, - #topics ol { list-style-type:none; margin: 0; padding: 0;} - #topics ul li, - #topics ol li { margin:0; padding: 0; margin-left: 1em; } - #topics ul li a.current, - #topics ol li a.current { font-weight: bold; } - #topics span, - #topics a { display: block; padding: 0; margin: 0; } - #topics span { cursor: pointer; } - #topics span.toggle { display: block; float: left; width: 1em; padding-right: 0.4em; margin-left: -1.4em; text-align: center; } +#kodoc-topics { } + #kodoc-topics ul, + #kodoc-topics ol { list-style-type:none; margin: 0; padding: 0;} + #kodoc-topics ul li, + #kodoc-topics ol li { margin:0; padding: 0; margin-left: 1em; } + #kodoc-topics ul li a.current, + #kodoc-topics ol li a.current { font-weight: bold; } + #kodoc-topics span, + #kodoc-topics a { display: block; padding: 0; margin: 0; } + #kodoc-topics span { cursor: pointer; } + #kodoc-topics span.toggle { display: block; float: left; width: 1em; padding-right: 0.4em; margin-left: -1.4em; text-align: center; } - #topics li span { cursor:pointer; } + #kodoc-topics li span { cursor:pointer; } -#footer { padding: 1em 0; background: #00262f; color: #405c63; text-shadow: #00262f 0.1em 0.1em 1px; font-size: 0.9em; } - #footer a { color: #809397; } - #footer div.last { text-align: right; } \ No newline at end of file +#kodoc-footer { padding: 1em 0; background: #00262f; color: #405c63; text-shadow: #00262f 0.1em 0.1em 1px; font-size: 0.9em; } + #kodoc-footer a { color: #809397; } + #kodoc-footer div.last { text-align: right; } \ No newline at end of file diff --git a/includes/kohana/modules/userguide/media/guide/js/kodoc.js b/includes/kohana/modules/userguide/media/guide/js/kodoc.js index 70d8b48..597ebf9 100644 --- a/includes/kohana/modules/userguide/media/guide/js/kodoc.js +++ b/includes/kohana/modules/userguide/media/guide/js/kodoc.js @@ -17,10 +17,10 @@ $(document).ready(function() $('a[href="'+ window.location.pathname +'"]').addClass('current'); // Breadcrumbs magic - $('#breadcrumb li.last').each(function() + $('#kodoc-breadcrumb li.last').each(function() { var $this = $(this); - var $topics = $('#topics li').has('a.current').slice(0, -1); + var $topics = $('#kodoc-topics li').has('a.current').slice(0, -1); $topics.each(function() { @@ -33,7 +33,7 @@ $(document).ready(function() }); // Collapsing menus - $('#topics li:has(li)').each(function() + $('#kodoc-topics li:has(li)').each(function() { var $this = $(this); var toggle = $(''); @@ -88,7 +88,7 @@ $(document).ready(function() }); // "Link to this" link that appears when you hover over a header - $('#body') + $('#kodoc-body') .find('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]') .append(function(){ var $this = $(this); diff --git a/includes/kohana/modules/userguide/tests/KodocTest.php b/includes/kohana/modules/userguide/tests/KodocTest.php new file mode 100644 index 0000000..8258f51 --- /dev/null +++ b/includes/kohana/modules/userguide/tests/KodocTest.php @@ -0,0 +1,368 @@ +Description

\n", array()), + ), + array( +<<<'COMMENT' +/** + * Description spanning + * multiple lines + */ +COMMENT +, + array("

Description spanning\nmultiple lines

\n", array()), + ), + array( +<<<'COMMENT' +/** + * Description including + * + * a code block + */ +COMMENT +, + array("

Description including

\n\n
a code block\n
\n", array()), + ), + array( +<<<'COMMENT' + /** + * Indented + */ +COMMENT +, + array("

Indented

\n", array()), + ), + array( +<<<'COMMENT' +/** + * @tag Content + */ +COMMENT +, + array('', array('tag' => array('Content'))), + ), + array( +<<<'COMMENT' +/** + * @tag Multiple + * @tag Tags + */ +COMMENT +, + array('', array('tag' => array('Multiple', 'Tags'))), + ), + array( +<<<'COMMENT' +/** + * Description with tag + * @tag Content + */ +COMMENT +, + array( + "

Description with tag

\n", + array('tag' => array('Content')), + ), + ), + array( +<<<'COMMENT' +/** + * @trailingspace + */ +COMMENT +, + array('', array('trailingspace' => array(''))), + ), + array( +<<<'COMMENT' +/** + * @tag Content that spans + * multiple lines + */ +COMMENT +, + array( + '', + array('tag' => array("Content that spans\nmultiple lines")), + ), + ), + array( +<<<'COMMENT' +/** + * @tag Content that spans + * multiple lines indented + */ +COMMENT +, + array( + '', + array('tag' => array("Content that spans\n multiple lines indented")), + ), + ), + ); + } + + /** + * @covers Kohana_Kodoc::parse + * + * @dataProvider provider_parse_basic + * + * @param string $comment Argument to the method + * @param array $expected Expected result + */ + public function test_parse_basic($comment, $expected) + { + $this->assertSame($expected, Kodoc::parse($comment)); + } + + public function provider_parse_tags() + { + $route_api = Route::get('docs/api'); + + return array( + array( +<<<'COMMENT' +/** + * @access public + */ +COMMENT +, + array('', array()), + ), + array( +<<<'COMMENT' +/** + * @copyright Some plain text + */ +COMMENT +, + array('', array('copyright' => array('Some plain text'))), + ), + array( +<<<'COMMENT' +/** + * @copyright (c) 2012 Kohana Team + */ +COMMENT +, + array('', array('copyright' => array('© 2012 Kohana Team'))), + ), + array( +<<<'COMMENT' +/** + * @license Kohana + */ +COMMENT +, + array('', array('license' => array('Kohana'))), + ), + array( +<<<'COMMENT' +/** + * @license http://kohanaframework.org/license + */ +COMMENT +, + array('', array('license' => array('http://kohanaframework.org/license'))), + ), + array( +<<<'COMMENT' +/** + * @link http://kohanaframework.org + */ +COMMENT +, + array('', array('link' => array('http://kohanaframework.org'))), + ), + array( +<<<'COMMENT' +/** + * @link http://kohanaframework.org Description + */ +COMMENT +, + array('', array('link' => array('Description'))), + ), + array( +<<<'COMMENT' +/** + * @see MyClass + */ +COMMENT +, + array( + '', + array( + 'see' => array( + 'MyClass', + ), + ), + ), + ), + array( +<<<'COMMENT' +/** + * @see MyClass::method() + */ +COMMENT +, + array( + '', + array( + 'see' => array( + 'MyClass::method()', + ), + ), + ), + ), + array( +<<<'COMMENT' +/** + * @throws Exception + */ +COMMENT +, + array( + '', + array( + 'throws' => array( + 'Exception', + ), + ), + ), + ), + array( +<<<'COMMENT' +/** + * @throws Exception During failure + */ +COMMENT +, + array( + '', + array( + 'throws' => array( + 'Exception During failure', + ), + ), + ), + ), + array( +<<<'COMMENT' +/** + * @uses MyClass + */ +COMMENT +, + array( + '', + array( + 'uses' => array( + 'MyClass', + ), + ), + ), + ), + array( +<<<'COMMENT' +/** + * @uses MyClass::method() + */ +COMMENT +, + array( + '', + array( + 'uses' => array( + 'MyClass::method()', + ), + ), + ), + ), + ); + } + + /** + * @covers Kohana_Kodoc::format_tag + * @covers Kohana_Kodoc::parse + * + * @dataProvider provider_parse_tags + * + * @param string $comment Argument to the method + * @param array $expected Expected result + */ + public function test_parse_tags($comment, $expected) + { + $this->assertSame($expected, Kodoc::parse($comment)); + } + + /** + * Provides test data for test_transparent_classes + * @return array + */ + public function provider_transparent_classes() + { + return array( + // Kohana_Core is a special case + array('Kohana','Kohana_Core',NULL), + array('Controller_Template','Kohana_Controller_Template',NULL), + array('Controller_Template','Kohana_Controller_Template', + array('Kohana_Controller_Template'=>'Kohana_Controller_Template', + 'Controller_Template'=>'Controller_Template') + ), + array(FALSE,'Kohana_Controller_Template', + array('Kohana_Controller_Template'=>'Kohana_Controller_Template')), + array(FALSE,'Controller_Template',NULL), + ); + } + + /** + * Tests Kodoc::is_transparent + * + * Checks that a selection of transparent and non-transparent classes give expected results + * + * @group kohana.userguide.3529-configurable-transparent-classes + * @dataProvider provider_transparent_classes + * @param mixed $expected + * @param string $class + * @param array $classes + */ + public function test_transparent_classes($expected, $class, $classes) + { + $result = Kodoc::is_transparent($class, $classes); + $this->assertSame($expected,$result); + } +} diff --git a/includes/kohana/modules/userguide/tests/userguide/ControllerTest.php b/includes/kohana/modules/userguide/tests/userguide/ControllerTest.php new file mode 100644 index 0000000..30ef475 --- /dev/null +++ b/includes/kohana/modules/userguide/tests/userguide/ControllerTest.php @@ -0,0 +1,45 @@ +getMock('Controller_Userguide', array('__construct'), array(), '', FALSE); + $path = $controller->file($page); + + // Only verify trailing segments to avoid problems if file overwritten in CFS + $expected_len = strlen($expected_file); + $file = substr($path, -$expected_len, $expected_len); + + $this->assertEquals($expected_file, $file); + } +} diff --git a/includes/kohana/modules/userguide/views/userguide/api/class.php b/includes/kohana/modules/userguide/views/userguide/api/class.php index 0b54712..08afd9d 100644 --- a/includes/kohana/modules/userguide/views/userguide/api/class.php +++ b/includes/kohana/modules/userguide/views/userguide/api/class.php @@ -1,16 +1,34 @@

modifiers, $doc->class->name ?> - class; ?> - getParentClass()): ?> + parents as $parent): ?>
extends uri(array('class' => $parent->name)), $parent->name, NULL, NULL, TRUE) ?> - +

-description ?> +class->getInterfaceNames()): ?> +

+Implements: +uri(array('class' => $interfaces[$i])), $interfaces[$i], NULL, NULL, TRUE); +} +?> +

+ + +is_transparent($doc->class->name)):?> +

+This class is a transparent base class for uri(array('class'=>$child)),$child) ?> and +should not be accessed directly. +

+ + +description() ?> tags): ?>
-tags as $name => $set): ?> +tags() as $name => $set): ?>
@@ -29,38 +47,38 @@ Class is not declared in a file, it is probably an internal
-

+

    constants): ?> constants as $name => $value): ?>
  • -
  • +
-

+

-

+

@@ -70,9 +88,9 @@ Class is not declared in a file, it is probably an internal constants): ?>
-

+

-constants as $name => $value): ?> +constants() as $name => $value): ?>

@@ -81,20 +99,23 @@ Class is not declared in a file, it is probably an internal properties()): ?> -

+

modifiers ?> type ?> $property->name ?>

description ?>
value ?>
+default !== $prop->value): ?> +

default ?>
+
methods()): ?> -

+

set('doc', $method)->set('route', $route) ?> diff --git a/includes/kohana/modules/userguide/views/userguide/api/method.php b/includes/kohana/modules/userguide/views/userguide/api/method.php index 521dfde..3dcd332 100644 --- a/includes/kohana/modules/userguide/views/userguide/api/method.php +++ b/includes/kohana/modules/userguide/views/userguide/api/method.php @@ -27,7 +27,7 @@ tags) echo View::factory('userguide/api/tags')->set('tags', $doc->tags) ?> return): ?> -

+

    return as $set): list($type, $text) = $set; ?>
  • @@ -37,7 +37,7 @@ source): ?>
    -

    +

    source) ?>
    diff --git a/includes/kohana/modules/userguide/views/userguide/api/toc.php b/includes/kohana/modules/userguide/views/userguide/api/toc.php index 2a69005..88075d9 100644 --- a/includes/kohana/modules/userguide/views/userguide/api/toc.php +++ b/includes/kohana/modules/userguide/views/userguide/api/toc.php @@ -1,62 +1,56 @@ -

    +

    - +
    diff --git a/includes/kohana/modules/userguide/views/userguide/error.php b/includes/kohana/modules/userguide/views/userguide/error.php index 1f7b97b..1fb7cbb 100644 --- a/includes/kohana/modules/userguide/views/userguide/error.php +++ b/includes/kohana/modules/userguide/views/userguide/error.php @@ -1,3 +1,3 @@ -

    Kodoc -

    +

    Kodoc -

    \ No newline at end of file diff --git a/includes/kohana/modules/userguide/views/userguide/template.php b/includes/kohana/modules/userguide/views/userguide/template.php index 8868d80..d94efc4 100644 --- a/includes/kohana/modules/userguide/views/userguide/template.php +++ b/includes/kohana/modules/userguide/views/userguide/template.php @@ -1,9 +1,9 @@ - + -<?php echo $title ?> | Kohana <?php echo __('User Guide'); ?> +<?php echo $title ?> | Kohana <?php echo 'User Guide'; ?> $media) echo HTML::style($style, array('media' => $media), NULL, TRUE), "\n" ?> @@ -15,29 +15,31 @@ -
PHP Version Kohana requires PHP 5.2.3 or newer, this version is .Kohana requires PHP 5.3.3 or newer, this version is .
System Directory The configured system directory does not exist or does not contain required files.