diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36bae9e..8a2b50c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,13 @@ # Developing locally -Since Kohana maintains many concurrent versions at once, there is no single `master` branch. All versions have branches named with a prefix of it's version: +Since Kohana maintains many concurrent versions at once, there is no single `master` branch. All versions have branches named with a prefix of its version: - 3.2/master - 3.2/develop - 3.3/master - 3.3/develop -and so on. All development of versions happens in the develop branch of that version. Before a release, new features are added here. After a major release is actually released, only bugfixes can happen here. New features and api changes must happen in the develop branch of the next version. +and so on. All development of versions happens in the develop branch of that version. Before a release, new features are added here. After a major release is actually released, only bugfixes can happen here. New features and API changes must happen in the develop branch of the next version. ## Branch name meanings @@ -17,7 +17,7 @@ and so on. All development of versions happens in the develop branch of that ver - **3.3/release/*** - release branches are for maintenance work before a release. This branch should be branched from the develop branch only. Change the version number/code name here, and apply any other maintenance items needed before actually releasing. Merges from master should only come from this branch. It should be merged to develop when it's complete as well. This branch is deleted after it's done. - **3.3/feature/*** - Details on these branches are outlined below. This branch is deleted after it's done. -If an bug/issue applies to multiple versions of kohana, it is first fixed in the lowest supported version it applies to, then merged to each higher branch it applies to. Each merge should only happen one version up. 3.1 should merge to 3.2, and 3.2 should merge to 3.3. 3.1 should not merge directly to 3.3. +If an bug/issue applies to multiple versions of Kohana, it is first fixed in the lowest supported version it applies to, then merged to each higher branch it applies to. Each merge should only happen one version up. 3.1 should merge to 3.2, and 3.2 should merge to 3.3. 3.1 should not merge directly to 3.3. To work on a specific release branch you need to check it out then check out the appropriate system branch. Release branch names follow the same convention in both kohana/kohana and kohana/core. @@ -37,7 +37,7 @@ To work on 3.3.x you'd do the following: > git submodule foreach "git fetch && git checkout 3.3/develop" # ... -It's important that you follow the last step, because unlike svn, git submodules point at a +It's important that you follow the last step, because unlike SVN, Git submodules point at a specific commit rather than the tip of a branch. If you cd into the system folder after a `git submodule update` and run `git status` you'll be told: @@ -48,7 +48,7 @@ a `git submodule update` and run `git status` you'll be told: # Contributing to the project -All features and bugfixes must be fully tested and reference an issue in the [tracker](http://dev.kohanaframework.org/projects/kohana3), **there are absolutely no exceptions**. +All features and bugfixes must be fully tested and reference an issue in [GitHub](https://github.com/kohana/kohana/issues), **there are absolutely no exceptions**. It's highly recommended that you write/run unit tests during development as it can help you pick up on issues early on. See the Unit Testing section below. @@ -68,7 +68,7 @@ The naming convention for feature branches is: 3.2/feature/4045-rewriting-config-system When a new feature is complete and fully tested it can be merged into its respective release branch using -`git pull --no-ff`. The `--no-ff` switch is important as it tells git to always create a commit +`git pull --no-ff`. The `--no-ff` switch is important as it tells Git to always create a commit detailing what branch you're merging from. This makes it a lot easier to analyse a feature's history. Here's a quick example: @@ -81,7 +81,7 @@ Here's a quick example: > git merge --no-ff 3.2/feature/4045-rewriting-everything -**If a change you make intentionally breaks the api then please correct the relevant tests before pushing!** +**If a change you make intentionally breaks the API then please correct the relevant tests before pushing!** ## Bug fixing @@ -92,7 +92,7 @@ using the `@ticket` notation in the test to reference the bug's issue number If you run the unit tests then the one you've just made should fail. Once you've written the bugfix, run the tests again before you commit to make sure that the -fix actually works,then commit both the fix and the test. +fix actually works, then commit both the fix and the test. **Bug fixes without tests written will be rejected! There are NO exceptions.** @@ -101,11 +101,11 @@ branch is perfectly acceptable. ## Tagging releases -Tag names should be prefixed with a `v`, this helps to separate tag references from branch references in git. +Tag names should be prefixed with a `v`, this helps to separate tag references from branch references in Git. For example, if you were creating a tag for the `3.1.0` release the tag name would be `v3.1.0` -# Merging Changes from Remote Repositories +# Merging changes from remote repositories Now that you have a remote repository, you can pull changes in the remote "kohana" repository into your local repository: @@ -115,14 +115,14 @@ into your local repository: **Note:** Before you pull changes you should make sure that any modifications you've made locally have been committed. -Sometimes a commit you've made locally will conflict with one made in the "kohana" one. +Sometimes a commit you've made locally will conflict with one made in the remote "kohana" repo. There are a couple of scenarios where this might happen: -## The conflict is to do with a few unrelated commits and you want to keep changes made in both commits +## The conflict is due to a few unrelated commits and you want to keep changes made in both commits You'll need to manually modify the files to resolve the conflict, see the "Resolving a merge" -section [in the git-scm book](http://book.git-scm.com/3_basic_branching_and_merging.html) for more info +section [in the Git SCM book](http://book.git-scm.com/3_basic_branching_and_merging.html) for more info ## You've fixed something locally which someone else has already done in the remote repo @@ -147,14 +147,14 @@ i.e. > git rebase -i 57d0b28 -A text editor will open with a list of commits, delete the line containing the offending commit +A text editor will open with a list of commits. Delete the line containing the offending commit before saving the file & closing your editor. Git will remove the commit and you can then pull/merge the remote changes. # Unit Testing -Kohana currently uses phpunit for unit testing. This is installed with composer. +Kohana currently uses PHPUnit for unit testing. This is installed with composer. ## How to run the tests @@ -164,4 +164,4 @@ Kohana currently uses phpunit for unit testing. This is installed with composer. * Run `php composer.phar install` from the root of this repository * Finally, run `phing test` -This will run the unit tests for core and all the modules and tell you if anything failed. If you haven't changed anything and you get failures, please create a new issue on [the tracker](http://dev.kohanaframework.org) and paste the output (including the error) in the issue. +This will run the unit tests for core and all the modules and tell you if anything failed. If you haven't changed anything and you get failures, please create a new issue on [the tracker](http://dev.kohanaframework.org) and paste the output (including the error) in the issue. diff --git a/application/bootstrap.php b/application/bootstrap.php index b603a1f..c795685 100644 --- a/application/bootstrap.php +++ b/application/bootstrap.php @@ -131,6 +131,15 @@ Kohana::modules(array( // 'userguide' => MODPATH.'userguide', // User guide and API documentation )); +/** + * Cookie Salt + * @see http://kohanaframework.org/3.3/guide/kohana/cookies + * + * If you have not defined a cookie salt in your Cookie class then + * uncomment the line below and define a preferrably long salt. + */ +// Cookie::$salt = NULL; + /** * Set the routes. Each route must have a minimum of a name, a URI and a set of * defaults for the URI. diff --git a/composer.json b/composer.json index 33babb4..fdfac8a 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "require": { - "phpunit/phpunit": "3.7.24", + "phpunit/phpunit": "3.7.24 - 4", "phing/phing": "dev-master" } } diff --git a/modules/auth/.travis.yml b/modules/auth/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/auth/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/auth/README.md b/modules/auth/README.md index 27ac9cd..9ee0886 100644 --- a/modules/auth/README.md +++ b/modules/auth/README.md @@ -1,5 +1,9 @@ -New Age Auth +Kohana auth module --- +| ver | Stable | Develop | +|-------|------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/auth.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/auth) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/auth.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/auth) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/auth.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/auth) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/auth.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/auth) | I've forked the main Auth module because there were some fundamental flaws with it: diff --git a/modules/auth/composer.json b/modules/auth/composer.json index c35732f..0313feb 100644 --- a/modules/auth/composer.json +++ b/modules/auth/composer.json @@ -24,10 +24,18 @@ "kohana/core": ">=3.3", "php": ">=5.3.3" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "extra": { "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] } } } diff --git a/modules/auth/koharness.php b/modules/auth/koharness.php new file mode 100644 index 0000000..5f51a74 --- /dev/null +++ b/modules/auth/koharness.php @@ -0,0 +1,8 @@ + array( + 'auth' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/cache/.travis.yml b/modules/cache/.travis.yml new file mode 100644 index 0000000..ad9ec7e --- /dev/null +++ b/modules/cache/.travis.yml @@ -0,0 +1,45 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +services: + - memcached + +before_script: + - if [[ $TRAVIS_PHP_VERSION != "hhvm" ]]; then INI_FILE=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; else INI_FILE=/etc/hhvm/php.ini; fi; + - if [[ $TRAVIS_PHP_VERSION = 5.* ]]; then echo extension = memcache.so >> $INI_FILE; fi; + - if [[ $TRAVIS_PHP_VERSION = 5.* ]]; then (echo yes | pecl install -f apcu-4.0.10 && echo apc.enable_cli = 1 >> $INI_FILE); fi; + - if [[ $TRAVIS_PHP_VERSION = 7.* ]]; then (echo yes | pecl install -f apcu-5.1.2 && echo apc.enable_cli = 1 >> $INI_FILE); fi; + - if [[ $TRAVIS_PHP_VERSION != "hhvm" ]] ; then echo -e 'apc.max_file_size = 0\napc.cache_by_default = 0' >> $INI_FILE ; fi + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/cache/README.md b/modules/cache/README.md index 7acff9c..ba5733b 100644 --- a/modules/cache/README.md +++ b/modules/cache/README.md @@ -1,6 +1,12 @@ Kohana Cache library ==================== +| ver | Stable | Develop | +|-------|--------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/cache.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/cache) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/cache.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/cache) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/cache.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/cache) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/cache.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/cache) | + + The cache library for Kohana 3 provides a simple interface to the most common cache solutions. Developers are free to add their own caching solutions that follow the cache design pattern defined within this module. Supported cache solutions diff --git a/modules/cache/classes/Cache/Apcu.php b/modules/cache/classes/Cache/Apcu.php new file mode 100644 index 0000000..439714d --- /dev/null +++ b/modules/cache/classes/Cache/Apcu.php @@ -0,0 +1,3 @@ + array( // Default group + * 'memcache' => array( // Name of group * 'driver' => 'memcache', // using Memcache driver * 'servers' => array( // Available server definitions * array( @@ -49,8 +49,12 @@ * ), * ) * - * 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. + * In cases where only one cache group is required, set `Cache::$default` (in your bootstrap, + * or by extending `Kohana_Cache` class) to the name of the group, and use: + * + * $cache = Cache::instance(); // instead of Cache::instance('memcache') + * + * It will return the cache instance of the group it has been set in `Cache::$default`. * * #### General cache group configuration settings * diff --git a/modules/cache/classes/Kohana/Cache/Apcu.php b/modules/cache/classes/Kohana/Cache/Apcu.php new file mode 100644 index 0000000..e866e13 --- /dev/null +++ b/modules/cache/classes/Kohana/Cache/Apcu.php @@ -0,0 +1,174 @@ + array( // Driver group + * 'driver' => 'apcu', // using APCu 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 + * * APCu PHP extension + * + * @package Kohana/Cache + * @category Base + * @author Kohana Team + * @copyright (c) 2009-2012 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Cache_Apcu extends Cache implements Cache_Arithmetic { + + /** + * Check for existence of the APCu extension This method cannot be invoked externally. The driver must + * be instantiated using the `Cache::instance()` method. + * + * @param array $config configuration + * @throws Cache_Exception + */ + protected function __construct(array $config) + { + if ( ! extension_loaded('apcu')) + { + throw new Cache_Exception('PHP APCu extension is not available.'); + } + + parent::__construct($config); + } + + /** + * Retrieve a cached value entry by id. + * + * // Retrieve cache entry from apcu group + * $data = Cache::instance('apcu')->get('foo'); + * + * // Retrieve cache entry from apcu group and return 'bar' if miss + * $data = Cache::instance('apcu')->get('foo', 'bar'); + * + * @param string $id id of cache to entry + * @param string $default default value to return if cache miss + * @return mixed + * @throws Cache_Exception + */ + public function get($id, $default = NULL) + { + $data = apcu_fetch($this->_sanitize_id($id), $success); + + return $success ? $data : $default; + } + + /** + * Set a value to cache with id and lifetime + * + * $data = 'bar'; + * + * // Set 'bar' to 'foo' in apcu group, using default expiry + * Cache::instance('apcu')->set('foo', $data); + * + * // Set 'bar' to 'foo' in apcu group for 30 seconds + * Cache::instance('apcu')->set('foo', $data, 30); + * + * @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) + { + if ($lifetime === NULL) + { + $lifetime = Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE); + } + + return apcu_store($this->_sanitize_id($id), $data, $lifetime); + } + + /** + * Delete a cache entry based on id + * + * // Delete 'foo' entry from the apcu group + * Cache::instance('apcu')->delete('foo'); + * + * @param string $id id to remove from cache + * @return boolean + */ + public function delete($id) + { + return apcu_delete($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 apcu group + * Cache::instance('apcu')->delete_all(); + * + * @return boolean + */ + public function delete_all() + { + return apcu_clear_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) + { + if (apcu_exists($id)) { + return apcu_inc($id, $step); + } else { + return FALSE; + } + } + + /** + * 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) + { + if (apcu_exists($id)) { + return apcu_dec($id, $step); + } else { + return FALSE; + } + } + +} // End Kohana_Cache_Apcu diff --git a/modules/cache/classes/Kohana/Cache/File.php b/modules/cache/classes/Kohana/Cache/File.php index 553de85..0e8b9bb 100644 --- a/modules/cache/classes/Kohana/Cache/File.php +++ b/modules/cache/classes/Kohana/Cache/File.php @@ -140,17 +140,21 @@ class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { } else { - // Open the file and parse data - $created = $file->getMTime(); - $data = $file->openFile(); - $lifetime = $data->fgets(); - - // If we're at the EOF at this point, corrupted! - if ($data->eof()) + // Test the expiry + if ($this->_is_expired($file)) { - throw new Cache_Exception(__METHOD__.' corrupted cache file!'); + // Delete the file + $this->_delete_file($file, FALSE, TRUE); + return $default; } + // open the file to read data + $data = $file->openFile(); + + // Run first fgets(). Cache data starts from the second line + // as the first contains the lifetime timestamp + $data->fgets(); + $cache = ''; while ($data->eof() === FALSE) @@ -158,17 +162,7 @@ class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { $cache .= $data->fgets(); } - // Test the expiry - if (($created + (int) $lifetime) < time()) - { - // Delete the file - $this->_delete_file($file, NULL, TRUE); - return $default; - } - else - { - return unserialize($cache); - } + return unserialize($cache); } } @@ -219,14 +213,7 @@ class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { // If the directory path is not a directory if ( ! $dir->isDir()) { - // Create the directory - if ( ! mkdir($directory, 0777, TRUE)) - { - throw new Cache_Exception(__METHOD__.' unable to create directory : :directory', array(':directory' => $directory)); - } - - // chmod to solve potential umask issues - chmod($directory, 0777); + $this->_make_directory($directory, 0777, TRUE); } // Open file to inspect @@ -267,7 +254,7 @@ class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { $filename = Cache_File::filename($this->_sanitize_id($id)); $directory = $this->_resolve_directory($filename); - return $this->_delete_file(new SplFileInfo($directory.$filename), NULL, TRUE); + return $this->_delete_file(new SplFileInfo($directory.$filename), FALSE, TRUE); } /** @@ -337,9 +324,7 @@ class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { else { // Assess the file expiry to flag it for deletion - $json = $file->openFile('r')->current(); - $data = json_decode($json); - $delete = $data->expiry < time(); + $delete = $this->_is_expired($file); } // If the delete flag is set delete file @@ -375,7 +360,7 @@ class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { // Create new file resource $fp = new SplFileInfo($files->getRealPath()); // Delete the file - $this->_delete_file($fp); + $this->_delete_file($fp, $retain_parent_directory, $ignore_errors, $only_expired); } // Move the file pointer on @@ -446,21 +431,55 @@ class Kohana_Cache_File extends Cache implements Cache_GarbageCollect { * `mkdir` to ensure DRY principles * * @link http://php.net/manual/en/function.mkdir.php - * @param string $directory - * @param integer $mode - * @param boolean $recursive - * @param resource $context + * @param string $directory directory path + * @param integer $mode chmod mode + * @param boolean $recursive allows nested directories creation + * @param resource $context a stream context * @return SplFileInfo * @throws Cache_Exception */ protected function _make_directory($directory, $mode = 0777, $recursive = FALSE, $context = NULL) { - if ( ! mkdir($directory, $mode, $recursive, $context)) + // call mkdir according to the availability of a passed $context param + $mkdir_result = $context ? + mkdir($directory, $mode, $recursive, $context) : + mkdir($directory, $mode, $recursive); + + // throw an exception if unsuccessful + if ( ! $mkdir_result) { throw new Cache_Exception('Failed to create the defined cache directory : :directory', array(':directory' => $directory)); } + + // chmod to solve potential umask issues chmod($directory, $mode); return new SplFileInfo($directory); } + + /** + * Test if cache file is expired + * + * @param SplFileInfo $file the cache file + * @return boolean TRUE if expired false otherwise + */ + protected function _is_expired(SplFileInfo $file) + { + // Open the file and parse data + $created = $file->getMTime(); + $data = $file->openFile("r"); + $lifetime = (int) $data->fgets(); + + // If we're at the EOF at this point, corrupted! + if ($data->eof()) + { + throw new Cache_Exception(__METHOD__ . ' corrupted cache file!'); + } + + //close file + $data = null; + + // test for expiry and return + return (($lifetime !== 0) AND ( ($created + $lifetime) < time())); + } } diff --git a/modules/cache/classes/Kohana/HTTP/Cache.php b/modules/cache/classes/Kohana/HTTP/Cache.php index 2507f81..2908152 100644 --- a/modules/cache/classes/Kohana/HTTP/Cache.php +++ b/modules/cache/classes/Kohana/HTTP/Cache.php @@ -1,6 +1,6 @@ =3.3", "php": ">=5.3.3" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "extra": { "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/cache/guide/cache/config.md b/modules/cache/guide/cache/config.md index 450bea8..5b4f20d 100644 --- a/modules/cache/guide/cache/config.md +++ b/modules/cache/guide/cache/config.md @@ -55,7 +55,7 @@ failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language 'driver' => 'memcache', 'default_expire' => 3600, 'compression' => FALSE, // Use Zlib compression - (can cause issues with integers) + // (can cause issues with integers) 'servers' => array ( 'local' => array @@ -71,7 +71,7 @@ failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language 'driver' => 'memcachetag', 'default_expire' => 3600, 'compression' => FALSE, // Use Zlib compression - (can cause issues with integers) + // (can cause issues with integers) 'servers' => array ( 'local' => array @@ -90,7 +90,15 @@ failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language 'driver' => 'apc', 'default_expire' => 3600, ), + +## APCu settings + 'apcu' => array + ( + 'driver' => 'apcu', + 'default_expire' => 3600, + ), + ## SQLite settings 'sqlite' => array diff --git a/modules/cache/guide/cache/index.md b/modules/cache/guide/cache/index.md index 0df10c9..f07b993 100644 --- a/modules/cache/guide/cache/index.md +++ b/modules/cache/guide/cache/index.md @@ -6,7 +6,7 @@ instances of cache engines through a grouped singleton pattern. ## Supported cache engines - * APC ([Cache_Apc]) + * APC/APCu ([Cache_Apc]) * File ([Cache_File]) * Memcached ([Cache_Memcache]) * Memcached-tags ([Cache_Memcachetag]) @@ -16,13 +16,13 @@ instances of cache engines through a grouped singleton pattern. ## Introduction to caching Caching should be implemented with consideration. Generally, caching the result of resources -is faster than reprocessing them. Choosing what, how and when to cache is vital. [PHP APC](http://php.net/manual/en/book.apc.php) is one of the fastest caching systems available, closely followed by [Memcached](http://memcached.org/). [SQLite](http://www.sqlite.org/) and File caching are two of the slowest cache methods, however usually faster than reprocessing +is faster than reprocessing them. Choosing what, how and when to cache is vital. [PHP APCu](http://php.net/manual/en/book.apcu.php) is one of the fastest caching systems available, closely followed by [Memcached](http://memcached.org/). [SQLite](http://www.sqlite.org/) and File caching are two of the slowest cache methods, however usually faster than reprocessing a complex set of instructions. Caching engines that use memory are considerably faster than file based alternatives. But memory is limited whereas disk space is plentiful. If caching large datasets, such as large database result sets, it is best to use file caching. - [!!] Cache drivers require the relevant PHP extensions to be installed. APC, eAccelerator, Memecached and Xcache all require non-standard PHP extensions. +[!!] Cache drivers require the relevant PHP extensions to be installed. APC, eAccelerator, Memecached and Xcache all require non-standard PHP extensions. ## What the Kohana Cache module does (and does not do) @@ -43,7 +43,7 @@ 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 +APC/APCu | __Memory__ | Excellent | No | No | Yes | Widely available PHP opcode caching solution, improves php execution performance 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 and serialization @@ -54,4 +54,4 @@ It is possible to have hybrid cache solutions that use a combination of the engi ## Minimum requirements * Kohana 3.0.4 - * PHP 5.2.4 or greater \ No newline at end of file + * PHP 5.2.4 or greater diff --git a/modules/cache/guide/cache/usage.md b/modules/cache/guide/cache/usage.md index 15d7c52..bc1d285 100644 --- a/modules/cache/guide/cache/usage.md +++ b/modules/cache/guide/cache/usage.md @@ -9,7 +9,7 @@ Creating a new _Kohana Cache_ instance is simple, however it must be done using // Create a new instance of cache using the default group $cache = Cache::instance(); -The default group will use whatever is set to [Cache::$default] and must have a corresponding [configuration](cache.config) definition for that group. +The default group will use whatever is set to [Cache::$default] and must have a corresponding [configuration](config) definition for that group. To create a cache instance using a group other than the _default_, simply provide the group name as an argument. @@ -18,7 +18,7 @@ To create a cache instance using a group other than the _default_, simply provid If there is a cache instance already instantiated then you can get it directly from the class member. - [!!] Beware that this can cause issues if you do not test for the instance before trying to access it. +[!!] Beware that this can cause issues if you do not test for the instance before trying to access it. // Check for the existance of the cache driver if (isset(Cache::$instances['memcache'])) @@ -36,11 +36,11 @@ If there is a cache instance already instantiated then you can get it directly f The cache library supports scalar and object values, utilising object serialization where required (or not supported by the caching engine). This means that the majority or objects can be cached without any modification. - [!!] Serialisation does not work with resource handles, such as filesystem, curl or socket resources. +[!!] Serialisation does not work with resource handles, such as filesystem, curl or socket resources. ### Setting a value to cache -Setting a value to cache using the [Cache::set] method can be done in one of two ways; either using the Cache instance interface, which is good for atomic operations; or getting an instance and using that for multiple operations. +Setting a value to cache using the [Cache::set] method can be done in one of two ways: either using the Cache instance interface, which is good for atomic operations, or getting an instance and using that for multiple operations. The first example demonstrates how to quickly load and set a value to the default cache instance. @@ -104,7 +104,7 @@ In cases where the requested key is not available or the entry has expired, a de It is possible to retrieve values from cache grouped by tag, using the [Cache::find] method with drivers that support tagging. - [!!] The __Memcachetag__ driver does not support the `Cache::find($tag)` interface and will throw an exception. +[!!] The __Memcachetag__ driver does not support the `Cache::find($tag)` interface and will throw an exception. // Get an instance of cache $cache = Cache::instance('memcachetag'); diff --git a/modules/cache/koharness.php b/modules/cache/koharness.php new file mode 100644 index 0000000..6c3c03c --- /dev/null +++ b/modules/cache/koharness.php @@ -0,0 +1,9 @@ + '3.3', + 'modules' => array( + 'cache' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/cache/tests/cache/CacheBasicMethodsTest.php b/modules/cache/tests/cache/CacheBasicMethodsTest.php index 5fdae60..c85e32b 100644 --- a/modules/cache/tests/cache/CacheBasicMethodsTest.php +++ b/modules/cache/tests/cache/CacheBasicMethodsTest.php @@ -164,6 +164,17 @@ TESTTEXT; ), 'foo bar snafu' ), + array( + array( + 'id' => 'test ttl 0 means never expire', + 'value' => 'cache value that should last', + 'ttl' => 0, + 'wait' => 1, + 'type' => 'string', + 'default' => NULL + ), + 'cache value that should last' + ), array( array( 'id' => 'bar', @@ -196,6 +207,28 @@ TESTTEXT; 'default' => NULL, ), $html_text + ), + array( + array( + 'id' => 'test with 60*5', + 'value' => 'blabla', + 'ttl' => 60*5, + 'wait' => FALSE, + 'type' => 'string', + 'default' => NULL, + ), + 'blabla' + ), + array( + array( + 'id' => 'test with 60*50', + 'value' => 'bla bla', + 'ttl' => 60*50, + 'wait' => FALSE, + 'type' => 'string', + 'default' => NULL, + ), + 'bla bla' ) ); } diff --git a/modules/cache/tests/cache/CacheTest.php b/modules/cache/tests/cache/CacheTest.php index a5c7564..41bc43a 100644 --- a/modules/cache/tests/cache/CacheTest.php +++ b/modules/cache/tests/cache/CacheTest.php @@ -90,14 +90,13 @@ class Kohana_CacheTest extends PHPUnit_Framework_TestCase { */ public function test_cloning_fails() { - if ( ! Kohana::$config->load('cache.file')) - { - $this->markTestSkipped('Unable to load File configuration'); - } + $cache = $this->getMockBuilder('Cache') + ->disableOriginalConstructor() + ->getMockForAbstractClass(); try { - $cache_clone = clone(Cache::instance('file')); + clone($cache); } catch (Cache_Exception $e) { diff --git a/modules/cache/tests/cache/FileTest.php b/modules/cache/tests/cache/FileTest.php index 803258f..a4262be 100644 --- a/modules/cache/tests/cache/FileTest.php +++ b/modules/cache/tests/cache/FileTest.php @@ -30,7 +30,21 @@ class Kohana_Cache_FileTest extends Kohana_CacheBasicMethodsTest { if ( ! Kohana::$config->load('cache.file')) { - $this->markTestSkipped('Unable to load File configuration'); + Kohana::$config->load('cache') + ->set( + 'file', + array( + 'driver' => 'file', + 'cache_dir' => APPPATH.'cache', + 'default_expire' => 3600, + 'ignore_on_delete' => array( + 'file_we_want_to_keep.cache', + '.gitignore', + '.git', + '.svn' + ) + ) + ); } $this->cache(Cache::instance('file')); @@ -45,7 +59,7 @@ class Kohana_Cache_FileTest extends Kohana_CacheBasicMethodsTest { { $cache = $this->cache(); $config = Kohana::$config->load('cache')->file; - $file = $config['cache_dir'].'/.gitignore'; + $file = $config['cache_dir'].'/file_we_want_to_keep.cache'; // Lets pollute the cache folder file_put_contents($file, 'foobar'); @@ -95,4 +109,55 @@ class Kohana_Cache_FileTest extends Kohana_CacheBasicMethodsTest { $this->assertSame($expected, $cache->get('utf8')); } + /** + * Tests garbage collection. + * Tests if non-expired cache files withstand garbage collection + * + * @test + */ + public function test_garbage_collection() + { + $cache = $this->cache(); + $cache->set('persistent', 'dummy persistent data', 3); + $cache->set('volatile', 'dummy volatile data', 1); + + $this->assertTrue($this->is_file('persistent')); + $this->assertTrue($this->is_file('volatile')); + + // sleep for more than a second + sleep(2); + + $cache->garbage_collect(); + + $this->assertTrue($this->is_file('persistent')); + $this->assertFalse($this->is_file('volatile')); + } + + /** + * helper method for test_garbage_collection. + * Tests if cache file exists given cache id. + * + * @param string $id cache id + * @return boolean TRUE if file exists FALSE otherwise + */ + protected function is_file($id) + { + $cache = $this->cache(); + + $method_sanitize_id = new ReflectionMethod($cache, '_sanitize_id'); + $method_sanitize_id->setAccessible(TRUE); + $method_filename = new ReflectionMethod($cache, 'filename'); + $method_filename->setAccessible(TRUE); + $method_resolve_directory = new ReflectionMethod($cache, '_resolve_directory'); + $method_resolve_directory->setAccessible(TRUE); + + $sanitized_id = $method_sanitize_id->invoke($cache, $id); + $filename = $method_filename->invoke($cache, $sanitized_id); + $directory = $method_resolve_directory->invoke($cache, $filename); + + $file = new SplFileInfo($directory.$filename); + + //var_dump($cache->_is_expired($file)); + return $file->isFile(); + } } // End Kohana_SqliteTest diff --git a/modules/cache/tests/cache/SqliteTest.php b/modules/cache/tests/cache/SqliteTest.php index 4a9c2ea..3340d97 100644 --- a/modules/cache/tests/cache/SqliteTest.php +++ b/modules/cache/tests/cache/SqliteTest.php @@ -35,7 +35,16 @@ class Kohana_SqliteTest extends Kohana_CacheBasicMethodsTest { if ( ! Kohana::$config->load('cache.sqlite')) { - $this->markTestIncomplete('Unable to load sqlite configuration'); + Kohana::$config->load('cache') + ->set( + 'sqlite', + array( + 'driver' => 'sqlite', + 'default_expire' => 3600, + 'database' => 'memory', + 'schema' => 'CREATE TABLE caches(id VARCHAR(127) PRIMARY KEY, tags VARCHAR(255), expiration INTEGER, cache TEXT)', + ) + ); } $this->cache(Cache::instance('sqlite')); diff --git a/modules/cache/tests/cache/WincacheTest.php b/modules/cache/tests/cache/WincacheTest.php index 70e6f79..96a51b6 100644 --- a/modules/cache/tests/cache/WincacheTest.php +++ b/modules/cache/tests/cache/WincacheTest.php @@ -1,39 +1,49 @@ markTestSkipped('Wincache PHP Extension is not available'); + parent::setUp(); + + if ( ! extension_loaded('wincache')) + { + $this->markTestSkipped('Wincache PHP Extension is not available'); + } + + $this->cache(Cache::instance('wincache')); } - $this->cache(Cache::instance('wincache')); - } - -} // End Kohana_WincacheTest + } // End Kohana_WincacheTest +} diff --git a/modules/cache/tests/cache/arithmetic/ApcTest.php b/modules/cache/tests/cache/arithmetic/ApcTest.php index e1597cd..f08eae7 100644 --- a/modules/cache/tests/cache/arithmetic/ApcTest.php +++ b/modules/cache/tests/cache/arithmetic/ApcTest.php @@ -39,6 +39,18 @@ class Kohana_ApcTest extends Kohana_CacheArithmeticMethodsTest { 'place "apc.enable_cli=1" in your php.ini file'); } + if ( ! Kohana::$config->load('cache.apc')) + { + Kohana::$config->load('cache') + ->set( + 'apc', + array( + 'driver' => 'apc', + 'default_expire' => 3600, + ) + ); + } + $this->cache(Cache::instance('apc')); } @@ -57,7 +69,7 @@ class Kohana_ApcTest extends Kohana_CacheArithmeticMethodsTest { * * @dataProvider provider_set_get * - * @param array data + * @param array data * @param mixed expected * @return void */ diff --git a/modules/cache/tests/cache/arithmetic/ApcuTest.php b/modules/cache/tests/cache/arithmetic/ApcuTest.php new file mode 100644 index 0000000..1930c98 --- /dev/null +++ b/modules/cache/tests/cache/arithmetic/ApcuTest.php @@ -0,0 +1,87 @@ +markTestSkipped('APCu PHP Extension is not available'); + } + + if ( ! (ini_get('apc.enabled') AND ini_get('apc.enable_cli'))) + { + $this->markTestSkipped('APCu is not enabled. To fix '. + 'set "apc.enabled=1" and "apc.enable_cli=1" in your php.ini file'); + } + + if ( ! Kohana::$config->load('cache.apcu')) + { + Kohana::$config->load('cache') + ->set( + 'apcu', + array( + 'driver' => 'apcu', + 'default_expire' => 3600, + ) + ); + } + + $this->cache(Cache::instance('apcu')); + } + + /** + * 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 APCu 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_ApcuTest diff --git a/modules/cache/tests/cache/arithmetic/MemcacheTest.php b/modules/cache/tests/cache/arithmetic/MemcacheTest.php index 07cb9ef..b29a7bd 100644 --- a/modules/cache/tests/cache/arithmetic/MemcacheTest.php +++ b/modules/cache/tests/cache/arithmetic/MemcacheTest.php @@ -35,7 +35,28 @@ class Kohana_CacheArithmeticMemcacheTest extends Kohana_CacheArithmeticMethodsTe } if ( ! $config = Kohana::$config->load('cache.memcache')) { - $this->markTestSkipped('Unable to load Memcache configuration'); + Kohana::$config->load('cache') + ->set( + 'memcache', + array( + 'driver' => 'memcache', + 'default_expire' => 3600, + 'compression' => FALSE, // Use Zlib compression (can cause issues with integers) + 'servers' => array( + 'local' => array( + 'host' => 'localhost', // Memcache Server + 'port' => 11211, // Memcache port number + 'persistent' => FALSE, // Persistent connection + 'weight' => 1, + 'timeout' => 1, + 'retry_interval' => 15, + 'status' => TRUE, + ), + ), + 'instant_death' => TRUE, + ) + ); + $config = Kohana::$config->load('cache.memcache'); } $memcache = new Memcache; diff --git a/modules/codebench/.travis.yml b/modules/codebench/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/codebench/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/codebench/composer.json b/modules/codebench/composer.json index 8857c68..fa06a86 100644 --- a/modules/codebench/composer.json +++ b/modules/codebench/composer.json @@ -24,10 +24,18 @@ "kohana/core": ">=3.3", "php": ">=5.3.3" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "extra": { "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/codebench/guide/codebench/index.md b/modules/codebench/guide/codebench/index.md index 78b6f21..c652016 100644 --- a/modules/codebench/guide/codebench/index.md +++ b/modules/codebench/guide/codebench/index.md @@ -26,7 +26,7 @@ Throwing valuable benchmark data away every time I needed to optimize another re Obviously providing a visual representation of the benchmark results, via simple graphs, would make interpreting them easier. Having not to think about Internet Explorer for once, made writing CSS a whole lot more easy and fun. It resulted in some fine graphs which are fully resizable. -Below are two screenshots of Codebench in action. `Valid_Color` is a class made for benchmarking different ways to validate hexadecimal HTML color values, e.g. `#FFF`. If you are interested in the story behind the actual regular expressions, take a look at [this topic in the Kohana forums](http://forum.kohanaphp.com/comments.php?DiscussionID=2192). +Below are two screenshots of Codebench in action. `Valid_Color` is a class made for benchmarking different ways to validate hexadecimal HTML color values, e.g. `#FFF`. If you are interested in the story behind the actual regular expressions, take a look at [this topic in the Kohana forums](http://forum.kohanaframework.org/discussion/2192). ![Benchmarking several ways to validate HTML color values](codebench_screenshot1.png) **Benchmarking seven ways to validate HTML color values** @@ -73,4 +73,4 @@ Here is another short example with some extra explanations. -And the winner is… [ltrim](http://php.net/ltrim). Happy benchmarking! \ No newline at end of file +And the winner is… [ltrim](http://php.net/ltrim). Happy benchmarking! diff --git a/modules/codebench/koharness.php b/modules/codebench/koharness.php new file mode 100644 index 0000000..0fb147c --- /dev/null +++ b/modules/codebench/koharness.php @@ -0,0 +1,8 @@ + array( + 'codebench' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/database/.travis.yml b/modules/database/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/database/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/database/README.md b/modules/database/README.md new file mode 100644 index 0000000..6a40ee8 --- /dev/null +++ b/modules/database/README.md @@ -0,0 +1,6 @@ +# Kohana - database access module + +| ver | Stable | Develop | +|-------|--------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/database.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/database) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/database.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/database) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/database.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/database) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/database.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/database) | diff --git a/modules/database/classes/Database/MySQLi.php b/modules/database/classes/Database/MySQLi.php new file mode 100644 index 0000000..df5773f --- /dev/null +++ b/modules/database/classes/Database/MySQLi.php @@ -0,0 +1,3 @@ +_internal_row++; + // FIXME mysql_fetch_object has been deprecated as of php 5.5! + // Please use mysqli_fetch_object or PDOStatement::fetch(PDO::FETCH_OBJ) instead. + if ($this->_as_object === TRUE) { // Return an stdClass @@ -58,8 +61,32 @@ class Kohana_Database_MySQL_Result extends Database_Result { } elseif (is_string($this->_as_object)) { - // Return an object of given class name - return mysql_fetch_object($this->_result, $this->_as_object, $this->_object_params); + /* The second and third argument for mysql_fetch_object are optional, but do + * not have default values defined. Passing _object_params with a non-array value results + * in undefined behavior that varies by PHP version. For example, if NULL is supplied on + * PHP 5.3, the resulting behavior is identical to calling with array(), which results in the + * classes __construct function being called with no arguments. This is only an issue when + * the _as_object class does not have an explicit __construct method resulting in the + * cryptic error "Class %s does not have a constructor hence you cannot use ctor_params." + * In contrast, the same function call on PHP 5.5 will 'functionally' interpret + * _object_params == NULL as an omission of the third argument, resulting in the original + * intended functionally. + * + * Because the backing code for the mysql_fetch_object has not changed between 5.3 and 5.5, + * I suspect this discrepancy is due to the way the classes are instantiated on a boarder + * level. Additionally, mysql_fetch_object has been deprecated in 5.5 and should probably be + * replaced by mysqli_fetch_object or PDOStatement::fetch(PDO::FETCH_OBJ) in Kohana 3.4. + */ + if ($this->_object_params !== NULL) + { + // Return an object of given class name with constructor params + return mysql_fetch_object($this->_result, $this->_as_object, $this->_object_params); + } + else + { + // Return an object of given class name without constructor params + return mysql_fetch_object($this->_result, $this->_as_object); + } } else { diff --git a/modules/database/classes/Kohana/Database/MySQLi.php b/modules/database/classes/Kohana/Database/MySQLi.php new file mode 100644 index 0000000..1684047 --- /dev/null +++ b/modules/database/classes/Kohana/Database/MySQLi.php @@ -0,0 +1,422 @@ +_connection) + return; + + if (Database_MySQLi::$_set_names === NULL) + { + // Determine if we can use mysqli_set_charset(), which is only + // available on PHP 5.2.3+ when compiled against MySQL 5.0+ + Database_MySQLi::$_set_names = ! function_exists('mysqli_set_charset'); + } + + // Extract the connection parameters, adding required variabels + extract($this->_config['connection'] + array( + 'database' => '', + 'hostname' => '', + 'username' => '', + 'password' => '', + 'socket' => '', + 'port' => 3306, + 'ssl' => NULL, + )); + + // Prevent this information from showing up in traces + unset($this->_config['connection']['username'], $this->_config['connection']['password']); + + try + { + if(is_array($ssl)) + { + $this->_connection = mysqli_init(); + $this->_connection->ssl_set( + Arr::get($ssl, 'client_key_path'), + Arr::get($ssl, 'client_cert_path'), + Arr::get($ssl, 'ca_cert_path'), + Arr::get($ssl, 'ca_dir_path'), + Arr::get($ssl, 'cipher') + ); + $this->_connection->real_connect($hostname, $username, $password, $database, $port, $socket, MYSQLI_CLIENT_SSL); + } + else + { + $this->_connection = new mysqli($hostname, $username, $password, $database, $port, $socket); + } + } + catch (Exception $e) + { + // No connection exists + $this->_connection = NULL; + + throw new Database_Exception(':error', array(':error' => $e->getMessage()), $e->getCode()); + } + + // \xFF is a better delimiter, but the PHP driver uses underscore + $this->_connection_id = sha1($hostname.'_'.$username.'_'.$password); + + if ( ! empty($this->_config['charset'])) + { + // 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); + } + + $this->_connection->query('SET '.implode(', ', $variables)); + } + } + + public function disconnect() + { + try + { + // Database is assumed disconnected + $status = TRUE; + + if (is_resource($this->_connection)) + { + if ($status = $this->_connection->close()) + { + // Clear the connection + $this->_connection = NULL; + + // Clear the instance + parent::disconnect(); + } + } + } + catch (Exception $e) + { + // Database is probably not disconnected + $status = ! is_resource($this->_connection); + } + + return $status; + } + + public function set_charset($charset) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if (Database_MySQLi::$_set_names === TRUE) + { + // PHP is compiled against MySQL 4.x + $status = (bool) $this->_connection->query('SET NAMES '.$this->quote($charset)); + } + else + { + // PHP is compiled against MySQL 5.x + $status = $this->_connection->set_charset($charset); + } + + if ($status === FALSE) + { + throw new Database_Exception(':error', array(':error' => $this->_connection->error), $this->_connection->errno); + } + } + + public function query($type, $sql, $as_object = FALSE, array $params = NULL) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if (Kohana::$profiling) + { + // Benchmark this query for the current instance + $benchmark = Profiler::start("Database ({$this->_instance})", $sql); + } + + // Execute the query + if (($result = $this->_connection->query($sql)) === FALSE) + { + if (isset($benchmark)) + { + // This benchmark is worthless + Profiler::delete($benchmark); + } + + throw new Database_Exception(':error [ :query ]', array( + ':error' => $this->_connection->error, + ':query' => $sql + ), $this->_connection->errno); + } + + if (isset($benchmark)) + { + Profiler::stop($benchmark); + } + + // Set the last query + $this->last_query = $sql; + + if ($type === Database::SELECT) + { + // Return an iterator of results + return new Database_MySQLi_Result($result, $sql, $as_object, $params); + } + elseif ($type === Database::INSERT) + { + // Return a list of insert id and rows created + return array( + $this->_connection->insert_id, + $this->_connection->affected_rows, + ); + } + else + { + // Return the number of rows affected + return $this->_connection->affected_rows; + } + } + + public function datatype($type) + { + static $types = array + ( + 'blob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '65535'), + 'bool' => array('type' => 'bool'), + 'bigint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '18446744073709551615'), + 'datetime' => array('type' => 'string'), + 'decimal unsigned' => array('type' => 'float', 'exact' => TRUE, 'min' => '0'), + 'double' => array('type' => 'float'), + 'double precision unsigned' => array('type' => 'float', 'min' => '0'), + 'double unsigned' => array('type' => 'float', 'min' => '0'), + 'enum' => array('type' => 'string'), + 'fixed' => array('type' => 'float', 'exact' => TRUE), + 'fixed unsigned' => array('type' => 'float', 'exact' => TRUE, 'min' => '0'), + 'float unsigned' => array('type' => 'float', 'min' => '0'), + 'geometry' => array('type' => 'string', 'binary' => TRUE), + 'int unsigned' => array('type' => 'int', 'min' => '0', 'max' => '4294967295'), + 'integer unsigned' => array('type' => 'int', 'min' => '0', 'max' => '4294967295'), + 'longblob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '4294967295'), + 'longtext' => array('type' => 'string', 'character_maximum_length' => '4294967295'), + 'mediumblob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '16777215'), + 'mediumint' => array('type' => 'int', 'min' => '-8388608', 'max' => '8388607'), + 'mediumint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '16777215'), + 'mediumtext' => array('type' => 'string', 'character_maximum_length' => '16777215'), + 'national varchar' => array('type' => 'string'), + 'numeric unsigned' => array('type' => 'float', 'exact' => TRUE, 'min' => '0'), + 'nvarchar' => array('type' => 'string'), + 'point' => array('type' => 'string', 'binary' => TRUE), + 'real unsigned' => array('type' => 'float', 'min' => '0'), + 'set' => array('type' => 'string'), + 'smallint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '65535'), + 'text' => array('type' => 'string', 'character_maximum_length' => '65535'), + 'tinyblob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '255'), + 'tinyint' => array('type' => 'int', 'min' => '-128', 'max' => '127'), + 'tinyint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '255'), + 'tinytext' => array('type' => 'string', 'character_maximum_length' => '255'), + 'year' => array('type' => 'string'), + ); + + $type = str_replace(' zerofill', '', $type); + + if (isset($types[$type])) + return $types[$type]; + + return parent::datatype($type); + } + + /** + * Start a SQL transaction + * + * @link http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + * + * @param string $mode Isolation level + * @return boolean + */ + public function begin($mode = NULL) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if ($mode AND ! $this->_connection->query("SET TRANSACTION ISOLATION LEVEL $mode")) + { + throw new Database_Exception(':error', array( + ':error' => $this->_connection->error + ), $this->_connection->errno); + } + + return (bool) $this->_connection->query('START TRANSACTION'); + } + + /** + * Commit a SQL transaction + * + * @return boolean + */ + public function commit() + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + return (bool) $this->_connection->query('COMMIT'); + } + + /** + * Rollback a SQL transaction + * + * @return boolean + */ + public function rollback() + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + return (bool) $this->_connection->query('ROLLBACK'); + } + + public function list_tables($like = NULL) + { + if (is_string($like)) + { + // Search for table names + $result = $this->query(Database::SELECT, 'SHOW TABLES LIKE '.$this->quote($like), FALSE); + } + else + { + // Find all table names + $result = $this->query(Database::SELECT, 'SHOW TABLES', FALSE); + } + + $tables = array(); + foreach ($result as $row) + { + $tables[] = reset($row); + } + + return $tables; + } + + public function list_columns($table, $like = NULL, $add_prefix = TRUE) + { + // Quote the table name + $table = ($add_prefix === TRUE) ? $this->quote_table($table) : $table; + + if (is_string($like)) + { + // Search for column names + $result = $this->query(Database::SELECT, 'SHOW FULL COLUMNS FROM '.$table.' LIKE '.$this->quote($like), FALSE); + } + else + { + // Find all column names + $result = $this->query(Database::SELECT, 'SHOW FULL COLUMNS FROM '.$table, FALSE); + } + + $count = 0; + $columns = array(); + foreach ($result as $row) + { + list($type, $length) = $this->_parse_type($row['Type']); + + $column = $this->datatype($type); + + $column['column_name'] = $row['Field']; + $column['column_default'] = $row['Default']; + $column['data_type'] = $type; + $column['is_nullable'] = ($row['Null'] == 'YES'); + $column['ordinal_position'] = ++$count; + + switch ($column['type']) + { + case 'float': + if (isset($length)) + { + list($column['numeric_precision'], $column['numeric_scale']) = explode(',', $length); + } + break; + case 'int': + if (isset($length)) + { + // MySQL attribute + $column['display'] = $length; + } + break; + case 'string': + switch ($column['data_type']) + { + case 'binary': + case 'varbinary': + $column['character_maximum_length'] = $length; + break; + case 'char': + case 'varchar': + $column['character_maximum_length'] = $length; + case 'text': + case 'tinytext': + case 'mediumtext': + case 'longtext': + $column['collation_name'] = $row['Collation']; + break; + case 'enum': + case 'set': + $column['collation_name'] = $row['Collation']; + $column['options'] = explode('\',\'', substr($length, 1, -1)); + break; + } + break; + } + + // MySQL attributes + $column['comment'] = $row['Comment']; + $column['extra'] = $row['Extra']; + $column['key'] = $row['Key']; + $column['privileges'] = $row['Privileges']; + + $columns[$row['Field']] = $column; + } + + return $columns; + } + + public function escape($value) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if (($value = $this->_connection->real_escape_string( (string) $value)) === FALSE) + { + throw new Database_Exception(':error', array( + ':error' => $this->_connection->error, + ), $this->_connection->errno); + } + + // SQL standard is to use single-quotes for all values + return "'$value'"; + } + +} // End Database_MySQLi diff --git a/modules/database/classes/Kohana/Database/MySQLi/Result.php b/modules/database/classes/Kohana/Database/MySQLi/Result.php new file mode 100644 index 0000000..4a12441 --- /dev/null +++ b/modules/database/classes/Kohana/Database/MySQLi/Result.php @@ -0,0 +1,71 @@ +_total_rows = $result->num_rows; + } + + public function __destruct() + { + if (is_resource($this->_result)) + { + $this->_result->free(); + } + } + + public function seek($offset) + { + if ($this->offsetExists($offset) AND $this->_result->data_seek($offset)) + { + // Set the current row to the offset + $this->_current_row = $this->_internal_row = $offset; + + return TRUE; + } + else + { + return FALSE; + } + } + + public function current() + { + if ($this->_current_row !== $this->_internal_row AND ! $this->seek($this->_current_row)) + return NULL; + + // Increment internal row for optimization assuming rows are fetched in order + $this->_internal_row++; + + if ($this->_as_object === TRUE) + { + // Return an stdClass + return $this->_result->fetch_object(); + } + elseif (is_string($this->_as_object)) + { + // Return an object of given class name + return $this->_result->fetch_object($this->_as_object, (array) $this->_object_params); + } + else + { + // Return an array of the row + return $this->_result->fetch_assoc(); + } + } + +} // End Database_MySQLi_Result_Select diff --git a/modules/database/classes/Kohana/Database/PDO.php b/modules/database/classes/Kohana/Database/PDO.php index 573f8d3..ba6de22 100644 --- a/modules/database/classes/Kohana/Database/PDO.php +++ b/modules/database/classes/Kohana/Database/PDO.php @@ -60,6 +60,12 @@ 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']); + } } /** diff --git a/modules/database/classes/Kohana/Database/Query/Builder.php b/modules/database/classes/Kohana/Database/Query/Builder.php index 3e1bdb5..c0915dc 100644 --- a/modules/database/classes/Kohana/Database/Query/Builder.php +++ b/modules/database/classes/Kohana/Database/Query/Builder.php @@ -80,7 +80,7 @@ abstract class Kohana_Database_Query_Builder extends Database_Query { // Convert "val = NULL" to "val IS NULL" $op = 'IS'; } - elseif ($op === '!=') + elseif ($op === '!=' OR $op === '<>') { // Convert "val != NULL" to "valu IS NOT NULL" $op = 'IS NOT'; diff --git a/modules/database/classes/Kohana/Database/Query/Builder/Insert.php b/modules/database/classes/Kohana/Database/Query/Builder/Insert.php index aa3c807..4d11371 100644 --- a/modules/database/classes/Kohana/Database/Query/Builder/Insert.php +++ b/modules/database/classes/Kohana/Database/Query/Builder/Insert.php @@ -31,7 +31,7 @@ class Kohana_Database_Query_Builder_Insert extends Database_Query_Builder { if ($table) { // Set the inital table name - $this->_table = $table; + $this->table($table); } if ($columns) @@ -47,11 +47,14 @@ class Kohana_Database_Query_Builder_Insert extends Database_Query_Builder { /** * Sets the table to insert into. * - * @param mixed $table table name or array($table, $alias) or object + * @param string $table table name * @return $this */ public function table($table) { + if ( ! is_string($table)) + throw new Kohana_Exception('INSERT INTO syntax does not allow table aliasing'); + $this->_table = $table; return $this; @@ -86,8 +89,11 @@ class Kohana_Database_Query_Builder_Insert extends Database_Query_Builder { // Get all of the passed values $values = func_get_args(); - - $this->_values = array_merge($this->_values, $values); + + foreach ($values as $value) + { + $this->_values[] = $value; + } return $this; } diff --git a/modules/database/classes/Kohana/Database/Query/Builder/Select.php b/modules/database/classes/Kohana/Database/Query/Builder/Select.php index 3492a71..21da70a 100644 --- a/modules/database/classes/Kohana/Database/Query/Builder/Select.php +++ b/modules/database/classes/Kohana/Database/Query/Builder/Select.php @@ -309,7 +309,7 @@ class Kohana_Database_Query_Builder_Select extends Database_Query_Builder_Where */ public function offset($number) { - $this->_offset = $number; + $this->_offset = ($number === NULL) ? NULL : (int) $number; return $this; } @@ -404,13 +404,14 @@ class Kohana_Database_Query_Builder_Select extends Database_Query_Builder_Where if ( ! empty($this->_union)) { + $query = '('.$query.')'; foreach ($this->_union as $u) { $query .= ' UNION '; if ($u['all'] === TRUE) { $query .= 'ALL '; } - $query .= $u['select']->compile($db); + $query .= '('.$u['select']->compile($db).')'; } } diff --git a/modules/database/classes/Kohana/Database/Query/Builder/Where.php b/modules/database/classes/Kohana/Database/Query/Builder/Where.php index 58f6b5d..a6589e4 100644 --- a/modules/database/classes/Kohana/Database/Query/Builder/Where.php +++ b/modules/database/classes/Kohana/Database/Query/Builder/Where.php @@ -172,7 +172,7 @@ abstract class Kohana_Database_Query_Builder_Where extends Database_Query_Builde */ public function limit($number) { - $this->_limit = $number; + $this->_limit = ($number === NULL) ? NULL : (int) $number; return $this; } diff --git a/modules/database/classes/Kohana/Session/Database.php b/modules/database/classes/Kohana/Session/Database.php index 34abbbd..35d73b9 100644 --- a/modules/database/classes/Kohana/Session/Database.php +++ b/modules/database/classes/Kohana/Session/Database.php @@ -204,6 +204,9 @@ class Kohana_Session_Database extends Session { // Execute the query $query->execute($this->_db); + // Delete the old session id + $this->_update_id = NULL; + // Delete the cookie Cookie::delete($this->_name); } diff --git a/modules/database/composer.json b/modules/database/composer.json index 33b1962..8a3ddd2 100644 --- a/modules/database/composer.json +++ b/modules/database/composer.json @@ -22,8 +22,13 @@ "require": { "composer/installers": "~1.0", "kohana/core": ">=3.3", - "php": ">=5.3.3" + "php": ">=5.3.6" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "suggest": { "ext-mysql": "*", "ext-pdo": "*" @@ -32,6 +37,9 @@ "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/database/config/database.php b/modules/database/config/database.php index e2021bd..a19d9e4 100644 --- a/modules/database/config/database.php +++ b/modules/database/config/database.php @@ -53,4 +53,38 @@ return array 'charset' => 'utf8', 'caching' => FALSE, ), + /** + * MySQLi driver config information + * + * The following options are available for MySQLi: + * + * string hostname server hostname, or socket + * string database database name + * string username database username + * string password database password + * boolean persistent use persistent connections? + * array ssl ssl parameters as "key => value" pairs. + * Available keys: client_key_path, client_cert_path, ca_cert_path, ca_dir_path, cipher + * array variables system variables as "key => value" pairs + * + * Ports and sockets may be appended to the hostname. + * + * MySQLi driver config example: + * + */ +// 'alternate_mysqli' => array +// ( +// 'type' => 'MySQLi', +// 'connection' => array( +// 'hostname' => 'localhost', +// 'database' => 'kohana', +// 'username' => FALSE, +// 'password' => FALSE, +// 'persistent' => FALSE, +// 'ssl' => NULL, +// ), +// 'table_prefix' => '', +// 'charset' => 'utf8', +// 'caching' => FALSE, +// ), ); diff --git a/modules/database/guide/database/config.md b/modules/database/guide/database/config.md index 27ae9b4..5d3785d 100644 --- a/modules/database/guide/database/config.md +++ b/modules/database/guide/database/config.md @@ -10,6 +10,7 @@ The database configuration file contains an array of configuration groups. The s 'table_prefix' => string TABLE_PREFIX, 'charset' => string CHARACTER_SET, ), + Understanding each of these settings is important. @@ -17,7 +18,7 @@ INSTANCE_NAME : Connections can be named anything you want, but you should always have at least one connection called "default". DATABASE_TYPE -: One of the installed database drivers. Kohana comes with "mysql" and "pdo" drivers. Drivers must extend the Database class. +: One of the installed database drivers. Kohana comes with "MySQL", "MySQLi", and "PDO" drivers. Drivers must extend the Database class. This parameter is case sensitive. Note the mysql php extension used by the MySQL driver is deprecated as of PHP 5.5 and you should look to use an alternative driver. CONNECTION_ARRAY : Specific driver options for connecting to your database. (Driver options are explained [below](#connection-settings).) @@ -49,7 +50,7 @@ The example file below shows 2 MySQL connections, one local and one remote. ( 'default' => array ( - 'type' => 'mysql', + 'type' => 'MySQL', 'connection' => array( 'hostname' => 'localhost', 'username' => 'dbuser', @@ -61,7 +62,7 @@ The example file below shows 2 MySQL connections, one local and one remote. 'charset' => 'utf8', ), 'remote' => array( - 'type' => 'mysql', + 'type' => 'MySQL', 'connection' => array( 'hostname' => '55.55.55.55', 'username' => 'remote_user', @@ -74,6 +75,8 @@ The example file below shows 2 MySQL connections, one local and one remote. ), ); +[!!] Note that the 'type' parameter is case sensitive (eg 'MySQL', 'PDO'). + ## Connections and Instances Each configuration group is referred to as a database instance. Each instance can be accessed by calling [Database::instance]. If you don't provide a parameter, the default instance is used. @@ -114,6 +117,24 @@ Type | Option | Description | Default value `boolean` | persistent | Persistent connections | `FALSE` `string` | database | Database name | `kohana` +### MySQLi + +A [MySQL database](http://php.net/manual/en/book.mysqli.php) can accept the following options in the `connection` array: + +Type | Option | Description | Default value +----------|------------|----------------------------| ------------------------- +`string` | hostname | Hostname of the database | `localhost` +`integer` | port | Port number | `NULL` +`string` | socket | UNIX socket | `NULL` +`string` | username | Database username | `NULL` +`string` | password | Database password | `NULL` +`boolean` | persistent | Persistent connections | `FALSE` +`string` | database | Database name | `kohana` +`array` | ssl | SSL parameters | `NULL` + +SSL parameters should be specified as `key` => `value` pairs. +Available keys: client_key_path, client_cert_path, ca_cert_path, ca_dir_path, cipher + ### PDO A [PDO database](http://php.net/manual/en/book.pdo.php) can accept these options in the `connection` array: @@ -128,4 +149,4 @@ Type | Option | Description | Default value 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 +[!!] If you are using PDO and are not sure what to use for the `dsn` option, review [PDO::__construct](http://php.net/pdo.construct). diff --git a/modules/database/guide/database/examples.md b/modules/database/guide/database/examples.md index 6a9d1b5..ff14ec5 100644 --- a/modules/database/guide/database/examples.md +++ b/modules/database/guide/database/examples.md @@ -25,7 +25,7 @@ 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(DB::expr('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->load('pagination'); @@ -49,4 +49,4 @@ In this example, we loop through an array of whitelisted input fields and for ea TODO: example goes here -[!!] We could use more examples on this page. \ No newline at end of file +[!!] We could use more examples on this page. diff --git a/modules/database/guide/database/query/builder.md b/modules/database/guide/database/query/builder.md index d2fd893..2b03b19 100644 --- a/modules/database/guide/database/query/builder.md +++ b/modules/database/guide/database/query/builder.md @@ -76,7 +76,7 @@ This query would generate the following SQL: Often you will want the results in a particular order and rather than sorting the results, it's better to have the results returned to you in the correct order. You can do this by using the order_by() method. It takes the column name and an optional direction string as the parameters. Multiple `order_by()` methods can be used to add additional sorting capability. - $query = DB::select()->from(`posts`)->order_by(`published`, `DESC`); + $query = DB::select()->from('posts')->order_by('published', 'DESC'); This query would generate the following SQL: @@ -248,4 +248,4 @@ Once you are done building, you can execute the query using `execute()` and use To use a different database [config group](config) pass either the name or the config object to `execute()`. - $result = $query->execute('config_name') \ No newline at end of file + $result = $query->execute('config_name') diff --git a/modules/database/koharness.php b/modules/database/koharness.php new file mode 100644 index 0000000..fd2366d --- /dev/null +++ b/modules/database/koharness.php @@ -0,0 +1,8 @@ + array( + 'database' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/image/.travis.yml b/modules/image/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/image/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/image/README.markdown b/modules/image/README.markdown index e69de29..7e03525 100644 --- a/modules/image/README.markdown +++ b/modules/image/README.markdown @@ -0,0 +1,6 @@ +# Kohana - image processing module + +| ver | Stable | Develop | +|-------|--------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/image.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/image) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/image.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/image) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/image.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/image) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/image.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/image) | diff --git a/modules/image/classes/Kohana/Image.php b/modules/image/classes/Kohana/Image.php index 3fe91fc..b67e6a6 100644 --- a/modules/image/classes/Kohana/Image.php +++ b/modules/image/classes/Kohana/Image.php @@ -23,6 +23,7 @@ abstract class Kohana_Image { const VERTICAL = 0x12; /** + * @deprecated - provide an image.default_driver value in your configuration instead * @var string default driver: GD, ImageMagick, etc */ public static $default_driver = 'GD'; @@ -44,8 +45,9 @@ abstract class Kohana_Image { { if ($driver === NULL) { - // Use the default driver - $driver = Image::$default_driver; + // Use the driver from configuration file or default one + $configured_driver = Kohana::$config->load('image.default_driver'); + $driver = ($configured_driver) ? $configured_driver : Image::$default_driver; } // Set the class name diff --git a/modules/image/classes/Kohana/Image/GD.php b/modules/image/classes/Kohana/Image/GD.php index e9c7988..5b52cab 100644 --- a/modules/image/classes/Kohana/Image/GD.php +++ b/modules/image/classes/Kohana/Image/GD.php @@ -611,6 +611,7 @@ class Kohana_Image_GD extends Image { switch (strtolower($extension)) { case 'jpg': + case 'jpe': case 'jpeg': // Save a JPG file $save = 'imagejpeg'; diff --git a/modules/image/classes/Kohana/Image/Imagick.php b/modules/image/classes/Kohana/Image/Imagick.php index 21adbbf..aa060c2 100644 --- a/modules/image/classes/Kohana/Image/Imagick.php +++ b/modules/image/classes/Kohana/Image/Imagick.php @@ -314,6 +314,7 @@ class Kohana_Image_Imagick extends Image { switch ($format) { case 'jpg': + case 'jpe': case 'jpeg': $type = IMAGETYPE_JPEG; break; diff --git a/modules/image/composer.json b/modules/image/composer.json index ed5cae4..a5c6049 100644 --- a/modules/image/composer.json +++ b/modules/image/composer.json @@ -24,6 +24,11 @@ "kohana/core": ">=3.3", "php": ">=5.3.3" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "suggest": { "ext-gd": "*" }, @@ -31,6 +36,9 @@ "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/image/config/image.php b/modules/image/config/image.php new file mode 100644 index 0000000..2d8bb04 --- /dev/null +++ b/modules/image/config/image.php @@ -0,0 +1,8 @@ + NULL, +); diff --git a/modules/image/guide/image/index.md b/modules/image/guide/image/index.md index 0225c26..dd1e6b8 100644 --- a/modules/image/guide/image/index.md +++ b/modules/image/guide/image/index.md @@ -4,7 +4,24 @@ Kohana 3.x provides a simple yet powerful image manipulation module. The [Image] ## 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. +[Image] module ships with [Image_GD] driver which requires `GD` extension enabled in your PHP installation, and +[Image_Imagick] driver which requires the `imagick` PHP extension. Additional drivers can be created by extending +the [Image] class. + +The [Image_GD] driver is the default. You can change this by providing an `image.default_driver` configuration option +- for example: + +~~~ +// application/config/image.php + 'Imagick' +); +~~~ + +[!!] Older versions of Kohana allowed you to configure the driver with the `Image::$default_driver` static variable in +the bootstrap, an extension class, or elsewhere. That variable is now deprecated and will be ignored if you set a +config value. ## Getting Started @@ -18,4 +35,4 @@ Kohana::modules(array( )); ~~~ -Next: [Using the image module](using). \ No newline at end of file +Next: [Using the image module](using). diff --git a/modules/image/koharness.php b/modules/image/koharness.php new file mode 100644 index 0000000..fc5e95a --- /dev/null +++ b/modules/image/koharness.php @@ -0,0 +1,8 @@ + array( + 'image' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/minion/.travis.yml b/modules/minion/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/minion/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/minion/README.md b/modules/minion/README.md index 6d317cb..15c6525 100644 --- a/modules/minion/README.md +++ b/modules/minion/README.md @@ -1,5 +1,10 @@ # Minion +| ver | Stable | Develop | +|-------|----------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/minion.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/minion) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/minion.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/minion) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/minion.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/minion) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/minion.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/minion) | + Minion is a framework for running tasks via the CLI. The system is inspired by ruckusing, which had a nice system for defining tasks but lacked the desired flexibility for kohana integration. diff --git a/modules/minion/classes/Kohana/Minion/CLI.php b/modules/minion/classes/Kohana/Minion/CLI.php index bab6c1c..e1ae3e9 100644 --- a/modules/minion/classes/Kohana/Minion/CLI.php +++ b/modules/minion/classes/Kohana/Minion/CLI.php @@ -170,7 +170,7 @@ class Kohana_Minion_CLI { // Create temporary file file_put_contents($vbscript, 'wscript.echo(InputBox("'.addslashes($text).'"))'); - $password = shell_exec('cscript //nologo '.escapeshellarg($text)); + $password = shell_exec('cscript //nologo '.escapeshellarg($vbscript)); // Remove temporary file. unlink($vbscript); @@ -278,7 +278,7 @@ class Kohana_Minion_CLI { * @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 $foreground the foreground color * @param string $background the background color * @return string the color coded string */ diff --git a/modules/minion/classes/Kohana/Minion/Exception.php b/modules/minion/classes/Kohana/Minion/Exception.php index 735dac4..b8044cb 100644 --- a/modules/minion/classes/Kohana/Minion/Exception.php +++ b/modules/minion/classes/Kohana/Minion/Exception.php @@ -1,6 +1,6 @@ format_for_cli(); diff --git a/modules/minion/composer.json b/modules/minion/composer.json index af7e9a0..a32e30b 100644 --- a/modules/minion/composer.json +++ b/modules/minion/composer.json @@ -24,10 +24,18 @@ "kohana/core": ">=3.3", "php": ">=5.3.3" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "extra": { "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/minion/koharness.php b/modules/minion/koharness.php new file mode 100644 index 0000000..fa0ed4d --- /dev/null +++ b/modules/minion/koharness.php @@ -0,0 +1,8 @@ + array( + 'minion' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/minion/tests/minion/task.php b/modules/minion/tests/minion/task.php deleted file mode 100644 index e7918a0..0000000 --- a/modules/minion/tests/minion/task.php +++ /dev/null @@ -1,70 +0,0 @@ -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/modules/orm/.travis.yml b/modules/orm/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/orm/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/orm/README.md b/modules/orm/README.md new file mode 100644 index 0000000..fd6bc83 --- /dev/null +++ b/modules/orm/README.md @@ -0,0 +1,6 @@ +# Kohana - ORM module + +| ver | Stable | Develop | +|-------|----------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/orm.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/orm) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/orm.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/orm) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/orm.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/orm) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/orm.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/orm) | diff --git a/modules/orm/classes/Kohana/ORM.php b/modules/orm/classes/Kohana/ORM.php index 50067c4..bda79f2 100644 --- a/modules/orm/classes/Kohana/ORM.php +++ b/modules/orm/classes/Kohana/ORM.php @@ -1627,7 +1627,7 @@ class Kohana_ORM extends Model implements serializable { if ($method['name'] == 'select') { // Ignore any selected columns for now - $selects[] = $method; + $selects[$key] = $method; unset($this->_db_pending[$key]); } } diff --git a/modules/orm/composer.json b/modules/orm/composer.json index faa776f..41f4928 100644 --- a/modules/orm/composer.json +++ b/modules/orm/composer.json @@ -25,10 +25,19 @@ "kohana/database": ">=3.3", "php": ">=5.3.3" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/database": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "extra": { "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/orm/koharness.php b/modules/orm/koharness.php new file mode 100644 index 0000000..e2c0b77 --- /dev/null +++ b/modules/orm/koharness.php @@ -0,0 +1,8 @@ + array( + 'orm' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/unittest/.travis.yml b/modules/unittest/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/unittest/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/unittest/README.markdown b/modules/unittest/README.markdown index af4251a..ad96c7c 100644 --- a/modules/unittest/README.markdown +++ b/modules/unittest/README.markdown @@ -1,13 +1,29 @@ # Kohana-PHPUnit integration -This module integrates PHPUnit with Kohana. +| ver | Stable | Develop | +|-------|--------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/unittest.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/unittest) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/unittest.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/unittest) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/unittest.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/unittest) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/unittest.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/unittest) | + +This module integrates PHPUnit with Kohana and is used to run all the core Kohana tests. In most cases you will not +need to use this module for testing your own projects. If there are particular helpers provided here that you rely on, +that may be a sign that your own code is too closely coupled to the behaviour of the Kohana core classes. If you look through any of the tests provided in this module you'll probably notice all theHorribleCamelCase. I've chosen to do this because it's part of the PHPUnit coding conventions and is required for certain features such as auto documentation. -## Requirements +## Requirements and installation -* [PHPUnit](http://www.phpunit.de/) >= 3.4 +Dependencies are listed in the composer.json - run `composer install` to install the module and all external requirements. +Note that more usually you will add this module to your own module's composer.json: + +```json +{ + "require-dev": { + "kohana/unittest": "3.3.*@dev" + } +} +``` ## Usage diff --git a/modules/unittest/bootstrap.php b/modules/unittest/bootstrap.php index 177dae1..f4ce048 100644 --- a/modules/unittest/bootstrap.php +++ b/modules/unittest/bootstrap.php @@ -121,5 +121,10 @@ if (($ob_len = ob_get_length()) !== FALSE) } } -// Enable the unittest module -Kohana::modules(Kohana::modules() + array('unittest' => MODPATH.'unittest')); \ No newline at end of file +// Enable the unittest module if it is not already loaded - use the absolute path +$modules = Kohana::modules(); +$unittest_path = realpath(__DIR__).DIRECTORY_SEPARATOR; +if ( ! in_array($unittest_path, $modules)) { + $modules['unittest'] = $unittest_path; + Kohana::modules($modules); +} diff --git a/modules/unittest/bootstrap_all_modules.php b/modules/unittest/bootstrap_all_modules.php index 9a591a2..3121041 100644 --- a/modules/unittest/bootstrap_all_modules.php +++ b/modules/unittest/bootstrap_all_modules.php @@ -9,7 +9,7 @@ $modules = array(); foreach ($modules_iterator as $module) { - if ($module->isDir()) + if ($module->isDir() AND ! $module->isDot()) { $modules[$module->getFilename()] = MODPATH.$module->getFilename(); } diff --git a/modules/unittest/classes/Kohana/Unittest/Database/TestCase.php b/modules/unittest/classes/Kohana/Unittest/Database/TestCase.php index a6353c8..6693e9a 100644 --- a/modules/unittest/classes/Kohana/Unittest/Database/TestCase.php +++ b/modules/unittest/classes/Kohana/Unittest/Database/TestCase.php @@ -1,4 +1,4 @@ -_helpers = new Kohana_Unittest_Helpers; $this->setEnvironment($this->environmentDefault); @@ -161,151 +146,4 @@ abstract class Kohana_Unittest_Database_TestCase extends PHPUnit_Extensions_Data return Kohana_Unittest_Helpers::has_internet(); } - /** - * Asserts that a variable is of a given type. - * - * @param string $expected - * @param mixed $actual - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertInstanceOf($expected, $actual, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertType($expected, $actual, $message); - } - - return parent::assertInstanceOf($expected, $actual, $message); - } - - /** - * Asserts that an attribute is of a given type. - * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); - } - - return parent::assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message); - } - - /** - * Asserts that a variable is not of a given type. - * - * @param string $expected - * @param mixed $actual - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertNotInstanceOf($expected, $actual, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertNotType($expected, $actual, $message); - } - - return self::assertNotInstanceOf($expected, $actual, $message); - } - - /** - * Asserts that an attribute is of a given type. - * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); - } - - return self::assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message); - } - - /** - * Asserts that a variable is of a given type. - * - * @param string $expected - * @param mixed $actual - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertInternalType($expected, $actual, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertType($expected, $actual, $message); - } - - return parent::assertInternalType($expected, $actual, $message); - } - - /** - * Asserts that an attribute is of a given type. - * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertAttributeInternalType($expected, $attributeName, $classOrObject, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); - } - - return self::assertAttributeInternalType($expected, $attributeName, $classOrObject, $message); - } - - /** - * Asserts that a variable is not of a given type. - * - * @param string $expected - * @param mixed $actual - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertNotInternalType($expected, $actual, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertNotType($expected, $actual, $message); - } - - return self::assertNotInternalType($expected, $actual, $message); - } - - /** - * Asserts that an attribute is of a given type. - * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); - } - - return self::assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message); - } } diff --git a/modules/unittest/classes/Kohana/Unittest/Helpers.php b/modules/unittest/classes/Kohana/Unittest/Helpers.php index 410090e..d076eb5 100644 --- a/modules/unittest/classes/Kohana/Unittest/Helpers.php +++ b/modules/unittest/classes/Kohana/Unittest/Helpers.php @@ -1,4 +1,4 @@ -_helpers = new Unittest_Helpers; $this->setEnvironment($this->environmentDefault); @@ -112,150 +96,75 @@ abstract class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase { } /** - * Asserts that a variable is of a given type. + * Evaluate an HTML or XML string and assert its structure and/or contents. * - * @param string $expected - * @param mixed $actual - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertInstanceOf($expected, $actual, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertType($expected, $actual, $message); - } - - return parent::assertInstanceOf($expected, $actual, $message); - } - - /** - * Asserts that an attribute is of a given type. + * NOTE: + * Overriding this method to remove the deprecation error + * when tested with PHPUnit 4.2.0+ * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject + * TODO: + * this should be removed when phpunit-dom-assertions gets released + * https://github.com/phpunit/phpunit-dom-assertions + * + * @param array $matcher + * @param string $actual * @param string $message - * @since Method available since Release 3.5.0 + * @param bool $isHtml + * @uses Unittest_TestCase::tag_match */ - public static function assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message = '') + public static function assertTag($matcher, $actual, $message = '', $isHtml = true) { - if(self::$_assert_type_compatability) - { - return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); - } + //trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED); - return parent::assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message); + $matched = static::tag_match($matcher, $actual, $message, $isHtml); + static::assertTrue($matched, $message); } /** - * Asserts that a variable is not of a given type. + * This assertion is the exact opposite of assertTag * - * @param string $expected - * @param mixed $actual + * Rather than asserting that $matcher results in a match, it asserts that + * $matcher does not match + * + * NOTE: + * Overriding this method to remove the deprecation error + * when tested with PHPUnit 4.2.0+ + * + * TODO: + * this should be removed when phpunit-dom-assertions gets released + * https://github.com/phpunit/phpunit-dom-assertions + * + * @param array $matcher + * @param string $actual * @param string $message - * @since Method available since Release 3.5.0 + * @param bool $isHtml + * @uses Unittest_TestCase::tag_match */ - public static function assertNotInstanceOf($expected, $actual, $message = '') + public static function assertNotTag($matcher, $actual, $message = '', $isHtml = true) { - if(self::$_assert_type_compatability) - { - return self::assertNotType($expected, $actual, $message); - } + //trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED); - return parent::assertNotInstanceOf($expected, $actual, $message); + $matched = static::tag_match($matcher, $actual, $message, $isHtml); + static::assertFalse($matched, $message); } /** - * Asserts that an attribute is of a given type. + * Helper function to match HTML string tags against certain criteria * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); - } - - return parent::assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message); - } - - /** - * Asserts that a variable is of a given type. + * TODO: + * this should be removed when phpunit-dom-assertions gets released + * https://github.com/phpunit/phpunit-dom-assertions * - * @param string $expected - * @param mixed $actual + * @param array $matcher + * @param string $actual * @param string $message - * @since Method available since Release 3.5.0 + * @param bool $isHtml + * @return bool TRUE if there is a match FALSE otherwise */ - public static function assertInternalType($expected, $actual, $message = '') + protected static function tag_match($matcher, $actual, $message = '', $isHtml = true) { - if(self::$_assert_type_compatability) - { - return self::assertType($expected, $actual, $message); - } - - return parent::assertInternalType($expected, $actual, $message); - } - - /** - * Asserts that an attribute is of a given type. - * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertAttributeInternalType($expected, $attributeName, $classOrObject, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertAttributeType($expected, $attributeName, $classOrObject, $message); - } - - return parent::assertAttributeInternalType($expected, $attributeName, $classOrObject, $message); - } - - /** - * Asserts that a variable is not of a given type. - * - * @param string $expected - * @param mixed $actual - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertNotInternalType($expected, $actual, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertNotType($expected, $actual, $message); - } - - return parent::assertNotInternalType($expected, $actual, $message); - } - - /** - * Asserts that an attribute is of a given type. - * - * @param string $expected - * @param string $attributeName - * @param mixed $classOrObject - * @param string $message - * @since Method available since Release 3.5.0 - */ - public static function assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message = '') - { - if(self::$_assert_type_compatability) - { - return self::assertAttributeNotType($expected, $attributeName, $classOrObject, $message); - } - - return parent::assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message); + $dom = PHPUnit_Util_XML::load($actual, $isHtml); + $tags = PHPUnit_Util_XML::findNodes($dom, $matcher, $isHtml); + return count($tags) > 0 && $tags[0] instanceof DOMNode; } } diff --git a/modules/unittest/classes/Unittest/Database/TestCase.php b/modules/unittest/classes/Unittest/Database/TestCase.php index baeaa0a..6cff1e7 100644 --- a/modules/unittest/classes/Unittest/Database/TestCase.php +++ b/modules/unittest/classes/Unittest/Database/TestCase.php @@ -1,4 +1,4 @@ -=3.3", "php": ">=5.3.3", - "phpunit/phpunit": "3.7.*" + "phpunit/phpunit": "3.7.24 - 4" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "extra": { "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/unittest/koharness.php b/modules/unittest/koharness.php new file mode 100644 index 0000000..ef9d916 --- /dev/null +++ b/modules/unittest/koharness.php @@ -0,0 +1,7 @@ + array( + 'unittest' => __DIR__ + ), +); diff --git a/modules/userguide/.travis.yml b/modules/userguide/.travis.yml new file mode 100644 index 0000000..dacfadb --- /dev/null +++ b/modules/userguide/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - vendor + - $HOME/.composer/cache + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer install --prefer-dist + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/modules/userguide/README.md b/modules/userguide/README.md index f79888d..eae4446 100644 --- a/modules/userguide/README.md +++ b/modules/userguide/README.md @@ -1,3 +1,10 @@ +# Kohana - userguide module + +| ver | Stable | Develop | +|-------|----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/userguide.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/userguide) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/userguide.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/userguide) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/userguide.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/userguide) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/userguide.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/userguide) | + ## What needs to be done? Most articles are stubs, with a couple links to pages to be used as a reference when writing the page. The idea is to use the information on those links to help write the new ones. Some of the old userguide pages can probably be mostly copied, with a few improvements, others will be better to be completely rewritten. If you ever have questions, please feel free to jump in the kohana irc channel. @@ -66,4 +73,4 @@ If you want to have parameters, only put the brackets around the class and funct 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}} \ No newline at end of file + {{some/view}} diff --git a/modules/userguide/classes/Kohana/Controller/Userguide.php b/modules/userguide/classes/Kohana/Controller/Userguide.php index fc7d94b..5b6d150 100644 --- a/modules/userguide/classes/Kohana/Controller/Userguide.php +++ b/modules/userguide/classes/Kohana/Controller/Userguide.php @@ -3,8 +3,10 @@ * Kohana user guide and api browser. * * @package Kohana/Userguide - * @category Controllers + * @category Controller * @author Kohana Team + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license */ abstract class Kohana_Controller_Userguide extends Controller_Template { @@ -76,7 +78,7 @@ abstract class Kohana_Controller_Userguide extends Controller_Template { ); } // If we are in the api browser, show the menu and show the api browser in the breadcrumbs - else if (Route::name($this->request->route()) == 'docs/api') + elseif (Route::name($this->request->route()) == 'docs/api') { $this->template->menu = Kodoc::menu(); @@ -116,13 +118,13 @@ abstract class Kohana_Controller_Userguide extends Controller_Template { } // Prevent "guide/module" and "guide/module/index" from having duplicate content - if ( $page == 'index') + if ($page == 'index') { 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 ) + if ( ! $page) { $page = 'index'; } @@ -141,12 +143,14 @@ abstract class Kohana_Controller_Userguide extends Controller_Template { Kodoc_Markdown::$image_url = URL::site($this->media->uri()).'/'.$module.'/'; // Set the page title - $this->template->title = $page == 'index' ? Kohana::$config->load('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; + Kodoc_Markdown::$show_toc = TRUE; $this->template->content = Kodoc_Markdown::markdown(file_get_contents($file)); - Kodoc_Markdown::$show_toc = false; + Kodoc_Markdown::$show_toc = FALSE; // Attach this module's menu to the template $this->template->menu = Kodoc_Markdown::markdown($this->_get_all_menu_markdown()); @@ -358,11 +362,7 @@ abstract class Kohana_Controller_Userguide extends Controller_Template { if ($file AND $text = file_get_contents($file)) { // Add spans around non-link categories. This is a terrible hack. - //echo Debug::vars($text); - - //$text = preg_replace('/(\s*[\-\*\+]\s*)(.*)/','$1$2',$text); $text = preg_replace('/^(\s*[\-\*\+]\s*)([^\[\]]+)$/m','$1$2',$text); - //echo Debug::vars($text); $markdown .= $text; } diff --git a/modules/userguide/classes/Kohana/Kodoc.php b/modules/userguide/classes/Kohana/Kodoc.php index acd9cbc..33de4ed 100644 --- a/modules/userguide/classes/Kohana/Kodoc.php +++ b/modules/userguide/classes/Kohana/Kodoc.php @@ -5,8 +5,8 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Kodoc { @@ -34,6 +34,10 @@ class Kohana_Kodoc { { $member = '#property:'.substr($matches[3], 1); } + elseif (preg_match('/^[A-Z_\x7f-\xff][A-Z0-9_\x7f-\xff]*$/', $matches[3])) + { + $member = '#constant:'.substr($matches[3],0); + } else { $member = '#'.$matches[3]; @@ -275,7 +279,7 @@ class Kohana_Kodoc { * @param string $text Content of the tag * @return void */ - $add_tag = function($tag, $text) use ($html, &$tags) + $add_tag = function ($tag, $text) use ($html, & $tags) { // Don't show @access lines, they are shown elsewhere if ($tag !== 'access') @@ -290,7 +294,7 @@ class Kohana_Kodoc { } }; - $comment = $tag = null; + $comment = $tag = NULL; $end = count($lines[1]) - 1; foreach ($lines[1] as $i => $line) @@ -415,12 +419,12 @@ class Kohana_Kodoc { * * 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 + * + * @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) { @@ -447,11 +451,11 @@ class Kohana_Kodoc { // 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; } @@ -462,5 +466,4 @@ class Kohana_Kodoc { } } - } // End Kodoc diff --git a/modules/userguide/classes/Kohana/Kodoc/Class.php b/modules/userguide/classes/Kohana/Kodoc/Class.php index 2c2f2f2..9ef9b16 100644 --- a/modules/userguide/classes/Kohana/Kodoc/Class.php +++ b/modules/userguide/classes/Kohana/Kodoc/Class.php @@ -5,8 +5,8 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Kodoc_Class extends Kodoc { @@ -45,7 +45,7 @@ class Kohana_Kodoc_Class extends Kodoc { * the class. Reads the class modifiers, constants and comment. Parses the * comment to find the description and tags. * - * @param string class name + * @param string Class name * @return void */ public function __construct($class) @@ -59,7 +59,7 @@ class Kohana_Kodoc_Class extends Kodoc { $this->constants = $this->class->getConstants(); - // If ReflectionClass::getParentClass() won't work if the class in + // If ReflectionClass::getParentClass() won't work if the class in // question is an interface if ($this->class->isInterface()) { @@ -154,7 +154,7 @@ class Kohana_Kodoc_Class extends Kodoc { return $props; } - + protected function _prop_sort($a, $b) { // If one property is public, and the other is not, it goes on top @@ -162,13 +162,13 @@ class Kohana_Kodoc_Class extends Kodoc { return -1; if ($b->isPublic() AND ( ! $a->isPublic())) return 1; - + // If one property is protected and the other is private, it goes on top if ($a->isProtected() AND $b->isPrivate()) return -1; if ($b->isProtected() AND $a->isPrivate()) return 1; - + // Otherwise just do alphabetical return strcmp($a->name, $b->name); } @@ -191,14 +191,15 @@ class Kohana_Kodoc_Class extends Kodoc { return $methods; } - + /** * Sort methods based on their visibility and declaring class based on: - * - methods will be sorted public, protected, then private. - * - methods that are declared by an ancestor will be after classes + * + * * methods will be sorted public, protected, then private. + * * methods that are declared by an ancestor will be after classes * declared by the current class - * - lastly, they will be sorted alphabetically - * + * * lastly, they will be sorted alphabetically + * */ protected function _method_sort($a, $b) { @@ -207,16 +208,16 @@ class Kohana_Kodoc_Class extends Kodoc { return -1; if ($b->isPublic() AND ( ! $a->isPublic())) return 1; - + // If one method is protected and the other is private, it goes on top if ($a->isProtected() AND $b->isPrivate()) return -1; if ($b->isProtected() AND $a->isPrivate()) return 1; - + // The methods have the same visibility, so check the declaring class depth: - - + + /* 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), @@ -276,4 +277,5 @@ class Kohana_Kodoc_Class extends Kodoc { return $result; } -} + +} // End Kodoc_Class diff --git a/modules/userguide/classes/Kohana/Kodoc/Markdown.php b/modules/userguide/classes/Kohana/Kodoc/Markdown.php index 856be95..7328ab5 100644 --- a/modules/userguide/classes/Kohana/Kodoc/Markdown.php +++ b/modules/userguide/classes/Kohana/Kodoc/Markdown.php @@ -5,8 +5,8 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { @@ -21,9 +21,10 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { public static $image_url = ''; /** - * Currently defined heading ids. + * Currently defined heading ids. * Used to prevent creating multiple headings with same id. - * @var array + * + * @var array */ protected $_heading_ids = array(); @@ -36,7 +37,7 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * Slightly less terrible way to make it so the TOC only shows up when we * want it to. set this to true to show the toc. */ - public static $show_toc = false; + public static $show_toc = FALSE; /** * Transform some text using [Kodoc_Markdown] @@ -78,8 +79,8 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { // Show table of contents for userguide pages $this->document_gamut['doTOC'] = 100; - // PHP4 makes me sad. - parent::MarkdownExtra_Parser(); + // Call parent constructor. + parent::__construct(); } /** @@ -88,25 +89,27 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * Heading 1 * ========= * - * @param array Matches from regex call - * @return string Generated html + * @param array Matches from regex call + * @return string Generated html */ - function _doHeaders_callback_setext($matches) + function _doHeaders_callback_setext($matches) { - if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) + if ($matches[3] == '-' AND preg_match('{^- }', $matches[1])) return $matches[0]; - $level = $matches[3]{0} == '=' ? 1 : 2; + $level = ($matches[3]{0} == '=') ? 1 : 2; $attr = $this->_doHeaders_attr($id =& $matches[2]); // Only auto-generate id if one doesn't exist - if(empty($attr)) + if (empty($attr)) + { $attr = ' id="'.$this->make_heading_id($matches[1]).'"'; + } // Add this header to the page toc $this->_add_to_toc($level,$matches[1],$this->make_heading_id($matches[1])); $block = "".$this->runSpanGamut($matches[1]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; + return "\n".$this->hashBlock($block)."\n\n"; } /** @@ -114,23 +117,25 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * * # Heading 1 * - * @param array Matches from regex call - * @return string Generated html + * @param array Matches from regex call + * @return string Generated html */ - function _doHeaders_callback_atx($matches) + function _doHeaders_callback_atx($matches) { $level = strlen($matches[1]); $attr = $this->_doHeaders_attr($id =& $matches[3]); // Only auto-generate id if one doesn't exist - if(empty($attr)) + if (empty($attr)) + { $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(empty($matches[3]) ? $matches[2] : $matches[3])); $block = "".$this->runSpanGamut($matches[2]).""; - return "\n" . $this->hashBlock($block) . "\n\n"; + return "\n".$this->hashBlock($block)."\n\n"; } @@ -138,14 +143,14 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * Makes a heading id from the heading text * If any heading share the same name then subsequent headings will have an integer appended * - * @param string The heading text - * @return string ID for the heading + * @param string The heading text + * @return string ID for the heading */ function make_heading_id($heading) { $id = url::title($heading, '-', TRUE); - if(isset($this->_heading_ids[$id])) + if (isset($this->_heading_ids[$id])) { $id .= '-'; @@ -155,8 +160,8 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { { $id .= $count; } - } - + } + return $id; } @@ -166,8 +171,6 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { { $replace = array(); - $replace = array(); - foreach ($matches as $set) { list($search, $view) = $set; @@ -203,7 +206,7 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * * [filesystem](about.filesystem "Optional title") * - * @param string span text + * @param string Span text * @return string */ public function doBaseURL($text) @@ -217,7 +220,7 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * * ![Install Page](img/install.png "Optional title") * - * @param string span text + * @param string Span text * @return string */ public function doImageURL($text) @@ -231,7 +234,7 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * * [Class_Name], [Class::method] or [Class::$property] * - * @param string span text + * @param string Span text * @return string */ public function doAPI($text) @@ -244,7 +247,7 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { * * [!!] Remember the milk! * - * @param string span text + * @param string Span text * @return string */ public function doNotes($text) @@ -264,7 +267,7 @@ class Kohana_Kodoc_Markdown extends MarkdownExtra_Parser { 'name' => $name, 'id' => $id); } - + public function doTOC($text) { // Only add the toc do userguide pages, not api since they already have one diff --git a/modules/userguide/classes/Kohana/Kodoc/Method.php b/modules/userguide/classes/Kohana/Kodoc/Method.php index 2e673d4..8b26cc2 100644 --- a/modules/userguide/classes/Kohana/Kodoc/Method.php +++ b/modules/userguide/classes/Kohana/Kodoc/Method.php @@ -5,28 +5,28 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Kodoc_Method extends Kodoc { /** - * @var ReflectionMethod The ReflectionMethod for this class + * @var ReflectionMethod The ReflectionMethod for this class */ public $method; /** - * @var array array of Kodoc_Method_Param + * @var array Array of Kodoc_Method_Param */ public $params; /** - * @var array the things this function can return + * @var array The things this function can return */ public $return = array(); /** - * @var string the source code for this function + * @var string The source code for this function */ public $source; diff --git a/modules/userguide/classes/Kohana/Kodoc/Method/Param.php b/modules/userguide/classes/Kohana/Kodoc/Method/Param.php index 7b5976f..a29c0e2 100644 --- a/modules/userguide/classes/Kohana/Kodoc/Method/Param.php +++ b/modules/userguide/classes/Kohana/Kodoc/Method/Param.php @@ -5,8 +5,8 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Kodoc_Method_Param extends Kodoc { diff --git a/modules/userguide/classes/Kohana/Kodoc/Missing.php b/modules/userguide/classes/Kohana/Kodoc/Missing.php index b3ebcdf..85e841b 100644 --- a/modules/userguide/classes/Kohana/Kodoc/Missing.php +++ b/modules/userguide/classes/Kohana/Kodoc/Missing.php @@ -7,6 +7,8 @@ * @package Kohana/Userguide * @category Undocumented * @author Kohana Team + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license * @since 3.0.7 */ abstract class Kohana_Kodoc_Missing { diff --git a/modules/userguide/classes/Kohana/Kodoc/Property.php b/modules/userguide/classes/Kohana/Kodoc/Property.php index af2cbee..a4b5bef 100644 --- a/modules/userguide/classes/Kohana/Kodoc/Property.php +++ b/modules/userguide/classes/Kohana/Kodoc/Property.php @@ -5,8 +5,8 @@ * @package Kohana/Userguide * @category Base * @author Kohana Team - * @copyright (c) 2009-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2013 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Kodoc_Property extends Kodoc { diff --git a/modules/userguide/composer.json b/modules/userguide/composer.json index b27114e..d1b9cd0 100644 --- a/modules/userguide/composer.json +++ b/modules/userguide/composer.json @@ -24,10 +24,18 @@ "kohana/core": ">=3.3", "php": ">=5.3.3" }, + "require-dev": { + "kohana/core": "3.3.*@dev", + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "extra": { "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" - } + }, + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + } } } diff --git a/modules/userguide/config/userguide.php b/modules/userguide/config/userguide.php index 0258f69..4a2d1d9 100644 --- a/modules/userguide/config/userguide.php +++ b/modules/userguide/config/userguide.php @@ -20,16 +20,16 @@ return array // Whether this modules userguide pages should be shown 'enabled' => TRUE, - + // The name that should show up on the userguide index page 'name' => 'Userguide', // A short description of this module, shown on the index page 'description' => 'Documentation viewer and api generation.', - + // Copyright message, shown in the footer for this module - 'copyright' => '© 2008–2012 Kohana Team', - ) + 'copyright' => '© 2008–2014 Kohana Team', + ) ), // Set transparent class name segments diff --git a/modules/userguide/guide/userguide/adding.md b/modules/userguide/guide/userguide/adding.md index 156eca8..cd12fe7 100644 --- a/modules/userguide/guide/userguide/adding.md +++ b/modules/userguide/guide/userguide/adding.md @@ -1,38 +1,42 @@ -# Adding your module to the userguide +# Adding your module -Making your module work with the userguide is simple. +Making your module work with the User Guide is simple. -First, copy this config and place in it `/config/userguide.php`, replacing anything in `<>` with the appropriate things: +First, copy this config and place in it `/config/userguide.php`, replacing anything in `<>` with the appropriate values: return array ( // Leave this alone 'modules' => array( - - // This should be the path to this modules userguide pages, without the 'guide/'. Ex: '/guide/modulename/' would be 'modulename' + + /* + * The path to this module's userguide pages, without the 'guide/'. + * + * For example, '/guide/modulename/' would be 'modulename' + */ '' => array( - - // Whether this modules userguide pages should be shown + + // Whether this module's user guide pages should be shown 'enabled' => TRUE, - - // The name that should show up on the userguide index page + + // The name that should show up on the user guide index page 'name' => '', - + // A short description of this module, shown on the index page - 'description' => '', - + 'description' => '', + // Copyright message, shown in the footer for this module - 'copyright' => '© 2010–2011 ', - ) + 'copyright' => '© 2012 ', + ) ), /* - * If you use transparent extension outside the Kohana_ namespace, + * If you use transparent extensions outside the Kohana_ namespace, * add your class prefix here. Both common Kohana naming conventions are - * excluded: + * excluded: * - Modulename extends Modulename_Core * - Foo extends Modulename_Foo - * + * * For example, if you use Modulename_ for your base classes * then you would define: */ @@ -41,7 +45,11 @@ First, copy this config and place in it `/config/userguide.php`, replaci ) ); -Next, create a folder in your module directory called `guide/` and create `index.md` and `menu.md`. All userguide pages use [Markdown](markdown). The index page is what is shown on the index of your module, the menu is what shows up in the side column. The menu should be formatted like this: +Next, create a folder in your module directory called `guide/` and create `index.md` and `menu.md`. The contents of `index.md` is what is shown on the index page of your module. + +## Creating the side menu + +The contents of the `menu.md` file is what shows up in the side column and should be formatted like this: ## [Module Name]() - [Page name](page-path) @@ -52,4 +60,16 @@ Next, create a folder in your module directory called `guide/` and c - Categories do not have to be a link to a page - [Etcetera](etc) -Page paths are relative to `guide/`. So `[Page name](path-path)` would look for `guide//page-name.md` and `[Another](category/another)` would look for `guide//page-name.md`. The guide pages can be named or arranged any way you want within that folder (with the exception of `menu.md` and `index.md`). The breadcrumbs and page titles are pulled from the `menu.md file`, not the file names or paths. You can have items that are not pages (a category that doesn't have a corresponding page). To link to the `index.md` page, you should have an empty link, e.g. `[Module Name]()`. Do not include `.md` in your links. \ No newline at end of file +You can have items that are not linked pages (a category that doesn't have a corresponding page). + +Guide pages can be named or arranged any way you want within that folder (with the exception of `index.md` and `menu.md` which must appear directly below the `guide/` folder). + +## Formatting page titles and links + +Page paths are relative to `guide/`. So `[Page name](page-name)` would look for `guide//page-name.md` and `[Another](category/another)` would look for `guide//category/another.md`. + +The breadcrumbs and page titles are pulled from the `menu.md` file, not the filenames or paths. + +To link to the `index.md` page, you should have an empty link, e.g. `[Module Name]()`. Do not include `.md` in your links. + +All user guide pages use [Markdown](markdown). \ No newline at end of file diff --git a/modules/userguide/guide/userguide/menu.md b/modules/userguide/guide/userguide/menu.md index e3f53ef..8fed1d2 100644 --- a/modules/userguide/guide/userguide/menu.md +++ b/modules/userguide/guide/userguide/menu.md @@ -1,7 +1,10 @@ ## [Userguide]() - - [Using the Userguide](using) - - [How userguide works](works) - - [Contributing](contributing) - - [Markdown Syntax](markdown) - - [Configuration](config) - - [Adding your module](adding) \ No newline at end of file + +- Getting Started + - [Using the Userguide](using) + - [How the Userguide works](works) + - [Configuration](config) +- Learning More + - [Contributing](contributing) + - [Markdown Syntax](markdown) + - [Adding your module](adding) \ No newline at end of file diff --git a/modules/userguide/koharness.php b/modules/userguide/koharness.php new file mode 100644 index 0000000..0c70d08 --- /dev/null +++ b/modules/userguide/koharness.php @@ -0,0 +1,8 @@ + array( + 'userguide' => __DIR__, + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), +); diff --git a/modules/userguide/media/guide/css/kodoc.css b/modules/userguide/media/guide/css/kodoc.css index 0807bcc..5557da2 100644 --- a/modules/userguide/media/guide/css/kodoc.css +++ b/modules/userguide/media/guide/css/kodoc.css @@ -99,7 +99,7 @@ h6:hover a.permalink { #kodoc-content, #kodoc-footer { float: left; clear: both; width: 100%; } -#kodoc-header { padding: 58px 0 2em; background: #77c244 url(../img/header.png) center top repeat-x; } +#kodoc-header { padding: 20px 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; } @@ -125,11 +125,23 @@ h6:hover a.permalink { #kodoc-topics { } #kodoc-topics ul, - #kodoc-topics ol { list-style-type:none; margin: 0; padding: 0;} + #kodoc-topics ol { list-style-type:none; margin: 0 0 0 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 > ul > li { + margin-bottom: 20px; + } + + #kodoc-topics > ul > li > a, + #kodoc-topics > ul > li > span { + font-weight: bold; + color: #EE8C0E; + } + + #kodoc-topics span, #kodoc-topics a { display: block; padding: 0; margin: 0; } #kodoc-topics span { cursor: pointer; } diff --git a/modules/userguide/media/guide/js/kodoc.js b/modules/userguide/media/guide/js/kodoc.js index 597ebf9..1dab95d 100644 --- a/modules/userguide/media/guide/js/kodoc.js +++ b/modules/userguide/media/guide/js/kodoc.js @@ -32,41 +32,6 @@ $(document).ready(function() }); }); - // Collapsing menus - $('#kodoc-topics li:has(li)').each(function() - { - var $this = $(this); - var toggle = $(''); - var menu = $this.find('>ul,>ol'); - - toggle.click(function() - { - if (menu.is(':visible')) - { - menu.stop(true, true).slideUp('fast'); - toggle.html('+'); - } - else - { - menu.stop(true, true).slideDown('fast'); - toggle.html('–'); - } - }); - - $this.find('>span').click(function() - { - // Menu without a link - toggle.click(); - }); - - if ( ! $this.is(':has(a.current)')) - { - menu.hide(); - } - - toggle.html(menu.is(':visible') ? '–' : '+').prependTo($this); - }); - // Show source links $('#kodoc-main .method-source').each(function() diff --git a/modules/userguide/tests/KodocTest.php b/modules/userguide/tests/KodocTest.php index 8258f51..1432547 100644 --- a/modules/userguide/tests/KodocTest.php +++ b/modules/userguide/tests/KodocTest.php @@ -6,7 +6,7 @@ * * @package Kohana/Userguide * @author Kohana Team - * @copyright (c) 2012 Kohana Team + * @copyright (c) 2008-2013 Kohana Team * @license http://kohanaframework.org/license */ class Kohana_KodocTest extends PHPUnit_Framework_TestCase @@ -88,7 +88,7 @@ COMMENT array( <<<'COMMENT' /** - * @trailingspace + * @trailingspace */ COMMENT , @@ -162,11 +162,11 @@ COMMENT array( <<<'COMMENT' /** - * @copyright (c) 2012 Kohana Team + * @copyright (c) 2008-2013 Kohana Team */ COMMENT , - array('', array('copyright' => array('© 2012 Kohana Team'))), + array('', array('copyright' => array('© 2008-2013 Kohana Team'))), ), array( <<<'COMMENT' @@ -328,7 +328,7 @@ COMMENT { $this->assertSame($expected, Kodoc::parse($comment)); } - + /** * Provides test data for test_transparent_classes * @return array diff --git a/modules/userguide/tests/userguide/ControllerTest.php b/modules/userguide/tests/userguide/ControllerTest.php index 30ef475..d9f1313 100644 --- a/modules/userguide/tests/userguide/ControllerTest.php +++ b/modules/userguide/tests/userguide/ControllerTest.php @@ -10,7 +10,7 @@ * @package Kohana/Userguide * @category Tests * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team + * @copyright (c) 2008-2013 Kohana Team * @license http://kohanaframework.org/license */ class Userguide_ControllerTest extends Unittest_TestCase @@ -28,8 +28,8 @@ class Userguide_ControllerTest extends Unittest_TestCase /** * @dataProvider provider_file_finds_markdown_files - * @param string $page Page name passed in the URL - * @param string $expected_file Expected result from Controller_Userguide::file + * @param string $page Page name passed in the URL + * @param string $expected_file Expected result from Controller_Userguide::file */ public function test_file_finds_markdown_files($page, $expected_file) { @@ -42,4 +42,5 @@ class Userguide_ControllerTest extends Unittest_TestCase $this->assertEquals($expected_file, $file); } + } diff --git a/modules/userguide/vendor/markdown/markdown.php b/modules/userguide/vendor/markdown/markdown.php index b649f6c..fa97f3b 100644 --- a/modules/userguide/vendor/markdown/markdown.php +++ b/modules/userguide/vendor/markdown/markdown.php @@ -239,7 +239,7 @@ class Markdown_Parser { var $predef_titles = array(); - function Markdown_Parser() { + function __construct() { # # Constructor function. Initialize appropriate member variables. # @@ -1669,7 +1669,7 @@ class MarkdownExtra_Parser extends Markdown_Parser { var $predef_abbr = array(); - function MarkdownExtra_Parser() { + function __construct() { # # Constructor function. Initialize the parser object. # @@ -1695,7 +1695,7 @@ class MarkdownExtra_Parser extends Markdown_Parser { "doAbbreviations" => 70, ); - parent::Markdown_Parser(); + parent::__construct(); } @@ -2906,4 +2906,4 @@ negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. */ -?> \ No newline at end of file +?> diff --git a/modules/userguide/views/userguide/template.php b/modules/userguide/views/userguide/template.php index d94efc4..d0751aa 100644 --- a/modules/userguide/views/userguide/template.php +++ b/modules/userguide/views/userguide/template.php @@ -38,6 +38,7 @@
+ 1): ?>
    $title): ?> @@ -49,6 +50,7 @@
+
diff --git a/system/.travis.yml b/system/.travis.yml new file mode 100644 index 0000000..3f9e63a --- /dev/null +++ b/system/.travis.yml @@ -0,0 +1,37 @@ +sudo: false + +language: php + +# Only build the main develop/master branches - feature branches will be covered by PRs +branches: + only: + - /^[0-9\.]+\/(develop|master)$/ + +cache: + directories: + - $HOME/.composer/cache/files + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + include: + - php: 5.3 + env: 'COMPOSER_PHPUNIT="lowest"' + +before_script: + - composer self-update + - COMPOSER_ROOT_VERSION=3.3.x-dev composer install --prefer-dist --no-interaction + - if [ "$COMPOSER_PHPUNIT" = "lowest" ]; then COMPOSER_ROOT_VERSION=3.3.x-dev composer update --prefer-lowest --with-dependencies phpunit/phpunit; fi; + - vendor/bin/koharness + +script: + - cd /tmp/koharness && ./vendor/bin/phpunit --bootstrap=modules/unittest/bootstrap.php modules/unittest/tests.php + +notifications: + email: false diff --git a/system/CONTRIBUTING.md b/system/CONTRIBUTING.md new file mode 100644 index 0000000..3abb5dd --- /dev/null +++ b/system/CONTRIBUTING.md @@ -0,0 +1 @@ +Please refer to the CONTRIBUTING file located under the [kohana](https://github.com/kohana/kohana) repo. \ No newline at end of file diff --git a/system/README.md b/system/README.md new file mode 100644 index 0000000..b9bb295 --- /dev/null +++ b/system/README.md @@ -0,0 +1,33 @@ +# Kohana PHP Framework - core + +| ver | Stable | Develop | +|-------|------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| 3.3.x | [![Build Status - 3.3/master](https://travis-ci.org/kohana/core.svg?branch=3.3%2Fmaster)](https://travis-ci.org/kohana/core) | [![Build Status - 3.3/develop](https://travis-ci.org/kohana/core.svg?branch=3.3%2Fdevelop)](https://travis-ci.org/kohana/core) | +| 3.4.x | [![Build Status - 3.4/master](https://travis-ci.org/kohana/core.svg?branch=3.4%2Fmaster)](https://travis-ci.org/kohana/core) | [![Build Status - 3.4/develop](https://travis-ci.org/kohana/core.svg?branch=3.4%2Fdevelop)](https://travis-ci.org/kohana/core) | + +This is the core package for the [Kohana](http://kohanaframework.org/) object oriented HMVC framework built using PHP5. +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 and installation + +See the [sample application repository](https://github.com/kohana/kohana) for full readme and contributing information. +You will usually add `kohana/core` as a dependency in your own project's composer.json to install and work with this +package. + +## Installation for development + +To work on this package, you'll want to install it with composer to get the required dependencies. Note that there are +currently circular dependencies between this module and kohana/unittest. These may cause you problems if you are working +on a feature branch, because composer may not be able to figure out which version of kohana core you have. + +To work around this, run composer like: `COMPOSER_ROOT_VERSION=3.3.x-dev composer install`. This tells composer that the +current checkout is a 3.3.* development version. Obviously change the argument if your branch is based on a different +version. + +After installing the dependencies, you'll need a skeleton Kohana application before you can run the unit tests etc. The +simplest way to do this is to use kohana/koharness to build a bare project in `/tmp/koharness`. + +If in doubt, check the install and test steps in the [.travis.yml](.travis.yml) file. diff --git a/system/classes/Kohana/Arr.php b/system/classes/Kohana/Arr.php index 766369a..9d906f2 100644 --- a/system/classes/Kohana/Arr.php +++ b/system/classes/Kohana/Arr.php @@ -279,7 +279,13 @@ class Kohana_Arr { */ public static function get($array, $key, $default = NULL) { - return isset($array[$key]) ? $array[$key] : $default; + if ($array instanceof ArrayObject) { + // This is a workaround for inconsistent implementation of isset between PHP and HHVM + // See https://github.com/facebook/hhvm/issues/3437 + return $array->offsetExists($key) ? $array->offsetGet($key) : $default; + } else { + return isset($array[$key]) ? $array[$key] : $default; + } } /** @@ -387,7 +393,7 @@ class Kohana_Arr { { if (is_array($val)) { - $array[$key] = Arr::map($callbacks, $array[$key]); + $array[$key] = Arr::map($callbacks, $array[$key], $keys); } elseif ( ! is_array($keys) OR in_array($key, $keys)) { diff --git a/system/classes/Kohana/Cookie.php b/system/classes/Kohana/Cookie.php index dafb7f5..e68f84f 100644 --- a/system/classes/Kohana/Cookie.php +++ b/system/classes/Kohana/Cookie.php @@ -71,14 +71,14 @@ class Kohana_Cookie { // Separate the salt and the value list ($hash, $value) = explode('~', $cookie, 2); - if (Cookie::salt($key, $value) === $hash) + if (Security::slow_equals(Cookie::salt($key, $value), $hash)) { // Cookie signature is valid return $value; } // The cookie signature is invalid, delete it - Cookie::delete($key); + static::delete($key); } return $default; @@ -88,33 +88,38 @@ class Kohana_Cookie { * Sets a signed cookie. Note that all cookie values must be strings and no * automatic serialization will be performed! * + * [!!] By default, Cookie::$expiration is 0 - if you skip/pass NULL for the optional + * lifetime argument your cookies will expire immediately unless you have separately + * configured Cookie::$expiration. + * + * * // Set the "theme" cookie * Cookie::set('theme', 'red'); * * @param string $name name of cookie * @param string $value value of cookie - * @param integer $expiration lifetime in seconds + * @param integer $lifetime lifetime in seconds * @return boolean * @uses Cookie::salt */ - public static function set($name, $value, $expiration = NULL) + public static function set($name, $value, $lifetime = NULL) { - if ($expiration === NULL) + if ($lifetime === NULL) { // Use the default expiration - $expiration = Cookie::$expiration; + $lifetime = Cookie::$expiration; } - if ($expiration !== 0) + if ($lifetime !== 0) { // The expiration is expected to be a UNIX timestamp - $expiration += time(); + $lifetime += static::_time(); } // Add the salt to the cookie value $value = Cookie::salt($name, $value).'~'.$value; - return setcookie($name, $value, $expiration, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly); + return static::_setcookie($name, $value, $lifetime, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly); } /** @@ -131,7 +136,7 @@ class Kohana_Cookie { unset($_COOKIE[$name]); // Nullify the cookie and make it expire - return setcookie($name, NULL, -86400, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly); + return static::_setcookie($name, NULL, -86400, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly); } /** @@ -139,8 +144,10 @@ class Kohana_Cookie { * * $salt = Cookie::salt('theme', 'red'); * - * @param string $name name of cookie - * @param string $value value of cookie + * @param string $name name of cookie + * @param string $value value of cookie + * + * @throws Kohana_Exception if Cookie::$salt is not configured * @return string */ public static function salt($name, $value) @@ -154,7 +161,38 @@ class Kohana_Cookie { // Determine the user agent $agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : 'unknown'; - return sha1($agent.$name.$value.Cookie::$salt); + return hash_hmac('sha1', $agent.$name.$value.Cookie::$salt, Cookie::$salt); + } + + /** + * Proxy for the native setcookie function - to allow mocking in unit tests so that they do not fail when headers + * have been sent. + * + * @param string $name + * @param string $value + * @param integer $expire + * @param string $path + * @param string $domain + * @param boolean $secure + * @param boolean $httponly + * + * @return bool + * @see setcookie + */ + protected static function _setcookie($name, $value, $expire, $path, $domain, $secure, $httponly) + { + return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly); + } + + /** + * Proxy for the native time function - to allow mocking of time-related logic in unit tests + * + * @return int + * @see time + */ + protected static function _time() + { + return time(); } } diff --git a/system/classes/Kohana/Core.php b/system/classes/Kohana/Core.php index 081d4bb..860090d 100644 --- a/system/classes/Kohana/Core.php +++ b/system/classes/Kohana/Core.php @@ -16,8 +16,8 @@ class Kohana_Core { // Release version and codename - const VERSION = '3.3.1'; - const CODENAME = 'peregrinus'; + const VERSION = '3.3.5'; + const CODENAME = 'pharrell'; // Common environment type constants for consistency and convenience const PRODUCTION = 10; @@ -322,7 +322,7 @@ class Kohana_Core { } // Determine if the extremely evil magic quotes are enabled - Kohana::$magic_quotes = (version_compare(PHP_VERSION, '5.4') < 0 AND get_magic_quotes_gpc()); + Kohana::$magic_quotes = (bool) get_magic_quotes_gpc(); // Sanitize all request variables $_GET = Kohana::sanitize($_GET); diff --git a/system/classes/Kohana/Date.php b/system/classes/Kohana/Date.php index 692d658..930edc3 100644 --- a/system/classes/Kohana/Date.php +++ b/system/classes/Kohana/Date.php @@ -592,10 +592,10 @@ class Kohana_Date { $tz = new DateTimeZone($timezone ? $timezone : date_default_timezone_get()); $time = new DateTime($datetime_str, $tz); - if ($time->getTimeZone()->getName() !== $tz->getName()) - { - $time->setTimeZone($tz); - } + // Convert the time back to the expected timezone if required (in case the datetime_str provided a timezone, + // offset or unix timestamp. This also ensures that the timezone reported by the object is correct on HHVM + // (see https://github.com/facebook/hhvm/issues/2302). + $time->setTimeZone($tz); return $time->format($timestamp_format); } diff --git a/system/classes/Kohana/Debug.php b/system/classes/Kohana/Debug.php index 5a82bb6..9106b00 100644 --- a/system/classes/Kohana/Debug.php +++ b/system/classes/Kohana/Debug.php @@ -133,8 +133,8 @@ class Kohana_Debug { if ($marker === NULL) { - // Make a unique marker - $marker = uniqid("\x00"); + // Make a unique marker - force it to be alphanumeric so that it is always treated as a string array key + $marker = uniqid("\x00")."x"; } if (empty($var)) diff --git a/system/classes/Kohana/Encrypt.php b/system/classes/Kohana/Encrypt.php index 6428607..5c59f83 100644 --- a/system/classes/Kohana/Encrypt.php +++ b/system/classes/Kohana/Encrypt.php @@ -36,10 +36,33 @@ class Kohana_Encrypt { public static $instances = array(); /** - * @var string OS-dependent RAND type to use + * @var string RAND type to use + * + * Only MCRYPT_DEV_URANDOM and MCRYPT_DEV_RANDOM are considered safe. + * Using MCRYPT_RAND will silently revert to MCRYPT_DEV_URANDOM */ - protected static $_rand; + protected static $_rand = MCRYPT_DEV_URANDOM; + /** + * @var string Encryption key + */ + protected $_key; + + /** + * @var string mcrypt mode + */ + protected $_mode; + + /** + * @var string mcrypt cipher + */ + protected $_cipher; + + /** + * @var int the size of the Initialization Vector (IV) in bytes + */ + protected $_iv_size; + /** * Returns a singleton instance of Encrypt. An encryption key must be * provided in your "encrypt" configuration file. @@ -105,6 +128,10 @@ class Kohana_Encrypt { // Shorten the key to the maximum size $key = substr($key, 0, $size); } + else if (version_compare(PHP_VERSION, '5.6.0', '>=')) + { + $key = $this->_normalize_key($key, $cipher, $mode); + } // Store the key, mode, and cipher $this->_key = $key; @@ -129,43 +156,8 @@ class Kohana_Encrypt { */ public function encode($data) { - // Set the rand type if it has not already been set - if (Encrypt::$_rand === NULL) - { - if (Kohana::$is_windows) - { - // Windows only supports the system random number generator - Encrypt::$_rand = MCRYPT_RAND; - } - else - { - if (defined('MCRYPT_DEV_URANDOM')) - { - // Use /dev/urandom - Encrypt::$_rand = MCRYPT_DEV_URANDOM; - } - elseif (defined('MCRYPT_DEV_RANDOM')) - { - // Use /dev/random - Encrypt::$_rand = MCRYPT_DEV_RANDOM; - } - else - { - // Use the system random number generator - Encrypt::$_rand = MCRYPT_RAND; - } - } - } - - if (Encrypt::$_rand === MCRYPT_RAND) - { - // The system random number generator must always be seeded each - // time it is used, or it will not produce true random results - mt_srand(); - } - - // Create a random initialization vector of the proper size for the current cipher - $iv = mcrypt_create_iv($this->_iv_size, Encrypt::$_rand); + // Get an initialization vector + $iv = $this->_create_iv(); // Encrypt the data using the configured options and generated iv $data = mcrypt_encrypt($this->_cipher, $this->_key, $data, $this->_mode, $iv); @@ -210,4 +202,54 @@ class Kohana_Encrypt { return rtrim(mcrypt_decrypt($this->_cipher, $this->_key, $data, $this->_mode, $iv), "\0"); } + /** + * Proxy for the mcrypt_create_iv function - to allow mocking and testing against KAT vectors + * + * @return string the initialization vector or FALSE on error + */ + protected function _create_iv() + { + /* + * Silently use MCRYPT_DEV_URANDOM when the chosen random number generator + * is not one of those that are considered secure. + * + * Also sets Encrypt::$_rand to MCRYPT_DEV_URANDOM when it's not already set + */ + if ((Encrypt::$_rand !== MCRYPT_DEV_URANDOM) AND ( Encrypt::$_rand !== MCRYPT_DEV_RANDOM)) + { + Encrypt::$_rand = MCRYPT_DEV_URANDOM; + } + + // Create a random initialization vector of the proper size for the current cipher + return mcrypt_create_iv($this->_iv_size, Encrypt::$_rand); + } + + /** + * Normalize key for PHP 5.6 for backwards compatibility + * + * This method is a shim to make PHP 5.6 behave in a B/C way for + * legacy key padding when shorter-than-supported keys are used + * + * @param string $key encryption key + * @param string $cipher mcrypt cipher + * @param string $mode mcrypt mode + */ + protected function _normalize_key($key, $cipher, $mode) + { + // open the cipher + $td = mcrypt_module_open($cipher, '', $mode, ''); + + // loop through the supported key sizes + foreach (mcrypt_enc_get_supported_key_sizes($td) as $supported) { + // if key is short, needs padding + if (strlen($key) <= $supported) + { + return str_pad($key, $supported, "\0"); + } + } + + // at this point key must be greater than max supported size, shorten it + return substr($key, 0, mcrypt_get_key_size($cipher, $mode)); + } + } diff --git a/system/classes/Kohana/Form.php b/system/classes/Kohana/Form.php index 510bd81..65ed30b 100644 --- a/system/classes/Kohana/Form.php +++ b/system/classes/Kohana/Form.php @@ -28,7 +28,7 @@ class Kohana_Form { * @param mixed $action form action, defaults to the current request URI, or [Request] class to use * @param array $attributes html attributes * @return string - * @uses Request::instance + * @uses Request * @uses URL::site * @uses HTML::attributes */ diff --git a/system/classes/Kohana/HTML.php b/system/classes/Kohana/HTML.php index a78bc76..9752a1e 100644 --- a/system/classes/Kohana/HTML.php +++ b/system/classes/Kohana/HTML.php @@ -126,9 +126,9 @@ class Kohana_HTML { $attributes['target'] = '_blank'; } } - elseif ($uri[0] !== '#') + elseif ($uri[0] !== '#' AND $uri[0] !== '?') { - // Make the URI absolute for non-id anchors + // Make the URI absolute for non-fragment and non-query anchors $uri = URL::site($uri, $protocol, $index); } } @@ -206,7 +206,7 @@ class Kohana_HTML { */ public static function style($file, array $attributes = NULL, $protocol = NULL, $index = FALSE) { - if (strpos($file, '://') === FALSE) + if (strpos($file, '://') === FALSE AND strpos($file, '//') !== 0) { // Add the base URL $file = URL::site($file, $protocol, $index); @@ -239,7 +239,7 @@ class Kohana_HTML { */ public static function script($file, array $attributes = NULL, $protocol = NULL, $index = FALSE) { - if (strpos($file, '://') === FALSE) + if (strpos($file, '://') === FALSE AND strpos($file, '//') !== 0) { // Add the base URL $file = URL::site($file, $protocol, $index); diff --git a/system/classes/Kohana/HTTP.php b/system/classes/Kohana/HTTP.php index eca52c4..4c22c52 100644 --- a/system/classes/Kohana/HTTP.php +++ b/system/classes/Kohana/HTTP.php @@ -95,7 +95,10 @@ abstract class Kohana_HTTP { if (extension_loaded('http')) { // Use the fast method to parse header string - return new HTTP_Header(http_parse_headers($header_string)); + $headers = version_compare(phpversion('http'), '2.0.0', '>=') ? + \http\Header::parse($header_string) : + http_parse_headers($header_string); + return new HTTP_Header($headers); } // Otherwise we use the slower PHP parsing @@ -160,7 +163,10 @@ abstract class Kohana_HTTP { elseif (extension_loaded('http')) { // Return the much faster method - return new HTTP_Header(http_get_request_headers()); + $headers = version_compare(phpversion('http'), '2.0.0', '>=') ? + \http\Env::getRequestHeader() : + http_get_request_headers(); + return new HTTP_Header($headers); } // Setup the output @@ -186,8 +192,8 @@ abstract class Kohana_HTTP { continue; } - // This is a dirty hack to ensure HTTP_X_FOO_BAR becomes x-foo-bar - $headers[str_replace(array('HTTP_', '_'), array('', '-'), $key)] = $value; + // This is a dirty hack to ensure HTTP_X_FOO_BAR becomes X-FOO-BAR + $headers[str_replace('_', '-', substr($key, 5))] = $value; } return new HTTP_Header($headers); diff --git a/system/classes/Kohana/Kohana/Exception.php b/system/classes/Kohana/Kohana/Exception.php index d54b464..d11a2f4 100644 --- a/system/classes/Kohana/Kohana/Exception.php +++ b/system/classes/Kohana/Kohana/Exception.php @@ -217,6 +217,16 @@ class Kohana_Kohana_Exception extends Exception { $frame['type'] = '??'; } + // Xdebug returns the words 'dynamic' and 'static' instead of using '->' and '::' symbols + if ('dynamic' === $frame['type']) + { + $frame['type'] = '->'; + } + elseif ('static' === $frame['type']) + { + $frame['type'] = '::'; + } + // XDebug also has a different name for the parameters array if (isset($frame['params']) AND ! isset($frame['args'])) { @@ -238,7 +248,13 @@ class Kohana_Kohana_Exception extends Exception { * The error view ends up several GB in size, taking * serveral minutes to render. */ - if (defined('PHPUnit_MAIN_METHOD')) + if ( + defined('PHPUnit_MAIN_METHOD') + OR + defined('PHPUNIT_COMPOSER_INSTALL') + OR + defined('__PHPUNIT_PHAR__') + ) { $trace = array_slice($trace, 0, 2); } diff --git a/system/classes/Kohana/Request.php b/system/classes/Kohana/Request.php index ba77385..84bf9dc 100644 --- a/system/classes/Kohana/Request.php +++ b/system/classes/Kohana/Request.php @@ -38,7 +38,7 @@ class Kohana_Request implements HTTP_Request { /** * Creates a new request object for the given URI. New requests should be - * created using the [Request::instance] or [Request::factory] methods. + * Created using the [Request::factory] method. * * $request = Request::factory($uri); * @@ -462,6 +462,12 @@ class Kohana_Request implements HTTP_Request { foreach ($routes as $name => $route) { + // Use external routes for reverse routing only + if ($route->is_external()) + { + continue; + } + // We found something suitable if ($params = $route->matches($request)) { @@ -631,7 +637,7 @@ class Kohana_Request implements HTTP_Request { /** * Creates a new request object for the given URI. New requests should be - * created using the [Request::instance] or [Request::factory] methods. + * Created using the [Request::factory] method. * * $request = new Request($uri); * @@ -662,7 +668,7 @@ class Kohana_Request implements HTTP_Request { $uri = array_shift($split_uri); // Initial request has global $_GET already applied - if (Request::$initial !== NULL) + if (Request::$initial === NULL) { if ($split_uri) { @@ -675,7 +681,7 @@ class Kohana_Request implements HTTP_Request { // being able to proxy external pages. if ( ! $allow_external OR strpos($uri, '://') === FALSE) { - // Remove trailing slashes from the URI + // Remove leading and trailing slashes from the URI $this->_uri = trim($uri, '/'); // Apply the client @@ -726,7 +732,7 @@ class Kohana_Request implements HTTP_Request { if ($uri === NULL) { // Act as a getter - return empty($this->_uri) ? '/' : $this->_uri; + return ($this->_uri === '') ? '/' : $this->_uri; } // Act as a setter @@ -740,7 +746,6 @@ class Kohana_Request implements HTTP_Request { * * echo URL::site($this->request->uri(), $protocol); * - * @param array $params URI parameters * @param mixed $protocol protocol string or Request object * @return string * @since 3.0.7 @@ -748,7 +753,13 @@ class Kohana_Request implements HTTP_Request { */ public function url($protocol = NULL) { - // Create a URI with the current route and convert it to a URL + if ($this->is_external()) + { + // If it's an external request return the URI + return $this->uri(); + } + + // Create a URI with the current route, convert to a URL and returns return URL::site($this->uri(), $protocol); } @@ -1219,9 +1230,9 @@ class Kohana_Request implements HTTP_Request { } else { - $this->headers('content-type', - 'application/x-www-form-urlencoded; charset='.Kohana::$charset); $body = http_build_query($post, NULL, '&'); + $this->body($body) + ->headers('content-type', 'application/x-www-form-urlencoded; charset='.Kohana::$charset); } // Set the content length diff --git a/system/classes/Kohana/Request/Client.php b/system/classes/Kohana/Request/Client.php index 1190a30..3c35faf 100644 --- a/system/classes/Kohana/Request/Client.php +++ b/system/classes/Kohana/Request/Client.php @@ -26,7 +26,7 @@ abstract class Kohana_Request_Client { /** * @var array Headers to preserve when following a redirect */ - protected $_follow_headers = array('Authorization'); + protected $_follow_headers = array('authorization'); /** * @var bool Follow 302 redirect with original request method? @@ -205,7 +205,7 @@ abstract class Kohana_Request_Client { if ($follow_headers === NULL) return $this->_follow_headers; - $this->_follow_headers = $follow_headers; + $this->_follow_headers = array_map('strtolower', $follow_headers); return $this; } @@ -407,7 +407,8 @@ abstract class Kohana_Request_Client { // Prepare the additional request, copying any follow_headers that were present on the original request $orig_headers = $request->headers()->getArrayCopy(); - $follow_headers = array_intersect_assoc($orig_headers, array_fill_keys($client->follow_headers(), TRUE)); + $follow_header_keys = array_intersect(array_keys($orig_headers), $client->follow_headers()); + $follow_headers = \Arr::extract($orig_headers, $follow_header_keys); $follow_request = Request::factory($response->headers('Location')) ->method($follow_method) diff --git a/system/classes/Kohana/Request/Client/Curl.php b/system/classes/Kohana/Request/Client/Curl.php index c5dffa6..c9c7e21 100644 --- a/system/classes/Kohana/Request/Client/Curl.php +++ b/system/classes/Kohana/Request/Client/Curl.php @@ -34,7 +34,10 @@ class Kohana_Request_Client_Curl extends Request_Client_External { // if using a request other than POST. PUT does support this method // and DOES NOT require writing data to disk before putting it, if // reading the PHP docs you may have got that impression. SdF - $options[CURLOPT_POSTFIELDS] = $request->body(); + // This will also add a Content-Type: application/x-www-form-urlencoded header unless you override it + if ($body = $request->body()) { + $options[CURLOPT_POSTFIELDS] = $body; + } // Process headers if ($headers = $request->headers()) diff --git a/system/classes/Kohana/Request/Client/External.php b/system/classes/Kohana/Request/Client/External.php index 985b915..363d331 100644 --- a/system/classes/Kohana/Request/Client/External.php +++ b/system/classes/Kohana/Request/Client/External.php @@ -127,6 +127,8 @@ abstract class Kohana_Request_Client_External extends Request_Client { ->headers('content-type', 'application/x-www-form-urlencoded; charset='.Kohana::$charset); } + $request->headers('content-length', (string) $request->content_length()); + // If Kohana expose, set the user-agent if (Kohana::$expose) { diff --git a/system/classes/Kohana/Response.php b/system/classes/Kohana/Response.php index 6869979..298158d 100644 --- a/system/classes/Kohana/Response.php +++ b/system/classes/Kohana/Response.php @@ -604,7 +604,10 @@ class Kohana_Response implements HTTP_Response { { if (extension_loaded('http')) { - $this->_header['set-cookie'] = http_build_cookie($this->_cookies); + $cookies = version_compare(phpversion('http'), '2.0.0', '>=') ? + (string) new \http\Cookie($this->_cookies) : + http_build_cookie($this->_cookies); + $this->_header['set-cookie'] = $cookies; } else { diff --git a/system/classes/Kohana/Route.php b/system/classes/Kohana/Route.php index 718bf17..976c46f 100644 --- a/system/classes/Kohana/Route.php +++ b/system/classes/Kohana/Route.php @@ -509,6 +509,14 @@ class Kohana_Route { */ public function uri(array $params = NULL) { + if ($params) + { + // @issue #4079 rawurlencode parameters + $params = array_map('rawurlencode', $params); + // decode slashes back, see Apache docs about AllowEncodedSlashes and AcceptPathInfo + $params = str_replace(array('%2F', '%5C'), array('/', '\\'), $params); + } + $defaults = $this->_defaults; /** diff --git a/system/classes/Kohana/Security.php b/system/classes/Kohana/Security.php index b8f66c6..214e8b1 100644 --- a/system/classes/Kohana/Security.php +++ b/system/classes/Kohana/Security.php @@ -28,8 +28,8 @@ class Kohana_Security { * And then check it when using [Validation]: * * $array->rules('csrf', array( - * 'not_empty' => NULL, - * 'Security::check' => NULL, + * array('not_empty'), + * array('Security::check'), * )); * * This provides a basic, but effective, method of preventing CSRF attacks. @@ -81,8 +81,29 @@ class Kohana_Security { */ public static function check($token) { - return Security::token() === $token; + return Security::slow_equals(Security::token(), $token); } + + + + /** + * Compare two hashes in a time-invariant manner. + * Prevents cryptographic side-channel attacks (timing attacks, specifically) + * + * @param string $a cryptographic hash + * @param string $b cryptographic hash + * @return boolean + */ + public static function slow_equals($a, $b) + { + $diff = strlen($a) ^ strlen($b); + for($i = 0; $i < strlen($a) AND $i < strlen($b); $i++) + { + $diff |= ord($a[$i]) ^ ord($b[$i]); + } + return $diff === 0; + } + /** * Remove image tags from a string. diff --git a/system/classes/Kohana/Session/Native.php b/system/classes/Kohana/Session/Native.php index a5c8917..ffce159 100644 --- a/system/classes/Kohana/Session/Native.php +++ b/system/classes/Kohana/Session/Native.php @@ -24,8 +24,31 @@ class Kohana_Session_Native extends Session { */ protected function _read($id = NULL) { + /** + * session_set_cookie_params will override php ini settings + * If Cookie::$domain is NULL or empty and is passed, PHP + * will override ini and sent cookies with the host name + * of the server which generated the cookie + * + * see issue #3604 + * + * see http://www.php.net/manual/en/function.session-set-cookie-params.php + * see http://www.php.net/manual/en/session.configuration.php#ini.session.cookie-domain + * + * set to Cookie::$domain if available, otherwise default to ini setting + */ + $session_cookie_domain = empty(Cookie::$domain) + ? ini_get('session.cookie_domain') + : Cookie::$domain; + // Sync up the session cookie with Cookie parameters - session_set_cookie_params($this->_lifetime, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly); + session_set_cookie_params( + $this->_lifetime, + Cookie::$path, + $session_cookie_domain, + Cookie::$secure, + Cookie::$httponly + ); // Do not allow PHP to send Cache-Control headers session_cache_limiter(FALSE); diff --git a/system/classes/Kohana/Text.php b/system/classes/Kohana/Text.php index 7514fd6..828c595 100644 --- a/system/classes/Kohana/Text.php +++ b/system/classes/Kohana/Text.php @@ -240,12 +240,13 @@ class Kohana_Text { * * @param string $string string to transform * @param string $delimiter delimiter to use + * @uses UTF8::ucfirst * @return string */ public static function ucfirst($string, $delimiter = '-') { // Put the keys back the Case-Convention expected - return implode($delimiter, array_map('ucfirst', explode($delimiter, $string))); + return implode($delimiter, array_map('UTF8::ucfirst', explode($delimiter, $string))); } /** @@ -293,12 +294,15 @@ class Kohana_Text { $regex = '!'.$regex.'!ui'; + // if $replacement is a single character: replace each of the characters of the badword with $replacement if (UTF8::strlen($replacement) == 1) { - $regex .= 'e'; - return preg_replace($regex, 'str_repeat($replacement, UTF8::strlen(\'$1\'))', $str); + return preg_replace_callback($regex, function($matches) use ($replacement) { + return str_repeat($replacement, UTF8::strlen($matches[1])); + }, $str); } + // if $replacement is not a single character, fully replace the badword with $replacement return preg_replace($regex, $replacement, $str); } @@ -587,35 +591,40 @@ class Kohana_Text { * * echo Text::widont($text); * + * regex courtesy of the Typogrify project + * @link http://code.google.com/p/typogrify/ + * * @param string $str text to remove widows from * @return string */ public static function widont($str) { - $str = rtrim($str); - $space = strrpos($str, ' '); - - if ($space !== FALSE) - { - $str = substr($str, 0, $space).' '.substr($str, $space + 1); - } - - return $str; + // use '%' as delimiter and 'x' as modifier + $widont_regex = "% + ((?:]*>)|[^<>\s]) # must be proceeded by an approved inline opening or closing tag or a nontag/nonspace + \s+ # the space to replace + ([^<>\s]+ # must be flollowed by non-tag non-space characters + \s* # optional white space! + (\s*)* # optional closing inline tags with optional white space after each + (()|$)) # end with a closing p, h1-6, li or the end of the string + %x"; + return preg_replace($widont_regex, '$1 $2', $str); } /** * Returns information about the client user agent. * * // Returns "Chrome" when using Google Chrome - * $browser = Text::user_agent('browser'); + * $browser = Text::user_agent($agent, 'browser'); * * Multiple values can be returned at once by using an array: * * // Get the browser and platform with a single call - * $info = Text::user_agent(array('browser', 'platform')); + * $info = Text::user_agent($agent, array('browser', 'platform')); * * When using an array for the value, an associative array will be returned. * + * @param string $agent user_agent * @param mixed $value array or string to return: browser, version, robot, mobile, platform * @return mixed requested information, FALSE if nothing is found * @uses Kohana::$config @@ -649,7 +658,7 @@ class Kohana_Text { // Set the browser name $info['browser'] = $name; - if (preg_match('#'.preg_quote($search).'[^0-9.]*+([0-9.][0-9.a-z]*)#i', Request::$user_agent, $matches)) + if (preg_match('#'.preg_quote($search).'[^0-9.]*+([0-9.][0-9.a-z]*)#i', $agent, $matches)) { // Set the version number $info['version'] = $matches[1]; diff --git a/system/classes/Kohana/URL.php b/system/classes/Kohana/URL.php index b758095..62144f5 100644 --- a/system/classes/Kohana/URL.php +++ b/system/classes/Kohana/URL.php @@ -2,6 +2,8 @@ /** * URL helper class. * + * [!!] You need to setup the list of trusted hosts in the `url.php` config file, before starting using this helper class. + * * @package Kohana * @category Helpers * @author Kohana Team @@ -14,7 +16,9 @@ class Kohana_URL { * Gets the base URL to the application. * To specify a protocol, provide the protocol as a string or request object. * If a protocol is used, a complete URL will be generated using the - * `$_SERVER['HTTP_HOST']` variable. + * `$_SERVER['HTTP_HOST']` variable, which will be validated against RFC 952 + * and RFC 2181, as well as against the list of trusted hosts you have set + * in the `url.php` config file. * * // Absolute URL path with no host or protocol * echo URL::base(); @@ -75,7 +79,7 @@ class Kohana_URL { $port = ':'.$port; } - if ($domain = parse_url($base_url, PHP_URL_HOST)) + if ($host = parse_url($base_url, PHP_URL_HOST)) { // Remove everything but the path from the URL $base_url = parse_url($base_url, PHP_URL_PATH); @@ -83,11 +87,32 @@ class Kohana_URL { else { // Attempt to use HTTP_HOST and fallback to SERVER_NAME - $domain = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; + $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; + + // make $host lowercase + $host = strtolower($host); + + // check that host does not contain forbidden characters (see RFC 952 and RFC 2181) + // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names + if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) { + throw new Kohana_Exception( + 'Invalid host :host', + array(':host' => $host) + ); + } + + // Validate $host, see if it matches trusted hosts + if ( ! static::is_trusted_host($host)) + { + throw new Kohana_Exception( + 'Untrusted host :host. If you trust :host, add it to the trusted hosts in the `url` config file.', + array(':host' => $host) + ); + } } // Add the protocol and domain to the base URL - $base_url = $protocol.'://'.$domain.$port.$base_url; + $base_url = $protocol.'://'.$host.$port.$base_url; } return $base_url; @@ -210,4 +235,41 @@ class Kohana_URL { return trim($title, $separator); } + /** + * Test if given $host should be trusted. + * + * Tests against given $trusted_hosts + * or looks for key `trusted_hosts` in `url` config + * + * @param string $host + * @param array $trusted_hosts + * @return boolean TRUE if $host is trustworthy + */ + public static function is_trusted_host($host, array $trusted_hosts = NULL) + { + + // If list of trusted hosts is not directly provided read from config + if (empty($trusted_hosts)) + { + $trusted_hosts = (array) Kohana::$config->load('url')->get('trusted_hosts'); + } + + // loop through the $trusted_hosts array for a match + foreach ($trusted_hosts as $trusted_host) + { + + // make sure we fully match the trusted hosts + $pattern = '#^'.$trusted_host.'$#uD'; + + // return TRUE if there is match + if (preg_match($pattern, $host)) { + return TRUE; + } + + } + + // return FALSE as nothing is matched + return FALSE; + + } } diff --git a/system/classes/Kohana/UTF8.php b/system/classes/Kohana/UTF8.php index ca5e315..fec509e 100644 --- a/system/classes/Kohana/UTF8.php +++ b/system/classes/Kohana/UTF8.php @@ -70,13 +70,17 @@ class Kohana_UTF8 { if ( ! UTF8::is_ascii($var)) { - // Disable notices - $error_reporting = error_reporting(~E_NOTICE); + // Temporarily save the mb_substitute_character() value into a variable + $mb_substitute_character = mb_substitute_character(); + // Disable substituting illegal characters with the default '?' character + mb_substitute_character('none'); + + // convert encoding, this is expensive, used when $var is not ASCII $var = mb_convert_encoding($var, $charset, $charset); - // Turn notices back on - error_reporting($error_reporting); + // Reset mb_substitute_character() value back to the original setting + mb_substitute_character($mb_substitute_character); } } diff --git a/system/classes/Kohana/Validation.php b/system/classes/Kohana/Validation.php index 4b46497..71f2ea8 100644 --- a/system/classes/Kohana/Validation.php +++ b/system/classes/Kohana/Validation.php @@ -219,7 +219,7 @@ class Kohana_Validation implements ArrayAccess { if ($field !== TRUE AND ! isset($this->_labels[$field])) { // Set the field label to the field name - $this->_labels[$field] = preg_replace('/[^\pL]+/u', ' ', $field); + $this->_labels[$field] = $field; } // Store the rule and params for this rule @@ -430,6 +430,13 @@ class Kohana_Validation implements ArrayAccess { } } + // Unbind all the automatic bindings to avoid memory leaks. + unset($this->_bound[':validation']); + unset($this->_bound[':data']); + unset($this->_bound[':field']); + unset($this->_bound[':value']); + + // Restore the data to its original form $this->_data = $original; diff --git a/system/classes/Kohana/View.php b/system/classes/Kohana/View.php index 662a932..3ca5ba0 100644 --- a/system/classes/Kohana/View.php +++ b/system/classes/Kohana/View.php @@ -40,6 +40,7 @@ class Kohana_View { * @param string $kohana_view_filename filename * @param array $kohana_view_data variables * @return string + * @throws Exception */ protected static function capture($kohana_view_filename, array $kohana_view_data) { @@ -79,17 +80,25 @@ class Kohana_View { * * View::set_global($name, $value); * - * @param string $key variable name or an array of variables - * @param mixed $value value + * You can also use an array or Traversable object to set several values at once: + * + * // Create the values $food and $beverage in the view + * View::set_global(array('food' => 'bread', 'beverage' => 'water')); + * + * [!!] Note: When setting with using Traversable object we're not attaching the whole object to the view, + * i.e. the object's standard properties will not be available in the view context. + * + * @param string|array|Traversable $key variable name or an array of variables + * @param mixed $value value * @return void */ public static function set_global($key, $value = NULL) { - if (is_array($key)) + if (is_array($key) OR $key instanceof Traversable) { - foreach ($key as $key2 => $value) + foreach ($key as $name => $value) { - View::$_global_data[$key2] = $value; + View::$_global_data[$name] = $value; } } else @@ -127,7 +136,6 @@ class Kohana_View { * * @param string $file view filename * @param array $data array of values - * @return void * @uses View::set_filename */ public function __construct($file = NULL, array $data = NULL) @@ -272,18 +280,21 @@ class Kohana_View { * // This value can be accessed as $foo within the view * $view->set('foo', 'my value'); * - * You can also use an array to set several values at once: + * You can also use an array or Traversable object to set several values at once: * * // Create the values $food and $beverage in the view * $view->set(array('food' => 'bread', 'beverage' => 'water')); * - * @param string $key variable name or an array of variables - * @param mixed $value value + * [!!] Note: When setting with using Traversable object we're not attaching the whole object to the view, + * i.e. the object's standard properties will not be available in the view context. + * + * @param string|array|Traversable $key variable name or an array of variables + * @param mixed $value value * @return $this */ public function set($key, $value = NULL) { - if (is_array($key)) + if (is_array($key) OR $key instanceof Traversable) { foreach ($key as $name => $value) { diff --git a/system/composer.json b/system/composer.json index 2c07aa8..9a88f40 100644 --- a/system/composer.json +++ b/system/composer.json @@ -1,4 +1,5 @@ { + "_readme": "NOTE: see readme for COMPOSER_ROOT_VERSION instructions if you have dependency issues", "name": "kohana/core", "description": "Core system classes for the Kohana application framework", "homepage": "http://kohanaframework.org", @@ -21,12 +22,19 @@ "require": { "php": ">=5.3.3" }, + "require-dev": { + "kohana/unittest": "3.3.*@dev", + "kohana/koharness": "*@dev" + }, "suggest": { "ext-http": "*", "ext-curl": "*", "ext-mcrypt": "*" }, "extra": { + "installer-paths": { + "vendor/{$vendor}/{$name}": ["type:kohana-module"] + }, "branch-alias": { "dev-3.3/develop": "3.3.x-dev", "dev-3.4/develop": "3.4.x-dev" diff --git a/system/config/mimes.php b/system/config/mimes.php index 2e31b0b..ba2fe4a 100644 --- a/system/config/mimes.php +++ b/system/config/mimes.php @@ -192,6 +192,7 @@ return array( 'wav' => array('audio/x-wav'), 'wax' => array('audio/x-ms-wax'), 'wbxml' => array('application/wbxml'), + 'webapp' => array('application/x-web-app-manifest+json'), 'webm' => array('video/webm'), 'wm' => array('video/x-ms-wm'), 'wma' => array('audio/x-ms-wma'), diff --git a/system/config/url.php b/system/config/url.php new file mode 100644 index 0000000..96bd293 --- /dev/null +++ b/system/config/url.php @@ -0,0 +1,18 @@ + array( + // Set up your hostnames here + // + // Example: + // + // 'example\.org', + // '.*\.example\.org', + // + // Do not forget to escape your dots (.) as these are regex patterns. + // These patterns should always fully match, + // as they are prepended with `^` and appended with `$` + ), + +); diff --git a/system/config/user_agents.php b/system/config/user_agents.php index f4b92ea..78f03c0 100644 --- a/system/config/user_agents.php +++ b/system/config/user_agents.php @@ -3,6 +3,8 @@ return array( 'platform' => array( + 'windows nt 10.0'=> 'Windows 10', + 'windows nt 6.3' => 'Windows 8.1', 'windows nt 6.2' => 'Windows 8', 'windows nt 6.1' => 'Windows 7', 'windows nt 6.0' => 'Windows Vista', @@ -47,6 +49,7 @@ return array( 'browser' => array( 'Opera' => 'Opera', + 'Edge/12' => 'Edge', 'MSIE' => 'Internet Explorer', 'Internet Explorer' => 'Internet Explorer', 'Shiira' => 'Shiira', diff --git a/system/guide/kohana/bootstrap.md b/system/guide/kohana/bootstrap.md index 22c560f..dd66e66 100644 --- a/system/guide/kohana/bootstrap.md +++ b/system/guide/kohana/bootstrap.md @@ -52,7 +52,7 @@ You can add conditional statements to make the bootstrap have different values b /** * Set the environment status by the domain. */ -if (strpos($_SERVER['HTTP_HOST'], 'kohanaphp.com') !== FALSE) +if (strpos($_SERVER['HTTP_HOST'], 'kohanaframework.org') !== FALSE) { // We are live! Kohana::$environment = Kohana::PRODUCTION; @@ -66,7 +66,7 @@ if (strpos($_SERVER['HTTP_HOST'], 'kohanaphp.com') !== FALSE) ... [trimmed] */ Kohana::init(array( - 'base_url' => Kohana::$environment === Kohana::PRODUCTION ? '/' : '/kohanaphp.com/', + 'base_url' => Kohana::$environment === Kohana::PRODUCTION ? '/' : '/kohanaframework.org/', 'caching' => Kohana::$environment === Kohana::PRODUCTION, 'profile' => Kohana::$environment !== Kohana::PRODUCTION, 'index_file' => FALSE, diff --git a/system/guide/kohana/flow.md b/system/guide/kohana/flow.md index 81a2e54..76fdfe8 100644 --- a/system/guide/kohana/flow.md +++ b/system/guide/kohana/flow.md @@ -16,7 +16,7 @@ Every application follows the same flow: * Includes each module's `init.php` file, if it exists. * The `init.php` file can perform additional environment setup, including adding routes. 10. [Route::set] is called multiple times to define the [application routes](routing). - 11. [Request::instance] is called to start processing the request. + 11. [Request::factory] is called to start processing the request. 1. Checks each route that has been set until a match is found. 2. Creates the controller instance and passes the request to it. 3. Calls the [Controller::before] method. @@ -24,4 +24,4 @@ Every application follows the same flow: 5. Calls the [Controller::after] method. * The above 5 steps can be repeated multiple times when using [HMVC sub-requests](requests). 3. Application flow returns to index.php - 12. The main [Request] response is displayed \ No newline at end of file + 12. The main [Request] response is displayed diff --git a/system/guide/kohana/install.md b/system/guide/kohana/install.md index f8e0e42..4e4d212 100644 --- a/system/guide/kohana/install.md +++ b/system/guide/kohana/install.md @@ -34,15 +34,27 @@ Kohana::init(array( )); ~~~ - - Make sure the `application/cache` and `application/logs` directories are writable by the web server. + - List your trusted hosts. Open `application/config/url.php` and add regex patterns of the hosts you expect your application to be accessible from. + + [!!] Do not forget to escape your dots (.) as these are regex patterns. These patterns should always fully match, as they are prepended with `^` and appended with `$`. ~~~ -sudo chmod -R a+rwx application/cache -sudo chmod -R a+rwx application/logs +return array( + 'trusted_hosts' => array( + 'example\.org', + '.*\.example\.org', + ), +); ~~~ - Define a salt for the `Cookie` class. ~~~ -Cookie::$salt = [really-long-cookie-salt-here] +Cookie::$salt = 'some-really-long-cookie-salt-here'; +~~~ + + - Make sure the `application/cache` and `application/logs` directories are writable by the web server. +~~~ +sudo chmod -R a+rwx application/cache +sudo chmod -R a+rwx application/logs ~~~ [!!] Make sure to use a unique salt for your application and never to share it. Take a look at the [Cookies](cookies) page for more information on how cookies work in Kohana. If you do not define a `Cookie::$salt` value, Kohana will throw an exception when it encounters any cookie on your domain. diff --git a/system/guide/kohana/menu.md b/system/guide/kohana/menu.md index 01e1de4..60a1a7d 100644 --- a/system/guide/kohana/menu.md +++ b/system/guide/kohana/menu.md @@ -20,6 +20,7 @@ - [Error Handling](errors) - [Tips & Common Mistakes](tips) - [Upgrading from v3.2](upgrading) + - [Upgrading from v3.3.3.1](upgrading-from-3-3-3-1) - Basic Usage - [Debugging](debugging) - [Loading Classes](autoloading) diff --git a/system/guide/kohana/mvc/controllers.md b/system/guide/kohana/mvc/controllers.md index 3a155db..2c9f8ab 100644 --- a/system/guide/kohana/mvc/controllers.md +++ b/system/guide/kohana/mvc/controllers.md @@ -55,7 +55,7 @@ You can also have a controller extend another controller to share common things, Every controller has the `$this->request` property which is the [Request] object that called the controller. You can use this to get information about the current request, as well as set the response body via `$this->response->body($ouput)`. -Here is a partial list of the properties and methods available to `$this->request`. These can also be accessed via `Request::instance()`, but `$this->request` is provided as a shortcut. See the [Request] class for more information on any of these. +Here is a partial list of the properties and methods available to `$this->request`. See the [Request] class for more information on any of these. Property/method | What it does --- | --- diff --git a/system/guide/kohana/security/validation.md b/system/guide/kohana/security/validation.md index 43b1e1c..baace7f 100644 --- a/system/guide/kohana/security/validation.md +++ b/system/guide/kohana/security/validation.md @@ -182,6 +182,7 @@ First, we need a [View] that contains the HTML form, which will be placed in `ap
  • +
    diff --git a/system/guide/kohana/tutorials.md b/system/guide/kohana/tutorials.md deleted file mode 100644 index c921bcd..0000000 --- a/system/guide/kohana/tutorials.md +++ /dev/null @@ -1,17 +0,0 @@ -# Tutorials - -## Tutorials in this guide - -## Tutorials written elsewhere - -### Ellisgl's KO3 tutorial on dealtaker.com: - -1. [Install and Basic Usage](http://www.dealtaker.com/blog/2009/11/20/kohana-php-3-0-ko3-tutorial-part-1/) -2. [Views](http://www.dealtaker.com/blog/2009/12/07/kohana-php-3-0-ko3-tutorial-part-2/) -3. [Controllers](http://www.dealtaker.com/blog/2009/12/30/kohana-php-3-0-ko3-tutorial-part-3/) -4. [Models](http://www.dealtaker.com/blog/2010/02/01/kohana-php-3-0-ko3-tutorial-part-4/) -5. [Subrequests](http://www.dealtaker.com/blog/2010/02/25/kohana-php-3-0-ko3-tutorial-part-5/) -6. [Routes](http://www.dealtaker.com/blog/2010/03/03/kohana-php-3-0-ko3-tutorial-part-6/) -7. [Helpers](http://www.dealtaker.com/blog/2010/03/26/kohana-php-3-0-ko3-tutorial-part-7/) -8. [Modules](http://www.dealtaker.com/blog/2010/04/30/kohana-php-3-0-ko3-tutorial-part-8/) -9. [Vendor Libraries](http://www.dealtaker.com/blog/2010/06/02/kohana-php-3-0-ko3-tutorial-part-9/) \ No newline at end of file diff --git a/system/guide/kohana/tutorials/templates.md b/system/guide/kohana/tutorials/templates.md deleted file mode 100644 index 4bfd9c2..0000000 --- a/system/guide/kohana/tutorials/templates.md +++ /dev/null @@ -1,7 +0,0 @@ -Making a template driven site. - - - - - - \ No newline at end of file diff --git a/system/guide/kohana/tutorials/translation.md b/system/guide/kohana/tutorials/translation.md deleted file mode 100644 index 12f7cfc..0000000 --- a/system/guide/kohana/tutorials/translation.md +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/system/guide/kohana/upgrading-from-3-3-3-1.md b/system/guide/kohana/upgrading-from-3-3-3-1.md new file mode 100644 index 0000000..f4f07fd --- /dev/null +++ b/system/guide/kohana/upgrading-from-3-3-3-1.md @@ -0,0 +1,23 @@ +# Upgrading from 3.3.3.1 + +Minor version upgrades are usually done in a drop-in fashion. Unfortunately, however, upgrading from 3.3.3.1 to 3.3.4 needs a little configuration. This is because a [security disclosure from HP Fortify](https://github.com/kohana/kohana/issues/74), that unveiled a serious [host header attack](https://github.com/kohana/core/issues/613) vulnerability. + +[!!] You *might* still be able to have a drop-in upgrade, in case you have set the `base_url` in the [Kohana::init] call to an absolute URL. We advise you however that you follow the step below to make your application secure, in case some day you decide to change your `base_url` to a relative URL. + +## Trusted Hosts + +You need to setup a list of trusted hosts. Trusted hosts are hosts that you expect your application to be accessible from. + +Open `application/config/url.php` and add regex patterns of these hosts. An example is given hereunder: + +~~~ +return array( + 'trusted_hosts' => array( + 'example\.org', + '.*\.example\.org', + ), +); +~~~ + +[!!] Do not forget to escape your dots (.) as these are regex patterns. These patterns should always fully match, as they are prepended with `^` and appended with `$`. + diff --git a/system/koharness.php b/system/koharness.php new file mode 100644 index 0000000..7845498 --- /dev/null +++ b/system/koharness.php @@ -0,0 +1,8 @@ + array( + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), + 'syspath' => __DIR__, +); diff --git a/system/tests/kohana/ArrTest.php b/system/tests/kohana/ArrTest.php index 12a85ef..a20ec23 100644 --- a/system/tests/kohana/ArrTest.php +++ b/system/tests/kohana/ArrTest.php @@ -655,6 +655,22 @@ class Kohana_ArrTest extends Unittest_TestCase 'bar' => 'foobar', ), ), + array( + 'strip_tags', + array( + array( + 'foo' => '

    foobar

    ', + 'bar' => '

    foobar

    ', + ), + ), + array('foo'), + array( + array( + 'foo' => 'foobar', + 'bar' => '

    foobar

    ', + ), + ), + ), ); } diff --git a/system/tests/kohana/CookieTest.php b/system/tests/kohana/CookieTest.php index 9c1fe40..25159b9 100644 --- a/system/tests/kohana/CookieTest.php +++ b/system/tests/kohana/CookieTest.php @@ -11,13 +11,15 @@ * @category Tests * @author Kohana Team * @author Jeremy Bush - * @copyright (c) 2008-2012 Kohana Team + * @author Andrew Coulton + * @copyright (c) 2008-2014 Kohana Team * @license http://kohanaframework.org/license */ class Kohana_CookieTest extends Unittest_TestCase { + const UNIX_TIMESTAMP = 1411040141; + const COOKIE_EXPIRATION = 60; - protected $_default_salt = 'AdaoidadnA£ASDNadnaoiwdnawd'; /** * Sets up the environment */ @@ -26,152 +28,298 @@ class Kohana_CookieTest extends Unittest_TestCase // @codingStandardsIgnoreEnd { parent::setUp(); + Kohana_CookieTest_TestableCookie::$_mock_cookies_set = array(); - Cookie::$salt = $this->_default_salt; + $this->setEnvironment(array( + 'Cookie::$salt' => 'some-random-salt', + 'HTTP_USER_AGENT' => 'cli' + )); } /** - * Tears down the environment + * Tests that cookies are set with the global path, domain, etc options. + * + * @covers Cookie::set + */ + public function test_set_creates_cookie_with_configured_cookie_options() + { + $this->setEnvironment(array( + 'Cookie::$path' => '/path', + 'Cookie::$domain' => 'my.domain', + 'Cookie::$secure' => TRUE, + 'Cookie::$httponly' => FALSE, + )); + + Kohana_CookieTest_TestableCookie::set('cookie', 'value'); + + $this->assertSetCookieWith(array( + 'path' => '/path', + 'domain' => 'my.domain', + 'secure' => TRUE, + 'httponly' => FALSE + )); + } + + /** + * Provider for test_set_calculates_expiry_from_lifetime + * + * @return array of $lifetime, $expect_expiry + */ + public function provider_set_calculates_expiry_from_lifetime() + { + return array( + array(NULL, self::COOKIE_EXPIRATION + self::UNIX_TIMESTAMP), + array(0, 0), + array(10, 10 + self::UNIX_TIMESTAMP), + ); + } + + /** + * @param int $expiration + * @param int $expect_expiry + * + * @dataProvider provider_set_calculates_expiry_from_lifetime + * @covers Cookie::set + */ + public function test_set_calculates_expiry_from_lifetime($expiration, $expect_expiry) + { + $this->setEnvironment(array('Cookie::$expiration' => self::COOKIE_EXPIRATION)); + Kohana_CookieTest_TestableCookie::set('foo', 'bar', $expiration); + $this->assertSetCookieWith(array('expire' => $expect_expiry)); + } + + /** + * @covers Cookie::get + */ + public function test_get_returns_default_if_cookie_missing() + { + unset($_COOKIE['missing_cookie']); + $this->assertEquals('default', Cookie::get('missing_cookie', 'default')); + } + + /** + * @covers Cookie::get + */ + public function test_get_returns_value_if_cookie_present_and_signed() + { + Kohana_CookieTest_TestableCookie::set('cookie', 'value'); + $cookie = Kohana_CookieTest_TestableCookie::$_mock_cookies_set[0]; + $_COOKIE[$cookie['name']] = $cookie['value']; + $this->assertEquals('value', Cookie::get('cookie', 'default')); + } + + /** + * Provider for test_get_returns_default_without_deleting_if_cookie_unsigned + * + * @return array + */ + public function provider_get_returns_default_without_deleting_if_cookie_unsigned() + { + return array( + array('unsalted'), + array('un~salted'), + ); + } + + /** + * Verifies that unsigned cookies are not available to the kohana application, but are not affected for other + * consumers. + * + * @param string $unsigned_value + * + * @dataProvider provider_get_returns_default_without_deleting_if_cookie_unsigned + * @covers Cookie::get + */ + public function test_get_returns_default_without_deleting_if_cookie_unsigned($unsigned_value) + { + $_COOKIE['cookie'] = $unsigned_value; + $this->assertEquals('default', Kohana_CookieTest_TestableCookie::get('cookie', 'default')); + $this->assertEquals($unsigned_value, $_COOKIE['cookie'], '$_COOKIE not affected'); + $this->assertEmpty(Kohana_CookieTest_TestableCookie::$_mock_cookies_set, 'No cookies set or changed'); + } + + /** + * If a cookie looks like a signed cookie but the signature no longer matches, it should be deleted. + * + * @covers Cookie::get + */ + public function test_get_returns_default_and_deletes_tampered_signed_cookie() + { + $_COOKIE['cookie'] = Cookie::salt('cookie', 'value').'~tampered'; + $this->assertEquals('default', Kohana_CookieTest_TestableCookie::get('cookie', 'default')); + $this->assertDeletedCookie('cookie'); + } + + /** + * @covers Cookie::delete + */ + public function test_delete_removes_cookie_from_globals_and_expires_cookie() + { + $_COOKIE['cookie'] = Cookie::salt('cookie', 'value').'~tampered'; + $this->assertTrue(Kohana_CookieTest_TestableCookie::delete('cookie')); + $this->assertDeletedCookie('cookie'); + } + + /** + * @covers Cookie::delete + * @link http://dev.kohanaframework.org/issues/3501 + * @link http://dev.kohanaframework.org/issues/3020 + */ + public function test_delete_does_not_require_configured_salt() + { + Cookie::$salt = NULL; + $this->assertTrue(Kohana_CookieTest_TestableCookie::delete('cookie')); + $this->assertDeletedCookie('cookie'); + } + + /** + * @covers Cookie::salt + * @expectedException Kohana_Exception + */ + public function test_salt_throws_with_no_configured_salt() + { + Cookie::$salt = NULL; + Cookie::salt('key', 'value'); + } + + /** + * @covers Cookie::salt + */ + public function test_salt_creates_same_hash_for_same_values_and_state() + { + $name = 'cookie'; + $value = 'value'; + $this->assertEquals(Cookie::salt($name, $value), Cookie::salt($name, $value)); + } + + /** + * Provider for test_salt_creates_different_hash_for_different_data + * + * @return array + */ + public function provider_salt_creates_different_hash_for_different_data() + { + return array( + array(array('name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'), array('name' => 'changed')), + array(array('name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'), array('value' => 'changed')), + array(array('name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'), array('salt' => 'changed-salt')), + array(array('name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'), array('user-agent' => 'Firefox')), + array(array('name' => 'foo', 'value' => 'bar', 'salt' => 'our-salt', 'user-agent' => 'Chrome'), array('user-agent' => NULL)), + ); + } + + /** + * @param array $first_args + * @param array $changed_args + * + * @dataProvider provider_salt_creates_different_hash_for_different_data + * @covers Cookie::salt + */ + public function test_salt_creates_different_hash_for_different_data($first_args, $changed_args) + { + $second_args = array_merge($first_args, $changed_args); + $hashes = array(); + foreach (array($first_args, $second_args) as $args) + { + Cookie::$salt = $args['salt']; + $this->set_or_remove_http_user_agent($args['user-agent']); + + $hashes[] = Cookie::salt($args['name'], $args['value']); + } + + $this->assertNotEquals($hashes[0], $hashes[1]); + } + + /** + * Verify that a cookie was deleted from the global $_COOKIE array, and that a setcookie call was made to remove it + * from the client. + * + * @param string $name */ // @codingStandardsIgnoreStart - public function tearDown() + protected function assertDeletedCookie($name) // @codingStandardsIgnoreEnd { - parent::tearDown(); - - Cookie::$salt = NULL; + $this->assertArrayNotHasKey($name, $_COOKIE); + // To delete the client-side cookie, Cookie::delete should send a new cookie with value NULL and expiry in the past + $this->assertSetCookieWith(array( + 'name' => $name, + 'value' => NULL, + 'expire' => -86400, + 'path' => Cookie::$path, + 'domain' => Cookie::$domain, + 'secure' => Cookie::$secure, + 'httponly' => Cookie::$httponly + )); } /** - * Provides test data for test_set() + * Verify that there was a single call to setcookie including the provided named arguments * - * @return array + * @param array $expected */ - public function provider_set() + // @codingStandardsIgnoreStart + protected function assertSetCookieWith($expected) + // @codingStandardsIgnoreEnd { - return array( - array('foo', 'bar', NULL, TRUE), - array('foo', 'bar', 10, TRUE), - ); + $this->assertCount(1, Kohana_CookieTest_TestableCookie::$_mock_cookies_set); + $relevant_values = array_intersect_key(Kohana_CookieTest_TestableCookie::$_mock_cookies_set[0], $expected); + $this->assertEquals($expected, $relevant_values); } /** - * Tests cookie::set() + * Configure the $_SERVER[HTTP_USER_AGENT] environment variable for the test * - * @test - * @dataProvider provider_set - * @covers cookie::set - * @param mixed $key key to use - * @param mixed $value value to set - * @param mixed $exp exp to set - * @param boolean $expected Output for cookie::set() + * @param string $user_agent */ - public function test_set($key, $value, $exp, $expected) + protected function set_or_remove_http_user_agent($user_agent) { - if (headers_sent()) { - $this->markTestSkipped('Cannot test setting cookies as headers have already been sent'); - } - - $this->assertSame($expected, cookie::set($key, $value, $exp)); - } - - /** - * Provides test data for test_get() - * - * @return array - */ - public function provider_get() - { - // setUp is called after the provider so we need to specify a - // salt here in order to use it in the provider - Cookie::$salt = $this->_default_salt; - - return array( - array('foo', Cookie::salt('foo', 'bar').'~bar', 'bar'), - array('bar', Cookie::salt('foo', 'bar').'~bar', NULL), - array(NULL, Cookie::salt('foo', 'bar').'~bar', NULL), - ); - } - - /** - * Tests cookie::set() - * - * @test - * @dataProvider provider_get - * @covers cookie::get - * @param mixed $key key to use - * @param mixed $value value to set - * @param boolean $expected Output for cookie::get() - */ - public function test_get($key, $value, $expected) - { - if (headers_sent()) { - $this->markTestSkipped('Cannot test setting cookies as headers have already been sent'); - } - - // Force $_COOKIE - if ($key !== NULL) + if ($user_agent === NULL) { - $_COOKIE[$key] = $value; + unset($_SERVER['HTTP_USER_AGENT']); } - - $this->assertSame($expected, cookie::get($key)); - } - - /** - * Provides test data for test_delete() - * - * @return array - */ - public function provider_delete() - { - return array( - array('foo', TRUE), - ); - } - - /** - * Tests cookie::delete() - * - * @test - * @dataProvider provider_delete - * @covers cookie::delete - * @param mixed $key key to use - * @param boolean $expected Output for cookie::delete() - */ - public function test_delete($key, $expected) - { - if (headers_sent()) { - $this->markTestSkipped('Cannot test setting cookies as headers have already been sent'); + else + { + $_SERVER['HTTP_USER_AGENT'] = $user_agent; } - - $this->assertSame($expected, cookie::delete($key)); - } - - /** - * Provides test data for test_salt() - * - * @return array - */ - public function provider_salt() - { - return array( - array('foo', 'bar', 'b5773a6255d1deefc23f9f69bcc40fdc998e5802'), - ); - } - - /** - * Tests cookie::salt() - * - * @test - * @dataProvider provider_salt - * @covers cookie::salt - * @param mixed $key key to use - * @param mixed $value value to salt with - * @param boolean $expected Output for cookie::delete() - */ - public function test_salt($key, $value, $expected) - { - $this->assertSame($expected, cookie::salt($key, $value)); } } + +/** + * Class Kohana_CookieTest_TestableCookie wraps the cookie class to mock out the actual setcookie and time calls for + * unit testing. + */ +class Kohana_CookieTest_TestableCookie extends Cookie { + + /** + * @var array setcookie calls that were made + */ + public static $_mock_cookies_set = array(); + + /** + * {@inheritdoc} + */ + protected static function _setcookie($name, $value, $expire, $path, $domain, $secure, $httponly) + { + self::$_mock_cookies_set[] = array( + 'name' => $name, + 'value' => $value, + 'expire' => $expire, + 'path' => $path, + 'domain' => $domain, + 'secure' => $secure, + 'httponly' => $httponly + ); + + return TRUE; + } + + /** + * @return int + */ + protected static function _time() + { + return Kohana_CookieTest::UNIX_TIMESTAMP; + } + +} diff --git a/system/tests/kohana/CoreTest.php b/system/tests/kohana/CoreTest.php index a0099d1..bdb7509 100644 --- a/system/tests/kohana/CoreTest.php +++ b/system/tests/kohana/CoreTest.php @@ -18,6 +18,32 @@ */ class Kohana_CoreTest extends Unittest_TestCase { + protected $old_modules = array(); + + /** + * Captures the module list as it was before this test + * + * @return null + */ + // @codingStandardsIgnoreStart + public function setUp() + // @codingStandardsIgnoreEnd + { + parent::setUp(); + $this->old_modules = Kohana::modules(); + } + + /** + * Restores the module list + * + * @return null + */ + // @codingStandardsIgnoreStart + public function tearDown() + // @codingStandardsIgnoreEnd + { + Kohana::modules($this->old_modules); + } /** * Provides test data for test_sanitize() @@ -107,33 +133,15 @@ class Kohana_CoreTest extends Unittest_TestCase */ public function test_globals_removes_user_def_globals() { - // Store the globals - $temp_globals = array( - 'cookie' => $_COOKIE, - 'get' => $_GET, - 'files' => $_FILES, - 'post' => $_POST, - 'request' => $_REQUEST, - 'server' => $_SERVER, - 'session' => $_SESSION, - 'globals' => $GLOBALS, - ); - - $GLOBALS = array('hackers' => 'foobar','name' => array('','',''), '_POST' => array()); + $GLOBALS['hackers'] = 'foobar'; + $GLOBALS['name'] = array('','',''); + $GLOBALS['_POST'] = array(); Kohana::globals(); - $this->assertEquals(array('_POST' => array()), $GLOBALS); - - // Reset the globals for other tests - $_COOKIE = $temp_globals['cookie']; - $_GET = $temp_globals['get']; - $_FILES = $temp_globals['files']; - $_POST = $temp_globals['post']; - $_REQUEST = $temp_globals['request']; - $_SERVER = $temp_globals['server']; - $_SESSION = $temp_globals['session']; - $GLOBALS = $temp_globals['globals']; + $this->assertFalse(isset($GLOBALS['hackers'])); + $this->assertFalse(isset($GLOBALS['name'])); + $this->assertTrue(isset($GLOBALS['_POST'])); } /** @@ -175,35 +183,18 @@ class Kohana_CoreTest extends Unittest_TestCase public function provider_message() { return array( - // $value, $result - array(':field must not be empty', 'validation', 'not_empty'), - array( + array('no_message_file', 'anything', 'default', 'default'), + array('no_message_file', NULL, 'anything', array()), + array('kohana_core_message_tests', 'bottom_only', 'anything', 'inherited bottom message'), + array('kohana_core_message_tests', 'cfs_replaced', 'anything', 'overriding cfs_replaced message'), + array('kohana_core_message_tests', 'top_only', 'anything', 'top only message'), + array('kohana_core_message_tests', 'missing', 'default', 'default'), + array('kohana_core_message_tests', NULL, 'anything', array( - 'alpha' => ':field must contain only letters', - 'alpha_dash' => ':field must contain only numbers, letters and dashes', - 'alpha_numeric' => ':field must contain only letters and numbers', - 'color' => ':field must be a color', - 'credit_card' => ':field must be a credit card number', - 'date' => ':field must be a date', - 'decimal' => ':field must be a decimal with :param2 places', - 'digit' => ':field must be a digit', - 'email' => ':field must be a email address', - 'email_domain' => ':field must contain a valid email domain', - 'equals' => ':field must equal :param2', - 'exact_length' => ':field must be exactly :param2 characters long', - 'in_array' => ':field must be one of the available options', - 'ip' => ':field must be an ip address', - 'matches' => ':field must be the same as :param2', - 'min_length' => ':field must be at least :param2 characters long', - 'max_length' => ':field must not exceed :param2 characters long', - 'not_empty' => ':field must not be empty', - 'numeric' => ':field must be numeric', - 'phone' => ':field must be a phone number', - 'range' => ':field must be within the range of :param2 to :param3', - 'regex' => ':field does not match the required format', - 'url' => ':field must be a url', - ), - 'validation', NULL, + 'bottom_only' => 'inherited bottom message', + 'cfs_replaced' => 'overriding cfs_replaced message', + 'top_only' => 'top only message' + ) ), ); } @@ -213,15 +204,18 @@ class Kohana_CoreTest extends Unittest_TestCase * * @test * @dataProvider provider_message - * @covers Kohana::message - * @param boolean $expected Output for Kohana::message - * @param boolean $file File to look in for Kohana::message - * @param boolean $key Key for Kohana::message + * @covers Kohana::message + * @param string $file to pass to Kohana::message + * @param string $key to pass to Kohana::message + * @param string $default to pass to Kohana::message + * @param string $expected Output for Kohana::message */ - public function test_message($expected, $file, $key) + public function test_message($file, $key, $default, $expected) { - $this->markTestSkipped('This test is incredibly fragile and needs to be re-done'); - $this->assertEquals($expected, Kohana::message($file, $key)); + $test_path = realpath(dirname(__FILE__).'/../test_data/message_tests'); + Kohana::modules(array('top' => "$test_path/top_module", 'bottom' => "$test_path/bottom_module")); + + $this->assertEquals($expected, Kohana::message($file, $key, $default, $expected)); } /** @@ -314,7 +308,7 @@ class Kohana_CoreTest extends Unittest_TestCase { return array( array(array(), array()), - array(array('unittest' => MODPATH.'unittest'), array('unittest' => $this->dirSeparator(MODPATH.'unittest/'))), + array(array('module' => __DIR__), array('module' => $this->dirSeparator(__DIR__.'/'))), ); } diff --git a/system/tests/kohana/EncryptTest.php b/system/tests/kohana/EncryptTest.php new file mode 100644 index 0000000..9e93aca --- /dev/null +++ b/system/tests/kohana/EncryptTest.php @@ -0,0 +1,747 @@ + + * @copyright (c) 2014 Kohana Team + * @license http://kohanaframework.org/license + */ +class Kohana_EncryptTest extends Unittest_TestCase +{ + + /** + * Provider for test_encode + * AES Multiblock Message Test (MMT) Sample Vectors - Known Answer Test (KAT) + * @link http://csrc.nist.gov/groups/STM/cavp/index.html NIST - Cryptographic Algorithm Validation Program + * @link http://csrc.nist.gov/groups/STM/cavp/documents/aes/aesmmt.zip file used CBCMMT128.rsp + * + * @return array of $mode, $cipher, $key, $iv, $txt_plain, $txt_encoded + */ + public function provider_encode() + { + return array( + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "1f8e4973953f3fb0bd6b16662e9a3c17"), + // IV + pack("H*", "2fe2b333ceda8f98f4a99b40d2cd34a8"), + // txt_plain + pack("H*", "45cf12964fc824ab76616ae2f4bf0822"), + // txt_encoded + pack("H*", "0f61c4d44c5147c03c195ad7e2cc12b2"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "1f8e4973953f3fb0bd6b16662e9a3c17"), + // IV + pack("H*", "2fe2b333ceda8f98f4a99b40d2cd34a8"), + // txt_plain + pack("H*", "45cf12964fc824ab76616ae2f4bf0822"), + // txt_encoded + pack("H*", "0f61c4d44c5147c03c195ad7e2cc12b2"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "0700d603a1c514e46b6191ba430a3a0c"), + // IV + pack("H*", "aad1583cd91365e3bb2f0c3430d065bb"), + // txt_plain + pack("H*", "068b25c7bfb1f8bdd4cfc908f69dffc5ddc726a197f0e5f720f730393279be91"), + // txt_encoded + pack("H*", "c4dc61d9725967a3020104a9738f23868527ce839aab1752fd8bdb95a82c4d00"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "3348aa51e9a45c2dbe33ccc47f96e8de"), + // IV + pack("H*", "19153c673160df2b1d38c28060e59b96"), + // txt_plain + pack("H*", "9b7cee827a26575afdbb7c7a329f887238052e3601a7917456ba61251c214763d5e1847a6ad5d54127a399ab07ee3599"), + // txt_encoded + pack("H*", "d5aed6c9622ec451a15db12819952b6752501cf05cdbf8cda34a457726ded97818e1f127a28d72db5652749f0c6afee5"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "b7f3c9576e12dd0db63e8f8fac2b9a39"), + // IV + pack("H*", "c80f095d8bb1a060699f7c19974a1aa0"), + // txt_plain + pack("H*", "9ac19954ce1319b354d3220460f71c1e373f1cd336240881160cfde46ebfed2e791e8d5a1a136ebd1dc469dec00c4187722b841cdabcb22c1be8a14657da200e"), + // txt_encoded + pack("H*", "19b9609772c63f338608bf6eb52ca10be65097f89c1e0905c42401fd47791ae2c5440b2d473116ca78bd9ff2fb6015cfd316524eae7dcb95ae738ebeae84a467"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "b6f9afbfe5a1562bba1368fc72ac9d9c"), + // IV + pack("H*", "3f9d5ebe250ee7ce384b0d00ee849322"), + // txt_plain + pack("H*", "db397ec22718dbffb9c9d13de0efcd4611bf792be4fce0dc5f25d4f577ed8cdbd4eb9208d593dda3d4653954ab64f05676caa3ce9bfa795b08b67ceebc923fdc89a8c431188e9e482d8553982cf304d1"), + // txt_encoded + pack("H*", "10ea27b19e16b93af169c4a88e06e35c99d8b420980b058e34b4b8f132b13766f72728202b089f428fecdb41c79f8aa0d0ef68f5786481cca29e2126f69bc14160f1ae2187878ba5c49cf3961e1b7ee9"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "bbe7b7ba07124ff1ae7c3416fe8b465e"), + // IV + pack("H*", "7f65b5ee3630bed6b84202d97fb97a1e"), + // txt_plain + pack("H*", "2aad0c2c4306568bad7447460fd3dac054346d26feddbc9abd9110914011b4794be2a9a00a519a51a5b5124014f4ed2735480db21b434e99a911bb0b60fe0253763725b628d5739a5117b7ee3aefafc5b4c1bf446467e7bf5f78f31ff7caf187"), + // txt_encoded + pack("H*", "3b8611bfc4973c5cd8e982b073b33184cd26110159172e44988eb5ff5661a1e16fad67258fcbfee55469267a12dc374893b4e3533d36f5634c3095583596f135aa8cd1138dc898bc5651ee35a92ebf89ab6aeb5366653bc60a70e0074fc11efe"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "89a553730433f7e6d67d16d373bd5360"), + // IV + pack("H*", "f724558db3433a523f4e51a5bea70497"), + // txt_plain + pack("H*", "807bc4ea684eedcfdcca30180680b0f1ae2814f35f36d053c5aea6595a386c1442770f4d7297d8b91825ee7237241da8925dd594ccf676aecd46ca2068e8d37a3a0ec8a7d5185a201e663b5ff36ae197110188a23503763b8218826d23ced74b31e9f6e2d7fbfa6cb43420c7807a8625"), + // txt_encoded + pack("H*", "406af1429a478c3d07e555c5287a60500d37fc39b68e5bbb9bafd6ddb223828561d6171a308d5b1a4551e8a5e7d572918d25c968d3871848d2f16635caa9847f38590b1df58ab5efb985f2c66cfaf86f61b3f9c0afad6c963c49cee9b8bc81a2ddb06c967f325515a4849eec37ce721a"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "c491ca31f91708458e29a925ec558d78"), + // IV + pack("H*", "9ef934946e5cd0ae97bd58532cb49381"), + // txt_plain + pack("H*", "cb6a787e0dec56f9a165957f81af336ca6b40785d9e94093c6190e5152649f882e874d79ac5e167bd2a74ce5ae088d2ee854f6539e0a94796b1e1bd4c9fcdbc79acbef4d01eeb89776d18af71ae2a4fc47dd66df6c4dbe1d1850e466549a47b636bcc7c2b3a62495b56bb67b6d455f1eebd9bfefecbca6c7f335cfce9b45cb9d"), + // txt_encoded + pack("H*", "7b2931f5855f717145e00f152a9f4794359b1ffcb3e55f594e33098b51c23a6c74a06c1d94fded7fd2ae42c7db7acaef5844cb33aeddc6852585ed0020a6699d2cb53809cefd169148ce42292afab063443978306c582c18b9ce0da3d084ce4d3c482cfd8fcf1a85084e89fb88b40a084d5e972466d07666126fb761f84078f2"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "f6e87d71b0104d6eb06a68dc6a71f498"), + // IV + pack("H*", "1c245f26195b76ebebc2edcac412a2f8"), + // txt_plain + pack("H*", "f82bef3c73a6f7f80db285726d691db6bf55eec25a859d3ba0e0445f26b9bb3b16a3161ed1866e4dd8f2e5f8ecb4e46d74a7a78c20cdfc7bcc9e479ba7a0caba9438238ad0c01651d5d98de37f03ddce6e6b4bd4ab03cf9e8ed818aedfa1cf963b932067b97d776dce1087196e7e913f7448e38244509f0caf36bd8217e15336d35c149fd4e41707893fdb84014f8729"), + // txt_encoded + pack("H*", "b09512f3eff9ed0d85890983a73dadbb7c3678d52581be64a8a8fc586f490f2521297a478a0598040ebd0f5509fafb0969f9d9e600eaef33b1b93eed99687b167f89a5065aac439ce46f3b8d22d30865e64e45ef8cd30b6984353a844a11c8cd60dba0e8866b3ee30d24b3fa8a643b328353e06010fa8273c8fd54ef0a2b6930e5520aae5cd5902f9b86a33592ca4365"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "2c14413751c31e2730570ba3361c786b"), + // IV + pack("H*", "1dbbeb2f19abb448af849796244a19d7"), + // txt_plain + pack("H*", "40d930f9a05334d9816fe204999c3f82a03f6a0457a8c475c94553d1d116693adc618049f0a769a2eed6a6cb14c0143ec5cccdbc8dec4ce560cfd206225709326d4de7948e54d603d01b12d7fed752fb23f1aa4494fbb00130e9ded4e77e37c079042d828040c325b1a5efd15fc842e44014ca4374bf38f3c3fc3ee327733b0c8aee1abcd055772f18dc04603f7b2c1ea69ff662361f2be0a171bbdcea1e5d3f"), + // txt_encoded + pack("H*", "6be8a12800455a320538853e0cba31bd2d80ea0c85164a4c5c261ae485417d93effe2ebc0d0a0b51d6ea18633d210cf63c0c4ddbc27607f2e81ed9113191ef86d56f3b99be6c415a4150299fb846ce7160b40b63baf1179d19275a2e83698376d28b92548c68e06e6d994e2c1501ed297014e702cdefee2f656447706009614d801de1caaf73f8b7fa56cf1ba94b631933bbe577624380850f117435a0355b2b"), + ), + ); + } + + /** + * @param string $mode + * @param string $cipher + * @param string $key Encryption key + * @param string $iv Initialization vector + * @param string $txt_plain Plain text to be encrypted + * @param string $txt_encoded Known ecrypted text + * + * @dataProvider provider_encode + * @covers Encrypt::encode + */ + public function test_encode($mode, $cipher, $key, $iv, $txt_plain, $txt_encoded) + { + // initialize + $e = new Kohana_EncryptTest_IvStubbed($key, $iv, $mode, $cipher); + + // prepare data + $expected = base64_encode($iv . $txt_encoded); + $actual = $e->encode($txt_plain); + + // assert + $this->assertSame($expected, $actual); + } + + /** + * Provider for test_decode + * AES Multiblock Message Test (MMT) Sample Vectors - Known Answer Test (KAT) + * @link http://csrc.nist.gov/groups/STM/cavp/index.html NIST - Cryptographic Algorithm Validation Program + * @link http://csrc.nist.gov/groups/STM/cavp/documents/aes/aesmmt.zip file used CBCMMT128.rsp + * + * @return array of $mode, $cipher, $key, $iv, $txt_encoded, $txt_plain + */ + public function provider_decode() + { + return array( + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "6a7082cf8cda13eff48c8158dda206ae"), + // IV + pack("H*", "bd4172934078c2011cb1f31cffaf486e"), + // txt_encoded + pack("H*", "f8eb31b31e374e960030cd1cadb0ef0c"), + // txt_plain + pack("H*", "940bc76d61e2c49dddd5df7f37fcf105"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "625eefa18a4756454e218d8bfed56e36"), + // IV + pack("H*", "73d9d0e27c2ec568fbc11f6a0998d7c8"), + // txt_encoded + pack("H*", "5d6fed86f0c4fe59a078d6361a142812514b295dc62ff5d608a42ea37614e6a1"), + // txt_plain + pack("H*", "360dc1896ce601dfb2a949250067aad96737847a4580ede2654a329b842fe81e"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "fd6e0b954ae2e3b723d6c9fcae6ab09b"), + // IV + pack("H*", "f08b65c9f4dd950039941da2e8058c4e"), + // txt_encoded + pack("H*", "e29e3114c8000eb484395b256b1b3267894f290d3999819ff35da03e6463c186c4d7ebb964941f1986a2d69572fcaba8"), + // txt_plain + pack("H*", "a206385945b21f812a9475f47fddbb7fbdda958a8d14c0dbcdaec36e8b28f1f6ececa1ceae4ce17721d162c1d42a66c1"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "7b1ab9144b0239315cd5eec6c75663bd"), + // IV + pack("H*", "0b1e74f45c17ff304d99c059ce5cde09"), + // txt_encoded + pack("H*", "d3f89b71e033070f9d7516a6cb4ea5ef51d6fb63d4f0fea089d0a60e47bbb3c2e10e9ba3b282c7cb79aefe3068ce228377c21a58fe5a0f8883d0dbd3d096beca"), + // txt_plain + pack("H*", "b968aeb199ad6b3c8e01f26c2edad444538c78bfa36ed68ca76123b8cdce615a01f6112bb80bfc3f17490578fb1f909a52e162637b062db04efee291a1f1af60"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "36466b6bd25ea3857ea42f0cac1919b1"), + // IV + pack("H*", "7186fb6bdfa98a16189544b228f3bcd3"), + // txt_encoded + pack("H*", "9ed957bd9bc52bba76f68cfbcde52157a8ca4f71ac050a3d92bdebbfd7c78316b4c9f0ba509fad0235fdafe90056ad115dfdbf08338b2acb1c807a88182dd2a882d1810d4302d598454e34ef2b23687d"), + // txt_plain + pack("H*", "999983467c47bb1d66d7327ab5c58f61ddb09b93bd2460cb78cbc12b5fa1ea0c5f759ccc5e478697687012ff4673f6e61eecaeda0ccad2d674d3098c7d17f887b62b56f56b03b4d055bf3a4460e83efa"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "89373ee6e28397640d5082eed4123239"), + // IV + pack("H*", "1a74d7c859672c804b82472f7e6d3c6b"), + // txt_encoded + pack("H*", "1bcba44ddff503db7c8c2ec4c4eea0e827957740cce125c1e11769842fa97e25f1b89269e6d77923a512a358312f4ba1cd33f2d111280cd83e1ef9e7cf7036d55048d5c273652afa611cc81b4e9dac7b5078b7c4716062e1032ead1e3329588a"), + // txt_plain + pack("H*", "45efd00daa4cdc8273ef785cae9e944a7664a2391e1e2c449f475acec0124bbc22944331678617408a1702917971f4654310ffb9229bec6173715ae512d37f93aaa6abf009f7e30d65669d1db0366b5bce4c7b00f871014f5753744a1878dc57"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "bab0cceddc0abd63e3f82e9fbff7b8aa"), + // IV + pack("H*", "68b9140f300490c5c942f66e777eb806"), + // txt_encoded + pack("H*", "c65b94b1f291fa9f0600f22c3c0432c895ad5d177bcccc9ea44e8ec339c9adf43855b326179d6d81aa36ef59462fd86127e9d81b0f286f93306bf74d4c79e47c1b3d4b74edd3a16290e3c63b742e41f20d66ceee794316bb63d3bd002712a1b136ba6185bd5c1dab81b07db90d2af5e5"), + // txt_plain + pack("H*", "c5585ff215bbb73ba5393440852fb199436de0d15e55c631f877670aa3eda9f672eb1f876f09544e63558436b8928000db2f02a5ad90f95b05ac4cf49e198e617e7678480fdf0efacc6aae691271e6cdd3541ebf719a1ccaedb24e2f80f92455dd5910cb5086b0960a3942ec182dcbd7"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "9c702898efa44557b29ed283f5bc0293"), + // IV + pack("H*", "cec6e1b82e8b2a591a9fa5ff1cf5cc51"), + // txt_encoded + pack("H*", "ba9f646755dacc22911f51d7de2f7e7cb0bc0b75257ea44fe883edb055c7c28ede04c3a0adcb10128ad4517d0093fa16bb0bcd2635e7a0ba92c7609bc8d8568002a7a983473724d256513aa7d51b477aabec1975ab5faf2872a6407e922180eff02f1ef86a4591c8bd3d143da6f0ef0e4806f94ace0d5b0151c99640fccbc843"), + // txt_plain + pack("H*", "1d1f8d81bdc3e2c7cb057f408e6450000c5aaed3260ff1e87fbb6f324df6887ffd8f78d7e2a04c9ed9deda9d64482d2b002f4a2b78d8b4f691875c8295d4a64b22257ceaf713ed2f4b92530d7ad7151d629acda882b4829577a43990b0948c1149c22fe4273656d1b08833930e8b06709a94579a78fc220f7057bbc1fa9f6563"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "5674636dbdb38f705f0b08c372ef4785"), + // IV + pack("H*", "3f20ce0509b57420d53b6be4d0b7f0a9"), + // txt_encoded + pack("H*", "198351f453103face6655666fe90bdbd9630e3733b2d66c013a634e91f2bf015bd2d975d71b26322e44defa32d4e9dce50363557046ece08ba38f258dae5fd3e5049c647476c81e73482e40c171d89f9fea29452caf995733589b0061464fbd5dabe27dc5ea463a3deeb7dcb43664ae6a65c498c143883ab8e83b51e5410b181647602443dc3cfffe86f0205398fa83c"), + // txt_plain + pack("H*", "6d40fd2f908f48ce19241b6b278b1b1676dffd4a97ce9f8a1574c33bc59237deb536bee376fd6c381e6987700e39283aa111cf1a59f26fae6fb6700bf012646a2ab80239bf5e1632329043aa87d7911978b36523a2bc0bed9a9737ccf7a00baa2f3822b4e9e742e168e7069290705fed2eb63aa044b78f97dd33a8d6b24741ec1fd8c8db79d93b884e762dba0f406961"), + ), + array( + // mode + MCRYPT_MODE_CBC, + // cypher + MCRYPT_RIJNDAEL_128, + // key + pack("H*", "97a1025529b9925e25bbe78770ca2f99"), + // IV + pack("H*", "d4b4eab92aa9637e87d366384ed6915c"), + // txt_encoded + pack("H*", "22cdc3306fcd4d31ccd32720cbb61bad28d855670657c48c7b88c31f4fa1f93c01b57da90be63ead67d6a325525e6ed45083e6fb70a53529d1fa0f55653b942af59d78a2660361d63a7290155ac5c43312a25b235dacbbc863faf00940c99624076dfa44068e7c554c9038176953e571751dfc0954d41d113771b06466b1c8d13e0d4cb675ed58d1a619e1540970983781dc11d2dd8525ab5745958d615defda"), + // txt_plain + pack("H*", "e8b89150d8438bf5b17449d6ed26bd72127e10e4aa57cad85283e8359e089208e84921649f5b60ea21f7867cbc9620560c4c6238db021216db453c9943f1f1a60546173daef2557c3cdd855031b353d4bf176f28439e48785c37d38f270aa4a6faad2baabcb0c0b2d1dd5322937498ce803ba1148440a52e227ddba4872fe4d81d2d76a939d24755adb8a7b8452ceed2d179e1a5848f316f5c016300a390bfa7"), + ), + ); + } + + /** + * @param string $mode + * @param string $cipher + * @param string $key Encryption key + * @param string $iv Initialization vector + * @param string $txt_encoded ecrypted text + * @param string $txt_plain Known plain text that is decripted + * + * @dataProvider provider_decode + * @covers Encrypt::decode + */ + public function test_decode($mode, $cipher, $key, $iv, $txt_encoded, $txt_plain) + { + // initialize + $e = new Encrypt($key, $mode, $cipher); + + // prepare data + $expected = $txt_plain; + $actual = $e->decode(base64_encode($iv . $txt_encoded)); + + // assert + $this->assertSame($expected, $actual); + } + + /** + * Provider for test_encode_decode, test_consecutive_encode_different_results + * + * @return array of $key, $mode, $cipher, $txt_plain + */ + public function provider_encode_decode() + { + return array( + array( + // key + "Some super secret key", + // mode + MCRYPT_MODE_NOFB, + // cypher + MCRYPT_RIJNDAEL_128, + // txt_plain + "The quick brown fox jumps over the lazy dog", + ), + array( + // key + "De finibus bonorum et malorum", + // mode + MCRYPT_MODE_NOFB, + // cypher + MCRYPT_RIJNDAEL_128, + // txt_plain + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ), + ); + } + + /** + * @param type $key Encryption Key + * @param type $mode Encryption Mode + * @param type $cipher Encryption Cipher + * @param type $txt_plain Plain text to encode and then decode back + * + * @dataProvider provider_encode_decode + * @covers Encrypt::encode + * @covers Encrypt::decode + */ + public function test_encode_decode($key, $mode, $cipher, $txt_plain) + { + // initialize, encode + $e = new Encrypt($key, $mode, $cipher); + $txt_encoded = $e->encode($txt_plain); + + // prepare data + $expected = $txt_plain; + $actual = $e->decode($txt_encoded); + + // assert + $this->assertSame($expected, $actual); + } + + /** + * Provider for test_decode_invalid_data + * + * @return array of $key, $mode, $cipher, $txt_invalid_encoded + */ + public function provider_decode_invalid_data() + { + return array( + array( + // key + "Some super secret key", + // mode + MCRYPT_MODE_NOFB, + // cypher + MCRYPT_RIJNDAEL_128, + // txt_invalid_encoded + ".:This data is not a valid base 64 string:.", + ), + array( + // key + "Some super secret key", + // mode + MCRYPT_MODE_NOFB, + // cypher + MCRYPT_RIJNDAEL_128, + // txt_invalid_encoded + base64_encode("too short"), + ), + ); + } + + /** + * Tests for decode when the string is not valid base64, + * or is too short to contain a valid IV + * + * @param type $key + * @param type $mode + * @param type $cipher + * @param type $txt_encoded + * + * @dataProvider provider_decode_invalid_data + */ + public function test_decode_invalid_data($key, $mode, $cipher, $txt_invalid_encoded) + { + // initialize + $e = new Encrypt($key, $mode, $cipher); + + // assert + $this->AssertFalse($e->decode($txt_invalid_encoded)); + } + + /** + * @param type $key Encryption Key + * @param type $mode Encryption Mode + * @param type $cipher Encryption Cipher + * @param type $txt_plain Plain text to encode and then decode back + * + * @dataProvider provider_encode_decode + * @covers Encrypt::encode + */ + public function test_consecutive_encode_produce_different_results($key, $mode, $cipher, $txt_plain) + { + // initialize, encode twice + $e = new Encrypt($key, $mode, $cipher); + $txt_encoded_first = $e->encode($txt_plain); + $txt_encoded_second = $e->encode($txt_plain); + + // assert + $this->assertNotEquals($txt_encoded_first, $txt_encoded_second); + } + + /** + * Provider for test_key_normalization + * + * @return array of $key, $iv, $mode, $cipher, $txt_plain + */ + public function provider_key_normalization() + { + return array( + array( + // key + "Some super secret key", + // IV + pack("H*", "2fe2b333ceda8f98f4a99b40d2cd34a8"), + // mode + MCRYPT_MODE_NOFB, + // cypher + MCRYPT_RIJNDAEL_128, + // txt_plain + "The quick brown fox jumps over the lazy dog", + ), + array( + // key + "De finibus bonorum et malorum", + // IV + pack("H*", "2fe2b333ceda8f98f4a99b40d2cd34a8"), + // mode + MCRYPT_MODE_NOFB, + // cypher + MCRYPT_RIJNDAEL_128, + // txt_plain + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ), + ); + } + + /** + * Test if key normalization logic behaves well + * Encrypt::_normalize_key was ment for PHP > 5.6.0 + * + * We are testing our key normalization only against lower versions of PHP + * (PHP < 5.6.0) to see if it matches the internal key padding those + * PHP versions already have + * + * @param type $key Encryption Key + * @param type $mode Encryption Mode + * @param type $cipher Encryption Cipher + * @param type $txt_plain Plain text to encode and then decode back + * + * @dataProvider provider_key_normalization + * @covers Encrypt::_normalize_key + */ + public function test_key_normalization($key, $iv, $mode, $cipher, $txt_plain) + { + if (version_compare(PHP_VERSION, '5.6.0', '>=')) + { + $this->markTestSkipped('Starting from PHP 5.6.0, mcrypt does not pad encryption keys with null bytes.'); + } + + // initialize, encode twice + $e1 = new Kohana_EncryptTest_IvStubbed($key, $iv, $mode, $cipher); + $e2 = new Kohana_EncryptTest_KeyNormalized($key, $iv, $mode, $cipher); + + $txt_encoded_1 = $e1->encode($txt_plain); + $txt_encoded_2 = $e2->encode($txt_plain); + + // assert + $this->assertSame($txt_encoded_1, $txt_encoded_2); + } + + /** + * @expectedException Kohana_Exception + * @expectedExceptionMessage No encryption key is defined in the encryption configuration group + */ + public function test_instance_throw_exception_when_no_key_provided() + { + Encrypt::instance(); + } + + /** + * Provider for test_instance_returns_singleton + * + * @return array of $instance_name, $missing_config + */ + public function provider_instance_returns_singleton() + { + return array( + array( + 'default', + array( + 'key' => 'trwQwVXX96TIJoKxyBHB9AJkwAOHixuV1ENZmIWyanI0j1zNgSVvqywy044Agaj', + ) + ), + array( + 'blowfish', + array( + 'key' => '7bZJJkmNrelj5NaKoY6h6rMSRSmeUlJuTeOd5HHka5XknyMX4uGSfeVolTz4IYy', + 'cipher' => MCRYPT_BLOWFISH, + 'mode' => MCRYPT_MODE_ECB, + ) + ), + array( + 'tripledes', + array( + 'key' => 'a9hcSLRvA3LkFc7EJgxXIKQuz1ec91J7P6WNq1IaxMZp4CTj5m31gZLARLxI1jD', + 'cipher' => MCRYPT_3DES, + 'mode' => MCRYPT_MODE_CBC, + ) + ), + ); + } + + /** + * Test to multiple calls to the instance() method returns same instance + * also test if the instances are appropriately configured. + * + * @param string $instance_name instance name + * @param array $config_array array of config variables missing from config + * + * @dataProvider provider_instance_returns_singleton + */ + public function test_instance_returns_singleton($instance_name, array $config_array) + { + // load config + $config = Kohana::$config->load('encrypt'); + // if instance name is NULL the config group should be the default + $config_group = $instance_name ? : Encrypt::$default; + // if config group does not exists, create + if (!array_key_exists($config_group, $config)) + { + $config[$config_group] = array(); + } + // fill in the missing config variables + $config[$config_group] = $config[$config_group] + $config_array; + + // call instance twice + $e = Encrypt::instance($instance_name); + $e2 = Encrypt::instance($instance_name); + + // assert instances + $this->assertInstanceOf('Encrypt', $e); + $this->assertInstanceOf('Encrypt', $e2); + $this->assertSame($e, $e2); + + // test if instances are well configured + // prepare expected variables + $expected_cipher = $config[$config_group]['cipher']; + $expected_mode = $config[$config_group]['mode']; + $expected_key_size = mcrypt_get_key_size($expected_cipher, $expected_mode); + $expected_key = substr($config[$config_group]['key'], 0, $expected_key_size); + + // assert + $this->assertSameProtectedProperty($expected_key, $e, '_key'); + $this->assertSameProtectedProperty($expected_cipher, $e, '_cipher'); + $this->assertSameProtectedProperty($expected_mode, $e, '_mode'); + } + + /** + * Helper method to test for private/protected properties + * + * @param mixed $expect Expected value + * @param mixed $object object that holds the private/protected property + * @param string $name the name of the private/protected property + */ + protected function assertSameProtectedProperty($expect, $object, $name) + { + $refl = new ReflectionClass($object); + $property = $refl->getProperty($name); + $property->setAccessible(TRUE); + $this->assertSame($expect, $property->getValue($object)); + } + +} + +/** + * Class Kohana_EncryptTest_IvStubbed wraps the Encrypt class to mock out + * the actual mcrypt_create_iv calls for unit testing. + */ +class Kohana_EncryptTest_IvStubbed extends Encrypt +{ + + /** + * override constructor to force class use known IVs + * + * @param string $key encryption key + * @param string $iv feed a known IV + * @param string $mode mcrypt mode + * @param string $cipher mcrypt cipher + */ + public function __construct($key, $iv, $mode, $cipher) + { + parent::__construct($key, $mode, $cipher); + + $this->_iv = $iv; + } + + /** + * Fake a random initialization vector by returning a known one + * + * @return string a known IV + */ + protected function _create_iv() + { + return isset($this->_iv) ? $this->_iv : FALSE; + } + +} + +/** + * Class Kohana_EncryptTest_KeyNormalized wraps the Encrypt class to mock out + * the actual mcrypt_create_iv calls for unit testing, as well as to always + * normalize keys + */ +class Kohana_EncryptTest_KeyNormalized extends Kohana_EncryptTest_IvStubbed +{ + + /** + * override constructor to force key normalization + * + * @param string $key encryption key + * @param string $mode mcrypt mode + * @param string $cipher mcrypt cipher + */ + public function __construct($key, $iv, $mode, $cipher) + { + parent::__construct($key, $iv, $mode, $cipher); + + $this->_key = $this->_normalize_key($this->_key, $this->_cipher, $this->_mode); + } + + +} + diff --git a/system/tests/kohana/FeedTest.php b/system/tests/kohana/FeedTest.php index 4098630..e6b401c 100644 --- a/system/tests/kohana/FeedTest.php +++ b/system/tests/kohana/FeedTest.php @@ -16,6 +16,18 @@ */ class Kohana_FeedTest extends Unittest_TestCase { + + /** + * Sets up the environment + */ + // @codingStandardsIgnoreStart + public function setUp() + // @codingStandardsIgnoreEnd + { + parent::setUp(); + Kohana::$config->load('url')->set('trusted_hosts', array('localhost')); + } + /** * Provides test data for test_parse() * @@ -25,7 +37,8 @@ class Kohana_FeedTest extends Unittest_TestCase { return array( // $source, $expected - array('http://dev.kohanaframework.org/projects/kohana3/activity.atom', 15), + array(realpath(__DIR__.'/../test_data/feeds/activity.atom'), array('Proposals (Political/Workflow) #4839 (New)', 'Proposals (Political/Workflow) #4782')), + array(realpath(__DIR__.'/../test_data/feeds/example.rss20'), array('Example entry')), ); } @@ -38,11 +51,15 @@ class Kohana_FeedTest extends Unittest_TestCase * @param string $source URL to test * @param integer $expected Count of items */ - public function test_parse($source, $expected) + public function test_parse($source, $expected_titles) { - $this->markTestSkipped('We don\'t go to the internet for tests.'); + $titles = array(); + foreach (Feed::parse($source) as $item) + { + $titles[] = $item['title']; + } - $this->assertEquals($expected, count(Feed::parse($source))); + $this->assertSame($expected_titles, $titles); } /** diff --git a/system/tests/kohana/FileTest.php b/system/tests/kohana/FileTest.php index 8d4c491..712bb2c 100644 --- a/system/tests/kohana/FileTest.php +++ b/system/tests/kohana/FileTest.php @@ -5,7 +5,7 @@ * * @group kohana * @group kohana.core - * @group kohana.core.url + * @group kohana.core.file * * @package Kohana * @category Tests @@ -25,8 +25,7 @@ class Kohana_FileTest extends Unittest_TestCase { return array( // $value, $result - array(Kohana::find_file('classes', 'File')), - array(Kohana::find_file('tests', 'test_data/github', 'png')), + array(Kohana::find_file('tests', 'test_data/github', 'png'), 'image/png'), ); } @@ -38,12 +37,10 @@ class Kohana_FileTest extends Unittest_TestCase * @param boolean $input Input for File::mime * @param boolean $expected Output for File::mime */ - public function test_mime($input) + public function test_mime($input, $expected) { - $this->markTestSkipped( - 'This test doesn\'t do anything useful!' - ); - $this->assertSame(1, preg_match('/^(?:application|audio|image|message|multipart|text|video)\/[a-z.+0-9-]+$/i', File::mime($input))); + //@todo: File::mime coverage needs significant improvement or to be dropped for a composer package - it's a "horribly unreliable" method with very little testing + $this->assertSame($expected, File::mime($input)); } /** diff --git a/system/tests/kohana/HTMLTest.php b/system/tests/kohana/HTMLTest.php index 015a65d..acc203b 100644 --- a/system/tests/kohana/HTMLTest.php +++ b/system/tests/kohana/HTMLTest.php @@ -16,6 +16,18 @@ */ class Kohana_HTMLTest extends Unittest_TestCase { + + /** + * Sets up the environment + */ + // @codingStandardsIgnoreStart + public function setUp() + // @codingStandardsIgnoreEnd + { + parent::setUp(); + Kohana::$config->load('url')->set('trusted_hosts', array('www\.kohanaframework\.org')); + } + /** * Defaults for this test * @var array @@ -117,6 +129,10 @@ class Kohana_HTMLTest extends Unittest_TestCase 'https', FALSE ), + array( + '', + '//google.com/script.js', + ), ); } @@ -193,6 +209,13 @@ class Kohana_HTMLTest extends Unittest_TestCase 'https', TRUE ), + array( + '', + '//google.com/style.css', + array(), + NULL, + FALSE + ), ); } @@ -223,6 +246,20 @@ class Kohana_HTMLTest extends Unittest_TestCase public function provider_anchor() { return array( + // a fragment-only anchor + array( + 'Kohana', + array(), + '#go-to-section-kohana', + 'Kohana', + ), + // a query-only anchor + array( + 'Category A', + array(), + '?cat=a', + 'Category A', + ), array( 'Kohana', array(), diff --git a/system/tests/kohana/HTTPTest.php b/system/tests/kohana/HTTPTest.php index 0f80548..33ce726 100644 --- a/system/tests/kohana/HTTPTest.php +++ b/system/tests/kohana/HTTPTest.php @@ -15,6 +15,33 @@ */ class Kohana_HTTPTest extends Unittest_TestCase { + protected $_inital_request; + + /** + * Sets up the environment + */ + // @codingStandardsIgnoreStart + public function setUp() + // @codingStandardsIgnoreEnd + { + parent::setUp(); + Kohana::$config->load('url')->set('trusted_hosts', array('www\.example\.com')); + $this->_initial_request = Request::$initial; + Request::$initial = new Request('/'); + } + + /** + * Tears down whatever is setUp + */ + // @codingStandardsIgnoreStart + public function tearDown() + // @codingStandardsIgnoreEnd + { + Request::$initial = $this->_initial_request; + parent::tearDown(); + } + // @codingStandardsIgnoreStart + /** * Defaults for this test * @var array @@ -84,4 +111,70 @@ class Kohana_HTTPTest extends Unittest_TestCase { $this->fail('HTTP_Exception_Redirect not thrown'); } + + /** + * Provides test data for test_request_headers + * + * @return array + */ + public function provider_request_headers() + { + return array( + array( + array( + 'CONTENT_TYPE' => 'text/html; charset=utf-8', + 'CONTENT_LENGTH' => '3547', + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'HTTP_ACCEPT_ENCODING' => 'gzip, deflate, sdch', + 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8,fr;q=0.6,hy;q=0.4', + ), + array( + 'content-type' => 'text/html; charset=utf-8', + 'content-length' => '3547', + 'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'accept-encoding'=>'gzip, deflate, sdch', + 'accept-language'=>'en-US,en;q=0.8,fr;q=0.6,hy;q=0.4', + ) + ), + array( + array( + 'HTTP_WEIRD_HTTP_HEADER' => 'A weird value for a weird header', + ), + array( + 'weird-http-header' => 'A weird value for a weird header', + ) + ), + ); + } + + /** + * Tests HTTP::request_headers() + * + * HTTP::request_headers relies on the $_SERVER superglobal if the function + * `apache_request_headers` or the PECL `http` extension are not available. + * + * The test feeds the $_SERVER superglobal with the test cases' datasets + * and then restores the $_SERVER superglobal so that it does not affect + * other tests. + * + * @test + * @dataProvider provider_request_headers + * @param array $server_globals globals to feed $_SERVER + * @param array $expected_headers Expected, cleaned HTTP headers + */ + public function test_request_headers(array $server_globals, array $expected_headers) + { + // save the $_SERVER super-global into temporary local var + $tmp_server = $_SERVER; + + $_SERVER = array_replace_recursive($_SERVER, $server_globals); + $headers = HTTP::request_headers(); + + $actual_headers = array_intersect_key($headers->getArrayCopy(), $expected_headers); + + $this->assertSame($expected_headers, $actual_headers); + + // revert the super-global to its previous state + $_SERVER = $tmp_server; + } } diff --git a/system/tests/kohana/RequestTest.php b/system/tests/kohana/RequestTest.php index 3ba516f..a5df3db 100644 --- a/system/tests/kohana/RequestTest.php +++ b/system/tests/kohana/RequestTest.php @@ -23,6 +23,7 @@ class Kohana_RequestTest extends Unittest_TestCase // @codingStandardsIgnoreEnd { parent::setUp(); + Kohana::$config->load('url')->set('trusted_hosts', array('localhost')); $this->_initial_request = Request::$initial; Request::$initial = new Request('/'); } @@ -125,16 +126,15 @@ class Kohana_RequestTest extends Unittest_TestCase { $route = new Route('((/(/)))'); - $uri = 'foo/bar/id'; + $uri = 'kohana_requesttest_dummy/foobar/some_id'; $request = Request::factory($uri, NULL, TRUE, array($route)); // We need to execute the request before it has matched a route - try - { - $request->execute(); - } - catch (Exception $e) {} + $response = $request->execute(); + $controller = new Controller_Kohana_RequestTest_Dummy($request, $response); + $this->assertSame(200, $response->status()); + $this->assertSame($controller->get_expected_response(), $response->body()); $this->assertArrayHasKey('id', $request->param()); $this->assertArrayNotHasKey('foo', $request->param()); $this->assertEquals($request->uri(), $uri); @@ -150,17 +150,16 @@ class Kohana_RequestTest extends Unittest_TestCase $this->assertArrayNotHasKey('route', $params); $route = new Route('()', array('uri' => '.+')); - $route->defaults(array('controller' => 'foobar', 'action' => 'index')); - $request = Request::factory('foobar', NULL, TRUE, array($route)); + $route->defaults(array('controller' => 'kohana_requesttest_dummy', 'action' => 'foobar')); + $request = Request::factory('kohana_requesttest_dummy', NULL, TRUE, array($route)); // We need to execute the request before it has matched a route - try - { - $request->execute(); - } - catch (Exception $e) {} + $response = $request->execute(); + $controller = new Controller_Kohana_RequestTest_Dummy($request, $response); - $this->assertSame('foobar', $request->param('uri')); + $this->assertSame(200, $response->status()); + $this->assertSame($controller->get_expected_response(), $response->body()); + $this->assertSame('kohana_requesttest_dummy', $request->param('uri')); } /** @@ -270,6 +269,16 @@ class Kohana_RequestTest extends Unittest_TestCase 'http', 'http://localhost/kohana/foo' ), + array( + 'http://www.google.com', + 'http', + 'http://www.google.com' + ), + array( + '0', + 'http', + 'http://localhost/kohana/0' + ) ); } @@ -296,7 +305,14 @@ class Kohana_RequestTest extends Unittest_TestCase 'Kohana::$index_file' => FALSE, )); - $this->assertEquals(Request::factory($uri)->url($protocol), $expected); + // issue #3967: inject the route so that we don't conflict with the application's default route + $route = new Route('((/))'); + $route->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + + $this->assertEquals(Request::factory($uri, array(), TRUE, array($route))->url($protocol), $expected); } /** @@ -397,8 +413,15 @@ class Kohana_RequestTest extends Unittest_TestCase */ public function provider_uri_only_trimed_on_internal() { + // issue #3967: inject the route so that we don't conflict with the application's default route + $route = new Route('((/))'); + $route->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + $old_request = Request::$initial; - Request::$initial = new Request(TRUE); + Request::$initial = new Request(TRUE, array(), TRUE, array($route)); $result = array( array( @@ -417,6 +440,14 @@ class Kohana_RequestTest extends Unittest_TestCase new Request('foo/bar'), 'foo/bar' ), + array( + new Request('/0'), + '0' + ), + array( + new Request('0'), + '0' + ), array( new Request('/'), '/' @@ -505,18 +536,18 @@ class Kohana_RequestTest extends Unittest_TestCase { $x_powered_by = 'Kohana Unit Test'; $content_type = 'application/x-www-form-urlencoded'; + $request = new Request('foo/bar', array(), TRUE, array()); return array( array( - $request = Request::factory('foo/bar') - ->headers(array( + $request->headers(array( 'x-powered-by' => $x_powered_by, 'content-type' => $content_type ) ), - array( - 'x-powered-by' => $x_powered_by, - 'content-type' => $content_type + array( + 'x-powered-by' => $x_powered_by, + 'content-type' => $content_type ) ) ); @@ -548,7 +579,6 @@ class Kohana_RequestTest extends Unittest_TestCase { return array( array( - Request::factory(), array( 'content-type' => 'application/x-www-form-urlencoded', 'x-test-header' => 'foo' @@ -556,7 +586,6 @@ class Kohana_RequestTest extends Unittest_TestCase "Content-Type: application/x-www-form-urlencoded\r\nX-Test-Header: foo\r\n\r\n" ), array( - Request::factory(), array( 'content-type' => 'application/json', 'x-powered-by' => 'kohana' @@ -571,13 +600,13 @@ class Kohana_RequestTest extends Unittest_TestCase * * @dataProvider provider_headers_set * - * @param Request request object * @param array header(s) to set to the request object * @param string expected http header * @return void */ - public function test_headers_set(Request $request, $headers, $expected) + public function test_headers_set($headers, $expected) { + $request = new Request(TRUE, array(), TRUE, array()); $request->headers($headers); $this->assertSame($expected, (string) $request->headers()); } @@ -707,14 +736,83 @@ class Kohana_RequestTest extends Unittest_TestCase $this->assertEquals($client->strict_redirect(), FALSE); } + /** + * Tests correctness request content-length header after calling render + */ + public function test_content_length_after_render() + { + $request = Request::factory('https://example.org/post') + ->client(new Kohana_RequestTest_Header_Spying_Request_Client_External) + ->method(Request::POST) + ->post(array('aaa' => 'bbb')); + $request->render(); + + $request->execute(); + + $headers = $request->client()->get_received_request_headers(); + + $this->assertEquals(strlen($request->body()), $headers['content-length']); + } + + /** + * Tests correctness request content-length header after calling render + * and changing post + */ + public function test_content_length_after_changing_post() + { + $request = Request::factory('https://example.org/post') + ->client(new Kohana_RequestTest_Header_Spying_Request_Client_External) + ->method(Request::POST) + ->post(array('aaa' => 'bbb')); + + $request->render(); + + $request->post(array('one' => 'one', 'two' => 'two', 'three' => 'three')); + + $request->execute(); + + $headers = $request->client()->get_received_request_headers(); + + $this->assertEquals(strlen($request->body()), $headers['content-length']); + } } // End Kohana_RequestTest +/** + * A dummy Request_Client_External implementation, that spies on the headers + * of the request + */ +class Kohana_RequestTest_Header_Spying_Request_Client_External extends Request_Client_External +{ + private $headers; + + protected function _send_message(\Request $request, \Response $response) + { + $this->headers = $request->headers(); + + return $response; + } + + public function get_received_request_headers() + { + return $this->headers; + } +} + class Controller_Kohana_RequestTest_Dummy extends Controller { - public function action_index() + // hard coded dummy response + protected $dummy_response = "this is a dummy response"; + + public function action_foobar() { - + $this->response->body($this->dummy_response); } + + public function get_expected_response() + { + return $this->dummy_response; + } + } // End Kohana_RequestTest diff --git a/system/tests/kohana/ResponseTest.php b/system/tests/kohana/ResponseTest.php index d5e8870..9989163 100644 --- a/system/tests/kohana/ResponseTest.php +++ b/system/tests/kohana/ResponseTest.php @@ -171,27 +171,6 @@ class Kohana_ResponseTest extends Unittest_TestCase $this->assertSame(Cookie::$expiration, $cookie['expiration']); } - /** - * Tests that the headers are not sent by PHP in CLI mode - * - * @return void - */ - public function test_send_headers_cli() - { - if (headers_sent()) - { - $this->markTestSkipped('Cannot test this feature as headers have already been sent!'); - } - - $content_type = 'application/json'; - $response = new Response; - $response->headers('content-type', $content_type) - ->send_headers(); - - $this->assertFalse(headers_sent()); - - } - /** * Test the content type is sent when set * @@ -205,4 +184,4 @@ class Kohana_ResponseTest extends Unittest_TestCase $headers = $response->send_headers()->headers(); $this->assertSame($content_type, (string) $headers['content-type']); } -} \ No newline at end of file +} diff --git a/system/tests/kohana/RouteTest.php b/system/tests/kohana/RouteTest.php index dd7a3f9..bf36278 100644 --- a/system/tests/kohana/RouteTest.php +++ b/system/tests/kohana/RouteTest.php @@ -28,6 +28,8 @@ class Kohana_RouteTest extends Unittest_TestCase { parent::setUp(); + Kohana::$config->load('url')->set('trusted_hosts', array('kohanaframework\.org')); + $this->cleanCacheDir(); } @@ -268,11 +270,7 @@ class Kohana_RouteTest extends Unittest_TestCase $route = new Route($uri); // Mock a request class with the $match uri - $stub = $this->getMock('Request', array('uri'), array($match)); - $stub->expects($this->any()) - ->method('uri') - // Request::uri() called by Route::matches() will return $match - ->will($this->returnValue($match)); + $stub = $this->get_request_mock($match); $this->assertSame(FALSE, $route->matches($stub)); } @@ -308,11 +306,7 @@ class Kohana_RouteTest extends Unittest_TestCase $route = new Route($uri); // Mock a request class with the $m uri - $request = $this->getMock('Request', array('uri'), array($m)); - $request->expects($this->any()) - ->method('uri') - // Request::uri() called by Route::matches() will return $m - ->will($this->returnValue($m)); + $request = $this->get_request_mock($m); $matches = $route->matches($request); @@ -381,10 +375,7 @@ class Kohana_RouteTest extends Unittest_TestCase $this->assertSame($defaults, $route->defaults()); // Mock a request class - $request = $this->getMock('Request', array('uri'), array($default_uri)); - $request->expects($this->any()) - ->method('uri') - ->will($this->returnValue($default_uri)); + $request = $this->get_request_mock($default_uri); $matches = $route->matches($request); @@ -550,28 +541,19 @@ class Kohana_RouteTest extends Unittest_TestCase $route = new Route($uri); // Mock a request class that will return empty uri - $request = $this->getMock('Request', array('uri'), array('')); - $request->expects($this->any()) - ->method('uri') - ->will($this->returnValue('')); + $request = $this->get_request_mock(''); $this->assertFalse($route->matches($request)); // Mock a request class that will return route1 - $request = $this->getMock('Request', array('uri'), array($matches_route1)); - $request->expects($this->any()) - ->method('uri') - ->will($this->returnValue($matches_route1)); + $request = $this->get_request_mock($matches_route1); $matches = $route->matches($request); $this->assertInternalType('array', $matches); // Mock a request class that will return route2 uri - $request = $this->getMock('Request', array('uri'), array($matches_route2)); - $request->expects($this->any()) - ->method('uri') - ->will($this->returnValue($matches_route2)); + $request = $this->get_request_mock($matches_route2); $matches = $route->matches($request); @@ -899,14 +881,78 @@ class Kohana_RouteTest extends Unittest_TestCase $route = new Route($route); // Mock a request class - $request = $this->getMock('Request', array('uri'), array($uri)); - $request->expects($this->any()) - ->method('uri') - ->will($this->returnValue($uri)); + $request = $this->get_request_mock($uri); $params = $route->defaults($defaults)->filter($filter)->matches($request); $this->assertSame($expected_params, $params); } + /** + * Provides test data for test_route_uri_encode_parameters + * + * @return array + */ + public function provider_route_uri_encode_parameters() + { + return array( + array( + 'article', + 'blog/article/', + array( + 'controller' => 'home', + 'action' => 'index' + ), + 'article_name', + 'Article name with special chars \\ ##', + 'blog/article/Article%20name%20with%20special%20chars%20\\%20%23%23' + ) + ); + } + + /** + * http://dev.kohanaframework.org/issues/4079 + * + * @test + * @covers Route::get + * @ticket 4079 + * @dataProvider provider_route_uri_encode_parameters + */ + public function test_route_uri_encode_parameters($name, $uri_callback, $defaults, $uri_key, $uri_value, $expected) + { + Route::set($name, $uri_callback)->defaults($defaults); + + $get_route_uri = Route::get($name)->uri(array($uri_key => $uri_value)); + + $this->assertSame($expected, $get_route_uri); + } + + /** + * Get a mock of the Request class with a mocked `uri` method + * + * We are also mocking `method` method as it conflicts with newer PHPUnit, + * in order to avoid the fatal errors + * + * @param string $uri + * @return type + */ + public function get_request_mock($uri) + { + // Mock a request class with the $uri uri + $request = $this->getMock('Request', array('uri', 'method'), array($uri)); + + // mock `uri` method + $request->expects($this->any()) + ->method('uri') + // Request::uri() called by Route::matches() in the tests will return $uri + ->will($this->returnValue($uri)); + + // also mock `method` method + $request->expects($this->any()) + ->method('method') + ->withAnyParameters(); + + return $request; + } + } diff --git a/system/tests/kohana/SecurityTest.php b/system/tests/kohana/SecurityTest.php index 4c3b368..25eb020 100644 --- a/system/tests/kohana/SecurityTest.php +++ b/system/tests/kohana/SecurityTest.php @@ -67,17 +67,6 @@ class Kohana_SecurityTest extends Unittest_TestCase */ public function provider_csrf_token() { - // Unfortunately this data provider has to use the session in order to - // generate its data. If headers have already been sent then this method - // throws an error, even if the test is does not run. If we return an - // empty array then this also causes an error, so the only way to get - // around it is to return an array of misc data and have the test skip - // if headers have been sent. It's annoying this hack has to be - // implemented, but the security code isn't exactly brilliantly - // implemented. Ideally we'd be able to inject a session instance - if (headers_sent()) - return array(array('', '', 0)); - $array = array(); for ($i = 0; $i <= 4; $i++) { @@ -96,10 +85,7 @@ class Kohana_SecurityTest extends Unittest_TestCase */ public function test_csrf_token($expected, $input, $iteration) { - if (headers_sent()) { - $this->markTestSkipped('Headers have already been sent, session not available'); - } - + //@todo: the Security::token tests need to be reviewed to check how much of the logic they're actually covering Security::$token_name = 'token_'.$iteration; $this->assertSame(TRUE, $input); $this->assertSame($expected, Security::token(FALSE)); diff --git a/system/tests/kohana/SessionTest.php b/system/tests/kohana/SessionTest.php index 1f34fa8..83a3592 100644 --- a/system/tests/kohana/SessionTest.php +++ b/system/tests/kohana/SessionTest.php @@ -95,20 +95,14 @@ class Kohana_SessionTest extends Unittest_TestCase */ public function test_constructor_loads_session_with_session_id() { - $this->markTestIncomplete( - 'Need to work out why constructor is not being called' - ); - $config = array(); $session_id = 'lolums'; // Don't auto-call constructor, we need to setup the mock first - $session = $this->getMockForAbstractClass( - 'Session', - array(), - '', - FALSE - ); + $session = $this->getMockBuilder('Session') + ->disableOriginalConstructor() + ->setMethods(array('read')) + ->getMockForAbstractClass(); $session ->expects($this->once()) diff --git a/system/tests/kohana/TextTest.php b/system/tests/kohana/TextTest.php index a59bd59..0c93270 100644 --- a/system/tests/kohana/TextTest.php +++ b/system/tests/kohana/TextTest.php @@ -196,6 +196,30 @@ class Kohana_TextTest extends Unittest_TestCase $this->assertSame('yes', Text::alternate($val_a, $val_b, $val_c)); } + /** + * Provides test data for test_ucfirst + * + * @return array Test data + */ + public function provider_ucfirst() + { + return array( + array('Content-Type', 'content-type', '-'), + array('Բարեւ|Ձեզ', 'բարեւ|ձեզ', '|'), + ); + } + + /** + * Covers Text::ucfirst() + * + * @test + * @dataProvider provider_ucfirst + */ + public function test_ucfirst($expected, $string, $delimiter) + { + $this->assertSame($expected, Text::ucfirst($string, $delimiter)); + } + /** * Provides test data for test_reducde_slashes() * @@ -386,9 +410,90 @@ class Kohana_TextTest extends Unittest_TestCase { return array ( - array('No gain, no pain', 'No gain, no pain'), - array("spaces?what'rethey?", "spaces?what'rethey?"), - array('', ''), + // A very simple widont test + array( + 'A very simple test', + 'A very simple test', + ), + // Single word items shouldn't be changed + array( + 'Test', + 'Test', + ), + // Single word after single space shouldn't be changed either + array( + ' Test', + ' Test', + ), + // Single word with HTML all around + array( + '
    • Test

      • ', + '
        • Test

          • ', + ), + // Single word after single space with HTML all around + array( + '
            • Test

              • ', + '
                • Test

                  • ', + ), + // Widont with more than one paragraph + array( + '

                    In a couple of paragraphs

                    paragraph two

                    ', + '

                    In a couple of paragraphs

                    paragraph two

                    ', + ), + // a link inside a heading + array( + '

                    In a link inside a heading

                    ', + '

                    In a link inside a heading

                    ', + ), + // a link followed by text + array( + '

                    In a link followed by other text

                    ', + '

                    In a link followed by other text

                    ', + ), + // empty html, with no text inside + array( + '

                    ', + '

                    ', + ), + // apparently, we don't love DIVs + array( + '
                    Divs get no love!
                    ', + '
                    Divs get no love!
                    ', + ), + // we don't love PREs, either + array( + '
                    Neither do PREs
                    ', + '
                    Neither do PREs
                    ', + ), + // but we love DIVs with paragraphs + array( + '

                    But divs with paragraphs do!

                    ', + '

                    But divs with paragraphs do!

                    ', + ), + array( + 'No gain, no pain', + 'No gain, no pain', + ), + array( + "spaces?what'rethey?", + "spaces?what'rethey?", + ), + /* + * // @issue 3499, with HTML at the end + * array( + * 'with HTML at the end  Kohana', + * 'with HTML at the end Kohana', + * ), + * // @issue 3499, with HTML with attributes at the end + * array( + * 'with HTML at the end: Kohana', + * 'with HTML at the end: Kohana', + * ), + */ + array( + '', + '', + ), ); } @@ -639,4 +744,111 @@ class Kohana_TextTest extends Unittest_TestCase } + + public function provider_user_agents() + { + return array( + array( + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", + array( + 'browser' => 'Chrome', + 'version' => '37.0.2049.0', + 'platform' => "Windows 8.1" + ) + ), + array( + "Mozilla/5.0 (Macintosh; U; Mac OS X 10_6_1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5", + array( + 'browser' => 'Chrome', + 'version' => '530.5', + 'platform' => "Mac OS X" + ) + ), + array( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + array( + 'browser' => 'Safari', + 'version' => '534.57.2', + 'platform' => 'Mac OS X' + ) + ), + array( + "Lynx/2.8.8dev.3 libwww-FM/2.14 SSL-MM/1.4.1", + array( + 'browser' => 'Lynx', + 'version' => '2.8.8dev.3', + 'platform' => false + ) + ) + ); + } + + /** + * Tests Text::user_agent + * + * @dataProvider provider_user_agents + * @group current + */ + public function test_user_agent_returns_correct_browser($userAgent, $expectedData) + { + $browser = Text::user_agent($userAgent, 'browser'); + + $this->assertEquals($expectedData['browser'], $browser); + } + + /** + * Tests Text::user_agent + * + * @dataProvider provider_user_agents + * @test + */ + public function test_user_agent_returns_correct_version($userAgent, $expectedData) + { + $version = Text::user_agent($userAgent, 'version'); + + $this->assertEquals($expectedData['version'], $version); + } + + /** + * Tests Text::user_agent + * @test + */ + public function test_user_agent_recognizes_robots() + { + $bot = Text::user_agent('Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', 'robot'); + + $this->assertEquals('Googlebot', $bot); + } + + /** + * Tests Text::user_agent + * + * @dataProvider provider_user_agents + * @test + */ + public function test_user_agent_returns_correct_platform($userAgent, $expectedData) + { + $platform = Text::user_agent($userAgent, 'platform'); + + $this->assertEquals($expectedData['platform'], $platform); + } + + + /** + * Tests Text::user_agent + * @test + */ + public function test_user_agent_accepts_array() + { + $agent_info = Text::user_agent( + 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 '. + '(KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36', + array('browser', 'version', 'platform')); + + $this->assertArrayHasKey('browser', $agent_info); + $this->assertArrayHasKey('version', $agent_info); + $this->assertArrayHasKey('platform', $agent_info); + + } + } diff --git a/system/tests/kohana/URLTest.php b/system/tests/kohana/URLTest.php index ed5d9f1..d0ee8c4 100644 --- a/system/tests/kohana/URLTest.php +++ b/system/tests/kohana/URLTest.php @@ -16,6 +16,18 @@ */ class Kohana_URLTest extends Unittest_TestCase { + + /** + * Sets up the environment + */ + // @codingStandardsIgnoreStart + public function setUp() + // @codingStandardsIgnoreEnd + { + parent::setUp(); + Kohana::$config->load('url')->set('trusted_hosts', array('example\.com', 'example\.org')); + } + /** * Default values for the environment, see setEnvironment * @var array @@ -276,4 +288,95 @@ class Kohana_URLTest extends Unittest_TestCase URL::query($params, $use_get) ); } + + /** + * Provides test data for URL::is_trusted_host() + * @return array + */ + public function provider_is_trusted_host() + { + return array( + // data set #0 + array( + 'givenhost', + array( + 'list-of-trusted-hosts', + ), + FALSE + ), + // data set #1 + array( + 'givenhost', + array( + 'givenhost', + 'example\.com', + ), + TRUE + ), + // data set #2 + array( + 'www.kohanaframework.org', + array( + '.*\.kohanaframework\.org', + ), + TRUE + ), + // data set #3 + array( + 'kohanaframework.org', + array( + '.*\.kohanaframework\.org', + ), + FALSE // because we are requesting a subdomain + ), + ); + } + + /** + * Tests URL::is_trusted_hosts() + * + * @test + * @dataProvider provider_is_trusted_host + * @param string $host the given host + * @param array $trusted_hosts list of trusted hosts + * @param boolean $expected TRUE if host is trusted, FALSE otherwise + */ + public function test_is_trusted_host($host, $trusted_hosts, $expected) + { + $this->assertSame( + $expected, + URL::is_trusted_host($host, $trusted_hosts) + ); + } + + /** + * Tests if invalid host throws "Invalid host" exception + * + * @test + * @expectedException Kohana_Exception + * @expectedExceptionMessage Invalid host + */ + public function test_if_invalid_host_throws_exception() + { + // set the global HTTP_HOST to + $_SERVER['HTTP_HOST'] = ''; + // trigger exception + URL::base('https'); + } + + /** + * Tests if untrusted host throws "Untrusted host" exception + * + * @test + * @expectedException Kohana_Exception + * @expectedExceptionMessage Untrusted host untrusted.com + */ + public function test_if_untrusted_host_throws_exception() + { + // set the global HTTP_HOST to a valid but untrusted host + $_SERVER['HTTP_HOST'] = 'untrusted.com'; + // trigger exception + URL::base('https'); + } + } diff --git a/system/tests/kohana/ValidationTest.php b/system/tests/kohana/ValidationTest.php index c9fce26..4f05030 100644 --- a/system/tests/kohana/ValidationTest.php +++ b/system/tests/kohana/ValidationTest.php @@ -673,4 +673,48 @@ class Kohana_ValidationTest extends Unittest_TestCase $this->assertSame($errors, $validation->errors('validation')); } + /** + * Provides test data for test_rule_label_regex + * + * @return array + */ + public function provider_rule_label_regex() + { + // $data, $field, $rules, $expected + return array( + array( + array( + 'email1' => '', + ), + 'email1', + array( + array( + 'not_empty' + ) + ), + array( + 'email1' => 'email1 must not be empty' + ), + ) + ); + } + + /** + * http://dev.kohanaframework.org/issues/4201 + * + * @test + * @ticket 4201 + * @covers Validation::rule + * @dataProvider provider_rule_label_regex + */ + public function test_rule_label_regex($data, $field, $rules, $expected) + { + $validation = Validation::factory($data)->rules($field, $rules); + + $validation->check(); + + $errors = $validation->errors(''); + + $this->assertSame($errors, $expected); + } } diff --git a/system/tests/kohana/ViewTest.php b/system/tests/kohana/ViewTest.php index 543f411..a20b4f6 100644 --- a/system/tests/kohana/ViewTest.php +++ b/system/tests/kohana/ViewTest.php @@ -60,9 +60,23 @@ class Kohana_ViewTest extends Unittest_TestCase ); } + /** + * Provider to test_set + * + * @return array + */ + public function provider_set() + { + return array( + array('foo', 'bar', 'foo', 'bar'), + array(array('foo' => 'bar'), NULL, 'foo', 'bar'), + array(new ArrayIterator(array('foo' => 'bar')), NULL, 'foo', 'bar'), + ); + } + /** * Tests that we can instantiate a view file - * + * * @test * @dataProvider provider_instantiate * @@ -80,4 +94,33 @@ class Kohana_ViewTest extends Unittest_TestCase $this->assertSame(TRUE, $expects_exception); } } + + /** + * Tests that we can set using string, array or Traversable object + * + * @test + * @dataProvider provider_set + * + * @return null + */ + public function test_set($data_key, $value, $test_key, $expected) + { + $view = View::factory()->set($data_key, $value); + $this->assertSame($expected, $view->$test_key); + } + + /** + * Tests that we can set global using string, array or Traversable object + * + * @test + * @dataProvider provider_set + * + * @return null + */ + public function test_set_global($data_key, $value, $test_key, $expected) + { + $view = View::factory(); + $view::set_global($data_key, $value); + $this->assertSame($expected, $view->$test_key); + } } diff --git a/system/tests/kohana/request/client/InternalTest.php b/system/tests/kohana/request/client/InternalTest.php index 2c87893..78827bd 100644 --- a/system/tests/kohana/request/client/InternalTest.php +++ b/system/tests/kohana/request/client/InternalTest.php @@ -17,6 +17,30 @@ */ class Kohana_Request_Client_InternalTest extends Unittest_TestCase { + + protected $_log_object; + + // @codingStandardsIgnoreStart + public function setUp() + // @codingStandardsIgnoreEnd + { + parent::setUp(); + + // temporarily save $log object + $this->_log_object = Kohana::$log; + Kohana::$log = NULL; + } + + // @codingStandardsIgnoreStart + public function tearDown() + // @codingStandardsIgnoreEnd + { + // re-assign log object + Kohana::$log = $this->_log_object; + + parent::tearDown(); + } + public function provider_response_failure_status() { return array( @@ -37,7 +61,7 @@ class Kohana_Request_Client_InternalTest extends Unittest_TestCase public function test_response_failure_status($directory, $controller, $action, $uri, $expected) { // Mock for request object - $request = $this->getMock('Request', array('directory', 'controller', 'action', 'uri', 'response'), array($uri)); + $request = $this->getMock('Request', array('directory', 'controller', 'action', 'uri', 'response', 'method'), array($uri)); $request->expects($this->any()) ->method('directory') @@ -59,10 +83,15 @@ class Kohana_Request_Client_InternalTest extends Unittest_TestCase ->method('response') ->will($this->returnValue($this->getMock('Response'))); + // mock `method` method to avoid fatals in newer versions of PHPUnit + $request->expects($this->any()) + ->method('method') + ->withAnyParameters(); + $internal_client = new Request_Client_Internal; $response = $internal_client->execute($request); $this->assertSame($expected, $response->status()); } -} \ No newline at end of file +} diff --git a/system/tests/test_data/feeds/activity.atom b/system/tests/test_data/feeds/activity.atom new file mode 100644 index 0000000..7fe42c6 --- /dev/null +++ b/system/tests/test_data/feeds/activity.atom @@ -0,0 +1,58 @@ + + + Kohana v3.x: Activity + + + http://dev.kohanaframework.org/ + http://dev.kohanaframework.org/favicon.ico?1392677580 + 2014-08-28T01:52:12Z + + Kohana Development + + +Redmine + + Proposals (Political/Workflow) #4839 (New) + + http://dev.kohanaframework.org/issues/4839 + 2014-08-28T01:52:12Z + + Guillaume Poirier-Morency + guillaumepoiriermorency@gmail.com + + +<p>I have a prototype here <a class="external" href="https://github.com/arteymix/kohana-makefile">https://github.com/arteymix/kohana-makefile</a></p> + + + <p>The tool is very useful for settings permissions and running tests.</p> + + + <p>I think we should consider having a good make tool in the sample application for the 3.4.*.</p> + + + Proposals (Political/Workflow) #4782 + + http://dev.kohanaframework.org/issues/4782#change-17279 + 2014-08-28T01:44:26Z + + Guillaume Poirier-Morency + guillaumepoiriermorency@gmail.com + + +<p>Moving to composer is a nice idea. This will allow Kohana modules to define a wide range of dependencies.</p> + + + <p>Although, I think that modules designed specifically for Kohana should end in modules and external libraries in application/vendor. This makes a clear dinsinction between what gets autoloaded by the CFS and what gets loaded by composer. Technically, we add "vendor-dir": "application/vendor" in "config" in composer.json.</p> + + + <p>Then, only add a line after the modules loading in bootstrap.php</p> + + +<pre> +// Autoloading composer packages +require Kohana::find_file('vendor', 'autoload'); +</pre> + + <p>This is pretty much what I do right now. This doesn't break anything and allow a full access to composer.</p> + + diff --git a/system/tests/test_data/feeds/example.rss20 b/system/tests/test_data/feeds/example.rss20 new file mode 100644 index 0000000..9fc6c39 --- /dev/null +++ b/system/tests/test_data/feeds/example.rss20 @@ -0,0 +1,20 @@ + + + + RSS Title + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 06 Sep 2010 00:01:00 +0000 + Sun, 06 Sep 2009 16:20:00 +0000 + 1800 + + + Example entry + Here is some text containing an interesting description. + http://www.example.com/blog/post/1 + 7bd204c6-1655-4c27-aeee-53f933c5395f + Sun, 06 Sep 2009 16:20:00 +0000 + + + + diff --git a/system/tests/test_data/message_tests/bottom_module/messages/kohana_core_message_tests.php b/system/tests/test_data/message_tests/bottom_module/messages/kohana_core_message_tests.php new file mode 100644 index 0000000..efd1679 --- /dev/null +++ b/system/tests/test_data/message_tests/bottom_module/messages/kohana_core_message_tests.php @@ -0,0 +1,6 @@ + 'inherited bottom message', + 'cfs_replaced' => 'inherited cfs_replaced message', +); diff --git a/system/tests/test_data/message_tests/top_module/messages/kohana_core_message_tests.php b/system/tests/test_data/message_tests/top_module/messages/kohana_core_message_tests.php new file mode 100644 index 0000000..07fcd7e --- /dev/null +++ b/system/tests/test_data/message_tests/top_module/messages/kohana_core_message_tests.php @@ -0,0 +1,6 @@ + 'top only message', + 'cfs_replaced' => 'overriding cfs_replaced message', +); diff --git a/system/utf8/ucwords.php b/system/utf8/ucwords.php index 1411ff1..2cc8b35 100644 --- a/system/utf8/ucwords.php +++ b/system/utf8/ucwords.php @@ -15,9 +15,10 @@ function _ucwords($str) // [\x0c\x09\x0b\x0a\x0d\x20] matches form feeds, horizontal tabs, vertical tabs, linefeeds and carriage returns. // This corresponds to the definition of a 'word' defined at http://php.net/ucwords - return preg_replace( - '/(?<=^|[\x0c\x09\x0b\x0a\x0d\x20])[^\x0c\x09\x0b\x0a\x0d\x20]/ue', - 'UTF8::strtoupper(\'$0\')', - $str - ); + return preg_replace_callback( + '/(?<=^|[\x0c\x09\x0b\x0a\x0d\x20])[^\x0c\x09\x0b\x0a\x0d\x20]/u', + function($matches){ + return UTF8::strtoupper($matches[0]); + }, + $str); } diff --git a/system/views/kohana/error.php b/system/views/kohana/error.php index 4d8b1d8..f31b5f1 100644 --- a/system/views/kohana/error.php +++ b/system/views/kohana/error.php @@ -49,7 +49,7 @@ function koggle(elem) }
                    -

                    [ ]:

                    +

                    [ ]:

                    [ ]

                    diff --git a/system/views/profiler/stats.php b/system/views/profiler/stats.php index f3e2868..b684dd6 100644 --- a/system/views/profiler/stats.php +++ b/system/views/profiler/stats.php @@ -35,7 +35,7 @@ $application_cols = array('min', 'max', 'average', 'current');
                    s
                    -
                    +
                    @@ -47,7 +47,7 @@ $application_cols = array('min', 'max', 'average', 'current');
                    kB
                    -
                    +
                    @@ -71,4 +71,4 @@ $application_cols = array('min', 'max', 'average', 'current'); -
                    \ No newline at end of file +