diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 77458a1..c9a34e6 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2461,9 +2461,7 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) { $container ->register('config.storage.staging', 'Drupal\Core\Config\FileStorage') ->addArgument(config_get_config_directory(CONFIG_STAGING_DIRECTORY)); - $container - ->register('state.storage', 'Drupal\Core\KeyValueStore\DatabaseStorage') - ->addArgument('state'); + $container->register('keyvaluestore', 'Drupal\Core\KeyValueStore\KeyValueStoreFactory'); } return $container; } @@ -2480,7 +2478,7 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) { * @return Drupal\Core\KeyValueStore\KeyValueStoreInterface */ function state() { - return drupal_container()->get('state.storage'); + return drupal_container()->get('keyvaluestore')->get('state'); } /** diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index c1abfe5..84a28ff 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -54,7 +54,15 @@ public function build(ContainerBuilder $container) { ->setFactoryClass('Drupal\Core\Database\Database') ->setFactoryMethod('getConnection') ->addArgument('slave'); + $container + ->register('keyvaluestore.expirable', 'Drupal\Core\KeyValueStore\KeyValueStoreExpirableFactory') + ->addArgument(new Reference('database')); $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager'); + // Add the user's storage for temporary, non-cache data. + $container->register('lock', 'Drupal\Core\Lock\DatabaseLockBackend'); + $container->register('user.tempstore', 'Drupal\user\TempStoreFactory') + ->addArgument(new Reference('database')) + ->addArgument(new Reference('lock')); $container->register('router.dumper', '\Drupal\Core\Routing\MatcherDumper') ->addArgument(new Reference('database')); diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php index 532f690..532be48 100644 --- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php @@ -7,15 +7,28 @@ namespace Drupal\Core\KeyValueStore; +use Drupal\Core\Database\Query\Merge; + /** * Defines a default key/value store implementation. * * This is Drupal's default key/value store implementation. It uses the database * to store key/value data. + * + * @todo This class still calls db_* functions directly because it's needed + * very early, pre-Container. Once the early bootstrap dependencies are + * sorted out, consider using an injected database connection instead. */ class DatabaseStorage extends StorageBase { /** + * The name of the SQL table to use. + * + * @var string + */ + protected $table; + + /** * Overrides Drupal\Core\KeyValueStore\StorageBase::__construct(). * * @param string $collection @@ -42,9 +55,9 @@ public function getMultiple(array $keys) { } } catch (\Exception $e) { - // @todo: Perhaps if the database is never going to be available, - // key/value requests should return FALSE in order to allow exception - // handling to occur but for now, keep it an array, always. + // @todo Perhaps if the database is never going to be available, + // key/value requests should return FALSE in order to allow exception + // handling to occur but for now, keep it an array, always. } return $values; } @@ -78,6 +91,22 @@ public function set($key, $value) { } /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setIfNotExists(). + */ + public function setIfNotExists($key, $value) { + $result = db_merge($this->table) + ->insertFields(array( + 'collection' => $this->collection, + 'name' => $key, + 'value' => serialize($value), + )) + ->condition('collection', $this->collection) + ->condition('name', $key) + ->execute(); + return $result == Merge::STATUS_INSERT; + } + + /** * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple(). */ public function deleteMultiple(array $keys) { @@ -91,4 +120,12 @@ public function deleteMultiple(array $keys) { while (count($keys)); } + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteAll(). + */ + public function deleteAll() { + db_delete($this->table) + ->condition('collection', $this->collection) + ->execute(); + } } diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php new file mode 100644 index 0000000..58785c4 --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php @@ -0,0 +1,147 @@ +connection = $options['connection']; + $this->table = isset($options['table']) ? $options['table'] : 'key_value_expire'; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getMultiple(). + */ + public function getMultiple(array $keys) { + $values = array(); + try { + $result = $this->connection->query('SELECT name, value, expire FROM {' . $this->connection->escapeTable($this->table) . '} WHERE expire > :now AND name IN (:keys) AND collection = :collection', + array( + ':now' => REQUEST_TIME, + ':keys' => $keys, + ':collection' => $this->collection, + ))->fetchAllAssoc('name'); + foreach ($keys as $key) { + if (isset($result[$key])) { + $values[$key] = unserialize($result[$key]->value); + } + } + } + catch (\Exception $e) { + // @todo Perhaps if the database is never going to be available, + // key/value requests should return FALSE in order to allow exception + // handling to occur but for now, keep it an array, always. + } + return $values; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getAll(). + */ + public function getAll() { + $result = $this->connection->query('SELECT name, value FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND expire > :now', array(':collection' => $this->collection, ':now' => REQUEST_TIME)); + $values = array(); + + foreach ($result as $item) { + if ($item) { + $values[$item->name] = unserialize($item->value); + } + } + return $values; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpireInterface::setWithExpire(). + */ + function setWithExpire($key, $value, $expire) { + $this->garbageCollection(); + $this->connection->merge($this->table) + ->key(array( + 'name' => $key, + 'collection' => $this->collection, + )) + ->fields(array( + 'value' => serialize($value), + 'expire' => REQUEST_TIME + $expire, + )) + ->execute(); + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface::setWithExpireIfNotExists(). + */ + function setWithExpireIfNotExists($key, $value, $expire) { + $this->garbageCollection(); + $result = $this->connection->merge($this->table) + ->insertFields(array( + 'collection' => $this->collection, + 'name' => $key, + 'value' => serialize($value), + 'expire' => REQUEST_TIME + $expire, + )) + ->condition('collection', $this->collection) + ->condition('name', $key) + ->execute(); + return $result == Merge::STATUS_INSERT; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpirablInterface::setMultipleWithExpire(). + */ + function setMultipleWithExpire(array $data, $expire) { + foreach ($data as $key => $value) { + $this->set($key, $value, $expire); + } + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple(). + */ + public function deleteMultiple(array $keys) { + $this->garbageCollection(); + parent::deleteMultiple($keys); + } + + /** + * Deletes expired items. + */ + protected function garbageCollection() { + $this->connection->delete($this->table) + ->condition('expire', REQUEST_TIME, '<') + ->execute(); + } + +} diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableFactory.php new file mode 100644 index 0000000..35ef604 --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableFactory.php @@ -0,0 +1,47 @@ +connection = $connection; + } + + /** + * Creates a Drupal\Core\KeyValueStore\DatabaseStorageExpirable object. + * + * @param string $collection + * The collection to use for this key/value store. + * + * @return Drupal\Core\KeyValueStore\DatabaseStorageExpirable + * An instance of the the key/value store. + */ + function get($collection) { + return new DatabaseStorageExpirable($collection, array('connection' => $this->connection)); + } + +} diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php new file mode 100644 index 0000000..e39dbe5 --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php @@ -0,0 +1,52 @@ +data[$key])) { + $this->data[$key] = $value; + return TRUE; + } + return FALSE; + } + + /** * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setMultiple(). */ public function setMultiple(array $data) { @@ -70,4 +81,10 @@ public function deleteMultiple(array $keys) { } } + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteAll(). + */ + public function deleteAll() { + $this->data = array(); + } } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 6370033..7320a74 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -439,6 +439,35 @@ protected function assertNotIdentical($first, $second, $message = '', $group = ' } /** + * Checks to see if two objects are identical. + * + * @param object $object1 + * The first object to check. + * @param object $object2 + * The second object to check. + * @param $message + * The message to display along with the assertion. + * @param $group + * The type of assertion - examples are "Browser", "PHP". + * + * @return + * TRUE if the assertion succeeded, FALSE otherwise. + */ + protected function assertIdenticalObject($object1, $object2, $message = '', $group = '') { + $message = $message ?: format_string('!object1 is identical to !object2', array( + '!object1' => var_export($object1, TRUE), + '!object2' => var_export($object2, TRUE), + )); + $identical = TRUE; + foreach ($object1 as $key => $value) { + $identical = $identical && isset($object2->$key) && $object2->$key === $value; + } + return $this->assertTrue($identical, $message); + } + + + + /** * Fire an assertion that is always positive. * * @param $message @@ -940,6 +969,26 @@ public static function randomName($length = 8) { } /** + * Generates a random PHP object. + * + * @param int $size + * The number of random keys to add to the object. + * + * @return \stdClass + * The generated object, with the specified number of random keys. Each key + * has a random string value. + */ + public static function randomObject($size = 4) { + $object = new \stdClass(); + for ($i = 0; $i < $size; $i++) { + $random_key = self::randomName(); + $random_value = self::randomString(); + $object->{$random_key} = $random_value; + } + return $object; + } + + /** * Converts a list of possible parameters into a stack of permutations. * * Takes a list of parameters containing possible values, and converts all of diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 8331d55..a177b3e 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -832,6 +832,44 @@ function system_schema() { 'primary key' => array('collection', 'name'), ); + $schema['key_value_expire'] = array( + 'description' => 'Generic key/value storage table with an expiration.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + // KEY is an SQL reserved word, so use 'name' as the key's field name. + 'description' => 'The key of the key/value pair.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value of the key/value pair.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + 'expire' => array( + 'description' => 'The time since Unix epoch in seconds when this item expires.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('collection', 'name'), + 'indexes' => array( + 'all' => array('name', 'collection', 'expire'), + ), + ); + $schema['menu_router'] = array( 'description' => 'Maps paths to various callbacks (access, page and title)', 'fields' => array( @@ -2102,6 +2140,51 @@ function system_update_8022() { } /** + * Create the 'key_value_expire' table. + */ +function system_update_8023() { + $table = array( + 'description' => 'Generic key/value storage table with an expiration.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + // KEY is an SQL reserved word, so use 'name' as the key's field name. + 'description' => 'The key of the key/value pair.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value of the key/value pair.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + 'expire' => array( + 'description' => 'The time since Unix epoch in seconds when this item expires.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('collection', 'name'), + 'indexes' => array( + 'all' => array('name', 'collection', 'expire'), + ), + ); + + db_create_table('key_value_expire', $table); +} + +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index 0001cba..a697eef 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -234,5 +234,5 @@ function hook_themes_enabled($theme_list) { */ function hook_themes_disabled($theme_list) { // Clear all update module caches. - _update_cache_clear(); + update_storage_clear(); } diff --git a/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php b/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php index 5dc09fd..dd9626b 100644 --- a/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php +++ b/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php @@ -210,8 +210,8 @@ function testFetchTasks() { update_create_fetch_task($projecta); $this->assertEqual($queue->numberOfItems(), 2, 'Queue still contains two items'); - // Clear cache and try again. - _update_cache_clear(); + // Clear storage and try again. + update_storage_clear(); drupal_static_reset('_update_create_fetch_task'); update_create_fetch_task($projecta); $this->assertEqual($queue->numberOfItems(), 2, 'Queue contains two items'); diff --git a/core/modules/update/update.authorize.inc b/core/modules/update/update.authorize.inc index d22f235..c074ff8 100644 --- a/core/modules/update/update.authorize.inc +++ b/core/modules/update/update.authorize.inc @@ -10,6 +10,9 @@ * the Update Manager module. */ +use Drupal\Core\Database\Database; +use Drupal\Core\FileTransfer\FileTransfer; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableFactory; use Drupal\Core\Updater\UpdaterException; /** @@ -177,7 +180,7 @@ function update_authorize_batch_copy_project($project, $updater_name, $local_url * * This processes the results and stashes them into SESSION such that * authorize.php will render a report. Also responsible for putting the site - * back online and clearing the update status cache after a successful update. + * back online and clearing the update status storage after a successful update. * * @param $success * TRUE if the batch operation was successful; FALSE if there were errors. @@ -192,8 +195,8 @@ function update_authorize_update_batch_finished($success, $results) { } $offline = config('system.maintenance')->get('enabled'); if ($success) { - // Now that the update completed, we need to clear the cache of available - // update data and recompute our status, so prevent show bogus results. + // Now that the update completed, we need to clear the available update data + // and recompute our status, so prevent show bogus results. _update_authorize_clear_update_status(); // Take the site out of maintenance mode if it was previously that way. @@ -314,25 +317,23 @@ function _update_batch_create_message(&$project_results, $message, $success = TR } /** - * Clears cached available update status data. + * Clears available update status data. * * Since this function is run at such a low bootstrap level, the Update Manager - * module is not loaded. So, we can't just call _update_cache_clear(). However, - * the database is bootstrapped, so we can do a query ourselves to clear out - * what we want to clear. + * module is not loaded. So, we can't just call update_storage_clear(). However, + * the key-value backend is available, so we just call that. * - * Note that we do not want to just truncate the table, since that would remove - * items related to currently pending fetch attempts. + * Note that we do not want to delete items related to currently pending fetch + * attempts. * * @see update_authorize_update_batch_finished() - * @see _update_cache_clear() + * @see update_storage_clear() */ function _update_authorize_clear_update_status() { - $query = db_delete('cache_update'); - $query->condition( - db_or() - ->condition('cid', 'update_project_%', 'LIKE') - ->condition('cid', 'available_releases::%', 'LIKE') - ); - $query->execute(); + // @todo: keyvaluestore.expirable is defined in CoreBundle, which is not + // loaded in authorize.php. So we have to hardcode it. This does not work + // because this is pluggable, so it needs to be pluggable in any case. + $factory = new KeyValueStoreExpirableFactory(Database::getConnection()); + $factory->get('update')->deleteAll(); + $factory->get('update_available_releases')->deleteAll(); } diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc index 2262404..6510bb6 100644 --- a/core/modules/update/update.compare.inc +++ b/core/modules/update/update.compare.inc @@ -16,13 +16,12 @@ * fetching the available release data. * * This array is fairly expensive to construct, since it involves a lot of disk - * I/O, so we cache the results into the {cache_update} table using the - * 'update_project_projects' cache ID. However, since this is not the data about + * I/O, so we store the results. However, since this is not the data about * available updates fetched from the network, it is acceptable to invalidate it * somewhat quickly. If we keep this data for very long, site administrators are * more likely to see incorrect results if they upgrade to a newer version of a * module or theme but do not visit certain pages that automatically clear this - * cache. + * data. * * @return * An associative array of currently enabled projects keyed by the @@ -51,15 +50,15 @@ * * @see update_process_project_info() * @see update_calculate_project_data() - * @see update_project_cache() + * @see update_project_storage() */ function update_get_projects() { $projects = &drupal_static(__FUNCTION__, array()); if (empty($projects)) { - // Retrieve the projects from cache, if present. - $projects = update_project_cache('update_project_projects'); + // Retrieve the projects from storage, if present. + $projects = update_project_storage('update_project_projects'); if (empty($projects)) { - // Still empty, so we have to rebuild the cache. + // Still empty, so we have to rebuild. $module_data = system_rebuild_module_data(); $theme_data = system_rebuild_theme_data(); update_process_info_list($projects, $module_data, 'module', TRUE); @@ -70,8 +69,8 @@ function update_get_projects() { } // Allow other modules to alter projects before fetching and comparing. drupal_alter('update_projects', $projects); - // Cache the site's project data for at most 1 hour. - _update_cache_set('update_project_projects', $projects, REQUEST_TIME + 3600); + // Store the site's project data for at most 1 hour. + drupal_container()->get('keyvaluestore.expirable')->get('update')->setWithExpire('update_project_projects', $projects, 3600); } } return $projects; @@ -89,7 +88,7 @@ function update_get_projects() { * enabled code. Modules and themes set as hidden are always ignored. This * function also records the latest change time on the .info files for each * module or theme, which is important data which is used when deciding if the - * cached available update data should be invalidated. + * available update data should be invalidated. * * @param $projects * Reference to the array of project data of what's installed on this site. @@ -316,13 +315,12 @@ function update_process_project_info(&$projects) { * * The results of this function are expensive to compute, especially on sites * with lots of modules or themes, since it involves a lot of comparisons and - * other operations. Therefore, we cache the results into the {cache_update} - * table using the 'update_project_data' cache ID. However, since this is not + * other operations. Therefore, we store the results. However, since this is not * the data about available updates fetched from the network, it is ok to * invalidate it somewhat quickly. If we keep this data for very long, site * administrators are more likely to see incorrect results if they upgrade to a * newer version of a module or theme but do not visit certain pages that - * automatically clear this cache. + * automatically clear this. * * @param array $available * Data about available project releases. @@ -333,13 +331,13 @@ function update_process_project_info(&$projects) { * @see update_get_available() * @see update_get_projects() * @see update_process_project_info() - * @see update_project_cache() + * @see update_project_storage() */ function update_calculate_project_data($available) { - // Retrieve the projects from cache, if present. - $projects = update_project_cache('update_project_data'); - // If $projects is empty, then the cache must be rebuilt. - // Otherwise, return the cached data and skip the rest of the function. + // Retrieve the projects from storage, if present. + $projects = update_project_storage('update_project_data'); + // If $projects is empty, then the data must be rebuilt. + // Otherwise, return the data and skip the rest of the function. if (!empty($projects)) { return $projects; } @@ -359,8 +357,8 @@ function update_calculate_project_data($available) { // projects or releases). drupal_alter('update_status', $projects); - // Cache the site's update status for at most 1 hour. - _update_cache_set('update_project_data', $projects, REQUEST_TIME + 3600); + // Store the site's update status for at most 1 hour. + drupal_container()->get('keyvaluestore.expirable')->get('update')->setWithExpire('update_project_data', $projects, 3600); return $projects; } @@ -750,37 +748,36 @@ function update_calculate_project_update_status(&$project_data, $available) { } /** - * Retrieves data from {cache_update} or empties the cache when necessary. + * Retrieves update storage data or empties it. * * Two very expensive arrays computed by this module are the list of all * installed modules and themes (and .info data, project associations, etc), and * the current status of the site relative to the currently available releases. - * These two arrays are cached in the {cache_update} table and used whenever - * possible. The cache is cleared whenever the administrator visits the status - * report, available updates report, or the module or theme administration - * pages, since we should always recompute the most current values on any of - * those pages. + * These two arrays are stored and used whenever possible. The data is cleared + * whenever the administrator visits the status report, available updates + * report, or the module or theme administration pages, since we should always + * recompute the most current values on any of those pages. * * Note: while both of these arrays are expensive to compute (in terms of disk * I/O and some fairly heavy CPU processing), neither of these is the actual * data about available updates that we have to fetch over the network from - * updates.drupal.org. That information is stored with the - * 'update_available_releases' cache ID -- it needs to persist longer than 1 + * updates.drupal.org. That information is stored in the + * 'update_available_releases' collection -- it needs to persist longer than 1 * hour and never get invalidated just by visiting a page on the site. * - * @param $cid - * The cache ID of data to return from the cache. Valid options are - * 'update_project_data' and 'update_project_projects'. + * @param $key + * The key of data to return. Valid options are 'update_project_data' and + * 'update_project_projects'. * * @return - * The cached value of the $projects array generated by + * The stored value of the $projects array generated by * update_calculate_project_data() or update_get_projects(), or an empty array - * when the cache is cleared. + * when the storage is cleared. */ -function update_project_cache($cid) { +function update_project_storage($key) { $projects = array(); - // On certain paths, we should clear the cache and recompute the projects for + // On certain paths, we should clear the data and recompute the projects for // update status of the site to avoid presenting stale information. $paths = array( 'admin/modules', @@ -794,13 +791,10 @@ function update_project_cache($cid) { 'admin/reports/updates/check', ); if (in_array(current_path(), $paths)) { - _update_cache_clear($cid); + drupal_container()->get('keyvaluestore.expirable')->get('update')->delete($key); } else { - $cache = _update_cache_get($cid); - if (!empty($cache->data) && $cache->expire > REQUEST_TIME) { - $projects = $cache->data; - } + $projects = drupal_container()->get('keyvaluestore.expirable')->get('update')->get($key); } return $projects; } diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc index a4d1bd7..4613b6f 100644 --- a/core/modules/update/update.fetch.inc +++ b/core/modules/update/update.fetch.inc @@ -117,8 +117,8 @@ function _update_fetch_data() { /** * Processes a task to fetch available update data for a single project. * - * Once the release history XML data is downloaded, it is parsed and saved into - * the {cache_update} table in an entry just for that project. + * Once the release history XML data is downloaded, it is parsed and saved in an + * entry just for that project. * * @param $project * Associative array of information about the project to fetch data for. @@ -132,13 +132,11 @@ function _update_process_fetch_task($project) { $fail = &drupal_static(__FUNCTION__, array()); // This can be in the middle of a long-running batch, so REQUEST_TIME won't // necessarily be valid. - $now = time(); + $request_time_difference = time() - REQUEST_TIME; if (empty($fail)) { // If we have valid data about release history XML servers that we have - // failed to fetch from on previous attempts, load that from the cache. - if (($cache = _update_cache_get('fetch_failures')) && ($cache->expire > $now)) { - $fail = $cache->data; - } + // failed to fetch from on previous attempts, load that. + $fail = drupal_container()->get('keyvaluestore.expirable')->get('update')->get('fetch_failures'); } $max_fetch_attempts = $update_config->get('fetch.max_attempts'); @@ -179,43 +177,43 @@ function _update_process_fetch_task($project) { } $frequency = $update_config->get('check.interval_days'); - $cid = 'available_releases::' . $project_name; - _update_cache_set($cid, $available, $now + (60 * 60 * 24 * $frequency)); + $available['last_fetch'] = REQUEST_TIME + $request_time_difference; + drupal_container()->get('keyvaluestore.expirable')->get('update_available_releases')->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency)); // Stash the $fail data back in the DB for the next 5 minutes. - _update_cache_set('fetch_failures', $fail, $now + (60 * 5)); + drupal_container()->get('keyvaluestore.expirable')->get('update')->setWithExpire('fetch_failures', $fail, $request_time_difference + (60 * 5)); // Whether this worked or not, we did just (try to) check for updates. - variable_set('update_last_check', $now); + variable_set('update_last_check', REQUEST_TIME + $request_time_difference); // Now that we processed the fetch task for this project, clear out the - // record in {cache_update} for this task so we're willing to fetch again. - _update_cache_clear('fetch_task::' . $project_name); + // record for this task so we're willing to fetch again. + drupal_container()->get('keyvaluestore')->get('update_fetch_task')->delete($project_name); return $success; } /** - * Clears out all the cached available update data and initiates re-fetching. + * Clears out all the available update data and initiates re-fetching. */ function _update_refresh() { module_load_include('inc', 'update', 'update.compare'); // Since we're fetching new available update data, we want to clear - // our cache of both the projects we care about, and the current update - // status of the site. We do *not* want to clear the cache of available + // both the projects we care about, and the current update + // status of the site. We do *not* want to clear the data of available // releases just yet, since that data (even if it's stale) can be useful // during update_get_projects(); for example, to modules that implement // hook_system_info_alter() such as cvs_deploy. - _update_cache_clear('update_project_projects'); - _update_cache_clear('update_project_data'); + drupal_container()->get('keyvaluestore.expirable')->get('update')->delete('update_project_projects'); + drupal_container()->get('keyvaluestore.expirable')->get('update')->delete('update_project_data'); $projects = update_get_projects(); - // Now that we have the list of projects, we should also clear our cache of - // available release data, since even if we fail to fetch new data, we need + // Now that we have the list of projects, we should also clear the available + // release data, since even if we fail to fetch new data, we need // to clear out the stale data at this point. - _update_cache_clear('available_releases::', TRUE); + drupal_container()->get('keyvaluestore.expirable')->get('update_available_releases')->deleteAll(); foreach ($projects as $key => $project) { update_create_fetch_task($project); @@ -226,8 +224,7 @@ function _update_refresh() { * Adds a task to the queue for fetching release history data for a project. * * We only create a new fetch task if there's no task already in the queue for - * this particular project (based on 'fetch_task::' entries in the - * {cache_update} table). + * this particular project (based on 'update_fetch_task' key-value collection). * * @param $project * Associative array of information about a project as created by @@ -243,29 +240,14 @@ function _update_refresh() { function _update_create_fetch_task($project) { $fetch_tasks = &drupal_static(__FUNCTION__, array()); if (empty($fetch_tasks)) { - $fetch_tasks = _update_get_cache_multiple('fetch_task'); + $fetch_tasks = drupal_container()->get('keyvaluestore')->get('update_fetch_task')->getAll(); } - $cid = 'fetch_task::' . $project['name']; - if (empty($fetch_tasks[$cid])) { + debug($fetch_tasks); + if (empty($fetch_tasks[$project['name']])) { $queue = queue('update_fetch_tasks'); $queue->createItem($project); - // Due to race conditions, it is possible that another process already - // inserted a row into the {cache_update} table and the following query will - // throw an exception. - // @todo: Remove the need for the manual check by relying on a queue that - // enforces unique items. - try { - db_insert('cache_update') - ->fields(array( - 'cid' => $cid, - 'created' => REQUEST_TIME, - )) - ->execute(); - } - catch (Exception $e) { - // The exception can be ignored safely. - } - $fetch_tasks[$cid] = REQUEST_TIME; + drupal_container()->get('keyvaluestore')->get('update_fetch_task')->set($project['name'], $project); + $fetch_tasks[$project['name']] = REQUEST_TIME; } } diff --git a/core/modules/update/update.install b/core/modules/update/update.install index 14efc3a..b4807a7 100644 --- a/core/modules/update/update.install +++ b/core/modules/update/update.install @@ -57,15 +57,6 @@ function update_requirements($phase) { } /** - * Implements hook_schema(). - */ -function update_schema() { - $schema['cache_update'] = drupal_get_schema_unprocessed('system', 'cache'); - $schema['cache_update']['description'] = 'Cache table for the Update module to store information about available releases, fetched from central server.'; - return $schema; -} - -/** * Implements hook_install(). */ function update_install() { @@ -162,3 +153,10 @@ function update_update_8000() { 'update_notification_threshold' => 'notification.threshold', )); } + +/** + * Deletes the {cache_update} table. + */ +function update_update_8001() { + db_drop_table('cache_update'); +} diff --git a/core/modules/update/update.module b/core/modules/update/update.module index 51e77e6..7cfd5d2 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -1,5 +1,8 @@ $interval) { // If the configured update interval has elapsed, we want to invalidate - // the cached data for all projects, attempt to re-fetch, and trigger any + // the data for all projects, attempt to re-fetch, and trigger any // configured notifications about the new status. update_refresh(); update_fetch_data(); @@ -311,33 +314,33 @@ function update_cron() { /** * Implements hook_themes_enabled(). * - * If themes are enabled, we invalidate the cache of available updates. + * If themes are enabled, we invalidate the information of available updates. */ function update_themes_enabled($themes) { - // Clear all update module caches. - _update_cache_clear(); + // Clear all update module data. + update_storage_clear(); } /** * Implements hook_themes_disabled(). * - * If themes are disabled, we invalidate the cache of available updates. + * If themes are disabled, we invalidate the information of available updates. */ function update_themes_disabled($themes) { - // Clear all update module caches. - _update_cache_clear(); + // Clear all update module data. + update_storage_clear(); } /** * Implements hook_form_FORM_ID_alter() for system_modules(). * * Adds a form submission handler to the system modules form, so that if a site - * admin saves the form, we invalidate the cache of available updates. + * admin saves the form, we invalidate the information of available updates. * * @see _update_cache_clear() */ function update_form_system_modules_alter(&$form, $form_state) { - $form['#submit'][] = 'update_cache_clear_submit'; + $form['#submit'][] = 'update_storage_clear_submit'; } /** @@ -345,9 +348,9 @@ function update_form_system_modules_alter(&$form, $form_state) { * * @see update_form_system_modules_alter() */ -function update_cache_clear_submit($form, &$form_state) { - // Clear all update module caches. - _update_cache_clear(); +function update_storage_clear_submit($form, &$form_state) { + // Clear all update module data. + update_storage_clear(); } /** @@ -362,9 +365,9 @@ function _update_no_data() { } /** - * Tries to get update information from cache and refreshes it when necessary. + * Tries to get update information and refreshes it when necessary. * - * In addition to checking the cache lifetime, this function also ensures that + * In addition to checking the lifetime, this function also ensures that * there are no .info files for enabled modules or themes that have a newer * modification timestamp than the last time we checked for available update * data. If any .info file was modified, it almost certainly means a new version @@ -373,8 +376,8 @@ function _update_no_data() { * results. * * @param $refresh - * (optional) Boolean to indicate if this method should refresh the cache - * automatically if there's no data. Defaults to FALSE. + * (optional) Boolean to indicate if this method should refresh automatically + * if there's no data. Defaults to FALSE. * * @return * Array of data about available releases, keyed by project shortname. @@ -386,9 +389,8 @@ function update_get_available($refresh = FALSE) { module_load_include('inc', 'update', 'update.compare'); $needs_refresh = FALSE; - // Grab whatever data we currently have cached in the DB. - $available = _update_get_cached_available_releases(); - $num_avail = count($available); + // Grab whatever data we currently have. + $available = drupal_container()->get('keyvaluestore.expirable')->get('update_available_releases')->getAll(); $projects = update_get_projects(); foreach ($projects as $key => $project) { @@ -426,8 +428,8 @@ function update_get_available($refresh = FALSE) { // Attempt to drain the queue of fetch tasks. update_fetch_data(); // After processing the queue, we've (hopefully) got better data, so pull - // the latest from the cache again and use that directly. - $available = _update_get_cached_available_releases(); + // the latest data again and use that directly. + $available = drupal_container()->get('keyvaluestore.expirable')->get('update_available_releases')->getAll(); } return $available; @@ -468,29 +470,6 @@ function update_fetch_data() { } /** - * Returns all currently cached data about available releases for all projects. - * - * @return - * Array of data about available releases, keyed by project shortname. - */ -function _update_get_cached_available_releases() { - $data = array(); - $cache_items = _update_get_cache_multiple('available_releases'); - foreach ($cache_items as $cid => $cache) { - $cache->data['last_fetch'] = $cache->created; - if ($cache->expire < REQUEST_TIME) { - $cache->data['fetch_status'] = UPDATE_FETCH_PENDING; - } - // The project shortname is embedded in the cache ID, even if there's no - // data for this project in the DB at all, so use that for the indexes in - // the array. - $parts = explode('::', $cid, 2); - $data[$parts[1]] = $cache->data; - } - return $data; -} - -/** * Implements hook_mail(). * * Constructs the e-mail notification message when the site is out of date. @@ -725,137 +704,11 @@ function update_verify_update_archive($project, $archive_file, $directory) { } /** - * @defgroup update_status_cache Private update status cache system - * @{ - * Functions to manage the update status cache. - * - * We specifically do NOT use the core cache API for saving the fetched data - * about available updates. It is vitally important that this cache is only - * cleared when we're populating it after successfully fetching new available - * update data. Usage of the core cache API results in all sorts of potential - * problems that would result in attempting to fetch available update data all - * the time, including if a site has a "minimum cache lifetime" (which is both a - * minimum and a maximum) defined, or if a site uses memcache or another - * pluggable cache system that assumes volatile caches. - * - * The Update Manager module still uses the {cache_update} table, but instead of - * using the cache API, there are private helper functions that implement these - * same basic tasks but ensure that the cache is not prematurely cleared, and - * that the data is always stored in the database, even if memcache or another - * cache backend is in use. - */ - -/** - * Stores data in the private update status cache table. - * - * @param $cid - * The cache ID to save the data with. - * @param $data - * The data to store. - * @param $expire - * One of the following values: - * - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should - * never be removed except by explicitly using _update_cache_clear(). - * - A Unix timestamp: Indicates that the item should be kept at least until - * the given time, after which it will be invalidated. - * - * @see _update_cache_get() - */ -function _update_cache_set($cid, $data, $expire) { - $fields = array( - 'created' => REQUEST_TIME, - 'expire' => $expire, - ); - if (!is_string($data)) { - $fields['data'] = serialize($data); - $fields['serialized'] = 1; - } - else { - $fields['data'] = $data; - $fields['serialized'] = 0; - } - db_merge('cache_update') - ->key(array('cid' => $cid)) - ->fields($fields) - ->execute(); -} - -/** - * Retrieves data from the private update status cache table. - * - * @param $cid - * The cache ID to retrieve. - * - * @return - * An array of data for the given cache ID, or NULL if the ID was not found. - * - * @see _update_cache_set() - */ -function _update_cache_get($cid) { - $cache = db_query("SELECT data, created, expire, serialized FROM {cache_update} WHERE cid = :cid", array(':cid' => $cid))->fetchObject(); - if (isset($cache->data)) { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); - } - } - return $cache; -} - -/** - * Returns an array of cache items with a given cache ID prefix. - * - * @param string $cid_prefix - * The cache ID prefix. - * - * @return - * Associative array of cache items, keyed by cache ID. - */ -function _update_get_cache_multiple($cid_prefix) { - $data = array(); - $result = db_select('cache_update') - ->fields('cache_update', array('cid', 'data', 'created', 'expire', 'serialized')) - ->condition('cache_update.cid', $cid_prefix . '::%', 'LIKE') - ->execute(); - foreach ($result as $cache) { - if ($cache) { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); - } - $data[$cache->cid] = $cache; - } - } - return $data; -} - -/** - * Invalidates cached data relating to update status. - * - * @param $cid - * (optional) Cache ID of the record to clear from the private update module - * cache. If empty, all records will be cleared from the table except fetch - * tasks. Defaults to NULL. - * @param $wildcard - * (optional) If TRUE, cache IDs starting with $cid are deleted in addition to - * the exact cache ID specified by $cid. Defaults to FALSE. + * Invalidates stored data relating to update status. */ -function _update_cache_clear($cid = NULL, $wildcard = FALSE) { - if (empty($cid)) { - db_delete('cache_update') - // Clear everything except fetch task information because these are used - // to ensure that the fetch task queue items are not added multiple times. - ->condition('cid', 'fetch_task::%', 'NOT LIKE') - ->execute(); - } - else { - $query = db_delete('cache_update'); - if ($wildcard) { - $query->condition('cid', $cid . '%', 'LIKE'); - } - else { - $query->condition('cid', $cid); - } - $query->execute(); - } +function update_storage_clear() { + drupal_container()->get('keyvaluestore.expirable')->get('update')->deleteAll(); + drupal_container()->get('keyvaluestore.expirable')->get('update_available_release')->deleteAll(); } /** @@ -864,8 +717,8 @@ function _update_cache_clear($cid = NULL, $wildcard = FALSE) { * Called from update.php (among others) to flush the caches. Since we're * running update.php, we are likely to install a new version of something, in * which case, we want to check for available update data again. However, - * because we have our own caching system, we need to directly clear the - * the database table ourselves at this point and return nothing. + * because we use the key value storage system, we need to clear directly at + * this point and return nothing. * * However, we only want to do this from update.php, since otherwise, we'd lose * all the available update data on every cron run. So, we specifically check if @@ -874,16 +727,17 @@ function _update_cache_clear($cid = NULL, $wildcard = FALSE) { */ function update_cache_flush() { if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') { - _update_cache_clear(); + // @todo: keyvaluestore.expirable is defined in CoreBundle, which is not + // loaded in update.php. So we have to hardcode it. This does not work + // because this is pluggable, so it needs to be pluggable in any case. + $factory = new KeyValueStoreExpirableFactory(Database::getConnection()); + $factory->get('update')->deleteAll(); + $factory->get('update_available_releases')->deleteAll(); } return array(); } /** - * @} End of "defgroup update_status_cache". - */ - -/** * Returns a short unique identifier for this Drupal installation. * * @return diff --git a/core/modules/update/update.settings.inc b/core/modules/update/update.settings.inc index 0a5e337..442510b 100644 --- a/core/modules/update/update.settings.inc +++ b/core/modules/update/update.settings.inc @@ -92,7 +92,7 @@ function update_settings_validate($form, &$form_state) { /** * Form submission handler for update_settings(). * - * Also invalidates the cache of available updates if the "Check for updates of + * Also invalidates the data of available updates if the "Check for updates of * disabled modules and themes" setting is being changed. The available updates * report needs to refetch available update data after this setting changes or * it would show misleading things (e.g., listing the disabled projects on the @@ -104,9 +104,9 @@ function update_settings_submit($form, $form_state) { $config = config('update.settings'); // See if the update_check_disabled setting is being changed, and if so, - // invalidate all cached update status data. + // invalidate all update status data. if ($form_state['values']['update_check_disabled'] != $config->get('check.disabled_extensions')) { - _update_cache_clear(); + update_storage_clear(); } $config diff --git a/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwner.php b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwner.php new file mode 100644 index 0000000..24478c9 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwner.php @@ -0,0 +1,155 @@ +storage = $storage; + $this->lockBackend = $lockBackend; + $this->owner = $owner; + } + + /** + * Returns the stored value for the default key. + * + * @param string $key + * The key of the data to retrieve. + * + * @return mixed + * The data to retieve, or NULL if the key does not exist. + */ + function get($key) { + if ($object = $this->storage->get($key)) { + return $object->data; + } + } + + /** + * Adds the value for the default key if it doesn't exist yet. + * + * @param string $key + * The key of the data to check and store. + * @param mixed $value + * The data to store. + * + * @return bool + * TRUE if the data was set, FALSE if it already existed. + */ + function setIfNotExists($key, $value) { + $value = (object) array( + 'owner' => $this->owner, + 'data' => $value, + 'updated' => REQUEST_TIME, + ); + return $this->storage->setWithExpireIfNotExists($key, $value, $this->expire); + } + + /** + * Sets the value for the default key. + * + * @param string $key + * The key of the data to store. + * @param mixed $value + * The data to store. + */ + function set($key, $value) { + if ($this->lockBackend->acquire($key)) { + $object = $this->storage->get($key); + if (!$object || $object->owner == $this->owner) { + $value = (object) array( + 'owner' => $this->owner, + 'data' => $value, + 'updated' => REQUEST_TIME, + ); + $this->storage->setWithExpire($key, $value, $this->expire); + } + $this->lockBackend->release($key); + } + } + + /** + * Gets the metadata for a key. + * + * @param string $key + * The key of the data to store. + * + * @return mixed + * An object with the owner and updated time if the key has a value, NULL + * otherwise. + */ + function getMetadata($key) { + $object = $this->storage->get($key); + if ($object) { + unset($object->data); + return $object; + } + } + + /** + * Deletes the value of the default key. + * + * @param string $key + * The key of the data to store. + */ + function delete($key) { + if (!$this->lockBackend->acquire($key)) { + $this->lockBackend->wait($key); + if (!$this->lockBackend->acquire($key)) { + throw new KeyValueStoreWithOwnerException("Couldn't acquire lock"); + } + } + $this->storage->delete($key); + $this->lockBackend->release($key); + } + +} diff --git a/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwnerException.php b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwnerException.php new file mode 100644 index 0000000..4a35e23 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwnerException.php @@ -0,0 +1,13 @@ +connection = $connection; + $this->lockBackend = $lockBackend; + } + + /** + * Creates a Drupal\user\KeyValueStoreWithOwner stored in the database. + * + * @param string $namespace + * The namespace to use for this key/value store. + * + * @return Drupal\user\KeyValueStoreWithOwner + * An instance of the the key/value store. + */ + function get($namespace) { + $storage = new DatabaseStorageExpirable($namespace, array('connection' => $this->connection)); + return new KeyValueStoreWithOwner($storage, $this->lockBackend, $GLOBALS['user']->uid ?: session_id()); + } + +} diff --git a/core/modules/user/lib/Drupal/user/TempStore.php b/core/modules/user/lib/Drupal/user/TempStore.php new file mode 100644 index 0000000..4b1c9d2 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/TempStore.php @@ -0,0 +1,189 @@ +storage = $storage; + $this->lockBackend = $lockBackend; + $this->owner = $owner; + } + + /** + * Retrieves a value from this TempStore for a given key. + * + * @param string $key + * The key of the data to retrieve. + * + * @return mixed + * The data associated with the key, or NULL if the key does not exist. + */ + function get($key) { + if ($object = $this->storage->get($key)) { + return $object->data; + } + } + + /** + * Stores a particular key/value pair only if the key doesn't already exist. + * + * @param string $key + * The key of the data to check and store. + * @param mixed $value + * The data to store. + * + * @return bool + * TRUE if the data was set, or FALSE if it already existed. + */ + function setIfNotExists($key, $value) { + $value = (object) array( + 'owner' => $this->owner, + 'data' => $value, + 'updated' => REQUEST_TIME, + ); + return $this->storage->setWithExpireIfNotExists($key, $value, $this->expire); + } + + /** + * Stores a particular key/value pair in this TempStore. + * + * If the key already exists in storage for this TempStore's collection, + * only the stored owner may update it. + * + * @param string $key + * The key of the data to store. + * @param mixed $value + * The data to store. + * + * @todo Should we throw an exception here if the lock cannot be acquired + * like we do in delete()? + * @todo Should we return something here so we know whether the operation + * was successful? + */ + function set($key, $value) { + if ($this->lockBackend->acquire($key)) { + // Retrieve the object from storage if it already exists. + $object = $this->storage->get($key); + // Store the object if this TempStore's owner already owns it or if it + // is not yet set. + if (!$object || $object->owner == $this->owner) { + $value = (object) array( + 'owner' => $this->owner, + 'data' => $value, + 'updated' => REQUEST_TIME, + ); + $this->storage->setWithExpire($key, $value, $this->expire); + } + $this->lockBackend->release($key); + } + } + + /** + * Gets the metadata associated with a particular key/value pair. + * + * @param string $key + * The key of the data to store. + * + * @return mixed + * An object with the owner and updated time if the key has a value, or + * NULL otherwise. + */ + function getMetadata($key) { + $object = $this->storage->get($key); + if ($object) { + unset($object->data); + return $object; + } + } + + /** + * Deletes data from the store for a given key and releases the lock on it. + * + * @param string $key + * The key of the data to delete. + * + * @todo Why does this ignore whether we're the data owner or not? + */ + function delete($key) { + if (!$this->lockBackend->acquire($key)) { + $this->lockBackend->wait($key); + if (!$this->lockBackend->acquire($key)) { + throw new TempStoreException(format_string("Couldn't acquire lock to delete item %key from %collection temporary storage.", array( + '%key' => $key, + '%collection' => $this->storage->collection, + ))); + } + } + $this->storage->delete($key); + $this->lockBackend->release($key); + } + +} diff --git a/core/modules/user/lib/Drupal/user/TempStoreException.php b/core/modules/user/lib/Drupal/user/TempStoreException.php new file mode 100644 index 0000000..497a2ef --- /dev/null +++ b/core/modules/user/lib/Drupal/user/TempStoreException.php @@ -0,0 +1,13 @@ +connection = $connection; + $this->lockBackend = $lockBackend; + } + + /** + * Creates a TempStore for the current user or anonymous session. + * + * @param string $collection + * The collection name to use for this key/value store. This is typically + * a shared namespace or module name, e.g. 'views', 'entity', etc. + * @param mixed $owner + * (optional) The owner of this TempStore. By default, the TempStore is + * owned by the currently authenticated user, or by the active anonymous + * session if no user is logged in. + * + * @return Drupal\user\TempStore + * An instance of the the key/value store. + */ + function get($collection, $owner = NULL) { + // Use the currently authenticated user ID or the active user ID unless + // the owner is overridden. + if (!isset($owner)) { + $owner = $GLOBALS['user']->uid ?: session_id(); + } + + // Store the data for this collection in the database. + $storage = new DatabaseStorageExpirable($collection, array('connection' => $this->connection)); + return new TempStore($storage, $this->lockBackend, $owner); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php b/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php new file mode 100644 index 0000000..0717abb --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php @@ -0,0 +1,131 @@ + 'TempStore', + 'description' => 'Tests the temporary object storage system.', + 'group' => 'TempStore', + ); + } + + protected function setUp() { + parent::setUp(); + + // Install system tables to test the key/value storage without installing a + // full Drupal environment. + module_load_install('system'); + $schema = system_schema(); + db_create_table('semaphore', $schema['semaphore']); + db_create_table('key_value_expire', $schema['key_value_expire']); + + // Create a key/value collection. + $this->storeFactory = new TempStoreFactory(Database::getConnection(), new DatabaseLockBackend()); + $this->collection = $this->randomName(); + + // Create four users and objects for testing. + for ($i = 0; $i <= 3; $i++) { + $this->objects[$i] = $this->randomObject(); + $this->users[$i] = mt_rand(500, 5000000); + } + } + + /** + * Tests the UserTempStore API. + */ + public function testUserTempStore() { + $key = $this->randomName(); + // Test that setIfNotExists() succeeds only the first time. + for ($i = 0; $i <= 1; $i++) { + $store = $this->getStorePerUID($this->users[$i]); + // setIfNotExists() should fail the second time ($i = 1). + $this->assertEqual(!$i, $store->setIfNotExists($key, $this->objects[$i])); + $metadata = $store->getMetadata($key); + $this->assertEqual($this->users[0], $metadata->owner); + $this->assertIdenticalObject($this->objects[0], $store->get($key)); + } + + // Remove the item and try to set it again. + $store->delete($key); + $store->setIfNotExists($key, $this->objects[1]); + // This time it should succeed. + $this->assertIdenticalObject($this->objects[1], $store->get($key)); + + // This user can update the object. + $store->set($key, $this->objects[2]); + $this->assertIdenticalObject($this->objects[2], $store->get($key)); + // But another can't. + $store = $this->getStorePerUID($this->users[2]); + $store->set($key, $this->objects[3]); + $this->assertIdenticalObject($this->objects[2], $store->get($key)); + + // Now manually expire the item (this is not exposed by the API) and then + // assert it is no longer accessible. + db_update('key_value_expire') + ->fields(array('expire' => REQUEST_TIME - 1)) + ->condition('collection', $this->collection) + ->condition('name', $key) + ->execute(); + $this->assertFalse($store->get($key)); + } + + /** + * Returns a TempStore belonging to the passed in user. + * + * @param int $uid + * A user ID. + * + * @return Drupal\user\TempStore + * The key/value store object. + */ + protected function getStorePerUID($uid) { + return $this->storeFactory->get($this->collection, $uid); + } + +}