diff --git a/.travis.yml b/.travis.yml index 3a9e45b74..507135c11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ env: - DB=pgsql - DB=sqlite +services: + - memcached + matrix: include: - php: 5.4 @@ -29,6 +32,7 @@ before_script: - sudo apt-get install lighttpd - sh -c "if [ '$PHPCS' = '1' ]; then pear channel-discover pear.cakephp.org; fi" - sh -c "if [ '$PHPCS' = '1' ]; then pear install --alldeps cakephp/CakePHP_CodeSniffer; fi" + - echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - phpenv rehash - set +H - echo " 'password', //plaintext password (xcache.admin.pass) * )); * - * Memcache (http://www.danga.com/memcached/) + * Memcached (http://www.danga.com/memcached/) + * + * Uses the memcached extension. See http://php.net/memcached * * Cache::config('default', array( - * 'engine' => 'Memcache', //[required] + * 'engine' => 'Memcached', //[required] * 'duration' => 3600, //[optional] * 'probability' => 100, //[optional] * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string * 'servers' => array( * '127.0.0.1:11211' // localhost, default port 11211 * ), //[optional] - * 'persistent' => true, // [optional] set this to false for non-persistent connections - * 'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory) + * 'persistent' => 'my_connection', // [optional] The name of the persistent connection. + * 'compress' => false, // [optional] compress data in Memcached (slower, but uses less memory) * )); * * Wincache (http://php.net/wincache) diff --git a/lib/Cake/Cache/Cache.php b/lib/Cake/Cache/Cache.php index c7c593916..acbd5dc81 100644 --- a/lib/Cake/Cache/Cache.php +++ b/lib/Cake/Cache/Cache.php @@ -542,4 +542,38 @@ class Cache { throw new CacheException(__d('cake_dev', 'Invalid cache group %s', $group)); } +/** + * Provides the ability to easily do read-through caching. + * + * When called if the $key is not set in $config, the $callable function + * will be invoked. The results will then be stored into the cache config + * at key. + * + * Examples: + * + * Using a Closure to provide data, assume $this is a Model: + * + * {{{ + * $model = $this; + * $results = Cache::remember('all_articles', function() use ($model) { + * return $model->find('all'); + * }); + * }}} + * + * @param string $key The cache key to read/store data at. + * @param callable $callable The callable that provides data in the case when + * the cache key is empty. Can be any callable type supported by your PHP. + * @param string $config The cache configuration to use for this operation. + * Defaults to default. + */ + public static function remember($key, $callable, $config = 'default') { + $existing = self::read($key, $config); + if ($existing !== false) { + return $existing; + } + $results = call_user_func($callable); + self::write($key, $results, $config); + return $results; + } + } diff --git a/lib/Cake/Cache/Engine/MemcacheEngine.php b/lib/Cake/Cache/Engine/MemcacheEngine.php index 98b91d63b..17221e911 100644 --- a/lib/Cake/Cache/Engine/MemcacheEngine.php +++ b/lib/Cake/Cache/Engine/MemcacheEngine.php @@ -24,7 +24,8 @@ * control you have over expire times far in the future. See MemcacheEngine::write() for * more information. * - * @package Cake.Cache.Engine + * @package Cake.Cache.Engine + * @deprecated You should use the Memcached adapter instead. */ class MemcacheEngine extends CacheEngine { diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php new file mode 100755 index 000000000..78ccda1d8 --- /dev/null +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -0,0 +1,317 @@ + 127.0.0.1. If an + * array MemcacheEngine will use them as a pool. + * - compress = boolean, default => false + * - persistent = string The name of the persistent connection. All configurations using + * the same persistent value will share a single underlying connection. + * - serialize = string, default => php. The serializer engine used to serialize data. + * Available engines are php, igbinary and json. Beside php, the memcached extension + * must be compiled with the appropriate serializer support. + * + * @var array + */ + public $settings = array(); + +/** + * List of available serializer engines + * + * Memcached must be compiled with json and igbinary support to use these engines + * + * @var array + */ + protected $_serializers = array( + 'igbinary' => Memcached::SERIALIZER_IGBINARY, + 'json' => Memcached::SERIALIZER_JSON, + 'php' => Memcached::SERIALIZER_PHP + ); + +/** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return boolean True if the engine has been successfully initialized, false if not + * @throws CacheException when you try use authentication without Memcached compiled with SASL support + */ + public function init($settings = array()) { + if (!class_exists('Memcached')) { + return false; + } + if (!isset($settings['prefix'])) { + $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; + } + $settings += array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1'), + 'compress' => false, + 'persistent' => false, + 'login' => null, + 'password' => null, + 'serialize' => 'php' + ); + parent::init($settings); + + if (!is_array($this->settings['servers'])) { + $this->settings['servers'] = array($this->settings['servers']); + } + + if (isset($this->_Memcached)) { + return true; + } + + $this->_Memcached = new Memcached($this->settings['persistent'] ? (string)$this->settings['persistent'] : null); + $this->_setOptions(); + + if (count($this->_Memcached->getServerList())) { + return true; + } + + $servers = array(); + foreach ($this->settings['servers'] as $server) { + $servers[] = $this->_parseServerString($server); + } + + if (!$this->_Memcached->addServers($servers)) { + return false; + } + + if ($this->settings['login'] !== null && $this->settings['password'] !== null) { + if (!method_exists($this->_Memcached, 'setSaslAuthData')) { + throw new CacheException( + __d('cake_dev', 'Memcached extension is not build with SASL support') + ); + } + $this->_Memcached->setSaslAuthData($this->settings['login'], $this->settings['password']); + } + + return true; + } + +/** + * Settings the memcached instance + * + * @throws CacheException when the Memcached extension is not built with the desired serializer engine + */ + protected function _setOptions() { + $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); + + $serializer = strtolower($this->settings['serialize']); + if (!isset($this->_serializers[$serializer])) { + throw new CacheException( + __d('cake_dev', '%s is not a valid serializer engine for Memcached', $serializer) + ); + } + + if ($serializer !== 'php' && !constant('Memcached::HAVE_' . strtoupper($serializer))) { + throw new CacheException( + __d('cake_dev', 'Memcached extension is not compiled with %s support', $serializer) + ); + } + + $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, $this->_serializers[$serializer]); + + // Check for Amazon ElastiCache instance + if (defined('Memcached::OPT_CLIENT_MODE') && defined('Memcached::DYNAMIC_CLIENT_MODE')) { + $this->_Memcached->setOption(Memcached::OPT_CLIENT_MODE, Memcached::DYNAMIC_CLIENT_MODE); + } + + $this->_Memcached->setOption(Memcached::OPT_COMPRESSION, (bool)$this->settings['compress']); + } + +/** + * Parses the server address into the host/port. Handles both IPv6 and IPv4 + * addresses and Unix sockets + * + * @param string $server The server address string. + * @return array Array containing host, port + */ + protected function _parseServerString($server) { + if ($server[0] === 'u') { + return array($server, 0); + } + if (substr($server, 0, 1) === '[') { + $position = strpos($server, ']:'); + if ($position !== false) { + $position++; + } + } else { + $position = strpos($server, ':'); + } + $port = 11211; + $host = $server; + if ($position !== false) { + $host = substr($server, 0, $position); + $port = substr($server, $position + 1); + } + return array($host, (int)$port); + } + +/** + * Write data for key into cache. When using memcached as your cache engine + * remember that the Memcached pecl extension does not support cache expiry times greater + * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param integer $duration How long to cache the data, in seconds + * @return boolean True if the data was successfully cached, false on failure + * @see http://php.net/manual/en/memcache.set.php + */ + public function write($key, $value, $duration) { + if ($duration > 30 * DAY) { + $duration = 0; + } + + return $this->_Memcached->set($key, $value, $duration); + } + +/** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) { + return $this->_Memcached->get($key); + } + +/** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param integer $offset How much to increment + * @return New incremented value, false otherwise + * @throws CacheException when you try to increment with compress = true + */ + public function increment($key, $offset = 1) { + return $this->_Memcached->increment($key, $offset); + } + +/** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param integer $offset How much to subtract + * @return New decremented value, false otherwise + * @throws CacheException when you try to decrement with compress = true + */ + public function decrement($key, $offset = 1) { + return $this->_Memcached->decrement($key, $offset); + } + +/** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return boolean True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) { + return $this->_Memcached->delete($key); + } + +/** + * Delete all keys from the cache + * + * @param boolean $check + * @return boolean True if the cache was successfully cleared, false otherwise + */ + public function clear($check) { + if ($check) { + return true; + } + + $keys = $this->_Memcached->getAllKeys(); + + foreach ($keys as $key) { + if (strpos($key, $this->settings['prefix']) === 0) { + $this->_Memcached->delete($key); + } + } + + return true; + } + +/** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() { + if (empty($this->_compiledGroupNames)) { + foreach ($this->settings['groups'] as $group) { + $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; + } + } + + $groups = $this->_Memcached->getMulti($this->_compiledGroupNames); + if (count($groups) !== count($this->settings['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + $this->_Memcached->set($group, 1, 0); + $groups[$group] = 1; + } + } + ksort($groups); + } + + $result = array(); + $groups = array_values($groups); + foreach ($this->settings['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } + + return $result; + } + +/** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @return boolean success + */ + public function clearGroup($group) { + return (bool)$this->_Memcached->increment($this->settings['prefix'] . $group); + } +} diff --git a/lib/Cake/Console/Command/CommandListShell.php b/lib/Cake/Console/Command/CommandListShell.php index 6dfe1d699..40cc8fc28 100644 --- a/lib/Cake/Console/Command/CommandListShell.php +++ b/lib/Cake/Console/Command/CommandListShell.php @@ -24,6 +24,13 @@ App::uses('Inflector', 'Utility'); */ class CommandListShell extends AppShell { +/** + * Contains tasks to load and instantiate + * + * @var array + */ + public $tasks = array('Command'); + /** * startup * @@ -55,7 +62,7 @@ class CommandListShell extends AppShell { $this->out(__d('cake_console', "Available Shells:"), 2); } - $shellList = $this->_getShellList(); + $shellList = $this->Command->getShellList(); if (empty($shellList)) { return; } @@ -67,48 +74,6 @@ class CommandListShell extends AppShell { } } -/** - * Gets the shell command listing. - * - * @return array - */ - protected function _getShellList() { - $skipFiles = array('AppShell'); - - $plugins = CakePlugin::loaded(); - $shellList = array_fill_keys($plugins, null) + array('CORE' => null, 'app' => null); - - $corePath = App::core('Console/Command'); - $shells = App::objects('file', $corePath[0]); - $shells = array_diff($shells, $skipFiles); - $this->_appendShells('CORE', $shells, $shellList); - - $appShells = App::objects('Console/Command', null, false); - $appShells = array_diff($appShells, $shells, $skipFiles); - $this->_appendShells('app', $appShells, $shellList); - - foreach ($plugins as $plugin) { - $pluginShells = App::objects($plugin . '.Console/Command'); - $this->_appendShells($plugin, $pluginShells, $shellList); - } - - return array_filter($shellList); - } - -/** - * Scan the provided paths for shells, and append them into $shellList - * - * @param string $type - * @param array $shells - * @param array $shellList - * @return void - */ - protected function _appendShells($type, $shells, &$shellList) { - foreach ($shells as $shell) { - $shellList[$type][] = Inflector::underscore(str_replace('Shell', '', $shell)); - } - } - /** * Output text. * diff --git a/lib/Cake/Console/Command/CompletionShell.php b/lib/Cake/Console/Command/CompletionShell.php new file mode 100644 index 000000000..94e809efa --- /dev/null +++ b/lib/Cake/Console/Command/CompletionShell.php @@ -0,0 +1,155 @@ +out($this->getOptionParser()->help()); + } + +/** + * list commands + * + * @return void + */ + public function commands() { + $options = $this->Command->commands(); + return $this->_output($options); + } + +/** + * list options for the named command + * + * @return void + */ + public function options() { + $commandName = ''; + if (!empty($this->args[0])) { + $commandName = $this->args[0]; + } + $options = $this->Command->options($commandName); + + return $this->_output($options); + } + +/** + * list subcommands for the named command + * + * @return void + */ + public function subCommands() { + if (!$this->args) { + return $this->_output(); + } + + $options = $this->Command->subCommands($this->args[0]); + return $this->_output($options); + } + +/** + * Guess autocomplete from the whole argument string + * + * @return void + */ + public function fuzzy() { + return $this->_output(); + } + +/** + * getOptionParser for _this_ shell + * + * @return ConsoleOptionParser + */ + public function getOptionParser() { + $parser = parent::getOptionParser(); + + $parser->description(__d('cake_console', 'Used by shells like bash to autocomplete command name, options and arguments')) + ->addSubcommand('commands', array( + 'help' => __d('cake_console', 'Output a list of available commands'), + 'parser' => array( + 'description' => __d('cake_console', 'List all availables'), + 'arguments' => array( + ) + ) + ))->addSubcommand('subcommands', array( + 'help' => __d('cake_console', 'Output a list of available subcommands'), + 'parser' => array( + 'description' => __d('cake_console', 'List subcommands for a command'), + 'arguments' => array( + 'command' => array( + 'help' => __d('cake_console', 'The command name'), + 'required' => true, + ) + ) + ) + ))->addSubcommand('options', array( + 'help' => __d('cake_console', 'Output a list of available options'), + 'parser' => array( + 'description' => __d('cake_console', 'List options'), + 'arguments' => array( + 'command' => array( + 'help' => __d('cake_console', 'The command name'), + 'required' => false, + ) + ) + ) + ))->epilog( + array( + __d('cake_console', 'This command is not intended to be called manually'), + ) + ); + return $parser; + } + +/** + * Emit results as a string, space delimited + * + * @param array $options + * @return void + */ + protected function _output($options = array()) { + if ($options) { + return $this->out(implode($options, ' ')); + } + } +} diff --git a/lib/Cake/Console/Command/SchemaShell.php b/lib/Cake/Console/Command/SchemaShell.php index f7adcd5e9..121cba91c 100644 --- a/lib/Cake/Console/Command/SchemaShell.php +++ b/lib/Cake/Console/Command/SchemaShell.php @@ -333,7 +333,10 @@ class SchemaShell extends AppShell { $this->out("\n" . __d('cake_console', 'The following table(s) will be dropped.')); $this->out(array_keys($drop)); - if ($this->in(__d('cake_console', 'Are you sure you want to drop the table(s)?'), array('y', 'n'), 'n') === 'y') { + if ( + !empty($this->params['yes']) || + $this->in(__d('cake_console', 'Are you sure you want to drop the table(s)?'), array('y', 'n'), 'n') === 'y' + ) { $this->out(__d('cake_console', 'Dropping table(s).')); $this->_run($drop, 'drop', $Schema); } @@ -341,7 +344,10 @@ class SchemaShell extends AppShell { $this->out("\n" . __d('cake_console', 'The following table(s) will be created.')); $this->out(array_keys($create)); - if ($this->in(__d('cake_console', 'Are you sure you want to create the table(s)?'), array('y', 'n'), 'y') === 'y') { + if ( + !empty($this->params['yes']) || + $this->in(__d('cake_console', 'Are you sure you want to create the table(s)?'), array('y', 'n'), 'y') === 'y' + ) { $this->out(__d('cake_console', 'Creating table(s).')); $this->_run($create, 'create', $Schema); } @@ -392,7 +398,10 @@ class SchemaShell extends AppShell { $this->out("\n" . __d('cake_console', 'The following statements will run.')); $this->out(array_map('trim', $contents)); - if ($this->in(__d('cake_console', 'Are you sure you want to alter the tables?'), array('y', 'n'), 'n') === 'y') { + if ( + !empty($this->params['yes']) || + $this->in(__d('cake_console', 'Are you sure you want to alter the tables?'), array('y', 'n'), 'n') === 'y' + ) { $this->out(); $this->out(__d('cake_console', 'Updating Database...')); $this->_run($contents, 'update', $Schema); @@ -471,7 +480,9 @@ class SchemaShell extends AppShell { 'default' => 'schema.php' ); $name = array( - 'help' => __d('cake_console', 'Classname to use. If its Plugin.class, both name and plugin options will be set.') + 'help' => __d('cake_console', + 'Classname to use. If its Plugin.class, both name and plugin options will be set.' + ) ); $snapshot = array( 'short' => 's', @@ -482,7 +493,9 @@ class SchemaShell extends AppShell { 'help' => __d('cake_console', 'Specify models as comma separated list.'), ); $dry = array( - 'help' => __d('cake_console', 'Perform a dry run on create and update commands. Queries will be output instead of run.'), + 'help' => __d('cake_console', + 'Perform a dry run on create and update commands. Queries will be output instead of run.' + ), 'boolean' => true ); $force = array( @@ -496,10 +509,17 @@ class SchemaShell extends AppShell { $exclude = array( 'help' => __d('cake_console', 'Tables to exclude as comma separated list.') ); + $yes = array( + 'short' => 'y', + 'help' => __d('cake_console', 'Do not prompt for confirmation. Be careful!'), + 'boolean' => true + ); $parser = parent::getOptionParser(); $parser->description( - __d('cake_console', 'The Schema Shell generates a schema object from the database and updates the database from the schema.') + __d('cake_console', + 'The Schema Shell generates a schema object from the database and updates the database from the schema.' + ) )->addSubcommand('view', array( 'help' => __d('cake_console', 'Read and output the contents of a schema file'), 'parser' => array( @@ -523,7 +543,7 @@ class SchemaShell extends AppShell { ))->addSubcommand('create', array( 'help' => __d('cake_console', 'Drop and create tables based on the schema file.'), 'parser' => array( - 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot'), + 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot', 'yes'), 'args' => array( 'name' => array( 'help' => __d('cake_console', 'Name of schema to use.') @@ -536,7 +556,7 @@ class SchemaShell extends AppShell { ))->addSubcommand('update', array( 'help' => __d('cake_console', 'Alter the tables based on the schema file.'), 'parser' => array( - 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot', 'force'), + 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot', 'force', 'yes'), 'args' => array( 'name' => array( 'help' => __d('cake_console', 'Name of schema to use.') diff --git a/lib/Cake/Console/Command/Task/CommandTask.php b/lib/Cake/Console/Command/Task/CommandTask.php new file mode 100644 index 000000000..607d318d1 --- /dev/null +++ b/lib/Cake/Console/Command/Task/CommandTask.php @@ -0,0 +1,183 @@ + null, 'app' => null); + + $corePath = App::core('Console/Command'); + $shells = App::objects('file', $corePath[0]); + $shells = array_diff($shells, $skipFiles); + $this->_appendShells('CORE', $shells, $shellList); + + $appShells = App::objects('Console/Command', null, false); + $appShells = array_diff($appShells, $shells, $skipFiles); + $this->_appendShells('app', $appShells, $shellList); + + foreach ($plugins as $plugin) { + $pluginShells = App::objects($plugin . '.Console/Command'); + $this->_appendShells($plugin, $pluginShells, $shellList); + } + + return array_filter($shellList); + } + +/** + * Scan the provided paths for shells, and append them into $shellList + * + * @param string $type + * @param array $shells + * @param array $shellList + * @return void + */ + protected function _appendShells($type, $shells, &$shellList) { + foreach ($shells as $shell) { + $shellList[$type][] = Inflector::underscore(str_replace('Shell', '', $shell)); + } + } + +/** + * Return a list of all commands + * + * @return array + */ + public function commands() { + $shellList = $this->getShellList(); + + $options = array(); + foreach ($shellList as $type => $commands) { + $prefix = ''; + if (!in_array(strtolower($type), array('app', 'core'))) { + $prefix = $type . '.'; + } + + foreach ($commands as $shell) { + $options[] = $prefix . $shell; + } + } + + return $options; + } + +/** + * Return a list of subcommands for a given command + * + * @param string $commandName + * @return array + */ + public function subCommands($commandName) { + $Shell = $this->getShell($commandName); + + if (!$Shell) { + return array(); + } + + $taskMap = TaskCollection::normalizeObjectArray((array)$Shell->tasks); + $return = array_keys($taskMap); + $return = array_map('Inflector::underscore', $return); + + $ShellReflection = new ReflectionClass('AppShell'); + $shellMethods = $ShellReflection->getMethods(ReflectionMethod::IS_PUBLIC); + $shellMethodNames = array('main', 'help'); + foreach ($shellMethods as $method) { + $shellMethodNames[] = $method->getName(); + } + + $Reflection = new ReflectionClass($Shell); + $methods = $Reflection->getMethods(ReflectionMethod::IS_PUBLIC); + $methodNames = array(); + foreach ($methods as $method) { + $methodNames[] = $method->getName(); + } + + $return += array_diff($methodNames, $shellMethodNames); + sort($return); + + return $return; + } + +/** + * Get Shell instance for the given command + * + * @param mixed $commandName + * @return mixed + */ + public function getShell($commandName) { + list($pluginDot, $name) = pluginSplit($commandName, true); + + if (in_array(strtolower($pluginDot), array('app.', 'core.'))) { + $commandName = $name; + $pluginDot = ''; + } + + if (!in_array($commandName, $this->commands())) { + return false; + } + + $name = Inflector::camelize($name); + $pluginDot = Inflector::camelize($pluginDot); + $class = $name . 'Shell'; + APP::uses($class, $pluginDot . 'Console/Command'); + + $Shell = new $class(); + $Shell->plugin = trim($pluginDot, '.'); + $Shell->initialize(); + + return $Shell; + } + +/** + * Get Shell instance for the given command + * + * @param mixed $commandName + * @return array + */ + public function options($commandName) { + $Shell = $this->getShell($commandName); + if (!$Shell) { + $parser = new ConsoleOptionParser(); + } else { + $parser = $Shell->getOptionParser(); + } + + $options = array(); + $array = $parser->options(); + foreach ($array as $name => $obj) { + $options[] = "--$name"; + $short = $obj->short(); + if ($short) { + $options[] = "-$short"; + } + } + return $options; + } + +} diff --git a/lib/Cake/Controller/Component/CookieComponent.php b/lib/Cake/Controller/Component/CookieComponent.php index 4259f7e9f..0683de951 100644 --- a/lib/Cake/Controller/Component/CookieComponent.php +++ b/lib/Cake/Controller/Component/CookieComponent.php @@ -132,7 +132,9 @@ class CookieComponent extends Component { * Type of encryption to use. * * Currently two methods are available: cipher and rijndael - * Defaults to Security::cipher(); + * Defaults to Security::cipher(). Cipher is horribly insecure and only + * the default because of backwards compatibility. In new applications you should + * always change this to 'aes' or 'rijndael'. * * @var string */ @@ -364,10 +366,11 @@ class CookieComponent extends Component { public function type($type = 'cipher') { $availableTypes = array( 'cipher', - 'rijndael' + 'rijndael', + 'aes' ); if (!in_array($type, $availableTypes)) { - trigger_error(__d('cake_dev', 'You must use cipher or rijndael for cookie encryption type'), E_USER_WARNING); + trigger_error(__d('cake_dev', 'You must use cipher, rijndael or aes for cookie encryption type'), E_USER_WARNING); $type = 'cipher'; } $this->_type = $type; @@ -455,12 +458,20 @@ class CookieComponent extends Component { if (is_array($value)) { $value = $this->_implode($value); } - - if ($this->_encrypted === true) { - $type = $this->_type; - $value = "Q2FrZQ==." . base64_encode(Security::$type($value, $this->key, 'encrypt')); + if (!$this->_encrypted) { + return $value; } - return $value; + $prefix = "Q2FrZQ==."; + if ($this->_type === 'rijndael') { + $cipher = Security::rijndael($value, $this->key, 'encrypt'); + } + if ($this->_type === 'cipher') { + $cipher = Security::cipher($value, $this->key); + } + if ($this->_type === 'aes') { + $cipher = Security::encrypt($value, $this->key); + } + return $prefix . base64_encode($cipher); } /** @@ -476,27 +487,40 @@ class CookieComponent extends Component { foreach ((array)$values as $name => $value) { if (is_array($value)) { foreach ($value as $key => $val) { - $pos = strpos($val, 'Q2FrZQ==.'); - $decrypted[$name][$key] = $this->_explode($val); - - if ($pos !== false) { - $val = substr($val, 8); - $decrypted[$name][$key] = $this->_explode(Security::$type(base64_decode($val), $this->key, 'decrypt')); - } + $decrypted[$name][$key] = $this->_decode($val); } } else { - $pos = strpos($value, 'Q2FrZQ==.'); - $decrypted[$name] = $this->_explode($value); - - if ($pos !== false) { - $value = substr($value, 8); - $decrypted[$name] = $this->_explode(Security::$type(base64_decode($value), $this->key, 'decrypt')); - } + $decrypted[$name] = $this->_decode($value); } } return $decrypted; } +/** + * Decodes and decrypts a single value. + * + * @param string $value The value to decode & decrypt. + * @return string Decoded value. + */ + protected function _decode($value) { + $prefix = 'Q2FrZQ==.'; + $pos = strpos($value, $prefix); + if ($pos === false) { + return $this->_explode($value); + } + $value = base64_decode(substr($value, strlen($prefix))); + if ($this->_type === 'rijndael') { + $plain = Security::rijndael($value, $this->key, 'decrypt'); + } + if ($this->_type === 'cipher') { + $plain = Security::cipher($value, $this->key); + } + if ($this->_type === 'aes') { + $plain = Security::decrypt($value, $this->key); + } + return $this->_explode($plain); + } + /** * Implode method to keep keys are multidimensional arrays * diff --git a/lib/Cake/Log/CakeLog.php b/lib/Cake/Log/CakeLog.php index 8052ea2cd..15f761618 100644 --- a/lib/Cake/Log/CakeLog.php +++ b/lib/Cake/Log/CakeLog.php @@ -372,18 +372,6 @@ class CakeLog { return false; } -/** - * Configures the automatic/default stream a FileLog. - * - * @return void - */ - protected static function _autoConfig() { - self::$_Collection->load('default', array( - 'engine' => 'File', - 'path' => LOGS, - )); - } - /** * Writes the given message and type to all of the configured log adapters. * Configured adapters are passed both the $type and $message variables. $type @@ -455,11 +443,7 @@ class CakeLog { $logged = true; } } - if (!$logged) { - self::_autoConfig(); - self::stream('default')->write($type, $message); - } - return true; + return $logged; } /** diff --git a/lib/Cake/Model/Behavior/TreeBehavior.php b/lib/Cake/Model/Behavior/TreeBehavior.php index e87abedc3..94c2bb443 100644 --- a/lib/Cake/Model/Behavior/TreeBehavior.php +++ b/lib/Cake/Model/Behavior/TreeBehavior.php @@ -682,7 +682,7 @@ class TreeBehavior extends ModelBehavior { $children = $Model->find('all', $params); $hasChildren = (bool)$children; - if (!is_null($parentId)) { + if ($parentId !== null) { if ($hasChildren) { $Model->updateAll( array($this->settings[$Model->alias]['left'] => $counter), @@ -713,7 +713,7 @@ class TreeBehavior extends ModelBehavior { $children = $Model->find('all', $params); } - if (!is_null($parentId) && $hasChildren) { + if ($parentId !== null && $hasChildren) { $Model->updateAll( array($this->settings[$Model->alias]['right'] => $counter), array($Model->escapeField() => $parentId) diff --git a/lib/Cake/Model/Model.php b/lib/Cake/Model/Model.php index 875f8f5ee..6a0a62a95 100644 --- a/lib/Cake/Model/Model.php +++ b/lib/Cake/Model/Model.php @@ -1918,7 +1918,7 @@ class Model extends Object implements CakeEventListener { } foreach ((array)$data as $row) { - if ((is_string($row) && (strlen($row) == 36 || strlen($row) == 16)) || is_numeric($row)) { + if ((is_string($row) && (strlen($row) === 36 || strlen($row) === 16)) || is_numeric($row)) { $newJoins[] = $row; $values = array($id, $row); diff --git a/lib/Cake/Model/ModelValidator.php b/lib/Cake/Model/ModelValidator.php index bbc54c8da..508383df8 100644 --- a/lib/Cake/Model/ModelValidator.php +++ b/lib/Cake/Model/ModelValidator.php @@ -249,7 +249,15 @@ class ModelValidator implements ArrayAccess, IteratorAggregate, Countable { return $model->validationErrors; } - $fieldList = isset($options['fieldList']) ? $options['fieldList'] : array(); + $fieldList = $model->whitelist; + if (empty($fieldList) && !empty($options['fieldList'])) { + if (!empty($options['fieldList'][$model->alias]) && is_array($options['fieldList'][$model->alias])) { + $fieldList = $options['fieldList'][$model->alias]; + } else { + $fieldList = $options['fieldList']; + } + } + $exists = $model->exists(); $methods = $this->getMethods(); $fields = $this->_validationList($fieldList); @@ -376,32 +384,19 @@ class ModelValidator implements ArrayAccess, IteratorAggregate, Countable { } /** - * Processes the Model's whitelist or passed fieldList and returns the list of fields - * to be validated + * Processes the passed fieldList and returns the list of fields to be validated * * @param array $fieldList list of fields to be used for validation * @return array List of validation rules to be applied */ protected function _validationList($fieldList = array()) { - $model = $this->getModel(); - $whitelist = $model->whitelist; - - if (!empty($fieldList)) { - if (!empty($fieldList[$model->alias]) && is_array($fieldList[$model->alias])) { - $whitelist = $fieldList[$model->alias]; - } else { - $whitelist = $fieldList; - } - } - unset($fieldList); - - if (empty($whitelist) || Hash::dimensions($whitelist) > 1) { + if (empty($fieldList) || Hash::dimensions($fieldList) > 1) { return $this->_fields; } $validateList = array(); $this->validationErrors = array(); - foreach ((array)$whitelist as $f) { + foreach ((array)$fieldList as $f) { if (!empty($this->_fields[$f])) { $validateList[$f] = $this->_fields[$f]; } diff --git a/lib/Cake/Network/CakeRequest.php b/lib/Cake/Network/CakeRequest.php index b325f26f4..398197141 100644 --- a/lib/Cake/Network/CakeRequest.php +++ b/lib/Cake/Network/CakeRequest.php @@ -516,8 +516,13 @@ class CakeRequest implements ArrayAccess { } if (isset($detect['param'])) { $key = $detect['param']; - $value = $detect['value']; - return isset($this->params[$key]) ? $this->params[$key] == $value : false; + if (isset($detect['value'])) { + $value = $detect['value']; + return isset($this->params[$key]) ? $this->params[$key] == $value : false; + } + if (isset($detect['options'])) { + return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options']) : false; + } } if (isset($detect['callback']) && is_callable($detect['callback'])) { return call_user_func($detect['callback'], $this); @@ -576,7 +581,13 @@ class CakeRequest implements ArrayAccess { * * Allows for custom detectors on the request parameters. * - * e.g `addDetector('post', array('param' => 'requested', 'value' => 1)` + * e.g `addDetector('requested', array('param' => 'requested', 'value' => 1)` + * + * You can also make parameter detectors that accept multiple values + * using the `options` key. This is useful when you want to check + * if a request parameter is in a list of options. + * + * `addDetector('extension', array('param' => 'ext', 'options' => array('pdf', 'csv'))` * * @param string $name The name of the detector. * @param array $options The options for the detector definition. See above. diff --git a/lib/Cake/Network/CakeResponse.php b/lib/Cake/Network/CakeResponse.php index f6a0ed2e5..2f60667fe 100644 --- a/lib/Cake/Network/CakeResponse.php +++ b/lib/Cake/Network/CakeResponse.php @@ -570,7 +570,7 @@ class CakeResponse { if (is_numeric($header)) { list($header, $value) = array($value, null); } - if (is_null($value)) { + if ($value === null) { list($header, $value) = explode(':', $header, 2); } $this->_headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value); diff --git a/lib/Cake/Routing/Router.php b/lib/Cake/Routing/Router.php index 26f6f82c9..77ce25803 100644 --- a/lib/Cake/Routing/Router.php +++ b/lib/Cake/Routing/Router.php @@ -500,11 +500,14 @@ class Router { public static function mapResources($controller, $options = array()) { $hasPrefix = isset($options['prefix']); $options = array_merge(array( + 'connectOptions' => array(), 'prefix' => '/', 'id' => self::ID . '|' . self::UUID ), $options); $prefix = $options['prefix']; + $connectOptions = $options['connectOptions']; + unset($options['connectOptions']); foreach ((array)$controller as $name) { list($plugin, $name) = pluginSplit($name); @@ -524,7 +527,10 @@ class Router { 'action' => $params['action'], '[method]' => $params['method'] ), - array('id' => $options['id'], 'pass' => array('id')) + array_merge( + array('id' => $options['id'], 'pass' => array('id')), + $connectOptions + ) ); } self::$_resourceMapped[] = $urlName; @@ -1035,7 +1041,7 @@ class Router { } $addition = http_build_query($q, null, $join); - if ($out && $addition && substr($out, strlen($join) * -1, strlen($join)) != $join) { + if ($out && $addition && substr($out, strlen($join) * -1, strlen($join)) !== $join) { $out .= $join; } diff --git a/lib/Cake/Test/Case/Cache/CacheTest.php b/lib/Cake/Test/Case/Cache/CacheTest.php index 9b4279551..43bc3c5ff 100644 --- a/lib/Cake/Test/Case/Cache/CacheTest.php +++ b/lib/Cake/Test/Case/Cache/CacheTest.php @@ -27,6 +27,8 @@ App::uses('Cache', 'Cache'); */ class CacheTest extends CakeTestCase { + protected $_count = 0; + /** * setUp method * @@ -491,4 +493,28 @@ class CacheTest extends CakeTestCase { $this->assertEquals('test_file_', $settings['prefix']); $this->assertEquals(strtotime('+1 year') - time(), $settings['duration']); } + +/** + * test remember method. + * + * @return void + */ + public function testRemember() { + $expected = 'This is some data 0'; + $result = Cache::remember('test_key', array($this, 'cacher'), 'default'); + $this->assertEquals($expected, $result); + + $this->_count = 1; + $result = Cache::remember('test_key', array($this, 'cacher'), 'default'); + $this->assertEquals($expected, $result); + } + +/** + * Method for testing Cache::remember() + * + * @return string + */ + public function cacher() { + return 'This is some data ' . $this->_count; + } } diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php new file mode 100755 index 000000000..47dfe75ed --- /dev/null +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -0,0 +1,728 @@ + + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://book.cakephp.org/2.0/en/development/testing.html CakePHP(tm) Tests + * @package Cake.Test.Case.Cache.Engine + * @since CakePHP(tm) v 2.5.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +App::uses('Cache', 'Cache'); +App::uses('MemcachedEngine', 'Cache/Engine'); + +/** + * Class TestMemcachedEngine + * + * @package Cake.Test.Case.Cache.Engine + */ +class TestMemcachedEngine extends MemcachedEngine { + +/** + * public accessor to _parseServerString + * + * @param string $server + * @return array + */ + public function parseServerString($server) { + return $this->_parseServerString($server); + } + + public function setMemcached($memcached) { + $this->_Memcached = $memcached; + } + + public function getMemcached() { + return $this->_Memcached; + } + +} + +/** + * MemcachedEngineTest class + * + * @package Cake.Test.Case.Cache.Engine + */ +class MemcachedEngineTest extends CakeTestCase { + +/** + * setUp method + * + * @return void + */ + public function setUp() { + parent::setUp(); + $this->skipIf(!class_exists('Memcached'), 'Memcached is not installed or configured properly.'); + + Cache::config('memcached', array( + 'engine' => 'Memcached', + 'prefix' => 'cake_', + 'duration' => 3600 + )); + } + +/** + * tearDown method + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + Cache::drop('memcached'); + Cache::drop('memcached_groups'); + Cache::drop('memcached_helper'); + Cache::config('default'); + } + +/** + * testSettings method + * + * @return void + */ + public function testSettings() { + $settings = Cache::settings('memcached'); + unset($settings['path']); + $expecting = array( + 'prefix' => 'cake_', + 'duration' => 3600, + 'probability' => 100, + 'servers' => array('127.0.0.1'), + 'persistent' => false, + 'compress' => false, + 'engine' => 'Memcached', + 'login' => null, + 'password' => null, + 'groups' => array(), + 'serialize' => 'php' + ); + $this->assertEquals($expecting, $settings); + } + +/** + * testCompressionSetting method + * + * @return void + */ + public function testCompressionSetting() { + $Memcached = new TestMemcachedEngine(); + $Memcached->init(array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'compress' => false + )); + + $this->assertFalse($Memcached->getMemcached()->getOption(Memcached::OPT_COMPRESSION)); + + $MemcachedCompressed = new TestMemcachedEngine(); + $MemcachedCompressed->init(array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'compress' => true + )); + + $this->assertTrue($MemcachedCompressed->getMemcached()->getOption(Memcached::OPT_COMPRESSION)); + } + +/** + * test accepts only valid serializer engine + * + * @return void + */ + public function testInvalidSerializerSetting() { + $Memcached = new TestMemcachedEngine(); + $settings = array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'persistent' => false, + 'serialize' => 'invalid_serializer' + ); + + $this->setExpectedException( + 'CacheException', 'invalid_serializer is not a valid serializer engine for Memcached' + ); + $Memcached->init($settings); + } + +/** + * testPhpSerializerSetting method + * + * @return void + */ + public function testPhpSerializerSetting() { + $Memcached = new TestMemcachedEngine(); + $settings = array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'persistent' => false, + 'serialize' => 'php' + ); + + $Memcached->init($settings); + $this->assertEquals(Memcached::SERIALIZER_PHP, $Memcached->getMemcached()->getOption(Memcached::OPT_SERIALIZER)); + } + +/** + * testJsonSerializerSetting method + * + * @return void + */ + public function testJsonSerializerSetting() { + $this->skipIf( + !Memcached::HAVE_JSON, + 'Memcached extension is not compiled with json support' + ); + + $Memcached = new TestMemcachedEngine(); + $settings = array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'persistent' => false, + 'serialize' => 'json' + ); + + $Memcached->init($settings); + $this->assertEquals(Memcached::SERIALIZER_JSON, $Memcached->getMemcached()->getOption(Memcached::OPT_SERIALIZER)); + } + +/** + * testIgbinarySerializerSetting method + * + * @return void + */ + public function testIgbinarySerializerSetting() { + $this->skipIf( + !Memcached::HAVE_IGBINARY, + 'Memcached extension is not compiled with igbinary support' + ); + + $Memcached = new TestMemcachedEngine(); + $settings = array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'persistent' => false, + 'serialize' => 'igbinary' + ); + + $Memcached->init($settings); + $this->assertEquals(Memcached::SERIALIZER_IGBINARY, $Memcached->getMemcached()->getOption(Memcached::OPT_SERIALIZER)); + } + +/** + * testJsonSerializerThrowException method + * + * @return void + */ + public function testJsonSerializerThrowException() { + $this->skipIf( + Memcached::HAVE_JSON, + 'Memcached extension is compiled with json support' + ); + + $Memcached = new TestMemcachedEngine(); + $settings = array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'persistent' => false, + 'serialize' => 'json' + ); + + $this->setExpectedException( + 'CacheException', 'Memcached extension is not compiled with json support' + ); + $Memcached->init($settings); + } + +/** + * testIgbinarySerializerThrowException method + * + * @return void + */ + public function testIgbinarySerializerThrowException() { + $this->skipIf( + Memcached::HAVE_IGBINARY, + 'Memcached extension is compiled with igbinary support' + ); + + $Memcached = new TestMemcachedEngine(); + $settings = array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'persistent' => false, + 'serialize' => 'igbinary' + ); + + $this->setExpectedException( + 'CacheException', 'Memcached extension is not compiled with igbinary support' + ); + $Memcached->init($settings); + } + +/** + * test using authentication without memcached installed with SASL support + * throw an exception + * + * @return void + */ + public function testSaslAuthException() { + $Memcached = new TestMemcachedEngine(); + $settings = array( + 'engine' => 'Memcached', + 'servers' => array('127.0.0.1:11211'), + 'persistent' => false, + 'login' => 'test', + 'password' => 'password' + ); + + $this->skipIf( + method_exists($Memcached->getMemcached(), 'setSaslAuthData'), + 'Memcached extension is installed with SASL support' + ); + + $this->setExpectedException( + 'CacheException', 'Memcached extension is not build with SASL support' + ); + $Memcached->init($settings); + } + +/** + * testSettings method + * + * @return void + */ + public function testMultipleServers() { + $servers = array('127.0.0.1:11211', '127.0.0.1:11222'); + $available = true; + $Memcached = new Memcached(); + + foreach ($servers as $server) { + list($host, $port) = explode(':', $server); + //@codingStandardsIgnoreStart + if (!$Memcached->addServer($host, $port)) { + $available = false; + } + //@codingStandardsIgnoreEnd + } + + $this->skipIf(!$available, 'Need memcached servers at ' . implode(', ', $servers) . ' to run this test.'); + + $Memcached = new MemcachedEngine(); + $Memcached->init(array('engine' => 'Memcached', 'servers' => $servers)); + + $settings = $Memcached->settings(); + $this->assertEquals($settings['servers'], $servers); + Cache::drop('dual_server'); + } + +/** + * test connecting to an ipv6 server. + * + * @return void + */ + public function testConnectIpv6() { + $Memcached = new MemcachedEngine(); + $result = $Memcached->init(array( + 'prefix' => 'cake_', + 'duration' => 200, + 'engine' => 'Memcached', + 'servers' => array( + '[::1]:11211' + ) + )); + $this->assertTrue($result); + } + +/** + * test non latin domains. + * + * @return void + */ + public function testParseServerStringNonLatin() { + $Memcached = new TestMemcachedEngine(); + $result = $Memcached->parseServerString('schülervz.net:13211'); + $this->assertEquals(array('schülervz.net', '13211'), $result); + + $result = $Memcached->parseServerString('sülül:1111'); + $this->assertEquals(array('sülül', '1111'), $result); + } + +/** + * test unix sockets. + * + * @return void + */ + public function testParseServerStringUnix() { + $Memcached = new TestMemcachedEngine(); + $result = $Memcached->parseServerString('unix:///path/to/memcachedd.sock'); + $this->assertEquals(array('unix:///path/to/memcachedd.sock', 0), $result); + } + +/** + * testReadAndWriteCache method + * + * @return void + */ + public function testReadAndWriteCache() { + Cache::set(array('duration' => 1), null, 'memcached'); + + $result = Cache::read('test', 'memcached'); + $expecting = ''; + $this->assertEquals($expecting, $result); + + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('test', $data, 'memcached'); + $this->assertTrue($result); + + $result = Cache::read('test', 'memcached'); + $expecting = $data; + $this->assertEquals($expecting, $result); + + Cache::delete('test', 'memcached'); + } + +/** + * testExpiry method + * + * @return void + */ + public function testExpiry() { + Cache::set(array('duration' => 1), 'memcached'); + + $result = Cache::read('test', 'memcached'); + $this->assertFalse($result); + + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('other_test', $data, 'memcached'); + $this->assertTrue($result); + + sleep(2); + $result = Cache::read('other_test', 'memcached'); + $this->assertFalse($result); + + Cache::set(array('duration' => "+1 second"), 'memcached'); + + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('other_test', $data, 'memcached'); + $this->assertTrue($result); + + sleep(3); + $result = Cache::read('other_test', 'memcached'); + $this->assertFalse($result); + + Cache::config('memcached', array('duration' => '+1 second')); + + $result = Cache::read('other_test', 'memcached'); + $this->assertFalse($result); + + Cache::config('memcached', array('duration' => '+29 days')); + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('long_expiry_test', $data, 'memcached'); + $this->assertTrue($result); + + sleep(2); + $result = Cache::read('long_expiry_test', 'memcached'); + $expecting = $data; + $this->assertEquals($expecting, $result); + + Cache::config('memcached', array('duration' => 3600)); + } + +/** + * testDeleteCache method + * + * @return void + */ + public function testDeleteCache() { + $data = 'this is a test of the emergency broadcasting system'; + $result = Cache::write('delete_test', $data, 'memcached'); + $this->assertTrue($result); + + $result = Cache::delete('delete_test', 'memcached'); + $this->assertTrue($result); + } + +/** + * testDecrement method + * + * @return void + */ + public function testDecrement() { + $result = Cache::write('test_decrement', 5, 'memcached'); + $this->assertTrue($result); + + $result = Cache::decrement('test_decrement', 1, 'memcached'); + $this->assertEquals(4, $result); + + $result = Cache::read('test_decrement', 'memcached'); + $this->assertEquals(4, $result); + + $result = Cache::decrement('test_decrement', 2, 'memcached'); + $this->assertEquals(2, $result); + + $result = Cache::read('test_decrement', 'memcached'); + $this->assertEquals(2, $result); + + Cache::delete('test_decrement', 'memcached'); + } + +/** + * test decrementing compressed keys + * + * @return void + */ + public function testDecrementCompressedKeys() { + Cache::config('compressed_memcached', array( + 'engine' => 'Memcached', + 'duration' => '+2 seconds', + 'servers' => array('127.0.0.1:11211'), + 'compress' => true + )); + + $result = Cache::write('test_decrement', 5, 'compressed_memcached'); + $this->assertTrue($result); + + $result = Cache::decrement('test_decrement', 1, 'compressed_memcached'); + $this->assertEquals(4, $result); + + $result = Cache::read('test_decrement', 'compressed_memcached'); + $this->assertEquals(4, $result); + + $result = Cache::decrement('test_decrement', 2, 'compressed_memcached'); + $this->assertEquals(2, $result); + + $result = Cache::read('test_decrement', 'compressed_memcached'); + $this->assertEquals(2, $result); + + Cache::delete('test_decrement', 'compressed_memcached'); + } + +/** + * testIncrement method + * + * @return void + */ + public function testIncrement() { + $result = Cache::write('test_increment', 5, 'memcached'); + $this->assertTrue($result); + + $result = Cache::increment('test_increment', 1, 'memcached'); + $this->assertEquals(6, $result); + + $result = Cache::read('test_increment', 'memcached'); + $this->assertEquals(6, $result); + + $result = Cache::increment('test_increment', 2, 'memcached'); + $this->assertEquals(8, $result); + + $result = Cache::read('test_increment', 'memcached'); + $this->assertEquals(8, $result); + + Cache::delete('test_increment', 'memcached'); + } + +/** + * test incrementing compressed keys + * + * @return void + */ + public function testIncrementCompressedKeys() { + Cache::config('compressed_memcached', array( + 'engine' => 'Memcached', + 'duration' => '+2 seconds', + 'servers' => array('127.0.0.1:11211'), + 'compress' => true + )); + + $result = Cache::write('test_increment', 5, 'compressed_memcached'); + $this->assertTrue($result); + + $result = Cache::increment('test_increment', 1, 'compressed_memcached'); + $this->assertEquals(6, $result); + + $result = Cache::read('test_increment', 'compressed_memcached'); + $this->assertEquals(6, $result); + + $result = Cache::increment('test_increment', 2, 'compressed_memcached'); + $this->assertEquals(8, $result); + + $result = Cache::read('test_increment', 'compressed_memcached'); + $this->assertEquals(8, $result); + + Cache::delete('test_increment', 'compressed_memcached'); + } + +/** + * test that configurations don't conflict, when a file engine is declared after a memcached one. + * + * @return void + */ + public function testConfigurationConflict() { + Cache::config('long_memcached', array( + 'engine' => 'Memcached', + 'duration' => '+2 seconds', + 'servers' => array('127.0.0.1:11211'), + )); + Cache::config('short_memcached', array( + 'engine' => 'Memcached', + 'duration' => '+1 seconds', + 'servers' => array('127.0.0.1:11211'), + )); + Cache::config('some_file', array('engine' => 'File')); + + $this->assertTrue(Cache::write('duration_test', 'yay', 'long_memcached')); + $this->assertTrue(Cache::write('short_duration_test', 'boo', 'short_memcached')); + + $this->assertEquals('yay', Cache::read('duration_test', 'long_memcached'), 'Value was not read %s'); + $this->assertEquals('boo', Cache::read('short_duration_test', 'short_memcached'), 'Value was not read %s'); + + sleep(1); + $this->assertEquals('yay', Cache::read('duration_test', 'long_memcached'), 'Value was not read %s'); + + sleep(2); + $this->assertFalse(Cache::read('short_duration_test', 'short_memcached'), 'Cache was not invalidated %s'); + $this->assertFalse(Cache::read('duration_test', 'long_memcached'), 'Value did not expire %s'); + + Cache::delete('duration_test', 'long_memcached'); + Cache::delete('short_duration_test', 'short_memcached'); + } + +/** + * test clearing memcached. + * + * @return void + */ + public function testClear() { + Cache::config('memcached2', array( + 'engine' => 'Memcached', + 'prefix' => 'cake2_', + 'duration' => 3600 + )); + + Cache::write('some_value', 'cache1', 'memcached'); + $result = Cache::clear(true, 'memcached'); + $this->assertTrue($result); + $this->assertEquals('cache1', Cache::read('some_value', 'memcached')); + + Cache::write('some_value', 'cache2', 'memcached2'); + $result = Cache::clear(false, 'memcached'); + $this->assertTrue($result); + $this->assertFalse(Cache::read('some_value', 'memcached')); + $this->assertEquals('cache2', Cache::read('some_value', 'memcached2')); + + Cache::clear(false, 'memcached2'); + } + +/** + * test that a 0 duration can successfully write. + * + * @return void + */ + public function testZeroDuration() { + Cache::config('memcached', array('duration' => 0)); + $result = Cache::write('test_key', 'written!', 'memcached'); + + $this->assertTrue($result); + $result = Cache::read('test_key', 'memcached'); + $this->assertEquals('written!', $result); + } + +/** + * test that durations greater than 30 days never expire + * + * @return void + */ + public function testLongDurationEqualToZero() { + $memcached = new TestMemcachedEngine(); + $memcached->settings['compress'] = false; + + $mock = $this->getMock('Memcached'); + $memcached->setMemcached($mock); + $mock->expects($this->once()) + ->method('set') + ->with('key', 'value', 0); + + $value = 'value'; + $memcached->write('key', $value, 50 * DAY); + } + +/** + * Tests that configuring groups for stored keys return the correct values when read/written + * Shows that altering the group value is equivalent to deleting all keys under the same + * group + * + * @return void + */ + public function testGroupReadWrite() { + Cache::config('memcached_groups', array( + 'engine' => 'Memcached', + 'duration' => 3600, + 'groups' => array('group_a', 'group_b'), + 'prefix' => 'test_' + )); + Cache::config('memcached_helper', array( + 'engine' => 'Memcached', + 'duration' => 3600, + 'prefix' => 'test_' + )); + $this->assertTrue(Cache::write('test_groups', 'value', 'memcached_groups')); + $this->assertEquals('value', Cache::read('test_groups', 'memcached_groups')); + + Cache::increment('group_a', 1, 'memcached_helper'); + $this->assertFalse(Cache::read('test_groups', 'memcached_groups')); + $this->assertTrue(Cache::write('test_groups', 'value2', 'memcached_groups')); + $this->assertEquals('value2', Cache::read('test_groups', 'memcached_groups')); + + Cache::increment('group_b', 1, 'memcached_helper'); + $this->assertFalse(Cache::read('test_groups', 'memcached_groups')); + $this->assertTrue(Cache::write('test_groups', 'value3', 'memcached_groups')); + $this->assertEquals('value3', Cache::read('test_groups', 'memcached_groups')); + } + +/** + * Tests that deleteing from a groups-enabled config is possible + * + * @return void + */ + public function testGroupDelete() { + Cache::config('memcached_groups', array( + 'engine' => 'Memcached', + 'duration' => 3600, + 'groups' => array('group_a', 'group_b') + )); + $this->assertTrue(Cache::write('test_groups', 'value', 'memcached_groups')); + $this->assertEquals('value', Cache::read('test_groups', 'memcached_groups')); + $this->assertTrue(Cache::delete('test_groups', 'memcached_groups')); + + $this->assertFalse(Cache::read('test_groups', 'memcached_groups')); + } + +/** + * Test clearing a cache group + * + * @return void + */ + public function testGroupClear() { + Cache::config('memcached_groups', array( + 'engine' => 'Memcached', + 'duration' => 3600, + 'groups' => array('group_a', 'group_b') + )); + + $this->assertTrue(Cache::write('test_groups', 'value', 'memcached_groups')); + $this->assertTrue(Cache::clearGroup('group_a', 'memcached_groups')); + $this->assertFalse(Cache::read('test_groups', 'memcached_groups')); + + $this->assertTrue(Cache::write('test_groups', 'value2', 'memcached_groups')); + $this->assertTrue(Cache::clearGroup('group_b', 'memcached_groups')); + $this->assertFalse(Cache::read('test_groups', 'memcached_groups')); + } +} diff --git a/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php b/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php index ea81b3db1..05d145c7f 100644 --- a/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php +++ b/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php @@ -22,6 +22,7 @@ App::uses('CommandListShell', 'Console/Command'); App::uses('ConsoleOutput', 'Console'); App::uses('ConsoleInput', 'Console'); App::uses('Shell', 'Console'); +App::uses('CommandTask', 'Console/Command/Task'); /** * Class TestStringOutput @@ -70,6 +71,12 @@ class CommandListShellTest extends CakeTestCase { array('in', '_stop', 'clear'), array($out, $out, $in) ); + + $this->Shell->Command = $this->getMock( + 'CommandTask', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); } /** @@ -98,7 +105,7 @@ class CommandListShellTest extends CakeTestCase { $expected = "/\[.*TestPluginTwo.*\] example, welcome/"; $this->assertRegExp($expected, $output); - $expected = "/\[.*CORE.*\] acl, api, bake, command_list, console, i18n, schema, server, test, testsuite, upgrade/"; + $expected = "/\[.*CORE.*\] acl, api, bake, command_list, completion, console, i18n, schema, server, test, testsuite, upgrade/"; $this->assertRegExp($expected, $output); $expected = "/\[.*app.*\] sample/"; diff --git a/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php b/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php new file mode 100644 index 000000000..7ef3ef53a --- /dev/null +++ b/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php @@ -0,0 +1,261 @@ +output .= $message; + } + +} + +/** + * Class CompletionShellTest + * + * @package Cake.Test.Case.Console.Command + */ +class CompletionShellTest extends CakeTestCase { + +/** + * setUp method + * + * @return void + */ + public function setUp() { + parent::setUp(); + App::build(array( + 'Plugin' => array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS + ), + 'Console/Command' => array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Console' . DS . 'Command' . DS + ) + ), App::RESET); + CakePlugin::load(array('TestPlugin', 'TestPluginTwo')); + + $out = new TestCompletionStringOutput(); + $in = $this->getMock('ConsoleInput', array(), array(), '', false); + + $this->Shell = $this->getMock( + 'CompletionShell', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); + + $this->Shell->Command = $this->getMock( + 'CommandTask', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); + } + +/** + * tearDown + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + unset($this->Shell); + CakePlugin::unload(); + } + +/** + * test that the startup method supresses the shell header + * + * @return void + */ + public function testStartup() { + $this->Shell->runCommand('main', array()); + $output = $this->Shell->stdout->output; + + $needle = 'Welcome to CakePHP'; + $this->assertTextNotContains($needle, $output); + } + +/** + * test that main displays a warning + * + * @return void + */ + public function testMain() { + $this->Shell->runCommand('main', array()); + $output = $this->Shell->stdout->output; + + $expected = "/This command is not intended to be called manually/"; + $this->assertRegExp($expected, $output); + } + +/** + * test commands method that list all available commands + * + * @return void + */ + public function testCommands() { + $this->Shell->runCommand('commands', array()); + $output = $this->Shell->stdout->output; + + $expected = "TestPlugin.example TestPluginTwo.example TestPluginTwo.welcome acl api bake command_list completion console i18n schema server test testsuite upgrade sample\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that options without argument returns the default options + * + * @return void + */ + public function testOptionsNoArguments() { + $this->Shell->runCommand('options', array()); + $output = $this->Shell->stdout->output; + + $expected = "--help -h --verbose -v --quiet -q\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that options with a nonexisting command returns the default options + * + * @return void + */ + public function testOptionsNonExistingCommand() { + $this->Shell->runCommand('options', array('options', 'foo')); + $output = $this->Shell->stdout->output; + + $expected = "--help -h --verbose -v --quiet -q\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that options with a existing command returns the proper options + * + * @return void + */ + public function testOptions() { + $this->Shell->runCommand('options', array('options', 'bake')); + $output = $this->Shell->stdout->output; + + $expected = "--help -h --verbose -v --quiet -q --connection -c --theme -t\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that subCommands with a existing CORE command returns the proper sub commands + * + * @return void + */ + public function testSubCommandsCorePlugin() { + $this->Shell->runCommand('subCommands', array('subCommands', 'CORE.bake')); + $output = $this->Shell->stdout->output; + + $expected = "controller db_config fixture model plugin project test view\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that subCommands with a existing APP command returns the proper sub commands (in this case none) + * + * @return void + */ + public function testSubCommandsAppPlugin() { + $this->Shell->runCommand('subCommands', array('subCommands', 'app.sample')); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } + +/** + * test that subCommands with a existing plugin command returns the proper sub commands + * + * @return void + */ + public function testSubCommandsPlugin() { + $this->Shell->runCommand('subCommands', array('subCommands', 'TestPluginTwo.welcome')); + $output = $this->Shell->stdout->output; + + $expected = "say_hello\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that subcommands without arguments returns nothing + * + * @return void + */ + public function testSubCommandsNoArguments() { + $this->Shell->runCommand('subCommands', array()); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } + +/** + * test that subcommands with a nonexisting command returns nothing + * + * @return void + */ + public function testSubCommandsNonExistingCommand() { + $this->Shell->runCommand('subCommands', array('subCommands', 'foo')); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } + +/** + * test that subcommands returns the available subcommands for the given command + * + * @return void + */ + public function testSubCommands() { + $this->Shell->runCommand('subCommands', array('subCommands', 'bake')); + $output = $this->Shell->stdout->output; + + $expected = "controller db_config fixture model plugin project test view\n"; + $this->assertEquals($expected, $output); + } + +/** + * test that fuzzy returns nothing + * + * @return void + */ + public function testFuzzy() { + $this->Shell->runCommand('fuzzy', array()); + $output = $this->Shell->stdout->output; + + $expected = ''; + $this->assertEquals($expected, $output); + } +} diff --git a/lib/Cake/Test/Case/Console/Command/SchemaShellTest.php b/lib/Cake/Test/Case/Console/Command/SchemaShellTest.php index 9568b1381..1dadc5658 100644 --- a/lib/Cake/Test/Case/Console/Command/SchemaShellTest.php +++ b/lib/Cake/Test/Case/Console/Command/SchemaShellTest.php @@ -426,6 +426,29 @@ class SchemaShellTest extends CakeTestCase { $this->assertContains('public $aros_acos = array(', $contents); } +/** + * Test schema run create with --yes option + * + * @return void + */ + public function testCreateOptionYes() { + $this->Shell = $this->getMock( + 'SchemaShell', + array('in', 'out', 'hr', 'createFile', 'error', 'err', '_stop', '_run'), + array(&$this->Dispatcher) + ); + + $this->Shell->params = array( + 'connection' => 'test', + 'yes' => true, + ); + $this->Shell->args = array('i18n'); + $this->Shell->expects($this->never())->method('in'); + $this->Shell->expects($this->exactly(2))->method('_run'); + $this->Shell->startup(); + $this->Shell->create(); + } + /** * Test schema run create with no table args. * @@ -536,6 +559,33 @@ class SchemaShellTest extends CakeTestCase { $this->Shell->update(); } +/** + * test run update with --yes option + * + * @return void + */ + public function testUpdateWithOptionYes() { + $this->Shell = $this->getMock( + 'SchemaShell', + array('in', 'out', 'hr', 'createFile', 'error', 'err', '_stop', '_run'), + array(&$this->Dispatcher) + ); + + $this->Shell->params = array( + 'connection' => 'test', + 'force' => true, + 'yes' => true, + ); + $this->Shell->args = array('SchemaShellTest', 'articles'); + $this->Shell->startup(); + $this->Shell->expects($this->never())->method('in'); + $this->Shell->expects($this->once()) + ->method('_run') + ->with($this->arrayHasKey('articles'), 'update', $this->isInstanceOf('CakeSchema')); + + $this->Shell->update(); + } + /** * test that the plugin param creates the correct path in the schema object. * diff --git a/lib/Cake/Test/Case/Console/Command/Task/CommandTaskTest.php b/lib/Cake/Test/Case/Console/Command/Task/CommandTaskTest.php new file mode 100644 index 000000000..6a1293259 --- /dev/null +++ b/lib/Cake/Test/Case/Console/Command/Task/CommandTaskTest.php @@ -0,0 +1,240 @@ + array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS + ), + 'Console/Command' => array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Console' . DS . 'Command' . DS + ) + ), App::RESET); + CakePlugin::load(array('TestPlugin', 'TestPluginTwo')); + + $out = $this->getMock('ConsoleOutput', array(), array(), '', false); + $in = $this->getMock('ConsoleInput', array(), array(), '', false); + + $this->CommandTask = $this->getMock( + 'CommandTask', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); + } + +/** + * tearDown + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + unset($this->CommandTask); + CakePlugin::unload(); + } + +/** + * Test the resulting list of shells + * + * @return void + */ + public function testGetShellList() { + $result = $this->CommandTask->getShellList(); + + $expected = array( + 'CORE' => array( + 'acl', + 'api', + 'bake', + 'command_list', + 'completion', + 'console', + 'i18n', + 'schema', + 'server', + 'test', + 'testsuite', + 'upgrade' + ), + 'TestPlugin' => array( + 'example' + ), + 'TestPluginTwo' => array( + 'example', + 'welcome' + ), + 'app' => array( + 'sample' + ), + ); + $this->assertEquals($expected, $result); + } + +/** + * Test the resulting list of commands + * + * @return void + */ + public function testCommands() { + $result = $this->CommandTask->commands(); + + $expected = array( + 'TestPlugin.example', + 'TestPluginTwo.example', + 'TestPluginTwo.welcome', + 'acl', + 'api', + 'bake', + 'command_list', + 'completion', + 'console', + 'i18n', + 'schema', + 'server', + 'test', + 'testsuite', + 'upgrade', + 'sample' + ); + $this->assertEquals($expected, $result); + } + +/** + * Test the resulting list of subcommands for the given command + * + * @return void + */ + public function testSubCommands() { + $result = $this->CommandTask->subCommands('acl'); + + $expected = array( + 'check', + 'create', + 'db_config', + 'delete', + 'deny', + 'getPath', + 'grant', + 'inherit', + 'initdb', + 'nodeExists', + 'parseIdentifier', + 'setParent', + 'view' + ); + $this->assertEquals($expected, $result); + } + +/** + * Test that unknown commands return an empty array + * + * @return void + */ + public function testSubCommandsUnknownCommand() { + $result = $this->CommandTask->subCommands('yoghurt'); + + $expected = array(); + $this->assertEquals($expected, $result); + } + +/** + * Test that getting a existing shell returns the shell instance + * + * @return void + */ + public function testGetShell() { + $result = $this->CommandTask->getShell('acl'); + $this->assertInstanceOf('AclShell', $result); + } + +/** + * Test that getting a non-existing shell returns false + * + * @return void + */ + public function testGetShellNonExisting() { + $result = $this->CommandTask->getShell('strawberry'); + $this->assertFalse($result); + } + +/** + * Test that getting a existing core shell with 'core.' prefix returns the correct shell instance + * + * @return void + */ + public function testGetShellCore() { + $result = $this->CommandTask->getShell('core.bake'); + $this->assertInstanceOf('BakeShell', $result); + } + +/** + * Test the options array for a known command + * + * @return void + */ + public function testOptions() { + $result = $this->CommandTask->options('bake'); + + $expected = array( + '--help', + '-h', + '--verbose', + '-v', + '--quiet', + '-q', + '--connection', + '-c', + '--theme', + '-t' + ); + $this->assertEquals($expected, $result); + } + +/** + * Test the options array for an unknown command + * + * @return void + */ + public function testOptionsUnknownCommand() { + $result = $this->CommandTask->options('pie'); + + $expected = array( + '--help', + '-h', + '--verbose', + '-v', + '--quiet', + '-q' + ); + $this->assertEquals($expected, $result); + } + +} diff --git a/lib/Cake/Test/Case/Controller/ControllerTest.php b/lib/Cake/Test/Case/Controller/ControllerTest.php index 071a1f85e..8b52f417e 100644 --- a/lib/Cake/Test/Case/Controller/ControllerTest.php +++ b/lib/Cake/Test/Case/Controller/ControllerTest.php @@ -605,7 +605,6 @@ class ControllerTest extends CakeTestCase { $Controller->set('title', 'someTitle'); $this->assertSame($Controller->viewVars['title'], 'someTitle'); - $this->assertTrue(empty($Controller->pageTitle)); $Controller->viewVars = array(); $expected = array('ModelName' => 'name', 'ModelName2' => 'name2'); diff --git a/lib/Cake/Test/Case/Log/CakeLogTest.php b/lib/Cake/Test/Case/Log/CakeLogTest.php index 20a5c9132..b437cf04e 100644 --- a/lib/Cake/Test/Case/Log/CakeLogTest.php +++ b/lib/Cake/Test/Case/Log/CakeLogTest.php @@ -126,27 +126,20 @@ class CakeLogTest extends CakeTestCase { } /** - * Test that CakeLog autoconfigures itself to use a FileLogger with the LOGS dir. - * When no streams are there. + * Test that CakeLog does not auto create logs when no streams are there to listen. * * @return void */ - public function testAutoConfig() { + public function testNoStreamListenting() { if (file_exists(LOGS . 'error.log')) { unlink(LOGS . 'error.log'); } - CakeLog::write(LOG_WARNING, 'Test warning'); - $this->assertTrue(file_exists(LOGS . 'error.log')); + $res = CakeLog::write(LOG_WARNING, 'Test warning'); + $this->assertFalse($res); + $this->assertFalse(file_exists(LOGS . 'error.log')); $result = CakeLog::configured(); - $this->assertEquals(array('default'), $result); - - $testMessage = 'custom message'; - CakeLog::write('custom', $testMessage); - $content = file_get_contents(LOGS . 'custom.log'); - $this->assertContains($testMessage, $content); - unlink(LOGS . 'error.log'); - unlink(LOGS . 'custom.log'); + $this->assertEquals(array(), $result); } /** @@ -197,6 +190,10 @@ class CakeLogTest extends CakeTestCase { * @return void */ public function testLogFileWriting() { + CakeLog::config('file', array( + 'engine' => 'File', + 'path' => LOGS + )); if (file_exists(LOGS . 'error.log')) { unlink(LOGS . 'error.log'); } @@ -503,6 +500,11 @@ class CakeLogTest extends CakeTestCase { $this->_resetLogConfig(); $this->_deleteLogs(); + CakeLog::config('file', array( + 'engine' => 'File', + 'path' => LOGS + )); + CakeLog::write('bogus', 'bogus message'); $this->assertTrue(file_exists(LOGS . 'bogus.log')); $this->assertFalse(file_exists(LOGS . 'error.log')); diff --git a/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php b/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php index c6dc35e6c..5b594c474 100644 --- a/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php @@ -443,10 +443,10 @@ class CakeSessionTest extends CakeTestCase { public function testKeyExploit() { $key = "a'] = 1; phpinfo(); \$_SESSION['a"; $result = TestCakeSession::write($key, 'haxored'); - $this->assertTrue($result); + $this->assertFalse($result); $result = TestCakeSession::read($key); - $this->assertEquals('haxored', $result); + $this->assertNull($result); } /** diff --git a/lib/Cake/Test/Case/Model/ModelValidationTest.php b/lib/Cake/Test/Case/Model/ModelValidationTest.php index 6a042b3a2..e95f4b586 100644 --- a/lib/Cake/Test/Case/Model/ModelValidationTest.php +++ b/lib/Cake/Test/Case/Model/ModelValidationTest.php @@ -612,6 +612,34 @@ class ModelValidationTest extends BaseModelTest { $this->assertEquals(0, $joinRecords, 'Records were saved on the join table. %s'); } +/** + * Test that if a behavior modifies the model's whitelist validation gets triggered + * properly for those fields. + * + * @return void + */ + public function testValidateWithFieldListAndBehavior() { + $TestModel = new ValidationTest1(); + $TestModel->validate = array( + 'title' => array( + 'rule' => 'notEmpty', + ), + 'name' => array( + 'rule' => 'notEmpty', + )); + $TestModel->Behaviors->attach('ValidationRule', array('fields' => array('name'))); + + $data = array( + 'title' => '', + 'name' => '', + ); + $result = $TestModel->save($data, array('fieldList' => array('title'))); + $this->assertFalse($result); + + $expected = array('title' => array('This field cannot be left blank'), 'name' => array('This field cannot be left blank')); + $this->assertEquals($expected, $TestModel->validationErrors); + } + /** * test that saveAll and with models with validation interact well * @@ -2380,3 +2408,21 @@ class ModelValidationTest extends BaseModelTest { } } + +/** + * Behavior for testing validation rules. + */ +class ValidationRuleBehavior extends ModelBehavior { + + public function setup(Model $Model, $config = array()) { + $this->settings[$Model->alias] = $config; + } + + public function beforeValidate(Model $Model, $options = array()) { + $fields = $this->settings[$Model->alias]['fields']; + foreach ($fields as $field) { + $Model->whitelist[] = $field; + } + } + +} diff --git a/lib/Cake/Test/Case/Network/CakeRequestTest.php b/lib/Cake/Test/Case/Network/CakeRequestTest.php index ab6e18991..27e28458a 100644 --- a/lib/Cake/Test/Case/Network/CakeRequestTest.php +++ b/lib/Cake/Test/Case/Network/CakeRequestTest.php @@ -1046,6 +1046,13 @@ class CakeRequestTest extends CakeTestCase { $request->return = false; $this->assertFalse($request->isCallMe()); + + $request->addDetector('extension', array('param' => 'ext', 'options' => array('pdf', 'png', 'txt'))); + $request->params['ext'] = 'pdf'; + $this->assertTrue($request->is('extension')); + + $request->params['ext'] = 'exe'; + $this->assertFalse($request->isExtension()); } /** diff --git a/lib/Cake/Test/Case/Routing/RouterTest.php b/lib/Cake/Test/Case/Routing/RouterTest.php index 23896fde4..96d827ae1 100644 --- a/lib/Cake/Test/Case/Routing/RouterTest.php +++ b/lib/Cake/Test/Case/Routing/RouterTest.php @@ -191,6 +191,28 @@ class RouterTest extends CakeTestCase { $this->assertEquals($expected, $result); } +/** + * testMapResources with custom connectOptions + */ + public function testMapResourcesConnectOptions() { + App::build(array( + 'Plugin' => array( + CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS + ) + )); + CakePlugin::load('TestPlugin'); + App::uses('TestRoute', 'TestPlugin.Routing/Route'); + $resources = Router::mapResources('Posts', array( + 'connectOptions' => array( + 'routeClass' => 'TestPlugin.TestRoute', + 'foo' => '^(bar)$', + ), + )); + $route = end(Router::$routes); + $this->assertInstanceOf('TestRoute', $route); + $this->assertEquals('^(bar)$', $route->options['foo']); + } + /** * Test mapResources with a plugin and prefix. * diff --git a/lib/Cake/Test/Case/Utility/FolderTest.php b/lib/Cake/Test/Case/Utility/FolderTest.php index 885ef0802..92fc5abdc 100644 --- a/lib/Cake/Test/Case/Utility/FolderTest.php +++ b/lib/Cake/Test/Case/Utility/FolderTest.php @@ -346,11 +346,24 @@ class FolderTest extends CakeTestCase { * @return void */ public function testAddPathElement() { + $expected = DS . 'some' . DS . 'dir' . DS . 'another_path'; + $result = Folder::addPathElement(DS . 'some' . DS . 'dir', 'another_path'); - $this->assertEquals(DS . 'some' . DS . 'dir' . DS . 'another_path', $result); + $this->assertEquals($expected, $result); $result = Folder::addPathElement(DS . 'some' . DS . 'dir' . DS, 'another_path'); - $this->assertEquals(DS . 'some' . DS . 'dir' . DS . 'another_path', $result); + $this->assertEquals($expected, $result); + + $result = Folder::addPathElement(DS . 'some' . DS . 'dir', array('another_path')); + $this->assertEquals($expected, $result); + + $result = Folder::addPathElement(DS . 'some' . DS . 'dir' . DS, array('another_path')); + $this->assertEquals($expected, $result); + + $expected = DS . 'some' . DS . 'dir' . DS . 'another_path' . DS . 'and' . DS . 'another'; + + $result = Folder::addPathElement(DS . 'some' . DS . 'dir', array('another_path', 'and', 'another')); + $this->assertEquals($expected, $result); } /** diff --git a/lib/Cake/Test/Case/Utility/HashTest.php b/lib/Cake/Test/Case/Utility/HashTest.php index ebfa9c520..c14efc8c5 100644 --- a/lib/Cake/Test/Case/Utility/HashTest.php +++ b/lib/Cake/Test/Case/Utility/HashTest.php @@ -1303,6 +1303,23 @@ class HashTest extends CakeTestCase { $result = Hash::insert($data, '{n}.Comment.{n}.insert', 'value'); $this->assertEquals('value', $result[0]['Comment'][0]['insert']); $this->assertEquals('value', $result[0]['Comment'][1]['insert']); + + $data = array( + 0 => array('Item' => array('id' => 1, 'title' => 'first')), + 1 => array('Item' => array('id' => 2, 'title' => 'second')), + 2 => array('Item' => array('id' => 3, 'title' => 'third')), + 3 => array('Item' => array('id' => 4, 'title' => 'fourth')), + 4 => array('Item' => array('id' => 5, 'title' => 'fifth')), + ); + $result = Hash::insert($data, '{n}.Item[id=/\b2|\b4/]', array('test' => 2)); + $expected = array( + 0 => array('Item' => array('id' => 1, 'title' => 'first')), + 1 => array('Item' => array('id' => 2, 'title' => 'second', 'test' => 2)), + 2 => array('Item' => array('id' => 3, 'title' => 'third')), + 3 => array('Item' => array('id' => 4, 'title' => 'fourth', 'test' => 2)), + 4 => array('Item' => array('id' => 5, 'title' => 'fifth')), + ); + $this->assertEquals($expected, $result); } /** @@ -1366,6 +1383,23 @@ class HashTest extends CakeTestCase { $result = Hash::remove($a, 'pages.2.vars'); $expected = $a; $this->assertEquals($expected, $result); + + $a = array( + 0 => array( + 'name' => 'pages' + ), + 1 => array( + 'name' => 'files' + ) + ); + + $result = Hash::remove($a, '{n}[name=files]'); + $expected = array( + 0 => array( + 'name' => 'pages' + ) + ); + $this->assertEquals($expected, $result); } /** @@ -1385,6 +1419,22 @@ class HashTest extends CakeTestCase { $this->assertFalse(isset($result[0]['Article']['user_id'])); $this->assertFalse(isset($result[0]['Article']['title'])); $this->assertFalse(isset($result[0]['Article']['body'])); + + $data = array( + 0 => array('Item' => array('id' => 1, 'title' => 'first')), + 1 => array('Item' => array('id' => 2, 'title' => 'second')), + 2 => array('Item' => array('id' => 3, 'title' => 'third')), + 3 => array('Item' => array('id' => 4, 'title' => 'fourth')), + 4 => array('Item' => array('id' => 5, 'title' => 'fifth')), + ); + + $result = Hash::remove($data, '{n}.Item[id=/\b2|\b4/]'); + $expected = array( + 0 => array('Item' => array('id' => 1, 'title' => 'first')), + 2 => array('Item' => array('id' => 3, 'title' => 'third')), + 4 => array('Item' => array('id' => 5, 'title' => 'fifth')), + ); + $this->assertEquals($result, $expected); } /** diff --git a/lib/Cake/Test/Case/Utility/SecurityTest.php b/lib/Cake/Test/Case/Utility/SecurityTest.php index f1a0e33f8..3fe83b27c 100644 --- a/lib/Cake/Test/Case/Utility/SecurityTest.php +++ b/lib/Cake/Test/Case/Utility/SecurityTest.php @@ -302,4 +302,115 @@ class SecurityTest extends CakeTestCase { Security::rijndael($txt, $key, 'encrypt'); } +/** + * Test encrypt/decrypt. + * + * @return void + */ + public function testEncryptDecrypt() { + $txt = 'The quick brown fox'; + $key = 'This key is longer than 32 bytes long.'; + $result = Security::encrypt($txt, $key); + $this->assertNotEquals($txt, $result, 'Should be encrypted.'); + $this->assertNotEquals($result, Security::encrypt($txt, $key), 'Each result is unique.'); + $this->assertEquals($txt, Security::decrypt($result, $key)); + } + +/** + * Test that changing the key causes decryption to fail. + * + * @return void + */ + public function testDecryptKeyFailure() { + $txt = 'The quick brown fox'; + $key = 'This key is longer than 32 bytes long.'; + $result = Security::encrypt($txt, $key); + + $key = 'Not the same key. This one will fail'; + $this->assertFalse(Security::decrypt($txt, $key), 'Modified key will fail.'); + } + +/** + * Test that decrypt fails when there is an hmac error. + * + * @return void + */ + public function testDecryptHmacFailure() { + $txt = 'The quick brown fox'; + $key = 'This key is quite long and works well.'; + $salt = 'this is a delicious salt!'; + $result = Security::encrypt($txt, $key, $salt); + + // Change one of the bytes in the hmac. + $result[10] = 'x'; + $this->assertFalse(Security::decrypt($result, $key, $salt), 'Modified hmac causes failure.'); + } + +/** + * Test that changing the hmac salt will cause failures. + * + * @return void + */ + public function testDecryptHmacSaltFailure() { + $txt = 'The quick brown fox'; + $key = 'This key is quite long and works well.'; + $salt = 'this is a delicious salt!'; + $result = Security::encrypt($txt, $key, $salt); + + $salt = 'humpty dumpty had a great fall.'; + $this->assertFalse(Security::decrypt($result, $key, $salt), 'Modified salt causes failure.'); + } + +/** + * Test that short keys cause errors + * + * @expectedException CakeException + * @expectedExceptionMessage Invalid key for encrypt(), key must be at least 256 bits (32 bytes) long. + * @return void + */ + public function testEncryptInvalidKey() { + $txt = 'The quick brown fox jumped over the lazy dog.'; + $key = 'this is too short'; + Security::encrypt($txt, $key); + } + +/** + * Test that empty data cause errors + * + * @expectedException CakeException + * @expectedExceptionMessage The data to encrypt cannot be empty. + * @return void + */ + public function testEncryptInvalidData() { + $txt = ''; + $key = 'This is a key that is long enough to be ok.'; + Security::encrypt($txt, $key); + } + +/** + * Test that short keys cause errors + * + * @expectedException CakeException + * @expectedExceptionMessage Invalid key for decrypt(), key must be at least 256 bits (32 bytes) long. + * @return void + */ + public function testDecryptInvalidKey() { + $txt = 'The quick brown fox jumped over the lazy dog.'; + $key = 'this is too short'; + Security::decrypt($txt, $key); + } + +/** + * Test that empty data cause errors + * + * @expectedException CakeException + * @expectedExceptionMessage The data to decrypt cannot be empty. + * @return void + */ + public function testDecryptInvalidData() { + $txt = ''; + $key = 'This is a key that is long enough to be ok.'; + Security::decrypt($txt, $key); + } + } diff --git a/lib/Cake/Test/Case/Utility/ValidationTest.php b/lib/Cake/Test/Case/Utility/ValidationTest.php index 7838e713c..e1147645d 100644 --- a/lib/Cake/Test/Case/Utility/ValidationTest.php +++ b/lib/Cake/Test/Case/Utility/ValidationTest.php @@ -1946,8 +1946,15 @@ class ValidationTest extends CakeTestCase { $this->assertFalse(Validation::inList('three', array('one', 'two'))); $this->assertFalse(Validation::inList('1one', array(0, 1, 2, 3))); $this->assertFalse(Validation::inList('one', array(0, 1, 2, 3))); - $this->assertFalse(Validation::inList('2', array(1, 2, 3))); - $this->assertTrue(Validation::inList('2', array(1, 2, 3), false)); + $this->assertTrue(Validation::inList('2', array(1, 2, 3))); + $this->assertFalse(Validation::inList('2x', array(1, 2, 3))); + $this->assertFalse(Validation::inList(2, array('1', '2x', '3'))); + $this->assertFalse(Validation::inList('One', array('one', 'two'))); + + // case insensitive + $this->assertTrue(Validation::inList('one', array('One', 'Two'), true)); + $this->assertTrue(Validation::inList('Two', array('one', 'two'), true)); + $this->assertFalse(Validation::inList('three', array('one', 'two'), true)); } /** @@ -2066,14 +2073,24 @@ class ValidationTest extends CakeTestCase { $this->assertFalse(Validation::multiple(array('foo', 'bar', 'baz', 'squirrel'), array('min' => 10))); $this->assertTrue(Validation::multiple(array(0, 5, 9), array('in' => range(0, 10), 'max' => 5))); - $this->assertFalse(Validation::multiple(array('0', '5', '9'), array('in' => range(0, 10), 'max' => 5))); - $this->assertTrue(Validation::multiple(array('0', '5', '9'), array('in' => range(0, 10), 'max' => 5), false)); + $this->assertTrue(Validation::multiple(array('0', '5', '9'), array('in' => range(0, 10), 'max' => 5))); + $this->assertFalse(Validation::multiple(array(0, 5, 9, 8, 6, 2, 1), array('in' => range(0, 10), 'max' => 5))); $this->assertFalse(Validation::multiple(array(0, 5, 9, 8, 11), array('in' => range(0, 10), 'max' => 5))); $this->assertFalse(Validation::multiple(array(0, 5, 9), array('in' => range(0, 10), 'max' => 5, 'min' => 3))); $this->assertFalse(Validation::multiple(array(0, 5, 9, 8, 6, 2, 1), array('in' => range(0, 10), 'max' => 5, 'min' => 2))); $this->assertFalse(Validation::multiple(array(0, 5, 9, 8, 11), array('in' => range(0, 10), 'max' => 5, 'min' => 2))); + + $this->assertFalse(Validation::multiple(array('2x', '3x'), array('in' => array(1, 2, 3, 4, 5)))); + $this->assertFalse(Validation::multiple(array(2, 3), array('in' => array('1x', '2x', '3x', '4x')))); + $this->assertFalse(Validation::multiple(array('one'), array('in' => array('One', 'Two')))); + $this->assertFalse(Validation::multiple(array('Two'), array('in' => array('one', 'two')))); + + // case insensitive + $this->assertTrue(Validation::multiple(array('one'), array('in' => array('One', 'Two')), true)); + $this->assertTrue(Validation::multiple(array('Two'), array('in' => array('one', 'two')), true)); + $this->assertFalse(Validation::multiple(array('three'), array('in' => array('one', 'two')), true)); } /** @@ -2334,6 +2351,7 @@ class ValidationTest extends CakeTestCase { $this->assertTrue(Validation::mimeType($image, array('image/gif'))); $this->assertTrue(Validation::mimeType(array('tmp_name' => $image), array('image/gif'))); + $this->assertFalse(Validation::mimeType($image, array('image/GIF'))); $this->assertFalse(Validation::mimeType($image, array('image/png'))); $this->assertFalse(Validation::mimeType(array('tmp_name' => $image), array('image/png'))); } diff --git a/lib/Cake/Test/Case/View/Helper/RssHelperTest.php b/lib/Cake/Test/Case/View/Helper/RssHelperTest.php index 4f1e142a5..0abd24499 100644 --- a/lib/Cake/Test/Case/View/Helper/RssHelperTest.php +++ b/lib/Cake/Test/Case/View/Helper/RssHelperTest.php @@ -93,7 +93,7 @@ class RssHelperTest extends CakeTestCase { */ public function testChannel() { $attrib = array('a' => '1', 'b' => '2'); - $elements = array('title' => 'title'); + $elements = array('title' => 'Title'); $content = 'content'; $result = $this->Rss->channel($attrib, $elements, $content); @@ -103,30 +103,7 @@ class RssHelperTest extends CakeTestCase { 'b' => '2' ), 'Rss->url('/', true), - '/link', - 'assertTags($result, $expected); - - $this->View->pageTitle = 'title'; - $attrib = array('a' => '1', 'b' => '2'); - $elements = array(); - $content = 'content'; - - $result = $this->Rss->channel($attrib, $elements, $content); - $expected = array( - 'channel' => array( - 'a' => '1', - 'b' => '2' - ), - 'Rss->url('/', true), diff --git a/lib/Cake/Test/Case/View/ViewTest.php b/lib/Cake/Test/Case/View/ViewTest.php index 337591e7e..a7189cf9b 100644 --- a/lib/Cake/Test/Case/View/ViewTest.php +++ b/lib/Cake/Test/Case/View/ViewTest.php @@ -1610,19 +1610,6 @@ TEXT; $this->assertEquals($expected, $result); } -/** - * Test that setting arbitrary properties still works. - * - * @return void - */ - public function testPropertySetting() { - $this->assertFalse(isset($this->View->pageTitle)); - $this->View->pageTitle = 'test'; - $this->assertTrue(isset($this->View->pageTitle)); - $this->assertTrue(!empty($this->View->pageTitle)); - $this->assertEquals('test', $this->View->pageTitle); - } - /** * Test that setting arbitrary properties still works. * @@ -1660,7 +1647,7 @@ TEXT; } /** - * Tests that a vew block uses default value when not assigned and uses assigned value when it is + * Tests that a view block uses default value when not assigned and uses assigned value when it is * * @return void */ @@ -1674,4 +1661,20 @@ TEXT; $result = $this->View->fetch('title', $default); $this->assertEquals($expected, $result); } + +/** + * Tests that a view variable uses default value when not assigned and uses assigned value when it is + * + * @return void + */ + public function testViewVarDefaultValue() { + $default = 'Default'; + $result = $this->View->get('title', $default); + $this->assertEquals($default, $result); + + $expected = 'Back to the Future'; + $this->View->set('title', $expected); + $result = $this->View->get('title', $default); + $this->assertEquals($expected, $result); + } } diff --git a/lib/Cake/TestSuite/Coverage/BaseCoverageReport.php b/lib/Cake/TestSuite/Coverage/BaseCoverageReport.php index d737768c6..dd63831c6 100644 --- a/lib/Cake/TestSuite/Coverage/BaseCoverageReport.php +++ b/lib/Cake/TestSuite/Coverage/BaseCoverageReport.php @@ -96,7 +96,7 @@ abstract class BaseCoverageReport { /** * Gets the base path that the files we are interested in live in. * - * @return void + * @return string Path */ public function getPathFilter() { $path = ROOT . DS; diff --git a/lib/Cake/TestSuite/Coverage/HtmlCoverageReport.php b/lib/Cake/TestSuite/Coverage/HtmlCoverageReport.php index 124eecd16..adee524aa 100644 --- a/lib/Cake/TestSuite/Coverage/HtmlCoverageReport.php +++ b/lib/Cake/TestSuite/Coverage/HtmlCoverageReport.php @@ -1,7 +1,5 @@ getPathFilter(); @@ -48,6 +60,12 @@ HTML; $fileData = file($file); $output .= $this->generateDiff($file, $fileData, $coverageData); } + + $percentCovered = 100; + if ($this->_total > 0) { + $percentCovered = round(100 * $this->_covered / $this->_total, 2); + } + $output .= '
Overall coverage: ' . $percentCovered . '%
'; return $output; } @@ -69,6 +87,8 @@ HTML; $diff = array(); list($covered, $total) = $this->_calculateCoveredLines($fileLines, $coverageData); + $this->_covered += $covered; + $this->_total += $total; //shift line numbers forward one; array_unshift($fileLines, ' '); @@ -121,13 +141,13 @@ HTML; } /** - * Renders the html for a single line in the html diff. + * Renders the HTML for a single line in the HTML diff. * * @param string $line * @param integer $linenumber * @param string $class * @param array $coveringTests - * @return void + * @return string */ protected function _paintLine($line, $linenumber, $class, $coveringTests) { $coveredBy = ''; @@ -150,7 +170,7 @@ HTML; /** * generate some javascript for the coverage report. * - * @return void + * @return string */ public function coverageScript() { return << $v) { @@ -141,6 +136,22 @@ class Hash { } return $context[$_key]; } +/** + * Split token conditions + * + * @param string $token the token being splitted. + * @return array array(token, conditions) with token splitted + */ + protected static function _splitConditions($token) { + $conditions = false; + $position = strpos($token, '['); + if ($position !== false) { + $conditions = substr($token, $position); + $token = substr($token, 0, $position); + } + + return array($token, $conditions); + } /** * Check a key against a token. @@ -225,16 +236,30 @@ class Hash { * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::insert */ public static function insert(array $data, $path, $values = null) { - $tokens = explode('.', $path); - if (strpos($path, '{') === false) { + if (strpos($path, '[') === false) { + $tokens = explode('.', $path); + } else { + $tokens = String::tokenize($path, '.', '[', ']'); + } + + if (strpos($path, '{') === false && strpos($path, '[') === false) { return self::_simpleOp('insert', $data, $tokens, $values); } $token = array_shift($tokens); $nextPath = implode('.', $tokens); + + list($token, $conditions) = self::_splitConditions($token); + foreach ($data as $k => $v) { if (self::_matchToken($k, $token)) { - $data[$k] = self::insert($v, $nextPath, $values); + if ($conditions && self::_matches($v, $conditions)) { + $data[$k] = array_merge($v, $values); + continue; + } + if (!$conditions) { + $data[$k] = self::insert($v, $nextPath, $values); + } } } return $data; @@ -294,17 +319,32 @@ class Hash { * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::remove */ public static function remove(array $data, $path) { - $tokens = explode('.', $path); - if (strpos($path, '{') === false) { + if (strpos($path, '[') === false) { + $tokens = explode('.', $path); + } else { + $tokens = String::tokenize($path, '.', '[', ']'); + } + + if (strpos($path, '{') === false && strpos($path, '[') === false) { return self::_simpleOp('remove', $data, $tokens); } $token = array_shift($tokens); $nextPath = implode('.', $tokens); + + list($token, $conditions) = self::_splitConditions($token); + foreach ($data as $k => $v) { $match = self::_matchToken($k, $token); if ($match && is_array($v)) { + if ($conditions && self::_matches($v, $conditions)) { + unset($data[$k]); + continue; + } $data[$k] = self::remove($v, $nextPath); + if (empty($data[$k])) { + unset($data[$k]); + } } elseif ($match) { unset($data[$k]); } diff --git a/lib/Cake/Utility/Security.php b/lib/Cake/Utility/Security.php index 5df50f923..e9017c195 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -289,4 +289,94 @@ class Security { return crypt($password, $salt); } +/** + * Encrypt a value using AES-256. + * + * *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes. + * Any trailing null bytes will be removed on decryption due to how PHP pads messages + * with nulls prior to encryption. + * + * @param string $plain The value to encrypt. + * @param string $key The 256 bit/32 byte key to use as a cipher key. + * @param string $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt. + * @return string Encrypted data. + * @throws CakeException On invalid data or key. + */ + public static function encrypt($plain, $key, $hmacSalt = null) { + self::_checkKey($key, 'encrypt()'); + if (empty($plain)) { + throw new CakeException(__d('cake_dev', 'The data to encrypt cannot be empty.')); + } + if ($hmacSalt === null) { + $hmacSalt = Configure::read('Security.salt'); + } + + // Generate the encryption and hmac key. + $key = substr(hash('sha256', $key . $hmacSalt), 0, 32); + + $algorithm = MCRYPT_RIJNDAEL_128; + $mode = MCRYPT_MODE_CBC; + + $ivSize = mcrypt_get_iv_size($algorithm, $mode); + $iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM); + $ciphertext = $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv); + $hmac = hash_hmac('sha256', $ciphertext, $key); + return $hmac . $ciphertext; + } + +/** + * Check the encryption key for proper length. + * + * @param string $key + * @param string $method The method the key is being checked for. + * @return void + * @throws CakeException When key length is not 256 bit/32 bytes + */ + protected static function _checkKey($key, $method) { + if (strlen($key) < 32) { + throw new CakeException(__d('cake_dev', 'Invalid key for %s, key must be at least 256 bits (32 bytes) long.', $method)); + } + } + +/** + * Decrypt a value using AES-256. + * + * @param string $cipher The ciphertext to decrypt. + * @param string $key The 256 bit/32 byte key to use as a cipher key. + * @param string $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt. + * @return string Decrypted data. Any trailing null bytes will be removed. + * @throws CakeException On invalid data or key. + */ + public static function decrypt($cipher, $key, $hmacSalt = null) { + self::_checkKey($key, 'decrypt()'); + if (empty($cipher)) { + throw new CakeException(__d('cake_dev', 'The data to decrypt cannot be empty.')); + } + if ($hmacSalt === null) { + $hmacSalt = Configure::read('Security.salt'); + } + + // Generate the encryption and hmac key. + $key = substr(hash('sha256', $key . $hmacSalt), 0, 32); + + // Split out hmac for comparison + $macSize = 64; + $hmac = substr($cipher, 0, $macSize); + $cipher = substr($cipher, $macSize); + + $compareHmac = hash_hmac('sha256', $cipher, $key); + if ($hmac !== $compareHmac) { + return false; + } + + $algorithm = MCRYPT_RIJNDAEL_128; + $mode = MCRYPT_MODE_CBC; + $ivSize = mcrypt_get_iv_size($algorithm, $mode); + + $iv = substr($cipher, 0, $ivSize); + $cipher = substr($cipher, $ivSize); + $plain = mcrypt_decrypt($algorithm, $key, $cipher, $mode, $iv); + return rtrim($plain, "\0"); + } + } diff --git a/lib/Cake/Utility/Validation.php b/lib/Cake/Utility/Validation.php index 9a803e562..745e2a482 100644 --- a/lib/Cake/Utility/Validation.php +++ b/lib/Cake/Utility/Validation.php @@ -540,7 +540,7 @@ class Validation { } /** - * Validate a multiple select. + * Validate a multiple select. Comparison is case sensitive by default. * * Valid Options * @@ -550,12 +550,13 @@ class Validation { * * @param array $check Value to check * @param array $options Options for the check. - * @param boolean $strict Defaults to true, set to false to disable strict type check + * @param boolean $caseInsensitive Set to true for case insensitive comparison. * @return boolean Success */ - public static function multiple($check, $options = array(), $strict = true) { + public static function multiple($check, $options = array(), $caseInsensitive = false) { $defaults = array('in' => null, 'max' => null, 'min' => null); $options = array_merge($defaults, $options); + $check = array_filter((array)$check); if (empty($check)) { return false; @@ -567,8 +568,15 @@ class Validation { return false; } if ($options['in'] && is_array($options['in'])) { + if ($caseInsensitive) { + $options['in'] = array_map('mb_strtolower', $options['in']); + } foreach ($check as $val) { - if (!in_array($val, $options['in'], $strict)) { + $strict = !is_numeric($val); + if ($caseInsensitive) { + $val = mb_strtolower($val); + } + if (!in_array((string)$val, $options['in'], $strict)) { return false; } } @@ -766,15 +774,22 @@ class Validation { } /** - * Checks if a value is in a given list. + * Checks if a value is in a given list. Comparison is case sensitive by default. * - * @param string $check Value to check - * @param array $list List to check against - * @param boolean $strict Defaults to true, set to false to disable strict type check - * @return boolean Success + * @param string $check Value to check. + * @param array $list List to check against. + * @param boolean $caseInsensitive Set to true for case insensitive comparison. + * @return boolean Success. */ - public static function inList($check, $list, $strict = true) { - return in_array($check, $list, $strict); + public static function inList($check, $list, $caseInsensitive = false) { + $strict = !is_numeric($check); + + if ($caseInsensitive) { + $list = array_map('mb_strtolower', $list); + $check = mb_strtolower($check); + } + + return in_array((string)$check, $list, $strict); } /** @@ -896,7 +911,7 @@ class Validation { } /** - * Checks the mime type of a file + * Checks the mime type of a file. Comparison is case sensitive. * * @param string|array $check * @param array $mimeTypes to check for diff --git a/lib/Cake/VERSION.txt b/lib/Cake/VERSION.txt index fdd823fce..80bea0158 100644 --- a/lib/Cake/VERSION.txt +++ b/lib/Cake/VERSION.txt @@ -17,4 +17,4 @@ // @license http://www.opensource.org/licenses/mit-license.php MIT License // +--------------------------------------------------------------------------------------------+ // //////////////////////////////////////////////////////////////////////////////////////////////////// -2.4.1 +2.5.0-dev diff --git a/lib/Cake/View/Helper/RssHelper.php b/lib/Cake/View/Helper/RssHelper.php index ac0592442..348b9ace5 100644 --- a/lib/Cake/View/Helper/RssHelper.php +++ b/lib/Cake/View/Helper/RssHelper.php @@ -123,12 +123,12 @@ class RssHelper extends AppHelper { * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/rss.html#RssHelper::channel */ public function channel($attrib = array(), $elements = array(), $content = null) { - if (!isset($elements['title']) && !empty($this->_View->pageTitle)) { - $elements['title'] = $this->_View->pageTitle; - } if (!isset($elements['link'])) { $elements['link'] = '/'; } + if (!isset($elements['title'])) { + $elements['title'] = ''; + } if (!isset($elements['description'])) { $elements['description'] = ''; } diff --git a/lib/Cake/View/View.php b/lib/Cake/View/View.php index 78a042b69..3588baa5f 100644 --- a/lib/Cake/View/View.php +++ b/lib/Cake/View/View.php @@ -584,11 +584,12 @@ class View extends Object { * Blocks are checked before view variables. * * @param string $var The view var you want the contents of. - * @return mixed The content of the named var if its set, otherwise null. + * @param mixed $default The default/fallback content of $var. + * @return mixed The content of the named var if its set, otherwise $default. */ - public function get($var) { + public function get($var, $default = null) { if (!isset($this->viewVars[$var])) { - return null; + return $default; } return $this->viewVars[$var]; } diff --git a/lib/Cake/basics.php b/lib/Cake/basics.php index cf0dc13a5..97a6d5a88 100644 --- a/lib/Cake/basics.php +++ b/lib/Cake/basics.php @@ -65,7 +65,7 @@ if (!function_exists('debug')) { * * Only runs if debug level is greater than zero. * - * @param boolean $var Variable to show debug information for. + * @param mixed $var Variable to show debug information for. * @param boolean $showHtml If set to true, the method prints the debug data in a browser-friendly way. * @param boolean $showFrom If set to true, the method prints from where the function was called. * @return void