diff --git a/composer.lock b/composer.lock
index 86881211a..f2c6c183a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -891,16 +891,16 @@
},
{
"name": "masterminds/html5",
- "version": "2.1.2",
+ "version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
- "reference": "8f782e0f01a6e33a319bdc8f6de9cfd6569979a4"
+ "reference": "170aa5cb35b29fccafbf5ea63487c013f396fdc9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/8f782e0f01a6e33a319bdc8f6de9cfd6569979a4",
- "reference": "8f782e0f01a6e33a319bdc8f6de9cfd6569979a4",
+ "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/170aa5cb35b29fccafbf5ea63487c013f396fdc9",
+ "reference": "170aa5cb35b29fccafbf5ea63487c013f396fdc9",
"shasum": ""
},
"require": {
@@ -910,7 +910,7 @@
"require-dev": {
"phpunit/phpunit": "4.*",
"sami/sami": "~2.0",
- "satooshi/php-coveralls": "0.6.*"
+ "satooshi/php-coveralls": "1.0.*"
},
"type": "library",
"extra": {
@@ -952,7 +952,7 @@
"serializer",
"xml"
],
- "time": "2015-06-07 08:43:18"
+ "time": "2016-05-10 14:11:45"
},
{
"name": "paragonie/random_compat",
diff --git a/core/INSTALL.txt b/core/INSTALL.txt
index ff35ee334..8c5ee8723 100644
--- a/core/INSTALL.txt
+++ b/core/INSTALL.txt
@@ -27,7 +27,7 @@ Drupal requires:
- Percona Server 5.5.8 (or greater) (http://www.percona.com/). Percona
Server is a backwards-compatible replacement for MySQL.
- PostgreSQL 9.1.2 (or greater) (http://www.postgresql.org/).
- - SQLite 3.6.8 (or greater) (http://www.sqlite.org/).
+ - SQLite 3.7.11 (or greater) (http://www.sqlite.org/).
For more detailed information about Drupal requirements, including a list of
PHP extensions and configurations that are required, see "System requirements"
diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index 44b4d7a01..10ee48cf7 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -28,8 +28,12 @@ Provisional membership:
- Scott Reeves 'Cottser' https://www.drupal.org/u/cottser
Drupal 7
+- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
+ (Framework Manager)
- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
(Release Manager, Framework Manager, Product Manager)
+- Stefan Ruijsenaars 'stefan.r' https://www.drupal.org/u/stefanr-0
+ (Release Manager, Product Manager)
Provisional membership: None at this time.
@@ -200,7 +204,6 @@ Transliteration system
- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu
- Damien Tournoud 'damien-tournoud' https://www.drupal.org/u/damien-tournoud
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
-- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
Typed data system
- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
@@ -213,7 +216,7 @@ Accessibility
- Jesse Renée Beach 'jessebeach' https://www.drupal.org/u/jessebeach
Documentation
-- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
+- ?
Performance
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
@@ -413,7 +416,6 @@ Responsive Image module
- Jelle Sebreghts 'Jelle_S' https://www.drupal.org/u/jelle_s
Search module
-- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
Serialization module
diff --git a/core/composer.json b/core/composer.json
index 1266df391..a8ee73e4c 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -64,8 +64,10 @@
"drupal/content_translation": "self.version",
"drupal/contextual": "self.version",
"drupal/core-annotation": "self.version",
+ "drupal/core-assertion": "self.version",
"drupal/core-bridge": "self.version",
"drupal/core-datetime": "self.version",
+ "drupal/core-dependency-injection": "self.version",
"drupal/core-diff": "self.version",
"drupal/core-discovery": "self.version",
"drupal/core-event-dispatcher": "self.version",
diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index d48aa4eb2..cdf3120b9 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -32,12 +32,13 @@ function entity_render_cache_clear() {
* The bundle info for a specific entity type, or all entity types.
*
* @deprecated in Drupal 8.x-dev and will be removed before Drupal 9.0.0. Use
- * \Drupal\Core\Entity\EntityManagerInterface::getBundleInfo() for a single
- * bundle, or \Drupal\Core\Entity\EntityManagerInterface::getAllBundleInfo()
- * for all bundles.
+ * \Drupal\Core\Entity\EntityTypeBundleInfoInterface::getBundleInfo() for a
+ * single bundle, or
+ * \Drupal\Core\Entity\EntityTypeBundleInfoInterface::getAllBundleInfo() for
+ * all bundles.
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getBundleInfo()
- * @see \Drupal\Core\Entity\EntityManagerInterface::getAllBundleInfo()
+ * @see \Drupal\Core\Entity\EntityTypeBundleInfoInterface::getBundleInfo()
+ * @see \Drupal\Core\Entity\EntityTypeBundleInfoInterface::getAllBundleInfo()
*/
function entity_get_bundles($entity_type = NULL) {
if (isset($entity_type)) {
@@ -67,11 +68,11 @@ function entity_get_bundles($entity_type = NULL) {
* entity type is variable, use the entity manager service to load the entity
* from the entity storage:
* @code
- * \Drupal::entityManager()->getStorage($entity_type)->load($id)
+ * \Drupal::entityTypeManager()->getStorage($entity_type)->load($id);
* @endcode
*
* @see \Drupal\Core\Entity\EntityInterface::load()
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::load()
* @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage
* @see \Drupal\Core\Entity\Query\QueryInterface
@@ -100,10 +101,12 @@ function entity_load($entity_type, $id, $reset = FALSE) {
* the entity storage's loadRevision() method to load a specific entity
* revision:
* @code
- * \Drupal::entityManager()->getStorage($entity_type)->loadRevision($revision_id);
+ * \Drupal::entityTypeManager()
+ * ->getStorage($entity_type)
+ * ->loadRevision($revision_id);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::loadRevision()
* @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage
*/
@@ -125,10 +128,12 @@ function entity_revision_load($entity_type, $revision_id) {
* the entity storage's deleteRevision() method to delete a specific entity
* revision:
* @code
- * \Drupal::entityManager()->getStorage($entity_type)>deleteRevision($revision_id);
+ * \Drupal::entityTypeManager()
+ * ->getStorage($entity_type)
+ * ->deleteRevision($revision_id);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::deleteRevision()
*/
function entity_revision_delete($entity_type, $revision_id) {
@@ -171,11 +176,11 @@ function entity_revision_delete($entity_type, $revision_id) {
* the entity type is variable, use the entity manager service to load the
* entity from the entity storage:
* @code
- * \Drupal::entityManager()->getStorage($entity_type)->loadMultiple($id)
+ * \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple($id);
* @endcode
*
* @see \Drupal\Core\Entity\EntityInterface::loadMultiple()
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::loadMultiple()
* @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage
* @see \Drupal\Core\Entity\Query\QueryInterface
@@ -205,10 +210,12 @@ function entity_load_multiple($entity_type, array $ids = NULL, $reset = FALSE) {
* the entity storage's loadByProperties() method to load an entity by their
* property values:
* @code
- * \Drupal::entityManager()->getStorage($entity_type)->loadByProperties($values);
+ * \Drupal::entityTypeManager()
+ * ->getStorage($entity_type)
+ * ->loadByProperties($values);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::loadByProperties()
*/
function entity_load_multiple_by_properties($entity_type, array $values) {
@@ -236,10 +243,10 @@ function entity_load_multiple_by_properties($entity_type, array $values) {
* @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
* the entity storage's loadUnchanged() method to load an unchanged entity:
* @code
- * \Drupal::entityManager()->getStorage($entity_type)->loadUnchanged($id).
+ * \Drupal::entityTypeManager()->getStorage($entity_type)->loadUnchanged($id);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::loadUnchanged()
*/
function entity_load_unchanged($entity_type, $id) {
@@ -259,12 +266,12 @@ function entity_load_unchanged($entity_type, $id) {
* @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
* the entity storage's delete() method to delete multiple entities:
* @code
- * $storage_handler = \Drupal::entityManager()->getStorage($entity_type);
+ * $storage_handler = \Drupal::entityTypeManager()->getStorage($entity_type);
* $entities = $storage_handler->loadMultiple($ids);
* $storage_handler->delete($entities);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::loadMultiple()
* @see \Drupal\Core\Entity\EntityStorageInterface::delete()
*/
@@ -292,10 +299,10 @@ function entity_delete_multiple($entity_type, array $ids) {
* entity type is variable, use the entity storage's create() method to
* construct a new entity:
* @code
- * \Drupal::entityManager()->getStorage($entity_type)->create($values)
+ * \Drupal::entityTypeManager()->getStorage($entity_type)->create($values);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getStorage()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getStorage()
* @see \Drupal\Core\Entity\EntityStorageInterface::create()
*/
function entity_create($entity_type, array $values = array()) {
@@ -349,11 +356,12 @@ function entity_page_label(EntityInterface $entity, $langcode = NULL) {
* @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0.
* Use the entity view builder's view() method for creating a render array:
* @code
- * $view_builder = \Drupal::entityManager()->getViewBuilder($entity->getEntityTypeId());
+ * $view_builder = \Drupal::entityTypeManager()
+ * ->getViewBuilder($entity->getEntityTypeId());
* return $view_builder->view($entity, $view_mode, $langcode);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getViewBuilder()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getViewBuilder()
* @see \Drupal\Core\Entity\EntityViewBuilderInterface::view()
*/
function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL, $reset = FALSE) {
@@ -386,11 +394,12 @@ function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL, $res
* Use the entity view builder's viewMultiple() method for creating a render
* array for the provided entities:
* @code
- * $view_builder = \Drupal::entityManager()->getViewBuilder($entity->getEntityTypeId());
+ * $view_builder = \Drupal::entityTypeManager()
+ * ->getViewBuilder($entity->getEntityTypeId());
* return $view_builder->viewMultiple($entities, $view_mode, $langcode);
* @endcode
*
- * @see \Drupal\Core\Entity\EntityManagerInterface::getViewBuilder()
+ * @see \Drupal\Core\Entity\EntityTypeManagerInterface::getViewBuilder()
* @see \Drupal\Core\Entity\EntityViewBuilderInterface::viewMultiple()
*/
function entity_view_multiple(array $entities, $view_mode, $langcode = NULL, $reset = FALSE) {
@@ -443,7 +452,9 @@ function entity_view_multiple(array $entities, $view_mode, $langcode = NULL, $re
* @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0.
* If the display is available in configuration use:
* @code
- * \Drupal::entityManager()->getStorage('entity_view_display')->load($entity_type . '.' . $bundle . '.' . $view_mode);
+ * \Drupal::entityTypeManager()
+ * ->getStorage('entity_view_display')
+ * ->load($entity_type . '.' . $bundle . '.' . $view_mode);
* @endcode
* When the display is not available in configuration, you can create a new
* EntityViewDisplay object using:
@@ -454,7 +465,9 @@ function entity_view_multiple(array $entities, $view_mode, $langcode = NULL, $re
* 'mode' => $view_mode,
* 'status' => TRUE,
* ));
- * \Drupal::entityManager()->getStorage('entity_view_display')->create($values);
+ * \Drupal::entityTypeManager()
+ * ->getStorage('entity_view_display')
+ * ->create($values);
* @endcode
*
* @see \Drupal\Core\Entity\EntityStorageInterface::create()
@@ -519,10 +532,12 @@ function entity_get_display($entity_type, $bundle, $view_mode) {
* @deprecated as of Drupal 8.0.x, will be removed before Drupal 9.0.0.
* If the entity form display is available in configuration use:
* @code
- * \Drupal::entityManager()->getStorage('entity_form_display')->load($entity_type . '.' . $bundle . '.' . $form_mode);
+ * \Drupal::entityTypeManager()
+ * ->getStorage('entity_form_display')
+ * ->load($entity_type . '.' . $bundle . '.' . $form_mode);
* @endcode
- * When the entity form display is not available in configuration, you can create a new
- * EntityFormDisplay object using:
+ * When the entity form display is not available in configuration, you can
+ * create a new EntityFormDisplay object using:
* @code
* $values = ('entity_form_display', array(
* 'targetEntityType' => $entity_type,
@@ -530,7 +545,9 @@ function entity_get_display($entity_type, $bundle, $view_mode) {
* 'mode' => $form_mode,
* 'status' => TRUE,
* ));
- * \Drupal::entityManager()->getStorage('entity_form_display')->create($values);
+ * \Drupal::entityTypeManager()
+ * ->getStorage('entity_form_display')
+ * ->create($values);
* @endcode
*
* @see \Drupal\Core\Entity\EntityStorageInterface::create()
diff --git a/core/includes/file.inc b/core/includes/file.inc
index 3cd943982..528d00bc4 100644
--- a/core/includes/file.inc
+++ b/core/includes/file.inc
@@ -397,7 +397,7 @@ function file_valid_uri($uri) {
}
/**
- * Copies a file to a new location without invoking the file API.
+ * Copies a file to a new location without database changes or hook invocation.
*
* This is a powerful function that in many ways performs like an advanced
* version of copy().
@@ -407,10 +407,9 @@ function file_valid_uri($uri) {
* - If the $source and $destination are equal, the behavior depends on the
* $replace parameter. FILE_EXISTS_REPLACE will error out. FILE_EXISTS_RENAME
* will rename the file until the $destination is unique.
- * - Provides a fallback using realpaths if the move fails using stream
- * wrappers. This can occur because PHP's copy() function does not properly
- * support streams if open_basedir is enabled. See
- * https://bugs.php.net/bug.php?id=60456
+ * - Works around a PHP bug where copy() does not properly support streams if
+ * safe_mode or open_basedir are enabled.
+ * @see https://bugs.php.net/bug.php?id=60456
*
* @param $source
* A string specifying the filepath or URI of the source file.
@@ -431,18 +430,66 @@ function file_valid_uri($uri) {
* @see file_copy()
*/
function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
+ if (!file_unmanaged_prepare($source, $destination, $replace)) {
+ return FALSE;
+ }
+ // Attempt to resolve the URIs. This is necessary in certain configurations
+ // (see above).
+ $real_source = drupal_realpath($source) ?: $source;
+ $real_destination = drupal_realpath($destination) ?: $destination;
+ // Perform the copy operation.
+ if (!@copy($real_source, $real_destination)) {
+ \Drupal::logger('file')->error('The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination));
+ return FALSE;
+ }
+ // Set the permissions on the new file.
+ drupal_chmod($destination);
+ return $destination;
+}
+
+/**
+ * Internal function that prepares the destination for a file_unmanaged_copy or
+ * file_unmanaged_move operation.
+ *
+ * - Checks if $source and $destination are valid and readable/writable.
+ * - Checks that $source is not equal to $destination; if they are an error
+ * is reported.
+ * - If file already exists in $destination either the call will error out,
+ * replace the file or rename the file based on the $replace parameter.
+ *
+ * @param $source
+ * A string specifying the filepath or URI of the source file.
+ * @param $destination
+ * A URI containing the destination that $source should be moved/copied to.
+ * The URI may be a bare filepath (without a scheme) and in that case the
+ * default scheme (file://) will be used. If this value is omitted, Drupal's
+ * default files scheme will be used, usually "public://".
+ * @param $replace
+ * Replace behavior when the destination file already exists:
+ * - FILE_EXISTS_REPLACE - Replace the existing file.
+ * - FILE_EXISTS_RENAME - Append _{incrementing number} until the filename is
+ * unique.
+ * - FILE_EXISTS_ERROR - Do nothing and return FALSE.
+ *
+ * @return
+ * TRUE, or FALSE in the event of an error.
+ *
+ * @see file_unmanaged_copy()
+ * @see file_unmanaged_move()
+ */
+function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_EXISTS_RENAME) {
$original_source = $source;
$logger = \Drupal::logger('file');
// Assert that the source file actually exists.
if (!file_exists($source)) {
// @todo Replace drupal_set_message() calls with exceptions instead.
- drupal_set_message(t('The specified file %file could not be copied because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
+ drupal_set_message(t('The specified file %file could not be moved/copied because no file by that name exists. Please check that you supplied the correct filename.', array('%file' => $original_source)), 'error');
if (($realpath = drupal_realpath($original_source)) !== FALSE) {
- $logger->notice('File %file (%realpath) could not be copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
+ $logger->notice('File %file (%realpath) could not be moved/copied because it does not exist.', array('%file' => $original_source, '%realpath' => $realpath));
}
else {
- $logger->notice('File %file could not be copied because it does not exist.', array('%file' => $original_source));
+ $logger->notice('File %file could not be moved/copied because it does not exist.', array('%file' => $original_source));
}
return FALSE;
}
@@ -463,8 +510,8 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
$dirname = drupal_dirname($destination);
if (!file_prepare_directory($dirname)) {
// The destination is not valid.
- $logger->notice('File %file could not be copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
- drupal_set_message(t('The specified file %file could not be copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
+ $logger->notice('File %file could not be moved/copied because the destination directory %destination is not configured correctly.', array('%file' => $original_source, '%destination' => $dirname));
+ drupal_set_message(t('The specified file %file could not be moved/copied because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $original_source)), 'error');
return FALSE;
}
}
@@ -472,8 +519,8 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
// Determine whether we can perform this operation based on overwrite rules.
$destination = file_destination($destination, $replace);
if ($destination === FALSE) {
- drupal_set_message(t('The file %file could not be copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
- $logger->notice('File %file could not be copied because a file by that name already exists in the destination directory (%destination)', array('%file' => $original_source, '%destination' => $destination));
+ drupal_set_message(t('The file %file could not be moved/copied because a file by that name already exists in the destination directory.', array('%file' => $original_source)), 'error');
+ $logger->notice('File %file could not be moved/copied because a file by that name already exists in the destination directory (%destination)', array('%file' => $original_source, '%destination' => $destination));
return FALSE;
}
@@ -481,26 +528,13 @@ function file_unmanaged_copy($source, $destination = NULL, $replace = FILE_EXIST
$real_source = drupal_realpath($source);
$real_destination = drupal_realpath($destination);
if ($source == $destination || ($real_source !== FALSE) && ($real_source == $real_destination)) {
- drupal_set_message(t('The specified file %file was not copied because it would overwrite itself.', array('%file' => $source)), 'error');
- $logger->notice('File %file could not be copied because it would overwrite itself.', array('%file' => $source));
+ drupal_set_message(t('The specified file %file was not moved/copied because it would overwrite itself.', array('%file' => $source)), 'error');
+ $logger->notice('File %file could not be moved/copied because it would overwrite itself.', array('%file' => $source));
return FALSE;
}
// Make sure the .htaccess files are present.
file_ensure_htaccess();
- // Perform the copy operation.
- if (!@copy($source, $destination)) {
- // If the copy failed and realpaths exist, retry the operation using them
- // instead.
- if ($real_source === FALSE || $real_destination === FALSE || !@copy($real_source, $real_destination)) {
- $logger->error('The specified file %file could not be copied to %destination.', array('%file' => $source, '%destination' => $destination));
- return FALSE;
- }
- }
-
- // Set the permissions on the new file.
- drupal_chmod($destination);
-
- return $destination;
+ return TRUE;
}
/**
@@ -551,12 +585,24 @@ function file_destination($destination, $replace) {
/**
* Moves a file to a new location without database changes or hook invocation.
*
+ * This is a powerful function that in many ways performs like an advanced
+ * version of rename().
+ * - Checks if $source and $destination are valid and readable/writable.
+ * - Checks that $source is not equal to $destination; if they are an error
+ * is reported.
+ * - If file already exists in $destination either the call will error out,
+ * replace the file or rename the file based on the $replace parameter.
+ * - Works around a PHP bug where rename() does not properly support streams if
+ * safe_mode or open_basedir are enabled.
+ * @see https://bugs.php.net/bug.php?id=60456
+ *
* @param $source
- * A string specifying the filepath or URI of the original file.
+ * A string specifying the filepath or URI of the source file.
* @param $destination
- * A string containing the destination that $source should be moved to.
- * This must be a stream wrapper URI. If this value is omitted, Drupal's
- * default files scheme will be used, usually "public://".
+ * A URI containing the destination that $source should be moved to. The
+ * URI may be a bare filepath (without a scheme) and in that case the default
+ * scheme (file://) will be used. If this value is omitted, Drupal's default
+ * files scheme will be used, usually "public://".
* @param $replace
* Replace behavior when the destination file already exists:
* - FILE_EXISTS_REPLACE - Replace the existing file.
@@ -565,16 +611,37 @@ function file_destination($destination, $replace) {
* - FILE_EXISTS_ERROR - Do nothing and return FALSE.
*
* @return
- * The URI of the moved file, or FALSE in the event of an error.
+ * The path to the new file, or FALSE in the event of an error.
*
* @see file_move()
*/
function file_unmanaged_move($source, $destination = NULL, $replace = FILE_EXISTS_RENAME) {
- $filepath = file_unmanaged_copy($source, $destination, $replace);
- if ($filepath == FALSE || file_unmanaged_delete($source) == FALSE) {
+ if (!file_unmanaged_prepare($source, $destination, $replace)) {
return FALSE;
}
- return $filepath;
+ // Ensure compatibility with Windows.
+ // @see drupal_unlink()
+ if ((substr(PHP_OS, 0, 3) == 'WIN') && (!file_stream_wrapper_valid_scheme(file_uri_scheme($source)))) {
+ chmod($source, 0600);
+ }
+ // Attempt to resolve the URIs. This is necessary in certain configurations
+ // (see above) and can also permit fast moves across local schemes.
+ $real_source = drupal_realpath($source) ?: $source;
+ $real_destination = drupal_realpath($destination) ?: $destination;
+ // Perform the move operation.
+ if (!@rename($real_source, $real_destination)) {
+ // Fall back to slow copy and unlink procedure. This is necessary for
+ // renames across schemes that are not local, or where rename() has not been
+ // implemented. It's not necessary to use drupal_unlink() as the Windows
+ // issue has already been resolved above.
+ if (!@copy($real_source, $real_destination) || !@unlink($real_source)) {
+ \Drupal::logger('file')->error('The specified file %file could not be moved to %destination.', array('%file' => $source, '%destination' => $destination));
+ return FALSE;
+ }
+ }
+ // Set the permissions on the new file.
+ drupal_chmod($destination);
+ return $destination;
}
/**
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 8dd191cb1..1b3b255b8 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -1084,7 +1084,7 @@ function install_verify_completed_task() {
* The site path.
*
* @return bool
- * TRUE if there are no database errors.
+ * TRUE if there are no database errors.
*/
function install_verify_database_settings($site_path) {
if ($database = Database::getConnectionInfo()) {
diff --git a/core/includes/install.inc b/core/includes/install.inc
index 88ca1e2ad..f8008b3ed 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -115,8 +115,8 @@ function drupal_install_profile_distribution_name() {
* Loads the installation profile, extracting its defined version.
*
* @return string Distribution version defined in the profile's .info.yml file.
- * Defaults to \Drupal::VERSION if no version is explicitly provided
- * by the installation profile.
+ * Defaults to \Drupal::VERSION if no version is explicitly provided
+ * by the installation profile.
*
* @see install_profile_info()
*/
@@ -139,7 +139,7 @@ function drupal_install_profile_distribution_version() {
* Detects all supported databases that are compiled into PHP.
*
* @return
- * An array of database types compiled into PHP.
+ * An array of database types compiled into PHP.
*/
function drupal_detect_database_types() {
$databases = drupal_get_database_types();
@@ -724,7 +724,7 @@ function drupal_verify_install_file($file, $mask = NULL, $type = 'file') {
* (optional) Whether to output messages. Defaults to TRUE.
*
* @return
- * TRUE/FALSE whether or not the directory was successfully created.
+ * TRUE/FALSE whether or not the directory was successfully created.
*/
function drupal_install_mkdir($file, $mask, $message = TRUE) {
$mod = 0;
@@ -773,7 +773,7 @@ function drupal_install_mkdir($file, $mask, $message = TRUE) {
* (optional) Whether to output messages. Defaults to TRUE.
*
* @return
- * TRUE/FALSE whether or not we were able to fix the file's permissions.
+ * TRUE/FALSE whether or not we were able to fix the file's permissions.
*/
function drupal_install_fix_file($file, $mask, $message = TRUE) {
// If $file does not exist, fileperms() issues a PHP warning.
diff --git a/core/includes/update.inc b/core/includes/update.inc
index a07595e63..2c020db6b 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -65,7 +65,7 @@ function update_check_incompatibility($name, $type = 'module') {
* Returns whether the minimum schema requirement has been satisfied.
*
* @return array
- * A requirements info array.
+ * A requirements info array.
*/
function update_system_schema_requirements() {
$requirements = array();
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index 7b367069b..aa4fc0aee 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -81,7 +81,7 @@ class Drupal {
/**
* The current system version.
*/
- const VERSION = '8.1.3';
+ const VERSION = '8.1.5';
/**
* Core API compatibility.
diff --git a/core/lib/Drupal/Component/Annotation/Plugin.php b/core/lib/Drupal/Component/Annotation/Plugin.php
index 9ec9b9022..29f36759c 100644
--- a/core/lib/Drupal/Component/Annotation/Plugin.php
+++ b/core/lib/Drupal/Component/Annotation/Plugin.php
@@ -49,7 +49,7 @@ class Plugin implements AnnotationInterface {
* The annotation array.
*
* @return array
- * The parsed annotation as a definition.
+ * The parsed annotation as a definition.
*/
protected function parse(array $values) {
$definitions = array();
diff --git a/core/lib/Drupal/Component/Datetime/DateTimePlus.php b/core/lib/Drupal/Component/Datetime/DateTimePlus.php
index 377bb27b5..56bd92f23 100644
--- a/core/lib/Drupal/Component/Datetime/DateTimePlus.php
+++ b/core/lib/Drupal/Component/Datetime/DateTimePlus.php
@@ -455,7 +455,7 @@ class DateTimePlus {
/**
* Detects if there were errors in the processing of this date.
*
- * @return boolean
+ * @return bool
* TRUE if there were errors in the processing of this date, FALSE
* otherwise.
*/
diff --git a/core/lib/Drupal/Component/DependencyInjection/Container.php b/core/lib/Drupal/Component/DependencyInjection/Container.php
index 733490ca3..452a2c84d 100644
--- a/core/lib/Drupal/Component/DependencyInjection/Container.php
+++ b/core/lib/Drupal/Component/DependencyInjection/Container.php
@@ -373,7 +373,7 @@ class Container implements IntrospectableContainerInterface, ResettableContainer
* {@inheritdoc}
*/
public function has($id) {
- return isset($this->services[$id]) || isset($this->serviceDefinitions[$id]);
+ return isset($this->aliases[$id]) || isset($this->services[$id]) || isset($this->serviceDefinitions[$id]);
}
/**
diff --git a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
index d52fb89a0..317f2c49d 100644
--- a/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
+++ b/core/lib/Drupal/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumper.php
@@ -60,6 +60,7 @@ class OptimizedPhpArrayDumper extends Dumper {
*/
public function getArray() {
$definition = array();
+ $this->aliases = $this->getAliases();
$definition['aliases'] = $this->getAliases();
$definition['parameters'] = $this->getParameters();
$definition['services'] = $this->getServiceDefinitions();
@@ -454,6 +455,9 @@ class OptimizedPhpArrayDumper extends Dumper {
}
// Private shared service.
+ if (isset($this->aliases[$id])) {
+ $id = $this->aliases[$id];
+ }
$definition = $this->container->getDefinition($id);
if (!$definition->isPublic()) {
// The ContainerBuilder does not share a private service, but this means a
diff --git a/core/lib/Drupal/Component/DependencyInjection/composer.json b/core/lib/Drupal/Component/DependencyInjection/composer.json
index 28dc0312f..8dda45fbe 100644
--- a/core/lib/Drupal/Component/DependencyInjection/composer.json
+++ b/core/lib/Drupal/Component/DependencyInjection/composer.json
@@ -12,8 +12,8 @@
},
"require": {
"php": ">=5.5.9",
- "symfony/dependency-injection": "2.7.*",
- "symfony/expression-language": "2.7.*"
+ "symfony/dependency-injection": "~2.8",
+ "symfony/expression-language": "~2.7"
},
"autoload": {
"psr-4": {
diff --git a/core/lib/Drupal/Component/EventDispatcher/composer.json b/core/lib/Drupal/Component/EventDispatcher/composer.json
index 213284103..be6623283 100644
--- a/core/lib/Drupal/Component/EventDispatcher/composer.json
+++ b/core/lib/Drupal/Component/EventDispatcher/composer.json
@@ -6,8 +6,8 @@
"license": "GPL-2.0+",
"require": {
"php": ">=5.5.9",
- "symfony/dependency-injection": "2.7.*",
- "symfony/event-dispatcher": "2.7.*"
+ "symfony/dependency-injection": "~2.8",
+ "symfony/event-dispatcher": "~2.7"
},
"autoload": {
"psr-4": {
diff --git a/core/lib/Drupal/Component/HttpFoundation/composer.json b/core/lib/Drupal/Component/HttpFoundation/composer.json
index 57c929d18..f82c0679a 100644
--- a/core/lib/Drupal/Component/HttpFoundation/composer.json
+++ b/core/lib/Drupal/Component/HttpFoundation/composer.json
@@ -6,7 +6,7 @@
"license": "GPL-2.0+",
"require": {
"php": ">=5.5.9",
- "symfony/http-foundation": "2.7.*"
+ "symfony/http-foundation": "~2.7"
},
"autoload": {
"psr-4": {
diff --git a/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryTrait.php b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryTrait.php
index b065de959..7d076fa75 100644
--- a/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryTrait.php
+++ b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryTrait.php
@@ -30,8 +30,8 @@ trait DiscoveryTrait {
* @param string $plugin_id
* A plugin id.
* @param bool $exception_on_invalid
- * (optional) If TRUE, an invalid plugin ID will throw an exception.
- * Defaults to FALSE.
+ * If TRUE, an invalid plugin ID will cause an exception to be thrown; if
+ * FALSE, NULL will be returned.
*
* @return array|null
* A plugin definition, or NULL if the plugin ID is invalid and
diff --git a/core/lib/Drupal/Component/Plugin/composer.json b/core/lib/Drupal/Component/Plugin/composer.json
index 1acc8c5e5..4ffeeed3a 100644
--- a/core/lib/Drupal/Component/Plugin/composer.json
+++ b/core/lib/Drupal/Component/Plugin/composer.json
@@ -6,7 +6,7 @@
"license": "GPL-2.0+",
"require": {
"php": ">=5.5.9",
- "symfony/validator": "2.7.*"
+ "symfony/validator": "~2.7"
},
"autoload": {
"psr-4": {
diff --git a/core/lib/Drupal/Component/Render/OutputStrategyInterface.php b/core/lib/Drupal/Component/Render/OutputStrategyInterface.php
index 3406e8098..3ed9725b8 100644
--- a/core/lib/Drupal/Component/Render/OutputStrategyInterface.php
+++ b/core/lib/Drupal/Component/Render/OutputStrategyInterface.php
@@ -7,7 +7,7 @@ namespace Drupal\Component\Render;
*
* Output strategies assist in transforming HTML strings into strings that are
* appropriate for a given context (e.g. plain-text), through performing the
- * relevant formatting. No santization is applied.
+ * relevant formatting. No sanitization is applied.
*/
interface OutputStrategyInterface {
diff --git a/core/lib/Drupal/Component/Serialization/composer.json b/core/lib/Drupal/Component/Serialization/composer.json
index ff62221bc..5d629cfc1 100644
--- a/core/lib/Drupal/Component/Serialization/composer.json
+++ b/core/lib/Drupal/Component/Serialization/composer.json
@@ -6,7 +6,7 @@
"license": "GPL-2.0+",
"require": {
"php": ">=5.5.9",
- "symfony/yaml": "2.7.*"
+ "symfony/yaml": "~2.7"
},
"autoload": {
"psr-4": {
diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php
index 06b4ac2f2..2d0c22075 100644
--- a/core/lib/Drupal/Core/Cache/Cache.php
+++ b/core/lib/Drupal/Core/Cache/Cache.php
@@ -149,7 +149,7 @@ class Cache {
* Gets all cache bin services.
*
* @return array
- * An array of cache backend objects keyed by cache bins.
+ * An array of cache backend objects keyed by cache bins.
*/
public static function getBins() {
$bins = array();
diff --git a/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php b/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php
index 2bb89ba90..cbce6aac6 100644
--- a/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php
@@ -37,7 +37,7 @@ interface CacheableDependencyInterface {
* When this object is modified, these cache tags will be invalidated.
*
* @return string[]
- * A set of cache tags.
+ * A set of cache tags.
*/
public function getCacheTags();
diff --git a/core/lib/Drupal/Core/Cache/ChainedFastBackend.php b/core/lib/Drupal/Core/Cache/ChainedFastBackend.php
index 62839c2b0..5c0750d20 100644
--- a/core/lib/Drupal/Core/Cache/ChainedFastBackend.php
+++ b/core/lib/Drupal/Core/Cache/ChainedFastBackend.php
@@ -87,8 +87,16 @@ class ChainedFastBackend implements CacheBackendInterface, CacheTagsInvalidatorI
* The fast cache backend.
* @param string $bin
* The cache bin for which the object is created.
+ *
+ * @throws \Exception
+ * When the consistent cache backend and the fast cache backend are the same
+ * service.
*/
public function __construct(CacheBackendInterface $consistent_backend, CacheBackendInterface $fast_backend, $bin) {
+ if ($consistent_backend == $fast_backend) {
+ // @todo: should throw a proper exception. See https://www.drupal.org/node/2751847.
+ trigger_error('Consistent cache backend and fast cache backend cannot use the same service.', E_USER_ERROR);
+ }
$this->consistentBackend = $consistent_backend;
$this->fastBackend = $fast_backend;
$this->bin = 'cache_' . $bin;
diff --git a/core/lib/Drupal/Core/Cache/Context/QueryArgsCacheContext.php b/core/lib/Drupal/Core/Cache/Context/QueryArgsCacheContext.php
index 0926ba60b..a48664468 100644
--- a/core/lib/Drupal/Core/Cache/Context/QueryArgsCacheContext.php
+++ b/core/lib/Drupal/Core/Cache/Context/QueryArgsCacheContext.php
@@ -25,20 +25,22 @@ class QueryArgsCacheContext extends RequestStackCacheContextBase implements Calc
*/
public function getContext($query_arg = NULL) {
if ($query_arg === NULL) {
- return $this->requestStack->getCurrentRequest()->getQueryString();
+ // All arguments requested. Use normalized query string to minimize
+ // variations.
+ $value = $this->requestStack->getCurrentRequest()->getQueryString();
+ return ($value !== NULL) ? $value : '';
}
elseif ($this->requestStack->getCurrentRequest()->query->has($query_arg)) {
$value = $this->requestStack->getCurrentRequest()->query->get($query_arg);
- if ($value !== '') {
+ if (is_array($value)) {
+ return http_build_query($value);
+ }
+ elseif ($value !== '') {
return $value;
}
- else {
- return '?valueless?';
- }
- }
- else {
- return NULL;
+ return '?valueless?';
}
+ return '';
}
/**
diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php
index 1ab9fdb55..11d7c8439 100644
--- a/core/lib/Drupal/Core/Config/ConfigManager.php
+++ b/core/lib/Drupal/Core/Config/ConfigManager.php
@@ -307,8 +307,9 @@ class ConfigManager implements ConfigManagerInterface {
// Try to fix any dependencies and find out what will happen to the
// dependency graph. Entities are processed in the order of most dependent
- // first. For example, this ensures that fields are removed before
- // field storages.
+ // first. For example, this ensures that Menu UI third party dependencies on
+ // node types are fixed before processing the node type's other
+ // dependencies.
while ($dependent = array_pop($dependents)) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */
if ($dry_run) {
@@ -346,7 +347,9 @@ class ConfigManager implements ConfigManagerInterface {
// If the entity cannot be fixed then it has to be deleted.
if (!$fixed) {
$delete_uuids[] = $dependent->uuid();
- $return['delete'][] = $dependent;
+ // Deletes should occur in the order of the least dependent first. For
+ // example, this ensures that fields are removed before field storages.
+ array_unshift($return['delete'], $dependent);
}
}
// Use the lists of UUIDs to filter the original list to work out which
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php b/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php
index 377f7c4d0..c274f28a1 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php
@@ -173,7 +173,9 @@ class ConfigDependencyManager {
// dependent is at the top. For example, this ensures that fields are
// always after field storages. This is because field storages need to be
// created before a field.
- return array_reverse(array_intersect_key($this->graph, $dependencies));
+ $graph = $this->getGraph();
+ uasort($graph, array($this, 'sortGraph'));
+ return array_replace(array_intersect_key($graph, $dependencies), $dependencies);
}
/**
@@ -185,10 +187,32 @@ class ConfigDependencyManager {
*/
public function sortAll() {
$graph = $this->getGraph();
- // Sort by reverse weight and alphabetically. The most dependent entities
+ // Sort by weight and alphabetically. The most dependent entities
// are last and entities with the same weight are alphabetically ordered.
- uasort($graph, array($this, 'sortGraph'));
- return array_keys($graph);
+ uasort($graph, array($this, 'sortGraphByWeight'));
+ // Use array_intersect_key() to exclude modules and themes from the list.
+ return array_keys(array_intersect_key($graph, $this->data));
+ }
+
+ /**
+ * Sorts the dependency graph by weight and alphabetically.
+ *
+ * @param array $a
+ * First item for comparison. The compared items should be associative
+ * arrays that include a 'weight' and a 'name' key.
+ * @param array $b
+ * Second item for comparison.
+ *
+ * @return int
+ * The comparison result for uasort().
+ */
+ protected function sortGraphByWeight(array $a, array $b) {
+ $weight_cmp = SortArray::sortByKeyInt($a, $b, 'weight');
+
+ if ($weight_cmp === 0) {
+ return SortArray::sortByKeyString($a, $b, 'name');
+ }
+ return $weight_cmp;
}
/**
@@ -196,7 +220,7 @@ class ConfigDependencyManager {
*
* @param array $a
* First item for comparison. The compared items should be associative
- * arrays that include a 'weight' and a 'component' key.
+ * arrays that include a 'weight' and a 'name' key.
* @param array $b
* Second item for comparison.
*
@@ -207,7 +231,7 @@ class ConfigDependencyManager {
$weight_cmp = SortArray::sortByKeyInt($a, $b, 'weight') * -1;
if ($weight_cmp === 0) {
- return SortArray::sortByKeyString($a, $b, 'component');
+ return SortArray::sortByKeyString($a, $b, 'name');
}
return $weight_cmp;
}
@@ -228,9 +252,11 @@ class ConfigDependencyManager {
$graph = $this->getGraph();
foreach ($entities_to_check as $entity) {
- if (isset($graph[$entity]) && !empty($graph[$entity]['reverse_paths'])) {
- foreach ($graph[$entity]['reverse_paths'] as $dependency => $value) {
- $dependent_entities[$dependency] = $this->data[$dependency];
+ if (isset($graph[$entity]) && !empty($graph[$entity]['paths'])) {
+ foreach ($graph[$entity]['paths'] as $dependency => $value) {
+ if (isset($this->data[$dependency])) {
+ $dependent_entities[$dependency] = $this->data[$dependency];
+ }
}
}
}
@@ -248,14 +274,21 @@ class ConfigDependencyManager {
$graph = array();
foreach ($this->data as $entity) {
$graph_key = $entity->getConfigDependencyName();
- $graph[$graph_key]['edges'] = array();
- $dependencies = $entity->getDependencies('config');
- if (!empty($dependencies)) {
- foreach ($dependencies as $dependency) {
- $graph[$graph_key]['edges'][$dependency] = TRUE;
- }
+ if (!isset($graph[$graph_key])) {
+ $graph[$graph_key] = [
+ 'edges' => [],
+ 'name' => $graph_key,
+ ];
+ }
+ // Include all dependencies in the graph so that topographical sorting
+ // works.
+ foreach (array_merge($entity->getDependencies('config'), $entity->getDependencies('module'), $entity->getDependencies('theme')) as $dependency) {
+ $graph[$dependency]['edges'][$graph_key] = TRUE;
+ $graph[$dependency]['name'] = $dependency;
}
}
+ // Ensure that order of the graph is consistent.
+ krsort($graph);
$graph_object = new Graph($graph);
$this->graph = $graph_object->searchAndSort();
}
diff --git a/core/lib/Drupal/Core/Config/StorageInterface.php b/core/lib/Drupal/Core/Config/StorageInterface.php
index 49bc91676..18a712633 100644
--- a/core/lib/Drupal/Core/Config/StorageInterface.php
+++ b/core/lib/Drupal/Core/Config/StorageInterface.php
@@ -93,28 +93,28 @@ interface StorageInterface {
/**
* Encodes configuration data into the storage-specific format.
*
+ * This is a publicly accessible static method to allow for alternative
+ * usages in data conversion scripts and also tests.
+ *
* @param array $data
* The configuration data to encode.
*
* @return string
* The encoded configuration data.
- *
- * This is a publicly accessible static method to allow for alternative
- * usages in data conversion scripts and also tests.
*/
public function encode($data);
/**
* Decodes configuration data from the storage-specific format.
*
+ * This is a publicly accessible static method to allow for alternative
+ * usages in data conversion scripts and also tests.
+ *
* @param string $raw
* The raw configuration data string to decode.
*
* @return array
* The decoded configuration data as an associative array.
- *
- * This is a publicly accessible static method to allow for alternative
- * usages in data conversion scripts and also tests.
*/
public function decode($raw);
diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 2002e9ebc..791bf7a01 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -753,14 +753,11 @@ abstract class Connection {
*/
public function getDriverClass($class) {
if (empty($this->driverClasses[$class])) {
- $driver = $this->driver();
- if (!empty($this->connectionOptions['namespace'])) {
- $driver_class = $this->connectionOptions['namespace'] . '\\' . $class;
- }
- else {
- // Fallback for Drupal 7 settings.php.
- $driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\{$class}";
+ if (empty($this->connectionOptions['namespace'])) {
+ // Fallback for Drupal 7 settings.php and the test runner script.
+ $this->connectionOptions['namespace'] = (new \ReflectionObject($this))->getNamespaceName();
}
+ $driver_class = $this->connectionOptions['namespace'] . '\\' . $class;
$this->driverClasses[$class] = class_exists($driver_class) ? $driver_class : $class;
}
return $this->driverClasses[$class];
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
index a91b24056..95ff52716 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
@@ -246,6 +246,9 @@ class Tasks extends InstallTasks {
// concurrency issues, when both try to update at the same time.
try {
$connection = Database::getConnection();
+ // When testing, two installs might try to run the CREATE FUNCTION queries
+ // at the same time. Do not let that happen.
+ $connection->query('SELECT pg_advisory_lock(1)');
// Don't use {} around pg_proc table.
if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
$connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
@@ -264,6 +267,7 @@ class Tasks extends InstallTasks {
[ 'allow_delimiter_in_query' => TRUE ]
);
}
+ $connection->query('SELECT pg_advisory_unlock(1)');
$this->pass(t('PostgreSQL has initialized itself.'));
}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
index 3a5b97091..f56b26ce4 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
@@ -28,7 +28,7 @@ class Tasks extends InstallTasks {
* {@inheritdoc}
*/
public function minimumVersion() {
- return '3.6.8';
+ return '3.7.11';
}
/**
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
index f97a6ec67..a3c001cc8 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
@@ -420,8 +420,12 @@ class Schema extends DatabaseSchema {
*
* @param $table
* Name of the table.
+ *
* @return
* An array representing the schema, from drupal_get_schema().
+ *
+ * @throws \Exception
+ * If a column of the table could not be parsed.
*/
protected function introspectSchema($table) {
$mapped_fields = array_flip($this->getFieldTypeMap());
@@ -459,7 +463,7 @@ class Schema extends DatabaseSchema {
}
}
else {
- new \Exception("Unable to parse the column type " . $row->type);
+ throw new \Exception("Unable to parse the column type " . $row->type);
}
}
$indexes = array();
@@ -710,7 +714,7 @@ class Schema extends DatabaseSchema {
// Can't use query placeholders for the schema because the query would
// have to be :prefixsqlite_master, which does not work. We also need to
// ignore the internal SQLite tables.
- $result = db_query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", array(
+ $result = $this->connection->query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", array(
':type' => 'table',
':table_name' => $table_expression,
':pattern' => 'sqlite_%',
diff --git a/core/lib/Drupal/Core/Database/Query/ExtendableInterface.php b/core/lib/Drupal/Core/Database/Query/ExtendableInterface.php
index b0d6f762d..d512bd611 100644
--- a/core/lib/Drupal/Core/Database/Query/ExtendableInterface.php
+++ b/core/lib/Drupal/Core/Database/Query/ExtendableInterface.php
@@ -6,11 +6,11 @@ namespace Drupal\Core\Database\Query;
* Interface for extendable query objects.
*
* "Extenders" follow the "Decorator" OOP design pattern. That is, they wrap
- * and "decorate" another object. In our case, they implement the same interface
- * as select queries and wrap a select query, to which they delegate almost all
- * operations. Subclasses of this class may implement additional methods or
- * override existing methods as appropriate. Extenders may also wrap other
- * extender objects, allowing for arbitrarily complex "enhanced" queries.
+ * and "decorate" another object. In our case, they implement the same
+ * interface as select queries and wrap a select query, to which they delegate
+ * almost all operations. Subclasses of this class may implement additional
+ * methods or override existing methods as appropriate. Extenders may also wrap
+ * other extender objects, allowing for arbitrarily complex "enhanced" queries.
*/
interface ExtendableInterface {
@@ -18,9 +18,12 @@ interface ExtendableInterface {
* Enhance this object by wrapping it in an extender object.
*
* @param $extender_name
- * The base name of the extending class. The base name will be checked
- * against the current database connection to allow driver-specific subclasses
- * as well, using the same logic as the query objects themselves.
+ * The fully-qualified name of the extender class, without the leading '\'
+ * (for example, Drupal\my_module\myExtenderClass). The extender name will
+ * be checked against the current database connection to allow
+ * driver-specific subclasses as well, using the same logic as the query
+ * objects themselves.
+ *
* @return \Drupal\Core\Database\Query\ExtendableInterface
* The extender object, which now contains a reference to this object.
*/
diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php
index a7e340bfc..b269410b8 100644
--- a/core/lib/Drupal/Core/Database/Query/Select.php
+++ b/core/lib/Drupal/Core/Database/Query/Select.php
@@ -39,7 +39,7 @@ class Select extends Query implements SelectInterface {
* 'type' => $join_type (one of INNER, LEFT OUTER, RIGHT OUTER),
* 'table' => $table,
* 'alias' => $alias_of_the_table,
- * 'condition' => $condition_clause_on_which_to_join,
+ * 'condition' => $join_condition (string or Condition object),
* 'arguments' => $array_of_arguments_for_placeholders_in_the condition.
* 'all_fields' => TRUE to SELECT $alias.*, FALSE or NULL otherwise.
* )
@@ -47,6 +47,10 @@ class Select extends Query implements SelectInterface {
* If $table is a string, it is taken as the name of a table. If it is
* a Select query object, it is taken as a subquery.
*
+ * If $join_condition is a Condition object, any arguments should be
+ * incorporated into the object; a separate array of arguments does not
+ * need to be provided.
+ *
* @var array
*/
protected $tables = array();
@@ -196,6 +200,10 @@ class Select extends Query implements SelectInterface {
if ($table['table'] instanceof SelectInterface) {
$args += $table['table']->arguments();
}
+ // If the join condition is an object, grab its arguments recursively.
+ if (!empty($table['condition']) && $table['condition'] instanceof ConditionInterface) {
+ $args += $table['condition']->arguments();
+ }
}
foreach ($this->expressions as $expression) {
@@ -225,6 +233,10 @@ class Select extends Query implements SelectInterface {
if ($table['table'] instanceof SelectInterface) {
$table['table']->compile($connection, $queryPlaceholder);
}
+ // Make sure join conditions are also compiled.
+ if (!empty($table['condition']) && $table['condition'] instanceof ConditionInterface) {
+ $table['condition']->compile($connection, $queryPlaceholder);
+ }
}
// If there are any dependent queries to UNION, compile it recursively.
@@ -248,6 +260,11 @@ class Select extends Query implements SelectInterface {
return FALSE;
}
}
+ if (!empty($table['condition']) && $table['condition'] instanceof ConditionInterface) {
+ if (!$table['condition']->compiled()) {
+ return FALSE;
+ }
+ }
}
foreach ($this->union as $union) {
@@ -822,7 +839,7 @@ class Select extends Query implements SelectInterface {
$query .= $table_string . ' ' . $this->connection->escapeTable($table['alias']);
if (!empty($table['condition'])) {
- $query .= ' ON ' . $table['condition'];
+ $query .= ' ON ' . (string) $table['condition'];
}
}
diff --git a/core/lib/Drupal/Core/Datetime/Element/Datelist.php b/core/lib/Drupal/Core/Datetime/Element/Datelist.php
index 76f61bb04..f39d0a290 100644
--- a/core/lib/Drupal/Core/Datetime/Element/Datelist.php
+++ b/core/lib/Drupal/Core/Datetime/Element/Datelist.php
@@ -268,6 +268,7 @@ class Datelist extends DateElementBase {
'#options' => $options,
'#required' => $element['#required'],
'#error_no_message' => FALSE,
+ '#empty_option' => $title,
);
}
diff --git a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
index c0a874749..aa49cf737 100644
--- a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
+++ b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php
@@ -58,8 +58,6 @@ class YamlFileLoader
public function load($file)
{
// Load from the file cache, fall back to loading the file.
- // @todo Refactor this to cache parsed definition objects in
- // https://www.drupal.org/node/2464053
$content = $this->fileCache->get($file);
if (!$content) {
$content = $this->loadFile($file);
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 6e0b4d847..e89ce3164 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -306,7 +306,7 @@ abstract class Entity implements EntityInterface {
// The entity ID is needed as a route parameter.
$uri_route_parameters[$this->getEntityTypeId()] = $this->id();
}
- if ($rel === 'revision') {
+ if ($rel === 'revision' && $this instanceof RevisionableInterface) {
$uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
}
diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php
index ae436bbaa..3a38e072d 100644
--- a/core/lib/Drupal/Core/Entity/EntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityInterface.php
@@ -339,10 +339,17 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
public static function preCreate(EntityStorageInterface $storage, array &$values);
/**
- * Acts on an entity after it is created but before hooks are invoked.
+ * Acts on a created entity before hooks are invoked.
+ *
+ * Used after the entity is created, but before saving the entity and before
+ * any of the presave hooks are invoked.
+ *
+ * See the @link entity_crud Entity CRUD topic @endlink for more information.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage object.
+ *
+ * @see \Drupal\Core\Entity\EntityInterface::create()
*/
public function postCreate(EntityStorageInterface $storage);
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php
index 4425d2872..882f98b3d 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php
@@ -11,7 +11,10 @@ interface EntityTypeBundleInfoInterface {
* Get the bundle info of all entity types.
*
* @return array
- * An array of all bundle information.
+ * An array of bundle information where the outer array is keyed by entity
+ * type. The next level is keyed by the bundle name. The inner arrays are
+ * associative arrays of bundle information, such as the label for the
+ * bundle.
*/
public function getAllBundleInfo();
@@ -22,7 +25,10 @@ interface EntityTypeBundleInfoInterface {
* The entity type.
*
* @return array
- * Returns the bundle information for the specified entity type.
+ * An array of bundle information where the outer array is keyed by the
+ * bundle name, or the entity type name if the entity does not have bundles.
+ * The inner arrays are associative arrays of bundle information, such as
+ * the label for the bundle.
*/
public function getBundleInfo($entity_type);
diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
index e673db1cf..17b266ffc 100644
--- a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
+++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
@@ -28,24 +28,47 @@ interface QueryInterface extends AlterableInterface {
* and the Polish 'siema' within a 'greetings' text field:
* @code
* $entity_ids = \Drupal::entityQuery($entity_type)
- * ->condition('greetings', 'merhaba', '=', 'tr');
- * ->condition('greetings.value', 'siema', '=', 'pl');
+ * ->condition('greetings', 'merhaba', '=', 'tr')
+ * ->condition('greetings.value', 'siema', '=', 'pl')
* ->execute();
* $entity_ids = $query->execute();
* @endcode
*
* @param $field
- * Name of the field being queried. It must contain a field name,
- * optionally followed by a column name. The column can be "entity" for
- * reference fields and that can be followed similarly by a field name
- * and so on. Some examples:
+ * Name of the field being queried. It must contain a field name, optionally
+ * followed by a column name. The column can be "entity" for reference
+ * fields and that can be followed similarly by a field name and so on. Some
+ * examples:
* - nid
* - tags.value
* - tags
* - uid.entity.name
* "tags" "is the same as "tags.value" as value is the default column.
* If two or more conditions have the same field names they apply to the
- * same delta within that field.
+ * same delta within that field. In order to limit the condition to a
+ * specific item a numeric delta should be added between the field name and
+ * the column name.
+ * @code
+ * ->condition('tags.5.value', 'news')
+ * @endcode
+ * This will require condition to be satisfied on a specific delta of the
+ * field. The condition above will require the 6th value of the field to
+ * match the provided value. Further, it's possible to create a condition on
+ * the delta itself by using '%delta'. For example,
+ * @code
+ * ->condition('tags.%delta', 5)
+ * @endcode
+ * will find only entities which have at least six tags. Finally, the
+ * condition on the delta itself accompanied with a condition on the value
+ * will require the value to appear in the specific delta range. For
+ * example,
+ * @code
+ * ->condition('tags.%delta', 0, '>'))
+ * ->condition('tags.%delta.value', 'news'))
+ * @endcode
+ * will only find the "news" tag if it is not the first value. It should be
+ * noted that conditions on specific deltas and delta ranges are only
+ * supported when querying content entities.
* @param $value
* The value for $field. In most cases, this is a scalar and it's treated as
* case-insensitive. For more complex operators, it is an array. The meaning
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
index ca25fb97a..c8bf59d15 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -5,6 +5,7 @@ namespace Drupal\Core\Entity\Query\Sql;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Query\QueryException;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
+use Drupal\Core\Entity\Sql\TableMappingInterface;
/**
* Adds tables and fields to the SQL entity query.
@@ -112,11 +113,37 @@ class Tables implements TablesInterface {
// Check whether this field is stored in a dedicated table.
if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) {
+ $delta = NULL;
// Find the field column.
$column = $field_storage->getMainPropertyName();
if ($key < $count) {
$next = $specifiers[$key + 1];
+ // If this is a numeric specifier we're adding a condition on the
+ // specific delta.
+ if (is_numeric($next)) {
+ $delta = $next;
+ $index_prefix .= ".$delta";
+ // Do not process it again.
+ $key++;
+ $next = $specifiers[$key + 1];
+ }
+ // If this specifier is the reserved keyword "%delta" we're adding a
+ // condition on a delta range.
+ elseif ($next == TableMappingInterface::DELTA) {
+ $index_prefix .= TableMappingInterface::DELTA;
+ // Do not process it again.
+ $key++;
+ // If there are more specifiers to work with then continue
+ // processing. If this is the last specifier then use the reserved
+ // keyword as a column name.
+ if ($key < $count) {
+ $next = $specifiers[$key + 1];
+ }
+ else {
+ $column = TableMappingInterface::DELTA;
+ }
+ }
// Is this a field column?
$columns = $field_storage->getColumns();
if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
@@ -140,7 +167,7 @@ class Tables implements TablesInterface {
$next_index_prefix = "$relationship_specifier.$column";
}
}
- $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field);
+ $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta);
$sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
$property_definitions = $field_storage->getPropertyDefinitions();
if (isset($property_definitions[$column])) {
@@ -173,6 +200,27 @@ class Tables implements TablesInterface {
// next one is a column of this field.
if ($key < $count) {
$next = $specifiers[$key + 1];
+ // If this specifier is the reserved keyword "%delta" we're adding a
+ // condition on a delta range.
+ if ($next == TableMappingInterface::DELTA) {
+ $key++;
+ if ($key < $count) {
+ $next = $specifiers[$key + 1];
+ }
+ else {
+ return 0;
+ }
+ }
+ // If this is a numeric specifier we're adding a condition on the
+ // specific delta. Since we know that this is a single value base
+ // field no other value than 0 makes sense.
+ if (is_numeric($next)) {
+ if ($next > 0) {
+ $this->sqlQuery->condition('1 <> 1');
+ }
+ $key++;
+ $next = $specifiers[$key + 1];
+ }
// Is this a field column?
$columns = $field_storage->getColumns();
if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
@@ -264,7 +312,7 @@ class Tables implements TablesInterface {
* @return string
* @throws \Drupal\Core\Entity\Query\QueryException
*/
- protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) {
+ protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta) {
$field_name = $field->getName();
if (!isset($this->fieldTables[$index_prefix . $field_name])) {
$entity_type_id = $this->sqlQuery->getMetaData('entity_type');
@@ -274,12 +322,12 @@ class Tables implements TablesInterface {
if ($field->getCardinality() != 1) {
$this->sqlQuery->addMetaData('simple_query', FALSE);
}
- $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode);
+ $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode, $delta);
}
return $this->fieldTables[$index_prefix . $field_name];
}
- protected function addJoin($type, $table, $join_condition, $langcode) {
+ protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) {
$arguments = array();
if ($langcode) {
$entity_type_id = $this->sqlQuery->getMetaData('entity_type');
@@ -291,6 +339,11 @@ class Tables implements TablesInterface {
$join_condition .= ' AND %alias.' . $langcode_key . ' = ' . $placeholder;
$arguments[$placeholder] = $langcode;
}
+ if (isset($delta)) {
+ $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder();
+ $join_condition .= ' AND %alias.delta = ' . $placeholder;
+ $arguments[$placeholder] = $delta;
+ }
return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments);
}
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php b/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php
index 214e130fd..bd8e0de36 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/TablesInterface.php
@@ -11,8 +11,10 @@ interface TablesInterface {
* Adds a field to a database query.
*
* @param string $field
- * If it contains a dot, then field name dot field column. If it doesn't
- * then entity property name.
+ * If it doesn't contain a dot, then an entity base field name. If it
+ * contains a dot, then either field name dot field column or field name dot
+ * delta dot field column. Delta can be a numeric value or a "%delta" for
+ * any value.
* @param string $type
* Join type, can either be INNER or LEFT.
* @param string $langcode
diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
index e425fde35..5f20c3f08 100644
--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
+++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
@@ -204,7 +204,12 @@ class DefaultTableMapping implements TableMappingInterface {
$column_name = count($storage_definition->getColumns()) == 1 ? $field_name : $field_name . '__' . $property_name;
}
elseif ($this->requiresDedicatedTableStorage($storage_definition)) {
- $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name;
+ if ($property_name == TableMappingInterface::DELTA) {
+ $column_name = 'delta';
+ }
+ else {
+ $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name;
+ }
}
else {
throw new SqlContentEntityStorageException("Column information not available for the '$field_name' field.");
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index d58445477..391e3444a 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -150,7 +150,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* The database connection to be used.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
- * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+ * @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend to be used.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index d76c1329e..ec3ebaf00 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -1837,6 +1837,24 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
+ // Add unique keys.
+ foreach ($schema['unique keys'] as $index_name => $columns) {
+ $real_name = $this->getFieldIndexName($storage_definition, $index_name);
+ foreach ($columns as $column_name) {
+ // Unique keys can be specified as either a column name or an array with
+ // column name and length. Allow for either case.
+ if (is_array($column_name)) {
+ $data_schema['unique keys'][$real_name][] = array(
+ $table_mapping->getFieldColumnName($storage_definition, $column_name[0]),
+ $column_name[1],
+ );
+ }
+ else {
+ $data_schema['unique keys'][$real_name][] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
+ }
+ }
+ }
+
// Add foreign keys.
foreach ($schema['foreign keys'] as $specifier => $specification) {
$real_name = $this->getFieldIndexName($storage_definition, $specifier);
diff --git a/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php
index f21c2700d..3c553bf00 100644
--- a/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php
+++ b/core/lib/Drupal/Core/Entity/Sql/TableMappingInterface.php
@@ -19,6 +19,11 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface;
*/
interface TableMappingInterface {
+ /**
+ * A property that represents delta used in entity query conditions.
+ */
+ const DELTA = '%delta';
+
/**
* Gets a list of table names for this mapping.
*
diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
index 873996b02..57d43ec0b 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
@@ -3,7 +3,6 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Render\BareHtmlPageRendererInterface;
use Drupal\Core\Routing\RouteMatch;
@@ -13,6 +12,7 @@ use Drupal\Core\Site\MaintenanceModeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
@@ -90,18 +90,25 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
* The event to process.
*/
public function onKernelRequestMaintenance(GetResponseEvent $event) {
- $route_match = RouteMatch::createFromRequest($event->getRequest());
+ $request = $event->getRequest();
+ $route_match = RouteMatch::createFromRequest($request);
if ($this->maintenanceMode->applies($route_match)) {
// Don't cache maintenance mode pages.
\Drupal::service('page_cache_kill_switch')->trigger();
+
if (!$this->maintenanceMode->exempt($this->account)) {
// Deliver the 503 page if the site is in maintenance mode and the
// logged in user is not allowed to bypass it.
+
+ // If the request format is not 'html' then show default maintenance
+ // mode page else show a text/plain page with maintenance message.
+ if ($request->getRequestFormat() !== 'html') {
+ $response = new Response($this->getSiteMaintenanceMessage(), 503, array('Content-Type' => 'text/plain'));
+ $event->setResponse($response);
+ return;
+ }
drupal_maintenance_theme();
- $content = Xss::filterAdmin(SafeMarkup::format($this->config->get('system.maintenance')->get('message'), array(
- '@site' => $this->config->get('system.site')->get('name'),
- )));
- $response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $content], $this->t('Site under maintenance'), 'maintenance_page');
+ $response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $this->getSiteMaintenanceMessage()], $this->t('Site under maintenance'), 'maintenance_page');
$response->setStatusCode(503);
$event->setResponse($response);
}
@@ -121,6 +128,18 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
}
}
+ /**
+ * Gets the site maintenance message.
+ *
+ * @return \Drupal\Component\Render\MarkupInterface
+ * The formatted site maintenance message.
+ */
+ protected function getSiteMaintenanceMessage() {
+ return SafeMarkup::format($this->config->get('system.maintenance')->get('message'), array(
+ '@site' => $this->config->get('system.site')->get('name'),
+ ));
+ }
+
/**
* Wraps the drupal_set_message function.
*/
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
index 124efa2ed..e0336dd0a 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
@@ -214,7 +214,7 @@ interface ModuleHandlerInterface {
* The name of the module (without the .module extension).
* @param string $hook
* The name of the hook to invoke.
- * @param ...
+ * @param array $args
* Arguments to pass to the hook implementation.
*
* @return mixed
diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php
index 37f8b7759..b73238763 100644
--- a/core/lib/Drupal/Core/Extension/module.api.php
+++ b/core/lib/Drupal/Core/Extension/module.api.php
@@ -72,6 +72,8 @@ use Drupal\Core\Utility\UpdateException;
* frequently called should be left in the main module file so that they are
* always available.
*
+ * See system_hook_info() for all hook groups defined by Drupal core.
+ *
* @return
* An associative array whose keys are hook names and whose values are an
* associative array containing:
@@ -79,8 +81,6 @@ use Drupal\Core\Utility\UpdateException;
* system will determine whether a file with the name $module.$group.inc
* exists, and automatically load it when required.
*
- * See system_hook_info() for all hook groups defined by Drupal core.
- *
* @see hook_hook_info_alter()
*/
function hook_hook_info() {
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php
index 1df00cb4f..f86eac7e5 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php
@@ -28,6 +28,8 @@ class PasswordItem extends StringItem {
->setSetting('case_sensitive', TRUE);
$properties['existing'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Existing password'));
+ $properties['pre_hashed'] = DataDefinition::create('boolean')
+ ->setLabel(new TranslatableMarkup('Determines if a password needs hashing'));
return $properties;
}
@@ -40,8 +42,11 @@ class PasswordItem extends StringItem {
$entity = $this->getEntity();
- // Update the user password if it has changed.
- if ($entity->isNew() || (strlen(trim($this->value)) > 0 && $this->value != $entity->original->{$this->getFieldDefinition()->getName()}->value)) {
+ if ($this->pre_hashed) {
+ // Reset the pre_hashed value since it has now been used.
+ $this->pre_hashed = FALSE;
+ }
+ elseif ($entity->isNew() || (strlen(trim($this->value)) > 0 && $this->value != $entity->original->{$this->getFieldDefinition()->getName()}->value)) {
// Allow alternate password hashing schemes.
$this->value = \Drupal::service('password')->hash(trim($this->value));
// Abort if the hashing failed and returned FALSE.
diff --git a/core/lib/Drupal/Core/Form/FormBase.php b/core/lib/Drupal/Core/Form/FormBase.php
index c33f2a167..4a020dc2a 100644
--- a/core/lib/Drupal/Core/Form/FormBase.php
+++ b/core/lib/Drupal/Core/Form/FormBase.php
@@ -15,7 +15,29 @@ use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides a base class for forms.
*
+ * This class exists as a mid-point between dependency injection through
+ * ContainerInjectionInterface, and a less-structured use of traits which
+ * default to using the \Drupal accessor for service discovery.
+ *
+ * To properly inject services, override create() and use the setters provided
+ * by the traits to inject the needed services.
+ *
+ * @code
+ * public static function create($container) {
+ * $form = new static();
+ * // In this example we only need string translation so we use the
+ * // setStringTranslation() method provided by StringTranslationTrait.
+ * $form->setStringTranslation($container->get('string_translation'));
+ * return $form;
+ * }
+ * @endcode
+ *
+ * Alternately, do not use FormBase. A class can implement FormInterface, use
+ * the traits it needs, and inject services from the container as required.
+ *
* @ingroup form_api
+ *
+ * @see \Drupal\Core\DependencyInjection\ContainerInjectionInterface
*/
abstract class FormBase implements FormInterface, ContainerInjectionInterface {
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index 334e10be4..ca794379d 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -137,9 +137,9 @@ interface FormBuilderInterface {
* by calling $form_state->getErrors().
*
* @param \Drupal\Core\Form\FormInterface|string $form_arg
- * A form object to use to build the form, or the unique string identifying
- * the desired form. If $form_arg is a string and a function with that
- * name exists, it is called to build the form array.
+ * The value must be one of the following:
+ * - The name of a class that implements \Drupal\Core\Form\FormInterface.
+ * - An instance of a class that implements \Drupal\Core\Form\FormInterface.
* @param $form_state
* The current state of the form. Most important is the
* $form_state->getValues() collection, a tree of data used to simulate the
diff --git a/core/lib/Drupal/Core/Plugin/ContextAwarePluginBase.php b/core/lib/Drupal/Core/Plugin/ContextAwarePluginBase.php
index ab7359826..05ba8a83d 100644
--- a/core/lib/Drupal/Core/Plugin/ContextAwarePluginBase.php
+++ b/core/lib/Drupal/Core/Plugin/ContextAwarePluginBase.php
@@ -41,11 +41,11 @@ abstract class ContextAwarePluginBase extends ComponentContextAwarePluginBase im
/**
* {@inheritdoc}
*
- * @return \Drupal\Core\Plugin\Context\ContextInterface
- * The context object.
- *
* This code is identical to the Component in order to pick up a different
* Context class.
+ *
+ * @return \Drupal\Core\Plugin\Context\ContextInterface
+ * The context object.
*/
public function getContext($name) {
// Check for a valid context value.
diff --git a/core/lib/Drupal/Core/Render/Element/Checkboxes.php b/core/lib/Drupal/Core/Render/Element/Checkboxes.php
index 8487a0960..1ca1aea5d 100644
--- a/core/lib/Drupal/Core/Render/Element/Checkboxes.php
+++ b/core/lib/Drupal/Core/Render/Element/Checkboxes.php
@@ -17,7 +17,7 @@ use Drupal\Core\Form\FormStateInterface;
* @code
* $form['high_school']['tests_taken'] = array(
* '#type' => 'checkboxes',
- * '#options' => array('SAT' => $this->t('SAT'), 'ACT' => $this->t('ACT'))),
+ * '#options' => array('SAT' => $this->t('SAT'), 'ACT' => $this->t('ACT')),
* '#title' => $this->t('What standardized tests did you take?'),
* ...
* );
diff --git a/core/lib/Drupal/Core/Render/Element/StatusMessages.php b/core/lib/Drupal/Core/Render/Element/StatusMessages.php
index 55990e6b9..f026aa5c5 100644
--- a/core/lib/Drupal/Core/Render/Element/StatusMessages.php
+++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php
@@ -12,7 +12,7 @@ namespace Drupal\Core\Render\Element;
* $build['status_messages'] = [
* '#type' => 'status_messages',
* ];
- * @end
+ * @endcode
*
* @RenderElement("status_messages")
*/
diff --git a/core/lib/Drupal/Core/Render/RenderCacheInterface.php b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
index f31b33037..86f38d358 100644
--- a/core/lib/Drupal/Core/Render/RenderCacheInterface.php
+++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php
@@ -64,7 +64,7 @@ interface RenderCacheInterface {
* this array.
*
* @return bool|null
- * Returns FALSE if no cache item could be created, NULL otherwise.
+ * Returns FALSE if no cache item could be created, NULL otherwise.
*
* @see ::get()
*/
diff --git a/core/lib/Drupal/Core/Routing/RouteBuilderInterface.php b/core/lib/Drupal/Core/Routing/RouteBuilderInterface.php
index 83b1d3ed1..d191cb9a6 100644
--- a/core/lib/Drupal/Core/Routing/RouteBuilderInterface.php
+++ b/core/lib/Drupal/Core/Routing/RouteBuilderInterface.php
@@ -2,10 +2,27 @@
namespace Drupal\Core\Routing;
+/**
+ * Rebuilds the route information and dumps it.
+ *
+ * Rebuilding the route information is the process of gathering all routing data
+ * from .routing.yml files, creating a
+ * \Symfony\Component\Routing\RouteCollection object out of it, and dispatching
+ * that object as a \Drupal\Core\Routing\RouteBuildEvent to all registered
+ * listeners. After that, the \Symfony\Component\Routing\RouteCollection object
+ * is used to dump the data. Examples of a dump include filling up the routing
+ * table, auto-generating Apache mod_rewrite rules, or auto-generating a PHP
+ * matcher class.
+ *
+ * @see \Drupal\Core\Routing\MatcherDumperInterface
+ * @see \Drupal\Core\Routing\RouteProviderInterface
+ *
+ * @ingroup routing
+ */
interface RouteBuilderInterface {
/**
- * Rebuilds the route info and dumps to dumper.
+ * Rebuilds the route information and dumps it.
*
* @return bool
* Returns TRUE if the rebuild succeeds, FALSE otherwise.
@@ -13,7 +30,7 @@ interface RouteBuilderInterface {
public function rebuild();
/**
- * Rebuilds the route info and dumps to dumper if necessary.
+ * Rebuilds the route information if necessary, and dumps it.
*
* @return bool
* Returns TRUE if the rebuild occurs, FALSE otherwise.
diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php
index 9291c40fd..ccf2b60d8 100644
--- a/core/lib/Drupal/Core/Routing/UrlGenerator.php
+++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php
@@ -229,10 +229,8 @@ class UrlGenerator implements UrlGeneratorInterface {
// Add a query string if needed, including extra parameters.
$query_params += array_diff_key($parameters, $variables, $defaults);
- if ($query_params && $query = http_build_query($query_params, '', '&')) {
- // "/" and "?" can be left decoded for better user experience, see
- // http://tools.ietf.org/html/rfc3986#section-3.4
- $url .= '?' . strtr($query, array('%2F' => '/'));
+ if ($query_params && $query = UrlHelper::buildQuery($query_params)) {
+ $url .= '?' . $query;
}
return $url;
@@ -253,7 +251,7 @@ class UrlGenerator implements UrlGeneratorInterface {
* $parameters merged in.
*
* @return string
- * The url path corresponding to the route, without the base path.
+ * The url path corresponding to the route, without the base path.
*/
protected function getInternalPathFromRoute($name, SymfonyRoute $route, $parameters = array(), $query_params = array()) {
// The Route has a cache of its own and is not recompiled as long as it does
diff --git a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
index f5174f1d2..70829cd68 100644
--- a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
+++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php
@@ -21,7 +21,7 @@ interface UrlGeneratorInterface extends VersatileGeneratorInterface {
* \Symfony\Component\Routing\Generator\UrlGeneratorInterface::generate().
*
* @return string
- * The internal Drupal path corresponding to the route.
+ * The internal Drupal path corresponding to the route.
*/
public function getPathFromRoute($name, $parameters = array());
diff --git a/core/lib/Drupal/Core/StreamWrapper/PhpStreamWrapperInterface.php b/core/lib/Drupal/Core/StreamWrapper/PhpStreamWrapperInterface.php
index ed8db6465..4c11c8d66 100644
--- a/core/lib/Drupal/Core/StreamWrapper/PhpStreamWrapperInterface.php
+++ b/core/lib/Drupal/Core/StreamWrapper/PhpStreamWrapperInterface.php
@@ -64,7 +64,7 @@ interface PhpStreamWrapperInterface {
public function stream_cast($cast_as);
/**
- * @return void
+ * Closes stream.
*/
public function stream_close();
diff --git a/core/lib/Drupal/Core/StringTranslation/Translator/FileTranslation.php b/core/lib/Drupal/Core/StringTranslation/Translator/FileTranslation.php
index d4c46e3ef..abe5e9a7b 100644
--- a/core/lib/Drupal/Core/StringTranslation/Translator/FileTranslation.php
+++ b/core/lib/Drupal/Core/StringTranslation/Translator/FileTranslation.php
@@ -82,7 +82,7 @@ class FileTranslation extends StaticTranslation {
* want to find translation files.
*
* @return string
- * String file pattern.
+ * String file pattern.
*/
protected function getTranslationFilesPattern($langcode = NULL) {
// The file name matches: drupal-[release version].[language code].po
diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index f29e14697..6a58319e0 100644
--- a/core/lib/Drupal/Core/Url.php
+++ b/core/lib/Drupal/Core/Url.php
@@ -272,7 +272,11 @@ class Url {
if ($uri_parts === FALSE) {
throw new \InvalidArgumentException("The URI '$uri' is malformed.");
}
- if (empty($uri_parts['scheme'])) {
+ // We support protocol-relative URLs.
+ if (strpos($uri, '//') === 0) {
+ $uri_parts['scheme'] = '';
+ }
+ elseif (empty($uri_parts['scheme'])) {
throw new \InvalidArgumentException("The URI '$uri' is invalid. You must use a valid URI scheme.");
}
$uri_parts += ['path' => ''];
diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php
index 7acb83815..496100133 100644
--- a/core/lib/Drupal/Core/Utility/LinkGenerator.php
+++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php
@@ -140,6 +140,7 @@ class LinkGenerator implements LinkGeneratorInterface {
// Allow other modules to modify the structure of the link.
$this->moduleHandler->alter('link', $variables);
+ $url = $variables['url'];
// Move attributes out of options since generateFromRoute() doesn't need
// them. Include a placeholder for the href.
diff --git a/core/misc/autocomplete.js b/core/misc/autocomplete.js
index 032a15ca3..254e7e509 100644
--- a/core/misc/autocomplete.js
+++ b/core/misc/autocomplete.js
@@ -222,7 +222,7 @@
});
// Use jQuery UI Autocomplete on the textfield.
$autocomplete.autocomplete(autocomplete.options)
- .each(function() {
+ .each(function () {
$(this).data('ui-autocomplete')._renderItem = autocomplete.options.renderItem;
});
}
diff --git a/core/modules/action/src/Tests/ActionUninstallTest.php b/core/modules/action/tests/src/Functional/ActionUninstallTest.php
similarity index 87%
rename from core/modules/action/src/Tests/ActionUninstallTest.php
rename to core/modules/action/tests/src/Functional/ActionUninstallTest.php
index fd8faa5c9..25dd564a8 100644
--- a/core/modules/action/src/Tests/ActionUninstallTest.php
+++ b/core/modules/action/tests/src/Functional/ActionUninstallTest.php
@@ -1,8 +1,8 @@
drupalGet('test_bulk_form');
$result = $this->xpath('//label[@for="edit-action"]');
- $this->assertEqual('With selection', (string) $result[0]);
+ $this->assertEqual('With selection', $result[0]->getText());
// Setup up a different bulk form title.
$view = Views::getView('test_bulk_form');
@@ -133,7 +133,7 @@ class BulkFormTest extends WebTestBase {
$this->drupalGet('test_bulk_form');
$result = $this->xpath('//label[@for="edit-action"]');
- $this->assertEqual('Test title', (string) $result[0]);
+ $this->assertEqual('Test title', $result[0]->getText());
$this->drupalGet('test_bulk_form');
// Call the node delete action.
diff --git a/core/modules/action/src/Tests/ConfigurationTest.php b/core/modules/action/tests/src/Functional/ConfigurationTest.php
similarity index 96%
rename from core/modules/action/src/Tests/ConfigurationTest.php
rename to core/modules/action/tests/src/Functional/ConfigurationTest.php
index a6b20502f..17d913de6 100644
--- a/core/modules/action/src/Tests/ConfigurationTest.php
+++ b/core/modules/action/tests/src/Functional/ConfigurationTest.php
@@ -1,9 +1,9 @@
select('aggregator_item', 'ai')
->fields('ai')
- ->orderBy('iid');
+ ->orderBy('ai.iid');
}
/**
diff --git a/core/modules/ban/src/Tests/IpAddressBlockingTest.php b/core/modules/ban/tests/src/Functional/IpAddressBlockingTest.php
similarity index 94%
rename from core/modules/ban/src/Tests/IpAddressBlockingTest.php
rename to core/modules/ban/tests/src/Functional/IpAddressBlockingTest.php
index 9fb9ff850..738bddfb5 100644
--- a/core/modules/ban/src/Tests/IpAddressBlockingTest.php
+++ b/core/modules/ban/tests/src/Functional/IpAddressBlockingTest.php
@@ -1,15 +1,15 @@
drupalPostForm('admin/config/people/ban/' . $submit_ip, NULL, t('Add'));
+ $this->drupalPostForm('admin/config/people/ban/' . $submit_ip, array(), t('Add'));
$ip = db_query("SELECT iid from {ban_ip} WHERE ip = :ip", array(':ip' => $submit_ip))->fetchField();
$this->assertTrue($ip, 'IP address found in database');
$this->assertRaw(t('The IP address %ip has been banned.', array('%ip' => $submit_ip)), 'IP address was banned.');
diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php
index 3e67737f5..5e3cc1203 100644
--- a/core/modules/big_pipe/src/Render/BigPipe.php
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -226,6 +226,13 @@ class BigPipe implements BigPipeInterface {
$preg_placeholder_strings = array_map($prepare_for_preg_split, array_keys($no_js_placeholders));
$fragments = preg_split('/' . implode('|', $preg_placeholder_strings) . '/', $html, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+ // Determine how many occurrences there are of each no-JS placeholder.
+ $placeholder_occurrences = array_count_values(array_intersect($fragments, array_keys($no_js_placeholders)));
+
+ // Set up a variable to store the content of placeholders that have multiple
+ // occurrences.
+ $multi_occurrence_placeholders_content = [];
+
foreach ($fragments as $fragment) {
// If the fragment isn't one of the no-JS placeholders, it is the HTML in
// between placeholders and it must be printed & flushed immediately. The
@@ -236,6 +243,15 @@ class BigPipe implements BigPipeInterface {
continue;
}
+ // If there are multiple occurrences of this particular placeholder, and
+ // this is the second occurrence, we can skip all calculations and just
+ // send the same content.
+ if ($placeholder_occurrences[$fragment] > 1 && isset($multi_occurrence_placeholders_content[$fragment])) {
+ print $multi_occurrence_placeholders_content[$fragment];
+ flush();
+ continue;
+ }
+
$placeholder = $fragment;
assert('isset($no_js_placeholders[$placeholder])');
$token = Crypt::randomBytesBase64(55);
@@ -310,6 +326,13 @@ class BigPipe implements BigPipeInterface {
// they can be sent in ::sendPreBody().
$cumulative_assets->setAlreadyLoadedLibraries(array_merge($cumulative_assets->getAlreadyLoadedLibraries(), $html_response->getAttachments()['library']));
$cumulative_assets->setSettings($html_response->getAttachments()['drupalSettings']);
+
+ // If there are multiple occurrences of this particular placeholder, track
+ // the content that was sent, so we can skip all calculations for the next
+ // occurrence.
+ if ($placeholder_occurrences[$fragment] > 1) {
+ $multi_occurrence_placeholders_content[$fragment] = $html_response->getContent();
+ }
}
}
@@ -508,7 +531,9 @@ EOF;
*
* @return array
* Indexed array; the order in which the BigPipe placeholders must be sent.
- * Values are the BigPipe placeholder IDs.
+ * Values are the BigPipe placeholder IDs. Note that only unique
+ * placeholders are kept: if the same placeholder occurs multiple times, we
+ * only keep the first occurrence.
*/
protected function getPlaceholderOrder($html) {
$fragments = explode('
';
+ $this->assertRaw('The count is 1.');
+ $this->assertNoRaw('The count is 2.');
+ $this->assertNoRaw('The count is 3.');
+ $raw_content = $this->getRawContent();
+ $this->assertTrue(substr_count($raw_content, $expected_placeholder_replacement) == 1, 'Only one placeholder replacement was found for the duplicate #lazy_builder arrays.');
+
+ // By calling performMetaRefresh() here, we simulate JavaScript being
+ // disabled, because as far as the BigPipe module is concerned, it is
+ // enabled in the browser when the BigPipe no-JS cookie is set.
+ // @see setUp()
+ // @see performMetaRefresh()
+ $this->performMetaRefresh();
+ $this->assertBigPipeNoJsCookieExists(TRUE);
+ $this->drupalGet(Url::fromRoute('big_pipe_test_multi_occurrence'));
+ $this->assertRaw('The count is 1.');
+ $this->assertNoRaw('The count is 2.');
+ $this->assertNoRaw('The count is 3.');
+ }
+
protected function assertBigPipeResponseHeadersPresent() {
$this->pass('Verifying BigPipe response headers…', 'Debug');
$this->assertTrue(FALSE !== strpos($this->drupalGetHeader('Cache-Control'), 'private'), 'Cache-Control header set to "private".');
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml b/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml
index 4979406d5..710506063 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.routing.yml
@@ -15,3 +15,12 @@ no_big_pipe:
_no_big_pipe: TRUE
requirements:
_access: 'TRUE'
+
+big_pipe_test_multi_occurrence:
+ path: '/big_pipe_test_multi_occurrence'
+ defaults:
+ _controller: '\Drupal\big_pipe_test\BigPipeTestController::multiOccurrence'
+ _title: 'BigPipe test multiple occurrences of the same placeholder'
+ requirements:
+ _access: 'TRUE'
+
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
index 450a464bd..30594a555 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php
@@ -52,6 +52,30 @@ class BigPipeTestController {
return ['#markup' => '
Nope.
'];
}
+ /**
+ * A page with multiple occurrences of the same placeholder.
+ *
+ * @see \Drupal\big_pipe\Tests\BigPipeTest::testBigPipeMultipleOccurrencePlaceholders()
+ *
+ * @return array
+ */
+ public function multiOccurrence() {
+ return [
+ 'item1' => [
+ '#lazy_builder' => [static::class . '::counter', []],
+ '#create_placeholder' => TRUE,
+ ],
+ 'item2' => [
+ '#lazy_builder' => [static::class . '::counter', []],
+ '#create_placeholder' => TRUE,
+ ],
+ 'item3' => [
+ '#lazy_builder' => [static::class . '::counter', []],
+ '#create_placeholder' => TRUE,
+ ],
+ ];
+ }
+
/**
* #lazy_builder callback; builds