diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index 09e7e827ee3..b765cc2ea8c 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Cache; +use Drupal\Component\Utility\Crypt; use Drupal\Core\Database\Connection; use Drupal\Core\Database\SchemaObjectExistsException; @@ -62,6 +63,10 @@ class DatabaseBackend implements CacheBackendInterface { * Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple(). */ public function getMultiple(&$cids, $allow_invalid = FALSE) { + $cid_mapping = array(); + foreach ($cids as $cid) { + $cid_mapping[$this->normalizeCid($cid)] = $cid; + } // When serving cached pages, the overhead of using ::select() was found // to add around 30% overhead to the request. Since $this->bin is a // variable, this means the call to ::query() here uses a concatenated @@ -71,13 +76,15 @@ class DatabaseBackend implements CacheBackendInterface { // ::select() is a much smaller proportion of the request. $result = array(); try { - $result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum_invalidations, checksum_deletions FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids)); + $result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum_invalidations, checksum_deletions FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => array_keys($cid_mapping))); } catch (\Exception $e) { // Nothing to do. } $cache = array(); foreach ($result as $item) { + // Map the cache ID back to the original. + $item->cid = $cid_mapping[$item->cid]; $item = $this->prepareItem($item, $allow_invalid); if ($item) { $cache[$item->cid] = $item; @@ -196,7 +203,7 @@ class DatabaseBackend implements CacheBackendInterface { } $this->connection->merge($this->bin) - ->key('cid', $cid) + ->key('cid', $this->normalizeCid($cid)) ->fields($fields) ->execute(); } @@ -284,14 +291,14 @@ class DatabaseBackend implements CacheBackendInterface { * Implements Drupal\Core\Cache\CacheBackendInterface::deleteMultiple(). */ public function deleteMultiple(array $cids) { + $cids = array_values(array_map(array($this, 'normalizeCid'), $cids)); try { // Delete in chunks when a large array is passed. - do { + foreach (array_chunk($cids, 1000) as $cids_chunk) { $this->connection->delete($this->bin) - ->condition('cid', array_splice($cids, 0, 1000), 'IN') + ->condition('cid', $cids_chunk, 'IN') ->execute(); } - while (count($cids)); } catch (\Exception $e) { // Create the cache table, which will be empty. This fixes cases during @@ -357,15 +364,15 @@ class DatabaseBackend implements CacheBackendInterface { * Implements Drupal\Core\Cache\CacheBackendInterface::invalideMultiple(). */ public function invalidateMultiple(array $cids) { + $cids = array_values(array_map(array($this, 'normalizeCid'), $cids)); try { // Update in chunks when a large array is passed. - do { + foreach (array_chunk($cids, 1000) as $cids_chunk) { $this->connection->update($this->bin) ->fields(array('expire' => REQUEST_TIME - 1)) - ->condition('cid', array_splice($cids, 0, 1000), 'IN') + ->condition('cid', $cids_chunk, 'IN') ->execute(); } - while (count($cids)); } catch (\Exception $e) { $this->catchException($e); @@ -548,6 +555,26 @@ class DatabaseBackend implements CacheBackendInterface { } } + /** + * Ensures that cache IDs have a maximum length of 255 characters. + * + * @param string $cid + * The passed in cache ID. + * + * @return string + * A cache ID that is at most 255 characters long. + */ + protected function normalizeCid($cid) { + // Nothing to do if the ID length is 255 characters or less. + if (strlen($cid) <= 255) { + return $cid; + } + // Return a string that uses as much as possible of the original cache ID + // with the hash appended. + $hash = Crypt::hashBase64($cid); + return substr($cid, 0, 255 - strlen($hash)) . $hash; + } + /** * Defines the schema for the cache bin and cache_tags table. */ diff --git a/core/lib/Drupal/Core/Cache/PhpBackend.php b/core/lib/Drupal/Core/Cache/PhpBackend.php index 6c7fdd238f7..86138590568 100644 --- a/core/lib/Drupal/Core/Cache/PhpBackend.php +++ b/core/lib/Drupal/Core/Cache/PhpBackend.php @@ -8,7 +8,7 @@ namespace Drupal\Core\Cache; use Drupal\Core\PhpStorage\PhpStorageFactory; -use Drupal\Component\Utility\Variable; +use Drupal\Component\Utility\Crypt; /** * Defines a PHP cache implementation. @@ -49,7 +49,22 @@ class PhpBackend implements CacheBackendInterface { * {@inheritdoc} */ public function get($cid, $allow_invalid = FALSE) { - if ($file = $this->storage()->getFullPath($cid)) { + return $this->getByHash($this->normalizeCid($cid), $allow_invalid); + } + + /** + * Fetch a cache item using a hashed cache ID. + * + * @param string $cidhash + * The hashed version of the original cache ID after being normalized. + * @param bool $allow_invalid + * (optional) If TRUE, a cache item may be returned even if it is expired or + * has been invalidated. + * + * @return bool|mixed + */ + protected function getByHash($cidhash, $allow_invalid = FALSE) { + if ($file = $this->storage()->getFullPath($cidhash)) { $cache = @include $file; } if (isset($cache)) { @@ -125,14 +140,14 @@ class PhpBackend implements CacheBackendInterface { 'expire' => $expire, ); $item->tags = $this->flattenTags($tags); - $this->writeItem($cid, $item); + $this->writeItem($this->normalizeCid($cid), $item); } /** * {@inheritdoc} */ public function delete($cid) { - $this->storage()->delete($cid); + $this->storage()->delete($this->normalizeCid($cid)); } /** @@ -149,10 +164,10 @@ class PhpBackend implements CacheBackendInterface { */ public function deleteTags(array $tags) { $flat_tags = $this->flattenTags($tags); - foreach ($this->storage()->listAll() as $cid) { - $item = $this->get($cid); + foreach ($this->storage()->listAll() as $cidhash) { + $item = $this->getByHash($cidhash); if (is_object($item) && array_intersect($flat_tags, $item->tags)) { - $this->delete($cid); + $this->delete($item->cid); } } } @@ -168,9 +183,19 @@ class PhpBackend implements CacheBackendInterface { * {@inheritdoc} */ public function invalidate($cid) { - if ($item = $this->get($cid)) { + $this->invalidatebyHash($this->normalizeCid($cid)); + } + + /** + * Invalidate one cache item. + * + * @param string $cidhash + * The hashed version of the original cache ID after being normalized. + */ + protected function invalidatebyHash($cidhash) { + if ($item = $this->getByHash($cidhash)) { $item->expire = REQUEST_TIME - 1; - $this->writeItem($cid, $item); + $this->writeItem($cidhash, $item); } } @@ -188,10 +213,10 @@ class PhpBackend implements CacheBackendInterface { */ public function invalidateTags(array $tags) { $flat_tags = $this->flattenTags($tags); - foreach ($this->storage()->listAll() as $cid) { - $item = $this->get($cid); + foreach ($this->storage()->listAll() as $cidhash) { + $item = $this->getByHash($cidhash); if ($item && array_intersect($flat_tags, $item->tags)) { - $this->invalidate($cid); + $this->invalidate($item->cid); } } } @@ -200,7 +225,9 @@ class PhpBackend implements CacheBackendInterface { * {@inheritdoc} */ public function invalidateAll() { - $this->invalidateMultiple($this->storage()->listAll()); + foreach($this->storage()->listAll() as $cidhash) { + $this->invalidatebyHash($cidhash); + } } /** @@ -248,20 +275,20 @@ class PhpBackend implements CacheBackendInterface { /** * Writes a cache item to PhpStorage. * - * @param string $cid - * The cache ID of the data to store. + * @param string $cidhash + * The hashed version of the original cache ID after being normalized. * @param \stdClass $item * The cache item to store. */ - protected function writeItem($cid, \stdClass $item) { + protected function writeItem($cidhash, \stdClass $item) { $content = 'storage()->save($cid, $content); + $this->storage()->save($cidhash, $content); } /** * Gets the PHP code storage object to use. * - * @return \Drupal\Core\PhpStorage\PhpStorageInterface + * @return \Drupal\Component\PhpStorage\PhpStorageInterface */ protected function storage() { if (!isset($this->storage)) { @@ -270,4 +297,17 @@ class PhpBackend implements CacheBackendInterface { return $this->storage; } + /** + * Ensures a normalized cache ID. + * + * @param string $cid + * The passed in cache ID. + * + * @return string + * A normalized cache ID. + */ + protected function normalizeCid($cid) { + return Crypt::hashBase64($cid); + } + } diff --git a/core/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php b/core/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php index cf02d13bcfe..509ac880beb 100644 --- a/core/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php +++ b/core/modules/system/src/Tests/Cache/GenericCacheBackendUnitTestBase.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\system\Tests\Cache\GenericCacheBackendUnitTestBase. + * Contains \Drupal\system\Tests\Cache\GenericCacheBackendUnitTestBase. */ namespace Drupal\system\Tests\Cache; @@ -207,6 +207,11 @@ abstract class GenericCacheBackendUnitTestBase extends DrupalUnitTestBase { $cached->data->this_should_not_be_in_the_cache = TRUE; $fresh_cached = $backend->get('test7'); $this->assertFalse(isset($fresh_cached->data->this_should_not_be_in_the_cache)); + + // Check with a long key. + $cid = str_repeat('a', 300); + $backend->set($cid, 'test'); + $this->assertEqual('test', $backend->get($cid)->data); } /** @@ -230,6 +235,11 @@ abstract class GenericCacheBackendUnitTestBase extends DrupalUnitTestBase { $backend->delete('test2'); $this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2 after deletion."); + + $long_cid = str_repeat('a', 300); + $backend->set($long_cid, 'test'); + $backend->delete($long_cid); + $this->assertIdentical(FALSE, $backend->get($long_cid), "Backend does not contain data for long cache id after deletion."); } /** @@ -267,6 +277,7 @@ abstract class GenericCacheBackendUnitTestBase extends DrupalUnitTestBase { $backend = $this->getCacheBackend(); // Set numerous testing keys. + $long_cid = str_repeat('a', 300); $backend->set('test1', 1); $backend->set('test2', 3); $backend->set('test3', 5); @@ -274,6 +285,7 @@ abstract class GenericCacheBackendUnitTestBase extends DrupalUnitTestBase { $backend->set('test5', 11); $backend->set('test6', 13); $backend->set('test7', 17); + $backend->set($long_cid, 300); // Mismatch order for harder testing. $reference = array( @@ -341,6 +353,12 @@ abstract class GenericCacheBackendUnitTestBase extends DrupalUnitTestBase { $this->assertFalse(in_array('test2', $cids), "Existing cache id test2 is not in cids array."); $this->assertFalse(in_array('test7', $cids), "Existing cache id test7 is not in cids array."); $this->assertFalse(in_array('test19', $cids), "Added cache id test19 is not in cids array."); + + // Test with a long $cid and non-numeric array key. + $cids = array('key:key' => $long_cid); + $return = $backend->getMultiple($cids); + $this->assertEqual(300, $return[$long_cid]->data); + $this->assertTrue(empty($cids)); } /** @@ -420,6 +438,9 @@ abstract class GenericCacheBackendUnitTestBase extends DrupalUnitTestBase { // Test if that expected keys do not exist. $this->assertIdentical(FALSE, $backend->get('test19'), "Cache id test19 does not exist."); $this->assertIdentical(FALSE, $backend->get('test21'), "Cache id test21 does not exist."); + + // Calling deleteMultiple() with an empty array should not cause an error. + $this->assertFalse($backend->deleteMultiple(array())); } /** @@ -528,6 +549,10 @@ abstract class GenericCacheBackendUnitTestBase extends DrupalUnitTestBase { $cids = $reference; $ret = $backend->getMultiple($cids, TRUE); $this->assertEqual(count($ret), 4, 'Four items returned.'); + + // Calling invalidateMultiple() with an empty array should not cause an + // error. + $this->assertFalse($backend->invalidateMultiple(array())); } /**