Browse Source

updated core to 8.6.3

Bachir Soussi Chiadmi 5 years ago
parent
commit
c92c348eee
100 changed files with 881 additions and 333 deletions
  1. 6 6
      composer.lock
  2. 4 1
      core/.stylelintrc.json
  3. 1 1
      core/MAINTAINERS.txt
  4. 123 25
      core/UPDATE.txt
  5. 48 18
      core/assets/vendor/ckeditor/CHANGES.md
  6. 0 0
      core/assets/vendor/ckeditor/ckeditor.js
  7. 0 0
      core/assets/vendor/ckeditor/lang/af.js
  8. 0 0
      core/assets/vendor/ckeditor/lang/fa.js
  9. 0 0
      core/assets/vendor/ckeditor/lang/fr.js
  10. 0 0
      core/assets/vendor/ckeditor/lang/lv.js
  11. 0 0
      core/assets/vendor/ckeditor/lang/ro.js
  12. 0 0
      core/assets/vendor/ckeditor/lang/sk.js
  13. BIN
      core/assets/vendor/ckeditor/plugins/icons.png
  14. BIN
      core/assets/vendor/ckeditor/plugins/icons_hidpi.png
  15. 0 0
      core/assets/vendor/ckeditor/plugins/image2/dialogs/image2.js
  16. 0 0
      core/assets/vendor/ckeditor/skins/moono-lisa/editor.css
  17. 0 0
      core/assets/vendor/ckeditor/skins/moono-lisa/editor_gecko.css
  18. 0 0
      core/assets/vendor/ckeditor/skins/moono-lisa/editor_ie.css
  19. 0 0
      core/assets/vendor/ckeditor/skins/moono-lisa/editor_ie8.css
  20. 0 0
      core/assets/vendor/ckeditor/skins/moono-lisa/editor_iequirks.css
  21. BIN
      core/assets/vendor/ckeditor/skins/moono-lisa/icons.png
  22. BIN
      core/assets/vendor/ckeditor/skins/moono-lisa/icons_hidpi.png
  23. 1 1
      core/authorize.php
  24. 1 1
      core/composer.json
  25. 2 2
      core/core.libraries.yml
  26. 1 3
      core/core.services.yml
  27. 10 6
      core/includes/bootstrap.inc
  28. 16 7
      core/includes/install.core.inc
  29. 17 0
      core/includes/install.inc
  30. 2 2
      core/lib/Drupal.php
  31. 1 1
      core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php
  32. 1 1
      core/lib/Drupal/Component/Gettext/PoStreamReader.php
  33. 1 1
      core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php
  34. 4 1
      core/lib/Drupal/Component/Plugin/PluginManagerBase.php
  35. 3 3
      core/lib/Drupal/Core/Archiver/ArchiveTar.php
  36. 1 1
      core/lib/Drupal/Core/Cache/CacheBackendInterface.php
  37. 1 3
      core/lib/Drupal/Core/Cache/MemoryBackend.php
  38. 16 5
      core/lib/Drupal/Core/Config/ConfigInstaller.php
  39. 2 1
      core/lib/Drupal/Core/Config/ConfigInstallerInterface.php
  40. 1 1
      core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php
  41. 6 2
      core/lib/Drupal/Core/Config/FileStorage.php
  42. 1 1
      core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
  43. 1 1
      core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
  44. 7 0
      core/lib/Drupal/Core/Database/Query/Select.php
  45. 6 6
      core/lib/Drupal/Core/Database/StatementPrefetch.php
  46. 1 1
      core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php
  47. 6 2
      core/lib/Drupal/Core/DrupalKernel.php
  48. 2 1
      core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php
  49. 2 2
      core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php
  50. 1 0
      core/lib/Drupal/Core/Entity/EntityBundleListener.php
  51. 1 1
      core/lib/Drupal/Core/Entity/EntityFieldManager.php
  52. 1 1
      core/lib/Drupal/Core/Entity/EntityStorageBase.php
  53. 1 1
      core/lib/Drupal/Core/Entity/Plugin/DataType/EntityReference.php
  54. 1 1
      core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
  55. 21 9
      core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
  56. 1 4
      core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php
  57. 17 7
      core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampAgoFormatter.php
  58. 4 4
      core/lib/Drupal/Core/Form/ConfirmFormInterface.php
  59. 1 1
      core/lib/Drupal/Core/Form/FormBuilder.php
  60. 2 2
      core/lib/Drupal/Core/Form/FormState.php
  61. 1 1
      core/lib/Drupal/Core/Http/HandlerStackConfigurator.php
  62. 1 1
      core/lib/Drupal/Core/Http/TrustedHostsRequestFactory.php
  63. 1 1
      core/lib/Drupal/Core/Installer/Exception/InstallProfileMismatchException.php
  64. 10 0
      core/lib/Drupal/Core/Mail/MailManager.php
  65. 15 12
      core/lib/Drupal/Core/Menu/LocalActionDefault.php
  66. 3 3
      core/lib/Drupal/Core/Menu/LocalActionManager.php
  67. 15 13
      core/lib/Drupal/Core/Menu/LocalTaskDefault.php
  68. 1 1
      core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php
  69. 1 1
      core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php
  70. 8 1
      core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php
  71. 2 0
      core/lib/Drupal/Core/Plugin/Context/Context.php
  72. 1 1
      core/lib/Drupal/Core/ProxyBuilder/ProxyBuilder.php
  73. 2 0
      core/lib/Drupal/Core/Render/Element/Email.php
  74. 2 0
      core/lib/Drupal/Core/Render/Element/Password.php
  75. 1 1
      core/lib/Drupal/Core/Render/Element/RenderElement.php
  76. 2 0
      core/lib/Drupal/Core/Render/Element/Tel.php
  77. 3 1
      core/lib/Drupal/Core/Render/Element/Textfield.php
  78. 2 0
      core/lib/Drupal/Core/Render/Element/Url.php
  79. 1 1
      core/lib/Drupal/Core/Session/SessionConfiguration.php
  80. 2 4
      core/lib/Drupal/Core/Session/WriteSafeSessionHandlerInterface.php
  81. 41 29
      core/lib/Drupal/Core/State/State.php
  82. 1 1
      core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php
  83. 29 1
      core/lib/Drupal/Core/TempStore/PrivateTempStore.php
  84. 1 1
      core/lib/Drupal/Core/TypedData/TypedDataManager.php
  85. 1 1
      core/lib/Drupal/Core/Update/UpdateRegistry.php
  86. 1 1
      core/lib/Drupal/Core/Updater/Module.php
  87. 1 1
      core/lib/Drupal/Core/Updater/Theme.php
  88. 0 1
      core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php
  89. 1 1
      core/misc/announce.es6.js
  90. 1 1
      core/misc/autocomplete.es6.js
  91. 1 1
      core/misc/normalize-fixes.css
  92. 38 12
      core/misc/states.es6.js
  93. 21 8
      core/misc/states.js
  94. 1 1
      core/modules/action/src/Plugin/Action/GotoAction.php
  95. 1 1
      core/modules/aggregator/src/ItemsImporter.php
  96. 1 1
      core/modules/ban/src/Plugin/migrate/destination/BlockedIp.php
  97. 5 91
      core/modules/block_content/src/Plugin/migrate/source/d6/BoxTranslation.php
  98. 99 0
      core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php
  99. 69 0
      core/modules/block_content/tests/src/Kernel/Migrate/d7/MigrateCustomBlockContentTranslationTest.php
  100. 148 0
      core/modules/block_content/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTranslationTest.php

+ 6 - 6
composer.lock

@@ -1639,16 +1639,16 @@
         },
         {
             "name": "drupal/core",
-            "version": "8.6.2",
+            "version": "8.6.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/drupal/core.git",
-                "reference": "356292934802bb1aecc478e88a3cba77442d7c62"
+                "reference": "9e9a1dd9e280ebaf10622217e54448b529167965"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/drupal/core/zipball/356292934802bb1aecc478e88a3cba77442d7c62",
-                "reference": "356292934802bb1aecc478e88a3cba77442d7c62",
+                "url": "https://api.github.com/repos/drupal/core/zipball/9e9a1dd9e280ebaf10622217e54448b529167965",
+                "reference": "9e9a1dd9e280ebaf10622217e54448b529167965",
                 "shasum": ""
             },
             "require": {
@@ -1814,7 +1814,7 @@
                 "jcalderonzumba/gastonjs": "^1.0.2",
                 "jcalderonzumba/mink-phantomjs-driver": "^0.3.1",
                 "mikey179/vfsstream": "^1.2",
-                "phpspec/prophecy": "^1.4",
+                "phpspec/prophecy": "^1.7",
                 "phpunit/phpunit": "^4.8.35 || ^6.5",
                 "symfony/css-selector": "^3.4.0",
                 "symfony/debug": "^3.4.0",
@@ -1873,7 +1873,7 @@
                 "GPL-2.0-or-later"
             ],
             "description": "Drupal is an open source content management platform powering millions of websites and applications.",
-            "time": "2018-10-17T22:19:50+00:00"
+            "time": "2018-11-07T14:45:40+00:00"
         },
         {
             "name": "drush/drush",

+ 4 - 1
core/.stylelintrc.json

@@ -11,7 +11,10 @@
     "no-duplicate-selectors": null,
     "no-unknown-animations": true,
     "media-feature-name-no-unknown": [true, {
-      "ignoreMediaFeatureNames": ["prefers-reduced-motion"]
+      "ignoreMediaFeatureNames": [
+        "prefers-reduced-motion",
+        "min--moz-device-pixel-ratio"
+      ]
     }],
     "number-leading-zero": "always",
     "plugin/no-browser-hacks": [true, {

+ 1 - 1
core/MAINTAINERS.txt

@@ -113,7 +113,6 @@ CKEditor
 
 Classy
 - David Hernandez 'davidhernandez' https://www.drupal.org/u/davidhernandez
-- Morten Birch Heide-Jørgensen 'mortendk' https://www.drupal.org/u/mortendk
 
 Color
 - ?
@@ -475,6 +474,7 @@ their responsibilities. The initiative coordinators for Drupal 8 are:
 API-first Initiative
 - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers
 - Mateu Aguiló Bosch 'e0ipso' https://www.drupal.org/u/e0ipso
+- Gabe Sullice 'gabesullice' https://www.drupal.org/u/gabesullice
 
 Admin UI & JavaScript Modernisation Initiative
 - Angela Byron 'webchick' https://www.drupal.org/u/webchick

+ 123 - 25
core/UPDATE.txt

@@ -64,15 +64,51 @@ following the instructions in the INTRODUCTION section at the top of this file:
    Enable the "Put site into maintenance mode" checkbox and save the
    configuration.
 
-3. Remove the 'core' and 'vendor' directories. Also remove all of the files
-   in the top-level directory, except any that you added manually.
+3. Determine if your project is managed by Composer.
 
-   If you made modifications to files like .htaccess, composer.json, or
-   robots.txt you will need to re-apply them from your backup, after the new
-   files are in place.
+   On a typical Unix/Linux command line, this can be determined by running the
+   following command (replace /PATH/TO/composer with the appropriate location
+   for your system):
+
+     /PATH/TO/composer info drupal/core
+
+   If this is successful, your project is managed by Composer.
+
+   If you don't have Composer installed or access to the command line, you can
+   check the contents of composer.json. If "drupal/core" is present in the
+   "require" section of your composer.json file, then the project is managed by
+   Composer.
+
+   If the project is not managed by Composer, follow the steps under "UPDATING
+   CODE WITHOUT COMPOSER", otherwise go to "UPDATING CODE WITH COMPOSER".
+
+UPDATING CODE WITH COMPOSER
+---------------------------
+1. On a typical Unix/Linux command line, run the following command from the root
+   directory (replace /PATH/TO/composer with the appropriate location for your
+   system):
+
+     /PATH/TO/composer update
+
+   Note, if Composer is not installed you will need to install it in order to
+   update Drupal.
+
+   Note, if you want to only update drupal/core the following command will
+   probably work:
+
+     /PATH/TO/composer update drupal/core symfony/* --with-all-dependencies
+
+2. Check the release notes for the updated version of Drupal to find out if
+   there is a change to default.settings.php.
+
+   You can find the release notes for your version at
+   https://www.drupal.org/project/drupal. At bottom of the project page under
+   "Downloads" use the link for your version of Drupal to view the release
+   notes. If your version is not listed, use the 'View all releases' link. From
+   this page you can scroll down or use the filter to find your version and its
+   release notes.
 
-   Sometimes an update includes changes to default.settings.php (this will be
-   noted in the release notes). If that's the case, follow these steps:
+   If there is a change to default.settings.php, follow these steps:
 
    - Locate your settings.php file in the /sites/* directory. (Typically
      sites/default.)
@@ -87,16 +123,32 @@ following the instructions in the INTRODUCTION section at the top of this file:
      database information, and you will also want to copy in any other
      customizations you have added.
 
-   You can find the release notes for your version at
-   https://www.drupal.org/project/drupal. At bottom of the project page under
-   "Downloads" use the link for your version of Drupal to view the release
-   notes. If your version is not listed, use the 'View all releases' link. From
-   this page you can scroll down or use the filter to find your version and its
-   release notes.
+3. Determine if there are any modifications to files such as .htaccess or
+   robots.txt and re-apply them. The Drupal Scaffold composer plugin
+   (https://github.com/drupal-composer/drupal-scaffold) can help you with
+   excluding files you'd like to always preserve when updating Drupal.
+
+4. Go to the "UPLOADING THE CODE" section
+
+UPDATING CODE WITHOUT COMPOSER
+------------------------------
+1. Remove the 'core' and 'vendor' directories. Also remove all of the files
+   in the top-level directory, except any that you added manually.
 
-4. Download the latest Drupal 8.x.x release from https://www.drupal.org to a
-   directory outside of your web root. Extract the archive and copy the files
-   into your Drupal directory.
+   If you made modifications to files like .htaccess, composer.json, or
+   robots.txt you will need to re-apply them from your backup, after the new
+   files are in place.
+
+   This should leave you with the modules, profiles, sites, and themes
+   directories. These directories should only contain code that you've used to
+   extend Drupal.
+
+2. Download the latest Drupal 8.x.x release from https://www.drupal.org/download
+   to a directory outside of your web root. Extract the archive and copy the
+   files into your Drupal directory.
+
+   Copy all the files, but do not accidentally overwrite your modules, profiles,
+   sites, or themes directories.
 
    On a typical Unix/Linux command line, use the following commands to download
    and extract:
@@ -110,13 +162,60 @@ following the instructions in the INTRODUCTION section at the top of this file:
      cp -R drupal-x.y.z/* drupal-x.y.z/.htaccess /path/to/your/installation
 
    If you do not have command line access to your server, download the archive
-   from https://www.drupal.org using your web browser, extract it, and then use
-   an FTP client to upload the files to your web root.
+   from https://www.drupal.org using your web browser and extract it locally.
+
+3. Check the release notes for the updated version of Drupal to find out if
+   there is a change to default.settings.php.
+
+   You can find the release notes for your updated version at
+   https://www.drupal.org/project/drupal. At bottom of the project page under
+   "Downloads" use the link for your updated version of Drupal to view the
+   release notes. If your updated version is not listed, use the 'View all
+   releases' link. From this page you can scroll down or use the filter to find
+   your updated version and its release notes.
+
+   If there is a change to default.settings.php, follow these steps:
+
+   - Locate your settings.php file in the /sites/* directory. (Typically
+     sites/default.)
+
+   - Make a backup copy of your settings.php file, with a different file name.
 
-5. Re-apply any modifications to files such as .htaccess, composer.json, or
-   robots.txt.
+   - Make a copy of the new default.settings.php file, and name the copy
+     settings.php (overwriting your previous settings.php file).
+
+   - Copy the custom and site-specific entries from the backup you made into the
+     new settings.php file. You will definitely need the lines giving the
+     database information, and you will also want to copy in any other
+     customizations you have added.
+
+4. Re-apply any modifications to files such as .htaccess or robots.txt.
 
-6. Run update.php by visiting http://www.example.com/update.php (replace
+   If you have added requirements in composer.json, it is recommended that you
+   re-add the requirements using Composer instead of applying the changes by
+   hand. For example, on a typical Unix/Linux command line, to reinstall the
+   Address module and its dependencies run (replace /PATH/TO/composer with the
+   appropriate location for your system):
+
+     /PATH/TO/composer require drupal/address
+
+   If you do not have command line access to your server, you will need to run
+   the Composer commands locally before uploading the file system to your
+   server.
+
+5. Go to the "UPLOADING THE CODE" section
+
+UPLOADING THE CODE
+------------------
+1. If you updated the code in a different environment from where it is running
+   you need to upload the files to your web root including the vendor/
+   directory.
+
+2. Go to the "UPDATING THE DATABASE" section
+
+UPDATING THE DATABASE
+---------------------
+1. Run update.php by visiting http://www.example.com/update.php (replace
    www.example.com with your domain name). This will update the core database
    tables.
 
@@ -133,12 +232,11 @@ following the instructions in the INTRODUCTION section at the top of this file:
    - Once the update is done, $settings['update_free_access'] must be reverted
      to FALSE.
 
-7. Go to Administration > Reports > Status report. Verify that everything is
+2. Go to Administration > Reports > Status report. Verify that everything is
    working as expected.
 
-8. Ensure that $settings['update_free_access'] is FALSE in settings.php.
+3. Ensure that $settings['update_free_access'] is FALSE in settings.php.
 
-9. Go to Administration > Configuration > Development > Maintenance mode.
+4. Go to Administration > Configuration > Development > Maintenance mode.
    Disable the "Put site into maintenance mode" checkbox and save the
    configuration.
-

File diff suppressed because it is too large
+ 48 - 18
core/assets/vendor/ckeditor/CHANGES.md


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/ckeditor.js


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/lang/af.js


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/lang/fa.js


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/lang/fr.js


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/lang/lv.js


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/lang/ro.js


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/lang/sk.js


BIN
core/assets/vendor/ckeditor/plugins/icons.png


BIN
core/assets/vendor/ckeditor/plugins/icons_hidpi.png


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/plugins/image2/dialogs/image2.js


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/skins/moono-lisa/editor.css


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/skins/moono-lisa/editor_gecko.css


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/skins/moono-lisa/editor_ie.css


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/skins/moono-lisa/editor_ie8.css


File diff suppressed because it is too large
+ 0 - 0
core/assets/vendor/ckeditor/skins/moono-lisa/editor_iequirks.css


BIN
core/assets/vendor/ckeditor/skins/moono-lisa/icons.png


BIN
core/assets/vendor/ckeditor/skins/moono-lisa/icons_hidpi.png


+ 1 - 1
core/authorize.php

@@ -125,7 +125,7 @@ if ($is_allowed) {
       $links = $results['tasks'];
     }
     else {
-      // Since this is being called outsite of the primary front controller,
+      // Since this is being called outside of the primary front controller,
       // the base_url needs to be set explicitly to ensure that links are
       // relative to the site root.
       // @todo Simplify with https://www.drupal.org/node/2548095

+ 1 - 1
core/composer.json

@@ -59,7 +59,7 @@
         "jcalderonzumba/mink-phantomjs-driver": "^0.3.1",
         "mikey179/vfsStream": "^1.2",
         "phpunit/phpunit": "^4.8.35 || ^6.5",
-        "phpspec/prophecy": "^1.4",
+        "phpspec/prophecy": "^1.7",
         "symfony/css-selector": "^3.4.0",
         "symfony/phpunit-bridge": "^3.4.3",
         "symfony/debug": "^3.4.0"

+ 2 - 2
core/core.libraries.yml

@@ -24,10 +24,10 @@ classList:
 
 ckeditor:
   remote: https://github.com/ckeditor/ckeditor-dev
-  version: "4.10.0"
+  version: "4.10.1"
   license:
     name: GNU-GPL-2.0-or-later
-    url: https://github.com/ckeditor/ckeditor-dev/blob/4.10.0/LICENSE.md
+    url: https://github.com/ckeditor/ckeditor-dev/blob/4.10.1/LICENSE.md
     gpl-compatible: true
   js:
     assets/vendor/ckeditor/ckeditor.js: { preprocess: false, minified: true }

+ 1 - 3
core/core.services.yml

@@ -431,9 +431,7 @@ services:
     factory: Drupal\Core\Site\Settings::getInstance
   state:
     class: Drupal\Core\State\State
-    arguments: ['@keyvalue', '@cache.bootstrap', '@lock']
-    tags:
-      - { name: needs_destruction }
+    arguments: ['@keyvalue']
   queue:
     class: Drupal\Core\Queue\QueueFactory
     arguments: ['@settings']

+ 10 - 6
core/includes/bootstrap.inc

@@ -572,14 +572,14 @@ function drupal_get_user_timezone() {
  * @param $message
  *   The error message.
  * @param $filename
- *   The filename that the error was raised in.
+ *   (optional) The filename that the error was raised in.
  * @param $line
- *   The line number the error was raised at.
+ *   (optional) The line number the error was raised at.
  * @param $context
- *   An array that points to the active symbol table at the point the error
- *   occurred.
+ *   (optional) An array that points to the active symbol table at the point the
+ *   error occurred.
  */
-function _drupal_error_handler($error_level, $message, $filename, $line, $context) {
+function _drupal_error_handler($error_level, $message, $filename = NULL, $line = NULL, $context = NULL) {
   require_once __DIR__ . '/errors.inc';
   _drupal_error_handler_real($error_level, $message, $filename, $line, $context);
 }
@@ -1033,8 +1033,12 @@ function _drupal_shutdown_function() {
   chdir(DRUPAL_ROOT);
 
   try {
-    foreach ($callbacks as &$callback) {
+    reset($callbacks);
+    // Do not use foreach() here because it is possible that the callback will
+    // add to the $callbacks array via drupal_register_shutdown_function().
+    while ($callback = current($callbacks)) {
       call_user_func_array($callback['callback'], $callback['arguments']);
+      next($callbacks);
     }
   }
   // PHP 7 introduces Throwable, which covers both Error and

+ 16 - 7
core/includes/install.core.inc

@@ -1270,12 +1270,19 @@ function install_select_profile(&$install_state) {
 /**
  * Determines the installation profile to use in the installer.
  *
- * A profile will be selected in the following order of conditions:
+ * Depending on the context from which it's being called, this method
+ * may be used to:
+ * - Automatically select a profile under certain conditions.
+ * - Indicate which profile has already been selected.
+ * - Indicate that a profile still needs to be selected.
+ *
+ * A profile will be selected automatically if one of the following conditions
+ * is met. They are checked in the given order:
  * - Only one profile is available.
  * - A specific profile name is requested in installation parameters:
  *   - For interactive installations via request query parameters.
  *   - For non-interactive installations via install_drupal() settings.
- * - A discovered profile that is a distribution. If multiple profiles are
+ * - One of the available profiles is a distribution. If multiple profiles are
  *   distributions, then the first discovered profile will be selected.
  * - Only one visible profile is available.
  *
@@ -1283,35 +1290,37 @@ function install_select_profile(&$install_state) {
  *   The current installer state, containing a 'profiles' key, which is an
  *   associative array of profiles with the machine-readable names as keys.
  *
- * @return
+ * @return string|null
  *   The machine-readable name of the selected profile or NULL if no profile was
  *   selected.
+ *
+ *  @see install_select_profile()
  */
 function _install_select_profile(&$install_state) {
-  // Don't need to choose profile if only one available.
+  // If there is only one profile available it will always be the one selected.
   if (count($install_state['profiles']) == 1) {
     return key($install_state['profiles']);
   }
+  // If a valid profile has already been selected, return the selection.
   if (!empty($install_state['parameters']['profile'])) {
     $profile = $install_state['parameters']['profile'];
     if (isset($install_state['profiles'][$profile])) {
       return $profile;
     }
   }
-  // Check for a distribution profile.
+  // If any of the profiles are distribution profiles, return the first one.
   foreach ($install_state['profiles'] as $profile) {
     $profile_info = install_profile_info($profile->getName());
     if (!empty($profile_info['distribution'])) {
       return $profile->getName();
     }
   }
-
   // Get all visible (not hidden) profiles.
   $visible_profiles = array_filter($install_state['profiles'], function ($profile) {
     $profile_info = install_profile_info($profile->getName());
     return !isset($profile_info['hidden']) || !$profile_info['hidden'];
   });
-
+  // If there is only one visible profile, return it.
   if (count($visible_profiles) == 1) {
     return (key($visible_profiles));
   }

+ 17 - 0
core/includes/install.inc

@@ -11,6 +11,7 @@ use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\OpCodeCache;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Extension\ModuleHandler;
 use Drupal\Core\Site\Settings;
 
 /**
@@ -990,6 +991,12 @@ function drupal_check_profile($profile) {
     }
   }
 
+  // Add the profile requirements.
+  $function = $profile . '_requirements';
+  if (function_exists($function)) {
+    $requirements = array_merge($requirements, $function('install'));
+  }
+
   return $requirements;
 }
 
@@ -1113,6 +1120,16 @@ function install_profile_info($profile, $langcode = 'en') {
     $info = \Drupal::service('info_parser')->parse("$profile_path/$profile.info.yml");
     $info += $defaults;
 
+    // Convert dependencies in [project:module] format.
+    $info['dependencies'] = array_map(function ($dependency) {
+      return ModuleHandler::parseDependency($dependency)['name'];
+    }, $info['dependencies']);
+
+    // Convert install key in [project:module] format.
+    $info['install'] = array_map(function ($dependency) {
+      return ModuleHandler::parseDependency($dependency)['name'];
+    }, $info['install']);
+
     // drupal_required_modules() includes the current profile as a dependency.
     // Remove that dependency, since a module cannot depend on itself.
     $required = array_diff(drupal_required_modules(), [$profile]);

+ 2 - 2
core/lib/Drupal.php

@@ -82,7 +82,7 @@ class Drupal {
   /**
    * The current system version.
    */
-  const VERSION = '8.6.2';
+  const VERSION = '8.6.3';
 
   /**
    * Core API compatibility.
@@ -229,7 +229,7 @@ class Drupal {
   }
 
   /**
-   * Retrives the request stack.
+   * Retrieves the request stack.
    *
    * @return \Symfony\Component\HttpFoundation\RequestStack
    *   The request stack

+ 1 - 1
core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php

@@ -200,7 +200,7 @@ class PhpArrayContainer extends Container {
         // The PhpArrayDumper just uses the hash of the private service
         // definition to generate a unique ID.
         //
-        // @see \Drupal\Component\DependecyInjection\Dumper\OptimizedPhpArrayDumper::getPrivateServiceCall
+        // @see \Drupal\Component\DependencyInjection\Dumper\OptimizedPhpArrayDumper::getPrivateServiceCall
         if ($type == 'private_service') {
           $id = $argument->id;
 

+ 1 - 1
core/lib/Drupal/Component/Gettext/PoStreamReader.php

@@ -394,7 +394,7 @@ class PoStreamReader implements PoStreamInterface, PoReaderInterface {
             ($this->context != 'MSGCTXT') &&
             ($this->context != 'MSGID_PLURAL') &&
             ($this->context != 'MSGSTR_ARR')) {
-          // Plural message strings must come after msgid, msgxtxt,
+          // Plural message strings must come after msgid, msgctxt,
           // msgid_plural, or other msgstr[] entries.
           $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars);
           return FALSE;

+ 1 - 1
core/lib/Drupal/Component/HttpFoundation/SecuredRedirectResponse.php

@@ -36,7 +36,7 @@ abstract class SecuredRedirectResponse extends RedirectResponse {
    * Copies over the values from the given response.
    *
    * @param \Symfony\Component\HttpFoundation\RedirectResponse $response
-   *   The redirect reponse object.
+   *   The redirect response object.
    */
   protected function fromResponse(RedirectResponse $response) {
     $this->setProtocolVersion($response->getProtocolVersion());

+ 4 - 1
core/lib/Drupal/Component/Plugin/PluginManagerBase.php

@@ -29,7 +29,7 @@ abstract class PluginManagerBase implements PluginManagerInterface {
   /**
    * The object that returns the preconfigured plugin instance appropriate for a particular runtime condition.
    *
-   * @var \Drupal\Component\Plugin\Mapper\MapperInterface
+   * @var \Drupal\Component\Plugin\Mapper\MapperInterface|null
    */
   protected $mapper;
 
@@ -104,6 +104,9 @@ abstract class PluginManagerBase implements PluginManagerInterface {
    * {@inheritdoc}
    */
   public function getInstance(array $options) {
+    if (!$this->mapper) {
+      throw new \BadMethodCallException(sprintf('%s does not support this method unless %s::$mapper is set.', static::class, static::class));
+    }
     return $this->mapper->getInstance($options);
   }
 

+ 3 - 3
core/lib/Drupal/Core/Archiver/ArchiveTar.php

@@ -49,7 +49,7 @@
  * The following changes have been done:
  *  Added namespace Drupal\Core\Archiver.
  *  Removed require_once 'PEAR.php'.
- *  Added defintion of OS_WINDOWS taken from PEAR.php.
+ *  Added definition of OS_WINDOWS taken from PEAR.php.
  *  Renamed class to ArchiveTar.
  *  Removed extends PEAR from class.
  *  Removed call parent:: __construct().
@@ -181,7 +181,7 @@ class ArchiveTar
                     if ($data == "\37\213") {
                         $this->_compress = true;
                         $this->_compress_type = 'gz';
-                        // No sure it's enought for a magic code ....
+                        // Not sure it's enough for a magic code ....
                     } elseif ($data == "BZ") {
                         $this->_compress = true;
                         $this->_compress_type = 'bz2';
@@ -2385,7 +2385,7 @@ class ArchiveTar
 
     /**
      * Compress path by changing for example "/dir/foo/../bar" to "/dir/bar",
-     * rand emove double slashes.
+     * and remove double slashes.
      *
      * @param string $p_dir path to reduce
      *

+ 1 - 1
core/lib/Drupal/Core/Cache/CacheBackendInterface.php

@@ -9,7 +9,7 @@ namespace Drupal\Core\Cache;
  * Drupal\Core\Cache\DatabaseBackend provides the default implementation, which
  * can be consulted as an example.
  *
- * The cache indentifiers are case sensitive.
+ * The cache identifiers are case sensitive.
  *
  * @ingroup cache
  */

+ 1 - 3
core/lib/Drupal/Core/Cache/MemoryBackend.php

@@ -157,9 +157,7 @@ class MemoryBackend implements CacheBackendInterface, CacheTagsInvalidatorInterf
    */
   public function invalidateMultiple(array $cids) {
     foreach ($cids as $cid) {
-      if (isset($this->cache[$cid])) {
-        $this->cache[$cid]->expire = $this->getRequestTime() - 1;
-      }
+      $this->cache[$cid]->expire = $this->getRequestTime() - 1;
     }
   }
 

+ 16 - 5
core/lib/Drupal/Core/Config/ConfigInstaller.php

@@ -160,7 +160,10 @@ class ConfigInstaller implements ConfigInstallerInterface {
    */
   public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
     $profile = $this->drupalGetProfile();
-    $optional_profile_config = [];
+    $enabled_extensions = $this->getEnabledExtensions();
+    $existing_config = $this->getActiveStorages()->listAll();
+
+    // Create the storages to read configuration from.
     if (!$storage) {
       // Search the install profile's optional configuration too.
       $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $this->installProfile);
@@ -171,7 +174,6 @@ class ConfigInstaller implements ConfigInstallerInterface {
       // Creates a profile storage to search for overrides.
       $profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
       $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
-      $optional_profile_config = $profile_storage->listAll();
     }
     else {
       // Profile has not been set yet. For example during the first steps of the
@@ -179,10 +181,17 @@ class ConfigInstaller implements ConfigInstallerInterface {
       $profile_storage = NULL;
     }
 
-    $enabled_extensions = $this->getEnabledExtensions();
-    $existing_config = $this->getActiveStorages()->listAll();
+    // Build the list of possible configuration to create.
+    $list = $storage->listAll();
+    if ($profile_storage && !empty($dependency)) {
+      // Only add the optional profile configuration into the list if we are
+      // have a dependency to check. This ensures that optional profile
+      // configuration is not unexpectedly re-created after being deleted.
+      $list = array_unique(array_merge($list, $profile_storage->listAll()));
+    }
 
-    $list = array_unique(array_merge($storage->listAll(), $optional_profile_config));
+    // Filter the list of configuration to only include configuration that
+    // should be created.
     $list = array_filter($list, function ($config_name) use ($existing_config) {
       // Only list configuration that:
       // - does not already exist
@@ -226,6 +235,8 @@ class ConfigInstaller implements ConfigInstallerInterface {
         unset($all_config[$config_name]);
       }
     }
+
+    // Create the optional configuration if there is any left after filtering.
     if (!empty($config_to_create)) {
       $this->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create, TRUE);
     }

+ 2 - 1
core/lib/Drupal/Core/Config/ConfigInstallerInterface.php

@@ -43,7 +43,8 @@ interface ConfigInstallerInterface {
    * @param \Drupal\Core\Config\StorageInterface $storage
    *   (optional) The configuration storage to search for optional
    *   configuration. If not provided, all enabled extension's optional
-   *   configuration directories will be searched.
+   *   configuration directories including the install profile's will be
+   *   searched.
    * @param array $dependency
    *   (optional) If set, ensures that the configuration being installed has
    *   this dependency. The format is dependency type as the key ('module',

+ 1 - 1
core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php

@@ -189,7 +189,7 @@ interface ConfigEntityInterface extends EntityInterface, ThirdPartySettingsInter
    * For example, a default view might not be installable if the base table
    * doesn't exist.
    *
-   * @retun bool
+   * @return bool
    *   TRUE if the entity is installable, FALSE otherwise.
    */
   public function isInstallable();

+ 6 - 2
core/lib/Drupal/Core/Config/FileStorage.php

@@ -46,7 +46,7 @@ class FileStorage implements StorageInterface {
     $this->collection = $collection;
 
     // Use a NULL File Cache backend by default. This will ensure only the
-    // internal statc caching of FileCache is used and thus avoids blowing up
+    // internal static caching of FileCache is used and thus avoids blowing up
     // the APCu cache.
     $this->fileCache = FileCacheFactory::get('config', ['cache_backend_class' => NULL]);
   }
@@ -279,6 +279,9 @@ class FileStorage implements StorageInterface {
    * {@inheritdoc}
    */
   public function getAllCollectionNames() {
+    if (!is_dir($this->directory)) {
+      return [];
+    }
     $collections = $this->getAllCollectionNamesHelper($this->directory);
     sort($collections);
     return $collections;
@@ -305,7 +308,8 @@ class FileStorage implements StorageInterface {
    * @param string $directory
    *   The directory to check for sub directories. This allows this
    *   function to be used recursively to discover all the collections in the
-   *   storage.
+   *   storage. It is the responsibility of the caller to ensure the directory
+   *   exists.
    *
    * @return array
    *   A list of collection names contained within the provided directory.

+ 1 - 1
core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php

@@ -65,7 +65,7 @@ class Tasks extends InstallTasks {
         Database::getConnection();
       }
       catch (\Exception $e) {
-        // Detect utf8mb4 incompability.
+        // Detect utf8mb4 incompatibility.
         if ($e->getCode() == Connection::UNSUPPORTED_CHARSET || ($e->getCode() == Connection::SQLSTATE_SYNTAX_ERROR && $e->errorInfo[1] == Connection::UNKNOWN_CHARSET)) {
           $this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href=":documentation" target="_blank">MySQL documentation</a> for more information.', [':documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html']));
           $info = Database::getConnectionInfo();

+ 1 - 1
core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php

@@ -38,7 +38,7 @@ class Connection extends DatabaseConnection {
   /**
    * A map of condition operators to PostgreSQL operators.
    *
-   * In PostgreSQL, 'LIKE' is case-sensitive. ILKE should be used for
+   * In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for
    * case-insensitive statements.
    */
   protected static $postgresqlConditionOperatorMap = [

+ 7 - 0
core/lib/Drupal/Core/Database/Query/Select.php

@@ -914,6 +914,8 @@ class Select extends Query implements SelectInterface {
    * {@inheritdoc}
    */
   public function __clone() {
+    parent::__clone();
+
     // On cloning, also clone the dependent objects. However, we do not
     // want to clone the database connection object as that would duplicate the
     // connection itself.
@@ -923,6 +925,11 @@ class Select extends Query implements SelectInterface {
     foreach ($this->union as $key => $aggregate) {
       $this->union[$key]['query'] = clone($aggregate['query']);
     }
+    foreach ($this->tables as $alias => $table) {
+      if ($table['table'] instanceof SelectInterface) {
+        $this->tables[$alias]['table'] = clone $table['table'];
+      }
+    }
   }
 
 }

+ 6 - 6
core/lib/Drupal/Core/Database/StatementPrefetch.php

@@ -20,7 +20,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
   /**
    * Driver-specific options. Can be used by child classes.
    *
-   * @var Array
+   * @var array
    */
   protected $driverOptions;
 
@@ -41,14 +41,14 @@ class StatementPrefetch implements \Iterator, StatementInterface {
   /**
    * Main data store.
    *
-   * @var Array
+   * @var array
    */
   protected $data = [];
 
   /**
    * The current row, retrieved in \PDO::FETCH_ASSOC format.
    *
-   * @var Array
+   * @var array
    */
   protected $currentRow = NULL;
 
@@ -62,7 +62,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
   /**
    * The list of column names in this result set.
    *
-   * @var Array
+   * @var array
    */
   protected $columnNames = NULL;
 
@@ -91,7 +91,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
   /**
    * Holds supplementary current fetch options (which will be used by the next fetch).
    *
-   * @var Array
+   * @var array
    */
   protected $fetchOptions = [
     'class' => 'stdClass',
@@ -110,7 +110,7 @@ class StatementPrefetch implements \Iterator, StatementInterface {
   /**
    * Holds supplementary default fetch options.
    *
-   * @var Array
+   * @var array
    */
   protected $defaultFetchOptions = [
     'class' => 'stdClass',

+ 1 - 1
core/lib/Drupal/Core/DependencyInjection/Compiler/CorsCompilerPass.php

@@ -22,7 +22,7 @@ class CorsCompilerPass implements CompilerPassInterface {
       $enabled = !empty($cors_config['enabled']);
     }
 
-    // Remove the CORS middleware completly in case it was not enabled.
+    // Remove the CORS middleware completely in case it was not enabled.
     if (!$enabled) {
       $container->removeDefinition('http_middleware.cors');
     }

+ 6 - 2
core/lib/Drupal/Core/DrupalKernel.php

@@ -298,12 +298,16 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
   }
 
   /**
-   * Determine the application root directory based on assumptions.
+   * Determine the application root directory based on this file's location.
    *
    * @return string
    *   The application root.
    */
   protected static function guessApplicationRoot() {
+    // Determine the application root by:
+    // - Removing the namespace directories from the path.
+    // - Getting the path to the directory two levels up from the path
+    //   determined in the previous step.
     return dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
   }
 
@@ -1091,7 +1095,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
         // misses.
         $old_loader = $this->classLoader;
         $this->classLoader = $loader;
-        // Our class loaders are preprended to ensure they come first like the
+        // Our class loaders are prepended to ensure they come first like the
         // class loader they are replacing.
         $old_loader->register(TRUE);
         $loader->register(TRUE);

+ 2 - 1
core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php

@@ -60,6 +60,7 @@ class ContentEntityDeleteForm extends ContentEntityConfirmFormBase {
   public function submitForm(array &$form, FormStateInterface $form_state) {
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     $entity = $this->getEntity();
+    $message = $this->getDeletionMessage();
 
     // Make sure that deleting a translation does not delete the whole entity.
     if (!$entity->isDefaultTranslation()) {
@@ -73,7 +74,7 @@ class ContentEntityDeleteForm extends ContentEntityConfirmFormBase {
       $form_state->setRedirectUrl($this->getRedirectUrl());
     }
 
-    $this->messenger()->addStatus($this->getDeletionMessage());
+    $this->messenger()->addStatus($message);
     $this->logDeletionMessage();
   }
 

+ 2 - 2
core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php

@@ -251,8 +251,8 @@ class EntityAutocomplete extends Textfield {
   /**
    * Finds an entity from an autocomplete input without an explicit ID.
    *
-   * The method will return an entity ID if one single entity unambuguously
-   * matches the incoming input, and sill assign form errors otherwise.
+   * The method will return an entity ID if one single entity unambiguously
+   * matches the incoming input, and assign form errors otherwise.
    *
    * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler
    *   Entity reference selection plugin.

+ 1 - 0
core/lib/Drupal/Core/Entity/EntityBundleListener.php

@@ -68,6 +68,7 @@ class EntityBundleListener implements EntityBundleListenerInterface {
     }
     // Invoke hook_entity_bundle_create() hook.
     $this->moduleHandler->invokeAll('entity_bundle_create', [$entity_type_id, $bundle]);
+    $this->entityFieldManager->clearCachedFieldDefinitions();
   }
 
   /**

+ 1 - 1
core/lib/Drupal/Core/Entity/EntityFieldManager.php

@@ -497,7 +497,7 @@ class EntityFieldManager implements EntityFieldManagerInterface {
           }
         }
 
-        $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, ['entity_types']);
+        $this->cacheSet($cid, $this->fieldMap, Cache::PERMANENT, ['entity_types', 'entity_field_info']);
       }
     }
     return $this->fieldMap;

+ 1 - 1
core/lib/Drupal/Core/Entity/EntityStorageBase.php

@@ -194,7 +194,7 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
    * Invokes a hook on behalf of the entity.
    *
    * @param string $hook
-   *   One of 'presave', 'insert', 'update', 'predelete', 'delete', or
+   *   One of 'create', 'presave', 'insert', 'update', 'predelete', 'delete', or
    *   'revision_delete'.
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity object.

+ 1 - 1
core/lib/Drupal/Core/Entity/Plugin/DataType/EntityReference.php

@@ -16,7 +16,7 @@ use Drupal\Core\TypedData\DataReferenceBase;
  * or the entity ID may be passed.
  *
  * Note that the definition of the referenced entity's type is required, whereas
- * defining referencable entity bundle(s) is optional. A reference defining the
+ * defining referenceable entity bundle(s) is optional. A reference defining the
  * type and bundle of the referenced entity can be created as following:
  * @code
  * $definition = \Drupal\Core\Entity\EntityDefinition::create($entity_type)

+ 1 - 1
core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php

@@ -518,7 +518,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
           // Some fields can have more then one columns in the data table so
           // column names are needed.
           foreach ($data_fields as $data_field) {
-            // \Drupal\Core\Entity\Sql\TableMappingInterface:: getColumNames()
+            // \Drupal\Core\Entity\Sql\TableMappingInterface::getColumnNames()
             // returns an array keyed by property names so remove the keys
             // before array_merge() to avoid losing data with fields having the
             // same columns i.e. value.

+ 21 - 9
core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php

@@ -1174,12 +1174,12 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
    *   The entity type.
    * @param array $schema
    *   The table schema, passed by reference.
-   *
-   * @return array
-   *   A partial schema array for the base table.
    */
   protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
-    $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
+    // Process the schema for the 'id' entity key only if it exists.
+    if ($entity_type->hasKey('id')) {
+      $this->processIdentifierSchema($schema, $entity_type->getKey('id'));
+    }
   }
 
   /**
@@ -1189,12 +1189,12 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
    *   The entity type.
    * @param array $schema
    *   The table schema, passed by reference.
-   *
-   * @return array
-   *   A partial schema array for the base table.
    */
   protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
-    $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
+    // Process the schema for the 'revision' entity key only if it exists.
+    if ($entity_type->hasKey('revision')) {
+      $this->processIdentifierSchema($schema, $entity_type->getKey('revision'));
+    }
   }
 
   /**
@@ -1333,11 +1333,23 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
           // Create field columns.
           $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
           if (!$only_save) {
+            // The entity schema needs to be checked because the field schema is
+            // potentially incomplete.
+            // @todo Fix this in https://www.drupal.org/node/2929120.
+            $entity_schema = $this->getEntitySchema($this->entityType);
             foreach ($schema[$table_name]['fields'] as $name => $specifier) {
+              // Check if the field is part of the primary keys and pass along
+              // this information when adding the field.
+              // @see \Drupal\Core\Database\Schema::addField()
+              $new_keys = [];
+              if (isset($entity_schema[$table_name]['primary key']) && array_intersect($column_names, $entity_schema[$table_name]['primary key'])) {
+                $new_keys = ['primary key' => $entity_schema[$table_name]['primary key']];
+              }
+
               // Check if the field exists because it might already have been
               // created as part of the earlier entity type update event.
               if (!$schema_handler->fieldExists($table_name, $name)) {
-                $schema_handler->addField($table_name, $name, $specifier);
+                $schema_handler->addField($table_name, $name, $specifier, $new_keys);
               }
             }
             if (!empty($schema[$table_name]['indexes'])) {

+ 1 - 4
core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber.php

@@ -60,10 +60,7 @@ class KernelDestructionSubscriber implements EventSubscriberInterface, Container
    *   An array of event listener definitions.
    */
   public static function getSubscribedEvents() {
-    // Run this subscriber after others as those might use services that need
-    // to be terminated as well or run code that needs to run before
-    // termination.
-    $events[KernelEvents::TERMINATE][] = ['onKernelTerminate', -100];
+    $events[KernelEvents::TERMINATE][] = ['onKernelTerminate', 100];
     return $events;
   }
 

+ 17 - 7
core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampAgoFormatter.php

@@ -5,6 +5,7 @@ namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\FormatterBase;
@@ -104,7 +105,7 @@ class TimestampAgoFormatter extends FormatterBase implements ContainerFactoryPlu
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
-    $elements = parent::settingsForm($form, $form_state);
+    $form = parent::settingsForm($form, $form_state);
 
     $form['future_format'] = [
       '#type' => 'textfield',
@@ -120,7 +121,7 @@ class TimestampAgoFormatter extends FormatterBase implements ContainerFactoryPlu
       '#description' => $this->t('Use <em>@interval</em> where you want the formatted interval text to appear.'),
     ];
 
-    $elements['granularity'] = [
+    $form['granularity'] = [
       '#type' => 'number',
       '#title' => $this->t('Granularity'),
       '#description' => $this->t('How many time interval units should be shown in the formatted output.'),
@@ -129,7 +130,7 @@ class TimestampAgoFormatter extends FormatterBase implements ContainerFactoryPlu
       '#max' => 6,
     ];
 
-    return $elements;
+    return $form;
   }
 
   /**
@@ -138,10 +139,19 @@ class TimestampAgoFormatter extends FormatterBase implements ContainerFactoryPlu
   public function settingsSummary() {
     $summary = parent::settingsSummary();
 
-    $future_date = strtotime('1 year 1 month 1 week 1 day 1 hour 1 minute');
-    $past_date = strtotime('-1 year -1 month -1 week -1 day -1 hour -1 minute');
-    $summary[] = $this->t('Future date: %display', ['%display' => $this->formatTimestamp($future_date)]);
-    $summary[] = $this->t('Past date: %display', ['%display' => $this->formatTimestamp($past_date)]);
+    $future_date = new DrupalDateTime('1 year 1 month 1 week 1 day 1 hour 1 minute');
+    $past_date = new DrupalDateTime('-1 year -1 month -1 week -1 day -1 hour -1 minute');
+    $granularity = $this->getSetting('granularity');
+    $options = [
+      'granularity' => $granularity,
+      'return_as_object' => FALSE,
+    ];
+
+    $future_date_interval = new FormattableMarkup($this->getSetting('future_format'), ['@interval' => $this->dateFormatter->formatTimeDiffUntil($future_date->getTimestamp(), $options)]);
+    $past_date_interval = new FormattableMarkup($this->getSetting('past_format'), ['@interval' => $this->dateFormatter->formatTimeDiffSince($past_date->getTimestamp(), $options)]);
+
+    $summary[] = $this->t('Future date: %display', ['%display' => $future_date_interval]);
+    $summary[] = $this->t('Past date: %display', ['%display' => $past_date_interval]);
 
     return $summary;
   }

+ 4 - 4
core/lib/Drupal/Core/Form/ConfirmFormInterface.php

@@ -10,7 +10,7 @@ interface ConfirmFormInterface extends FormInterface {
   /**
    * Returns the question to ask the user.
    *
-   * @return string
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
    *   The form question. The page title will be set to this value.
    */
   public function getQuestion();
@@ -26,7 +26,7 @@ interface ConfirmFormInterface extends FormInterface {
   /**
    * Returns additional text to display as a description.
    *
-   * @return string
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
    *   The form description.
    */
   public function getDescription();
@@ -34,7 +34,7 @@ interface ConfirmFormInterface extends FormInterface {
   /**
    * Returns a caption for the button that confirms the action.
    *
-   * @return string
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
    *   The form confirmation text.
    */
   public function getConfirmText();
@@ -42,7 +42,7 @@ interface ConfirmFormInterface extends FormInterface {
   /**
    * Returns a caption for the link which cancels the action.
    *
-   * @return string
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
    *   The form cancellation text.
    */
   public function getCancelText();

+ 1 - 1
core/lib/Drupal/Core/Form/FormBuilder.php

@@ -687,7 +687,7 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
       // will be replaced at the very last moment. This ensures forms with
       // dynamically generated action URLs don't have poor cacheability.
       // Use the proper API to generate the placeholder, when we have one. See
-      // https://www.drupal.org/node/2562341. The placholder uses a fixed string
+      // https://www.drupal.org/node/2562341. The placeholder uses a fixed string
       // that is Crypt::hashBase64('Drupal\Core\Form\FormBuilder::prepareForm');
       $placeholder = 'form_action_p_pvdeGsVG5zNF_XLGPTvYSKCf43t8qZYSwcfZl2uzM';
 

+ 2 - 2
core/lib/Drupal/Core/Form/FormState.php

@@ -87,8 +87,8 @@ class FormState implements FormStateInterface {
    * copy of the form is immediately built and sent to the browser, instead of a
    * redirect. This is used for multi-step forms, such as wizards and
    * confirmation forms. Normally, self::$rebuild is set by a submit handler,
-   * since its is usually logic within a submit handler that determines whether
-   * a form is done or requires another step. However, a validation handler may
+   * since it is usually logic within a submit handler that determines whether a
+   * form is done or requires another step. However, a validation handler may
    * already set self::$rebuild to cause the form processing to bypass submit
    * handlers and rebuild the form instead, even if there are no validation
    * errors.

+ 1 - 1
core/lib/Drupal/Core/Http/HandlerStackConfigurator.php

@@ -43,7 +43,7 @@ class HandlerStackConfigurator {
   protected $container;
 
   /**
-   * Contructs a new HandlerStackConfigurator object.
+   * Constructs a new HandlerStackConfigurator object.
    *
    * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
    *   The service container.

+ 1 - 1
core/lib/Drupal/Core/Http/TrustedHostsRequestFactory.php

@@ -44,7 +44,7 @@ class TrustedHostsRequestFactory {
    * @param array $request
    *   (optional) An array of request variables.
    * @param array $attributes
-   *   (optioanl) An array of attributes.
+   *   (optional) An array of attributes.
    * @param array $cookies
    *   (optional) The request cookies ($_COOKIE).
    * @param array $files

+ 1 - 1
core/lib/Drupal/Core/Installer/Exception/InstallProfileMismatchException.php

@@ -19,7 +19,7 @@ class InstallProfileMismatchException extends InstallerException {
    * @param string $settings_profile
    *   The profile in settings.php.
    * @param string $settings_file
-   *   The path to settigns.php.
+   *   The path to settings.php.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    *   The string translation manager.
    *

+ 10 - 0
core/lib/Drupal/Core/Mail/MailManager.php

@@ -2,7 +2,9 @@
 
 namespace Drupal\Core\Mail;
 
+use Drupal\Component\Render\MarkupInterface;
 use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\Messenger\MessengerTrait;
@@ -10,6 +12,7 @@ use Drupal\Core\Plugin\DefaultPluginManager;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Render\Markup;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -277,6 +280,13 @@ class MailManager extends DefaultPluginManager implements MailManagerInterface {
     // Retrieve the responsible implementation for this message.
     $system = $this->getInstance(['module' => $module, 'key' => $key]);
 
+    // Attempt to convert relative URLs to absolute.
+    foreach ($message['body'] as &$body_part) {
+      if ($body_part instanceof MarkupInterface) {
+        $body_part = Markup::create(Html::transformRootRelativeUrlsToAbsolute((string) $body_part, \Drupal::request()->getSchemeAndHttpHost()));
+      }
+    }
+
     // Format the message body.
     $message = $system->format($message);
 

+ 15 - 12
core/lib/Drupal/Core/Menu/LocalActionDefault.php

@@ -83,33 +83,36 @@ class LocalActionDefault extends PluginBase implements LocalActionInterface, Con
    * {@inheritdoc}
    */
   public function getRouteParameters(RouteMatchInterface $route_match) {
-    $parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : [];
+    $route_parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : [];
     $route = $this->routeProvider->getRouteByName($this->getRouteName());
     $variables = $route->compile()->getVariables();
 
     // Normally the \Drupal\Core\ParamConverter\ParamConverterManager has
-    // processed the Request attributes, and in that case the _raw_variables
-    // attribute holds the original path strings keyed to the corresponding
-    // slugs in the path patterns. For example, if the route's path pattern is
+    // run, and the route parameters have been upcast. The original values can
+    // be retrieved from the raw parameters. For example, if the route's path is
     // /filter/tips/{filter_format} and the path is /filter/tips/plain_text then
-    // $raw_variables->get('filter_format') == 'plain_text'.
-    $raw_variables = $route_match->getRawParameters();
+    // $raw_parameters->get('filter_format') == 'plain_text'. Parameters that
+    // are not represented in the route path as slugs might be added by a route
+    // enhancer and will not be present in the raw parameters.
+    $raw_parameters = $route_match->getRawParameters();
+    $parameters = $route_match->getParameters();
 
     foreach ($variables as $name) {
-      if (isset($parameters[$name])) {
+      if (isset($route_parameters[$name])) {
         continue;
       }
 
-      if ($raw_variables && $raw_variables->has($name)) {
-        $parameters[$name] = $raw_variables->get($name);
+      if ($raw_parameters->has($name)) {
+        $route_parameters[$name] = $raw_parameters->get($name);
       }
-      elseif ($value = $route_match->getRawParameter($name)) {
-        $parameters[$name] = $value;
+      elseif ($parameters->has($name)) {
+        $route_parameters[$name] = $parameters->get($name);
       }
     }
+
     // The UrlGenerator will throw an exception if expected parameters are
     // missing. This method should be overridden if that is possible.
-    return $parameters;
+    return $route_parameters;
   }
 
   /**

+ 3 - 3
core/lib/Drupal/Core/Menu/LocalActionManager.php

@@ -196,9 +196,10 @@ class LocalActionManager extends DefaultPluginManager implements LocalActionMana
       }
     }
     $links = [];
+    $cacheability = new CacheableMetadata();
+    $cacheability->addCacheContexts(['route']);
     /** @var $plugin \Drupal\Core\Menu\LocalActionInterface */
     foreach ($this->instances[$route_appears] as $plugin_id => $plugin) {
-      $cacheability = new CacheableMetadata();
       $route_name = $plugin->getRouteName();
       $route_parameters = $plugin->getRouteParameters($this->routeMatch);
       $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE);
@@ -213,9 +214,8 @@ class LocalActionManager extends DefaultPluginManager implements LocalActionMana
         '#weight' => $plugin->getWeight(),
       ];
       $cacheability->addCacheableDependency($access)->addCacheableDependency($plugin);
-      $cacheability->applyTo($links[$plugin_id]);
     }
-    $links['#cache']['contexts'][] = 'route';
+    $cacheability->applyTo($links);
 
     return $links;
   }

+ 15 - 13
core/lib/Drupal/Core/Menu/LocalTaskDefault.php

@@ -41,34 +41,36 @@ class LocalTaskDefault extends PluginBase implements LocalTaskInterface, Cacheab
    * {@inheritdoc}
    */
   public function getRouteParameters(RouteMatchInterface $route_match) {
-    $parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : [];
+    $route_parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : [];
     $route = $this->routeProvider()->getRouteByName($this->getRouteName());
     $variables = $route->compile()->getVariables();
 
     // Normally the \Drupal\Core\ParamConverter\ParamConverterManager has
-    // processed the Request attributes, and in that case the _raw_variables
-    // attribute holds the original path strings keyed to the corresponding
-    // slugs in the path patterns. For example, if the route's path pattern is
+    // run, and the route parameters have been upcast. The original values can
+    // be retrieved from the raw parameters. For example, if the route's path is
     // /filter/tips/{filter_format} and the path is /filter/tips/plain_text then
-    // $raw_variables->get('filter_format') == 'plain_text'.
-
-    $raw_variables = $route_match->getRawParameters();
+    // $raw_parameters->get('filter_format') == 'plain_text'. Parameters that
+    // are not represented in the route path as slugs might be added by a route
+    // enhancer and will not be present in the raw parameters.
+    $raw_parameters = $route_match->getRawParameters();
+    $parameters = $route_match->getParameters();
 
     foreach ($variables as $name) {
-      if (isset($parameters[$name])) {
+      if (isset($route_parameters[$name])) {
         continue;
       }
 
-      if ($raw_variables && $raw_variables->has($name)) {
-        $parameters[$name] = $raw_variables->get($name);
+      if ($raw_parameters->has($name)) {
+        $route_parameters[$name] = $raw_parameters->get($name);
       }
-      elseif ($value = $route_match->getRawParameter($name)) {
-        $parameters[$name] = $value;
+      elseif ($parameters->has($name)) {
+        $route_parameters[$name] = $parameters->get($name);
       }
     }
+
     // The UrlGenerator will throw an exception if expected parameters are
     // missing. This method should be overridden if that is possible.
-    return $parameters;
+    return $route_parameters;
   }
 
   /**

+ 1 - 1
core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php

@@ -30,7 +30,7 @@ interface MenuLinkTreeInterface {
    *
    * Builds menu link tree parameters that:
    * - Expand all links in the active trail based on route being viewed.
-   * - Expand the descendents of the links in the active trail whose
+   * - Expand the descendants of the links in the active trail whose
    *   'expanded' flag is enabled.
    *
    * This only sets the (relatively complex) parameters to achieve the two above

+ 1 - 1
core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php

@@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Request;
  *
  * Do not serve cached pages to authenticated users, or to anonymous users when
  * $_SESSION is non-empty. $_SESSION may contain status messages from a form
- * submission, the contents of a shopping cart, or other userspecific content
+ * submission, the contents of a shopping cart, or other user-specific content
  * that should not be cached and displayed to other users.
  */
 class NoSessionOpen implements RequestPolicyInterface {

+ 8 - 1
core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php

@@ -12,10 +12,17 @@ interface InboundPathProcessorInterface {
   /**
    * Processes the inbound path.
    *
+   * Implementations may make changes to the request object passed in but should
+   * avoid all other side effects. This method can be called to process requests
+   * other than the current request.
+   *
    * @param string $path
    *   The path to process, with a leading slash.
    * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The HttpRequest object representing the current request.
+   *   The HttpRequest object representing the request to process. Note, if this
+   *   method is being called via the path_processor_manager service and is not
+   *   part of routing, the current request object must be cloned before being
+   *   passed in.
    *
    * @return string
    *   The processed path.

+ 2 - 0
core/lib/Drupal/Core/Plugin/Context/Context.php

@@ -6,6 +6,7 @@ use Drupal\Component\Plugin\Context\Context as ComponentContext;
 use Drupal\Component\Plugin\Exception\ContextException;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Core\TypedData\TypedDataTrait;
 
@@ -15,6 +16,7 @@ use Drupal\Core\TypedData\TypedDataTrait;
 class Context extends ComponentContext implements ContextInterface {
 
   use TypedDataTrait;
+  use DependencySerializationTrait;
 
   /**
    * The data associated with the context.

+ 1 - 1
core/lib/Drupal/Core/ProxyBuilder/ProxyBuilder.php

@@ -5,7 +5,7 @@ namespace Drupal\Core\ProxyBuilder;
 use Drupal\Component\ProxyBuilder\ProxyBuilder as BaseProxyBuilder;
 
 /**
- * Extend the component proxy builder by using the DependencySerialziationTrait.
+ * Extend the component proxy builder by using the DependencySerializationTrait.
  */
 class ProxyBuilder extends BaseProxyBuilder {
 

+ 2 - 0
core/lib/Drupal/Core/Render/Element/Email.php

@@ -11,12 +11,14 @@ use Drupal\Core\Render\Element;
  * Properties:
  * - #default_value: An RFC-compliant email address.
  * - #size: The size of the input element in characters.
+ * - #pattern: A string for the native HTML5 pattern attribute.
  *
  * Example usage:
  * @code
  * $form['email'] = array(
  *   '#type' => 'email',
  *   '#title' => $this->t('Email'),
+ *   '#pattern' => '*@example.com',
  * );
  * @end
  *

+ 2 - 0
core/lib/Drupal/Core/Render/Element/Password.php

@@ -10,6 +10,7 @@ use Drupal\Core\Render\Element;
  *
  * Properties:
  * - #size: The size of the input element in characters.
+ * - #pattern: A string for the native HTML5 pattern attribute.
  *
  * Usage example:
  * @code
@@ -17,6 +18,7 @@ use Drupal\Core\Render\Element;
  *   '#type' => 'password',
  *   '#title' => $this->t('Password'),
  *   '#size' => 25,
+ *   '#pattern' => '[01]+',
  * );
  * @endcode
  *

+ 1 - 1
core/lib/Drupal/Core/Render/Element/RenderElement.php

@@ -32,7 +32,7 @@ use Drupal\Core\Url;
  * strings, if they are literals provided by your module, should be
  * internationalized and translated; see the
  * @link i18n Internationalization topic @endlink for more information. Note
- * that although in the properies list that follows, they are designated to be
+ * that although in the properties list that follows, they are designated to be
  * of type string, they would generally end up being
  * \Drupal\Core\StringTranslation\TranslatableMarkup objects instead.
  *

+ 2 - 0
core/lib/Drupal/Core/Render/Element/Tel.php

@@ -12,12 +12,14 @@ use Drupal\Core\Render\Element;
  *
  * Properties:
  * - #size: The size of the input element in characters.
+ * - #pattern: A string for the native HTML5 pattern attribute.
  *
  * Usage example:
  * @code
  * $form['phone'] = array(
  *   '#type' => 'tel',
  *   '#title' => $this->t('Phone'),
+ *   '#pattern' => '[^\d]*',
  * );
  * @endcode
  *

+ 3 - 1
core/lib/Drupal/Core/Render/Element/Textfield.php

@@ -15,6 +15,7 @@ use Drupal\Core\Render\Element;
  *   autocomplete JavaScript library.
  * - #autocomplete_route_parameters: An array of parameters to be used in
  *   conjunction with the route name.
+ * - #pattern: A string for the native HTML5 pattern attribute.
  *
  * Usage example:
  * @code
@@ -24,7 +25,8 @@ use Drupal\Core\Render\Element;
  *   '#default_value' => $node->title,
  *   '#size' => 60,
  *   '#maxlength' => 128,
- * '#required' => TRUE,
+ *   '#pattern' => 'some-prefix-[a-z]+',
+ *   '#required' => TRUE,
  * );
  * @endcode
  *

+ 2 - 0
core/lib/Drupal/Core/Render/Element/Url.php

@@ -12,6 +12,7 @@ use Drupal\Core\Render\Element;
  * Properties:
  * - #default_value: A valid URL string.
  * - #size: The size of the input element in characters.
+ * - #pattern: A string for the native HTML5 pattern attribute.
  *
  * Usage example:
  * @code
@@ -19,6 +20,7 @@ use Drupal\Core\Render\Element;
  *   '#type' => 'url',
  *   '#title' => $this->t('Home Page'),
  *   '#size' => 30,
+ *   '#pattern' => '*.example.com',
  *   ...
  * );
  * @endcode

+ 1 - 1
core/lib/Drupal/Core/Session/SessionConfiguration.php

@@ -106,7 +106,7 @@ class SessionConfiguration implements SessionConfigurationInterface {
    * Return the session cookie domain.
    *
    * The Set-Cookie response header and its domain attribute are defined in RFC
-   * 2109, RFC 2965 and RFC 6265 each one superseeding the previous version.
+   * 2109, RFC 2965 and RFC 6265 each one superseding the previous version.
    *
    * @see http://tools.ietf.org/html/rfc2109
    * @see http://tools.ietf.org/html/rfc2965

+ 2 - 4
core/lib/Drupal/Core/Session/WriteSafeSessionHandlerInterface.php

@@ -14,8 +14,7 @@ interface WriteSafeSessionHandlerInterface {
    * only capable of forcibly disabling that session data is written to storage.
    *
    * @param bool $flag
-   *   TRUE if the session the session is allowed to be written, FALSE
-   *   otherwise.
+   *   TRUE if the session is allowed to be written, FALSE otherwise.
    */
   public function setSessionWritable($flag);
 
@@ -23,8 +22,7 @@ interface WriteSafeSessionHandlerInterface {
    * Returns whether or not a session may be written to storage.
    *
    * @return bool
-   *   TRUE if the session the session is allowed to be written, FALSE
-   *   otherwise.
+   *   TRUE if the session is allowed to be written, FALSE otherwise.
    */
   public function isSessionWritable();
 

+ 41 - 29
core/lib/Drupal/Core/State/State.php

@@ -2,15 +2,12 @@
 
 namespace Drupal\Core\State;
 
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Cache\CacheCollector;
 use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
-use Drupal\Core\Lock\LockBackendInterface;
 
 /**
  * Provides the state system using a key value store.
  */
-class State extends CacheCollector implements StateInterface {
+class State implements StateInterface {
 
   /**
    * The key value store to use.
@@ -19,18 +16,20 @@ class State extends CacheCollector implements StateInterface {
    */
   protected $keyValueStore;
 
+  /**
+   * Static state cache.
+   *
+   * @var array
+   */
+  protected $cache = [];
+
   /**
    * Constructs a State object.
    *
    * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
    *   The key value store to use.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
-   *   The cache backend.
-   * @param \Drupal\Core\Lock\LockBackendInterface $lock
-   *   The lock backend.
    */
-  public function __construct(KeyValueFactoryInterface $key_value_factory, CacheBackendInterface $cache, LockBackendInterface $lock) {
-    parent::__construct('state', $cache, $lock);
+  public function __construct(KeyValueFactoryInterface $key_value_factory) {
     $this->keyValueStore = $key_value_factory->get('state');
   }
 
@@ -38,18 +37,8 @@ class State extends CacheCollector implements StateInterface {
    * {@inheritdoc}
    */
   public function get($key, $default = NULL) {
-    $value = parent::get($key);
-    return $value !== NULL ? $value : $default;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function resolveCacheMiss($key) {
-    $value = $this->keyValueStore->get($key);
-    $this->storage[$key] = $value;
-    $this->persist($key);
-    return $value;
+    $values = $this->getMultiple([$key]);
+    return isset($values[$key]) ? $values[$key] : $default;
   }
 
   /**
@@ -57,9 +46,33 @@ class State extends CacheCollector implements StateInterface {
    */
   public function getMultiple(array $keys) {
     $values = [];
+    $load = [];
     foreach ($keys as $key) {
-      $values[$key] = $this->get($key);
+      // Check if we have a value in the cache.
+      if (isset($this->cache[$key])) {
+        $values[$key] = $this->cache[$key];
+      }
+      // Load the value if we don't have an explicit NULL value.
+      elseif (!array_key_exists($key, $this->cache)) {
+        $load[] = $key;
+      }
     }
+
+    if ($load) {
+      $loaded_values = $this->keyValueStore->getMultiple($load);
+      foreach ($load as $key) {
+        // If we find a value, even one that is NULL, add it to the cache and
+        // return it.
+        if (isset($loaded_values[$key]) || array_key_exists($key, $loaded_values)) {
+          $values[$key] = $loaded_values[$key];
+          $this->cache[$key] = $loaded_values[$key];
+        }
+        else {
+          $this->cache[$key] = NULL;
+        }
+      }
+    }
+
     return $values;
   }
 
@@ -67,7 +80,7 @@ class State extends CacheCollector implements StateInterface {
    * {@inheritdoc}
    */
   public function set($key, $value) {
-    parent::set($key, $value);
+    $this->cache[$key] = $value;
     $this->keyValueStore->set($key, $value);
   }
 
@@ -76,7 +89,7 @@ class State extends CacheCollector implements StateInterface {
    */
   public function setMultiple(array $data) {
     foreach ($data as $key => $value) {
-      parent::set($key, $value);
+      $this->cache[$key] = $value;
     }
     $this->keyValueStore->setMultiple($data);
   }
@@ -85,8 +98,7 @@ class State extends CacheCollector implements StateInterface {
    * {@inheritdoc}
    */
   public function delete($key) {
-    parent::delete($key);
-    $this->keyValueStore->delete($key);
+    $this->deleteMultiple([$key]);
   }
 
   /**
@@ -94,7 +106,7 @@ class State extends CacheCollector implements StateInterface {
    */
   public function deleteMultiple(array $keys) {
     foreach ($keys as $key) {
-      parent::delete($key);
+      unset($this->cache[$key]);
     }
     $this->keyValueStore->deleteMultiple($keys);
   }
@@ -103,7 +115,7 @@ class State extends CacheCollector implements StateInterface {
    * {@inheritdoc}
    */
   public function resetCache() {
-    $this->clear();
+    $this->cache = [];
   }
 
 }

+ 1 - 1
core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php

@@ -82,7 +82,7 @@ interface StreamWrapperInterface extends PhpStreamWrapperInterface {
   const READ_VISIBLE = 0x0014;
 
   /**
-   * This is the default 'type' falg. This does not include
+   * This is the default 'type' flag. This does not include
    * StreamWrapperInterface::LOCAL, because PHP grants a greater trust level to
    * local files (for example, they can be used in an "include" statement,
    * regardless of the "allow_url_include" setting), so stream wrappers need to

+ 29 - 1
core/lib/Drupal/Core/TempStore/PrivateTempStore.php

@@ -123,6 +123,7 @@ class PrivateTempStore {
     if ($this->currentUser->isAnonymous()) {
       // @todo when https://www.drupal.org/node/2865991 is resolved, use force
       //   start session API rather than setting an arbitrary value directly.
+      $this->startSession();
       $this->requestStack
         ->getCurrentRequest()
         ->getSession()
@@ -219,7 +220,34 @@ class PrivateTempStore {
    *   The owner.
    */
   protected function getOwner() {
-    return $this->currentUser->id() ?: $this->requestStack->getCurrentRequest()->getSession()->getId();
+    $owner = $this->currentUser->id();
+    if ($this->currentUser->isAnonymous()) {
+      $this->startSession();
+      $owner = $this->requestStack->getCurrentRequest()->getSession()->getId();
+    }
+    return $owner;
+  }
+
+  /**
+   * Start session because it is required for a private temp store.
+   *
+   * Ensures that an anonymous user has a session created for them, as
+   * otherwise subsequent page loads will not be able to retrieve their
+   * tempstore data.
+   *
+   * @todo when https://www.drupal.org/node/2865991 is resolved, use force
+   * start session API.
+   */
+  protected function startSession() {
+    $has_session = $this->requestStack
+      ->getCurrentRequest()
+      ->hasSession();
+    if (!$has_session) {
+      /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */
+      $session = \Drupal::service('session');
+      $this->requestStack->getCurrentRequest()->setSession($session);
+      $session->start();
+    }
   }
 
 }

+ 1 - 1
core/lib/Drupal/Core/TypedData/TypedDataManager.php

@@ -188,7 +188,7 @@ class TypedDataManager extends DefaultPluginManager implements TypedDataManagerI
         throw new \InvalidArgumentException("Property $property_name is unknown.");
       }
       // Create the prototype without any value, but with initial parenting
-      // so that constructors can set up the objects correclty.
+      // so that constructors can set up the objects correctly.
       $this->prototypes[$key] = $this->create($definition, NULL, $property_name, $object);
     }
 

+ 1 - 1
core/lib/Drupal/Core/Update/UpdateRegistry.php

@@ -197,7 +197,7 @@ class UpdateRegistry {
   }
 
   /**
-   * Registers that update fucntions got executed.
+   * Registers that update functions were executed.
    *
    * @param string[] $function_names
    *   The executed update functions.

+ 1 - 1
core/lib/Drupal/Core/Updater/Module.php

@@ -105,7 +105,7 @@ class Module extends Updater implements UpdaterInterface {
    * {@inheritdoc}
    */
   public function postInstallTasks() {
-    // Since this is being called outsite of the primary front controller,
+    // Since this is being called outside of the primary front controller,
     // the base_url needs to be set explicitly to ensure that links are
     // relative to the site root.
     // @todo Simplify with https://www.drupal.org/node/2548095

+ 1 - 1
core/lib/Drupal/Core/Updater/Theme.php

@@ -87,7 +87,7 @@ class Theme extends Updater implements UpdaterInterface {
    * {@inheritdoc}
    */
   public function postInstallTasks() {
-    // Since this is being called outsite of the primary front controller,
+    // Since this is being called outside of the primary front controller,
     // the base_url needs to be set explicitly to ensure that links are
     // relative to the site root.
     // @todo Simplify with https://www.drupal.org/node/2548095

+ 0 - 1
core/lib/Drupal/Core/Utility/UnroutedUrlAssembler.php

@@ -78,7 +78,6 @@ class UnroutedUrlAssembler implements UnroutedUrlAssemblerInterface {
     $options += ['query' => []];
 
     $options['query'] = NestedArray::mergeDeep($parsed['query'], $options['query']);
-    ksort($options['query']);
 
     if ($parsed['fragment'] && !$options['fragment']) {
       $options['fragment'] = '#' . $parsed['fragment'];

+ 1 - 1
core/misc/announce.es6.js

@@ -28,7 +28,7 @@
    * @type {Drupal~behavior}
    *
    * @prop {Drupal~behaviorAttach} attach
-   *   Attaches the behavior for drupalAnnouce.
+   *   Attaches the behavior for drupalAnnounce.
    */
   Drupal.behaviors.drupalAnnounce = {
     attach(context) {

+ 1 - 1
core/misc/autocomplete.es6.js

@@ -218,7 +218,7 @@
         .find('input.form-autocomplete')
         .once('autocomplete');
       if ($autocomplete.length) {
-        // Allow options to be overriden per instance.
+        // Allow options to be overridden per instance.
         const blacklist = $autocomplete.attr(
           'data-autocomplete-first-character-blacklist',
         );

+ 1 - 1
core/misc/normalize-fixes.css

@@ -6,7 +6,7 @@
 /**
  * Fix problem with details/summary lines missing the drop arrows.
  */
-@-moz-document url-prefix() {
+@media (min--moz-device-pixel-ratio: 0) {
   summary {
     display: list-item;
   }

+ 38 - 12
core/misc/states.es6.js

@@ -59,6 +59,30 @@
     return typeof a === 'undefined' || typeof b === 'undefined';
   }
 
+  /**
+   * Bitwise AND with a third undefined state.
+   *
+   * @function Drupal.states~ternary
+   *
+   * @param {*} a
+   *   Value a.
+   * @param {*} b
+   *   Value b
+   *
+   * @return {bool}
+   *   The result.
+   */
+  function ternary(a, b) {
+    if (typeof a === 'undefined') {
+      return b;
+    }
+    if (typeof b === 'undefined') {
+      return a;
+    }
+
+    return a && b;
+  }
+
   /**
    * Attaches the states.
    *
@@ -305,18 +329,20 @@
       // bogus, we don't want to end up with an infinite loop.
       else if ($.isPlainObject(constraints)) {
         // This constraint is an object (AND).
-        result = Object.keys(constraints).every(constraint => {
-          const check = this.checkConstraints(
-            constraints[constraint],
-            selector,
-            constraint,
-          );
-          /**
-           * The checkConstraints() function's return value can be undefined. If
-           * this so, consider it to have returned true.
-           */
-          return typeof check === 'undefined' ? true : check;
-        });
+        // eslint-disable-next-line no-restricted-syntax
+        for (const n in constraints) {
+          if (constraints.hasOwnProperty(n)) {
+            result = ternary(
+              result,
+              this.checkConstraints(constraints[n], selector, n),
+            );
+            // False and anything else will evaluate to false, so return when
+            // any false condition is found.
+            if (result === false) {
+              return false;
+            }
+          }
+        }
       }
       return result;
     },

+ 21 - 8
core/misc/states.js

@@ -24,6 +24,17 @@
     return typeof a === 'undefined' || typeof b === 'undefined';
   }
 
+  function ternary(a, b) {
+    if (typeof a === 'undefined') {
+      return b;
+    }
+    if (typeof b === 'undefined') {
+      return a;
+    }
+
+    return a && b;
+  }
+
   Drupal.behaviors.states = {
     attach: function attach(context, settings) {
       var $states = $(context).find('[data-drupal-states]');
@@ -127,8 +138,6 @@
       }
     },
     verifyConstraints: function verifyConstraints(constraints, selector) {
-      var _this3 = this;
-
       var result = void 0;
       if ($.isArray(constraints)) {
         var hasXor = $.inArray('xor', constraints) === -1;
@@ -144,11 +153,15 @@
           }
         }
       } else if ($.isPlainObject(constraints)) {
-          result = Object.keys(constraints).every(function (constraint) {
-            var check = _this3.checkConstraints(constraints[constraint], selector, constraint);
+          for (var n in constraints) {
+            if (constraints.hasOwnProperty(n)) {
+              result = ternary(result, this.checkConstraints(constraints[n], selector, n));
 
-            return typeof check === 'undefined' ? true : check;
-          });
+              if (result === false) {
+                return false;
+              }
+            }
+          }
         }
       return result;
     },
@@ -197,7 +210,7 @@
 
   states.Trigger.prototype = {
     initialize: function initialize() {
-      var _this4 = this;
+      var _this3 = this;
 
       var trigger = states.Trigger.states[this.state];
 
@@ -205,7 +218,7 @@
         trigger.call(window, this.element);
       } else {
         Object.keys(trigger || {}).forEach(function (event) {
-          _this4.defaultTrigger(event, trigger[event]);
+          _this3.defaultTrigger(event, trigger[event]);
         });
       }
 

+ 1 - 1
core/modules/action/src/Plugin/Action/GotoAction.php

@@ -40,7 +40,7 @@ class GotoAction extends ConfigurableActionBase implements ContainerFactoryPlugi
   protected $unroutedUrlAssembler;
 
   /**
-   * Constructs a new DeleteNode object.
+   * Constructs a GotoAction object.
    *
    * @param array $configuration
    *   A configuration array containing information about the plugin instance.

+ 1 - 1
core/modules/aggregator/src/ItemsImporter.php

@@ -94,7 +94,7 @@ class ItemsImporter implements ItemsImporterInterface {
       watchdog_exception('aggregator', $e);
     }
 
-    // Store instances in an array so we dont have to instantiate new objects.
+    // Store instances in an array so we don't have to instantiate new objects.
     $processor_instances = [];
     foreach ($this->config->get('processors') as $processor) {
       try {

+ 1 - 1
core/modules/ban/src/Plugin/migrate/destination/BlockedIp.php

@@ -33,7 +33,7 @@ class BlockedIP extends DestinationBase implements ContainerFactoryPluginInterfa
    * @param string $plugin_id
    *   The plugin ID.
    * @param mixed $plugin_definition
-   *   The plugin definiiton.
+   *   The plugin definition.
    * @param \Drupal\migrate\Plugin\MigrationInterface $migration
    *   The current migration.
    * @param \Drupal\ban\BanIpManagerInterface $ban_manager

+ 5 - 91
core/modules/block_content/src/Plugin/migrate/source/d6/BoxTranslation.php

@@ -2,8 +2,7 @@
 
 namespace Drupal\block_content\Plugin\migrate\source\d6;
 
-use Drupal\migrate\Row;
-use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
+use Drupal\block_content\Plugin\migrate\source\d7\BlockCustomTranslation as D7BlockCustomTranslation;
 
 /**
  * Gets Drupal 6 i18n custom block translations from database.
@@ -13,97 +12,12 @@ use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
  *   source_module = "i18nblocks"
  * )
  */
-class BoxTranslation extends DrupalSqlBase {
+class BoxTranslation extends D7BlockCustomTranslation {
 
   /**
-   * {@inheritdoc}
+   * Drupal 6 table names.
    */
-  public function query() {
-    // Build a query based on i18n_strings table where each row has the
-    // translation for only one property, either title or description. The
-    // method prepareRow() is then used to obtain the translation for the
-    // other property.
-    $query = $this->select('boxes', 'b')
-      ->fields('b', ['bid', 'format', 'body'])
-      ->fields('i18n', ['property'])
-      ->fields('lt', ['lid', 'translation', 'language'])
-      ->orderBy('b.bid')
-      ->isNotNull('lt.lid');
-
-    // Use 'title' for the info field to match the property name in the
-    // i18n_strings table.
-    $query->addField('b', 'info', 'title');
-
-    // Add in the property, which is either title or body. Cast the bid to text
-    // so PostgreSQL can make the join.
-    $query->leftJoin('i18n_strings', 'i18n', 'i18n.objectid = CAST(b.bid as CHAR(255))');
-    $query->condition('i18n.type', 'block');
-
-    // Add in the translation for the property.
-    $query->leftJoin('locales_target', 'lt', 'lt.lid = i18n.lid');
-    return $query;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function prepareRow(Row $row) {
-    $language = $row->getSourceProperty('language');
-    $bid = $row->getSourceProperty('bid');
-
-    // If this row has been migrated it is a duplicate then skip it.
-    if ($this->idMap->lookupDestinationIds(['bid' => $bid, 'language' => $language])) {
-      return FALSE;
-    }
-
-    // Save the translation for this property.
-    $property = $row->getSourceProperty('property');
-    $row->setSourceProperty($property . '_translated', $row->getSourceProperty('translation'));
-
-    // Get the translation for the property not already in the row.
-    $translation = ($property === 'title') ? 'body' : 'title';
-    $query = $this->select('i18n_strings', 'i18n')
-      ->fields('i18n', ['lid'])
-      ->condition('i18n.property', $translation)
-      ->condition('i18n.objectid', $bid);
-    $query->leftJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
-    $query->condition('lt.language', $language)
-      ->addField('lt', 'translation');
-    $results = $query->execute()->fetchAssoc();
-    if (!$results) {
-      $row->setSourceProperty($translation . '_translated', NULL);
-    }
-    else {
-      $row->setSourceProperty($translation . '_translated', $results['translation']);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function fields() {
-    return [
-      'bid' => $this->t('The block numeric identifier.'),
-      'format' => $this->t('Input format of the custom block/box content.'),
-      'lid' => $this->t('i18n_string table id'),
-      'language' => $this->t('Language for this field.'),
-      'property' => $this->t('Block property'),
-      'translation' => $this->t('The translation of the value of "property".'),
-      'title' => $this->t('Block title.'),
-      'title_translated' => $this->t('Block title translation.'),
-      'body' => $this->t('Block body.'),
-      'body_translated' => $this->t('Block body translation.'),
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIds() {
-    $ids['bid']['type'] = 'integer';
-    $ids['bid']['alias'] = 'b';
-    $ids['language']['type'] = 'string';
-    return $ids;
-  }
+  const CUSTOM_BLOCK_TABLE = 'boxes';
+  const I18N_STRING_TABLE = 'i18n_strings';
 
 }

+ 99 - 0
core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\block_content\Plugin\migrate\source\d7;
+
+use Drupal\migrate\Row;
+use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
+use Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait;
+
+/**
+ * Gets Drupal 7 custom block translation from database.
+ *
+ * @MigrateSource(
+ *   id = "d7_block_custom_translation",
+ *   source_module = "block"
+ * )
+ */
+class BlockCustomTranslation extends DrupalSqlBase {
+
+  use I18nQueryTrait;
+
+  /**
+   * Drupal 7 table names.
+   */
+  const CUSTOM_BLOCK_TABLE = 'block_custom';
+  const I18N_STRING_TABLE = 'i18n_string';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    // Build a query based on blockCustomTable table where each row has the
+    // translation for only one property, either title or description. The
+    // method prepareRow() is then used to obtain the translation for the
+    // other property.
+    $query = $this->select(static::CUSTOM_BLOCK_TABLE, 'b')
+      ->fields('b', ['bid', 'format', 'body'])
+      ->fields('i18n', ['property'])
+      ->fields('lt', ['lid', 'translation', 'language'])
+      ->orderBy('b.bid')
+      ->isNotNull('lt.lid');
+
+    // Use 'title' for the info field to match the property name in
+    // i18nStringTable.
+    $query->addField('b', 'info', 'title');
+
+    // Add in the property, which is either title or body. Cast the bid to text
+    // so PostgreSQL can make the join.
+    $query->leftJoin(static::I18N_STRING_TABLE, 'i18n', 'i18n.objectid = CAST(b.bid as CHAR(255))');
+    $query->condition('i18n.type', 'block');
+
+    // Add in the translation for the property.
+    $query->leftJoin('locales_target', 'lt', 'lt.lid = i18n.lid');
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareRow(Row $row) {
+    parent::prepareRow($row);
+    // Set the i18n string table for use in I18nQueryTrait.
+    $this->i18nStringTable = static::I18N_STRING_TABLE;
+    // Save the translation for this property.
+    $property_in_row = $row->getSourceProperty('property');
+    // Get the translation for the property not already in the row and save it
+    // in the row.
+    $property_not_in_row = ($property_in_row === 'title') ? 'body' : 'title';
+    return $this->getPropertyNotInRowTranslation($row, $property_not_in_row, 'bid', $this->idMap);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    return [
+      'bid' => $this->t('The block numeric identifier.'),
+      'format' => $this->t('Input format of the custom block/box content.'),
+      'lid' => $this->t('i18n_string table id'),
+      'language' => $this->t('Language for this field.'),
+      'property' => $this->t('Block property'),
+      'translation' => $this->t('The translation of the value of "property".'),
+      'title' => $this->t('Block title.'),
+      'title_translated' => $this->t('Block title translation.'),
+      'body' => $this->t('Block body.'),
+      'body_translated' => $this->t('Block body translation.'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids['bid']['type'] = 'integer';
+    $ids['bid']['alias'] = 'b';
+    $ids['language']['type'] = 'string';
+    return $ids;
+  }
+
+}

+ 69 - 0
core/modules/block_content/tests/src/Kernel/Migrate/d7/MigrateCustomBlockContentTranslationTest.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Tests\block_content\Kernel\Migrate\d7;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
+
+/**
+ * Tests migration of i18n custom block strings.
+ *
+ * @group migrate_drupal_7
+ */
+class MigrateCustomBlockContentTranslationTest extends MigrateDrupal7TestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'block_content',
+    'content_translation',
+    'filter',
+    'language',
+    'text',
+    // Required for translation migrations.
+    'migrate_drupal_multilingual',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installConfig(['block_content']);
+    $this->installEntitySchema('block_content');
+    $this->executeMigrations([
+      'language',
+      'd7_filter_format',
+      'block_content_type',
+      'block_content_body_field',
+      'd7_custom_block',
+      'd7_custom_block_translation',
+    ]);
+  }
+
+  /**
+   * Tests the Drupal 7 i18n custom block strings to Drupal 8 migration.
+   */
+  public function testCustomBlockContentTranslation() {
+    /** @var \Drupal\block_content\Entity\BlockContent $block */
+    $block = BlockContent::load(1)->getTranslation('fr');
+    $this->assertSame('fr - Mildly amusing limerick of the day', $block->label());
+    $this->assertGreaterThanOrEqual($block->getChangedTime(), \Drupal::time()->getRequestTime());
+    $this->assertLessThanOrEqual(time(), $block->getChangedTime());
+    $this->assertSame('fr', $block->language()->getId());
+    $translation = "fr - A fellow jumped off a high wall\r\nAnd had a most terrible fall\r\nHe went back to bed\r\nWith a bump on his head\r\nThat's why you don't jump off a wall";
+    $this->assertSame($translation, $block->body->value);
+    $this->assertSame('filtered_html', $block->body->format);
+
+    $block = $block->getTranslation('is');
+    $this->assertSame('is - Mildly amusing limerick of the day', $block->label());
+    $this->assertGreaterThanOrEqual($block->getChangedTime(), \Drupal::time()->getRequestTime());
+    $this->assertLessThanOrEqual(time(), $block->getChangedTime());
+    $this->assertSame('is', $block->language()->getId());
+    $text = "A fellow jumped off a high wall\r\nAnd had a most terrible fall\r\nHe went back to bed\r\nWith a bump on his head\r\nThat's why you don't jump off a wall";
+    $this->assertSame($text, $block->body->value);
+    $this->assertSame('filtered_html', $block->body->format);
+  }
+
+}

+ 148 - 0
core/modules/block_content/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTranslationTest.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\Tests\block_content\Kernel\Plugin\migrate\source\d7;
+
+use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
+
+/**
+ * Tests i18n custom block translations source plugin.
+ *
+ * @covers \Drupal\block_content\Plugin\migrate\source\d7\BlockCustomTranslation
+ *
+ * @group content_translation
+ */
+class BlockCustomTranslationTest extends MigrateSqlSourceTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['block_content', 'migrate_drupal'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function providerSource() {
+    $tests = [];
+
+    // The source data.
+    $tests[0]['database']['block_custom'] = [
+      [
+        'bid' => 1,
+        'body' => 'box 1 body',
+        'info' => 'box 1 title',
+        'format' => '2',
+      ],
+      [
+        'bid' => 2,
+        'body' => 'box 2 body',
+        'info' => 'box 2 title',
+        'format' => '2',
+      ],
+    ];
+
+    $tests[0]['database']['i18n_string'] = [
+      [
+        'lid' => 1,
+        'objectid' => 1,
+        'type' => 'block',
+        'property' => 'title',
+        'objectindex' => 1,
+        'format' => 0,
+      ],
+      [
+        'lid' => 2,
+        'objectid' => 1,
+        'type' => 'block',
+        'property' => 'body',
+        'objectindex' => 1,
+        'format' => 0,
+      ],
+      [
+        'lid' => 3,
+        'objectid' => 2,
+        'type' => 'block',
+        'property' => 'body',
+        'objectindex' => 2,
+        'format' => 2,
+      ],
+    ];
+
+    $tests[0]['database']['locales_target'] = [
+      [
+        'lid' => 1,
+        'language' => 'fr',
+        'translation' => 'fr - title translation',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+      [
+        'lid' => 2,
+        'language' => 'fr',
+        'translation' => 'fr - body translation',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+      [
+        'lid' => 3,
+        'language' => 'zu',
+        'translation' => 'zu - body translation',
+        'plid' => 0,
+        'plural' => 0,
+        'i18n_status' => 0,
+      ],
+    ];
+
+    $tests[0]['database']['system'] = [
+      [
+        'type' => 'module',
+        'name' => 'system',
+        'schema_version' => '7001',
+        'status' => '1',
+      ],
+    ];
+
+    $tests[0]['expected_results'] = [
+      [
+        'lid' => '1',
+        'property' => 'title',
+        'language' => 'fr',
+        'translation' => 'fr - title translation',
+        'bid' => '1',
+        'format' => '2',
+        'title_translated' => 'fr - title translation',
+        'body_translated' => 'fr - body translation',
+        'title' => 'box 1 title',
+        'body' => 'box 1 body',
+      ],
+      [
+        'lid' => '2',
+        'property' => 'body',
+        'language' => 'fr',
+        'translation' => 'fr - body translation',
+        'bid' => '1',
+        'format' => '2',
+        'title_translated' => 'fr - title translation',
+        'body_translated' => 'fr - body translation',
+        'title' => 'box 1 title',
+        'body' => 'box 1 body',
+      ],
+      [
+        'lid' => '3',
+        'property' => 'body',
+        'language' => 'zu',
+        'translation' => 'zu - body translation',
+        'bid' => '2',
+        'format' => '2',
+        'title_translated' => NULL,
+        'body_translated' => 'zu - body translation',
+        'title' => 'box 2 title',
+        'body' => 'box 2 body',
+      ],
+    ];
+
+    return $tests;
+  }
+
+}

Some files were not shown because too many files changed in this diff