diff --git a/README.txt b/README.txt index 5e49ac4..77a1d65 100644 --- a/README.txt +++ b/README.txt @@ -4,6 +4,7 @@ CONTENTS OF THIS FILE * About Drupal * Configuration and features + * Installation profiles * Appearance * Developing for Drupal @@ -42,6 +43,35 @@ More about configuration: http://drupal.org/project/modules * See also: "Developing for Drupal" for writing your own modules, below. + +INSTALLATION PROFILES +--------------------- + +Installation profiles define additional steps (such as enabling modules, +defining content types, etc.) that run after the base installation provided +by core when Drupal is first installed. There are two basic installation +profiles provided with Drupal core. + +Installation profiles from the Drupal community modify the installation process +to provide a website for a specific use case, such as a CMS for media +publishers, a web-based project tracking tool, or a full-fledged CRM for +non-profit organizations raising money and accepting donations. They can be +distributed as bare installation profiles or as "distributions". Distributions +include Drupal core, the installation profile, and all other required +extensions, such as contributed and custom modules, themes, and third-party +libraries. Bare installation profiles require you to download Drupal Core and +the required extensions separately; place the downloaded profile in the +/profiles directory before you start the installation process. + +More about installation profiles and distributions: +* Read about the difference between installation profiles and distributions: + http://drupal.org/node/1089736 +* Download contributed installation profiles and distributions: + http://drupal.org/project/distributions +* Develop your own installation profile or distribution: + http://drupal.org/developing/distributions + + APPEARANCE ---------- diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index c1abfe5..800d0ab 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -55,6 +55,10 @@ class CoreBundle extends Bundle ->setFactoryMethod('getConnection') ->addArgument('slave'); $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager'); + $container->register('lock', 'Drupal\Core\Lock\DatabaseLockBackend'); + $container->register('user.tempstore', 'Drupal\user\KeyValueStoreWithOwnerFactory') + ->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/FileTransfer/ChmodInterface.php b/core/lib/Drupal/Core/FileTransfer/ChmodInterface.php index d9035f7..c1c68cb 100644 --- a/core/lib/Drupal/Core/FileTransfer/ChmodInterface.php +++ b/core/lib/Drupal/Core/FileTransfer/ChmodInterface.php @@ -18,7 +18,7 @@ interface ChmodInterface { * @param string $path * Path to change permissions of. * @param int $mode - * See the $mode argument from http://php.net/chmod. + * The new file permission mode to be passed to chmod(). * @param bool $recursive * Pass TRUE to recursively chmod the entire directory specified in $path. * diff --git a/core/lib/Drupal/Core/FileTransfer/FileTransfer.php b/core/lib/Drupal/Core/FileTransfer/FileTransfer.php index 78d69d0..337b0b3 100644 --- a/core/lib/Drupal/Core/FileTransfer/FileTransfer.php +++ b/core/lib/Drupal/Core/FileTransfer/FileTransfer.php @@ -136,7 +136,7 @@ abstract class FileTransfer { * @param string $path * The file / directory to change the permissions of. * @param int $mode - * See the $mode argument from http://php.net/chmod. + * The new file permission mode to be passed to chmod(). * @param bool $recursive * Pass TRUE to recursively chmod the entire directory specified in $path. * diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php index 4484e9f..887bdfa 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, switch this to use an injected database connection. */ 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 @@ -44,9 +57,9 @@ class DatabaseStorage extends StorageBase { } } catch (\Exception $e) { - // @todo: Perhaps if the database is never going to be available, - // key/value requests should return FALSE in order to allow exception - // handling to occur but for now, keep it an array, always. + // @todo Perhaps if the database is never going to be available, + // key/value requests should return FALSE in order to allow exception + // handling to occur but for now, keep it an array, always. } return $values; } @@ -80,6 +93,23 @@ 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' => $value, + )) + ->condition('collection', $this->collection) + ->condition('name', $key) + ->condition('value', $value) + ->execute(); + return $result == Merge::STATUS_INSERT; + } + + /** * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple(). */ public function deleteMultiple(array $keys) { @@ -93,4 +123,12 @@ class DatabaseStorage extends StorageBase { while (count($keys)); } + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteAll(). + */ + public function deleteAll() { + db_delete($this->table) + ->condition('collection', $this->collection) + ->execute(); + } } diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php new file mode 100644 index 0000000..0082f75 --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php @@ -0,0 +1,145 @@ +connection = $options['connection']; + $this->table = isset($options['table']) ? $options['table'] : 'key_value_expire'; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getMultiple(). + */ + public function getMultiple(array $keys) { + $values = array(); + try { + $result = $this->connection->query('SELECT name, value, expire FROM {' . $this->connection->escapeTable($this->table) . '} WHERE expire > :now AND name IN (:keys) AND collection = :collection', + array( + ':now' => REQUEST_TIME, + ':keys' => $keys, + ':collection' => $this->collection, + ))->fetchAllAssoc('name'); + foreach ($keys as $key) { + if (isset($result[$key])) { + $values[$key] = unserialize($result[$key]->value); + } + } + } + catch (\Exception $e) { + // @todo Perhaps if the database is never going to be available, + // key/value requests should return FALSE in order to allow exception + // handling to occur but for now, keep it an array, always. + } + return $values; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getAll(). + */ + public function getAll() { + $result = $this->connection->query('SELECT name, value FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND expire > :now', array(':collection' => $this->collection, ':now' => REQUEST_TIME)); + $values = array(); + + foreach ($result as $item) { + if ($item) { + $values[$item->name] = unserialize($item->value); + } + } + return $values; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpireInterface::setWithExpire(). + */ + function setWithExpire($key, $value, $expire) { + $this->garbageCollection(); + $this->connection->merge($this->table) + ->key(array( + 'name' => $key, + 'collection' => $this->collection, + )) + ->fields(array( + 'value' => serialize($value), + 'expire' => REQUEST_TIME + $expire, + )) + ->execute(); + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setWithExpireIfNotExists(). + */ + function setWithExpireIfNotExists($key, $value, $expire) { + $this->garbageCollection(); + $result = $this->connection->merge($this->table) + ->insertFields(array( + 'collection' => $this->collection, + 'name' => $key, + 'value' => serialize($value), + 'expire' => REQUEST_TIME + $expire, + )) + ->condition('collection', $this->collection) + ->condition('name', $key) + ->execute(); + return $result == MERGE::STATUS_INSERT; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setMultipleWithExpire(). + */ + function setMultipleWithExpire(array $data, $expire) { + foreach ($data as $key => $value) { + $this->set($key, $value, $expire); + } + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple(). + */ + public function deleteMultiple(array $keys) { + $this->garbageCollection(); + parent::deleteMultiple($keys); + } + + /** + * Deletes expired items. + */ + protected function garbageCollection() { + $this->connection->delete($this->table) + ->condition('expire', REQUEST_TIME, '<') + ->execute(); + } + +} diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php new file mode 100644 index 0000000..113c6c6 --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php @@ -0,0 +1,52 @@ +data[$key])) { + $this->data[$key] = $value; + return TRUE; + } + return FALSE; + } + + /** * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setMultiple(). */ public function setMultiple(array $data) { @@ -87,4 +98,10 @@ class MemoryStorage implements KeyValueStoreInterface { } } + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteAll(). + */ + public function deleteAll() { + $this->data = array(); + } } diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc index 4a3220f..ef8a90b 100644 --- a/core/modules/comment/comment.admin.inc +++ b/core/modules/comment/comment.admin.inc @@ -110,13 +110,14 @@ function comment_admin_overview($form, &$form_state, $arg) { // Remove the first node title from the node_titles array and attach to // the comment. $comment->node_title = array_shift($node_titles); + $comment_body = field_get_items('comment', $comment, 'comment_body'); $options[$comment->cid] = array( 'subject' => array( 'data' => array( '#type' => 'link', '#title' => $comment->subject, '#href' => 'comment/' . $comment->cid, - '#options' => array('attributes' => array('title' => truncate_utf8($comment->comment_body[LANGUAGE_NOT_SPECIFIED][0]['value'], 128)), 'fragment' => 'comment-' . $comment->cid), + '#options' => array('attributes' => array('title' => truncate_utf8($comment_body[0]['value'], 128)), 'fragment' => 'comment-' . $comment->cid), ), ), 'author' => theme('username', array('account' => $comment)), diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 44b4c04..27ff7c3 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1690,7 +1690,8 @@ function comment_preview(Comment $comment) { $node = node_load($comment->nid); if (!form_get_errors()) { - $comment->format = $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0]['format']; + $comment_body = field_get_items('comment', $comment, 'comment_body'); + $comment->format = $comment_body[0]['format']; // Attach the user and time information. if (!empty($comment->name)) { $account = user_load_by_name($comment->name); diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php index 7dae92d..cead0cd 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php @@ -20,7 +20,6 @@ class CommentFormController extends EntityFormController { */ public function form(array $form, array &$form_state, EntityInterface $comment) { global $user; - $language_content = language(LANGUAGE_TYPE_CONTENT); $node = node_load($comment->nid); $form_state['comment']['node'] = $node; @@ -174,16 +173,16 @@ class CommentFormController extends EntityFormController { } $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type); - // Make the comment inherit the node language unless specifically set. - $comment_langcode = $comment->langcode; - if ($comment_langcode == LANGUAGE_NOT_SPECIFIED) { - $comment_langcode = $language_content->langcode; + // Make the comment inherit the current content language unless specifically + // set. + if ($comment->isNew()) { + $language_content = language(LANGUAGE_TYPE_CONTENT); + $comment->langcode = $language_content->langcode; } - // Uses the language of the content as comment language. $form['langcode'] = array( '#type' => 'value', - '#value' => $comment_langcode, + '#value' => $comment->langcode, ); // Attach fields. @@ -294,7 +293,9 @@ class CommentFormController extends EntityFormController { // 1) Filter it into HTML // 2) Strip out all HTML tags // 3) Convert entities back to plain-text. - $comment_body = $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0]; + $field = field_info_field('comment_body'); + $langcode = field_is_translatable('comment', $field) ? $this->getFormLangcode($form_state) : LANGUAGE_NOT_SPECIFIED; + $comment_body = $comment->comment_body[$langcode][0]; if (isset($comment_body['format'])) { $comment_text = check_markup($comment_body['value'], $comment_body['format']); } diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCommentLanguageTest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentLanguageTest.php similarity index 69% rename from core/modules/locale/lib/Drupal/locale/Tests/LocaleCommentLanguageTest.php rename to core/modules/comment/lib/Drupal/comment/Tests/CommentLanguageTest.php index 6776e31..9c3af8e 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCommentLanguageTest.php +++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentLanguageTest.php @@ -2,17 +2,17 @@ /** * @file - * Definition of Drupal\locale\Tests\LocaleCommentLanguageTest. + * Definition of Drupal\comment\Tests\CommentLanguageTest. */ -namespace Drupal\locale\Tests; +namespace Drupal\comment\Tests; use Drupal\simpletest\WebTestBase; /** * Functional tests for comment language. */ -class LocaleCommentLanguageTest extends WebTestBase { +class CommentLanguageTest extends WebTestBase { /** * Modules to enable. @@ -23,7 +23,7 @@ class LocaleCommentLanguageTest extends WebTestBase { * * @var array */ - public static $modules = array('locale', 'language_test'); + public static $modules = array('language', 'language_test', 'comment_test'); protected $profile = 'standard'; @@ -31,7 +31,7 @@ class LocaleCommentLanguageTest extends WebTestBase { return array( 'name' => 'Comment language', 'description' => 'Tests for comment language.', - 'group' => 'Locale', + 'group' => 'Comment', ); } @@ -39,7 +39,7 @@ class LocaleCommentLanguageTest extends WebTestBase { parent::setUp(); // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'create article content')); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'administer comments', 'create article content')); $this->drupalLogin($admin_user); // Add language. @@ -68,6 +68,12 @@ class LocaleCommentLanguageTest extends WebTestBase { // French no matter what path prefix the URLs have. $edit = array('preferred_langcode' => 'fr'); $this->drupalPost("user/{$admin_user->uid}/edit", $edit, t('Save')); + + // Make comment body translatable. + $field = field_info_field('comment_body'); + $field['translatable'] = TRUE; + field_update_field($field); + $this->assertTrue(field_is_translatable('comment', $field), 'Comment body is translatable.'); } /** @@ -99,19 +105,36 @@ class LocaleCommentLanguageTest extends WebTestBase { foreach (language_list() as $langcode => $language) { // Post a comment with content language $langcode. $prefix = empty($prefixes[$langcode]) ? '' : $prefixes[$langcode] . '/'; - $edit = array("comment_body[$langcode_not_specified][0][value]" => $this->randomName()); - $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Save')); + $comment_values[$node_langcode][$langcode] = $this->randomName(); + $edit = array( + 'subject' => $this->randomName(), + "comment_body[$langcode][0][value]" => $comment_values[$node_langcode][$langcode], + ); + $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Preview')); + $this->drupalPost(NULL, $edit, t('Save')); // Check that comment language matches the current content language. - $comment = db_select('comment', 'c') - ->fields('c') + $cid = db_select('comment', 'c') + ->fields('c', array('cid')) ->condition('nid', $node->nid) ->orderBy('cid', 'DESC') + ->range(0, 1) ->execute() - ->fetchObject(); + ->fetchField(); + $comment = comment_load($cid); $args = array('%node_language' => $node_langcode, '%comment_language' => $comment->langcode, '%langcode' => $langcode); - $this->assertEqual($comment->langcode, $langcode, t('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args)); + $this->assertEqual($comment->langcode, $langcode, format_string('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args)); + $this->assertEqual($comment->comment_body[$langcode][0]['value'], $comment_values[$node_langcode][$langcode], 'Comment body correctly stored.'); + } + } + + // Check that comment bodies appear in the administration UI. + $this->drupalGet('admin/content/comment'); + foreach ($comment_values as $node_values) { + foreach ($node_values as $value) { + $this->assertRaw($value); } } } + } diff --git a/core/modules/comment/tests/modules/comment_test/comment_test.info b/core/modules/comment/tests/modules/comment_test/comment_test.info new file mode 100644 index 0000000..a7ad565 --- /dev/null +++ b/core/modules/comment/tests/modules/comment_test/comment_test.info @@ -0,0 +1,8 @@ +name = Comment test +description = Support module for Comment module testing. +package = Testing +version = VERSION +core = 8.x +hidden = TRUE + +dependencies[] = comment diff --git a/core/modules/comment/tests/modules/comment_test/comment_test.module b/core/modules/comment/tests/modules/comment_test/comment_test.module new file mode 100644 index 0000000..8e903db --- /dev/null +++ b/core/modules/comment/tests/modules/comment_test/comment_test.module @@ -0,0 +1,17 @@ +bundle(); + $id = $entity->id(); $bundle = $entity->bundle(); $langcode = field_valid_language($langcode, FALSE); diff --git a/core/modules/overlay/overlay-child-rtl.css b/core/modules/overlay/overlay-child-rtl.css index febaab3..0c5edcb 100644 --- a/core/modules/overlay/overlay-child-rtl.css +++ b/core/modules/overlay/overlay-child-rtl.css @@ -1,4 +1,9 @@ +/** + * @file + * RTL styling for Overlay child pages. + */ + html { direction: rtl; } diff --git a/core/modules/overlay/overlay-child.css b/core/modules/overlay/overlay-child.css index 5ca0b4f..7afbe25 100644 --- a/core/modules/overlay/overlay-child.css +++ b/core/modules/overlay/overlay-child.css @@ -1,4 +1,9 @@ +/** + * @file + * Basic styling for the Overlay child pages. + */ + .js { background: transparent !important; overflow-y: scroll; diff --git a/core/modules/overlay/overlay-child.js b/core/modules/overlay/overlay-child.js index 39b6efb..80a8a2f 100644 --- a/core/modules/overlay/overlay-child.js +++ b/core/modules/overlay/overlay-child.js @@ -1,3 +1,8 @@ +/** + * @file + * Attaches the behaviors for the Overlay child pages. + */ + (function ($) { "use strict"; diff --git a/core/modules/overlay/overlay-parent.css b/core/modules/overlay/overlay-parent.css index dad6d55..9459a7a 100644 --- a/core/modules/overlay/overlay-parent.css +++ b/core/modules/overlay/overlay-parent.css @@ -1,4 +1,9 @@ +/** + * @file + * Basic styling for the Overlay module. + */ + html.overlay-open, html.overlay-open body { height: 100%; diff --git a/core/modules/overlay/overlay-parent.js b/core/modules/overlay/overlay-parent.js index 678d68a..1f38f2f 100644 --- a/core/modules/overlay/overlay-parent.js +++ b/core/modules/overlay/overlay-parent.js @@ -1,3 +1,8 @@ +/** + * @file + * Attaches the behaviors for the Overlay parent pages. + */ + (function ($) { "use strict"; diff --git a/core/modules/overlay/overlay.install b/core/modules/overlay/overlay.install index 2fa7c84..5fc2936 100644 --- a/core/modules/overlay/overlay.install +++ b/core/modules/overlay/overlay.install @@ -2,7 +2,7 @@ /** * @file - * Install, update and uninstall functions for the overlay module. + * Install, update, and uninstall functions for the Overlay module. */ /** diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module index 3ae575c..e2cdc4c 100644 --- a/core/modules/overlay/overlay.module +++ b/core/modules/overlay/overlay.module @@ -331,7 +331,7 @@ function overlay_user_dismiss_message_access() { } /** - * Menu callback; dismisses the overlay accessibility message for this user. + * Menu callback: Dismisses the overlay accessibility message for this user. * * @see overlay_user_dismiss_message_access() * @see overlay_menu() @@ -361,10 +361,12 @@ function overlay_user_dismiss_message() { * If the current user can access the overlay and has not previously indicated * that this message should be dismissed, this function returns a message * containing a link to disable the overlay. Nothing is returned for anonymous - * users, because the links control per-user settings. Therefore, because some - * screen readers are unable to properly read overlay contents, site builders - * are discouraged from granting the "access overlay" permission to the - * anonymous role. See http://drupal.org/node/890284. + * users, because the links control per-user settings. Because some screen + * readers are unable to properly read overlay contents, site builders are + * discouraged from granting the "access overlay" permission to the anonymous + * role. + * + * @see http://drupal.org/node/890284 */ function overlay_disable_message() { global $user; @@ -419,7 +421,13 @@ function overlay_disable_message() { /** * Returns the HTML for the message about how to disable the overlay. * - * @see overlay_disable_message() + * @param $variables + * An associative array with an 'element' element, which itself is an + * associative array containing: + * - profile_link: The link to this user's account. + * - dismiss_message_link: The link to dismiss the overlay. + * + * @ingroup themeable */ function theme_overlay_disable_message($variables) { $element = $variables['element']; @@ -505,8 +513,12 @@ function overlay_preprocess_maintenance_page(&$variables) { } /** - * Preprocesses template variables for overlay.tpl.php + * Implements template_preprocess_HOOK() for overlay.tpl.php + * + * If the current page request is inside the overlay, add appropriate classes + * to the element, and simplify the page title. * + * @see template_process_overlay() * @see overlay.tpl.php */ function template_preprocess_overlay(&$variables) { @@ -517,20 +529,21 @@ function template_preprocess_overlay(&$variables) { } /** - * Processes variables for overlay.tpl.php + * Implements template_process_HOOK() for overlay.tpl.php + * + * Places the rendered HTML for the page body into a top level variable. * * @see template_preprocess_overlay() * @see overlay.tpl.php */ function template_process_overlay(&$variables) { - // Place the rendered HTML for the page body into a top level variable. $variables['page'] = $variables['page']['#children']; } /** * Implements hook_preprocess_HOOK() for page.tpl.php. * - * Hide tabs inside the overlay. + * If the current page request is inside the overlay, hide the tabs. * * @see overlay_get_mode() */ @@ -582,7 +595,7 @@ function overlay_page_delivery_callback_alter(&$callback) { } /** - * Delivery callback to display an empty page. + * Prints an empty page. * * This function is used to print out a bare minimum empty page which still has * the scripts and styles necessary in order to trigger the overlay to close. @@ -594,7 +607,7 @@ function overlay_deliver_empty_page() { } /** - * Get the current overlay mode. + * Gets the current overlay mode. * * @see overlay_set_mode() */ @@ -787,7 +800,7 @@ function overlay_supplemental_regions() { } /** - * Helper function for returning a list of page regions related to the overlay. + * Returns a list of page regions related to the overlay. * * @param $type * The type of regions to return. This can either be 'overlay_regions' or diff --git a/core/modules/overlay/overlay.tpl.php b/core/modules/overlay/overlay.tpl.php index 622247b..38e6664 100644 --- a/core/modules/overlay/overlay.tpl.php +++ b/core/modules/overlay/overlay.tpl.php @@ -9,6 +9,9 @@ * - $page: The rendered page content. * - $tabs (array): Tabs linking to any sub-pages beneath the current page * (e.g., the view and edit tabs when displaying a node). + * Helper variables: + * - $classes_array: Array of HMTL class attribute values. It is flattened into + * a string within the variable $classes. * * @see template_preprocess() * @see template_preprocess_overlay() diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 05459d0..9ae0bba 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -832,6 +832,43 @@ function system_schema() { 'primary key' => array('collection', 'name'), ); + $schema['key_value_expire'] = array( + 'description' => 'Generic key-value storage table with an expire.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + 'description' => 'The key of the key-value pair. As KEY is a SQL reserved keyword, name was chosen instead.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + 'expire' => array( + 'description' => 'The time since Unix epoch in seconds when this item expires.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('collection', 'name'), + 'indexes' => array( + 'all' => array('name', 'collection', 'expire'), + ), + ); + $schema['menu_router'] = array( 'description' => 'Maps paths to various callbacks (access, page and title)', 'fields' => array( @@ -2102,6 +2139,50 @@ function system_update_8022() { } /** + * Create the 'key_value_expire' table. + */ +function system_update_8023() { + $table = array( + 'description' => 'Generic key-value storage table with an expire.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + 'description' => 'The key of the key-value pair. As KEY is a SQL reserved keyword, name was chosen instead.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + 'expire' => array( + 'description' => 'The time since Unix epoch in seconds when this item expires.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('collection', 'name'), + 'indexes' => array( + 'all' => array('name', 'collection', 'expire'), + ), + ); + + db_create_table('key_value_expire', $table); +} + +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index 0001cba..a697eef 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -234,5 +234,5 @@ function hook_themes_enabled($theme_list) { */ function hook_themes_disabled($theme_list) { // Clear all update module caches. - _update_cache_clear(); + update_storage_clear(); } diff --git a/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php b/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php index 5dc09fd..dd9626b 100644 --- a/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php +++ b/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php @@ -210,8 +210,8 @@ class UpdateCoreTest extends UpdateTestBase { update_create_fetch_task($projecta); $this->assertEqual($queue->numberOfItems(), 2, 'Queue still contains two items'); - // Clear cache and try again. - _update_cache_clear(); + // Clear storage and try again. + update_storage_clear(); drupal_static_reset('_update_create_fetch_task'); update_create_fetch_task($projecta); $this->assertEqual($queue->numberOfItems(), 2, 'Queue contains two items'); diff --git a/core/modules/update/update.authorize.inc b/core/modules/update/update.authorize.inc index d22f235..396fa75 100644 --- a/core/modules/update/update.authorize.inc +++ b/core/modules/update/update.authorize.inc @@ -177,7 +177,7 @@ function update_authorize_batch_copy_project($project, $updater_name, $local_url * * This processes the results and stashes them into SESSION such that * authorize.php will render a report. Also responsible for putting the site - * back online and clearing the update status cache after a successful update. + * back online and clearing the update status storage after a successful update. * * @param $success * TRUE if the batch operation was successful; FALSE if there were errors. @@ -192,8 +192,8 @@ function update_authorize_update_batch_finished($success, $results) { } $offline = config('system.maintenance')->get('enabled'); if ($success) { - // Now that the update completed, we need to clear the cache of available - // update data and recompute our status, so prevent show bogus results. + // Now that the update completed, we need to clear the available update data + // and recompute our status, so prevent show bogus results. _update_authorize_clear_update_status(); // Take the site out of maintenance mode if it was previously that way. @@ -314,25 +314,19 @@ function _update_batch_create_message(&$project_results, $message, $success = TR } /** - * Clears cached available update status data. + * Clears available update status data. * * Since this function is run at such a low bootstrap level, the Update Manager - * module is not loaded. So, we can't just call _update_cache_clear(). However, - * the database is bootstrapped, so we can do a query ourselves to clear out - * what we want to clear. + * module is not loaded. So, we can't just call update_storage_clear(). However, + * the key-value backend is available, so we just call that. * - * Note that we do not want to just truncate the table, since that would remove - * items related to currently pending fetch attempts. + * Note that we do not want to delete items related to currently pending fetch + * attempts. * * @see update_authorize_update_batch_finished() - * @see _update_cache_clear() + * @see update_storage_clear() */ function _update_authorize_clear_update_status() { - $query = db_delete('cache_update'); - $query->condition( - db_or() - ->condition('cid', 'update_project_%', 'LIKE') - ->condition('cid', 'available_releases::%', 'LIKE') - ); - $query->execute(); + DatabaseStorageExpirable('update', array('connection' => Database::getConnection()))->deleteAll(); + DatabaseStorageExpirable('update_available_releases', array('connection' => Database::getConnection()))->deleteAll(); } diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc index 2262404..9d0aa4b 100644 --- a/core/modules/update/update.compare.inc +++ b/core/modules/update/update.compare.inc @@ -16,13 +16,12 @@ * fetching the available release data. * * This array is fairly expensive to construct, since it involves a lot of disk - * I/O, so we cache the results into the {cache_update} table using the - * 'update_project_projects' cache ID. However, since this is not the data about + * I/O, so we store the results. However, since this is not the data about * available updates fetched from the network, it is acceptable to invalidate it * somewhat quickly. If we keep this data for very long, site administrators are * more likely to see incorrect results if they upgrade to a newer version of a * module or theme but do not visit certain pages that automatically clear this - * cache. + * data. * * @return * An associative array of currently enabled projects keyed by the @@ -51,15 +50,15 @@ * * @see update_process_project_info() * @see update_calculate_project_data() - * @see update_project_cache() + * @see update_project_storage() */ function update_get_projects() { $projects = &drupal_static(__FUNCTION__, array()); if (empty($projects)) { - // Retrieve the projects from cache, if present. - $projects = update_project_cache('update_project_projects'); + // Retrieve the projects from storage, if present. + $projects = update_project_storage('update_project_projects'); if (empty($projects)) { - // Still empty, so we have to rebuild the cache. + // Still empty, so we have to rebuild. $module_data = system_rebuild_module_data(); $theme_data = system_rebuild_theme_data(); update_process_info_list($projects, $module_data, 'module', TRUE); @@ -70,8 +69,8 @@ function update_get_projects() { } // Allow other modules to alter projects before fetching and comparing. drupal_alter('update_projects', $projects); - // Cache the site's project data for at most 1 hour. - _update_cache_set('update_project_projects', $projects, REQUEST_TIME + 3600); + // Store the site's project data for at most 1 hour. + update_storage()->setWithExpire('update_project_projects', $projects, 3600); } } return $projects; @@ -89,7 +88,7 @@ function update_get_projects() { * enabled code. Modules and themes set as hidden are always ignored. This * function also records the latest change time on the .info files for each * module or theme, which is important data which is used when deciding if the - * cached available update data should be invalidated. + * available update data should be invalidated. * * @param $projects * Reference to the array of project data of what's installed on this site. @@ -316,13 +315,12 @@ function update_process_project_info(&$projects) { * * The results of this function are expensive to compute, especially on sites * with lots of modules or themes, since it involves a lot of comparisons and - * other operations. Therefore, we cache the results into the {cache_update} - * table using the 'update_project_data' cache ID. However, since this is not + * other operations. Therefore, we store the results. However, since this is not * the data about available updates fetched from the network, it is ok to * invalidate it somewhat quickly. If we keep this data for very long, site * administrators are more likely to see incorrect results if they upgrade to a * newer version of a module or theme but do not visit certain pages that - * automatically clear this cache. + * automatically clear this. * * @param array $available * Data about available project releases. @@ -333,13 +331,13 @@ function update_process_project_info(&$projects) { * @see update_get_available() * @see update_get_projects() * @see update_process_project_info() - * @see update_project_cache() + * @see update_project_storage() */ function update_calculate_project_data($available) { - // Retrieve the projects from cache, if present. - $projects = update_project_cache('update_project_data'); - // If $projects is empty, then the cache must be rebuilt. - // Otherwise, return the cached data and skip the rest of the function. + // Retrieve the projects from storage, if present. + $projects = update_project_storage('update_project_data'); + // If $projects is empty, then the data must be rebuilt. + // Otherwise, return the data and skip the rest of the function. if (!empty($projects)) { return $projects; } @@ -359,8 +357,8 @@ function update_calculate_project_data($available) { // projects or releases). drupal_alter('update_status', $projects); - // Cache the site's update status for at most 1 hour. - _update_cache_set('update_project_data', $projects, REQUEST_TIME + 3600); + // Store the site's update status for at most 1 hour. + update_storage()->setWithExpire('update_project_data', $projects, 3600); return $projects; } @@ -750,37 +748,36 @@ function update_calculate_project_update_status(&$project_data, $available) { } /** - * Retrieves data from {cache_update} or empties the cache when necessary. + * Retrieves update storage data or empties it. * * Two very expensive arrays computed by this module are the list of all * installed modules and themes (and .info data, project associations, etc), and * the current status of the site relative to the currently available releases. - * These two arrays are cached in the {cache_update} table and used whenever - * possible. The cache is cleared whenever the administrator visits the status - * report, available updates report, or the module or theme administration - * pages, since we should always recompute the most current values on any of - * those pages. + * These two arrays are stored and used whenever possible. The data is cleared + * whenever the administrator visits the status report, available updates + * report, or the module or theme administration pages, since we should always + * recompute the most current values on any of those pages. * * Note: while both of these arrays are expensive to compute (in terms of disk * I/O and some fairly heavy CPU processing), neither of these is the actual * data about available updates that we have to fetch over the network from - * updates.drupal.org. That information is stored with the - * 'update_available_releases' cache ID -- it needs to persist longer than 1 + * updates.drupal.org. That information is stored in the + * 'update_available_releases' collection -- it needs to persist longer than 1 * hour and never get invalidated just by visiting a page on the site. * - * @param $cid - * The cache ID of data to return from the cache. Valid options are - * 'update_project_data' and 'update_project_projects'. + * @param $key + * The key of data to return. Valid options are 'update_project_data' and + * 'update_project_projects'. * * @return - * The cached value of the $projects array generated by + * The stored value of the $projects array generated by * update_calculate_project_data() or update_get_projects(), or an empty array - * when the cache is cleared. + * when the storage is cleared. */ -function update_project_cache($cid) { +function update_project_storage($key) { $projects = array(); - // On certain paths, we should clear the cache and recompute the projects for + // On certain paths, we should clear the data and recompute the projects for // update status of the site to avoid presenting stale information. $paths = array( 'admin/modules', @@ -794,13 +791,10 @@ function update_project_cache($cid) { 'admin/reports/updates/check', ); if (in_array(current_path(), $paths)) { - _update_cache_clear($cid); + update_storage()->delete($key); } else { - $cache = _update_cache_get($cid); - if (!empty($cache->data) && $cache->expire > REQUEST_TIME) { - $projects = $cache->data; - } + $projects = update_storage()->get($key); } return $projects; } diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc index a4d1bd7..61bc80f 100644 --- a/core/modules/update/update.fetch.inc +++ b/core/modules/update/update.fetch.inc @@ -117,8 +117,8 @@ function _update_fetch_data() { /** * Processes a task to fetch available update data for a single project. * - * Once the release history XML data is downloaded, it is parsed and saved into - * the {cache_update} table in an entry just for that project. + * Once the release history XML data is downloaded, it is parsed and saved in an + * entry just for that project. * * @param $project * Associative array of information about the project to fetch data for. @@ -132,13 +132,11 @@ function _update_process_fetch_task($project) { $fail = &drupal_static(__FUNCTION__, array()); // This can be in the middle of a long-running batch, so REQUEST_TIME won't // necessarily be valid. - $now = time(); + $request_time_difference = time() - REQUEST_TIME; if (empty($fail)) { // If we have valid data about release history XML servers that we have - // failed to fetch from on previous attempts, load that from the cache. - if (($cache = _update_cache_get('fetch_failures')) && ($cache->expire > $now)) { - $fail = $cache->data; - } + // failed to fetch from on previous attempts, load that. + $fail = update_storage()->get('fetch_failures'); } $max_fetch_attempts = $update_config->get('fetch.max_attempts'); @@ -179,43 +177,43 @@ function _update_process_fetch_task($project) { } $frequency = $update_config->get('check.interval_days'); - $cid = 'available_releases::' . $project_name; - _update_cache_set($cid, $available, $now + (60 * 60 * 24 * $frequency)); + $available['last_fetch'] = REQUEST_TIME + $request_time_difference; + update_storage('update_available_releases')->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency)); // Stash the $fail data back in the DB for the next 5 minutes. - _update_cache_set('fetch_failures', $fail, $now + (60 * 5)); + update_storage()->setWithExpire('fetch_failures', $fail, $request_time_difference + (60 * 5)); // Whether this worked or not, we did just (try to) check for updates. - variable_set('update_last_check', $now); + variable_set('update_last_check', REQUEST_TIME + $request_time_difference); // Now that we processed the fetch task for this project, clear out the - // record in {cache_update} for this task so we're willing to fetch again. - _update_cache_clear('fetch_task::' . $project_name); + // record for this task so we're willing to fetch again. + update_storage('update_fetch_task')->delete($project_name); return $success; } /** - * Clears out all the cached available update data and initiates re-fetching. + * Clears out all the available update data and initiates re-fetching. */ function _update_refresh() { module_load_include('inc', 'update', 'update.compare'); // Since we're fetching new available update data, we want to clear - // our cache of both the projects we care about, and the current update - // status of the site. We do *not* want to clear the cache of available + // both the projects we care about, and the current update + // status of the site. We do *not* want to clear the data of available // releases just yet, since that data (even if it's stale) can be useful // during update_get_projects(); for example, to modules that implement // hook_system_info_alter() such as cvs_deploy. - _update_cache_clear('update_project_projects'); - _update_cache_clear('update_project_data'); + update_storage()->delete('update_project_projects'); + update_storage()->delete('update_project_data'); $projects = update_get_projects(); - // Now that we have the list of projects, we should also clear our cache of - // available release data, since even if we fail to fetch new data, we need + // Now that we have the list of projects, we should also clear the available + // release data, since even if we fail to fetch new data, we need // to clear out the stale data at this point. - _update_cache_clear('available_releases::', TRUE); + update_storage('available_releases')->deleteAll(); foreach ($projects as $key => $project) { update_create_fetch_task($project); @@ -226,8 +224,7 @@ function _update_refresh() { * Adds a task to the queue for fetching release history data for a project. * * We only create a new fetch task if there's no task already in the queue for - * this particular project (based on 'fetch_task::' entries in the - * {cache_update} table). + * this particular project (based on 'update_fetch_task' key-value collection). * * @param $project * Associative array of information about a project as created by @@ -243,29 +240,13 @@ function _update_refresh() { function _update_create_fetch_task($project) { $fetch_tasks = &drupal_static(__FUNCTION__, array()); if (empty($fetch_tasks)) { - $fetch_tasks = _update_get_cache_multiple('fetch_task'); + $fetch_tasks = update_storage('update_fetch_task')->getAll(); } - $cid = 'fetch_task::' . $project['name']; - if (empty($fetch_tasks[$cid])) { + if (empty($fetch_tasks[$project['name']])) { $queue = queue('update_fetch_tasks'); $queue->createItem($project); - // Due to race conditions, it is possible that another process already - // inserted a row into the {cache_update} table and the following query will - // throw an exception. - // @todo: Remove the need for the manual check by relying on a queue that - // enforces unique items. - try { - db_insert('cache_update') - ->fields(array( - 'cid' => $cid, - 'created' => REQUEST_TIME, - )) - ->execute(); - } - catch (Exception $e) { - // The exception can be ignored safely. - } - $fetch_tasks[$cid] = REQUEST_TIME; + update_storage('update_fetch_task')->set($project['name'], $project); + $fetch_tasks[$project['name']] = REQUEST_TIME; } } diff --git a/core/modules/update/update.install b/core/modules/update/update.install index 14efc3a..8eedda1 100644 --- a/core/modules/update/update.install +++ b/core/modules/update/update.install @@ -57,15 +57,6 @@ function update_requirements($phase) { } /** - * Implements hook_schema(). - */ -function update_schema() { - $schema['cache_update'] = drupal_get_schema_unprocessed('system', 'cache'); - $schema['cache_update']['description'] = 'Cache table for the Update module to store information about available releases, fetched from central server.'; - return $schema; -} - -/** * Implements hook_install(). */ function update_install() { @@ -162,3 +153,10 @@ function update_update_8000() { 'update_notification_threshold' => 'notification.threshold', )); } + +/** + * Delete the {cache_update} table. + */ +function update_update_8001() { + db_delete('cache_update'); +} diff --git a/core/modules/update/update.module b/core/modules/update/update.module index 51e77e6..053f2e6 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -1,5 +1,9 @@ $interval) { // If the configured update interval has elapsed, we want to invalidate - // the cached data for all projects, attempt to re-fetch, and trigger any + // the data for all projects, attempt to re-fetch, and trigger any // configured notifications about the new status. update_refresh(); update_fetch_data(); @@ -311,33 +315,33 @@ function update_cron() { /** * Implements hook_themes_enabled(). * - * If themes are enabled, we invalidate the cache of available updates. + * If themes are enabled, we invalidate the information of available updates. */ function update_themes_enabled($themes) { - // Clear all update module caches. - _update_cache_clear(); + // Clear all update module data. + update_storage_clear(); } /** * Implements hook_themes_disabled(). * - * If themes are disabled, we invalidate the cache of available updates. + * If themes are disabled, we invalidate the information of available updates. */ function update_themes_disabled($themes) { - // Clear all update module caches. - _update_cache_clear(); + // Clear all update module data. + update_storage_clear(); } /** * Implements hook_form_FORM_ID_alter() for system_modules(). * * Adds a form submission handler to the system modules form, so that if a site - * admin saves the form, we invalidate the cache of available updates. + * admin saves the form, we invalidate the information of available updates. * * @see _update_cache_clear() */ function update_form_system_modules_alter(&$form, $form_state) { - $form['#submit'][] = 'update_cache_clear_submit'; + $form['#submit'][] = 'update_storage_clear_submit'; } /** @@ -345,9 +349,9 @@ function update_form_system_modules_alter(&$form, $form_state) { * * @see update_form_system_modules_alter() */ -function update_cache_clear_submit($form, &$form_state) { - // Clear all update module caches. - _update_cache_clear(); +function update_storage_clear_submit($form, &$form_state) { + // Clear all update module data. + update_storage_clear(); } /** @@ -362,9 +366,9 @@ function _update_no_data() { } /** - * Tries to get update information from cache and refreshes it when necessary. + * Tries to get update information and refreshes it when necessary. * - * In addition to checking the cache lifetime, this function also ensures that + * In addition to checking the lifetime, this function also ensures that * there are no .info files for enabled modules or themes that have a newer * modification timestamp than the last time we checked for available update * data. If any .info file was modified, it almost certainly means a new version @@ -373,8 +377,8 @@ function _update_no_data() { * results. * * @param $refresh - * (optional) Boolean to indicate if this method should refresh the cache - * automatically if there's no data. Defaults to FALSE. + * (optional) Boolean to indicate if this method should refresh automatically + * if there's no data. Defaults to FALSE. * * @return * Array of data about available releases, keyed by project shortname. @@ -386,9 +390,8 @@ function update_get_available($refresh = FALSE) { module_load_include('inc', 'update', 'update.compare'); $needs_refresh = FALSE; - // Grab whatever data we currently have cached in the DB. - $available = _update_get_cached_available_releases(); - $num_avail = count($available); + // Grab whatever data we currently have. + $available = update_storage('update_available_releases')->getAll(); $projects = update_get_projects(); foreach ($projects as $key => $project) { @@ -426,8 +429,8 @@ function update_get_available($refresh = FALSE) { // Attempt to drain the queue of fetch tasks. update_fetch_data(); // After processing the queue, we've (hopefully) got better data, so pull - // the latest from the cache again and use that directly. - $available = _update_get_cached_available_releases(); + // the latest data again and use that directly. + $available = update_storage('update_available_releases')->getAll(); } return $available; @@ -468,29 +471,6 @@ function update_fetch_data() { } /** - * Returns all currently cached data about available releases for all projects. - * - * @return - * Array of data about available releases, keyed by project shortname. - */ -function _update_get_cached_available_releases() { - $data = array(); - $cache_items = _update_get_cache_multiple('available_releases'); - foreach ($cache_items as $cid => $cache) { - $cache->data['last_fetch'] = $cache->created; - if ($cache->expire < REQUEST_TIME) { - $cache->data['fetch_status'] = UPDATE_FETCH_PENDING; - } - // The project shortname is embedded in the cache ID, even if there's no - // data for this project in the DB at all, so use that for the indexes in - // the array. - $parts = explode('::', $cid, 2); - $data[$parts[1]] = $cache->data; - } - return $data; -} - -/** * Implements hook_mail(). * * Constructs the e-mail notification message when the site is out of date. @@ -725,137 +705,30 @@ function update_verify_update_archive($project, $archive_file, $directory) { } /** - * @defgroup update_status_cache Private update status cache system - * @{ - * Functions to manage the update status cache. - * - * We specifically do NOT use the core cache API for saving the fetched data - * about available updates. It is vitally important that this cache is only - * cleared when we're populating it after successfully fetching new available - * update data. Usage of the core cache API results in all sorts of potential - * problems that would result in attempting to fetch available update data all - * the time, including if a site has a "minimum cache lifetime" (which is both a - * minimum and a maximum) defined, or if a site uses memcache or another - * pluggable cache system that assumes volatile caches. - * - * The Update Manager module still uses the {cache_update} table, but instead of - * using the cache API, there are private helper functions that implement these - * same basic tasks but ensure that the cache is not prematurely cleared, and - * that the data is always stored in the database, even if memcache or another - * cache backend is in use. - */ - -/** - * Stores data in the private update status cache table. * - * @param $cid - * The cache ID to save the data with. - * @param $data - * The data to store. - * @param $expire - * One of the following values: - * - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should - * never be removed except by explicitly using _update_cache_clear(). - * - A Unix timestamp: Indicates that the item should be kept at least until - * the given time, after which it will be invalidated. - * - * @see _update_cache_get() + * @return Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface */ -function _update_cache_set($cid, $data, $expire) { - $fields = array( - 'created' => REQUEST_TIME, - 'expire' => $expire, - ); - if (!is_string($data)) { - $fields['data'] = serialize($data); - $fields['serialized'] = 1; - } - else { - $fields['data'] = $data; - $fields['serialized'] = 0; - } - db_merge('cache_update') - ->key(array('cid' => $cid)) - ->fields($fields) - ->execute(); -} - -/** - * Retrieves data from the private update status cache table. - * - * @param $cid - * The cache ID to retrieve. - * - * @return - * An array of data for the given cache ID, or NULL if the ID was not found. - * - * @see _update_cache_set() - */ -function _update_cache_get($cid) { - $cache = db_query("SELECT data, created, expire, serialized FROM {cache_update} WHERE cid = :cid", array(':cid' => $cid))->fetchObject(); - if (isset($cache->data)) { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); +function update_storage($collection = 'update') { + $storage = &drupal_static(__FUNCTION__); + if (!isset($storage[$collection])) { + // @todo Set without expiration currently does not work on + // DatabaseStorageExpirable. + if ($collection == 'update_fetch_task') { + $storage[$collection] = new DatabaseStorage($collection, array('connection' => Database::getConnection())); } - } - return $cache; -} - -/** - * Returns an array of cache items with a given cache ID prefix. - * - * @param string $cid_prefix - * The cache ID prefix. - * - * @return - * Associative array of cache items, keyed by cache ID. - */ -function _update_get_cache_multiple($cid_prefix) { - $data = array(); - $result = db_select('cache_update') - ->fields('cache_update', array('cid', 'data', 'created', 'expire', 'serialized')) - ->condition('cache_update.cid', $cid_prefix . '::%', 'LIKE') - ->execute(); - foreach ($result as $cache) { - if ($cache) { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); - } - $data[$cache->cid] = $cache; + else { + $storage[$collection] = new DatabaseStorageExpirable($collection, array('connection' => Database::getConnection())); } } - return $data; + return $storage[$collection]; } /** - * Invalidates cached data relating to update status. - * - * @param $cid - * (optional) Cache ID of the record to clear from the private update module - * cache. If empty, all records will be cleared from the table except fetch - * tasks. Defaults to NULL. - * @param $wildcard - * (optional) If TRUE, cache IDs starting with $cid are deleted in addition to - * the exact cache ID specified by $cid. Defaults to FALSE. + * Invalidates stored data relating to update status. */ -function _update_cache_clear($cid = NULL, $wildcard = FALSE) { - if (empty($cid)) { - db_delete('cache_update') - // Clear everything except fetch task information because these are used - // to ensure that the fetch task queue items are not added multiple times. - ->condition('cid', 'fetch_task::%', 'NOT LIKE') - ->execute(); - } - else { - $query = db_delete('cache_update'); - if ($wildcard) { - $query->condition('cid', $cid . '%', 'LIKE'); - } - else { - $query->condition('cid', $cid); - } - $query->execute(); - } +function update_storage_clear() { + update_storage()->deleteAll(); + update_storage('update_available_release')->deleteAll(); } /** @@ -864,8 +737,8 @@ function _update_cache_clear($cid = NULL, $wildcard = FALSE) { * Called from update.php (among others) to flush the caches. Since we're * running update.php, we are likely to install a new version of something, in * which case, we want to check for available update data again. However, - * because we have our own caching system, we need to directly clear the - * the database table ourselves at this point and return nothing. + * because we use the key value storage system, we need to clear directly at + * this point and return nothing. * * However, we only want to do this from update.php, since otherwise, we'd lose * all the available update data on every cron run. So, we specifically check if @@ -874,16 +747,12 @@ function _update_cache_clear($cid = NULL, $wildcard = FALSE) { */ function update_cache_flush() { if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') { - _update_cache_clear(); + update_storage_clear(); } return array(); } /** - * @} End of "defgroup update_status_cache". - */ - -/** * Returns a short unique identifier for this Drupal installation. * * @return diff --git a/core/modules/update/update.settings.inc b/core/modules/update/update.settings.inc index 0a5e337..442510b 100644 --- a/core/modules/update/update.settings.inc +++ b/core/modules/update/update.settings.inc @@ -92,7 +92,7 @@ function update_settings_validate($form, &$form_state) { /** * Form submission handler for update_settings(). * - * Also invalidates the cache of available updates if the "Check for updates of + * Also invalidates the data of available updates if the "Check for updates of * disabled modules and themes" setting is being changed. The available updates * report needs to refetch available update data after this setting changes or * it would show misleading things (e.g., listing the disabled projects on the @@ -104,9 +104,9 @@ function update_settings_submit($form, $form_state) { $config = config('update.settings'); // See if the update_check_disabled setting is being changed, and if so, - // invalidate all cached update status data. + // invalidate all update status data. if ($form_state['values']['update_check_disabled'] != $config->get('check.disabled_extensions')) { - _update_cache_clear(); + update_storage_clear(); } $config diff --git a/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwner.php b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwner.php new file mode 100644 index 0000000..24478c9 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwner.php @@ -0,0 +1,155 @@ +storage = $storage; + $this->lockBackend = $lockBackend; + $this->owner = $owner; + } + + /** + * Returns the stored value for the default key. + * + * @param string $key + * The key of the data to retrieve. + * + * @return mixed + * The data to retieve, or NULL if the key does not exist. + */ + function get($key) { + if ($object = $this->storage->get($key)) { + return $object->data; + } + } + + /** + * Adds the value for the default key if it doesn't exist yet. + * + * @param string $key + * The key of the data to check and store. + * @param mixed $value + * The data to store. + * + * @return bool + * TRUE if the data was set, FALSE if it already existed. + */ + function setIfNotExists($key, $value) { + $value = (object) array( + 'owner' => $this->owner, + 'data' => $value, + 'updated' => REQUEST_TIME, + ); + return $this->storage->setWithExpireIfNotExists($key, $value, $this->expire); + } + + /** + * Sets the value for the default key. + * + * @param string $key + * The key of the data to store. + * @param mixed $value + * The data to store. + */ + function set($key, $value) { + if ($this->lockBackend->acquire($key)) { + $object = $this->storage->get($key); + if (!$object || $object->owner == $this->owner) { + $value = (object) array( + 'owner' => $this->owner, + 'data' => $value, + 'updated' => REQUEST_TIME, + ); + $this->storage->setWithExpire($key, $value, $this->expire); + } + $this->lockBackend->release($key); + } + } + + /** + * Gets the metadata for a key. + * + * @param string $key + * The key of the data to store. + * + * @return mixed + * An object with the owner and updated time if the key has a value, NULL + * otherwise. + */ + function getMetadata($key) { + $object = $this->storage->get($key); + if ($object) { + unset($object->data); + return $object; + } + } + + /** + * Deletes the value of the default key. + * + * @param string $key + * The key of the data to store. + */ + function delete($key) { + if (!$this->lockBackend->acquire($key)) { + $this->lockBackend->wait($key); + if (!$this->lockBackend->acquire($key)) { + throw new KeyValueStoreWithOwnerException("Couldn't acquire lock"); + } + } + $this->storage->delete($key); + $this->lockBackend->release($key); + } + +} diff --git a/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwnerException.php b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwnerException.php new file mode 100644 index 0000000..4a35e23 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/KeyValueStoreWithOwnerException.php @@ -0,0 +1,13 @@ +connection = $connection; + $this->lockBackend = $lockBackend; + } + + /** + * Creates a Drupal\user\KeyValueStoreWithOwner stored in the database. + * + * @param string $namespace + * The namespace to use for this key/value store. + * + * @return Drupal\user\KeyValueStoreWithOwner + * An instance of the the key/value store. + */ + function get($namespace) { + $storage = new DatabaseStorageExpirable($namespace, array('connection' => $this->connection)); + return new KeyValueStoreWithOwner($storage, $this->lockBackend, $GLOBALS['user']->uid ?: session_id()); + } + +} diff --git a/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php b/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php new file mode 100644 index 0000000..4927ffa --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php @@ -0,0 +1,165 @@ + 'TempStore', + 'description' => 'Tests the temporary object storage system.', + 'group' => 'TempStore', + ); + } + + protected function setUp() { + parent::setUp(); + module_load_install('system'); + $schema = system_schema(); + db_create_table('semaphore', $schema['semaphore']); + db_create_table('key_value_expire', $schema['key_value_expire']); + $this->storeFactory = new KeyValueStoreWithOwnerFactory(Database::getConnection(), new DatabaseLockBackend()); + $this->collection = $this->randomName(); + + // Create two users and two objects for testing. + for ($i = 0; $i <= 3; $i++) { + $this->objects[$i] = $this->randomObject(); + $this->users[$i] = mt_rand(500, 5000000); + } + } + + /** + * Generates a random PHP object. + * + * @param int $size + * The number of random keys to add to the object. + * + * @return stdClass + * The generated object, with the specified number of random keys. Each key + * has a random string value. + */ + public function randomObject($size = 4) { + $object = new stdClass(); + for ($i = 0; $i < $size; $i++) { + $random_key = $this->randomName(); + $random_value = $this->randomString(); + $object->{$random_key} = $random_value; + } + return $object; + } + + /** + * Tests the UserTempStore API. + */ + public function testUserTempStore() { + $key = $this->randomName(); + // First test that only one setIfNotExists succeeds. + for ($i = 0; $i <= 1; $i++) { + $store = $this->getStorePerUid($this->users[$i]); + // Setting twice results only in the first succeeding. + $this->assertEqual(!$i, $store->setIfNotExists($key, $this->objects[$i])); + $metadata = $store->getMetadata($key); + $this->assertEqual($this->users[0], $metadata->owner); + $this->assertIdenticalObject($this->objects[0], $store->get($key)); + } + // Remove the item. + $store->delete($key); + // Try to set it again. + $store->setIfNotExists($key, $this->objects[1]); + // This time it succeeds. + $this->assertIdenticalObject($this->objects[1], $store->get($key)); + // This user can update the object. + $store->set($key, $this->objects[2]); + $this->assertIdenticalObject($this->objects[2], $store->get($key)); + // But another can't. + $store = $this->getStorePerUid($this->users[2]); + $store->set($key, $this->objects[3]); + $this->assertIdenticalObject($this->objects[2], $store->get($key)); + // Now manually expire the item (this is not exposed by the API) and then + // assert it is no longer accessible. + db_update('key_value_expire') + ->fields(array('expire' => REQUEST_TIME - 1)) + ->condition('collection', $this->collection) + ->condition('name', $key) + ->execute(); + $this->assertFalse($store->get($key)); + } + + /** + * Returns a KeyValueStoreWithOwner belonging to the passed in user. + * + * @param int $uid + * A user ID. + * + * @return Drupal\user\KeyValueStoreWithOwner + * The key/value store object. + */ + protected function getStorePerUid($uid) { + $GLOBALS['user']->uid = $uid; + // This relies on the logged user uid!! + return $this->storeFactory->get($this->collection); + } + + /** + * Checks to see if two objects are identical. + * + * @param object $object1 + * The first object to check. + * @param object $object2 + * The second object to check. + */ + protected function assertIdenticalObject($object1, $object2) { + $identical = TRUE; + foreach ($object1 as $key => $value) { + $identical = $identical && isset($object2->$key) && $object2->$key === $value; + } + $this->assertTrue($identical, format_string('!object1 is identical to !object2', array( + '!object1' => var_export($object1, TRUE), + '!object2' => var_export($object2, TRUE), + ))); + } +} diff --git a/modules/README.txt b/modules/README.txt index f4e35b5..339aa00 100644 --- a/modules/README.txt +++ b/modules/README.txt @@ -1,4 +1,18 @@ +Place downloaded and custom modules that extend your site functionality beyond +Drupal core in this directory to ensure clean separation from core modules and +to facilitate safe, self-contained code updates. Contributed modules from the +Drupal community may be downloaded at http://drupal.org/project/modules. -This directory should be used to place downloaded and custom modules -which are common to all sites. This will allow you to more easily -update Drupal core files. +It is safe to organize modules into subdirectories, such as "contrib" for +contributed modules, and "custom" for custom modules. Note that if you move a +module to a subdirectory after it has been enabled, you may need to clear the +Drupal cache so that it can be found. + +In multisite configuration, modules found in this directory are available to +all sites. In addition to this directory, shared common modules may also be kept +in the sites/all/modules directory and will take precedence over modules in this +directory. Alternatively, the sites/your_site_name/modules directory pattern may +be used to restrict modules to a specific site instance. + +Refer to the "Developing for Drupal" section of the README.txt in the Drupal +root directory for further information on extending Drupal with custom modules. diff --git a/profiles/README.txt b/profiles/README.txt index 2bbf4c9..69bb492 100644 --- a/profiles/README.txt +++ b/profiles/README.txt @@ -1,4 +1,18 @@ +Place downloaded and custom installation profiles in this directory to ensure +separation from Drupal core profiles and to facilitate safe, self-contained code +updates. -This directory should be used to place downloaded and custom profiles -which are common to all sites. This will allow you to more easily -update Drupal core files. +In multisite configuration, installation profiles found in this directory are +available to all sites during their initial site installation. Shared common +profiles may also be kept in the sites/all/profiles directory and will take +precedence over profiles in this directory. Alternatively, the +sites/your_site_name/profiles directory pattern may be used to restrict a +profile's availability to a specific site instance. + +Additionally, modules and themes may be placed inside subdirectories in a +specific installation profile such as profiles/your_site_profile/modules and +profiles/your_site_profile/themes respectively to restrict their usage to only +sites that were installed with that specific profile. + +Refer to the "Installation Profiles" section of the README.txt in the Drupal +root directory for further information. diff --git a/sites/README.txt b/sites/README.txt new file mode 100644 index 0000000..0372902 --- /dev/null +++ b/sites/README.txt @@ -0,0 +1,10 @@ +This directory structure contains the settings and configuration files specific +to your site or sites and is an integral part of multisite configurations. + +It is now recommended to place your custom and downloaded extensions in the +/modules, /themes, and /profiles directories located in the Drupal root. The +sites/all/ subdirectory structure, which was recommended in previous versions +of Drupal, is still supported. + +See core/INSTALL.txt for information about single-site installation or +multisite configuration. diff --git a/themes/README.txt b/themes/README.txt index e942521..320b967 100644 --- a/themes/README.txt +++ b/themes/README.txt @@ -1,4 +1,16 @@ +Place downloaded and custom themes that modify your site's appearance in this +directory to ensure clean separation from Drupal core and to facilitate safe, +self-contained code updates. Contributed themes from the Drupal community may +be downloaded at http://drupal.org/project/themes. -This directory should be used to place downloaded and custom themes -which are common to all sites. This will allow you to more easily -update Drupal core files. +It is safe to organize themes into subdirectories and is recommended to use +Drupal's sub-theme functionality to ensure easy maintenance and upgrades. + +In multisite configuration, themes found in this directory are available to +all sites. In addition to this directory, shared common themes may also be kept +in the sites/all/themes directory and will take precedence over themes in this +directory. Alternatively, the sites/your_site_name/themes directory pattern may +be used to restrict themes to a specific site instance. + +Refer to the "Appearance" section of the README.txt in the Drupal root +directory for further information on theming.