summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFrancesco Placella2012-10-06 23:22:37 (GMT)
committer Francesco Placella2012-10-06 23:22:37 (GMT)
commit8252e9e12e0daa6f5dc30e02e41ef60430276a94 (patch)
tree1400c93d423d2584f3a3893ff2278b70484ad14b
parent3dcc791c0301e96e5292d6b71938c9e72bf09d21 (diff)
parent5b815e3fef98a861259da7bc748b6e2f3591e3f4 (diff)
Merge remote-tracking branch 'drupal/8.x' into 8.x-et-ui-1188388-plach
-rw-r--r--core/includes/form.inc58
-rw-r--r--core/lib/Drupal/Core/CoreBundle.php5
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php29
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php133
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php52
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php13
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php11
-rw-r--r--core/modules/simpletest/lib/Drupal/simpletest/TestBase.php49
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Form/ElementsVerticalTabsTest.php24
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php2
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php10
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php46
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php167
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php73
-rw-r--r--core/modules/system/system.install83
-rw-r--r--core/modules/system/system.module1
-rw-r--r--core/modules/system/tests/modules/form_test/form_test.module33
-rw-r--r--core/modules/taxonomy/taxonomy.install124
-rw-r--r--core/modules/user/lib/Drupal/user/TempStore.php197
-rw-r--r--core/modules/user/lib/Drupal/user/TempStoreException.php13
-rw-r--r--core/modules/user/lib/Drupal/user/TempStoreFactory.php72
-rw-r--r--core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php155
-rw-r--r--core/modules/user/user.install224
23 files changed, 1366 insertions, 208 deletions
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 28a8847..5d6b32e 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -1407,16 +1407,11 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
$is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0);
$is_empty_value = ($elements['#value'] === 0);
if ($is_empty_multiple || $is_empty_string || $is_empty_value) {
- // Although discouraged, a #title is not mandatory for form elements. In
- // case there is no #title, we cannot set a form error message.
- // Instead of setting no #title, form constructors are encouraged to set
- // #title_display to 'invisible' to improve accessibility.
- if (isset($elements['#title'])) {
- form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
- }
- else {
- form_error($elements);
- }
+ // Flag this element as #required_but_empty to allow #element_validate
+ // handlers to set a custom required error message, but without having
+ // to re-implement the complex logic to figure out whether the field
+ // value is empty.
+ $elements['#required_but_empty'] = TRUE;
}
}
@@ -1431,6 +1426,27 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) {
$function($elements, $form_state, $form_state['complete_form']);
}
}
+
+ // Ensure that a #required form error is thrown, regardless of whether
+ // #element_validate handlers changed any properties. If $is_empty_value
+ // is defined, then above #required validation code ran, so the other
+ // variables are also known to be defined and we can test them again.
+ if (isset($is_empty_value) && ($is_empty_multiple || $is_empty_string || $is_empty_value)) {
+ if (isset($elements['#required_error'])) {
+ form_error($elements, $elements['#required_error']);
+ }
+ // A #title is not mandatory for form elements, but without it we cannot
+ // set a form error message. So when a visible title is undesirable, form
+ // constructors are encouraged to set #title anyway, and then set
+ // #title_display to 'invisible'. This improves accessibility.
+ elseif (isset($elements['#title'])) {
+ form_error($elements, $t('!name field is required.', array('!name' => $elements['#title'])));
+ }
+ else {
+ form_error($elements);
+ }
+ }
+
$elements['#validated'] = TRUE;
}
@@ -3815,11 +3831,25 @@ function form_process_vertical_tabs($element, &$form_state) {
*/
function theme_vertical_tabs($variables) {
$element = $variables['element'];
- // Add required JavaScript and Stylesheet.
- drupal_add_library('system', 'drupal.vertical-tabs');
+ // Even if there are no tabs the element will still have a child element for
+ // the active tab. We need to iterate over the tabs to ascertain if any
+ // are visible before showing the wrapper and h2.
+ $visible_tab = FALSE;
+ $output = '';
+ foreach (element_children($element['group']) as $tab_index) {
+ if (!isset($element['group'][$tab_index]['#access']) ||
+ !empty($element['group'][$tab_index]['#access'])) {
+ $visible_tab = TRUE;
+ break;
+ }
+ }
+ if ($visible_tab) {
+ // Add required JavaScript and Stylesheet.
+ drupal_add_library('system', 'drupal.vertical-tabs');
- $output = '<h2 class="element-invisible">' . t('Vertical Tabs') . '</h2>';
- $output .= '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
+ $output = '<h2 class="element-invisible">' . t('Vertical Tabs') . '</h2>';
+ $output .= '<div class="vertical-tabs-panes">' . $element['#children'] . '</div>';
+ }
return $output;
}
diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index c1abfe5..ed15dd5 100644
--- a/core/lib/Drupal/Core/CoreBundle.php
+++ b/core/lib/Drupal/Core/CoreBundle.php
@@ -55,6 +55,11 @@ class CoreBundle extends Bundle
->setFactoryMethod('getConnection')
->addArgument('slave');
$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..c837796 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
@@ -78,6 +91,22 @@ class DatabaseStorage extends StorageBase {
}
/**
+ * 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) {
diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php
new file mode 100644
index 0000000..eb73878
--- /dev/null
+++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\KeyValueStore\DatabaseStorageExpirable.
+ */
+
+namespace Drupal\Core\KeyValueStore;
+
+use Drupal\Core\Database\Query\Merge;
+use Drupal\Core\Database\Database;
+
+/**
+ * Defines a default key/value store implementation for expiring items.
+ *
+ * This key/value store implementation uses the database to store key/value
+ * data with an expire date.
+ */
+class DatabaseStorageExpirable extends DatabaseStorage implements KeyValueStoreExpirableInterface {
+
+ /**
+ * The connection object for this storage.
+ *
+ * @var Drupal\Core\Database\Connection
+ */
+ protected $connection;
+
+ /**
+ * Overrides Drupal\Core\KeyValueStore\StorageBase::__construct().
+ *
+ * @param string $collection
+ * The name of the collection holding key and value pairs.
+ * @param array $options
+ * An associative array of options for the key/value storage collection.
+ * Keys used:
+ * - connection: (optional) The database connection to use for storing the
+ * data. Defaults to the current connection.
+ * - table: (optional) The name of the SQL table to use. Defaults to
+ * key_value_expire.
+ */
+ public function __construct($collection, array $options = array()) {
+ parent::__construct($collection, $options);
+ $this->connection = isset($options['connection']) ? $options['connection'] : Database::getConnection();
+ $this->table = isset($options['table']) ? $options['table'] : 'key_value_expire';
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getMultiple().
+ */
+ public function getMultiple(array $keys) {
+ $values = $this->connection->query(
+ 'SELECT name, value 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,
+ ))->fetchAllKeyed();
+ return array_map('unserialize', $values);
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getAll().
+ */
+ public function getAll() {
+ $values = $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
+ ))->fetchAllKeyed();
+ return array_map('unserialize', $values);
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpireInterface::setWithExpire().
+ */
+ function setWithExpire($key, $value, $expire) {
+ $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) {
+ $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->setWithExpire($key, $value, $expire);
+ }
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple().
+ */
+ public function deleteMultiple(array $keys) {
+ $this->garbageCollection();
+ parent::deleteMultiple($keys);
+ }
+
+ /**
+ * Deletes expired items.
+ */
+ public function garbageCollection() {
+ $this->connection->delete($this->table)
+ ->condition('expire', REQUEST_TIME, '<')
+ ->execute();
+ }
+
+}
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 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface.
+ */
+
+namespace Drupal\Core\KeyValueStore;
+
+/**
+ * Defines the interface for expiring data in a key/value store.
+ */
+interface KeyValueStoreExpirableInterface extends KeyValueStoreInterface {
+
+ /**
+ * Saves a value for a given key with a time to live.
+ *
+ * @param string $key
+ * The key of the data to store.
+ * @param mixed $value
+ * The data to store.
+ * @param int $expire
+ * The time to live for items, in seconds.
+ */
+ function setWithExpire($key, $value, $expire);
+
+ /**
+ * Sets a value for a given key with a time to live if it does not yet exist.
+ *
+ * @param string $key
+ * The key of the data to store.
+ * @param mixed $value
+ * The data to store.
+ * @param int $expire
+ * The time to live for items, in seconds.
+ *
+ * @return bool
+ * TRUE if the data was set, or FALSE if it already existed.
+ */
+ function setWithExpireIfNotExists($key, $value, $expire);
+
+ /**
+ * Saves an array of values with a time to live.
+ *
+ * @param array $data
+ * An array of data to store.
+ * @param int $expire
+ * The time to live for items, in seconds.
+ */
+ function setMultipleWithExpire(array $data, $expire);
+
+}
diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php
index d350472..8f006ce 100644
--- a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php
+++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php
@@ -63,6 +63,19 @@ interface KeyValueStoreInterface {
public function set($key, $value);
/**
+ * Saves a value for a given key if it does not exist yet.
+ *
+ * @param string $key
+ * The key of the data to store.
+ * @param mixed $value
+ * The data to store.
+ *
+ * @return bool
+ * TRUE if the data was set, FALSE if it already existed.
+ */
+ public function setIfNotExists($key, $value);
+
+ /**
* Saves key/value pairs.
*
* @param array $data
diff --git a/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php b/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php
index ef3ef0e..6101507 100644
--- a/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php
+++ b/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php
@@ -48,6 +48,17 @@ class MemoryStorage extends StorageBase {
}
/**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setIfNotExists().
+ */
+ public function setIfNotExists($key, $value) {
+ if (!isset($this->data[$key])) {
+ $this->data[$key] = $value;
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
* Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setMultiple().
*/
public function setMultiple(array $data) {
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 @@ abstract class TestBase {
}
/**
+ * 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 @@ abstract class TestBase {
}
/**
+ * 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/lib/Drupal/system/Tests/Form/ElementsVerticalTabsTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/ElementsVerticalTabsTest.php
index 9c16ec3..dd96022 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/ElementsVerticalTabsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/ElementsVerticalTabsTest.php
@@ -29,6 +29,14 @@ class ElementsVerticalTabsTest extends WebTestBase {
);
}
+ function setUp() {
+ parent::setUp();
+
+ $this->admin_user = $this->drupalCreateUser(array('access vertical_tab_test tabs'));
+ $this->web_user = $this->drupalCreateUser();
+ $this->drupalLogin($this->admin_user);
+ }
+
/**
* Ensures that vertical-tabs.js is included before collapse.js.
*
@@ -40,4 +48,20 @@ class ElementsVerticalTabsTest extends WebTestBase {
$position2 = strpos($this->content, 'core/misc/collapse.js');
$this->assertTrue($position1 !== FALSE && $position2 !== FALSE && $position1 < $position2, t('vertical-tabs.js is included before collapse.js'));
}
+
+ /**
+ * Ensures that vertical tab markup is not shown if user has no tab access.
+ */
+ function testWrapperNotShownWhenEmpty() {
+ // Test admin user can see vertical tabs and wrapper.
+ $this->drupalGet('form_test/vertical-tabs');
+ $wrapper = $this->xpath("//div[@class='vertical-tabs-panes']");
+ $this->assertTrue(isset($wrapper[0]), 'Vertical tab panes found.');
+
+ // Test wrapper markup not present for non-privileged web user.
+ $this->drupalLogin($this->web_user);
+ $this->drupalGet('form_test/vertical-tabs');
+ $wrapper = $this->xpath("//div[@class='vertical-tabs-panes']");
+ $this->assertFalse(isset($wrapper[0]), 'Vertical tab wrappers are not displayed to unprivileged users.');
+ }
}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php
index 4dad937..1a499d3 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FileInclusionTest.php
@@ -38,7 +38,7 @@ class FileInclusionTest extends WebTestBase {
}
/**
- * Tests loading a custom specified inlcude.
+ * Tests loading a custom specified include.
*/
function testLoadCustomInclude() {
$this->drupalPost('form-test/load-include-custom', array(), t('Save'));
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
index 2586aa3..92898e0 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/FormTest.php
@@ -172,7 +172,15 @@ class FormTest extends WebTestBase {
// messages for each field.
$expected = array();
foreach (array('textfield', 'checkboxes', 'select', 'radios') as $key) {
- $expected[] = t('!name field is required.', array('!name' => $form[$key]['#title']));
+ if (isset($form[$key]['#required_error'])) {
+ $expected[] = $form[$key]['#required_error'];
+ }
+ elseif (isset($form[$key]['#form_test_required_error'])) {
+ $expected[] = $form[$key]['#form_test_required_error'];
+ }
+ else {
+ $expected[] = t('!name field is required.', array('!name' => $form[$key]['#title']));
+ }
}
// Check the page for error messages.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
index 7491df2..a76856b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/ValidationTest.php
@@ -186,4 +186,50 @@ class ValidationTest extends WebTestBase {
$this->drupalPost('form-test/pattern', $edit, 'Submit');
$this->assertNoRaw(t('%name field is not in the right format.', array('%name' => 'Client side validation')));
}
+
+ /**
+ * Tests #required with custom validation errors.
+ *
+ * @see form_test_validate_required_form()
+ */
+ function testCustomRequiredError() {
+ $form = $form_state = array();
+ $form = form_test_validate_required_form($form, $form_state);
+
+ // Verify that a custom #required error can be set.
+ $edit = array();
+ $this->drupalPost('form-test/validate-required', $edit, 'Submit');
+
+ foreach (element_children($form) as $key) {
+ if (isset($form[$key]['#required_error'])) {
+ $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+ $this->assertText($form[$key]['#required_error']);
+ }
+ elseif (isset($form[$key]['#form_test_required_error'])) {
+ $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+ $this->assertText($form[$key]['#form_test_required_error']);
+ }
+ }
+ $this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
+
+ // Verify that no custom validation error appears with valid values.
+ $edit = array(
+ 'textfield' => $this->randomString(),
+ 'checkboxes[foo]' => TRUE,
+ 'select' => 'foo',
+ );
+ $this->drupalPost('form-test/validate-required', $edit, 'Submit');
+
+ foreach (element_children($form) as $key) {
+ if (isset($form[$key]['#required_error'])) {
+ $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+ $this->assertNoText($form[$key]['#required_error']);
+ }
+ elseif (isset($form[$key]['#form_test_required_error'])) {
+ $this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
+ $this->assertNoText($form[$key]['#form_test_required_error']);
+ }
+ }
+ $this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
+ }
}
diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php
new file mode 100644
index 0000000..b173445
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\system\Tests\KeyValueStore\DatabaseStorageExpirableTest.
+ */
+
+namespace Drupal\system\Tests\KeyValueStore;
+
+/**
+ * Tests the key-value database storage.
+ */
+class DatabaseStorageExpirableTest extends StorageTestBase {
+
+ /**
+ * The name of the class to test.
+ *
+ * The tests themselves are in StorageTestBase and use this class.
+ */
+ protected $storageClass = 'Drupal\Core\KeyValueStore\DatabaseStorageExpirable';
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Expirable database storage',
+ 'description' => 'Tests the expirable key-value database storage.',
+ 'group' => 'Key-value store',
+ );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ module_load_install('system');
+ $schema = system_schema();
+ db_create_table('key_value_expire', $schema['key_value_expire']);
+ }
+
+ protected function tearDown() {
+ db_drop_table('key_value_expire');
+ parent::tearDown();
+ }
+
+ /**
+ * Tests CRUD functionality with expiration.
+ */
+ public function testCRUDWithExpiration() {
+ // Verify that an item can be stored with setWithExpire().
+ // Use a random expiration in each test.
+ $this->store1->setWithExpire('foo', $this->objects[0], rand(500, 299792458));
+ $this->assertIdenticalObject($this->objects[0], $this->store1->get('foo'));
+ // Verify that the other collection is not affected.
+ $this->assertFalse($this->store2->get('foo'));
+
+ // Verify that an item can be updated with setWithExpire().
+ $this->store1->setWithExpire('foo', $this->objects[1], rand(500, 299792458));
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get('foo'));
+ // Verify that the other collection is still not affected.
+ $this->assertFalse($this->store2->get('foo'));
+
+ // Verify that the expirable data key is unique.
+ $this->store2->setWithExpire('foo', $this->objects[2], rand(500, 299792458));
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get('foo'));
+ $this->assertIdenticalObject($this->objects[2], $this->store2->get('foo'));
+
+ // Verify that multiple items can be stored with setMultipleWithExpire().
+ $values = array(
+ 'foo' => $this->objects[3],
+ 'bar' => $this->objects[4],
+ );
+ $this->store1->setMultipleWithExpire($values, rand(500, 299792458));
+ $result = $this->store1->getMultiple(array('foo', 'bar'));
+ foreach ($values as $j => $value) {
+ $this->assertIdenticalObject($value, $result[$j]);
+ }
+
+ // Verify that the other collection was not affected.
+ $this->assertIdenticalObject($this->store2->get('foo'), $this->objects[2]);
+ $this->assertFalse($this->store2->get('bar'));
+
+ // Verify that all items in a collection can be retrieved.
+ // Ensure that an item with the same name exists in the other collection.
+ $this->store2->set('foo', $this->objects[5]);
+ $result = $this->store1->getAll();
+ // Not using assertIdentical(), since the order is not defined for getAll().
+ $this->assertEqual(count($result), count($values));
+ foreach ($result as $key => $value) {
+ $this->assertEqual($values[$key], $value);
+ }
+ // Verify that all items in the other collection are different.
+ $result = $this->store2->getAll();
+ $this->assertEqual($result, array('foo' => $this->objects[5]));
+
+ // Verify that multiple items can be deleted.
+ $this->store1->deleteMultiple(array_keys($values));
+ $this->assertFalse($this->store1->get('foo'));
+ $this->assertFalse($this->store1->get('bar'));
+ $this->assertFalse($this->store1->getMultiple(array('foo', 'bar')));
+ // Verify that the item in the other collection still exists.
+ $this->assertIdenticalObject($this->objects[5], $this->store2->get('foo'));
+
+ // Test that setWithExpireIfNotExists() succeeds only the first time.
+ $key = $this->randomName();
+ for ($i = 0; $i <= 1; $i++) {
+ // setWithExpireIfNotExists() should be TRUE the first time (when $i is
+ // 0) and FALSE the second time (when $i is 1).
+ $this->assertEqual(!$i, $this->store1->setWithExpireIfNotExists($key, $this->objects[$i], rand(500, 299792458)));
+ $this->assertIdenticalObject($this->objects[0], $this->store1->get($key));
+ // Verify that the other collection is not affected.
+ $this->assertFalse($this->store2->get($key));
+ }
+
+ // Remove the item and try to set it again.
+ $this->store1->delete($key);
+ $this->store1->setWithExpireIfNotExists($key, $this->objects[1], rand(500, 299792458));
+ // This time it should succeed.
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get($key));
+ // Verify that the other collection is still not affected.
+ $this->assertFalse($this->store2->get($key));
+
+ }
+
+ /**
+ * Tests data expiration and garbage collection.
+ */
+ public function testExpiration() {
+ $day = 604800;
+
+ // Set an item to expire in the past and another without an expiration.
+ $this->store1->setWithExpire('yesterday', 'all my troubles seemed so far away', -1 * $day);
+ $this->store1->set('troubles', 'here to stay');
+
+ // Only the non-expired item should be returned.
+ $this->assertFalse($this->store1->get('yesterday'));
+ $this->assertIdentical($this->store1->get('troubles'), 'here to stay');
+ $this->assertIdentical(count($this->store1->getMultiple(array('yesterday', 'troubles'))), 1);
+
+ // Store items set to expire in the past in various ways.
+ $this->store1->setWithExpire($this->randomName(), $this->objects[0], -7 * $day);
+ $this->store1->setWithExpireIfNotExists($this->randomName(), $this->objects[1], -5 * $day);
+ $this->store1->setMultipleWithExpire(
+ array(
+ $this->randomName() => $this->objects[2],
+ $this->randomName() => $this->objects[3],
+ ),
+ -3 * $day
+ );
+ $this->store1->setWithExpireIfNotExists('yesterday', "you'd forgiven me", -1 * $day);
+ $this->store1->setWithExpire('still', "'til we say we're sorry", 2 * $day);
+
+ // Ensure only non-expired items are retrived.
+ $all = $this->store1->getAll();
+ $this->assertIdentical(count($all), 2);
+ foreach (array('troubles', 'still') as $key) {
+ $this->assertTrue(!empty($all[$key]));
+ }
+
+ // Perform garbage collection and confirm that the expired items are
+ // deleted from the database.
+ $this->store1->garbageCollection();
+ $result = db_query(
+ 'SELECT name, value FROM {key_value_expire} WHERE collection = :collection',
+ array(
+ ':collection' => $this->collection1,
+ ))->fetchAll();
+ $this->assertIdentical(sizeof($result), 2);
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php
index 1eec831..41d216c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php
@@ -21,6 +21,13 @@ abstract class StorageTestBase extends UnitTestBase {
*/
protected $storageClass;
+ /**
+ * An array of random stdClass objects.
+ *
+ * @var array
+ */
+ protected $objects = array();
+
protected function setUp() {
parent::setUp();
@@ -29,6 +36,11 @@ abstract class StorageTestBase extends UnitTestBase {
$this->store1 = new $this->storageClass($this->collection1);
$this->store2 = new $this->storageClass($this->collection2);
+
+ // Create several objects for testing.
+ for ($i = 0; $i <= 5; $i++) {
+ $this->objects[$i] = $this->randomObject();
+ }
}
/**
@@ -40,49 +52,51 @@ abstract class StorageTestBase extends UnitTestBase {
$this->assertIdentical($this->store2->getCollectionName(), $this->collection2);
// Verify that an item can be stored.
- $this->store1->set('foo', 'bar');
- $this->assertIdentical('bar', $this->store1->get('foo'));
+ $this->store1->set('foo', $this->objects[0]);
+ $this->assertIdenticalObject($this->objects[0], $this->store1->get('foo'));
// Verify that the other collection is not affected.
$this->assertFalse($this->store2->get('foo'));
// Verify that an item can be updated.
- $this->store1->set('foo', 'baz');
- $this->assertIdentical('baz', $this->store1->get('foo'));
+ $this->store1->set('foo', $this->objects[1]);
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get('foo'));
// Verify that the other collection is still not affected.
$this->assertFalse($this->store2->get('foo'));
// Verify that a collection/name pair is unique.
- $this->store2->set('foo', 'other');
- $this->assertIdentical('baz', $this->store1->get('foo'));
- $this->assertIdentical('other', $this->store2->get('foo'));
+ $this->store2->set('foo', $this->objects[2]);
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get('foo'));
+ $this->assertIdenticalObject($this->objects[2], $this->store2->get('foo'));
// Verify that an item can be deleted.
$this->store1->delete('foo');
$this->assertFalse($this->store1->get('foo'));
// Verify that the other collection is not affected.
- $this->assertIdentical('other', $this->store2->get('foo'));
+ $this->assertIdenticalObject($this->objects[2], $this->store2->get('foo'));
$this->store2->delete('foo');
$this->assertFalse($this->store2->get('foo'));
// Verify that multiple items can be stored.
$values = array(
- 'foo' => 'bar',
- 'baz' => 'qux',
+ 'foo' => $this->objects[3],
+ 'bar' => $this->objects[4],
);
$this->store1->setMultiple($values);
// Verify that multiple items can be retrieved.
- $result = $this->store1->getMultiple(array('foo', 'baz'));
- $this->assertIdentical($values, $result);
+ $result = $this->store1->getMultiple(array('foo', 'bar'));
+ foreach ($values as $j => $value) {
+ $this->assertIdenticalObject($value, $result[$j]);
+ }
// Verify that the other collection was not affected.
$this->assertFalse($this->store2->get('foo'));
- $this->assertFalse($this->store2->get('baz'));
+ $this->assertFalse($this->store2->get('bar'));
// Verify that all items in a collection can be retrieved.
// Ensure that an item with the same name exists in the other collection.
- $this->store2->set('foo', 'other');
+ $this->store2->set('foo', $this->objects[5]);
$result = $this->store1->getAll();
// Not using assertIdentical(), since the order is not defined for getAll().
$this->assertEqual(count($result), count($values));
@@ -91,15 +105,15 @@ abstract class StorageTestBase extends UnitTestBase {
}
// Verify that all items in the other collection are different.
$result = $this->store2->getAll();
- $this->assertEqual($result, array('foo' => 'other'));
+ $this->assertEqual($result, array('foo' => $this->objects[5]));
// Verify that multiple items can be deleted.
$this->store1->deleteMultiple(array_keys($values));
$this->assertFalse($this->store1->get('foo'));
$this->assertFalse($this->store1->get('bar'));
- $this->assertFalse($this->store1->getMultiple(array('foo', 'baz')));
+ $this->assertFalse($this->store1->getMultiple(array('foo', 'bar')));
// Verify that the item in the other collection still exists.
- $this->assertIdentical('other', $this->store2->get('foo'));
+ $this->assertIdenticalObject($this->objects[5], $this->store2->get('foo'));
}
/**
@@ -123,4 +137,29 @@ abstract class StorageTestBase extends UnitTestBase {
$this->assertFalse(isset($values['foo']), "Key 'foo' not found.");
$this->assertIdentical($values['bar'], 'baz');
}
+
+ /**
+ * Tests the setIfNotExists() method.
+ */
+ public function testSetIfNotExists() {
+ $key = $this->randomName();
+ // Test that setIfNotExists() succeeds only the first time.
+ for ($i = 0; $i <= 1; $i++) {
+ // setIfNotExists() should be TRUE the first time (when $i is 0) and
+ // FALSE the second time (when $i is 1).
+ $this->assertEqual(!$i, $this->store1->setIfNotExists($key, $this->objects[$i]));
+ $this->assertIdenticalObject($this->objects[0], $this->store1->get($key));
+ // Verify that the other collection is not affected.
+ $this->assertFalse($this->store2->get($key));
+ }
+
+ // Remove the item and try to set it again.
+ $this->store1->delete($key);
+ $this->store1->setIfNotExists($key, $this->objects[1]);
+ // This time it should succeed.
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get($key));
+ // Verify that the other collection is still not affected.
+ $this->assertFalse($this->store2->get($key));
+ }
+
}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 8331d55..28d95da 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. Defaults to the maximum possible time.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 2147483647,
+ ),
+ ),
+ '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. Defaults to the maximum possible time.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 2147483647,
+ ),
+ ),
+ '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/system.module b/core/modules/system/system.module
index 4524a49..34e36fe 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1270,6 +1270,7 @@ function system_library_info() {
'dependencies' => array(
array('system', 'jquery'),
array('system', 'drupal'),
+ array('system', 'drupalSettings'),
// collapse.js relies on drupalGetSummary in form.js
array('system', 'drupal.form'),
),
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index 3c322a6..6ea5aa3 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -309,6 +309,19 @@ function form_test_menu() {
return $items;
}
+
+/**
+ * Implements hook_permission().
+ */
+function form_test_permission() {
+ $perms = array(
+ 'access vertical_tab_test tabs' => array(
+ 'title' => t('Access vertical_tab_test tabs'),
+ ),
+ );
+ return $perms;
+}
+
/**
* Form submit handler to return form values as JSON.
*/
@@ -448,23 +461,29 @@ function form_test_validate_form_validate(&$form, &$form_state) {
*/
function form_test_validate_required_form($form, &$form_state) {
$options = drupal_map_assoc(array('foo', 'bar'));
+ $validate = array('form_test_validate_required_form_element_validate');
$form['textfield'] = array(
'#type' => 'textfield',
- '#title' => 'Textfield',
+ '#title' => 'Name',
'#required' => TRUE,
+ '#required_error' => t('Please enter a name.'),
);
$form['checkboxes'] = array(
'#type' => 'checkboxes',
'#title' => 'Checkboxes',
'#options' => $options,
'#required' => TRUE,
+ '#form_test_required_error' => t('Please choose at least one option.'),
+ '#element_validate' => $validate,
);
$form['select'] = array(
'#type' => 'select',
'#title' => 'Select',
'#options' => $options,
'#required' => TRUE,
+ '#form_test_required_error' => t('Please select something.'),
+ '#element_validate' => $validate,
);
$form['radios'] = array(
'#type' => 'radios',
@@ -489,6 +508,16 @@ function form_test_validate_required_form($form, &$form_state) {
}
/**
+ * Form element validation handler for 'Name' field in form_test_validate_required_form().
+ */
+function form_test_validate_required_form_element_validate($element, &$form_state) {
+ // Set a custom validation error on the #required element.
+ if (!empty($element['#required_but_empty']) && isset($element['#form_test_required_error'])) {
+ form_error($element, $element['#form_test_required_error']);
+ }
+}
+
+/**
* Form submission handler for form_test_validate_required_form().
*/
function form_test_validate_required_form_submit($form, &$form_state) {
@@ -798,6 +827,7 @@ function _form_test_vertical_tabs_form($form, &$form_state) {
'#title' => t('Tab 1'),
'#collapsible' => TRUE,
'#group' => 'vertical_tabs',
+ '#access' => user_access('access vertical_tab_test tabs')
);
$form['tab1']['field1'] = array(
'#title' => t('Field 1'),
@@ -808,6 +838,7 @@ function _form_test_vertical_tabs_form($form, &$form_state) {
'#title' => t('Tab 2'),
'#collapsible' => TRUE,
'#group' => 'vertical_tabs',
+ '#access' => user_access('access vertical_tab_test tabs')
);
$form['tab2']['field2'] = array(
'#title' => t('Field 2'),
diff --git a/core/modules/taxonomy/taxonomy.install b/core/modules/taxonomy/taxonomy.install
index 361dc6a..1a0b849 100644
--- a/core/modules/taxonomy/taxonomy.install
+++ b/core/modules/taxonomy/taxonomy.install
@@ -23,6 +23,68 @@ function taxonomy_uninstall() {
* Implements hook_schema().
*/
function taxonomy_schema() {
+ $schema['taxonomy_vocabulary'] = array(
+ 'description' => 'Stores vocabulary information.',
+ 'fields' => array(
+ 'vid' => array(
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique vocabulary ID.',
+ ),
+ 'langcode' => array(
+ 'description' => 'The {language}.langcode of this vocabulary.',
+ 'type' => 'varchar',
+ 'length' => 12,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Name of the vocabulary.',
+ 'translatable' => TRUE,
+ ),
+ 'machine_name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The vocabulary machine name.',
+ ),
+ 'description' => array(
+ 'type' => 'text',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'description' => 'Description of the vocabulary.',
+ 'translatable' => TRUE,
+ ),
+ 'hierarchy' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'tiny',
+ 'description' => 'The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)',
+ ),
+ 'weight' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The weight of this vocabulary in relation to other vocabularies.',
+ ),
+ ),
+ 'primary key' => array('vid'),
+ 'indexes' => array(
+ 'list' => array('weight', 'name'),
+ ),
+ 'unique keys' => array(
+ 'machine_name' => array('machine_name'),
+ ),
+ );
+
$schema['taxonomy_term_data'] = array(
'description' => 'Stores term information.',
'fields' => array(
@@ -127,68 +189,6 @@ function taxonomy_schema() {
'primary key' => array('tid', 'parent'),
);
- $schema['taxonomy_vocabulary'] = array(
- 'description' => 'Stores vocabulary information.',
- 'fields' => array(
- 'vid' => array(
- 'type' => 'serial',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'description' => 'Primary Key: Unique vocabulary ID.',
- ),
- 'langcode' => array(
- 'description' => 'The {language}.langcode of this vocabulary.',
- 'type' => 'varchar',
- 'length' => 12,
- 'not null' => TRUE,
- 'default' => '',
- ),
- 'name' => array(
- 'type' => 'varchar',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'Name of the vocabulary.',
- 'translatable' => TRUE,
- ),
- 'machine_name' => array(
- 'type' => 'varchar',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'The vocabulary machine name.',
- ),
- 'description' => array(
- 'type' => 'text',
- 'not null' => FALSE,
- 'size' => 'big',
- 'description' => 'Description of the vocabulary.',
- 'translatable' => TRUE,
- ),
- 'hierarchy' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'default' => 0,
- 'size' => 'tiny',
- 'description' => 'The type of hierarchy allowed within the vocabulary. (0 = disabled, 1 = single, 2 = multiple)',
- ),
- 'weight' => array(
- 'type' => 'int',
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => 'The weight of this vocabulary in relation to other vocabularies.',
- ),
- ),
- 'primary key' => array('vid'),
- 'indexes' => array(
- 'list' => array('weight', 'name'),
- ),
- 'unique keys' => array(
- 'machine_name' => array('machine_name'),
- ),
- );
-
$schema['taxonomy_index'] = array(
'description' => 'Maintains denormalized information about node/term relationships.',
'fields' => array(
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..c8a6dd1
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/TempStore.php
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\user\TempStore.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
+use Drupal\Core\Lock\LockBackendInterface;
+
+/**
+ * Stores and retrieves temporary data for a given owner.
+ *
+ * A TempStore can be used to make temporary, non-cache data available across
+ * requests. The data for the TempStore is stored in one key/value collection.
+ * TempStore data expires automatically after a given timeframe.
+ *
+ * The TempStore is different from a cache, because the data in it is not yet
+ * saved permanently and so it cannot be rebuilt. Typically, the TempStore
+ * might be used to store work in progress that is later saved permanently
+ * elsewhere, e.g. autosave data, multistep forms, or in-progress changes
+ * to complex configuration that are not ready to be saved.
+ *
+ * Each TempStore belongs to a particular owner (e.g. a user, session, or
+ * process). Multiple owners may use the same key/value collection, and the
+ * owner is stored along with the key/value pair.
+ *
+ * Every key is unique within the collection, so the TempStore can check
+ * whether a particular key is already set by a different owner. This is
+ * useful for informing one owner that the data is already in use by another;
+ * for example, to let one user know that another user is in the process of
+ * editing certain data, or even to restrict other users from editing it at
+ * the same time. It is the responsibility of the implementation to decide
+ * when and whether one owner can use or update another owner's data.
+ *
+ * @todo We could add getIfOwner() or setIfOwner() methods to make this more
+ * explicit.
+ */
+class TempStore {
+
+ /**
+ * The key/value storage object used for this data.
+ *
+ * @var Drupal\Core\KeyValueStore\KeyValueStoreExpireInterface;
+ */
+ protected $storage;
+
+ /**
+ * The lock object used for this data.
+ *
+ * @var Drupal\Core\Lock\LockBackendInterface
+ */
+ protected $lockBackend;
+
+ /**
+ * The owner key to store along with the data (e.g. a user or session ID).
+ *
+ * @var mixed
+ */
+ protected $owner;
+
+ /**
+ * The time to live for items in seconds.
+ *
+ * By default, data is stored for one week (604800 seconds) before expiring.
+ *
+ * @var int
+ *
+ * @todo Currently, this property is not exposed anywhere, and so the only
+ * way to override it is by extending the class.
+ */
+ protected $expire = 604800;
+
+ /**
+ * Constructs a new object for accessing data from a key/value store.
+ *
+ * @param KeyValueStoreExpireInterface $storage
+ * The key/value storage object used for this data. Each storage object
+ * represents a particular collection of data and will contain any number
+ * of key/value pairs.
+ * @param Drupal\Core\Lock\LockBackendInterface $lockBackend
+ * The lock object used for this data.
+ * @param mixed $owner
+ * The owner key to store along with the data (e.g. a user or session ID).
+ */
+ function __construct(KeyValueStoreExpirableInterface $storage, LockBackendInterface $lockBackend, $owner) {
+ $this->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.
+ *
+ * @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)) {
+ $this->lockBackend->wait($key);
+ if (!$this->lockBackend->acquire($key)) {
+ throw new TempStoreException(format_string("Couldn't acquire lock to update item %key in %collection temporary storage.", array(
+ '%key' => $key,
+ '%collection' => $this->storage->collection,
+ )));
+ }
+ }
+
+ $value = (object) array(
+ 'owner' => $this->owner,
+ 'data' => $value,
+ 'updated' => REQUEST_TIME,
+ );
+ $this->storage->setWithExpire($key, $value, $this->expire);
+ $this->lockBackend->release($key);
+ }
+
+ /**
+ * Returns 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) {
+ // Fetch the key/value pair and its metadata.
+ $object = $this->storage->get($key);
+ if ($object) {
+ // Don't keep the data itself in memory.
+ 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.
+ */
+ 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 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\user\TempStoreException.
+ */
+
+namespace Drupal\user;
+
+/**
+ * Defines the exception thrown if the TempStore cannot acquire a lock.
+ */
+class TempStoreException extends \Exception {}
diff --git a/core/modules/user/lib/Drupal/user/TempStoreFactory.php b/core/modules/user/lib/Drupal/user/TempStoreFactory.php
new file mode 100644
index 0000000..473d515
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/TempStoreFactory.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\TempStoreFactory.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\KeyValueStore\DatabaseStorageExpirable;
+use Drupal\Core\Lock\LockBackendInterface;
+
+/**
+ * Creates a key/value storage object for the current user or anonymous session.
+ */
+class TempStoreFactory {
+
+ /**
+ * The connection object used for this data.
+ *
+ * @var Drupal\Core\Database\Connection $connection
+ */
+ protected $connection;
+
+ /**
+ * The lock object used for this data.
+ *
+ * @var Drupal\Core\Lock\LockBackendInterface $lockBackend
+ */
+ protected $lockBackend;
+
+ /**
+ * Constructs a Drupal\user\TempStoreFactory object.
+ *
+ * @param Drupal\Core\Database\Connection $connection
+ * The connection object used for this data.
+ * @param Drupal\Core\Lock\LockBackendInterface $lockBackend
+ * The lock object used for this data.
+ */
+ function __construct(Connection $connection, LockBackendInterface $lockBackend) {
+ $this->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..362e739
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\Tests\TempStoreDatabaseTest.
+ */
+
+namespace Drupal\user\Tests;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\user\TempStoreFactory;
+use Drupal\Core\Lock\DatabaseLockBackend;
+use Drupal\Core\Database\Database;
+
+/**
+ * Tests the TempStore namespace.
+ *
+ * @see Drupal\Core\TempStore\TempStore.
+ */
+class TempStoreDatabaseTest extends UnitTestBase {
+
+ /**
+ * A key/value store factory.
+ *
+ * @var Drupal\user\TempStoreFactory
+ */
+ protected $storeFactory;
+
+ /**
+ * The name of the key/value collection to set and retrieve.
+ *
+ * @var string
+ */
+ protected $collection;
+
+ /**
+ * An array of (fake) user IDs.
+ *
+ * @var array
+ */
+ protected $users = array();
+
+ /**
+ * An array of random stdClass objects.
+ *
+ * @var array
+ */
+ protected $objects = array();
+
+ public static function getInfo() {
+ return array(
+ 'name' => '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 several objects for testing.
+ for ($i = 0; $i <= 3; $i++) {
+ $this->objects[$i] = $this->randomObject();
+ }
+ // Create two mock users for testing.
+ for ($i = 0; $i <= 1; $i++) {
+ $this->users[$i] = mt_rand(500, 5000000);
+ $this->stores[$i] = $this->getStorePerUID($this->users[$i]);
+ }
+
+ }
+
+ protected function tearDown() {
+ db_drop_table('key_value_expire');
+ db_drop_table('semaphore');
+ parent::tearDown();
+ }
+
+ /**
+ * Tests the UserTempStore API.
+ */
+ public function testUserTempStore() {
+ $key = $this->randomName();
+ // Test that setIfNotExists() succeeds only the first time.
+ for ($i = 0; $i <= 1; $i++) {
+ // setIfNotExists() should be TRUE the first time (when $i is 0) and
+ // FALSE the second time (when $i is 1).
+ $this->assertEqual(!$i, $this->stores[0]->setIfNotExists($key, $this->objects[$i]));
+ $metadata = $this->stores[0]->getMetadata($key);
+ $this->assertEqual($this->users[0], $metadata->owner);
+ $this->assertIdenticalObject($this->objects[0], $this->stores[0]->get($key));
+ // Another user should get the same result.
+ $metadata = $this->stores[1]->getMetadata($key);
+ $this->assertEqual($this->users[0], $metadata->owner);
+ $this->assertIdenticalObject($this->objects[0], $this->stores[1]->get($key));
+ }
+
+ // Remove the item and try to set it again.
+ $this->stores[0]->delete($key);
+ $this->stores[0]->setIfNotExists($key, $this->objects[1]);
+ // This time it should succeed.
+ $this->assertIdenticalObject($this->objects[1], $this->stores[0]->get($key));
+
+ // This user can update the object.
+ $this->stores[0]->set($key, $this->objects[2]);
+ $this->assertIdenticalObject($this->objects[2], $this->stores[0]->get($key));
+ // The object is the same when another user loads it.
+ $this->assertIdenticalObject($this->objects[2], $this->stores[1]->get($key));
+ // Another user can update the object and become the owner.
+ $this->stores[1]->set($key, $this->objects[3]);
+ $this->assertIdenticalObject($this->objects[3], $this->stores[0]->get($key));
+ $this->assertIdenticalObject($this->objects[3], $this->stores[1]->get($key));
+ $metadata = $this->stores[1]->getMetadata($key);
+ $this->assertEqual($this->users[1], $metadata->owner);
+
+ // The first user should be informed that the second now owns the data.
+ $metadata = $this->stores[0]->getMetadata($key);
+ $this->assertEqual($this->users[1], $metadata->owner);
+
+ // 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($this->stores[0]->get($key));
+ $this->assertFalse($this->stores[1]->get($key));
+ }
+
+ /**
+ * Returns a TempStore for this collection belonging to the given 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);
+ }
+
+}
diff --git a/core/modules/user/user.install b/core/modules/user/user.install
index 422dcfb..45d35b6 100644
--- a/core/modules/user/user.install
+++ b/core/modules/user/user.install
@@ -9,118 +9,6 @@
* Implements hook_schema().
*/
function user_schema() {
- $schema['authmap'] = array(
- 'description' => 'Stores distributed authentication mapping.',
- 'fields' => array(
- 'aid' => array(
- 'description' => 'Primary Key: Unique authmap ID.',
- 'type' => 'serial',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- ),
- 'uid' => array(
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => "User's {users}.uid.",
- ),
- 'authname' => array(
- 'type' => 'varchar',
- 'length' => 128,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'Unique authentication name.',
- ),
- 'module' => array(
- 'type' => 'varchar',
- 'length' => 128,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'Module which is controlling the authentication.',
- ),
- ),
- 'unique keys' => array(
- 'authname' => array('authname'),
- ),
- 'primary key' => array('aid'),
- 'foreign keys' => array(
- 'user' => array(
- 'table' => 'users',
- 'columns' => array('uid' => 'uid'),
- ),
- ),
- );
-
- $schema['role_permission'] = array(
- 'description' => 'Stores the permissions assigned to user roles.',
- 'fields' => array(
- 'rid' => array(
- 'type' => 'varchar',
- 'length' => 64,
- 'not null' => TRUE,
- 'description' => 'Foreign Key: {role}.rid.',
- ),
- 'permission' => array(
- 'type' => 'varchar',
- 'length' => 128,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'A single permission granted to the role identified by rid.',
- ),
- 'module' => array(
- 'type' => 'varchar',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => "The module declaring the permission.",
- ),
- ),
- 'primary key' => array('rid', 'permission'),
- 'indexes' => array(
- 'permission' => array('permission'),
- ),
- 'foreign keys' => array(
- 'role' => array(
- 'table' => 'role',
- 'columns' => array('rid' => 'rid'),
- ),
- ),
- );
-
- $schema['role'] = array(
- 'description' => 'Stores user roles.',
- 'fields' => array(
- 'rid' => array(
- 'type' => 'varchar',
- // The role ID is often used as part of a compound index; at least MySQL
- // has a maximum index length of 1000 characters (333 on utf8), so we
- // limit the maximum length.
- 'length' => 64,
- 'not null' => TRUE,
- 'description' => 'Primary Key: Unique role ID.',
- ),
- 'name' => array(
- 'type' => 'varchar',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- 'description' => 'Role label.',
- 'translatable' => TRUE,
- ),
- 'weight' => array(
- 'type' => 'int',
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => 'The weight of this role in listings and the user interface.',
- ),
- ),
- 'primary key' => array('rid'),
- 'indexes' => array(
- 'name_weight' => array('name', 'weight'),
- ),
- );
-
// The table name here is plural, despite Drupal table naming standards,
// because "user" is a reserved word in many databases.
$schema['users'] = array(
@@ -272,6 +160,118 @@ function user_schema() {
),
);
+ $schema['authmap'] = array(
+ 'description' => 'Stores distributed authentication mapping.',
+ 'fields' => array(
+ 'aid' => array(
+ 'description' => 'Primary Key: Unique authmap ID.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'uid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => "User's {users}.uid.",
+ ),
+ 'authname' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Unique authentication name.',
+ ),
+ 'module' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Module which is controlling the authentication.',
+ ),
+ ),
+ 'unique keys' => array(
+ 'authname' => array('authname'),
+ ),
+ 'primary key' => array('aid'),
+ 'foreign keys' => array(
+ 'user' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ ),
+ );
+
+ $schema['role'] = array(
+ 'description' => 'Stores user roles.',
+ 'fields' => array(
+ 'rid' => array(
+ 'type' => 'varchar',
+ // The role ID is often used as part of a compound index; at least MySQL
+ // has a maximum index length of 1000 characters (333 on utf8), so we
+ // limit the maximum length.
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'description' => 'Primary Key: Unique role ID.',
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'Role label.',
+ 'translatable' => TRUE,
+ ),
+ 'weight' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The weight of this role in listings and the user interface.',
+ ),
+ ),
+ 'primary key' => array('rid'),
+ 'indexes' => array(
+ 'name_weight' => array('name', 'weight'),
+ ),
+ );
+
+ $schema['role_permission'] = array(
+ 'description' => 'Stores the permissions assigned to user roles.',
+ 'fields' => array(
+ 'rid' => array(
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'description' => 'Foreign Key: {role}.rid.',
+ ),
+ 'permission' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'A single permission granted to the role identified by rid.',
+ ),
+ 'module' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => "The module declaring the permission.",
+ ),
+ ),
+ 'primary key' => array('rid', 'permission'),
+ 'indexes' => array(
+ 'permission' => array('permission'),
+ ),
+ 'foreign keys' => array(
+ 'role' => array(
+ 'table' => 'role',
+ 'columns' => array('rid' => 'rid'),
+ ),
+ ),
+ );
+
$schema['users_roles'] = array(
'description' => 'Maps users to roles.',
'fields' => array(