From 4e74b931060db83735e3d5bac61688a54f8feff5 Mon Sep 17 00:00:00 2001 From: mark_story Date: Fri, 16 Aug 2013 14:47:44 -0400 Subject: [PATCH 01/58] Start the 2.5.x branch. --- lib/Cake/VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/VERSION.txt b/lib/Cake/VERSION.txt index 51d91d52e..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.0-RC1 +2.5.0-dev From 8e5a9cd7da8ca1be6168493ba5148e4aea0024e9 Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Fri, 26 Jul 2013 20:12:20 +0700 Subject: [PATCH 02/58] Accept 'connectOptions' in Router::mapResources() --- lib/Cake/Routing/Router.php | 8 +++++++- lib/Cake/Test/Case/Routing/RouterTest.php | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/Cake/Routing/Router.php b/lib/Cake/Routing/Router.php index e04ffc25f..c545f9768 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; diff --git a/lib/Cake/Test/Case/Routing/RouterTest.php b/lib/Cake/Test/Case/Routing/RouterTest.php index 57e480978..5f68e38bf 100644 --- a/lib/Cake/Test/Case/Routing/RouterTest.php +++ b/lib/Cake/Test/Case/Routing/RouterTest.php @@ -179,6 +179,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. * From a0014e7a303067bb9c36d438de5a70fe819d22a7 Mon Sep 17 00:00:00 2001 From: Chen Cohen Date: Fri, 23 Aug 2013 13:27:06 +0300 Subject: [PATCH 03/58] Ticket 4011 - Adding matchers support for Hash::remove() and Hash::insert() --- .../Case/Model/Datasource/CakeSessionTest.php | 4 +- lib/Cake/Test/Case/Utility/HashTest.php | 50 +++++++++++++++ lib/Cake/Utility/Hash.php | 63 +++++++++++++++---- 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php b/lib/Cake/Test/Case/Model/Datasource/CakeSessionTest.php index b034c2d54..b6d0a8c7d 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/Utility/HashTest.php b/lib/Cake/Test/Case/Utility/HashTest.php index 67adfc9db..7f28223e6 100644 --- a/lib/Cake/Test/Case/Utility/HashTest.php +++ b/lib/Cake/Test/Case/Utility/HashTest.php @@ -1297,6 +1297,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); } /** @@ -1360,6 +1377,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); } /** @@ -1379,6 +1413,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/Utility/Hash.php b/lib/Cake/Utility/Hash.php index 80d8f291a..ea514975e 100644 --- a/lib/Cake/Utility/Hash.php +++ b/lib/Cake/Utility/Hash.php @@ -109,12 +109,7 @@ class Hash { foreach ($tokens as $token) { $next = array(); - $conditions = false; - $position = strpos($token, '['); - if ($position !== false) { - $conditions = substr($token, $position); - $token = substr($token, 0, $position); - } + list($token, $conditions) = self::_splitConditions($token); foreach ($context[$_key] as $item) { foreach ((array)$item as $k => $v) { @@ -139,6 +134,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. @@ -222,16 +233,31 @@ class Hash { * @return array The data with $values inserted. */ 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) { + if (self::_matches($v, $conditions)) { + $data[$k] = array_merge($v, $values); + continue; + } + } else { + $data[$k] = self::insert($v, $nextPath, $values); + } } } return $data; @@ -290,17 +316,32 @@ class Hash { * @return array The modified array. */ 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]); } From f655e8a7bcc0a363c62466b5389b09b0846a5635 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 24 Aug 2013 21:49:17 -0400 Subject: [PATCH 04/58] Minor simplification. --- lib/Cake/Utility/Hash.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/Cake/Utility/Hash.php b/lib/Cake/Utility/Hash.php index ea514975e..e9605ddc7 100644 --- a/lib/Cake/Utility/Hash.php +++ b/lib/Cake/Utility/Hash.php @@ -250,12 +250,11 @@ class Hash { foreach ($data as $k => $v) { if (self::_matchToken($k, $token)) { - if ($conditions) { - if (self::_matches($v, $conditions)) { - $data[$k] = array_merge($v, $values); - continue; - } - } else { + if ($conditions && self::_matches($v, $conditions)) { + $data[$k] = array_merge($v, $values); + continue; + } + if (!$conditions) { $data[$k] = self::insert($v, $nextPath, $values); } } From e315f563be9f932eb8548b52824548d8801fdde7 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Mon, 26 Aug 2013 18:26:33 -0400 Subject: [PATCH 05/58] Add MemcachedEngine --- lib/Cake/Cache/Engine/MemcachedEngine.php | 280 +++++++++ .../Case/Cache/Engine/MemcachedEngineTest.php | 570 ++++++++++++++++++ 2 files changed, 850 insertions(+) create mode 100755 lib/Cake/Cache/Engine/MemcachedEngine.php create mode 100755 lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php new file mode 100755 index 000000000..9729283ac --- /dev/null +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -0,0 +1,280 @@ + 127.0.0.1. If an + * array MemcacheEngine will use them as a pool. + * - compress = boolean, default => false + * + * @var array + */ + public $settings = array(); + +/** + * 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 + */ + 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' => true, + 'persistent_id' => 'mc', + 'login' => null, + 'password' => null, + ); + parent::init($settings); + + if (!is_array($this->settings['servers'])) { + $this->settings['servers'] = array($this->settings['servers']); + } + if (!isset($this->_Memcached)) { + $this->_Memcached = new Memcached($this->settings['persistent'] ? $this->settings['persistent_id'] : null); + $this->_setOptions(); + + if (!count($this->_Memcached->getServerList())) { + $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) { + $this->_Memcached->setSaslAuthData($this->settings['login'], $this->settings['password']); + } + } + + return true; + } + + return true; + } + +/** + * Settings the memcached instance + * + */ + protected function _setOptions() + { + $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); + //$this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); + + if (Memcached::HAVE_IGBINARY) { + $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY); + } + + $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/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php new file mode 100755 index 000000000..f284c0461 --- /dev/null +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -0,0 +1,570 @@ + + * 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 1.2.0.5434 + * @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.'); + + $this->_cacheDisable = Configure::read('Cache.disable'); + Configure::write('Cache.disable', false); + Cache::config('memcached', array( + 'engine' => 'Memcached', + 'prefix' => 'cake_', + 'duration' => 3600 + )); + } + +/** + * tearDown method + * + * @return void + */ + public function tearDown() { + parent::tearDown(); + Configure::write('Cache.disable', $this->_cacheDisable); + 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['serialize'], $settings['path']); + $expecting = array( + 'prefix' => 'cake_', + 'duration' => 3600, + 'probability' => 100, + 'servers' => array('127.0.0.1'), + 'persistent' => true, + 'persistent_id' => 'mc', + 'compress' => false, + 'engine' => 'Memcached', + 'login' => null, + 'password' => null, + 'groups' => array() + ); + $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)); + } + +/** + * 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')); + } +} From 6a8913361f6fc43cd3b06778bd81433b5a8af3f4 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Mon, 26 Aug 2013 19:10:25 -0400 Subject: [PATCH 06/58] Fix coding standards --- lib/Cake/Cache/Engine/MemcachedEngine.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 9729283ac..6cdab23f9 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -109,8 +109,7 @@ class MemcachedEngine extends CacheEngine { * Settings the memcached instance * */ - protected function _setOptions() - { + protected function _setOptions() { $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); //$this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); @@ -225,7 +224,7 @@ class MemcachedEngine extends CacheEngine { $keys = $this->_Memcached->getAllKeys(); - foreach($keys as $key) { + foreach ($keys as $key) { if (strpos($key, $this->settings['prefix']) === 0) { $this->_Memcached->delete($key); } From 7c33878f4cc403ac0fae1753c23d626bc88559d2 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Mon, 26 Aug 2013 20:55:19 -0400 Subject: [PATCH 07/58] Coding standard fix --- lib/Cake/Cache/Engine/MemcachedEngine.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 6cdab23f9..4814b066f 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -6,16 +6,17 @@ * PHP 5 * * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) - * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * 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 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) * @link http://cakephp.org CakePHP(tm) Project * @package Cake.Cache.Engine - * @since CakePHP(tm) v 1.2.0.4933 - * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + * @since CakePHP(tm) v 2.5.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License */ /** @@ -111,7 +112,6 @@ class MemcachedEngine extends CacheEngine { */ protected function _setOptions() { $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); - //$this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); if (Memcached::HAVE_IGBINARY) { $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY); From 2b20ddc81c02f654ed6130c4a6da00211552b7ae Mon Sep 17 00:00:00 2001 From: Kamisama Date: Mon, 26 Aug 2013 20:55:38 -0400 Subject: [PATCH 08/58] Drop backup/restore Configure values --- lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index f284c0461..c1e926640 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -14,7 +14,7 @@ * @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 1.2.0.5434 + * @since CakePHP(tm) v 2.5.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ @@ -64,8 +64,6 @@ class MemcachedEngineTest extends CakeTestCase { parent::setUp(); $this->skipIf(!class_exists('Memcached'), 'Memcached is not installed or configured properly.'); - $this->_cacheDisable = Configure::read('Cache.disable'); - Configure::write('Cache.disable', false); Cache::config('memcached', array( 'engine' => 'Memcached', 'prefix' => 'cake_', @@ -80,7 +78,6 @@ class MemcachedEngineTest extends CakeTestCase { */ public function tearDown() { parent::tearDown(); - Configure::write('Cache.disable', $this->_cacheDisable); Cache::drop('memcached'); Cache::drop('memcached_groups'); Cache::drop('memcached_helper'); From c7dacb05e70fe14472663535e5734872a795daa4 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Mon, 26 Aug 2013 21:27:08 -0400 Subject: [PATCH 09/58] Add support for Amazon ElastiCache --- lib/Cake/Cache/Engine/MemcachedEngine.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 4814b066f..2155318c9 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -117,6 +117,11 @@ class MemcachedEngine extends CacheEngine { $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY); } + // 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']); } From cf3dfb0c66a6019b7484b27f37308a15ddc52c2b Mon Sep 17 00:00:00 2001 From: Calin Date: Tue, 27 Aug 2013 11:00:32 +0300 Subject: [PATCH 10/58] Allow 'options' to be given as array when adding a param detector - similar to env / options. Allow the following when adding request detectors using CakeRequest::addDetector(): array('param'=>'{param-name}', 'options'=>array()) --- lib/Cake/Network/CakeRequest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Cake/Network/CakeRequest.php b/lib/Cake/Network/CakeRequest.php index fb7ccc04d..12ac33853 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); From 075bf370307e78f762883d2fa49acb5cea3ade6c Mon Sep 17 00:00:00 2001 From: Calin Date: Tue, 27 Aug 2013 11:20:07 +0300 Subject: [PATCH 11/58] Added test case for param with options detector. Fix coding standards errors. --- lib/Cake/Network/CakeRequest.php | 2 +- lib/Cake/Test/Case/Network/CakeRequestTest.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Cake/Network/CakeRequest.php b/lib/Cake/Network/CakeRequest.php index 12ac33853..a4989d770 100644 --- a/lib/Cake/Network/CakeRequest.php +++ b/lib/Cake/Network/CakeRequest.php @@ -521,7 +521,7 @@ class CakeRequest implements ArrayAccess { 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; + return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options']) : false; } } if (isset($detect['callback']) && is_callable($detect['callback'])) { diff --git a/lib/Cake/Test/Case/Network/CakeRequestTest.php b/lib/Cake/Test/Case/Network/CakeRequestTest.php index 06e4f8151..a49fbf9e7 100644 --- a/lib/Cake/Test/Case/Network/CakeRequestTest.php +++ b/lib/Cake/Test/Case/Network/CakeRequestTest.php @@ -1050,6 +1050,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()); } /** From ad7563736f5735b0a373b9c127b86871441554e6 Mon Sep 17 00:00:00 2001 From: mark_story Date: Tue, 27 Aug 2013 09:46:59 -0400 Subject: [PATCH 12/58] Expand doc blocks for CakeRequest::addDetector. --- lib/Cake/Network/CakeRequest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Cake/Network/CakeRequest.php b/lib/Cake/Network/CakeRequest.php index a4989d770..574985754 100644 --- a/lib/Cake/Network/CakeRequest.php +++ b/lib/Cake/Network/CakeRequest.php @@ -581,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. From f093a31740a1c6a841980e1606c966d3ac9f3a13 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Tue, 27 Aug 2013 13:22:03 -0400 Subject: [PATCH 13/58] Remove unnecessary return --- lib/Cake/Cache/Engine/MemcachedEngine.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 2155318c9..f7fbca3e2 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -99,8 +99,6 @@ class MemcachedEngine extends CacheEngine { $this->_Memcached->setSaslAuthData($this->settings['login'], $this->settings['password']); } } - - return true; } return true; From bdd00c230702e5074fd3fb0a792fac4e66132beb Mon Sep 17 00:00:00 2001 From: Kamisama Date: Tue, 27 Aug 2013 13:57:57 -0400 Subject: [PATCH 14/58] Throw an exception if attempting to use authentication without SASL support installed --- lib/Cake/Cache/Engine/MemcachedEngine.php | 5 ++++ .../Case/Cache/Engine/MemcachedEngineTest.php | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index f7fbca3e2..6b5dd738f 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -96,6 +96,11 @@ class MemcachedEngine extends CacheEngine { } 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']); } } diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index c1e926640..73741dabc 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -133,6 +133,33 @@ class MemcachedEngineTest extends CakeTestCase { $this->assertTrue($MemcachedCompressed->getMemcached()->getOption(Memcached::OPT_COMPRESSION)); } +/** + * 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 * From 244df0764fe6475bf416a919ccc58baecb7475f6 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Tue, 27 Aug 2013 14:22:45 -0400 Subject: [PATCH 15/58] Coding standard and readability --- lib/Cake/Cache/Engine/MemcachedEngine.php | 48 +++++++++++-------- .../Case/Cache/Engine/MemcachedEngineTest.php | 4 +- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 6b5dd738f..3538becb3 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -59,6 +59,7 @@ class MemcachedEngine extends CacheEngine { * * @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')) { @@ -81,29 +82,34 @@ class MemcachedEngine extends CacheEngine { if (!is_array($this->settings['servers'])) { $this->settings['servers'] = array($this->settings['servers']); } - if (!isset($this->_Memcached)) { - $this->_Memcached = new Memcached($this->settings['persistent'] ? $this->settings['persistent_id'] : null); - $this->_setOptions(); - if (!count($this->_Memcached->getServerList())) { - $servers = array(); - foreach ($this->settings['servers'] as $server) { - $servers[] = $this->_parseServerString($server); - } + if (isset($this->_Memcached)) { + return true; + } - if (!$this->_Memcached->addServers($servers)) { - return false; - } + $this->_Memcached = new Memcached($this->settings['persistent'] ? $this->settings['persistent_id'] : null); + $this->_setOptions(); - 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']); - } + 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; @@ -136,10 +142,10 @@ class MemcachedEngine extends CacheEngine { * @return array Array containing host, port */ protected function _parseServerString($server) { - if ($server[0] == 'u') { + if ($server[0] === 'u') { return array($server, 0); } - if (substr($server, 0, 1) == '[') { + if (substr($server, 0, 1) === '[') { $position = strpos($server, ']:'); if ($position !== false) { $position++; diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index 73741dabc..cc8b61495 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -155,8 +155,8 @@ class MemcachedEngineTest extends CakeTestCase { ); $this->setExpectedException( - 'CacheException', 'Memcached extension is not build with SASL support' - ); + 'CacheException', 'Memcached extension is not build with SASL support' + ); $Memcached->init($settings); } From 70f321e32747b3a7717ff9ca956a0746b8d0caec Mon Sep 17 00:00:00 2001 From: Kamisama Date: Tue, 27 Aug 2013 14:48:36 -0400 Subject: [PATCH 16/58] Starting the memcached server on travis To properly test the memcached cache engine --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) 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 " Date: Tue, 27 Aug 2013 21:20:22 -0400 Subject: [PATCH 17/58] Add encrypt() and decrypt() methods. These methods use AES-256 and provide a simple to use API with easy to remember names. --- lib/Cake/Test/Case/Utility/SecurityTest.php | 67 +++++++++++++++++++++ lib/Cake/Utility/Security.php | 65 ++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/lib/Cake/Test/Case/Utility/SecurityTest.php b/lib/Cake/Test/Case/Utility/SecurityTest.php index f1a0e33f8..3f31219bd 100644 --- a/lib/Cake/Test/Case/Utility/SecurityTest.php +++ b/lib/Cake/Test/Case/Utility/SecurityTest.php @@ -302,4 +302,71 @@ 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 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/Utility/Security.php b/lib/Cake/Utility/Security.php index 5df50f923..cc47db7c1 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -289,4 +289,69 @@ 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. + * @return string Encrypted data. + * @throws CakeException On invalid data or key. + */ + public static function encrypt($plain, $key) { + static::_checkKey($key, 'encrypt()'); + if (empty($plain)) { + throw new CakeException(__d('cake_dev', 'The data to encrypt cannot be empty.')); + } + $key = substr($key, 0, 32); + $algorithm = MCRYPT_RIJNDAEL_128; + $mode = MCRYPT_MODE_CBC; + + $ivSize = mcrypt_get_iv_size($algorithm, $mode); + $iv = mcrypt_create_iv($ivSize, MCRYPT_RAND); + return $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv); + } + +/** + * 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. + * @return string Decrypted data. Any trailing null bytes will be removed. + * @throws CakeException On invalid data or key. + */ + public static function decrypt($cipher, $key) { + static::_checkKey($key, 'decrypt()'); + if (empty($cipher)) { + throw new CakeException(__d('cake_dev', 'The data to decrypt cannot be empty.')); + } + $key = substr($key, 0, 32); + + $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"); + } + } From 005a7d841d0843f1e8d8c808dd965ee6323c03ce Mon Sep 17 00:00:00 2001 From: mark_story Date: Tue, 27 Aug 2013 21:32:30 -0400 Subject: [PATCH 18/58] Add support for aes encrypted cookies. With Security supporting AES encryption it is also ideal to have AES compatible cookies. Refactor and simplify code. Dynamic invocation of static method is a bit obtuse and the various methods don't all have the same arguments. --- .../Controller/Component/CookieComponent.php | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/lib/Cake/Controller/Component/CookieComponent.php b/lib/Cake/Controller/Component/CookieComponent.php index e2831783d..1b7d6dea0 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 * From c5092851d1e0caa960aa324cebac6405be946179 Mon Sep 17 00:00:00 2001 From: mark_story Date: Tue, 27 Aug 2013 21:51:40 -0400 Subject: [PATCH 19/58] Fix compatibility with PHP 5.2 --- lib/Cake/Test/Case/Utility/SecurityTest.php | 1 - lib/Cake/Utility/Security.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Cake/Test/Case/Utility/SecurityTest.php b/lib/Cake/Test/Case/Utility/SecurityTest.php index 3f31219bd..07afdc09c 100644 --- a/lib/Cake/Test/Case/Utility/SecurityTest.php +++ b/lib/Cake/Test/Case/Utility/SecurityTest.php @@ -342,7 +342,6 @@ class SecurityTest extends CakeTestCase { Security::encrypt($txt, $key); } - /** * Test that short keys cause errors * diff --git a/lib/Cake/Utility/Security.php b/lib/Cake/Utility/Security.php index cc47db7c1..8a5cc1f3c 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -302,7 +302,7 @@ class Security { * @throws CakeException On invalid data or key. */ public static function encrypt($plain, $key) { - static::_checkKey($key, 'encrypt()'); + self::_checkKey($key, 'encrypt()'); if (empty($plain)) { throw new CakeException(__d('cake_dev', 'The data to encrypt cannot be empty.')); } @@ -338,7 +338,7 @@ class Security { * @throws CakeException On invalid data or key. */ public static function decrypt($cipher, $key) { - static::_checkKey($key, 'decrypt()'); + self::_checkKey($key, 'decrypt()'); if (empty($cipher)) { throw new CakeException(__d('cake_dev', 'The data to decrypt cannot be empty.')); } From a54e8f238e7a8fc5ba4d877ec5e3d1786a71bf58 Mon Sep 17 00:00:00 2001 From: Gilles Wittenberg Date: Thu, 29 Aug 2013 09:13:54 +0200 Subject: [PATCH 20/58] Add y/yes option to schema create and update subcommands --- lib/Cake/Console/Command/SchemaShell.php | 15 ++++-- .../Case/Console/Command/SchemaShellTest.php | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/lib/Cake/Console/Command/SchemaShell.php b/lib/Cake/Console/Command/SchemaShell.php index 223b1179b..58f03d86c 100644 --- a/lib/Cake/Console/Command/SchemaShell.php +++ b/lib/Cake/Console/Command/SchemaShell.php @@ -333,7 +333,7 @@ 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 +341,7 @@ 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 +392,7 @@ 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); @@ -496,6 +496,11 @@ 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( @@ -523,7 +528,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 +541,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/Test/Case/Console/Command/SchemaShellTest.php b/lib/Cake/Test/Case/Console/Command/SchemaShellTest.php index a78c0fbdd..4f4c96c35 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. * From 95ad5f5c78f8bff40924eb0803cdc62e3070bb3d Mon Sep 17 00:00:00 2001 From: mark_story Date: Thu, 29 Aug 2013 14:40:01 -0400 Subject: [PATCH 21/58] Add hmac to encrypted data. Using an HMAC ensures that the ciphertext has not been modified. --- lib/Cake/Test/Case/Utility/SecurityTest.php | 45 +++++++++++++++++++++ lib/Cake/Utility/Security.php | 37 ++++++++++++++--- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/lib/Cake/Test/Case/Utility/SecurityTest.php b/lib/Cake/Test/Case/Utility/SecurityTest.php index 07afdc09c..3fe83b27c 100644 --- a/lib/Cake/Test/Case/Utility/SecurityTest.php +++ b/lib/Cake/Test/Case/Utility/SecurityTest.php @@ -316,6 +316,51 @@ class SecurityTest extends CakeTestCase { $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 * diff --git a/lib/Cake/Utility/Security.php b/lib/Cake/Utility/Security.php index 8a5cc1f3c..247a94684 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -298,21 +298,30 @@ class Security { * * @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) { + 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.')); } - $key = substr($key, 0, 32); + 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_RAND); - return $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv); + $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; } /** @@ -334,15 +343,31 @@ class Security { * * @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) { + 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.')); } - $key = substr($key, 0, 32); + 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; From 9e8c4ad285af1a60e0d1b2067bcd539a950b1cb7 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 31 Aug 2013 13:15:53 -0400 Subject: [PATCH 22/58] Rename persistent_id to persistentId. The new name is more consistent with other settings in CakePHP. --- lib/Cake/Cache/Engine/MemcachedEngine.php | 4 ++-- lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 3538becb3..32c27d657 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -73,7 +73,7 @@ class MemcachedEngine extends CacheEngine { 'servers' => array('127.0.0.1'), 'compress' => false, 'persistent' => true, - 'persistent_id' => 'mc', + 'persistentId' => 'mc', 'login' => null, 'password' => null, ); @@ -87,7 +87,7 @@ class MemcachedEngine extends CacheEngine { return true; } - $this->_Memcached = new Memcached($this->settings['persistent'] ? $this->settings['persistent_id'] : null); + $this->_Memcached = new Memcached($this->settings['persistent'] ? $this->settings['persistentId'] : null); $this->_setOptions(); if (count($this->_Memcached->getServerList())) { diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index cc8b61495..a4539771f 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -98,7 +98,7 @@ class MemcachedEngineTest extends CakeTestCase { 'probability' => 100, 'servers' => array('127.0.0.1'), 'persistent' => true, - 'persistent_id' => 'mc', + 'persistentId' => 'mc', 'compress' => false, 'engine' => 'Memcached', 'login' => null, From 9b0e26cc2181ca9a36f757eb57a15dbfed3cc4d2 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 31 Aug 2013 13:20:41 -0400 Subject: [PATCH 23/58] Deprecate MemcacheEngine and update defaults for Memcached People should switch to Memcached instead. The underlying extension is better maintained and provides improved features and performance. Collapse the persistent and persistentId settings, while also making non-persistent connections the default. Persistent connections should be an opt-in feature as having them enabled by default could go very wrong on shared hosting environments. --- app/Config/core.php | 10 ++++++---- lib/Cake/Cache/Engine/MemcacheEngine.php | 3 ++- lib/Cake/Cache/Engine/MemcachedEngine.php | 7 ++++--- .../Test/Case/Cache/Engine/MemcachedEngineTest.php | 3 +-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/Config/core.php b/app/Config/core.php index ba7a9d0ea..f1fd4aa56 100644 --- a/app/Config/core.php +++ b/app/Config/core.php @@ -309,18 +309,20 @@ * 'password' => '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/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 index 32c27d657..4b7202719 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -46,6 +46,8 @@ class MemcachedEngine extends CacheEngine { * - servers = string or array of memcached servers, default => 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. * * @var array */ @@ -72,8 +74,7 @@ class MemcachedEngine extends CacheEngine { 'engine' => 'Memcached', 'servers' => array('127.0.0.1'), 'compress' => false, - 'persistent' => true, - 'persistentId' => 'mc', + 'persistent' => false, 'login' => null, 'password' => null, ); @@ -87,7 +88,7 @@ class MemcachedEngine extends CacheEngine { return true; } - $this->_Memcached = new Memcached($this->settings['persistent'] ? $this->settings['persistentId'] : null); + $this->_Memcached = new Memcached($this->settings['persistent'] ? (string)$this->settings['persistent'] : null); $this->_setOptions(); if (count($this->_Memcached->getServerList())) { diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index a4539771f..7ca8e4155 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -97,8 +97,7 @@ class MemcachedEngineTest extends CakeTestCase { 'duration' => 3600, 'probability' => 100, 'servers' => array('127.0.0.1'), - 'persistent' => true, - 'persistentId' => 'mc', + 'persistent' => false, 'compress' => false, 'engine' => 'Memcached', 'login' => null, From b8ad7f314a57f44885168b6202d7deda8de8346c Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 31 Aug 2013 14:03:32 -0400 Subject: [PATCH 24/58] Fix doc blocks. --- lib/Cake/Cache/Engine/MemcachedEngine.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 4b7202719..ad41f1d68 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -1,10 +1,5 @@ Date: Sun, 1 Sep 2013 21:44:45 -0400 Subject: [PATCH 25/58] Fix coding standards error. --- lib/Cake/Utility/Security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/Utility/Security.php b/lib/Cake/Utility/Security.php index 247a94684..e9017c195 100644 --- a/lib/Cake/Utility/Security.php +++ b/lib/Cake/Utility/Security.php @@ -319,7 +319,7 @@ class Security { $ivSize = mcrypt_get_iv_size($algorithm, $mode); $iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM); - $ciphertext = $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv); + $ciphertext = $iv . mcrypt_encrypt($algorithm, $key, $plain, $mode, $iv); $hmac = hash_hmac('sha256', $ciphertext, $key); return $hmac . $ciphertext; } From 84a669aa7ada64223d7ff9741b5266623be6edf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20W=C3=BCrth?= Date: Mon, 2 Sep 2013 20:38:31 +0200 Subject: [PATCH 26/58] Shortened the if clause line length by doing it the @markstory style ;-) Also shortened the line length of some arrays --- lib/Cake/Console/Command/SchemaShell.php | 27 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/Cake/Console/Command/SchemaShell.php b/lib/Cake/Console/Command/SchemaShell.php index 58f03d86c..9a73e8674 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 (!empty($this->params['yes']) || $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 (!empty($this->params['yes']) || $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 (!empty($this->params['yes']) || $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( @@ -504,7 +517,9 @@ class SchemaShell extends AppShell { $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( From 0c3197a435ee319ac78a6f7dff8255f0abc7892c Mon Sep 17 00:00:00 2001 From: Simon Males Date: Wed, 11 Sep 2013 11:10:27 +0800 Subject: [PATCH 27/58] View::get() to support a fallback param --- lib/Cake/View/View.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Cake/View/View.php b/lib/Cake/View/View.php index 79fd648fa..af3c6574b 100644 --- a/lib/Cake/View/View.php +++ b/lib/Cake/View/View.php @@ -582,11 +582,12 @@ class View extends Object { * Blocks are checked before view variables. * * @param string $var The view var you want the contents of. + * @param mixed $default The default/fallback content of $var. * @return mixed The content of the named var if its set, otherwise null. */ - public function get($var) { + public function get($var, $default = null) { if (!isset($this->viewVars[$var])) { - return null; + return $default; } return $this->viewVars[$var]; } From 50ad043092070a1598a6d1a80a40b4f6565e3ed7 Mon Sep 17 00:00:00 2001 From: Simon Males Date: Wed, 11 Sep 2013 22:22:50 +0800 Subject: [PATCH 28/58] View::get() default param test --- lib/Cake/Test/Case/View/ViewTest.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/Cake/Test/Case/View/ViewTest.php b/lib/Cake/Test/Case/View/ViewTest.php index 78a149bb0..fdf52d6e2 100644 --- a/lib/Cake/Test/Case/View/ViewTest.php +++ b/lib/Cake/Test/Case/View/ViewTest.php @@ -1660,7 +1660,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 +1674,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); + } } From 8311a16ebd6c1cf71bf7e8b3a8b05ad3c30e48ea Mon Sep 17 00:00:00 2001 From: Simon Males Date: Wed, 11 Sep 2013 22:28:09 +0800 Subject: [PATCH 29/58] Accurate description of what is returned --- lib/Cake/View/View.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/View/View.php b/lib/Cake/View/View.php index af3c6574b..6292f926f 100644 --- a/lib/Cake/View/View.php +++ b/lib/Cake/View/View.php @@ -583,7 +583,7 @@ class View extends Object { * * @param string $var The view var you want the contents of. * @param mixed $default The default/fallback content of $var. - * @return mixed The content of the named var if its set, otherwise null. + * @return mixed The content of the named var if its set, otherwise $default. */ public function get($var, $default = null) { if (!isset($this->viewVars[$var])) { From 61b05c40c63486dfc3ee2029f613ccc2d53873dc Mon Sep 17 00:00:00 2001 From: euromark Date: Fri, 13 Sep 2013 02:28:25 +0200 Subject: [PATCH 30/58] remove autologging --- lib/Cake/Log/CakeLog.php | 18 +---------------- lib/Cake/Test/Case/Log/CakeLogTest.php | 28 ++++++++++++++------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/lib/Cake/Log/CakeLog.php b/lib/Cake/Log/CakeLog.php index f7b932051..a2694cbce 100644 --- a/lib/Cake/Log/CakeLog.php +++ b/lib/Cake/Log/CakeLog.php @@ -371,18 +371,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 @@ -454,11 +442,7 @@ class CakeLog { $logged = true; } } - if (!$logged) { - self::_autoConfig(); - self::stream('default')->write($type, $message); - } - return true; + return $logged; } /** 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')); From bd3f005ab69189ab5cfec9e70d1391efccd83785 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Wed, 18 Sep 2013 00:29:48 -0400 Subject: [PATCH 31/58] Add serializer option to Memcached Cache engine Add option to select a serializer engine among php, json or igbinary --- lib/Cake/Cache/Engine/MemcachedEngine.php | 37 +++++++- .../Case/Cache/Engine/MemcachedEngineTest.php | 87 ++++++++++++++++++- 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index ad41f1d68..be2aa9b90 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -47,6 +47,19 @@ class MemcachedEngine extends CacheEngine { */ public $settings = array(); +/** + * List of available serializer engine + * + * Memcached must be compiled with json and igbinary support to use these engines + * + * @var array + */ + public static $serializer = array( + 'igbinary' => Memcached::SERIALIZER_IGBINARY, + 'json' => Memcached::SERIALIZER_JSON, + 'php' => Memcached::SERIALIZER_PHP + ); + /** * Initialize the Cache Engine * @@ -71,6 +84,7 @@ class MemcachedEngine extends CacheEngine { 'persistent' => false, 'login' => null, 'password' => null, + 'serializer' => 'php' ); parent::init($settings); @@ -113,14 +127,33 @@ class MemcachedEngine extends CacheEngine { /** * 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); - if (Memcached::HAVE_IGBINARY) { - $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY); + if (!array_key_exists($this->settings['serializer'], self::$serializer)) { + throw new CacheException( + __d('cake_dev', sprintf('%s is not a valid serializer engine for Memcached', $this->settings['serializer'])) + ); } + $serializer = self::$serializer['php']; + switch($this->settings['serializer']) { + case 'igbinary': + if (Memcached::HAVE_IGBINARY) { + $serializer = self::$serializer['igbinary']; + } + break; + case 'json': + if (Memcached::HAVE_JSON) { + $serializer = self::$serializer['json']; + } + break; + } + + $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, $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); diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index 7ca8e4155..037627f38 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -102,7 +102,8 @@ class MemcachedEngineTest extends CakeTestCase { 'engine' => 'Memcached', 'login' => null, 'password' => null, - 'groups' => array() + 'groups' => array(), + 'serializer' => 'php' ); $this->assertEquals($expecting, $settings); } @@ -132,6 +133,90 @@ class MemcachedEngineTest extends CakeTestCase { $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, + 'serializer' => '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, + 'serializer' => 'php' + ); + + $Memcached->init($settings); + $this->assertEquals(Memcached::SERIALIZER_PHP, $Memcached->getMemcached()->getOption(Memcached::OPT_SERIALIZER)); + } + +/** + * testPhpSerializerSetting 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, + 'serializer' => 'json' + ); + + $Memcached->init($settings); + $this->assertEquals(Memcached::SERIALIZER_JSON, $Memcached->getMemcached()->getOption(Memcached::OPT_SERIALIZER)); + } + +/** + * testPhpSerializerSetting 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, + 'serializer' => 'igbinary' + ); + + $Memcached->init($settings); + $this->assertEquals(Memcached::SERIALIZER_IGBINARY, $Memcached->getMemcached()->getOption(Memcached::OPT_SERIALIZER)); + } + /** * test using authentication without memcached installed with SASL support * throw an exception From 5d509e23c25e366b2efb48d2466faf54e203d4cc Mon Sep 17 00:00:00 2001 From: Kamisama Date: Wed, 18 Sep 2013 00:46:21 -0400 Subject: [PATCH 32/58] Throw exception when selected serializer is not installed --- lib/Cake/Cache/Engine/MemcachedEngine.php | 8 +++ .../Case/Cache/Engine/MemcachedEngineTest.php | 54 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index be2aa9b90..37a0ab7b4 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -143,11 +143,19 @@ class MemcachedEngine extends CacheEngine { case 'igbinary': if (Memcached::HAVE_IGBINARY) { $serializer = self::$serializer['igbinary']; + } else { + throw new CacheException( + __d('cake_dev', 'Memcached extension is not compiled with igbinary support') + ); } break; case 'json': if (Memcached::HAVE_JSON) { $serializer = self::$serializer['json']; + } else { + throw new CacheException( + __d('cake_dev', 'Memcached extension is not compiled with json support') + ); } break; } diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index 037627f38..ea9180273 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -172,7 +172,7 @@ class MemcachedEngineTest extends CakeTestCase { } /** - * testPhpSerializerSetting method + * testJsonSerializerSetting method * * @return void */ @@ -195,7 +195,7 @@ class MemcachedEngineTest extends CakeTestCase { } /** - * testPhpSerializerSetting method + * testIgbinarySerializerSetting method * * @return void */ @@ -217,6 +217,56 @@ class MemcachedEngineTest extends CakeTestCase { $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, + 'serializer' => '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, + 'serializer' => '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 From 72965f793c7c974a3e33548d1ec11a16ca35d155 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Wed, 18 Sep 2013 12:45:08 -0400 Subject: [PATCH 33/58] Remove unnecessary sprintf --- lib/Cake/Cache/Engine/MemcachedEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 37a0ab7b4..ff24eea86 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -134,7 +134,7 @@ class MemcachedEngine extends CacheEngine { if (!array_key_exists($this->settings['serializer'], self::$serializer)) { throw new CacheException( - __d('cake_dev', sprintf('%s is not a valid serializer engine for Memcached', $this->settings['serializer'])) + __d('cake_dev', '%s is not a valid serializer engine for Memcached', $this->settings['serializer']) ); } From 0a704cc288fdfa50d8631c7c0d7a751231968112 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Thu, 19 Sep 2013 18:38:02 -0400 Subject: [PATCH 34/58] Fix test --- lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index ea9180273..8eb2672a0 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -91,7 +91,7 @@ class MemcachedEngineTest extends CakeTestCase { */ public function testSettings() { $settings = Cache::settings('memcached'); - unset($settings['serialize'], $settings['path']); + unset($settings['path']); $expecting = array( 'prefix' => 'cake_', 'duration' => 3600, From 7c83f7140fe65c145435b0c630e6297367c07f1b Mon Sep 17 00:00:00 2001 From: Kamisama Date: Thu, 19 Sep 2013 18:40:41 -0400 Subject: [PATCH 35/58] Rename `serializer` to `serialize` --- lib/Cake/Cache/Engine/MemcachedEngine.php | 8 ++++---- .../Test/Case/Cache/Engine/MemcachedEngineTest.php | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index ff24eea86..deef67fb1 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -84,7 +84,7 @@ class MemcachedEngine extends CacheEngine { 'persistent' => false, 'login' => null, 'password' => null, - 'serializer' => 'php' + 'serialize' => 'php' ); parent::init($settings); @@ -132,14 +132,14 @@ class MemcachedEngine extends CacheEngine { protected function _setOptions() { $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); - if (!array_key_exists($this->settings['serializer'], self::$serializer)) { + if (!array_key_exists($this->settings['serialize'], self::$serializer)) { throw new CacheException( - __d('cake_dev', '%s is not a valid serializer engine for Memcached', $this->settings['serializer']) + __d('cake_dev', '%s is not a valid serializer engine for Memcached', $this->settings['serialize']) ); } $serializer = self::$serializer['php']; - switch($this->settings['serializer']) { + switch($this->settings['serialize']) { case 'igbinary': if (Memcached::HAVE_IGBINARY) { $serializer = self::$serializer['igbinary']; diff --git a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php index 8eb2672a0..47dfe75ed 100755 --- a/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php +++ b/lib/Cake/Test/Case/Cache/Engine/MemcachedEngineTest.php @@ -103,7 +103,7 @@ class MemcachedEngineTest extends CakeTestCase { 'login' => null, 'password' => null, 'groups' => array(), - 'serializer' => 'php' + 'serialize' => 'php' ); $this->assertEquals($expecting, $settings); } @@ -144,7 +144,7 @@ class MemcachedEngineTest extends CakeTestCase { 'engine' => 'Memcached', 'servers' => array('127.0.0.1:11211'), 'persistent' => false, - 'serializer' => 'invalid_serializer' + 'serialize' => 'invalid_serializer' ); $this->setExpectedException( @@ -164,7 +164,7 @@ class MemcachedEngineTest extends CakeTestCase { 'engine' => 'Memcached', 'servers' => array('127.0.0.1:11211'), 'persistent' => false, - 'serializer' => 'php' + 'serialize' => 'php' ); $Memcached->init($settings); @@ -187,7 +187,7 @@ class MemcachedEngineTest extends CakeTestCase { 'engine' => 'Memcached', 'servers' => array('127.0.0.1:11211'), 'persistent' => false, - 'serializer' => 'json' + 'serialize' => 'json' ); $Memcached->init($settings); @@ -210,7 +210,7 @@ class MemcachedEngineTest extends CakeTestCase { 'engine' => 'Memcached', 'servers' => array('127.0.0.1:11211'), 'persistent' => false, - 'serializer' => 'igbinary' + 'serialize' => 'igbinary' ); $Memcached->init($settings); @@ -233,7 +233,7 @@ class MemcachedEngineTest extends CakeTestCase { 'engine' => 'Memcached', 'servers' => array('127.0.0.1:11211'), 'persistent' => false, - 'serializer' => 'json' + 'serialize' => 'json' ); $this->setExpectedException( @@ -258,7 +258,7 @@ class MemcachedEngineTest extends CakeTestCase { 'engine' => 'Memcached', 'servers' => array('127.0.0.1:11211'), 'persistent' => false, - 'serializer' => 'igbinary' + 'serialize' => 'igbinary' ); $this->setExpectedException( From d099c5ad7e511789355c36ab09691c8839ec0f5f Mon Sep 17 00:00:00 2001 From: Kamisama Date: Thu, 19 Sep 2013 18:56:33 -0400 Subject: [PATCH 36/58] Add doc about the new serialize setting --- lib/Cake/Cache/Engine/MemcachedEngine.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index deef67fb1..a80a62429 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -42,6 +42,9 @@ class MemcachedEngine extends CacheEngine { * - 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 */ From b6a7cb7c7e0934d77415f44fa0d0886b266eddf1 Mon Sep 17 00:00:00 2001 From: euromark Date: Fri, 20 Sep 2013 19:15:51 +0200 Subject: [PATCH 37/58] Output overall coverage. --- .../TestSuite/Coverage/HtmlCoverageReport.php | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) 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 << Date: Fri, 20 Sep 2013 19:31:16 +0200 Subject: [PATCH 38/58] correct doc block --- lib/Cake/TestSuite/Coverage/BaseCoverageReport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From f7e507145c5d1933959b9ed4270f975ef20d4326 Mon Sep 17 00:00:00 2001 From: euromark Date: Tue, 24 Sep 2013 02:01:50 +0200 Subject: [PATCH 39/58] Case insensitive validation inList option. Resolves ticket 3521. Added two tests to proof the faulty behavior of $strict here. Also address the faulty behavior in multiple() and replace it with case sensitivity. --- lib/Cake/Test/Case/Utility/ValidationTest.php | 26 +++++++++++-- lib/Cake/Utility/Validation.php | 39 +++++++++++++------ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/lib/Cake/Test/Case/Utility/ValidationTest.php b/lib/Cake/Test/Case/Utility/ValidationTest.php index a9133676b..a7056ebdf 100644 --- a/lib/Cake/Test/Case/Utility/ValidationTest.php +++ b/lib/Cake/Test/Case/Utility/ValidationTest.php @@ -1945,8 +1945,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)); } /** @@ -2065,14 +2072,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)); } /** @@ -2333,6 +2350,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/Utility/Validation.php b/lib/Cake/Utility/Validation.php index 45d1e3e37..bc9518c5b 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 From 1e7f11aaffa09580a9b7783872dfbdf8e17ddd25 Mon Sep 17 00:00:00 2001 From: John Long Date: Wed, 25 Sep 2013 14:53:23 -0500 Subject: [PATCH 40/58] $var in debug() docblock should be declared as mixed, not boolean --- lib/Cake/basics.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/basics.php b/lib/Cake/basics.php index a5bf97003..dc477e9b9 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 From 85e5ef4d4ece0014211e380c038b71d058ce3c10 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Tue, 1 Oct 2013 21:13:26 -0400 Subject: [PATCH 41/58] Change $serializer visibility, and other fix --- lib/Cake/Cache/Engine/MemcachedEngine.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index a80a62429..3eda6fe72 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -51,13 +51,13 @@ class MemcachedEngine extends CacheEngine { public $settings = array(); /** - * List of available serializer engine + * List of available serializer engines * * Memcached must be compiled with json and igbinary support to use these engines * * @var array */ - public static $serializer = array( + protected $_serializers = array( 'igbinary' => Memcached::SERIALIZER_IGBINARY, 'json' => Memcached::SERIALIZER_JSON, 'php' => Memcached::SERIALIZER_PHP @@ -135,17 +135,17 @@ class MemcachedEngine extends CacheEngine { protected function _setOptions() { $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); - if (!array_key_exists($this->settings['serialize'], self::$serializer)) { + if (!isset($this->_serializers[$this->settings['serialize']])) { throw new CacheException( __d('cake_dev', '%s is not a valid serializer engine for Memcached', $this->settings['serialize']) ); } - $serializer = self::$serializer['php']; + $serializer = $this->_serializers['php']; switch($this->settings['serialize']) { case 'igbinary': if (Memcached::HAVE_IGBINARY) { - $serializer = self::$serializer['igbinary']; + $serializer = $this->_serializers['igbinary']; } else { throw new CacheException( __d('cake_dev', 'Memcached extension is not compiled with igbinary support') @@ -154,7 +154,7 @@ class MemcachedEngine extends CacheEngine { break; case 'json': if (Memcached::HAVE_JSON) { - $serializer = self::$serializer['json']; + $serializer = $this->_serializers['json']; } else { throw new CacheException( __d('cake_dev', 'Memcached extension is not compiled with json support') From cdf2b8c40ddd6e380cf82a10126894ca0ff127c6 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Sat, 14 Sep 2013 00:36:57 +0200 Subject: [PATCH 42/58] Base completion files from @AD7six/cakephp-completion adjusted to be part of the framework --- app/Vendor/cake.bash | 50 ++++ lib/Cake/Console/Command/CompletionShell.php | 226 +++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 app/Vendor/cake.bash create mode 100644 lib/Cake/Console/Command/CompletionShell.php diff --git a/app/Vendor/cake.bash b/app/Vendor/cake.bash new file mode 100644 index 000000000..685108191 --- /dev/null +++ b/app/Vendor/cake.bash @@ -0,0 +1,50 @@ +# bash completion for CakePHP console + +_cake() +{ + local cur prev opts cake + COMPREPLY=() + cake="${COMP_WORDS[0]}" + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + if [[ "$cur" == -* ]] ; then + if [[ ${COMP_CWORD} = 1 ]] ; then + opts=$(${cake} Completion options) + elif [[ ${COMP_CWORD} = 2 ]] ; then + opts=$(${cake} Completion options "${COMP_WORDS[1]}") + else + opts=$(${cake} Completion options "${COMP_WORDS[1]}" "${COMP_WORDS[2]}") + fi + + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + if [[ ${COMP_CWORD} = 1 ]] ; then + opts=$(${cake} Completion commands) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + if [[ ${COMP_CWORD} = 2 ]] ; then + opts=$(${cake} Completion subcommands $prev) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + if [[ $COMPREPLY = "" ]] ; then + COMPREPLY=( $(compgen -df -- ${cur}) ) + return 0 + fi + return 0 + fi + + + opts=$(${cake} Completion fuzzy "${COMP_WORDS[@]:1}") + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + if [[ $COMPREPLY = "" ]] ; then + COMPREPLY=( $(compgen -df -- ${cur}) ) + return 0 + fi + return 0; +} + +complete -F _cake cake Console/cake diff --git a/lib/Cake/Console/Command/CompletionShell.php b/lib/Cake/Console/Command/CompletionShell.php new file mode 100644 index 000000000..da704a674 --- /dev/null +++ b/lib/Cake/Console/Command/CompletionShell.php @@ -0,0 +1,226 @@ +out($this->OptionParser->help()); + } + +/** + * list commands + */ + public function commands() { + $options = $this->_commands(); + return $this->_output($options); + } + +/** + * list options for the named command + */ + public function options() { + if (!$this->args) { + $parser = new ConsoleOptionParser(); + } else { + $Shell = $this->_getShell($this->args[0]); + 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 $this->_output($options); + } + +/** + * list subcommands for the named command + */ + public function subCommands() { + if (!$this->args) { + return $this->_output(); + } + + $options = $this->_subCommands($this->args[0]); + return $this->_output($options); + } + +/** + * Guess autocomplete from the whole argument string + */ + public function fuzzy() { + return $this->_output(); + } + +/** + * getOptionParser for _this_ shell + */ + public function getOptionParser() { + $translationDomain = 'bash_completion'; + + $parser = AppShell::getOptionParser(); + + $parser->description(__d($translationDomain, 'Used by bash to autocomplete command name, options and arguments')) + ->addSubcommand('commands', array( + 'help' => __d($translationDomain, 'Output a list of available commands'), + 'parser' => array( + 'description' => __d($translationDomain, 'List all availables'), + 'arguments' => array( + ) + ) + ))->addSubcommand('subcommands', array( + 'help' => __d($translationDomain, 'Output a list of available subcommands'), + 'parser' => array( + 'description' => __d($translationDomain, 'List subcommands for a command'), + 'arguments' => array( + 'command' => array( + 'help' => __d($translationDomain, 'The command name'), + 'required' => true, + ) + ) + ) + ))->addSubcommand('options', array( + 'help' => __d($translationDomain, 'Output a list of available options'), + 'parser' => array( + 'description' => __d($translationDomain, 'List options'), + 'arguments' => array( + 'command' => array( + 'help' => __d($translationDomain, 'The command name'), + 'required' => false, + ) + ) + ) + ))->epilog( + array( + 'This command is not intended to be called manually', + ) + ); + return $parser; + } + +/** + * Return a list of all commands + * + * @return array + */ + protected function _commands() { + $shellList = $this->_getShellList(); + unset($shellList['Completion']); + + $options = array(); + foreach ($shellList as $type => $commands) { + $prefix = ''; + if (!in_array($type, array('app', 'core', '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 + */ + protected function _subCommands($commandName) { + $Shell = $this->_getShell($commandName); + + if (!$Shell) { + return array(); + } + + $return = array(); + $taskMap = TaskCollection::normalizeObjectArray((array)$Shell->tasks); + foreach ($taskMap as $task => $properties) { + $return[] = $task; + } + + $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; + } + +/** + * _getShell + * + * @param mixed $commandName + */ + protected function _getShell($commandName) { + list($plugin, $name) = pluginSplit($commandName, true); + + if (!in_array($commandName, $this->_commands())) { + return false; + } + if ($plugin === 'CORE.' || $plugin === 'APP.') { + $plugin = ''; + } + + $name = Inflector::camelize($name); + $plugin = Inflector::camelize($plugin); + $class = $name . 'Shell'; + APP::uses($class, $plugin . 'Console/Command'); + + $Shell = new $class(); + $Shell->plugin = trim($plugin, '.'); + $Shell->initialize(); + $Shell->loadTasks(); + + return $Shell; + } + +/** + * Emit results as a string, space delimited + * + * @param array $options + */ + protected function _output($options = array()) { + if ($options) { + $this->out(implode($options, ' ')); + } + } +} From 79202ad8a0b36fec8f8db4c0c567b55d79335496 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Mon, 16 Sep 2013 14:11:27 +0200 Subject: [PATCH 43/58] Updated doc blocks --- app/Vendor/cake.bash | 50 ---- lib/Cake/Console/Command/CompletionShell.php | 52 +++- .../Console/Command/CommandListShellTest.php | 2 +- .../Console/Command/CompletionShellTest.php | 254 ++++++++++++++++++ 4 files changed, 298 insertions(+), 60 deletions(-) delete mode 100644 app/Vendor/cake.bash create mode 100644 lib/Cake/Test/Case/Console/Command/CompletionShellTest.php diff --git a/app/Vendor/cake.bash b/app/Vendor/cake.bash deleted file mode 100644 index 685108191..000000000 --- a/app/Vendor/cake.bash +++ /dev/null @@ -1,50 +0,0 @@ -# bash completion for CakePHP console - -_cake() -{ - local cur prev opts cake - COMPREPLY=() - cake="${COMP_WORDS[0]}" - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - - if [[ "$cur" == -* ]] ; then - if [[ ${COMP_CWORD} = 1 ]] ; then - opts=$(${cake} Completion options) - elif [[ ${COMP_CWORD} = 2 ]] ; then - opts=$(${cake} Completion options "${COMP_WORDS[1]}") - else - opts=$(${cake} Completion options "${COMP_WORDS[1]}" "${COMP_WORDS[2]}") - fi - - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi - - if [[ ${COMP_CWORD} = 1 ]] ; then - opts=$(${cake} Completion commands) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi - - if [[ ${COMP_CWORD} = 2 ]] ; then - opts=$(${cake} Completion subcommands $prev) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - if [[ $COMPREPLY = "" ]] ; then - COMPREPLY=( $(compgen -df -- ${cur}) ) - return 0 - fi - return 0 - fi - - - opts=$(${cake} Completion fuzzy "${COMP_WORDS[@]:1}") - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - if [[ $COMPREPLY = "" ]] ; then - COMPREPLY=( $(compgen -df -- ${cur}) ) - return 0 - fi - return 0; -} - -complete -F _cake cake Console/cake diff --git a/lib/Cake/Console/Command/CompletionShell.php b/lib/Cake/Console/Command/CompletionShell.php index da704a674..d5793d90f 100644 --- a/lib/Cake/Console/Command/CompletionShell.php +++ b/lib/Cake/Console/Command/CompletionShell.php @@ -1,27 +1,49 @@ out($this->OptionParser->help()); + return $this->out($this->OptionParser->help()); } /** * list commands + * + * @return void */ public function commands() { $options = $this->_commands(); @@ -30,6 +52,8 @@ class CompletionShell extends CommandListShell { /** * list options for the named command + * + * @return void */ public function options() { if (!$this->args) { @@ -57,6 +81,8 @@ class CompletionShell extends CommandListShell { /** * list subcommands for the named command + * + * @return void */ public function subCommands() { if (!$this->args) { @@ -69,6 +95,8 @@ class CompletionShell extends CommandListShell { /** * Guess autocomplete from the whole argument string + * + * @return void */ public function fuzzy() { return $this->_output(); @@ -76,6 +104,8 @@ class CompletionShell extends CommandListShell { /** * getOptionParser for _this_ shell + * + * @return ConsoleOptionParser */ public function getOptionParser() { $translationDomain = 'bash_completion'; @@ -186,19 +216,22 @@ class CompletionShell extends CommandListShell { } /** - * _getShell + * Get Shell instance for the given command * * @param mixed $commandName + * @return mixed */ protected function _getShell($commandName) { list($plugin, $name) = pluginSplit($commandName, true); + if ($plugin === 'CORE.' || $plugin === 'APP.' || $plugin === 'core.' || $plugin === 'app.') { + $commandName = $name; + $plugin = ''; + } + if (!in_array($commandName, $this->_commands())) { return false; } - if ($plugin === 'CORE.' || $plugin === 'APP.') { - $plugin = ''; - } $name = Inflector::camelize($name); $plugin = Inflector::camelize($plugin); @@ -217,10 +250,11 @@ class CompletionShell extends CommandListShell { * Emit results as a string, space delimited * * @param array $options + * @return void */ protected function _output($options = array()) { if ($options) { - $this->out(implode($options, ' ')); + return $this->out(implode($options, ' ')); } } } diff --git a/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php b/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php index ea81b3db1..2ab7ed089 100644 --- a/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php +++ b/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php @@ -98,7 +98,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..9471f66e0 --- /dev/null +++ b/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php @@ -0,0 +1,254 @@ +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) + ); + } + +/** + * 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); + } +} From 5c523a0cd556f0c28bc9b95110318fc92668b927 Mon Sep 17 00:00:00 2001 From: Cees-Jan Date: Mon, 23 Sep 2013 14:44:21 +0200 Subject: [PATCH 44/58] Updated the shell given @renansaddam pointers --- lib/Cake/Console/Command/CompletionShell.php | 44 +++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/Cake/Console/Command/CompletionShell.php b/lib/Cake/Console/Command/CompletionShell.php index d5793d90f..1b9807ca2 100644 --- a/lib/Cake/Console/Command/CompletionShell.php +++ b/lib/Cake/Console/Command/CompletionShell.php @@ -108,43 +108,41 @@ class CompletionShell extends CommandListShell { * @return ConsoleOptionParser */ public function getOptionParser() { - $translationDomain = 'bash_completion'; + $parser = parent::getOptionParser(); - $parser = AppShell::getOptionParser(); - - $parser->description(__d($translationDomain, 'Used by bash to autocomplete command name, options and arguments')) + $parser->description(__d('cake_console', 'Used by shells like bash to autocomplete command name, options and arguments')) ->addSubcommand('commands', array( - 'help' => __d($translationDomain, 'Output a list of available commands'), + 'help' => __d('cake_console', 'Output a list of available commands'), 'parser' => array( - 'description' => __d($translationDomain, 'List all availables'), + 'description' => __d('cake_console', 'List all availables'), 'arguments' => array( ) ) ))->addSubcommand('subcommands', array( - 'help' => __d($translationDomain, 'Output a list of available subcommands'), + 'help' => __d('cake_console', 'Output a list of available subcommands'), 'parser' => array( - 'description' => __d($translationDomain, 'List subcommands for a command'), + 'description' => __d('cake_console', 'List subcommands for a command'), 'arguments' => array( 'command' => array( - 'help' => __d($translationDomain, 'The command name'), + 'help' => __d('cake_console', 'The command name'), 'required' => true, ) ) ) ))->addSubcommand('options', array( - 'help' => __d($translationDomain, 'Output a list of available options'), + 'help' => __d('cake_console', 'Output a list of available options'), 'parser' => array( - 'description' => __d($translationDomain, 'List options'), + 'description' => __d('cake_console', 'List options'), 'arguments' => array( 'command' => array( - 'help' => __d($translationDomain, 'The command name'), + 'help' => __d('cake_console', 'The command name'), 'required' => false, ) ) ) ))->epilog( array( - 'This command is not intended to be called manually', + __d('cake_console', 'This command is not intended to be called manually'), ) ); return $parser; @@ -162,7 +160,7 @@ class CompletionShell extends CommandListShell { $options = array(); foreach ($shellList as $type => $commands) { $prefix = ''; - if (!in_array($type, array('app', 'core', 'APP', 'CORE'))) { + if (!in_array(strtolower($type), array('app', 'core'))) { $prefix = $type . '.'; } @@ -187,12 +185,8 @@ class CompletionShell extends CommandListShell { return array(); } - $return = array(); $taskMap = TaskCollection::normalizeObjectArray((array)$Shell->tasks); - foreach ($taskMap as $task => $properties) { - $return[] = $task; - } - + $return = array_keys($taskMap); $return = array_map('Inflector::underscore', $return); $ShellReflection = new ReflectionClass('AppShell'); @@ -222,11 +216,11 @@ class CompletionShell extends CommandListShell { * @return mixed */ protected function _getShell($commandName) { - list($plugin, $name) = pluginSplit($commandName, true); + list($pluginDot, $name) = pluginSplit($commandName, true); - if ($plugin === 'CORE.' || $plugin === 'APP.' || $plugin === 'core.' || $plugin === 'app.') { + if (in_array(strtolower($pluginDot), array('app.', 'core.'))) { $commandName = $name; - $plugin = ''; + $pluginDot = ''; } if (!in_array($commandName, $this->_commands())) { @@ -234,12 +228,12 @@ class CompletionShell extends CommandListShell { } $name = Inflector::camelize($name); - $plugin = Inflector::camelize($plugin); + $pluginDot = Inflector::camelize($pluginDot); $class = $name . 'Shell'; - APP::uses($class, $plugin . 'Console/Command'); + APP::uses($class, $pluginDot . 'Console/Command'); $Shell = new $class(); - $Shell->plugin = trim($plugin, '.'); + $Shell->plugin = trim($pluginDot, '.'); $Shell->initialize(); $Shell->loadTasks(); From ac9b7f3882ba5ba8ab240508e46fc8d0ff30b056 Mon Sep 17 00:00:00 2001 From: wyrihaximus Date: Sat, 28 Sep 2013 17:42:28 +0200 Subject: [PATCH 45/58] Refactored the shell reflection portion into a seperate task. --- lib/Cake/Console/Command/CommandListShell.php | 51 +--- lib/Cake/Console/Command/CompletionShell.php | 131 ++-------- lib/Cake/Console/Command/Task/CommandTask.php | 183 +++++++++++++ .../Console/Command/CommandListShellTest.php | 7 + .../Console/Command/CompletionShellTest.php | 7 + .../Console/Command/Task/CommandTaskTest.php | 240 ++++++++++++++++++ 6 files changed, 461 insertions(+), 158 deletions(-) create mode 100644 lib/Cake/Console/Command/Task/CommandTask.php create mode 100644 lib/Cake/Test/Case/Console/Command/Task/CommandTaskTest.php 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 index 1b9807ca2..94e809efa 100644 --- a/lib/Cake/Console/Command/CompletionShell.php +++ b/lib/Cake/Console/Command/CompletionShell.php @@ -14,14 +14,21 @@ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ -App::uses('CommandListShell', 'Console/Command'); +App::uses('AppShell', 'Console/Command'); /** * Provide command completion shells such as bash. * * @package Cake.Console.Command */ -class CompletionShell extends CommandListShell { +class CompletionShell extends AppShell { + +/** + * Contains tasks to load and instantiate + * + * @var array + */ + public $tasks = array('Command'); /** * Echo no header by overriding the startup method @@ -37,7 +44,7 @@ class CompletionShell extends CommandListShell { * @return void */ public function main() { - return $this->out($this->OptionParser->help()); + return $this->out($this->getOptionParser()->help()); } /** @@ -46,7 +53,7 @@ class CompletionShell extends CommandListShell { * @return void */ public function commands() { - $options = $this->_commands(); + $options = $this->Command->commands(); return $this->_output($options); } @@ -56,26 +63,12 @@ class CompletionShell extends CommandListShell { * @return void */ public function options() { - if (!$this->args) { - $parser = new ConsoleOptionParser(); - } else { - $Shell = $this->_getShell($this->args[0]); - if (!$Shell) { - $parser = new ConsoleOptionParser(); - } else { - $parser = $Shell->getOptionParser(); - } + $commandName = ''; + if (!empty($this->args[0])) { + $commandName = $this->args[0]; } + $options = $this->Command->options($commandName); - $options = array(); - $array = $parser->options(); - foreach ($array as $name => $obj) { - $options[] = "--$name"; - $short = $obj->short(); - if ($short) { - $options[] = "-$short"; - } - } return $this->_output($options); } @@ -89,7 +82,7 @@ class CompletionShell extends CommandListShell { return $this->_output(); } - $options = $this->_subCommands($this->args[0]); + $options = $this->Command->subCommands($this->args[0]); return $this->_output($options); } @@ -148,98 +141,6 @@ class CompletionShell extends CommandListShell { return $parser; } -/** - * Return a list of all commands - * - * @return array - */ - protected function _commands() { - $shellList = $this->_getShellList(); - unset($shellList['Completion']); - - $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 - */ - protected 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 - */ - protected 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(); - $Shell->loadTasks(); - - return $Shell; - } - /** * Emit results as a string, space delimited * 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/Test/Case/Console/Command/CommandListShellTest.php b/lib/Cake/Test/Case/Console/Command/CommandListShellTest.php index 2ab7ed089..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) + ); } /** diff --git a/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php b/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php index 9471f66e0..7ef3ef53a 100644 --- a/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php +++ b/lib/Cake/Test/Case/Console/Command/CompletionShellTest.php @@ -22,6 +22,7 @@ App::uses('CompletionShell', 'Console/Command'); App::uses('ConsoleOutput', 'Console'); App::uses('ConsoleInput', 'Console'); App::uses('Shell', 'Console'); +App::uses('CommandTask', 'Console/Command/Task'); /** * Class TestCompletionStringOutput @@ -70,6 +71,12 @@ class CompletionShellTest extends CakeTestCase { array('in', '_stop', 'clear'), array($out, $out, $in) ); + + $this->Shell->Command = $this->getMock( + 'CommandTask', + array('in', '_stop', 'clear'), + array($out, $out, $in) + ); } /** 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); + } + +} From b9003e5f7c9b4710981c7037a2703f35c6a958a8 Mon Sep 17 00:00:00 2001 From: Bryan Crowe Date: Wed, 2 Oct 2013 22:45:22 -0400 Subject: [PATCH 46/58] Allow Folder::addPathElement() to accept arrays, also corrected docblock --- lib/Cake/Test/Case/Utility/FolderTest.php | 17 +++++++++++++++-- lib/Cake/Utility/Folder.php | 6 ++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/Cake/Test/Case/Utility/FolderTest.php b/lib/Cake/Test/Case/Utility/FolderTest.php index 5bf9fd840..f9c35f718 100644 --- a/lib/Cake/Test/Case/Utility/FolderTest.php +++ b/lib/Cake/Test/Case/Utility/FolderTest.php @@ -345,11 +345,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/Utility/Folder.php b/lib/Cake/Utility/Folder.php index ca6c4128d..4e47bff13 100644 --- a/lib/Cake/Utility/Folder.php +++ b/lib/Cake/Utility/Folder.php @@ -321,12 +321,14 @@ class Folder { * Returns $path with $element added, with correct slash in-between. * * @param string $path Path - * @param string $element Element to and at end of path + * @param string|array $element Element to add at end of path * @return string Combined path * @link http://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::addPathElement */ public static function addPathElement($path, $element) { - return rtrim($path, DS) . DS . $element; + $element = (array)$element; + array_unshift($element, rtrim($path, DS)); + return implode(DS, $element) } /** From 0cffcd1eca2e439e0ae8abd40be9b64497ede814 Mon Sep 17 00:00:00 2001 From: Bryan Crowe Date: Wed, 2 Oct 2013 22:55:09 -0400 Subject: [PATCH 47/58] Added missing semi-colon in return --- lib/Cake/Utility/Folder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/Utility/Folder.php b/lib/Cake/Utility/Folder.php index 4e47bff13..7cff4b548 100644 --- a/lib/Cake/Utility/Folder.php +++ b/lib/Cake/Utility/Folder.php @@ -328,7 +328,7 @@ class Folder { public static function addPathElement($path, $element) { $element = (array)$element; array_unshift($element, rtrim($path, DS)); - return implode(DS, $element) + return implode(DS, $element); } /** From eb9877030496925740458b25cc50080f1eced681 Mon Sep 17 00:00:00 2001 From: euromark Date: Fri, 4 Oct 2013 00:35:56 +0200 Subject: [PATCH 48/58] Remove leftover of pageTitle. --- .../Test/Case/Controller/ControllerTest.php | 1 - .../Test/Case/View/Helper/RssHelperTest.php | 27 ++----------------- lib/Cake/Test/Case/View/ViewTest.php | 13 --------- lib/Cake/View/Helper/RssHelper.php | 6 ++--- 4 files changed, 5 insertions(+), 42 deletions(-) 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/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 e068964b6..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. * 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'] = ''; } From 5d30cb15591af89b33fcbb61c10e4b96452f4ff6 Mon Sep 17 00:00:00 2001 From: Kamisama Date: Fri, 4 Oct 2013 00:05:01 -0400 Subject: [PATCH 49/58] Improve serializer engine assignment code --- lib/Cake/Cache/Engine/MemcachedEngine.php | 31 ++++++----------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 3eda6fe72..78ccda1d8 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -135,35 +135,20 @@ class MemcachedEngine extends CacheEngine { protected function _setOptions() { $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); - if (!isset($this->_serializers[$this->settings['serialize']])) { + $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', $this->settings['serialize']) + __d('cake_dev', '%s is not a valid serializer engine for Memcached', $serializer) ); } - $serializer = $this->_serializers['php']; - switch($this->settings['serialize']) { - case 'igbinary': - if (Memcached::HAVE_IGBINARY) { - $serializer = $this->_serializers['igbinary']; - } else { - throw new CacheException( - __d('cake_dev', 'Memcached extension is not compiled with igbinary support') - ); - } - break; - case 'json': - if (Memcached::HAVE_JSON) { - $serializer = $this->_serializers['json']; - } else { - throw new CacheException( - __d('cake_dev', 'Memcached extension is not compiled with json support') - ); - } - break; + 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, $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')) { From f146d4334a14b34778bfd35515e43aac7ed126d4 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 5 Oct 2013 10:47:07 -0300 Subject: [PATCH 50/58] Add Cache::remember() This method provides a really simple approach to read-through caching. A callable or Closure can be used to provide results when the cache is empty. --- lib/Cake/Cache/Cache.php | 23 +++++++++++++++++++++++ lib/Cake/Test/Case/Cache/CacheTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/lib/Cake/Cache/Cache.php b/lib/Cake/Cache/Cache.php index c7c593916..875c307e5 100644 --- a/lib/Cake/Cache/Cache.php +++ b/lib/Cake/Cache/Cache.php @@ -542,4 +542,27 @@ 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. + * + * @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. + * @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/Test/Case/Cache/CacheTest.php b/lib/Cake/Test/Case/Cache/CacheTest.php index 9b4279551..cd00bbba2 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', [$this, 'cacher'], 'default'); + $this->assertEquals($expected, $result); + + $this->_count = 1; + $result = Cache::remember('test_key', [$this, 'cacher'], 'default'); + $this->assertEquals($expected, $result); + } + +/** + * Method for testing Cache::remember() + * + * @return string + */ + public function cacher() { + return 'This is some data ' . $this->_count; + } } From cb91a2d33c7279542e27b61faa3ea49463ec7574 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 5 Oct 2013 10:56:55 -0300 Subject: [PATCH 51/58] Add example to documentation for Cache::remember() --- lib/Cake/Cache/Cache.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/Cake/Cache/Cache.php b/lib/Cake/Cache/Cache.php index 875c307e5..80479de80 100644 --- a/lib/Cake/Cache/Cache.php +++ b/lib/Cake/Cache/Cache.php @@ -549,9 +549,20 @@ class Cache { * 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. + * 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. */ From 1ce89545bd9f267713d6238e97387d9412cc25c6 Mon Sep 17 00:00:00 2001 From: mark_story Date: Sat, 5 Oct 2013 14:40:23 -0300 Subject: [PATCH 52/58] Fix 5.4 only syntax. --- lib/Cake/Test/Case/Cache/CacheTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Cake/Test/Case/Cache/CacheTest.php b/lib/Cake/Test/Case/Cache/CacheTest.php index cd00bbba2..43bc3c5ff 100644 --- a/lib/Cake/Test/Case/Cache/CacheTest.php +++ b/lib/Cake/Test/Case/Cache/CacheTest.php @@ -501,11 +501,11 @@ class CacheTest extends CakeTestCase { */ public function testRemember() { $expected = 'This is some data 0'; - $result = Cache::remember('test_key', [$this, 'cacher'], 'default'); + $result = Cache::remember('test_key', array($this, 'cacher'), 'default'); $this->assertEquals($expected, $result); $this->_count = 1; - $result = Cache::remember('test_key', [$this, 'cacher'], 'default'); + $result = Cache::remember('test_key', array($this, 'cacher'), 'default'); $this->assertEquals($expected, $result); } From e85ac9339e1b5a938ba98e7f4e81998250ec489c Mon Sep 17 00:00:00 2001 From: mark_story Date: Sun, 6 Oct 2013 10:35:21 -0300 Subject: [PATCH 53/58] Fix coding standards in comment. --- lib/Cake/Cache/Cache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/Cache/Cache.php b/lib/Cake/Cache/Cache.php index 80479de80..acbd5dc81 100644 --- a/lib/Cake/Cache/Cache.php +++ b/lib/Cake/Cache/Cache.php @@ -555,7 +555,7 @@ class Cache { * * {{{ * $model = $this; - * $results = Cache::remember('all_articles', function () use ($model) { + * $results = Cache::remember('all_articles', function() use ($model) { * return $model->find('all'); * }); * }}} From c621985f69930d97beb4778bbc4d385f869dc954 Mon Sep 17 00:00:00 2001 From: euromark Date: Mon, 7 Oct 2013 23:45:27 +0200 Subject: [PATCH 54/58] Fix whitelist to be modifiable from behaviors to work with validate. --- lib/Cake/Model/ModelValidator.php | 6 ++- .../Test/Case/Model/ModelValidationTest.php | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/Cake/Model/ModelValidator.php b/lib/Cake/Model/ModelValidator.php index bbc54c8da..70674938e 100644 --- a/lib/Cake/Model/ModelValidator.php +++ b/lib/Cake/Model/ModelValidator.php @@ -249,7 +249,11 @@ 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'])) { + $fieldList = $options['fieldList']; + } + $exists = $model->exists(); $methods = $this->getMethods(); $fields = $this->_validationList($fieldList); diff --git a/lib/Cake/Test/Case/Model/ModelValidationTest.php b/lib/Cake/Test/Case/Model/ModelValidationTest.php index d313a33df..e8114b057 100644 --- a/lib/Cake/Test/Case/Model/ModelValidationTest.php +++ b/lib/Cake/Test/Case/Model/ModelValidationTest.php @@ -612,6 +612,30 @@ class ModelValidationTest extends BaseModelTest { $this->assertEquals(0, $joinRecords, 'Records were saved on the join table. %s'); } + public function testValidateWithFieldListAndBehavior() { + $TestModel = new ValidationTest1(); + $TestModel->validate = array( + 'title' => array( + 'rule' => 'alphaNumeric', + 'required' => true + ), + 'name' => array( + 'rule' => 'alphaNumeric', + 'required' => true + )); + $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 +2404,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; + } + } + +} \ No newline at end of file From 35ca40635e9616ff083084d60915ad3a13a070be Mon Sep 17 00:00:00 2001 From: euromark Date: Tue, 8 Oct 2013 00:32:11 +0200 Subject: [PATCH 55/58] Add doc block --- lib/Cake/Test/Case/Model/ModelValidationTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Cake/Test/Case/Model/ModelValidationTest.php b/lib/Cake/Test/Case/Model/ModelValidationTest.php index e8114b057..33560194e 100644 --- a/lib/Cake/Test/Case/Model/ModelValidationTest.php +++ b/lib/Cake/Test/Case/Model/ModelValidationTest.php @@ -612,6 +612,12 @@ 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( @@ -2421,4 +2427,4 @@ class ValidationRuleBehavior extends ModelBehavior { } } -} \ No newline at end of file +} From 381a8c1216804eb0739cc069e49a3640d8e48015 Mon Sep 17 00:00:00 2001 From: euromark Date: Tue, 8 Oct 2013 00:47:47 +0200 Subject: [PATCH 56/58] Simplify test. --- lib/Cake/Test/Case/Model/ModelValidationTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Cake/Test/Case/Model/ModelValidationTest.php b/lib/Cake/Test/Case/Model/ModelValidationTest.php index 33560194e..7298513c4 100644 --- a/lib/Cake/Test/Case/Model/ModelValidationTest.php +++ b/lib/Cake/Test/Case/Model/ModelValidationTest.php @@ -622,12 +622,10 @@ class ModelValidationTest extends BaseModelTest { $TestModel = new ValidationTest1(); $TestModel->validate = array( 'title' => array( - 'rule' => 'alphaNumeric', - 'required' => true + 'rule' => 'notEmpty', ), 'name' => array( - 'rule' => 'alphaNumeric', - 'required' => true + 'rule' => 'notEmpty', )); $TestModel->Behaviors->attach('ValidationRule', array('fields' => array('name'))); From eb852a02476a3cde5a9e5b64385c95015e732f6b Mon Sep 17 00:00:00 2001 From: euromark Date: Tue, 8 Oct 2013 20:16:55 +0200 Subject: [PATCH 57/58] Strict checks where applicable. --- lib/Cake/Model/Behavior/TreeBehavior.php | 4 ++-- lib/Cake/Model/Model.php | 2 +- lib/Cake/Network/CakeResponse.php | 2 +- lib/Cake/Routing/Router.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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 670b71a6d..3b9563441 100644 --- a/lib/Cake/Model/Model.php +++ b/lib/Cake/Model/Model.php @@ -1849,7 +1849,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); if ($isUUID && $primaryAdded) { 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 375e08d3f..b91a6d5a6 100644 --- a/lib/Cake/Routing/Router.php +++ b/lib/Cake/Routing/Router.php @@ -1038,7 +1038,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; } From f9c1c2aa7fb445d38b13f6c70d76a714f8945d18 Mon Sep 17 00:00:00 2001 From: ADmad Date: Wed, 9 Oct 2013 00:04:18 +0530 Subject: [PATCH 58/58] Optimized whitelist jugglery. --- lib/Cake/Model/ModelValidator.php | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/lib/Cake/Model/ModelValidator.php b/lib/Cake/Model/ModelValidator.php index 70674938e..508383df8 100644 --- a/lib/Cake/Model/ModelValidator.php +++ b/lib/Cake/Model/ModelValidator.php @@ -251,7 +251,11 @@ class ModelValidator implements ArrayAccess, IteratorAggregate, Countable { $fieldList = $model->whitelist; if (empty($fieldList) && !empty($options['fieldList'])) { - $fieldList = $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(); @@ -380,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]; }