diff --git a/.gitignore b/.gitignore index b72f9be20..093963d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *~ *.swp +/composer.lock +/vendor/* +/koharness_bootstrap.php diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..603c929a3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - COMPOSER_ROOT_VERSION=3.3.x-dev composer install --prefer-dist + - 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/README.md b/README.md new file mode 100644 index 000000000..4689a2182 --- /dev/null +++ b/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 +pacakge. + +## 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/classes/Kohana/Arr.php b/classes/Kohana/Arr.php index 766369a81..fec1c2fa9 100644 --- a/classes/Kohana/Arr.php +++ b/classes/Kohana/Arr.php @@ -279,7 +279,13 @@ public static function range($step = 10, $max = 100) */ 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; + } } /** diff --git a/classes/Kohana/Config/Group.php b/classes/Kohana/Config/Group.php index 0460c898d..3e35fdf55 100644 --- a/classes/Kohana/Config/Group.php +++ b/classes/Kohana/Config/Group.php @@ -11,8 +11,8 @@ * @package Kohana * @category Configuration * @author Kohana Team - * @copyright (c) 2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2012-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Config_Group extends ArrayObject { diff --git a/classes/Kohana/Config/Source.php b/classes/Kohana/Config/Source.php index cfce07609..388986085 100644 --- a/classes/Kohana/Config/Source.php +++ b/classes/Kohana/Config/Source.php @@ -7,8 +7,8 @@ * @package Kohana * @category Configuration * @author Kohana Team - * @copyright (c) 2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2012-2014 Kohana Team + * @license http://kohanaframework.org/license */ interface Kohana_Config_Source {} diff --git a/classes/Kohana/Config/Writer.php b/classes/Kohana/Config/Writer.php index 3db22c037..856ebde8d 100644 --- a/classes/Kohana/Config/Writer.php +++ b/classes/Kohana/Config/Writer.php @@ -7,8 +7,8 @@ * * @package Kohana * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ interface Kohana_Config_Writer extends Kohana_Config_Source { diff --git a/classes/Kohana/Cookie.php b/classes/Kohana/Cookie.php index dafb7f5a4..e68f84fc0 100644 --- a/classes/Kohana/Cookie.php +++ b/classes/Kohana/Cookie.php @@ -71,14 +71,14 @@ public static function get($key, $default = NULL) // 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 @@ public static function get($key, $default = NULL) * 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 @@ public static function delete($name) 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 @@ public static function delete($name) * * $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 @@ public static function salt($name, $value) // 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/classes/Kohana/Core.php b/classes/Kohana/Core.php index 537ec6c56..30ef91755 100644 --- a/classes/Kohana/Core.php +++ b/classes/Kohana/Core.php @@ -16,8 +16,8 @@ class Kohana_Core { // Release version and codename - const VERSION = '3.3.2'; - const CODENAME = 'dryocopus'; + const VERSION = '3.3.3'; + const CODENAME = 'uluru'; // Common environment type constants for consistency and convenience const PRODUCTION = 10; @@ -322,7 +322,7 @@ public static function init(array $settings = NULL) } // 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 = get_magic_quotes_gpc(); // Sanitize all request variables $_GET = Kohana::sanitize($_GET); diff --git a/classes/Kohana/Date.php b/classes/Kohana/Date.php index 692d658cb..930edc381 100644 --- a/classes/Kohana/Date.php +++ b/classes/Kohana/Date.php @@ -592,10 +592,10 @@ public static function formatted_time($datetime_str = 'now', $timestamp_format = $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/classes/Kohana/Debug.php b/classes/Kohana/Debug.php index 9d1efdf52..9106b00c1 100644 --- a/classes/Kohana/Debug.php +++ b/classes/Kohana/Debug.php @@ -5,8 +5,8 @@ * @package Kohana * @category Base * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Debug { @@ -133,8 +133,8 @@ protected static function _dump( & $var, $length = 128, $limit = 10, $level = 0) 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/classes/Kohana/Encrypt.php b/classes/Kohana/Encrypt.php index 642860769..5c59f8306 100644 --- a/classes/Kohana/Encrypt.php +++ b/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 = MCRYPT_DEV_URANDOM; + + /** + * @var string Encryption key + */ + protected $_key; + + /** + * @var string mcrypt mode */ - protected static $_rand; + 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 @@ public function __construct($key, $mode, $cipher) // 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 @@ public function __construct($key, $mode, $cipher) */ 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 @@ public function decode($data) 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/classes/Kohana/Form.php b/classes/Kohana/Form.php index 510bd8109..65ed30bb6 100644 --- a/classes/Kohana/Form.php +++ b/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/classes/Kohana/HTML.php b/classes/Kohana/HTML.php index a78bc7621..9752a1e65 100644 --- a/classes/Kohana/HTML.php +++ b/classes/Kohana/HTML.php @@ -126,9 +126,9 @@ public static function anchor($uri, $title = NULL, array $attributes = NULL, $pr $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 @@ public static function mailto($email, $title = NULL, array $attributes = NULL) */ 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 @@ public static function style($file, array $attributes = NULL, $protocol = NULL, */ 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/classes/Kohana/HTTP.php b/classes/Kohana/HTTP.php index 083b43589..eca52c4e6 100644 --- a/classes/Kohana/HTTP.php +++ b/classes/Kohana/HTTP.php @@ -11,8 +11,8 @@ * @category HTTP * @author Kohana Team * @since 3.1.0 - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ abstract class Kohana_HTTP { diff --git a/classes/Kohana/HTTP/Exception.php b/classes/Kohana/HTTP/Exception.php index cb292e3ec..65c4f31d9 100644 --- a/classes/Kohana/HTTP/Exception.php +++ b/classes/Kohana/HTTP/Exception.php @@ -46,7 +46,7 @@ public function __construct($message = NULL, array $variables = NULL, Exception * Store the Request that triggered this exception. * * @param Request $request Request object that triggered this exception. - * @return Response + * @return HTTP_Exception */ public function request(Request $request = NULL) { diff --git a/classes/Kohana/HTTP/Header.php b/classes/Kohana/HTTP/Header.php index a74fbb2c6..0debabf6c 100644 --- a/classes/Kohana/HTTP/Header.php +++ b/classes/Kohana/HTTP/Header.php @@ -9,8 +9,8 @@ * @category HTTP * @author Kohana Team * @since 3.1.0 - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_HTTP_Header extends ArrayObject { @@ -287,7 +287,7 @@ public static function parse_cache_control($cache_control) * @param int $flags Flags * @param string $iterator_class The iterator class to use */ - public function __construct(array $input = array(), $flags = NULL, $iterator_class = 'ArrayIterator') + public function __construct(array $input = array(), $flags = 0, $iterator_class = 'ArrayIterator') { /** * @link http://www.w3.org/Protocols/rfc2616/rfc2616.html diff --git a/classes/Kohana/HTTP/Message.php b/classes/Kohana/HTTP/Message.php index 0861a733a..c24046ec1 100644 --- a/classes/Kohana/HTTP/Message.php +++ b/classes/Kohana/HTTP/Message.php @@ -7,8 +7,8 @@ * @category HTTP * @author Kohana Team * @since 3.1.0 - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ interface Kohana_HTTP_Message { diff --git a/classes/Kohana/HTTP/Request.php b/classes/Kohana/HTTP/Request.php index 8b2169c5f..b8992725f 100644 --- a/classes/Kohana/HTTP/Request.php +++ b/classes/Kohana/HTTP/Request.php @@ -8,8 +8,8 @@ * @category HTTP * @author Kohana Team * @since 3.1.0 - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ interface Kohana_HTTP_Request extends HTTP_Message { diff --git a/classes/Kohana/HTTP/Response.php b/classes/Kohana/HTTP/Response.php index 71058ac49..ddce6b4b9 100644 --- a/classes/Kohana/HTTP/Response.php +++ b/classes/Kohana/HTTP/Response.php @@ -8,8 +8,8 @@ * @category HTTP * @author Kohana Team * @since 3.1.0 - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ interface Kohana_HTTP_Response extends HTTP_Message { diff --git a/classes/Kohana/Kohana/Exception.php b/classes/Kohana/Kohana/Exception.php index d54b46436..3cbe166bb 100644 --- a/classes/Kohana/Kohana/Exception.php +++ b/classes/Kohana/Kohana/Exception.php @@ -217,6 +217,16 @@ public static function response(Exception $e) $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'])) { diff --git a/classes/Kohana/Log/StdErr.php b/classes/Kohana/Log/StdErr.php index 5097bb75d..53840387a 100644 --- a/classes/Kohana/Log/StdErr.php +++ b/classes/Kohana/Log/StdErr.php @@ -5,8 +5,8 @@ * @package Kohana * @category Logging * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Log_StdErr extends Log_Writer { /** diff --git a/classes/Kohana/Log/StdOut.php b/classes/Kohana/Log/StdOut.php index b0cfeb380..608c65389 100644 --- a/classes/Kohana/Log/StdOut.php +++ b/classes/Kohana/Log/StdOut.php @@ -5,8 +5,8 @@ * @package Kohana * @category Logging * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Log_StdOut extends Log_Writer { diff --git a/classes/Kohana/Request.php b/classes/Kohana/Request.php index ba7738548..2fe0f29e8 100644 --- a/classes/Kohana/Request.php +++ b/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 @@ public static function process(Request $request, $routes = NULL) 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 @@ protected static function _parse_accept( & $header, array $accepts = NULL) /** * 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); * @@ -740,7 +746,6 @@ public function uri($uri = NULL) * * 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 @@ public function uri($uri = NULL) */ 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); } diff --git a/classes/Kohana/Request/Client.php b/classes/Kohana/Request/Client.php index 26b744240..3c35faf61 100644 --- a/classes/Kohana/Request/Client.php +++ b/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 @@ public function follow_headers($follow_headers = NULL) if ($follow_headers === NULL) return $this->_follow_headers; - $this->_follow_headers = $follow_headers; + $this->_follow_headers = array_map('strtolower', $follow_headers); return $this; } @@ -405,10 +405,14 @@ public static function on_header_location(Request $request, Response $response, break; } - // Prepare the additional request + // Prepare the additional request, copying any follow_headers that were present on the original request + $orig_headers = $request->headers()->getArrayCopy(); + $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) - ->headers(Arr::extract($request->headers(), $client->follow_headers())); + ->headers($follow_headers); if ($follow_method !== Request::GET) { diff --git a/classes/Kohana/Request/Client/Curl.php b/classes/Kohana/Request/Client/Curl.php index c5dffa6f0..c9c7e21d2 100644 --- a/classes/Kohana/Request/Client/Curl.php +++ b/classes/Kohana/Request/Client/Curl.php @@ -34,7 +34,10 @@ public function _send_message(Request $request, Response $response) // 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/classes/Kohana/Response.php b/classes/Kohana/Response.php index 2a12e5e40..686997915 100644 --- a/classes/Kohana/Response.php +++ b/classes/Kohana/Response.php @@ -7,8 +7,8 @@ * @package Kohana * @category Base * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license * @since 3.1.0 */ class Kohana_Response implements HTTP_Response { diff --git a/classes/Kohana/Route.php b/classes/Kohana/Route.php index 718bf1796..976c46f5f 100644 --- a/classes/Kohana/Route.php +++ b/classes/Kohana/Route.php @@ -509,6 +509,14 @@ public function is_external() */ 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/classes/Kohana/Security.php b/classes/Kohana/Security.php index b8f66c635..b07e499e4 100644 --- a/classes/Kohana/Security.php +++ b/classes/Kohana/Security.php @@ -81,8 +81,29 @@ public static function token($new = FALSE) */ 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/classes/Kohana/Session/Native.php b/classes/Kohana/Session/Native.php index a5c8917b3..ffce15975 100644 --- a/classes/Kohana/Session/Native.php +++ b/classes/Kohana/Session/Native.php @@ -24,8 +24,31 @@ public function id() */ 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/classes/Kohana/Text.php b/classes/Kohana/Text.php index 7514fd66c..828c5950d 100644 --- a/classes/Kohana/Text.php +++ b/classes/Kohana/Text.php @@ -240,12 +240,13 @@ public static function random($type = NULL, $length = 8) * * @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 @@ public static function censor($str, $badwords, $replacement = '#', $replace_part $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 @@ public static function number($number) * * 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 @@ public static function user_agent($agent, $value) // 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/classes/Kohana/UTF8.php b/classes/Kohana/UTF8.php index ca5e315a8..fec509e0f 100644 --- a/classes/Kohana/UTF8.php +++ b/classes/Kohana/UTF8.php @@ -70,13 +70,17 @@ public static function clean($var, $charset = NULL) 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/classes/Kohana/Validation.php b/classes/Kohana/Validation.php index 4b46497e4..71f2ea86b 100644 --- a/classes/Kohana/Validation.php +++ b/classes/Kohana/Validation.php @@ -219,7 +219,7 @@ public function rule($field, $rule, array $params = NULL) 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 @@ public function check() } } + // 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/classes/Kohana/View.php b/classes/Kohana/View.php index 9a45c3548..3ca5ba0ca 100644 --- a/classes/Kohana/View.php +++ b/classes/Kohana/View.php @@ -40,6 +40,7 @@ public static function factory($file = NULL, array $data = NULL) * @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 @@ protected static function capture($kohana_view_filename, array $kohana_view_data * * 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 @@ public static function bind_global($key, & $value) * * @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) @@ -232,7 +240,7 @@ public function __toString() /** * Display the exception message. * - * We use this method here because it's impossible to throw and + * We use this method here because it's impossible to throw an * exception from __toString(). */ $error_response = Kohana_Exception::_handler($e); @@ -272,18 +280,21 @@ public function set_filename($file) * // 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/composer.json b/composer.json index 2c07aa849..9a88f4012 100644 --- a/composer.json +++ b/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/config/user_agents.php b/config/user_agents.php index f4b92eac4..3f631a4f0 100644 --- a/config/user_agents.php +++ b/config/user_agents.php @@ -3,6 +3,7 @@ return array( 'platform' => array( + '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', diff --git a/guide/kohana/bootstrap.md b/guide/kohana/bootstrap.md index 22c560f23..dd66e66dd 100644 --- a/guide/kohana/bootstrap.md +++ b/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/guide/kohana/flow.md b/guide/kohana/flow.md index 81a2e54b1..76fdfe8cb 100644 --- a/guide/kohana/flow.md +++ b/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/guide/kohana/install.md b/guide/kohana/install.md index f8e0e420b..287d8c616 100644 --- a/guide/kohana/install.md +++ b/guide/kohana/install.md @@ -34,15 +34,15 @@ Kohana::init(array( )); ~~~ - - Make sure the `application/cache` and `application/logs` directories are writable by the web server. + - Define a salt for the `Cookie` class. ~~~ -sudo chmod -R a+rwx application/cache -sudo chmod -R a+rwx application/logs +Cookie::$salt = 'some-really-long-cookie-salt-here'; ~~~ - - Define a salt for the `Cookie` class. + - Make sure the `application/cache` and `application/logs` directories are writable by the web server. ~~~ -Cookie::$salt = [really-long-cookie-salt-here] +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/guide/kohana/mvc/controllers.md b/guide/kohana/mvc/controllers.md index 3a155dbff..2c9f8ab10 100644 --- a/guide/kohana/mvc/controllers.md +++ b/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/guide/kohana/security/validation.md b/guide/kohana/security/validation.md index 43b1e1c26..baace7f91 100644 --- a/guide/kohana/security/validation.md +++ b/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/koharness.php b/koharness.php new file mode 100644 index 000000000..7845498db --- /dev/null +++ b/koharness.php @@ -0,0 +1,8 @@ + array( + 'unittest' => __DIR__ . '/vendor/kohana/unittest' + ), + 'syspath' => __DIR__, +); diff --git a/tests/kohana/Config/File/ReaderTest.php b/tests/kohana/Config/File/ReaderTest.php index ab22d14ae..bfc755eda 100644 --- a/tests/kohana/Config/File/ReaderTest.php +++ b/tests/kohana/Config/File/ReaderTest.php @@ -10,8 +10,8 @@ * @author Kohana Team * @author Jeremy Bush * @author Matt Button - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Config_File_ReaderTest extends Kohana_Unittest_TestCase { diff --git a/tests/kohana/Config/GroupTest.php b/tests/kohana/Config/GroupTest.php index 66fd05856..6222c8df6 100644 --- a/tests/kohana/Config/GroupTest.php +++ b/tests/kohana/Config/GroupTest.php @@ -10,8 +10,8 @@ * @author Kohana Team * @author Jeremy Bush * @author Matt Button - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_Config_GroupTest extends Kohana_Unittest_TestCase { diff --git a/tests/kohana/CookieTest.php b/tests/kohana/CookieTest.php index 9c1fe400c..25159b9eb 100644 --- a/tests/kohana/CookieTest.php +++ b/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 @@ public function setUp() // @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 */ - // @codingStandardsIgnoreStart - public function tearDown() - // @codingStandardsIgnoreEnd + public function test_set_creates_cookie_with_configured_cookie_options() { - parent::tearDown(); + $this->setEnvironment(array( + 'Cookie::$path' => '/path', + 'Cookie::$domain' => 'my.domain', + 'Cookie::$secure' => TRUE, + 'Cookie::$httponly' => FALSE, + )); - Cookie::$salt = NULL; + Kohana_CookieTest_TestableCookie::set('cookie', 'value'); + + $this->assertSetCookieWith(array( + 'path' => '/path', + 'domain' => 'my.domain', + 'secure' => TRUE, + 'httponly' => FALSE + )); } /** - * Provides test data for test_set() + * Provider for test_set_calculates_expiry_from_lifetime * - * @return array + * @return array of $lifetime, $expect_expiry */ - public function provider_set() + public function provider_set_calculates_expiry_from_lifetime() { return array( - array('foo', 'bar', NULL, TRUE), - array('foo', 'bar', 10, TRUE), + array(NULL, self::COOKIE_EXPIRATION + self::UNIX_TIMESTAMP), + array(0, 0), + array(10, 10 + self::UNIX_TIMESTAMP), ); } /** - * Tests cookie::set() + * @param int $expiration + * @param int $expect_expiry * - * @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() - */ - public function test_set($key, $value, $exp, $expected) - { - if (headers_sent()) { - $this->markTestSkipped('Cannot test setting cookies as headers have already been sent'); - } + * @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')); + } - $this->assertSame($expected, cookie::set($key, $value, $exp)); + /** + * @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')); } /** - * Provides test data for test_get() + * Provider for test_get_returns_default_without_deleting_if_cookie_unsigned * * @return array */ - public function provider_get() + public function provider_get_returns_default_without_deleting_if_cookie_unsigned() { - // 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), + array('unsalted'), + array('un~salted'), ); } /** - * Tests cookie::set() + * Verifies that unsigned cookies are not available to the kohana application, but are not affected for other + * consumers. + * + * @param string $unsigned_value * - * @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() + * @dataProvider provider_get_returns_default_without_deleting_if_cookie_unsigned + * @covers Cookie::get */ - public function test_get($key, $value, $expected) + public function test_get_returns_default_without_deleting_if_cookie_unsigned($unsigned_value) { - if (headers_sent()) { - $this->markTestSkipped('Cannot test setting cookies as headers have already been sent'); - } + $_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'); + } - // Force $_COOKIE - if ($key !== NULL) - { - $_COOKIE[$key] = $value; - } + /** + * 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'); + } - $this->assertSame($expected, cookie::get($key)); + /** + * @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)); } /** - * Provides test data for test_delete() + * Provider for test_salt_creates_different_hash_for_different_data * * @return array */ - public function provider_delete() + public function provider_salt_creates_different_hash_for_different_data() { return array( - array('foo', TRUE), + 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)), ); } /** - * Tests cookie::delete() + * @param array $first_args + * @param array $changed_args * - * @test - * @dataProvider provider_delete - * @covers cookie::delete - * @param mixed $key key to use - * @param boolean $expected Output for cookie::delete() + * @dataProvider provider_salt_creates_different_hash_for_different_data + * @covers Cookie::salt */ - public function test_delete($key, $expected) + public function test_salt_creates_different_hash_for_different_data($first_args, $changed_args) { - if (headers_sent()) { - $this->markTestSkipped('Cannot test setting cookies as headers have already been sent'); + $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->assertSame($expected, cookie::delete($key)); + $this->assertNotEquals($hashes[0], $hashes[1]); } /** - * Provides test data for test_salt() + * Verify that a cookie was deleted from the global $_COOKIE array, and that a setcookie call was made to remove it + * from the client. * - * @return array + * @param string $name */ - public function provider_salt() + // @codingStandardsIgnoreStart + protected function assertDeletedCookie($name) + // @codingStandardsIgnoreEnd { - return array( - array('foo', 'bar', 'b5773a6255d1deefc23f9f69bcc40fdc998e5802'), - ); + $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 + )); } /** - * Tests cookie::salt() + * Verify that there was a single call to setcookie including the provided named arguments * - * @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() + * @param array $expected + */ + // @codingStandardsIgnoreStart + protected function assertSetCookieWith($expected) + // @codingStandardsIgnoreEnd + { + $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); + } + + /** + * Configure the $_SERVER[HTTP_USER_AGENT] environment variable for the test + * + * @param string $user_agent + */ + protected function set_or_remove_http_user_agent($user_agent) + { + if ($user_agent === NULL) + { + unset($_SERVER['HTTP_USER_AGENT']); + } + else + { + $_SERVER['HTTP_USER_AGENT'] = $user_agent; + } + } +} + +/** + * 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 function test_salt($key, $value, $expected) + public static $_mock_cookies_set = array(); + + /** + * {@inheritdoc} + */ + protected static function _setcookie($name, $value, $expire, $path, $domain, $secure, $httponly) { - $this->assertSame($expected, cookie::salt($key, $value)); + 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/tests/kohana/CoreTest.php b/tests/kohana/CoreTest.php index a0099d103..bdb750914 100644 --- a/tests/kohana/CoreTest.php +++ b/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 @@ public function test_list_files_returns_array_on_success_and_failure() */ 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 @@ public function test_cache($key, $value, $lifetime) 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 @@ public function provider_message() * * @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 @@ public function provider_modules_sets_and_returns_valid_modules() { 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/tests/kohana/DebugTest.php b/tests/kohana/DebugTest.php index 39176ec17..3f845361e 100644 --- a/tests/kohana/DebugTest.php +++ b/tests/kohana/DebugTest.php @@ -13,8 +13,8 @@ * @category Tests * @author Kohana Team * @author Jeremy Bush - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_DebugTest extends Unittest_TestCase { diff --git a/tests/kohana/EncryptTest.php b/tests/kohana/EncryptTest.php new file mode 100644 index 000000000..9e93acabf --- /dev/null +++ b/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/tests/kohana/FeedTest.php b/tests/kohana/FeedTest.php index 40986303b..e4279dd51 100644 --- a/tests/kohana/FeedTest.php +++ b/tests/kohana/FeedTest.php @@ -25,7 +25,8 @@ public function provider_parse() { 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 +39,15 @@ public function provider_parse() * @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/tests/kohana/FileTest.php b/tests/kohana/FileTest.php index 8d4c49112..712bb2cdd 100644 --- a/tests/kohana/FileTest.php +++ b/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 @@ public function provider_mime() { 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 @@ public function provider_mime() * @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/tests/kohana/HTMLTest.php b/tests/kohana/HTMLTest.php index 015a65da4..c92ad1bbe 100644 --- a/tests/kohana/HTMLTest.php +++ b/tests/kohana/HTMLTest.php @@ -117,6 +117,10 @@ public function provider_script() 'https', FALSE ), + array( + '', + '//google.com/script.js', + ), ); } @@ -193,6 +197,13 @@ public function provider_style() 'https', TRUE ), + array( + '', + '//google.com/style.css', + array(), + NULL, + FALSE + ), ); } @@ -223,6 +234,20 @@ public function test_style($expected, $file, array $attributes = NULL, $protocol 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/tests/kohana/Http/HeaderTest.php b/tests/kohana/Http/HeaderTest.php index 0894b468e..8f241d3a5 100644 --- a/tests/kohana/Http/HeaderTest.php +++ b/tests/kohana/Http/HeaderTest.php @@ -11,8 +11,8 @@ * @package Kohana * @category Tests * @author Kohana Team - * @copyright (c) 2008-2012 Kohana Team - * @license http://kohanaphp.com/license + * @copyright (c) 2008-2014 Kohana Team + * @license http://kohanaframework.org/license */ class Kohana_HTTP_HeaderTest extends Unittest_TestCase { diff --git a/tests/kohana/RequestTest.php b/tests/kohana/RequestTest.php index 3ba516f35..eb31f4710 100644 --- a/tests/kohana/RequestTest.php +++ b/tests/kohana/RequestTest.php @@ -270,6 +270,11 @@ public function provider_url() 'http', 'http://localhost/kohana/foo' ), + array( + 'http://www.google.com', + 'http', + 'http://www.google.com' + ), ); } @@ -296,7 +301,14 @@ public function test_url($uri, $protocol, $expected) '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 +409,15 @@ public function test_post_max_size_exceeded($content_length, $expected) */ 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( diff --git a/tests/kohana/ResponseTest.php b/tests/kohana/ResponseTest.php index d5e8870c1..998916314 100644 --- a/tests/kohana/ResponseTest.php +++ b/tests/kohana/ResponseTest.php @@ -171,27 +171,6 @@ public function test_cookie_get() $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 @@ public function test_content_type_when_set() $headers = $response->send_headers()->headers(); $this->assertSame($content_type, (string) $headers['content-type']); } -} \ No newline at end of file +} diff --git a/tests/kohana/RouteTest.php b/tests/kohana/RouteTest.php index dd7a3f9d9..ff7cec319 100644 --- a/tests/kohana/RouteTest.php +++ b/tests/kohana/RouteTest.php @@ -909,4 +909,43 @@ public function test_route_filter_modify_params($route, $defaults, $filter, $uri $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); + } + } diff --git a/tests/kohana/SecurityTest.php b/tests/kohana/SecurityTest.php index 4c3b368f5..25eb020b5 100644 --- a/tests/kohana/SecurityTest.php +++ b/tests/kohana/SecurityTest.php @@ -67,17 +67,6 @@ public function test_strip_image_tags($expected, $input) */ 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 @@ public function provider_csrf_token() */ 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/tests/kohana/SessionTest.php b/tests/kohana/SessionTest.php index 1f34fa807..83a35921d 100644 --- a/tests/kohana/SessionTest.php +++ b/tests/kohana/SessionTest.php @@ -95,20 +95,14 @@ public function test_constructor_uses_settings_from_config_and_casts($expected, */ 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/tests/kohana/TextTest.php b/tests/kohana/TextTest.php index a59bd5939..0c93270d5 100644 --- a/tests/kohana/TextTest.php +++ b/tests/kohana/TextTest.php @@ -196,6 +196,30 @@ function test_alternate_resets_when_called_with_no_params_and_returns_empty_stri $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 @@ function provider_widont() { 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 @@ public function test_auto_link($text, $urls = array(), $emails = array()) } + + 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/tests/kohana/ValidationTest.php b/tests/kohana/ValidationTest.php index c9fce26fd..4f0503073 100644 --- a/tests/kohana/ValidationTest.php +++ b/tests/kohana/ValidationTest.php @@ -673,4 +673,48 @@ public function test_error_type_check() $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/tests/kohana/ViewTest.php b/tests/kohana/ViewTest.php index 543f411e5..a20b4f623 100644 --- a/tests/kohana/ViewTest.php +++ b/tests/kohana/ViewTest.php @@ -60,9 +60,23 @@ public function provider_instantiate() ); } + /** + * 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 @@ public function test_instantiate($path, $expects_exception) $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/tests/kohana/request/ClientTest.php b/tests/kohana/request/ClientTest.php index fffc1d5d3..9ad870297 100644 --- a/tests/kohana/request/ClientTest.php +++ b/tests/kohana/request/ClientTest.php @@ -175,6 +175,29 @@ public function test_follows_with_headers() $this->assertFalse(isset($headers['x-not-in-follow']), 'X-Not-In-Follow should not be passed to next request'); } + /** + * Tests that the follow_headers are only added to a redirect request if they were present in the original + * + * @ticket 4790 + */ + public function test_follow_does_not_add_extra_headers() + { + $response = Request::factory( + $this->_dummy_redirect_uri(301), + array( + 'follow' => TRUE, + 'follow_headers' => array('Authorization') + )) + ->headers(array()) + ->execute(); + + $data = json_decode($response->body(),TRUE); + $headers = $data['rq_headers']; + + $this->assertArrayNotHasKey('authorization', $headers, 'Empty headers should not be added when following redirects'); + } + + /** * Provider for test_follows_with_strict_method * diff --git a/tests/test_data/feeds/activity.atom b/tests/test_data/feeds/activity.atom new file mode 100644 index 000000000..7fe42c6b1 --- /dev/null +++ b/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/tests/test_data/feeds/example.rss20 b/tests/test_data/feeds/example.rss20 new file mode 100644 index 000000000..9fc6c39f1 --- /dev/null +++ b/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/tests/test_data/message_tests/bottom_module/messages/kohana_core_message_tests.php b/tests/test_data/message_tests/bottom_module/messages/kohana_core_message_tests.php new file mode 100644 index 000000000..efd167999 --- /dev/null +++ b/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/tests/test_data/message_tests/top_module/messages/kohana_core_message_tests.php b/tests/test_data/message_tests/top_module/messages/kohana_core_message_tests.php new file mode 100644 index 000000000..07fcd7e2b --- /dev/null +++ b/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/utf8/ucwords.php b/utf8/ucwords.php index 1411ff1d4..2cc8b35f9 100644 --- a/utf8/ucwords.php +++ b/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/views/kohana/error.php b/views/kohana/error.php index 4d8b1d8f5..f31b5f191 100644 --- a/views/kohana/error.php +++ b/views/kohana/error.php @@ -49,7 +49,7 @@ function koggle(elem) }
                    -

                    [ ]:

                    +

                    [ ]:

                    [ ]

                    diff --git a/views/profiler/stats.php b/views/profiler/stats.php index f3e286815..b684dd611 100755 --- a/views/profiler/stats.php +++ b/views/profiler/stats.php @@ -35,7 +35,7 @@
                    s
                    -
                    +
                    @@ -47,7 +47,7 @@
                    kB
                    -
                    +
                    @@ -71,4 +71,4 @@ -
                    \ No newline at end of file +