diff --git a/core/lib/Drupal/Core/TempStore/TempStore.php b/core/lib/Drupal/Core/TempStore/TempStore.php new file mode 100644 index 0000000..be5248a --- /dev/null +++ b/core/lib/Drupal/Core/TempStore/TempStore.php @@ -0,0 +1,249 @@ +subsystem = $subsystem; + $this->ownerID = $owner_id; + } + + /** + * Fetches the data from the store. + * + * @param string $key + * The key to the stored object. See TempStore::set() for details. + * + * @return object|null + * The stored data object, or NULL if none exist. + */ + function get($key) { + $data = db_query( + 'SELECT data FROM {temp_store} WHERE owner_id = :owner_id AND subsystem = :subsystem AND temp_key = :temp_key', + array( + ':owner_id' => $this->ownerID, + ':subsystem' => $this->subsystem, + ':temp_key' => $key, + ) + ) + ->fetchObject(); + if ($data) { + return unserialize($data->data); + } + } + + /** + * Writes the data to the store. + * + * @param string $key + * The key to the object being stored. For objects that already exist in + * the database somewhere else, this is typically the primary key of that + * object. For objects that do not already exist, this is typically 'new' + * or some special key that indicates that the object does not yet exist. + * @param mixed $data + * The data to be cached. It will be serialized. + * + * @todo + * Using 'new' as a key might result in collisions if the same user tries + * to create multiple new objects simultaneously. Document a workaround? + */ + function set($key, $data) { + // Store the new data. + db_merge('temp_store') + ->key(array('temp_key' => $key)) + ->fields(array( + 'owner_id' => $this->ownerID, + 'subsystem' => $this->subsystem, + 'temp_key' => $key, + 'data' => serialize($data), + 'updated' => REQUEST_TIME, + )) + ->execute(); + } + + /** + * Removes one or more objects from this store for this owner. + * + * @param string|array $key + * The key to the stored object, or an array of keys. See + * TempStore::set() for details. + */ + function delete($key) { + $this->deleteRecords($key); + } + + /** + * Removes one or more objects from this store for all owners. + * + * @param string|array $key + * The key to the stored object, or an array of keys. See + * TempStore::set() for details. + */ + function deleteAll($key) { + $this->deleteRecords($key, TRUE); + } + + /** + * Deletes database records for objects. + * + * @param string|array $key + * The key to the stored object, or an array of keys. See + * TempStore::set() for details. + * @param bool $all + * Whether to delete all records for this key (TRUE) or just the current + * owner's (FALSE). Defaults to FALSE. + */ + protected function deleteRecords($key, $all = FALSE) { + // The query builder will automatically use an IN condition when an array + // is passed. + $query = db_delete('temp_store') + ->condition('temp_key', $key) + ->condition('subsystem', $this->subsystem); + + if (!$all) { + $query->condition('owner_id', $this->ownerID); + } + + $query->execute(); + } + + /** + * Determines if the object is in use by another store for locking purposes. + * + * @param string $key + * The key to the stored object. See TempStore::set() for details. + * @param bool $exclude_owner + * (optional) Whether or not to disregard the current user when determining + * the lock owner. Defaults to FALSE. + * + * @return stdClass|null + * An object with the user ID and updated date if found, otherwise NULL. + */ + public function getLockOwner($key) { + return db_query( + 'SELECT owner_id AS ownerID, updated FROM {temp_store} WHERE subsystem = :subsystem AND temp_key = :temp_key ORDER BY updated ASC', + array( + ':subsystem' => $this->subsystem, + ':temp_key' => $key, + ) + )->fetchObject(); + } + + /** + * Checks to see if another owner has locked the object. + * + * @param string $key + * The key to the stored object. See TempStore::set() for details. + * + * @return stdClass|null + * An object with the owner ID and updated date, or NULL if there is no + * lock on the object belonging to a different owner. + */ + public function isLocked($key) { + $lock_owner = $this->getLockOwner($key); + if ((isset($lock_owner->ownerID) && $this->ownerID != $lock_owner->ownerID)) { + return $lock_owner; + } + } + + /** + * Fetches the last updated time for multiple objects in a given subsystem. + * + * @param string $subsystem + * The module or subsystem. Possible values might include 'entity', + * 'form', 'views', etc. + * @param array $keys + * An array of keys of stored objects. See TempStore::set() for details. + * + * @return + * An associative array of objects and their last updated time, keyed by + * object key. + */ + public static function testStoredObjects($subsystem, $keys) { + return db_query( + "SELECT t.temp_key, t.updated FROM {temp_store} t WHERE t.subsystem = :subsystem AND t.temp_key IN (:keys) ORDER BY t.updated ASC", + array(':subsystem' => $subsystem, ':temp_keys' => $keys) + ) + ->fetchAllAssoc('temp_key'); + } + + /** + * Truncates all objects in all stores for a given key and subsystem. + * + * @param string $subsystem + * The module or subsystem. Possible values might include 'entity', + * 'form', 'views', etc. + * @param array $key + * The key to the stored object. See TempStore::set() for details. + */ + public static function clearAll($subsystem, $key) { + $query = db_delete('temp_store') + ->condition('temp_key', $key) + ->condition('subsystem', $subsystem); + + $query->execute(); + } + + /** + * Truncates all objects older than a certain age, for all stores. + * + * @param int $age + * The minimum age of objects to remove, in seconds. For example, 86400 is + * one day. Defaults to 7 days. + */ + public static function clearOldObjects($age = NULL) { + if (!isset($age)) { + // 7 days. + $age = 86400 * 7; + } + db_delete('temp_store') + ->condition('updated', REQUEST_TIME - $age, '<') + ->execute(); + } + +} diff --git a/core/lib/Drupal/Core/TempStore/UserTempStore.php b/core/lib/Drupal/Core/TempStore/UserTempStore.php new file mode 100644 index 0000000..21e9c0f --- /dev/null +++ b/core/lib/Drupal/Core/TempStore/UserTempStore.php @@ -0,0 +1,44 @@ +uid : session_id(); + } + + parent::__construct($subsystem, $owner_id); + } + + /** + * Overrides TempStore::set(). + */ + function set($key, $data) { + // Ensure that a session cookie is set for anonymous users. + if (!user_is_logged_in()) { + // A session is written so long as $_SESSION is not empty. Force this. + // @todo This feels really hacky. Is there a better way? + // @see http://drupalcode.org/project/ctools.git/blob/refs/heads/8.x-1.x:/includes/object-cache.inc#l69 + $_SESSION['temp_store_use_session'] = TRUE; + } + + parent::set($key, $data); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/TempStore/TempStoreTest.php b/core/modules/system/lib/Drupal/system/Tests/TempStore/TempStoreTest.php new file mode 100644 index 0000000..f4f35fd --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/TempStore/TempStoreTest.php @@ -0,0 +1,199 @@ + 'TempStore', + 'description' => 'Tests the temporary object storage system.', + 'group' => 'TempStore', + ); + } + + protected function setUp() { + parent::setUp(); + // Create two users for testing. + $this->firstUser = $this->drupalCreateUser(); + $this->secondUser = $this->drupalCreateUser(); + } + + /** + * 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 function randomObject($size = 4) { + $object = new stdClass(); + for ($i = 0; $i < $size; $i++) { + $random_key = $this->randomName(); + $random_value = $this->randomString(); + $object->{$random_key} = $random_value; + } + return $object; + } + + /** + * Tests the UserTempStore API. + */ + public function testUserTempStore() { + $this->drupalLogin($this->firstUser); + $subsystem = $this->randomName(); + $temp_store = new UserTempStore($subsystem, $this->loggedInUser->uid); + + // An empty storage should return nothing. + $this->assertFalse($temp_store->get($this->randomName())); + + // Add an additional key to the store. + $random_key = $this->randomName(); + $random_object = $this->randomObject(); + $temp_store->set($random_key, $random_object); + + // Ensure the new key is returned when fetched. + $result_data = $temp_store->get($random_key); + $this->assertEqual( + $random_object, + $result_data, + format_string( + 'Key %key is properly added to the store.', + array('%key' => $random_key) + ) + ); + + // Delete the key and ensure that it is properly removed. + $temp_store->delete($random_key); + $this->assertFalse( + $temp_store->get($random_key), + format_string( + 'Key %key is properly removed from the store.', + array('%key' => $random_key) + ) + ); + + // Create multiple keys to test the deleteAll() functionality. + $keys = array(); + for ($i = 0; $i < 4; $i++) { + $key = $this->randomName(); + $keys[] = $key; + $temp_store->set($key, $this->randomObject()); + $stored_data = $temp_store->get($key); + $this->assertTrue( + !empty($stored_data), + format_string( + 'Key %key is properly added to the store.', + array('%key' => $random_key) + ) + ); + } + $temp_store->deleteAll($keys); + foreach ($keys as $key) { + $this->assertFalse( + $temp_store->get($key), + format_string( + 'Key %key is properly removed from the store.', + array('%key' => $random_key) + ) + ); + } + + // Create two entries, one 10 days old and one 4 days old. + // 1 day = 86400 s. + $old_key = $this->randomName(); + $new_key = $this->randomName(); + $temp_store->set($old_key, $this->randomObject()); + $temp_store->set($new_key, $this->randomObject()); + // Manually change the updated time as this is not possible in the API. + foreach (array($old_key, $new_key) as $key) { + db_update('temp_store') + ->fields(array( + 'updated' => $key == $old_key ? REQUEST_TIME - (10 * 86400) : REQUEST_TIME - (4 * 86400), + )) + ->condition('temp_key', $key) + ->execute(); + } + + // Clear objects more than 7 days old. + UserTempStore::clearOldObjects(7 * 86400); + + // Confirm that the older entry is deleted and the newer is not. + $this->assertFalse( + $temp_store->get($old_key), + 'An entry more than 7 days old is cleaned.' + ); + $this->assertTrue( + $temp_store->get($new_key), + 'An entry less than 7 days old is not cleaned.' + ); + + // Test the getLockOwner() method. + $temp_store_one = new UserTempStore($subsystem, $this->loggedInUser->uid); + + // Allow two users to be logged in at once. + $this->curlClose(); + $this->loggedInUser = FALSE; + + $this->drupalLogin($this->secondUser); + + // Add another stored object to the original user's data store. + $key_first_user = $this->randomName(); + $temp_store_one->set($key_first_user, $this->randomObject()); + + // Add a data store for the second user and store an object to it. + $temp_store_two = new UserTempStore($subsystem, $this->loggedInUser->uid); + $key_second_user = $this->randomName(); + $temp_store_two->set($key_second_user, $this->randomObject()); + + // Confirm that the data is correctly saved for both users. + $this->assertEqual( + $temp_store_one->getLockOwner($key_first_user)->ownerID, + $this->firstUser->uid, + format_string( + 'Object %key is locked by user %user', + array('%key' => $key_first_user, '%user' => $this->firstUser->uid) + ) + ); + $this->assertEqual( + $temp_store_two->getLockOwner($key_second_user)->ownerID, + $this->secondUser->uid, + format_string( + 'Object %key is locked by user %user', + array('%key' => $key_first_user, '%user' => $this->firstUser->uid) + ) + ); + } + +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index b8d402c..0b7a086 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1373,6 +1373,45 @@ function system_schema() { 'primary key' => array('mlid'), ); + $schema['temp_store'] = array( + 'description' => t('A temporary data store for objects that are being edited. Allows state to be saved in a stateless environment.'), + 'fields' => array( + 'owner_id' => array( + 'type' => 'varchar', + 'length' => '64', + 'not null' => TRUE, + 'description' => 'The session ID this object belongs to.', + ), + 'subsystem' => array( + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'description' => 'The owner (type of the object) for this data store. Allows multiple subsystems to use this data store.', + ), + 'temp_key' => array( + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'description' => 'The key of the object this data store is attached to.', + ), + 'updated' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The time this data store was created or updated.', + ), + 'data' => array( + 'type' => 'text', + 'size' => 'big', + 'description' => 'Serialized data being stored.', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('owner_id', 'subsystem', 'temp_key'), + 'indexes' => array('updated' => array('updated')), + ); + $schema['queue'] = array( 'description' => 'Stores items in queues.', 'fields' => array( @@ -2062,6 +2101,52 @@ function system_update_8017() { } /** + * Create the 'temp_store' table. + */ +function system_update_8017() { + $table = array( + 'description' => t('A temporary data store for objects that are being edited. Allows state to be saved in a stateless environment.'), + 'fields' => array( + 'owner_id' => array( + 'type' => 'varchar', + 'length' => '64', + 'not null' => TRUE, + 'description' => 'The session ID this object belongs to.', + ), + 'subsystem' => array( + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'description' => 'The owner (type of the object) for this data store. Allows multiple subsystems to use this data store.', + ), + 'temp_key' => array( + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'description' => 'The key of the object this data store is attached to.', + ), + 'updated' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The time this data store was created or updated.', + ), + 'data' => array( + 'type' => 'text', + 'size' => 'big', + 'description' => 'Serialized data being stored.', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('owner_id', 'subsystem', 'temp_key'), + 'indexes' => array('updated' => array('updated')), + ); + + db_create_table('temp_store', $table); +} + +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */