diff --git a/drush/views.drush.inc b/drush/views.drush.inc index 93a0b4265c421f245e3ebd7dbcb162b30018200a..2c0aa63ce02dd23fb6ca8fd24163bfebac649db1 100644 --- a/drush/views.drush.inc +++ b/drush/views.drush.inc @@ -220,9 +220,7 @@ function views_revert_view($view) { // Revert the view. $view->delete(); // Clear its cache. - // @todo Convert this: http://drupal.org/node/1658068. - ctools_include('object-cache'); - ctools_object_cache_clear('view', $view->name); + views_temp_store()->delete($view->name); // Give feedback. $message = dt("Reverted the view '@viewname'", array('@viewname' => $view->name)); drush_log($message, 'success'); diff --git a/includes/admin.inc b/includes/admin.inc index 467c5be3def9a7f7a9029aea24c42bb2b586a126..8d9bce485f6210e19ae39b4518b6684d148bd258 100644 --- a/includes/admin.inc +++ b/includes/admin.inc @@ -6,6 +6,7 @@ */ use Drupal\Core\Database\Database; +use Drupal\views\TempStore\UserTempStore; use Drupal\views\View; use Drupal\views\Analyzer; use Drupal\views\Plugin\Type\ViewsPluginManager; @@ -853,7 +854,7 @@ function views_ui_break_lock_confirm($form, &$form_state, $view) { $cancel = $_REQUEST['cancel']; } - $account = user_load($view->locked->uid); + $account = user_load($view->locked->ownerId); return confirm_form($form, t('Are you sure you want to break the lock on view %name?', array('%name' => $view->name)), @@ -867,7 +868,7 @@ function views_ui_break_lock_confirm($form, &$form_state, $view) { * Submit handler to break_lock a view. */ function views_ui_break_lock_confirm_submit(&$form, &$form_state) { - ctools_object_cache_clear_all('view', $form_state['view']->name); + UserTempStore::clearAll('view', $form_state['view']->name); $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit'; drupal_set_message(t('The lock has been broken and you may now edit this view.')); } @@ -1027,7 +1028,7 @@ function views_ui_edit_form($form, &$form_state, $view, $display_id = NULL) { $form['locked'] = array( '#theme_wrappers' => array('container'), '#attributes' => array('class' => array('view-locked', 'messages', 'warning')), - '#markup' => t('This view is being edited by user !user, and is therefore locked from editing by others. This lock is !age old. Click here to <a href="!break">break this lock</a>.', array('!user' => theme('username', array('account' => user_load($view->locked->uid))), '!age' => format_interval(REQUEST_TIME - $view->locked->updated), '!break' => url('admin/structure/views/view/' . $view->name . '/break-lock'))), + '#markup' => t('This view is being edited by user !user, and is therefore locked from editing by others. This lock is !age old. Click here to <a href="!break">break this lock</a>.', array('!user' => theme('username', array('account' => user_load($view->locked->ownerId))), '!age' => format_interval(REQUEST_TIME - $view->locked->updated), '!break' => url('admin/structure/views/view/' . $view->name . '/break-lock'))), ); } if (isset($view->vid) && $view->vid == 'new') { @@ -2154,7 +2155,7 @@ function views_ui_edit_view_form_submit($form, &$form_state) { drupal_set_message(t('The view %name has been saved.', array('%name' => $form_state['view']->get_human_name()))); // Remove this view from cache so we can edit it properly. - ctools_object_cache_clear('view', $form_state['view']->name); + views_temp_store()->delete($form_state['view']->name); } /** @@ -2162,7 +2163,7 @@ function views_ui_edit_view_form_submit($form, &$form_state) { */ function views_ui_edit_view_form_cancel($form, &$form_state) { // Remove this view from cache so edits will be lost. - ctools_object_cache_clear('view', $form_state['view']->name); + views_temp_store()->delete($form_state['view']->name); if (empty($form['view']->vid)) { // I seem to have to drupal_goto here because I can't get fapi to // honor the redirect target. Not sure what I screwed up here. diff --git a/lib/Drupal/views/TempStore/TempStore.php b/lib/Drupal/views/TempStore/TempStore.php new file mode 100644 index 0000000000000000000000000000000000000000..2b4dcfb055c6ec0a72d6080ba479e60a70ad0ebe --- /dev/null +++ b/lib/Drupal/views/TempStore/TempStore.php @@ -0,0 +1,249 @@ +<?php + +/** + * @file + * Definition of Drupal\views\TempStore\TempStore. + */ + +namespace Drupal\views\TempStore; + +/** + * Handles reading and writing to a non-volatile temporary storage area. + * + * A TempStore is not a true cache, because it is non-volatile. While a cache + * can be reconstructed if the data disappears (i.e, a backend goes away + * or a cache is cleared), TempStore cannot tolerate the data disappearing. + * + * It is primarily used to handle in-progress edits on complicated objects + * in order to provide state to an ordinarily stateless HTTP transaction. + */ +class TempStore { + + /** + * The subsystem or module that owns this TempStore. + * + * @var string + */ + protected $subsystem; + + /** + * The unique identifier for the owner of the temporary data. + * + * In order to ensure that users do not accidentally acquire each other's + * changes, session IDs can be used to differentiate them. However, there + * are cases where session IDs are not ideal. In these cases, an + * alternative ID can be set (such as a user ID or the number 0) which + * would indicate no special session handling is required. + * + * @var string + */ + protected $ownerID; + + /** + * Constructs a temporary storage object. + * + * @param string $subsystem + * The module or subsystem. Possible values might include 'entity', + * 'form', 'views', etc. + * @param string $owner_id + * A unique identifier for the owner of the temporary storage data. + */ + function __construct($subsystem, $owner_id) { + $this->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/lib/Drupal/views/TempStore/UserTempStore.php b/lib/Drupal/views/TempStore/UserTempStore.php new file mode 100644 index 0000000000000000000000000000000000000000..253b56cd83282af4140b180a614ce108ed190e6d --- /dev/null +++ b/lib/Drupal/views/TempStore/UserTempStore.php @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Definition of Drupal\views\TempStore\UserTempStore. + */ + +namespace Drupal\views\TempStore; + +/** + * Defines a TempStore using either the user or the session as the owner ID. + */ +class UserTempStore extends TempStore { + + /** + * Overrides TempStore::__construct(). + * + * The $owner_id is given a default value of NULL. + */ + function __construct($subsystem, $owner_id = NULL) { + if (!isset($owner_id)) { + // If the user is anonymous, fall back to the session ID. + $owner_id = user_is_logged_in() ? $GLOBALS['user']->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/views.install b/views.install index 91bffbbe87f8cebf49cfd13335895a977d65b818..b986b05b63af1654fdbef3f9ca85e601446298ff 100644 --- a/views.install +++ b/views.install @@ -152,6 +152,45 @@ function views_schema() { $schema['cache_views_data']['description'] = 'Cache table for views to store pre-rendered queries, results, and display output.'; $schema['cache_views_data']['fields']['serialized']['default'] = 1; + $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')), + ); + return $schema; } diff --git a/views.module b/views.module index 61bdf8944739204770b511ebe49d4ca78a80439e..f9422e0e5287bd958ea1dffcea560a1a14937cec 100644 --- a/views.module +++ b/views.module @@ -10,6 +10,7 @@ */ use Drupal\Core\Database\Query\AlterableInterface; +use Drupal\views\TempStore\UserTempStore; use Drupal\views\View; use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\views\Plugin\Type\ViewsPluginManager; @@ -38,6 +39,16 @@ function views_init() { } } +/** + * Provides a TempStore for editing views. + * + * @return UserTempStore + * A TempStore object for the 'view' type. + */ +function views_temp_store() { + return new UserTempStore('view'); +} + /** * Implements hook_ctools_exportable_info(). */ diff --git a/views_ui.module b/views_ui.module index 2a7cdb76503613885c3f8094eb32634a9b001c38..a34b53ff041e41136474883c59550e67e2da2395 100644 --- a/views_ui.module +++ b/views_ui.module @@ -290,16 +290,15 @@ function views_ui_edit_page_title($view) { * someone else is already editing the view. */ function views_ui_cache_load($name) { - // @todo Convert this: http://drupal.org/node/1658068. - ctools_include('object-cache'); - $view = ctools_object_cache_get('view', $name); + $views_temp_store = views_temp_store(); + $view = $views_temp_store->get($name); $original_view = views_get_view($name); if (empty($view)) { $view = $original_view; if (!empty($view)) { // Check to see if someone else is already editing this view. - $view->locked = ctools_object_cache_test('view', $view->name); + $view->locked = $views_temp_store->isLocked($view->name); // Set a flag to indicate that this view is being edited. // This flag will be used e.g. to determine whether strings // should be localized. @@ -331,8 +330,7 @@ function views_ui_cache_set(&$view) { drupal_set_message(t('Changes cannot be made to a locked view.'), 'error'); return; } - // @todo Convert this: http://drupal.org/node/1658068. - ctools_include('object-cache'); + $view->changed = TRUE; // let any future object know that this view has changed. if (isset($view->current_display)) { @@ -349,7 +347,7 @@ function views_ui_cache_set(&$view) { unset($view->display[$id]->handler); unset($view->display[$id]->default_display); } - ctools_object_cache_set('view', $view->name, $view); + views_temp_store()->set($view->name, $view); }