diff --git a/drush/views.drush.inc b/drush/views.drush.inc
index 93a0b4265c4..2c0aa63ce02 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 467c5be3def..8d9bce485f6 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 break this lock.', 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 break this lock.', 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 00000000000..2b4dcfb055c
--- /dev/null
+++ b/lib/Drupal/views/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/lib/Drupal/views/TempStore/UserTempStore.php b/lib/Drupal/views/TempStore/UserTempStore.php
new file mode 100644
index 00000000000..253b56cd832
--- /dev/null
+++ b/lib/Drupal/views/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/views.install b/views.install
index 91bffbbe87f..b986b05b63a 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 61bdf894473..f9422e0e528 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 2a7cdb76503..a34b53ff041 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);
}