From 05384df10bb5cc9667b454dfb0dfcfa634cdf890 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Fri, 25 May 2018 13:22:48 +1200 Subject: [PATCH 01/12] Add new CacheProxy --- cache/Cache.php | 10 ++- cache/CacheProxy.php | 149 ++++++++++++++++++++++++++++++++++++++ tests/cache/CacheTest.php | 30 +++++++- 3 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 cache/CacheProxy.php diff --git a/cache/Cache.php b/cache/Cache.php index 672355261..654a7540e 100644 --- a/cache/Cache.php +++ b/cache/Cache.php @@ -146,7 +146,7 @@ class SS_Cache { * @param string $frontend (optional) The type of Zend_Cache frontend * @param array $frontendOptions (optional) Any frontend options to use. * - * @return Zend_Cache_Core The cache object + * @return CacheProxy */ public static function factory($for, $frontend='Output', $frontendOptions=null) { self::init(); @@ -188,8 +188,10 @@ class SS_Cache { require_once 'Zend/Cache.php'; - return Zend_Cache::factory( - $frontend, $backend[0], $frontendOptions, $backend[1] - ); + $container = Zend_Cache::factory( + $frontend, $backend[0], $frontendOptions, $backend[1] + ); + + return Injector::inst()->createWithArgs('CacheProxy', [$container]); } } diff --git a/cache/CacheProxy.php b/cache/CacheProxy.php new file mode 100644 index 000000000..3f2eafda2 --- /dev/null +++ b/cache/CacheProxy.php @@ -0,0 +1,149 @@ +container = $container; + + parent::__construct(); + } + + /** + * @param array $directives + */ + public function setDirectives($directives) + { + $this->container->setDirectives($directives); + } + + public function setConfig(Zend_Config $config) + { + return $this->container->setConfig($config); + } + + public function setBackend(Zend_Cache_Backend $backendObject) + { + return $this->container->setBackend($backendObject); + } + + public function getBackend() + { + return $this->container->getBackend(); + } + + /** + * @param string $name + * @param mixed $value + */ + public function setOption($name, $value) + { + $this->container->setOption($name, $value); + } + + public function getOption($name) + { + return $this->container->getOption($name); + } + + public function setLifetime($newLifetime) + { + return $this->container->setLifetime($newLifetime); + } + + public function getIds() + { + return $this->container->getIds(); + } + + public function getTags() + { + return $this->container->getTags(); + } + + public function getIdsMatchingTags($tags = array()) + { + return $this->container->getIdsMatchingTags($tags); + } + + public function getIdsNotMatchingTags($tags = array()) + { + return $this->container->getIdsNotMatchingTags($tags); + } + + public function getIdsMatchingAnyTags($tags = array()) + { + return $this->container->getIdsMatchingAnyTags($tags); + } + + public function getFillingPercentage() + { + return $this->container->getFillingPercentage(); + } + + public function getMetadatas($id) + { + return $this->container->getMetadatas($this->createKey($id)); + } + + public function touch($id, $extraLifetime) + { + return $this->container->touch($this->createKey($id), $extraLifetime); + } + + public function load($id, $doNotTestCacheValidity = false, $doNotUnserialize = false) + { + return $this->container->load($this->createKey($id), $doNotTestCacheValidity, $doNotUnserialize); + } + + public function test($id) + { + return $this->container->test($this->createKey($id)); + } + + public function save($data, $id = null, $tags = array(), $specificLifetime = false, $priority = 8) + { + return $this->container->save( + $data, + $this->createKey($id), + $tags, + $specificLifetime, + $priority + ); + } + + public function remove($id) + { + return $this->container->remove($this->createKey($id)); + } + + public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) + { + return $this->container->clean($mode, $tags); + } + + /** + * Creates a dynamic key based on versioned state + * @param $key + * @return string + */ + protected function createKey($key) + { + $state = Versioned::get_reading_mode(); + if ($state) { + return $key . '_' . md5($state); + } + return $key; + } +} \ No newline at end of file diff --git a/tests/cache/CacheTest.php b/tests/cache/CacheTest.php index 9ab385df5..5a5467d24 100644 --- a/tests/cache/CacheTest.php +++ b/tests/cache/CacheTest.php @@ -2,7 +2,12 @@ class CacheTest extends SapphireTest { - public function testCacheBasics() { + public function setUpOnce() { + parent::setUpOnce(); + Versioned::set_reading_mode('Stage.Live'); + } + + public function testCacheBasics() { $cache = SS_Cache::factory('test'); $cache->save('Good', 'cachekey'); @@ -64,5 +69,28 @@ class CacheTest extends SapphireTest { $this->assertEquals(1200, $cache->getOption('lifetime')); } + public function testVersionedCacheSegmentation() { + $cacheInstance = SS_Cache::factory('versioned'); + $cacheInstance->clean(); + + Versioned::set_reading_mode('Stage.Live'); + $result = $cacheInstance->load('test'); + $this->assertFalse($result); + $cacheInstance->save('uncle', 'test'); + $this->assertEquals('uncle', $cacheInstance->load('test')); + Versioned::set_reading_mode('Stage.Stage'); + $this->assertFalse($cacheInstance->load('test')); + $cacheInstance->save('cheese', 'test'); + $cacheInstance->save('bar', 'foo'); + $this->assertEquals('cheese', $cacheInstance->load('test')); + $this->assertEquals('bar', $cacheInstance->load('foo')); + Versioned::set_reading_mode('Stage.Live'); + $this->assertFalse($cacheInstance->load('foo')); + $this->assertEquals('uncle', $cacheInstance->load('test')); + + $cacheInstance->clean(); + + } + } From 5583565480a9d0c1f078f22fd2b3d87c09fdc7da Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Sat, 26 May 2018 21:53:03 +1200 Subject: [PATCH 02/12] Add disable container --- cache/Cache.php | 6 +++++- cache/CacheProxy.php | 9 +++------ tests/cache/CacheTest.php | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/cache/Cache.php b/cache/Cache.php index 654a7540e..477eb86f4 100644 --- a/cache/Cache.php +++ b/cache/Cache.php @@ -146,7 +146,7 @@ class SS_Cache { * @param string $frontend (optional) The type of Zend_Cache frontend * @param array $frontendOptions (optional) Any frontend options to use. * - * @return CacheProxy + * @return CacheProxy|Zend_Cache_Core */ public static function factory($for, $frontend='Output', $frontendOptions=null) { self::init(); @@ -192,6 +192,10 @@ class SS_Cache { $frontend, $backend[0], $frontendOptions, $backend[1] ); + if (isset($frontendOptions['disable-container']) && $frontendOptions['disable-container']) { + return $container; + } + return Injector::inst()->createWithArgs('CacheProxy', [$container]); } } diff --git a/cache/CacheProxy.php b/cache/CacheProxy.php index 3f2eafda2..dd84bbece 100644 --- a/cache/CacheProxy.php +++ b/cache/CacheProxy.php @@ -2,8 +2,7 @@ require_once 'Zend/Cache.php'; -class CacheProxy extends Zend_Cache_Core -{ +class CacheProxy extends Zend_Cache_Core { /** * @var Zend_Cache_Backend|Zend_Cache_Backend_ExtendedInterface */ @@ -13,8 +12,7 @@ class CacheProxy extends Zend_Cache_Core * CacheProxy constructor. * @param Zend_Cache_Core $container */ - public function __construct(Zend_Cache_Core $container) - { + public function __construct(Zend_Cache_Core $container) { $this->container = $container; parent::__construct(); @@ -23,8 +21,7 @@ class CacheProxy extends Zend_Cache_Core /** * @param array $directives */ - public function setDirectives($directives) - { + public function setDirectives($directives) { $this->container->setDirectives($directives); } diff --git a/tests/cache/CacheTest.php b/tests/cache/CacheTest.php index 5a5467d24..9b6f54e95 100644 --- a/tests/cache/CacheTest.php +++ b/tests/cache/CacheTest.php @@ -92,5 +92,27 @@ class CacheTest extends SapphireTest { } + public function testDisableVersionedCacheSegmentation() { + $cacheInstance = SS_Cache::factory('versioned_disabled', 'Output', ['disable-container' => true]); + $cacheInstance->clean(); + + Versioned::set_reading_mode('Stage.Live'); + $result = $cacheInstance->load('test'); + $this->assertFalse($result); + $cacheInstance->save('uncle', 'test'); + $this->assertEquals('uncle', $cacheInstance->load('test')); + Versioned::set_reading_mode('Stage.Stage'); + $this->assertEquals('uncle', $cacheInstance->load('test')); + $cacheInstance->save('cheese', 'test'); + $cacheInstance->save('bar', 'foo'); + $this->assertEquals('cheese', $cacheInstance->load('test')); + $this->assertEquals('bar', $cacheInstance->load('foo')); + Versioned::set_reading_mode('Stage.Live'); + $this->assertEquals('bar', $cacheInstance->load('foo')); + $this->assertEquals('cheese', $cacheInstance->load('test')); + + $cacheInstance->clean(); + } + } From 265ad700118ee126183c860802d4f45c757bb0dc Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 30 May 2018 16:07:31 +1200 Subject: [PATCH 03/12] Refactor per chillu, reverse linting --- cache/CacheProxy.php | 123 ++++++++++++++++---------------------- tests/cache/CacheTest.php | 40 ++++++------- 2 files changed, 70 insertions(+), 93 deletions(-) diff --git a/cache/CacheProxy.php b/cache/CacheProxy.php index dd84bbece..07a75a33e 100644 --- a/cache/CacheProxy.php +++ b/cache/CacheProxy.php @@ -2,141 +2,118 @@ require_once 'Zend/Cache.php'; +/** + * A decorator for a Zend_Cache_Backend cache service that mutates cache keys + * dynamically depending on versioned state + */ class CacheProxy extends Zend_Cache_Core { /** * @var Zend_Cache_Backend|Zend_Cache_Backend_ExtendedInterface */ - protected $container; + protected $cache; /** * CacheProxy constructor. - * @param Zend_Cache_Core $container + * @param Zend_Cache_Core $cache */ - public function __construct(Zend_Cache_Core $container) { - $this->container = $container; + public function __construct(Zend_Cache_Core $cache) { + $this->cache = $cache; parent::__construct(); } - /** - * @param array $directives - */ public function setDirectives($directives) { - $this->container->setDirectives($directives); + $this->cache->setDirectives($directives); } - public function setConfig(Zend_Config $config) - { - return $this->container->setConfig($config); + public function setConfig(Zend_Config $config) { + return $this->cache->setConfig($config); } - public function setBackend(Zend_Cache_Backend $backendObject) - { - return $this->container->setBackend($backendObject); + public function setBackend(Zend_Cache_Backend $backendObject) { + return $this->cache->setBackend($backendObject); } - public function getBackend() - { - return $this->container->getBackend(); + public function getBackend() { + return $this->cache->getBackend(); } - /** - * @param string $name - * @param mixed $value - */ - public function setOption($name, $value) - { - $this->container->setOption($name, $value); + public function setOption($name, $value) { + $this->cache->setOption($name, $value); } - public function getOption($name) - { - return $this->container->getOption($name); + public function getOption($name) { + return $this->cache->getOption($name); } - public function setLifetime($newLifetime) - { - return $this->container->setLifetime($newLifetime); + public function setLifetime($newLifetime) { + return $this->cache->setLifetime($newLifetime); } - public function getIds() - { - return $this->container->getIds(); + public function getIds() { + return $this->cache->getIds(); } - public function getTags() - { - return $this->container->getTags(); + public function getTags() { + return $this->cache->getTags(); } - public function getIdsMatchingTags($tags = array()) - { - return $this->container->getIdsMatchingTags($tags); + public function getIdsMatchingTags($tags = array()) { + return $this->cache->getIdsMatchingTags($tags); } - public function getIdsNotMatchingTags($tags = array()) - { - return $this->container->getIdsNotMatchingTags($tags); + public function getIdsNotMatchingTags($tags = array()) { + return $this->cache->getIdsNotMatchingTags($tags); } - public function getIdsMatchingAnyTags($tags = array()) - { - return $this->container->getIdsMatchingAnyTags($tags); + public function getIdsMatchingAnyTags($tags = array()) { + return $this->cache->getIdsMatchingAnyTags($tags); } - public function getFillingPercentage() - { - return $this->container->getFillingPercentage(); + public function getFillingPercentage() { + return $this->cache->getFillingPercentage(); } - public function getMetadatas($id) - { - return $this->container->getMetadatas($this->createKey($id)); + public function getMetadatas($id) { + return $this->cache->getMetadatas($this->getKeyID($id)); } - public function touch($id, $extraLifetime) - { - return $this->container->touch($this->createKey($id), $extraLifetime); + public function touch($id, $extraLifetime) { + return $this->cache->touch($this->getKeyID($id), $extraLifetime); } - public function load($id, $doNotTestCacheValidity = false, $doNotUnserialize = false) - { - return $this->container->load($this->createKey($id), $doNotTestCacheValidity, $doNotUnserialize); + public function load($id, $doNotTestCacheValidity = false, $doNotUnserialize = false) { + return $this->cache->load($this->getKeyID($id), $doNotTestCacheValidity, $doNotUnserialize); } - public function test($id) - { - return $this->container->test($this->createKey($id)); + public function test($id) { + return $this->cache->test($this->getKeyID($id)); } - public function save($data, $id = null, $tags = array(), $specificLifetime = false, $priority = 8) - { - return $this->container->save( + public function save($data, $id = null, $tags = array(), $specificLifetime = false, $priority = 8) { + return $this->cache->save( $data, - $this->createKey($id), + $this->getKeyID($id), $tags, $specificLifetime, $priority ); } - public function remove($id) - { - return $this->container->remove($this->createKey($id)); + public function remove($id) { + return $this->cache->remove($this->getKeyID($id)); } - public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) - { - return $this->container->clean($mode, $tags); + public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) { + return $this->cache->clean($mode, $tags); } /** * Creates a dynamic key based on versioned state - * @param $key + * @param string $key * @return string */ - protected function createKey($key) - { + protected function getKeyID($key) { $state = Versioned::get_reading_mode(); if ($state) { return $key . '_' . md5($state); diff --git a/tests/cache/CacheTest.php b/tests/cache/CacheTest.php index 9b6f54e95..feda314f6 100644 --- a/tests/cache/CacheTest.php +++ b/tests/cache/CacheTest.php @@ -74,19 +74,19 @@ class CacheTest extends SapphireTest { $cacheInstance->clean(); Versioned::set_reading_mode('Stage.Live'); - $result = $cacheInstance->load('test'); + $result = $cacheInstance->load('shared_key'); $this->assertFalse($result); - $cacheInstance->save('uncle', 'test'); - $this->assertEquals('uncle', $cacheInstance->load('test')); + $cacheInstance->save('uncle', 'shared_key'); + $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key is cached on LIVE'); Versioned::set_reading_mode('Stage.Stage'); - $this->assertFalse($cacheInstance->load('test')); - $cacheInstance->save('cheese', 'test'); - $cacheInstance->save('bar', 'foo'); - $this->assertEquals('cheese', $cacheInstance->load('test')); - $this->assertEquals('bar', $cacheInstance->load('foo')); + $this->assertFalse($cacheInstance->load('shared_key'), 'Shared key does not exist on STAGE'); + $cacheInstance->save('cheese', 'shared_key'); + $cacheInstance->save('bar', 'stage_key'); + $this->assertEquals('cheese', $cacheInstance->load('shared_key'), 'Shared key has its own value on STAGE'); + $this->assertEquals('bar', $cacheInstance->load('stage_key'), 'New key is cached on STAGE'); Versioned::set_reading_mode('Stage.Live'); - $this->assertFalse($cacheInstance->load('foo')); - $this->assertEquals('uncle', $cacheInstance->load('test')); + $this->assertFalse($cacheInstance->load('stage_key'), 'New key does not exist on LIVE'); + $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key retains its own value on LIVE'); $cacheInstance->clean(); @@ -97,19 +97,19 @@ class CacheTest extends SapphireTest { $cacheInstance->clean(); Versioned::set_reading_mode('Stage.Live'); - $result = $cacheInstance->load('test'); + $result = $cacheInstance->load('shared_key'); $this->assertFalse($result); - $cacheInstance->save('uncle', 'test'); - $this->assertEquals('uncle', $cacheInstance->load('test')); + $cacheInstance->save('uncle', 'shared_key'); + $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key is cached on LIVE'); Versioned::set_reading_mode('Stage.Stage'); - $this->assertEquals('uncle', $cacheInstance->load('test')); - $cacheInstance->save('cheese', 'test'); - $cacheInstance->save('bar', 'foo'); - $this->assertEquals('cheese', $cacheInstance->load('test')); - $this->assertEquals('bar', $cacheInstance->load('foo')); + $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key is same on STAGE'); + $cacheInstance->save('cheese', 'shared_key'); + $cacheInstance->save('bar', 'stage_key'); + $this->assertEquals('cheese', $cacheInstance->load('shared_key'), 'Shared key is overwritten on STAGE'); + $this->assertEquals('bar', $cacheInstance->load('stage_key'), 'New key is written on STAGE'); Versioned::set_reading_mode('Stage.Live'); - $this->assertEquals('bar', $cacheInstance->load('foo')); - $this->assertEquals('cheese', $cacheInstance->load('test')); + $this->assertEquals('bar', $cacheInstance->load('stage_key'), 'New key has same value on LIVE'); + $this->assertEquals('cheese', $cacheInstance->load('shared_key'), 'New value for existing key is same on LIVE'); $cacheInstance->clean(); } From 7ca95b66d8f707ae1624412a5d4d9dfa0eafc70f Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 30 May 2018 16:16:46 +1200 Subject: [PATCH 04/12] Remove messages --- tests/cache/CacheTest.php | 47 +++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/tests/cache/CacheTest.php b/tests/cache/CacheTest.php index feda314f6..6b6e83695 100644 --- a/tests/cache/CacheTest.php +++ b/tests/cache/CacheTest.php @@ -77,16 +77,28 @@ class CacheTest extends SapphireTest { $result = $cacheInstance->load('shared_key'); $this->assertFalse($result); $cacheInstance->save('uncle', 'shared_key'); - $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key is cached on LIVE'); + // Shared key is cached on LIVE + $this->assertEquals('uncle', $cacheInstance->load('shared_key')); + Versioned::set_reading_mode('Stage.Stage'); - $this->assertFalse($cacheInstance->load('shared_key'), 'Shared key does not exist on STAGE'); + + // Shared key does not exist on STAGE + $this->assertFalse($cacheInstance->load('shared_key')); + $cacheInstance->save('cheese', 'shared_key'); $cacheInstance->save('bar', 'stage_key'); - $this->assertEquals('cheese', $cacheInstance->load('shared_key'), 'Shared key has its own value on STAGE'); - $this->assertEquals('bar', $cacheInstance->load('stage_key'), 'New key is cached on STAGE'); + + // Shared key has its own value on STAGE + $this->assertEquals('cheese', $cacheInstance->load('shared_key')); + // New key is cached on STAGE + $this->assertEquals('bar', $cacheInstance->load('stage_key')); + Versioned::set_reading_mode('Stage.Live'); - $this->assertFalse($cacheInstance->load('stage_key'), 'New key does not exist on LIVE'); - $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key retains its own value on LIVE'); + + // New key does not exist on LIVE + $this->assertFalse($cacheInstance->load('stage_key')); + // Shared key retains its own value on LIVE + $this->assertEquals('uncle', $cacheInstance->load('shared_key')); $cacheInstance->clean(); @@ -100,16 +112,27 @@ class CacheTest extends SapphireTest { $result = $cacheInstance->load('shared_key'); $this->assertFalse($result); $cacheInstance->save('uncle', 'shared_key'); - $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key is cached on LIVE'); + // Shared key is cached on LIVE + $this->assertEquals('uncle', $cacheInstance->load('shared_key')); + Versioned::set_reading_mode('Stage.Stage'); - $this->assertEquals('uncle', $cacheInstance->load('shared_key'), 'Shared key is same on STAGE'); + + // Shared key is same on STAGE + $this->assertEquals('uncle', $cacheInstance->load('shared_key')); + $cacheInstance->save('cheese', 'shared_key'); $cacheInstance->save('bar', 'stage_key'); - $this->assertEquals('cheese', $cacheInstance->load('shared_key'), 'Shared key is overwritten on STAGE'); - $this->assertEquals('bar', $cacheInstance->load('stage_key'), 'New key is written on STAGE'); + + // Shared key is overwritten on STAGE + $this->assertEquals('cheese', $cacheInstance->load('shared_key')); + // New key is written on STAGE + $this->assertEquals('bar', $cacheInstance->load('stage_key')); + Versioned::set_reading_mode('Stage.Live'); - $this->assertEquals('bar', $cacheInstance->load('stage_key'), 'New key has same value on LIVE'); - $this->assertEquals('cheese', $cacheInstance->load('shared_key'), 'New value for existing key is same on LIVE'); + // New key has same value on LIVE + $this->assertEquals('bar', $cacheInstance->load('stage_key')); + // New value for existing key is same on LIVE + $this->assertEquals('cheese', $cacheInstance->load('shared_key')); $cacheInstance->clean(); } From 96701b8ea04bf5815072ad3350f7952ad64aed4e Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 30 May 2018 16:24:09 +1200 Subject: [PATCH 05/12] Add upgrade docs --- docs/en/04_Changelogs/3.7.0.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/en/04_Changelogs/3.7.0.md diff --git a/docs/en/04_Changelogs/3.7.0.md b/docs/en/04_Changelogs/3.7.0.md new file mode 100644 index 000000000..48a8825b0 --- /dev/null +++ b/docs/en/04_Changelogs/3.7.0.md @@ -0,0 +1,26 @@ +# 3.7.0 + +### Versioned cache segmentation + +The cache API now maintains separate cache pools for each versioned stage. This prevents users from caching draft data and then exposing it on the live stage. + +```php +// Before: +$cache = SS_Cache::factory('myapp'); +Versioned::set_reading_mode('Stage.Live'); +$cache->save('Some draft content. Not for public viewing yet.', 'my_key'); +Versioned::set_reading_mode('Stage.Stage'); +$cache->load('my_key'); // 'Some draft content. Not for public viewing yet' + +// After: +$cache = SS_Cache::factory('myapp'); +Versioned::set_reading_mode('Stage.Live'); +$cache->save('Some draft content. Not for public viewing yet.', 'my_key'); +Versioned::set_reading_mode('Stage.Stage'); +$cache->load('my_key'); // null +``` +Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache with the `disable-container` argument. + +```php +$cache = SS_Cache::factory('myapp', 'Output', array('disable-container' => true)); +``` \ No newline at end of file From 489aca2fc9846836e619a11fc43489f29c889482 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 30 May 2018 16:27:16 +1200 Subject: [PATCH 06/12] Rename variable in factory to --- cache/Cache.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cache/Cache.php b/cache/Cache.php index 477eb86f4..5efe45658 100644 --- a/cache/Cache.php +++ b/cache/Cache.php @@ -188,14 +188,14 @@ class SS_Cache { require_once 'Zend/Cache.php'; - $container = Zend_Cache::factory( + $cache = Zend_Cache::factory( $frontend, $backend[0], $frontendOptions, $backend[1] ); if (isset($frontendOptions['disable-container']) && $frontendOptions['disable-container']) { - return $container; + return $cache; } - return Injector::inst()->createWithArgs('CacheProxy', [$container]); + return Injector::inst()->createWithArgs('CacheProxy', [$cache]); } } From 0b1809000376cdeeb84075b6fc24c98198eca19b Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Wed, 30 May 2018 16:44:47 +1200 Subject: [PATCH 07/12] Docs updates --- docs/en/04_Changelogs/3.7.0.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/04_Changelogs/3.7.0.md b/docs/en/04_Changelogs/3.7.0.md index 48a8825b0..244965f60 100644 --- a/docs/en/04_Changelogs/3.7.0.md +++ b/docs/en/04_Changelogs/3.7.0.md @@ -2,21 +2,21 @@ ### Versioned cache segmentation -The cache API now maintains separate cache pools for each versioned stage. This prevents users from caching draft data and then exposing it on the live stage. +`SS_Cache` now maintains separate cache pools for each versioned stage. This prevents developers from caching draft data and then accidentally exposing it on the live stage without potentially required authorisation checks. Unless you rely on caching across stages, you don't need to change your own code for this change to take effect. Note that cache keys will be internally rewritten, causing any existing cache items to become invalid when this change is deployed. ```php // Before: $cache = SS_Cache::factory('myapp'); -Versioned::set_reading_mode('Stage.Live'); -$cache->save('Some draft content. Not for public viewing yet.', 'my_key'); Versioned::set_reading_mode('Stage.Stage'); +$cache->save('Some draft content. Not for public viewing yet.', 'my_key'); +Versioned::set_reading_mode('Stage.Live'); $cache->load('my_key'); // 'Some draft content. Not for public viewing yet' // After: $cache = SS_Cache::factory('myapp'); -Versioned::set_reading_mode('Stage.Live'); -$cache->save('Some draft content. Not for public viewing yet.', 'my_key'); Versioned::set_reading_mode('Stage.Stage'); +$cache->save('Some draft content. Not for public viewing yet.', 'my_key'); +Versioned::set_reading_mode('Stage.Live'); $cache->load('my_key'); // null ``` Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache with the `disable-container` argument. From dac3c5ee164099de5a8fb961954a244eca4b99b5 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Fri, 1 Jun 2018 11:05:03 +1200 Subject: [PATCH 08/12] Remove segmentation from core caches --- cache/Cache.php | 2 +- core/manifest/ConfigManifest.php | 1 + core/manifest/SilverStripeVersionProvider.php | 2 +- filesystem/GD.php | 2 +- i18n/i18n.php | 10 +++++++++- tasks/CleanImageManipulationCache.php | 2 +- tests/cache/CacheTest.php | 2 +- 7 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cache/Cache.php b/cache/Cache.php index 5efe45658..1f67592ac 100644 --- a/cache/Cache.php +++ b/cache/Cache.php @@ -192,7 +192,7 @@ class SS_Cache { $frontend, $backend[0], $frontendOptions, $backend[1] ); - if (isset($frontendOptions['disable-container']) && $frontendOptions['disable-container']) { + if (isset($frontendOptions['disable-segmentation']) && $frontendOptions['disable-segmentation']) { return $cache; } diff --git a/core/manifest/ConfigManifest.php b/core/manifest/ConfigManifest.php index 6bb5485b3..20aec2214 100644 --- a/core/manifest/ConfigManifest.php +++ b/core/manifest/ConfigManifest.php @@ -118,6 +118,7 @@ class SS_ConfigManifest { 'automatic_serialization' => true, 'lifetime' => null, 'cache_id_prefix' => 'SS_Configuration_', + 'disable-segmentation' => true, )); } diff --git a/core/manifest/SilverStripeVersionProvider.php b/core/manifest/SilverStripeVersionProvider.php index c0e6ecf9f..5223055b9 100644 --- a/core/manifest/SilverStripeVersionProvider.php +++ b/core/manifest/SilverStripeVersionProvider.php @@ -83,7 +83,7 @@ class SilverStripeVersionProvider $lockData = array(); if ($cache) { - $cache = SS_Cache::factory('SilverStripeVersionProvider_composerlock'); + $cache = SS_Cache::factory('SilverStripeVersionProvider_composerlock', 'Output', array('disable-segmentation' => true)); $cacheKey = filemtime($composerLockPath); if ($versions = $cache->load($cacheKey)) { $lockData = json_decode($versions, true); diff --git a/filesystem/GD.php b/filesystem/GD.php index 201d49528..4b863c712 100644 --- a/filesystem/GD.php +++ b/filesystem/GD.php @@ -39,7 +39,7 @@ class GDBackend extends SS_Object implements Image_Backend { // If we're working with image resampling, things could take a while. Bump up the time-limit increase_time_limit_to(300); - $this->cache = SS_Cache::factory('GDBackend_Manipulations'); + $this->cache = SS_Cache::factory('GDBackend_Manipulations', 'Output', array('disable-segmentation' => true)); if($filename && is_readable($filename)) { $this->cacheKey = md5(implode('_', array($filename, filemtime($filename)))); diff --git a/i18n/i18n.php b/i18n/i18n.php index 668a8bcaa..6100dbe9f 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -121,7 +121,15 @@ class i18n extends SS_Object implements TemplateGlobalProvider, Flushable { * @return Zend_Cache */ public static function get_cache() { - return SS_Cache::factory('i18n', 'Output', array('lifetime' => null, 'automatic_serialization' => true)); + return SS_Cache::factory( + 'i18n', + 'Output', + array( + 'lifetime' => null, + 'automatic_serialization' => true, + 'disable-segmentation' => true, + ) + ); } /** diff --git a/tasks/CleanImageManipulationCache.php b/tasks/CleanImageManipulationCache.php index 03c7c8a29..38d742069 100644 --- a/tasks/CleanImageManipulationCache.php +++ b/tasks/CleanImageManipulationCache.php @@ -35,7 +35,7 @@ class CleanImageManipulationCache extends BuildTask { $images = DataObject::get('Image'); if($images && Image::get_backend() == "GDBackend") { - $cache = SS_Cache::factory('GDBackend_Manipulations'); + $cache = SS_Cache::factory('GDBackend_Manipulations', 'Output', array('disable-segmentation' => true)); foreach($images as $image) { $path = $image->getFullPath(); diff --git a/tests/cache/CacheTest.php b/tests/cache/CacheTest.php index 6b6e83695..4887420e1 100644 --- a/tests/cache/CacheTest.php +++ b/tests/cache/CacheTest.php @@ -105,7 +105,7 @@ class CacheTest extends SapphireTest { } public function testDisableVersionedCacheSegmentation() { - $cacheInstance = SS_Cache::factory('versioned_disabled', 'Output', ['disable-container' => true]); + $cacheInstance = SS_Cache::factory('versioned_disabled', 'Output', array('disable-segmentation' => true)); $cacheInstance->clean(); Versioned::set_reading_mode('Stage.Live'); From 05a519ecc5c8f68e049b68714c2ea60d9abd0e54 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Fri, 1 Jun 2018 15:38:53 +1200 Subject: [PATCH 09/12] Fix code style / php 5.3 compat --- cache/Cache.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cache/Cache.php b/cache/Cache.php index 1f67592ac..cf5518c2f 100644 --- a/cache/Cache.php +++ b/cache/Cache.php @@ -175,8 +175,8 @@ class SS_Cache { $backend = self::$backends[$backend_name]; $basicOptions = array( - 'cache_id_prefix' => md5(BASE_PATH) . '_' . $for . '_', - ); + 'cache_id_prefix' => md5(BASE_PATH) . '_' . $for . '_', + ); if ($cache_lifetime >= 0) { $basicOptions['lifetime'] = $cache_lifetime; @@ -188,14 +188,14 @@ class SS_Cache { require_once 'Zend/Cache.php'; - $cache = Zend_Cache::factory( - $frontend, $backend[0], $frontendOptions, $backend[1] - ); + $cache = Zend_Cache::factory( + $frontend, $backend[0], $frontendOptions, $backend[1] + ); - if (isset($frontendOptions['disable-segmentation']) && $frontendOptions['disable-segmentation']) { - return $cache; - } + if (!empty($frontendOptions['disable-segmentation'])) { + return $cache; + } - return Injector::inst()->createWithArgs('CacheProxy', [$cache]); + return Injector::inst()->createWithArgs('CacheProxy', array($cache)); } } From 779a4e24432149628e39b15caadc5a8753ef6b37 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Fri, 1 Jun 2018 15:49:16 +1200 Subject: [PATCH 10/12] Set disable-segmentation for SSViewer cache Cleanup --- core/manifest/ConfigManifest.php | 2 +- core/manifest/SilverStripeVersionProvider.php | 6 +++++- i18n/i18n.php | 2 +- view/SSViewer.php | 16 ++++++++++++---- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/manifest/ConfigManifest.php b/core/manifest/ConfigManifest.php index 20aec2214..caf63f716 100644 --- a/core/manifest/ConfigManifest.php +++ b/core/manifest/ConfigManifest.php @@ -110,7 +110,7 @@ class SS_ConfigManifest { /** * Provides a hook for mock unit tests despite no DI - * @return Zend_Cache_Frontend + * @return Zend_Cache_Core */ protected function getCache() { diff --git a/core/manifest/SilverStripeVersionProvider.php b/core/manifest/SilverStripeVersionProvider.php index 5223055b9..b1cd92426 100644 --- a/core/manifest/SilverStripeVersionProvider.php +++ b/core/manifest/SilverStripeVersionProvider.php @@ -83,7 +83,11 @@ class SilverStripeVersionProvider $lockData = array(); if ($cache) { - $cache = SS_Cache::factory('SilverStripeVersionProvider_composerlock', 'Output', array('disable-segmentation' => true)); + $cache = SS_Cache::factory( + 'SilverStripeVersionProvider_composerlock', + 'Output', + array('disable-segmentation' => true) + ); $cacheKey = filemtime($composerLockPath); if ($versions = $cache->load($cacheKey)) { $lockData = json_decode($versions, true); diff --git a/i18n/i18n.php b/i18n/i18n.php index 6100dbe9f..c1912fc19 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -118,7 +118,7 @@ class i18n extends SS_Object implements TemplateGlobalProvider, Flushable { /** * Return an instance of the cache used for i18n data. - * @return Zend_Cache + * @return Zend_Cache_Core */ public static function get_cache() { return SS_Cache::factory( diff --git a/view/SSViewer.php b/view/SSViewer.php index c78b05fc9..85a540d92 100644 --- a/view/SSViewer.php +++ b/view/SSViewer.php @@ -31,10 +31,10 @@ class SSViewer_Scope { const UP_INDEX = 4; const CURRENT_INDEX = 5; const ITEM_OVERLAY = 6; - + // The stack of previous "global" items // An indexed array of item, item iterator, item iterator total, pop index, up index, current index & parent overlay - private $itemStack = array(); + private $itemStack = array(); // The current "global" item (the one any lookup starts from) protected $item; @@ -1016,6 +1016,14 @@ class SSViewer implements Flushable { } } + /** + * @return Zend_Cache_Core + */ + protected static function defaultPartialCacheStore() + { + return SS_Cache::factory('cacheblock', 'Output', array('disable-segmentation' => true)); + } + /** * Call this to disable rewriting of links. This is useful in Ajax applications. * It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process(); @@ -1082,7 +1090,7 @@ class SSViewer implements Flushable { */ public static function flush_cacheblock_cache($force = false) { if (!self::$cacheblock_cache_flushed || $force) { - $cache = SS_Cache::factory('cacheblock'); + $cache = self::defaultPartialCacheStore(); $backend = $cache->getBackend(); if( @@ -1120,7 +1128,7 @@ class SSViewer implements Flushable { * @return Zend_Cache_Core */ public function getPartialCacheStore() { - return $this->partialCacheStore ? $this->partialCacheStore : SS_Cache::factory('cacheblock'); + return $this->partialCacheStore ? $this->partialCacheStore : self::defaultPartialCacheStore(); } /** From 85a712e1c9288a398de03e374a8a3bb980486d82 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 5 Jun 2018 11:34:27 +1200 Subject: [PATCH 11/12] Fix postgres test --- tests/model/DataObjectDuplicationTest.php | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/model/DataObjectDuplicationTest.php b/tests/model/DataObjectDuplicationTest.php index 2fcab1c4d..d058fafbb 100644 --- a/tests/model/DataObjectDuplicationTest.php +++ b/tests/model/DataObjectDuplicationTest.php @@ -111,13 +111,21 @@ class DataObjectDuplicationTest extends SapphireTest { "Match between relation of copy and the original"); $this->assertEquals(0, $oneCopy->twos()->Count(), "Many-to-one relation not copied (has_many)"); - $this->assertEquals($three->ID, $oneCopy->threes()->First()->ID, - "Match between relation of copy and the original"); - $this->assertEquals($one->ID, $threeCopy->ones()->First()->ID, - "Match between relation of copy and the original"); - - $this->assertEquals('three', $oneCopy->threes()->First()->TestExtra, - "Match between extra field of copy and the original"); + $this->assertContains( + $three->ID, + $oneCopy->threes()->column('ID'), + "Match between relation of copy and the original" + ); + $this->assertContains( + $one->ID, + $threeCopy->ones()->column('ID'), + "Match between relation of copy and the original" + ); + $this->assertContains( + 'three', + $oneCopy->threes()->column('TestExtra'), + "Match between extra field of copy and the original" + ); } } From f4411df75c14dc6241fe0cb5345a0f6dfa7c2dab Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 5 Jun 2018 11:34:46 +1200 Subject: [PATCH 12/12] Update documentation --- .../08_Performance/01_Caching.md | 14 ++++++++++++++ docs/en/04_Changelogs/3.7.0.md | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/en/02_Developer_Guides/08_Performance/01_Caching.md b/docs/en/02_Developer_Guides/08_Performance/01_Caching.md index ca76d60da..1a680d0ae 100644 --- a/docs/en/02_Developer_Guides/08_Performance/01_Caching.md +++ b/docs/en/02_Developer_Guides/08_Performance/01_Caching.md @@ -90,6 +90,20 @@ e.g. by including the `LastEdited` value when caching `DataObject` results. // set all caches to 3 hours SS_Cache::set_cache_lifetime('any', 60*60*3); +### Versioned cache segmentation + +`SS_Cache` segments caches based on the versioned reading mode. This prevents developers +from caching draft data and then accidentally exposing it on the live stage without potentially +required authorisation checks. This segmentation is automatic for all caches generated using +`SS_Cache::factory` method. + +Data that is not content sensitive can be cached across stages by simply opting out of the +segmented cache with the `disable-segmentation` argument. + +```php +$cache = SS_Cache::factory('myapp', 'Output', array('disable-segmentation' => true)); +``` + ## Alternative Cache Backends By default, SilverStripe uses a file-based caching backend. diff --git a/docs/en/04_Changelogs/3.7.0.md b/docs/en/04_Changelogs/3.7.0.md index 244965f60..6dc129cd6 100644 --- a/docs/en/04_Changelogs/3.7.0.md +++ b/docs/en/04_Changelogs/3.7.0.md @@ -19,8 +19,8 @@ $cache->save('Some draft content. Not for public viewing yet.', 'my_key'); Versioned::set_reading_mode('Stage.Live'); $cache->load('my_key'); // null ``` -Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache with the `disable-container` argument. +Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache with the `disable-segmentation` argument. ```php -$cache = SS_Cache::factory('myapp', 'Output', array('disable-container' => true)); -``` \ No newline at end of file +$cache = SS_Cache::factory('myapp', 'Output', array('disable-segmentation' => true)); +```