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