diff --git a/.gitignore b/.gitignore index 25a11e1df..66de35ab4 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ Thumbs.db # SASS # ########## .sass-cache + +# Things in the core directory that Drupal 8 commits in the repository. +!core/**/*.gz + diff --git a/.htaccess b/.htaccess index b26c63bb5..01c63af98 100644 --- a/.htaccess +++ b/.htaccess @@ -15,9 +15,6 @@ # Don't show directory listings for URLs which map to a directory. Options -Indexes -# Follow symbolic links in this directory. -Options +FollowSymLinks - # Set the default handler. DirectoryIndex index.php index.html index.htm diff --git a/composer.json b/composer.json index 04e1806ee..7f170ce71 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "GPL-2.0+", "require": { "composer/installers": "^1.0.21", - "wikimedia/composer-merge-plugin": "^1.3.0" + "wikimedia/composer-merge-plugin": "~1.3" }, "replace": { "drupal/core": "~8.0" diff --git a/composer.lock b/composer.lock index 58be85866..32535367b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "2be29019515c847055593ea41b88475d", - "content-hash": "f38613812a285c03a1a18458384fe0b1", + "hash": "dac77f10c1f7585fd1f7344c6a376338", + "content-hash": "73cbcb262208c5d802cb528279f2a95c", "packages": [ { "name": "composer/installers", @@ -1107,24 +1107,23 @@ }, { "name": "symfony/class-loader", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "d957ea6295d7016e20d7eff33a6c1deef819c0d4" + "reference": "320f8d2a9cdbcbeb24be602c124aae9d998474a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/d957ea6295d7016e20d7eff33a6c1deef819c0d4", - "reference": "d957ea6295d7016e20d7eff33a6c1deef819c0d4", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/320f8d2a9cdbcbeb24be602c124aae9d998474a4", + "reference": "320f8d2a9cdbcbeb24be602c124aae9d998474a4", "shasum": "" }, "require": { "php": ">=5.3.9" }, "require-dev": { - "symfony/finder": "~2.0,>=2.0.5", - "symfony/phpunit-bridge": "~2.7" + "symfony/finder": "~2.0,>=2.0.5" }, "type": "library", "extra": { @@ -1153,20 +1152,20 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2015-08-26 17:56:37" + "time": "2015-10-23 14:47:27" }, { "name": "symfony/console", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "06cb17c013a82f94a3d840682b49425cd00a2161" + "reference": "5efd632294c8320ea52492db22292ff853a43766" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/06cb17c013a82f94a3d840682b49425cd00a2161", - "reference": "06cb17c013a82f94a3d840682b49425cd00a2161", + "url": "https://api.github.com/repos/symfony/console/zipball/5efd632294c8320ea52492db22292ff853a43766", + "reference": "5efd632294c8320ea52492db22292ff853a43766", "shasum": "" }, "require": { @@ -1175,7 +1174,6 @@ "require-dev": { "psr/log": "~1.0", "symfony/event-dispatcher": "~2.1", - "symfony/phpunit-bridge": "~2.7", "symfony/process": "~2.1" }, "suggest": { @@ -1210,20 +1208,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2015-09-25 08:32:23" + "time": "2015-10-20 14:38:46" }, { "name": "symfony/debug", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "c79c361bca8e5ada6a47603875a3c964d03b67b1" + "reference": "fb9e6887db716939f41af0ba8ef38a1582eb501b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/c79c361bca8e5ada6a47603875a3c964d03b67b1", - "reference": "c79c361bca8e5ada6a47603875a3c964d03b67b1", + "url": "https://api.github.com/repos/symfony/debug/zipball/fb9e6887db716939f41af0ba8ef38a1582eb501b", + "reference": "fb9e6887db716939f41af0ba8ef38a1582eb501b", "shasum": "" }, "require": { @@ -1235,8 +1233,7 @@ }, "require-dev": { "symfony/class-loader": "~2.2", - "symfony/http-kernel": "~2.3.24|~2.5.9|~2.6,>=2.6.2", - "symfony/phpunit-bridge": "~2.7" + "symfony/http-kernel": "~2.3.24|~2.5.9|~2.6,>=2.6.2" }, "type": "library", "extra": { @@ -1265,20 +1262,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2015-09-14 08:41:38" + "time": "2015-10-11 09:39:48" }, { "name": "symfony/dependency-injection", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "422c3819b110f610d79c6f1dc38af23787dc790e" + "reference": "af284e795ec8a08c80d1fc47518fd23004b89847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/422c3819b110f610d79c6f1dc38af23787dc790e", - "reference": "422c3819b110f610d79c6f1dc38af23787dc790e", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/af284e795ec8a08c80d1fc47518fd23004b89847", + "reference": "af284e795ec8a08c80d1fc47518fd23004b89847", "shasum": "" }, "require": { @@ -1290,7 +1287,6 @@ "require-dev": { "symfony/config": "~2.2", "symfony/expression-language": "~2.6", - "symfony/phpunit-bridge": "~2.7", "symfony/yaml": "~2.1" }, "suggest": { @@ -1325,20 +1321,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2015-09-15 08:30:42" + "time": "2015-10-27 15:38:06" }, { "name": "symfony/event-dispatcher", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9" + "reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", - "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87a5db5ea887763fa3a31a5471b512ff1596d9b8", + "reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8", "shasum": "" }, "require": { @@ -1349,7 +1345,6 @@ "symfony/config": "~2.0,>=2.0.5", "symfony/dependency-injection": "~2.6", "symfony/expression-language": "~2.6", - "symfony/phpunit-bridge": "~2.7", "symfony/stopwatch": "~2.3" }, "suggest": { @@ -1383,28 +1378,27 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-09-22 13:49:29" + "time": "2015-10-11 09:39:48" }, { "name": "symfony/http-foundation", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e1509119f164a0d0a940d7d924d693a7a28a5470" + "reference": "7598eea151ae3d4134df1f9957364b17809eea75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e1509119f164a0d0a940d7d924d693a7a28a5470", - "reference": "e1509119f164a0d0a940d7d924d693a7a28a5470", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7598eea151ae3d4134df1f9957364b17809eea75", + "reference": "7598eea151ae3d4134df1f9957364b17809eea75", "shasum": "" }, "require": { "php": ">=5.3.9" }, "require-dev": { - "symfony/expression-language": "~2.4", - "symfony/phpunit-bridge": "~2.7" + "symfony/expression-language": "~2.4" }, "type": "library", "extra": { @@ -1436,20 +1430,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2015-09-22 13:49:29" + "time": "2015-10-23 14:47:27" }, { "name": "symfony/http-kernel", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "353aa457424262d7d4e4289ea483145921cffcb5" + "reference": "4260f2273a446a6715063dc9ca89fd0c475c2f77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/353aa457424262d7d4e4289ea483145921cffcb5", - "reference": "353aa457424262d7d4e4289ea483145921cffcb5", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4260f2273a446a6715063dc9ca89fd0c475c2f77", + "reference": "4260f2273a446a6715063dc9ca89fd0c475c2f77", "shasum": "" }, "require": { @@ -1472,7 +1466,6 @@ "symfony/dom-crawler": "~2.0,>=2.0.5", "symfony/expression-language": "~2.4", "symfony/finder": "~2.0,>=2.0.5", - "symfony/phpunit-bridge": "~2.7", "symfony/process": "~2.0,>=2.0.5", "symfony/routing": "~2.2", "symfony/stopwatch": "~2.3", @@ -1516,28 +1509,25 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2015-09-25 11:16:52" + "time": "2015-10-27 19:07:21" }, { "name": "symfony/process", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "b27c8e317922cd3cdd3600850273cf6b82b2e8e9" + "reference": "4a959dd4e19c2c5d7512689413921e0a74386ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/b27c8e317922cd3cdd3600850273cf6b82b2e8e9", - "reference": "b27c8e317922cd3cdd3600850273cf6b82b2e8e9", + "url": "https://api.github.com/repos/symfony/process/zipball/4a959dd4e19c2c5d7512689413921e0a74386ec7", + "reference": "4a959dd4e19c2c5d7512689413921e0a74386ec7", "shasum": "" }, "require": { "php": ">=5.3.9" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, "type": "library", "extra": { "branch-alias": { @@ -1565,7 +1555,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2015-09-19 19:59:23" + "time": "2015-10-23 14:47:27" }, { "name": "symfony/psr-http-message-bridge", @@ -1623,16 +1613,16 @@ }, { "name": "symfony/routing", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "6c5fae83efa20baf166fcf4582f57094e9f60f16" + "reference": "f353e1f588679c3ec987624e6c617646bd01ba38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/6c5fae83efa20baf166fcf4582f57094e9f60f16", - "reference": "6c5fae83efa20baf166fcf4582f57094e9f60f16", + "url": "https://api.github.com/repos/symfony/routing/zipball/f353e1f588679c3ec987624e6c617646bd01ba38", + "reference": "f353e1f588679c3ec987624e6c617646bd01ba38", "shasum": "" }, "require": { @@ -1648,7 +1638,6 @@ "symfony/config": "~2.7", "symfony/expression-language": "~2.4", "symfony/http-foundation": "~2.3", - "symfony/phpunit-bridge": "~2.7", "symfony/yaml": "~2.0,>=2.0.5" }, "suggest": { @@ -1690,20 +1679,20 @@ "uri", "url" ], - "time": "2015-09-14 14:14:09" + "time": "2015-10-27 15:38:06" }, { "name": "symfony/serializer", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "baf24f86a8656eea9c80988f332e51461bfcb67f" + "reference": "14056684acad23b8815eb336bccc0b4ac76bd823" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/baf24f86a8656eea9c80988f332e51461bfcb67f", - "reference": "baf24f86a8656eea9c80988f332e51461bfcb67f", + "url": "https://api.github.com/repos/symfony/serializer/zipball/14056684acad23b8815eb336bccc0b4ac76bd823", + "reference": "14056684acad23b8815eb336bccc0b4ac76bd823", "shasum": "" }, "require": { @@ -1713,7 +1702,6 @@ "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0", "symfony/config": "~2.2", - "symfony/phpunit-bridge": "~2.7", "symfony/property-access": "~2.3", "symfony/yaml": "~2.0,>=2.0.5" }, @@ -1751,20 +1739,20 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "time": "2015-08-31 16:44:53" + "time": "2015-10-11 09:39:48" }, { "name": "symfony/translation", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "485877661835e188cd78345c6d4eef1290d17571" + "reference": "6ccd9289ec1c71d01a49d83480de3b5293ce30c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/485877661835e188cd78345c6d4eef1290d17571", - "reference": "485877661835e188cd78345c6d4eef1290d17571", + "url": "https://api.github.com/repos/symfony/translation/zipball/6ccd9289ec1c71d01a49d83480de3b5293ce30c8", + "reference": "6ccd9289ec1c71d01a49d83480de3b5293ce30c8", "shasum": "" }, "require": { @@ -1777,7 +1765,6 @@ "psr/log": "~1.0", "symfony/config": "~2.7", "symfony/intl": "~2.4", - "symfony/phpunit-bridge": "~2.7", "symfony/yaml": "~2.2" }, "suggest": { @@ -1812,20 +1799,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2015-09-06 08:36:38" + "time": "2015-10-27 15:38:06" }, { "name": "symfony/validator", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "b359dc71e253ce6eb69eefbd5088032241e7a66f" + "reference": "df9021e689aa3d08367881e7f8917219fabe5e64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/b359dc71e253ce6eb69eefbd5088032241e7a66f", - "reference": "b359dc71e253ce6eb69eefbd5088032241e7a66f", + "url": "https://api.github.com/repos/symfony/validator/zipball/df9021e689aa3d08367881e7f8917219fabe5e64", + "reference": "df9021e689aa3d08367881e7f8917219fabe5e64", "shasum": "" }, "require": { @@ -1835,12 +1822,12 @@ "require-dev": { "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0", + "doctrine/common": "~2.3", "egulias/email-validator": "~1.2,>=1.2.1", "symfony/config": "~2.2", "symfony/expression-language": "~2.4", "symfony/http-foundation": "~2.1", "symfony/intl": "~2.4", - "symfony/phpunit-bridge": "~2.7", "symfony/property-access": "~2.3", "symfony/yaml": "~2.0,>=2.0.5" }, @@ -1882,28 +1869,25 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2015-09-23 11:13:27" + "time": "2015-10-18 20:23:18" }, { "name": "symfony/yaml", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770" + "reference": "eca9019c88fbe250164affd107bc8057771f3f4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/31cb2ad0155c95b88ee55fe12bc7ff92232c1770", - "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770", + "url": "https://api.github.com/repos/symfony/yaml/zipball/eca9019c88fbe250164affd107bc8057771f3f4d", + "reference": "eca9019c88fbe250164affd107bc8057771f3f4d", "shasum": "" }, "require": { "php": ">=5.3.9" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, "type": "library", "extra": { "branch-alias": { @@ -1931,20 +1915,20 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-09-14 14:14:09" + "time": "2015-10-11 09:39:48" }, { "name": "twig/twig", - "version": "v1.22.2", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "79249fc8c9ff62e41e217e0c630e2e00bcadda6a" + "reference": "d9b6333ae8dd2c8e3fd256e127548def0bc614c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/79249fc8c9ff62e41e217e0c630e2e00bcadda6a", - "reference": "79249fc8c9ff62e41e217e0c630e2e00bcadda6a", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/d9b6333ae8dd2c8e3fd256e127548def0bc614c6", + "reference": "d9b6333ae8dd2c8e3fd256e127548def0bc614c6", "shasum": "" }, "require": { @@ -1957,7 +1941,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "1.23-dev" } }, "autoload": { @@ -1992,20 +1976,20 @@ "keywords": [ "templating" ], - "time": "2015-09-22 13:59:32" + "time": "2015-11-05 12:49:06" }, { "name": "wikimedia/composer-merge-plugin", - "version": "dev-master", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/wikimedia/composer-merge-plugin.git", - "reference": "47bb3388cfeae41a38087ac8465a7d08fa92ea2e" + "reference": "bfed1f8d4eb97e9ba80eee57ea46229d7e5364d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/6196fdb001faf681f92db2ae10abafb5815affde", - "reference": "47bb3388cfeae41a38087ac8465a7d08fa92ea2e", + "url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/bfed1f8d4eb97e9ba80eee57ea46229d7e5364d9", + "reference": "bfed1f8d4eb97e9ba80eee57ea46229d7e5364d9", "shasum": "" }, "require": { @@ -2015,8 +1999,7 @@ "require-dev": { "composer/composer": "1.0.*@dev", "jakub-onderka/php-parallel-lint": "~0.8", - "phpspec/prophecy-phpunit": "~1.0", - "phpunit/phpunit": "~4.0", + "phpunit/phpunit": "~4.8|~5.0", "squizlabs/php_codesniffer": "~2.1.0" }, "type": "composer-plugin", @@ -2042,7 +2025,7 @@ } ], "description": "Composer plugin to merge multiple composer.json files", - "time": "2015-09-22 21:14:25" + "time": "2015-11-06 20:31:16" }, { "name": "zendframework/zend-diactoros", @@ -3599,16 +3582,16 @@ }, { "name": "symfony/browser-kit", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4" + "reference": "07d664a052572ccc28eb2ab7dbbe82155b1ad367" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/277a2457776d4cc25706fbdd9d1e4ab2dac884e4", - "reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/07d664a052572ccc28eb2ab7dbbe82155b1ad367", + "reference": "07d664a052572ccc28eb2ab7dbbe82155b1ad367", "shasum": "" }, "require": { @@ -3617,8 +3600,7 @@ }, "require-dev": { "symfony/css-selector": "~2.0,>=2.0.5", - "symfony/phpunit-bridge": "~2.7", - "symfony/process": "~2.0,>=2.0.5" + "symfony/process": "~2.3.34|~2.7,>=2.7.6" }, "suggest": { "symfony/process": "" @@ -3650,28 +3632,25 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2015-09-06 08:36:38" + "time": "2015-10-23 14:47:27" }, { "name": "symfony/css-selector", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "abe19cc0429a06be0c133056d1f9859854860970" + "reference": "e1b865b26be4a56d22a8dee398375044a80c865b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/abe19cc0429a06be0c133056d1f9859854860970", - "reference": "abe19cc0429a06be0c133056d1f9859854860970", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/e1b865b26be4a56d22a8dee398375044a80c865b", + "reference": "e1b865b26be4a56d22a8dee398375044a80c865b", "shasum": "" }, "require": { "php": ">=5.3.9" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, "type": "library", "extra": { "branch-alias": { @@ -3703,28 +3682,27 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2015-09-22 13:49:29" + "time": "2015-10-11 09:39:48" }, { "name": "symfony/dom-crawler", - "version": "v2.7.5", + "version": "v2.7.6", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "2e185ca136399f902b948694987e62c80099c052" + "reference": "5fef7d8b80d8f9992df99d8ee283f420484c9612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2e185ca136399f902b948694987e62c80099c052", - "reference": "2e185ca136399f902b948694987e62c80099c052", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/5fef7d8b80d8f9992df99d8ee283f420484c9612", + "reference": "5fef7d8b80d8f9992df99d8ee283f420484c9612", "shasum": "" }, "require": { "php": ">=5.3.9" }, "require-dev": { - "symfony/css-selector": "~2.3", - "symfony/phpunit-bridge": "~2.7" + "symfony/css-selector": "~2.3" }, "suggest": { "symfony/css-selector": "" @@ -3756,7 +3734,7 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2015-09-20 21:13:58" + "time": "2015-10-11 09:39:48" } ], "aliases": [], diff --git a/core/CHANGELOG.txt b/core/CHANGELOG.txt index df80cfbb8..1c5f8a588 100644 --- a/core/CHANGELOG.txt +++ b/core/CHANGELOG.txt @@ -1,24 +1,69 @@ -Drupal 8.0, xxxx-xx-xx (development version) +Drupal 8.0.x, xxxx-xx-xx (development version) ---------------------- -- Added Twig as the default template engine and converted all .tpl.php templates - to .html.twig. +- Dramatically improved the front end: + * Made all built-in themes responsive. + * Added support for responsive images. + * Added Twig as the default template engine and converted all .tpl.php + templates and theme functions to .html.twig. + * Removed the PHPTemplate engine. + * Several large scale cleanups of the markup produced by Drupal. + * Added Classy as a base theme to maintain CSS classes and wrappers. + * Added Stable as the default base theme to maintain backwards compatibility + for core template and CSS changes, because templates and CSS outside + Stable can be improved in minor releases (8.1.0, 8.2.0 …). + * Redesigned several key elements of the Seven theme. + * Added support for HTML5 elements. + * Included the HTML5 Shiv library to support HTML5 elements in IE 8 and + below. + * Included Backbone.js and Underscore.js JavaScript frameworks. + * Updated to jQuery 2.1.4. + * Updated to jQuery UI 1.11.4. + * Removed jquery.bbq. + * Removed the Garland theme from core. + * Removed the Overlay module from core. + * Improved the asset library system to manage CSS and JavaScript files and + their dependencies. Allowing for smaller AJAX request payloads. + * jQuery is no longer loaded on all pages, only when another asset needs it. + * No JavaScript is loaded at all for anonymous users by default, for faster + page loads. + * Implemented SMACSS-style categorization for CSS files. + * Removed most support for Internet Explorer 8 and below. + * Added Modernizr for making styling changes based on browser support. + * All page template variables converted to blocks. - Added tour module. Provides highly contextual tips for UI elements. - Improved entity system. * Added support for saving and deleting entities through the controller. + * Base entity fields (such as labels) support widgets, formatters and + translation. + * Form modes introduced, similar to display modes. * Entities are now classed objects, implementing EntityInterface. * Drupal now understands the concept of a "default" revision, tracked independently from the latest revision, allowing for the creation of drafts while the current revision stays published. * All entity types, not just nodes, now have support for revisions. -- Replaced the core routing system with one built on the Symfony2 framework. +- Refactored routing system based on Symfony2 components. +- Reworked menu links, local actions, and local tasks based upon the new routing + system. +- Added plugin system to standardize implementation of several core APIs. - Configuration: * Added a centralized file-based configuration system. * Allows module authors to provide configuration in a standard format. * Implements functionality to get, set, add and remove configuration. * Includes ability to override configuration values with language variants and other runtime values. -- Added the CKEditor WYSIWYG editor. Provides a drag-and-drop configuration UI. -- Included the HTML5 Shiv library to support HTML5 elements in IE 8 and below. + * Supports configuration schema, dependencies, and validation to maintain + data-integrity between deployments and updates. +- Improved authoring experience: + * Added the CKEditor WYSIWYG editor. Clean markup guaranteed thanks to tight + integration with the filter system. + * Includes uploading, aligning and captioning of images. + * Correspondingly modernized the default text formats. + * Provides a drag-and-drop configuration UI, which automatically updates the + HTML filter settings, making configuring text formats trivial for typical + use cases. + * Added align and caption filters that can be applied to any element: + images, blockquotes, code snippets, videos… + * In-place editing of any entity: nodes, blocks… - Included the following Symfony2 components: * ClassLoader - PSR-0-compatible autoload routines. * DependencyInjection - Flexible dependency injection container. @@ -28,10 +73,13 @@ Drupal 8.0, xxxx-xx-xx (development version) * Process - Allows for executing commands in a sub-process. * Routing - Framework for mapping incoming requests to controller information. + * Serialization - Serialize complex nested objects into JSON/XML etc. + * Validator - Ensure that an object is in a valid state based upon some + validation rules defined for it. * Yaml - Parser for YAML files. -- Included the Assetic asset management framework for PHP. -- Included Backbone.js and Underscore.js JavaScript frameworks. -- Support added for making HTTP requests through a proxy server. +- Added routing component from Symfony CMF. +- Added Guzzle HTTP library. +- Added Zend Feed component. - Removed modules from core. * The following modules have been removed from core, because contributed modules with similar functionality are available: @@ -42,35 +90,30 @@ Drupal 8.0, xxxx-xx-xx (development version) * Poll * Profile * Trigger -- Removed the Overlay module from core. -- Removed the Garland theme from core. - Removed the Statistics module's accesslog functionality and reports from core. - Removed XML-RPC functionality from core. - Removed user signatures support from core. -- Removed backwards-compatibility with 'magic_quotes_gpc'/'magic_quotes_runtime' - PHP configuration settings. Both are required to be disabled. - Universally Unique IDentifier (UUID): * Support for generating and validating UUIDs. -- JavaScript changes: - * Updated to jQuery 2.1.0 - * Updated to jQuery UI 1.10.2 - * Removed jquery.bbq - Tremendously improved language support all around. * Great language improvements for users: * Improved language selection with user preference detection in the - installer. + installer based on browser settings. + * The installer is presented in the user's native language. * Moved base language support to Language module. * Greatly simplified the interface for setting up languages. * Improved browser language detection considerably. - * Language domain and path prefix configuraton simplified and centralized; - path prefix detection is now default. + * Language domain and path prefix configuration simplified and + centralized; path prefix detection is now default. * Added HTML 5 language markup; language information added in markup in several more places. * Made it possible to assign external language codes to local languages. * Introduced the possibility of an administration-specific language preference for users. + * Language selection fallback language is now independently configurable + without needing to change the site default language. * Simplified and added new features in interface translation: - * Made interface translation directly accessible from language list. + * Made interface translation directly accessible from the language list. * Centralized interface translation import to one directory. * Drupal can now be translated to English and English can be deleted. * Much improved built-in translation interface. @@ -79,6 +122,7 @@ Drupal 8.0, xxxx-xx-xx (development version) be identified and protected from translation update overwrites. * All Gettext files are now imported in chunks, better for low resource environments. + * Automated import and update of translations in the installer and later. * Improved content language support: * Made it possible to assign language to taxonomy terms, vocabularies, menu items, and files. @@ -107,29 +151,64 @@ Drupal 8.0, xxxx-xx-xx (development version) developers. * Made it possible for users to have a preferred language separate from their user entity language. - * The text formatter from t() is now available as format_string(). + * The text formatter from t() is now available as FormattableMarkup. * Added support for interface translation contexts in Drupal.t(), - Drupal.formatPlural() as well as routing, tabs, actions, and contextual - links. + Drupal.formatPlural() as well as routing, tabs, actions, shipped + menu items and contextual links. * Removed textgroups support from interface translation in favor of native configuration language support. * Added configuration schema system to support generating translation forms for any configuration. * Reworked Gettext PO support to use pluggable read/write handlers. * Added language select form element in the Form API. -- Added Email field type to core. -- Added Link field type to core. -- Added Phone number field type to core. + * Added a transliteration API. (Only used for machine names in core.) +- New field types added to core: + - Email + - Link + - Phone number + - Entity reference + - Date + - Comment (allows comment threads on entity types other than node). - Added local image input filter, to enable secure image posting. -- Added Views and Views UI module to core. -- Added Entity Reference field type to core. -- Added Date field type to core. +- Added Views and Views UI module to core: + * Various core listings: /node, /admin/content/node, /admin/people etc. are + now served by views. + * REST API support built in. + * Rewrote caching integration for better performance. +- Custom blocks are now fieldable, revisionable, and translatable entities. +- An accessible modal API based on improvements made in collaboration with the + jQuery UI team and the Views team. +- Fieldable contact forms allowing site-builders to easily build custom forms + for soliciting feedback from users. - Added a Web Services module package. * Added a RESTful web services provider module. * Added a serialization module using the Symfony serialization component. * Added a Hypertext Application Language (HAL) serialization module. * Added a HTTP Basic authentication provider module. +- Significant performance/scalability improvements: + * Cache tags, which allow content to be invalidated accurately and instantly, + including reverse proxies and CDNs. + * Cache contexts, which allow content to be cached correctly, and placeholdered + to improve cache hit rates. + * Cacheability bubbling, which allows strict tracking of assets and + cacheability throughout page rendering. + * Page caching has been factored out to its own module and is enabled by + default. + * Authenticated page caching has been added to core via the Dynamic Page Cache + module and is enabled by default. + * APCu, memory, and PHP file caching backends added to core, alongside support + for a chained, consistent cache backend to support correctly using fast + local cache implementations with multiple web servers. - When using MySQL, the MyISAM engine is no longer supported. +- Testing improvements + * Added PHPUnit for proper unit testing, see + https://phpunit.de/manual/4.8/en/index.html so you can run tests via + your IDE. + * Added BrowserTestBase as an alternative to simpletest for browser + testing (JavaScript support to be included in the future) + * Added KernelTestBase to provide a fast API testing of integration of + different components + * Core branch nightly tests include PHP 5.5, 5.6, 7, sqlite and PostgreSQL. Drupal 7.0, 2011-01-05 ---------------------- diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 5d00d6650..1fa3118a6 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -283,6 +283,7 @@ Breakpoint module CKEditor module - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers +- Marek 'mlewand' Lewandowski https://www.drupal.org/u/mlewand Color module - ? diff --git a/core/composer.json b/core/composer.json index fd33b45b7..afb170ad5 100644 --- a/core/composer.json +++ b/core/composer.json @@ -17,7 +17,7 @@ "symfony/validator": "2.7.*", "symfony/process": "2.7.*", "symfony/yaml": "2.7.*", - "twig/twig": "^1.22.2", + "twig/twig": "^1.23.1", "doctrine/common": "2.5.*", "doctrine/annotations": "1.2.*", "guzzlehttp/guzzle": "~6.1", diff --git a/core/core.libraries.yml b/core/core.libraries.yml index a05f09410..2d16fc975 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -260,7 +260,6 @@ drupal.tabledrag: misc/tabledrag.js: { weight: -1 } dependencies: - core/jquery - - core/modernizr - core/drupal - core/drupalSettings - core/jquery.once diff --git a/core/core.services.yml b/core/core.services.yml index a83af3460..afccb3177 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -949,14 +949,14 @@ services: tags: - { name: route_enhancer } - { name: event_subscriber } + route_enhancer.form: + class: Drupal\Core\Routing\Enhancer\FormRouteEnhancer + tags: + - { name: route_enhancer } route_enhancer.entity: class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer tags: - { name: route_enhancer, priority: 20 } - route_content_controller_subscriber: - class: Drupal\Core\EventSubscriber\ContentControllerSubscriber - tags: - - { name: event_subscriber } route_special_attributes_subscriber: class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber tags: diff --git a/core/includes/errors.inc b/core/includes/errors.inc index 479548065..39d8ffd03 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -119,8 +119,11 @@ function error_displayable($error = NULL) { * %line, severity_level, and backtrace. All the parameters are plain-text, * with the exception of @message, which needs to be an HTML string, and * backtrace, which is a standard PHP backtrace. - * @param $fatal - * TRUE if the error is fatal. + * @param bool $fatal + * TRUE for: + * - An exception is thrown and not caught by something else. + * - A recoverable fatal error, which is a fatal error. + * Non-recoverable fatal errors cannot be logged by Drupal. */ function _drupal_log_error($error, $fatal = FALSE) { $is_installer = drupal_installation_attempted(); @@ -169,6 +172,11 @@ function _drupal_log_error($error, $fatal = FALSE) { } } + // Log fatal errors, so developers can find and debug them. + if ($fatal) { + error_log(sprintf('%s: %s in %s on line %d', $error['%type'], $error['@message'], $error['%file'], $error['%line'])); + } + if (PHP_SAPI === 'cli') { if ($fatal) { // When called from CLI, simply output a plain text message. diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 60ef2d7cd..7b669114e 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -15,6 +15,7 @@ use Drupal\Core\Language\LanguageManager; use Drupal\Core\Logger\LoggerChannelFactory; use Drupal\Core\Site\Settings; use Drupal\Core\StringTranslation\Translator\FileTranslation; +use Drupal\Core\StackMiddleware\ReverseProxyMiddleware; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Url; @@ -403,6 +404,8 @@ function install_begin_request($class_loader, &$install_state) { $kernel->setSitePath($site_path); $kernel->boot(); $container = $kernel->getContainer(); + // If Drupal is being installed behind a proxy, configure the request. + ReverseProxyMiddleware::setSettingsOnRequest($request, Settings::getInstance()); // Register the file translation service. if (isset($GLOBALS['config']['locale.settings']['translation']['path'])) { diff --git a/core/includes/theme.inc b/core/includes/theme.inc index e8cff8ba6..62f2d6ff7 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1778,7 +1778,7 @@ function drupal_common_theme() { ), // From menu.inc. 'menu' => array( - 'variables' => array('items' => array(), 'attributes' => array()), + 'variables' => array('menu_name' => NULL, 'items' => array(), 'attributes' => array()), ), 'menu_local_task' => array( 'render element' => 'element', diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index b98d28af7..80cc52cb8 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -82,19 +82,21 @@ function _drupal_maintenance_theme() { $theme = $custom_theme; // Find all our ancestor themes and put them in an array. - $base_theme = array(); + // @todo This is just a workaround. Find a better way how to handle themes + // on maintenance pages, see https://www.drupal.org/node/2322619. + // This code is basically a duplicate of + // \Drupal\Core\Theme\ThemeInitialization::getActiveThemeByName. + $base_themes = []; $ancestor = $theme; while ($ancestor && isset($themes[$ancestor]->base_theme)) { - $base_theme[] = $themes[$themes[$ancestor]->base_theme]; + $base_themes[] = $themes[$themes[$ancestor]->base_theme]; $ancestor = $themes[$ancestor]->base_theme; if ($ancestor) { - // Ensure that the base theme is added. + // Ensure that the base theme is added and installed. $theme_handler->addTheme($themes[$ancestor]); } } - // @todo This is just a workaround. Find a better way how to handle themes - // on maintenance pages, see https://www.drupal.org/node/2322619. - \Drupal::theme()->setActiveTheme($theme_init->getActiveTheme($themes[$custom_theme], array_reverse($base_theme))); + \Drupal::theme()->setActiveTheme($theme_init->getActiveTheme($themes[$custom_theme], $base_themes)); // Prime the theme registry. Drupal::service('theme.registry'); } diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index b2edb81da..d75f50b72 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -81,7 +81,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '8.0.0-rc3'; + const VERSION = '8.0.0-dev-2015-11-17'; /** * Core API compatibility. @@ -252,11 +252,27 @@ class Drupal { * * @return \Drupal\Core\Entity\EntityManagerInterface * The entity manager service. + * + * @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. + * Use \Drupal::entityTypeManager() instead in most cases. If the needed + * method is not on \Drupal\Core\Entity\EntityTypeManagerInterface, see the + * deprecated \Drupal\Core\Entity\EntityManager to find the + * correct interface or service. */ public static function entityManager() { return static::getContainer()->get('entity.manager'); } + /** + * Retrieves the entity type manager. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager. + */ + public static function entityTypeManager() { + return static::getContainer()->get('entity_type.manager'); + } + /** * Returns the current primary database. * diff --git a/core/lib/Drupal/Component/PhpStorage/FileStorage.php b/core/lib/Drupal/Component/PhpStorage/FileStorage.php index e4e7fde44..b6a2a9dc2 100644 --- a/core/lib/Drupal/Component/PhpStorage/FileStorage.php +++ b/core/lib/Drupal/Component/PhpStorage/FileStorage.php @@ -79,8 +79,7 @@ class FileStorage implements PhpStorageInterface { public static function htaccessLines($private = TRUE) { $lines = <<themeManager->getActiveTheme(); // Add the theme name to the cache key since themes may implement - // hook_css_alter(). - $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize; + // hook_library_info_alter(). + $libraries_to_load = $this->getLibrariesToLoad($assets); + $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize; if ($cached = $this->cache->get($cid)) { return $cached->data; } @@ -132,7 +133,7 @@ class AssetResolver implements AssetResolverInterface { 'browsers' => [], ]; - foreach ($this->getLibrariesToLoad($assets) as $library) { + foreach ($libraries_to_load as $library) { list($extension, $name) = explode('/', $library, 2); $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); if (isset($definition['css'])) { @@ -187,9 +188,7 @@ class AssetResolver implements AssetResolverInterface { * Returns the JavaScript settings assets for this response's libraries. * * Gathers all drupalSettings from all libraries in the attached assets - * collection and merges them, then it merges individual attached settings, - * and finally invokes hook_js_settings_alter() to allow alterations of - * JavaScript settings by modules and themes. + * collection and merges them. * * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets * The assets attached to the current response. @@ -207,9 +206,6 @@ class AssetResolver implements AssetResolverInterface { } } - // Attached settings win over settings in libraries. - $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); - return $settings; } @@ -219,9 +215,10 @@ class AssetResolver implements AssetResolverInterface { public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { $theme_info = $this->themeManager->getActiveTheme(); // Add the theme name to the cache key since themes may implement - // hook_js_alter(). Additionally add the current language to support - // translation of JavaScript files. - $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize; + // hook_library_info_alter(). Additionally add the current language to + // support translation of JavaScript files via hook_js_alter(). + $libraries_to_load = $this->getLibrariesToLoad($assets); + $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; if ($cached = $this->cache->get($cid)) { list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data; @@ -239,8 +236,6 @@ class AssetResolver implements AssetResolverInterface { 'browsers' => [], ]; - $libraries_to_load = $this->getLibrariesToLoad($assets); - // Collect all libraries that contain JS assets and are in the header. $header_js_libraries = []; foreach ($libraries_to_load as $library) { @@ -329,8 +324,10 @@ class AssetResolver implements AssetResolverInterface { $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']); } - if ($settings !== FALSE) { + // Attached settings override both library definitions and + // hook_js_settings_build(). + $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); // Allow modules and themes to alter the JavaScript settings. $this->moduleHandler->alter('js_settings', $settings, $assets); $this->themeManager->alter('js_settings', $settings, $assets); diff --git a/core/lib/Drupal/Core/Cache/CacheCollector.php b/core/lib/Drupal/Core/Cache/CacheCollector.php index eb1ca57a5..9755c1abb 100644 --- a/core/lib/Drupal/Core/Cache/CacheCollector.php +++ b/core/lib/Drupal/Core/Cache/CacheCollector.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Cache; +use Drupal\Component\Utility\Crypt; use Drupal\Core\DestructableInterface; use Drupal\Core\Lock\LockBackendInterface; @@ -232,7 +233,7 @@ abstract class CacheCollector implements CacheCollectorInterface, DestructableIn // Lock cache writes to help avoid stampedes. $cid = $this->getCid(); - $lock_name = $cid . ':' . __CLASS__; + $lock_name = $this->normalizeLockName($cid . ':' . __CLASS__); if (!$lock || $this->lock->acquire($lock_name)) { // Set and delete operations invalidate the cache item. Try to also load // an eventually invalidated cache entry, only update an invalidated cache @@ -264,6 +265,30 @@ abstract class CacheCollector implements CacheCollectorInterface, DestructableIn $this->keysToRemove = array(); } + /** + * Normalizes a cache ID in order to comply with database limitations. + * + * @param string $cid + * The passed in cache ID. + * + * @return string + * An ASCII-encoded cache ID that is at most 255 characters long. + */ + protected function normalizeLockName($cid) { + // Nothing to do if the ID is a US ASCII string of 255 characters or less. + $cid_is_ascii = mb_check_encoding($cid, 'ASCII'); + if (strlen($cid) <= 255 && $cid_is_ascii) { + return $cid; + } + // Return a string that uses as much as possible of the original cache ID + // with the hash appended. + $hash = Crypt::hashBase64($cid); + if (!$cid_is_ascii) { + return $hash; + } + return substr($cid, 0, 255 - strlen($hash)) . $hash; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php index 50e694256..0d5b6aacd 100644 --- a/core/lib/Drupal/Core/Composer/Composer.php +++ b/core/lib/Drupal/Core/Composer/Composer.php @@ -10,6 +10,7 @@ namespace Drupal\Core\Composer; use Drupal\Component\PhpStorage\FileStorage; use Composer\Script\Event; use Composer\Installer\PackageEvent; +use Composer\Semver\Constraint\Constraint; /** * Provides static functions for composer script events. @@ -71,23 +72,38 @@ class Composer { ]; /** - * Add vendor classes to composers static classmap. + * Add vendor classes to Composer's static classmap. */ public static function preAutoloadDump(Event $event) { - $composer = $event->getComposer(); - $package = $composer->getPackage(); - $autoload = $package->getAutoload(); - $autoload['classmap'] = array_merge($autoload['classmap'], array( - 'vendor/symfony/http-foundation/Request.php', - 'vendor/symfony/http-foundation/ParameterBag.php', - 'vendor/symfony/http-foundation/FileBag.php', - 'vendor/symfony/http-foundation/ServerBag.php', - 'vendor/symfony/http-foundation/HeaderBag.php', - 'vendor/symfony/http-kernel/HttpKernel.php', - 'vendor/symfony/http-kernel/HttpKernelInterface.php', - 'vendor/symfony/http-kernel/TerminableInterface.php', - )); - $package->setAutoload($autoload); + // We need the root package so we can add our classmaps to its loader. + $package = $event->getComposer()->getPackage(); + // We need the local repository so that we can query and see if it's likely + // that our files are present there. + $repository = $event->getComposer()->getRepositoryManager()->getLocalRepository(); + // This is, essentially, a null constraint. We only care whether the package + // is present in vendor/ yet, but findPackage() requires it. + $constraint = new Constraint('>', ''); + // Check for our packages, and then optimize them if they're present. + if ($repository->findPackage('symfony/http-foundation', $constraint)) { + $autoload = $package->getAutoload(); + $autoload['classmap'] = array_merge($autoload['classmap'], array( + 'vendor/symfony/http-foundation/Request.php', + 'vendor/symfony/http-foundation/ParameterBag.php', + 'vendor/symfony/http-foundation/FileBag.php', + 'vendor/symfony/http-foundation/ServerBag.php', + 'vendor/symfony/http-foundation/HeaderBag.php', + )); + $package->setAutoload($autoload); + } + if ($repository->findPackage('symfony/http-kernel', $constraint)) { + $autoload = $package->getAutoload(); + $autoload['classmap'] = array_merge($autoload['classmap'], array( + 'vendor/symfony/http-kernel/HttpKernel.php', + 'vendor/symfony/http-kernel/HttpKernelInterface.php', + 'vendor/symfony/http-kernel/TerminableInterface.php', + )); + $package->setAutoload($autoload); + } } /** diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 7209030dd..ae12c9186 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -112,13 +112,13 @@ class ConfigInstaller implements ConfigInstallerInterface { $prefix = $name . '.'; } - // Gets a profile storage to search for overrides if necessary. - $profile_storage = $this->getProfileStorage($name); + // Gets profile storages to search for overrides if necessary. + $profile_storages = $this->getProfileStorages($name); // Gather information about all the supported collections. $collection_info = $this->configManager->getConfigCollectionInfo(); foreach ($collection_info->getCollectionNames() as $collection) { - $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage); + $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storages); // If we're installing a profile ensure configuration that is overriding // is excluded. if ($name == $this->drupalGetProfile()) { @@ -223,19 +223,22 @@ class ConfigInstaller implements ConfigInstallerInterface { * The configuration collection to use. * @param string $prefix * (optional) Limit to configuration starting with the provided string. + * @param \Drupal\Core\Config\StorageInterface[] $profile_storages + * An array of storage interfaces containing profile configuration to check + * for overrides. * * @return array * An array of configuration data read from the source storage keyed by the * configuration object name. */ - protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', StorageInterface $profile_storage = NULL) { + protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', array $profile_storages = []) { if ($storage->getCollectionName() != $collection) { $storage = $storage->createCollection($collection); } $data = $storage->readMultiple($storage->listAll($prefix)); // Check to see if the corresponding override storage has any overrides. - if ($profile_storage) { + foreach ($profile_storages as $profile_storage) { if ($profile_storage->getCollectionName() != $collection) { $profile_storage = $profile_storage->createCollection($collection); } @@ -435,11 +438,11 @@ class ConfigInstaller implements ConfigInstallerInterface { $enabled_extensions = $this->getEnabledExtensions(); // Add the extension that will be enabled to the list of enabled extensions. $enabled_extensions[] = $name; - // Gets a profile storage to search for overrides if necessary. - $profile_storage = $this->getProfileStorage($name); + // Gets profile storages to search for overrides if necessary. + $profile_storages = $this->getProfileStorages($name); // Check the dependencies of configuration provided by the module. - $invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storage); + $invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storages); if (!empty($invalid_default_config)) { throw UnmetDependenciesException::create($name, $invalid_default_config); } @@ -460,14 +463,19 @@ class ConfigInstaller implements ConfigInstallerInterface { /** * Finds default configuration with unmet dependencies. * + * @param \Drupal\Core\Config\StorageInterface $storage + * The storage containing the default configuration. * @param array $enabled_extensions * A list of all the currently enabled modules and themes. + * @param \Drupal\Core\Config\StorageInterface[] $profile_storages + * An array of storage interfaces containing profile configuration to check + * for overrides. * * @return array * List of configuration that has unmet dependencies */ - protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, StorageInterface $profile_storage = NULL) { - $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storage); + protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, array $profile_storages = []) { + $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storages); $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create)); return array_filter(array_keys($config_to_create), function($config_name) use ($enabled_extensions, $all_config, $config_to_create) { return !$this->validateDependencies($config_name, $config_to_create[$config_name], $enabled_extensions, $all_config); @@ -550,27 +558,31 @@ class ConfigInstaller implements ConfigInstallerInterface { /** * Gets the profile storage to use to check for profile overrides. * + * The install profile can override module configuration during a module + * install. Both the install and optional directories are checked for matching + * configuration. This allows profiles to override default configuration for + * modules they do not depend on. + * * @param string $installing_name * (optional) The name of the extension currently being installed. * - * @return \Drupal\Core\Config\StorageInterface|null - * A storage to access configuration from the installation profile. If a - * Drupal installation is not in progress or we're installing the profile - * itself, then it will return NULL as the profile storage should not be - * used. + * @return \Drupal\Core\Config\StorageInterface[]|null + * Storages to access configuration from the installation profile. If we're + * installing the profile itself, then it will return an empty array as the + * profile storage should not be used. */ - protected function getProfileStorage($installing_name = '') { + protected function getProfileStorages($installing_name = '') { $profile = $this->drupalGetProfile(); - if ($this->drupalInstallationAttempted() && $profile != $installing_name) { - // Profiles should not contain optional configuration so always use the - // install directory. - $profile_install_path = $this->getDefaultConfigDirectory('module', $profile); - $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION); + $profile_storages = []; + if ($profile && $profile != $installing_name) { + $profile_path = $this->drupalGetPath('module', $profile); + foreach ([InstallStorage::CONFIG_INSTALL_DIRECTORY, InstallStorage::CONFIG_OPTIONAL_DIRECTORY] as $directory) { + if (is_dir($profile_path . '/' . $directory)) { + $profile_storages[] = new FileStorage($profile_path . '/' . $directory, StorageInterface::DEFAULT_COLLECTION); + } + } } - else { - $profile_storage = NULL; - } - return $profile_storage; + return $profile_storages; } /** diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index ab2fb1058..bb396e6d4 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -387,6 +387,7 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface * {@inheritdoc} */ public function url($rel = 'edit-form', $options = array()) { + // Do not remove this override: the default value of $rel is different. return parent::url($rel, $options); } @@ -394,9 +395,19 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface * {@inheritdoc} */ public function link($text = NULL, $rel = 'edit-form', array $options = []) { + // Do not remove this override: the default value of $rel is different. return parent::link($text, $rel, $options); } + /** + * {@inheritdoc} + */ + public function toUrl($rel = 'edit-form', array $options = []) { + // Unless language was already provided, avoid setting an explicit language. + $options += ['language' => NULL]; + return parent::toUrl($rel, $options); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Controller/ControllerBase.php b/core/lib/Drupal/Core/Controller/ControllerBase.php index ce0354ff2..544e247fe 100644 --- a/core/lib/Drupal/Core/Controller/ControllerBase.php +++ b/core/lib/Drupal/Core/Controller/ControllerBase.php @@ -49,6 +49,13 @@ abstract class ControllerBase implements ContainerInjectionInterface { */ protected $entityManager; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * The entity form builder. * @@ -117,6 +124,10 @@ abstract class ControllerBase implements ContainerInjectionInterface { * * @return \Drupal\Core\Entity\EntityManagerInterface * The entity manager service. + * + * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0. + * Most of the time static::entityTypeManager() is supposed to be used + * instead. */ protected function entityManager() { if (!$this->entityManager) { @@ -125,6 +136,19 @@ abstract class ControllerBase implements ContainerInjectionInterface { return $this->entityManager; } + /** + * Retrieves the entity type manager. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager. + */ + protected function entityTypeManager() { + if (!isset($this->entityTypeManager)) { + $this->entityTypeManager = $this->container()->get('entity_type.manager'); + } + return $this->entityTypeManager; + } + /** * Retrieves the entity form builder. * diff --git a/core/lib/Drupal/Core/Datetime/DateHelper.php b/core/lib/Drupal/Core/Datetime/DateHelper.php index ad49016c0..4cfc74640 100644 --- a/core/lib/Drupal/Core/Datetime/DateHelper.php +++ b/core/lib/Drupal/Core/Datetime/DateHelper.php @@ -254,14 +254,24 @@ class DateHelper { * An array of weekdays. * * @return array - * An array of weekdays reordered to match the first day of the week. + * An array of weekdays reordered to match the first day of the week. The + * keys will remain unchanged. For example, if the first day of the week is + * set to be Monday, the array keys will be [1, 2, 3, 4, 5, 6, 0]. */ public static function weekDaysOrdered($weekdays) { $first_day = \Drupal::config('system.date')->get('first_day'); if ($first_day > 0) { for ($i = 1; $i <= $first_day; $i++) { - $last = array_shift($weekdays); - array_push($weekdays, $last); + // Reset the array to the first element. + reset($weekdays); + // Retrieve the first week day value. + $last = current($weekdays); + // Store the corresponding key. + $key = key($weekdays); + // Remove this week day from the beginning of the array. + unset($weekdays[$key]); + // Add this week day to the end of the array. + $weekdays[$key] = $last; } } return $weekdays; diff --git a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php index e00426fc4..1edaec363 100644 --- a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php +++ b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php @@ -72,33 +72,6 @@ class ContainerBuilder extends SymfonyContainerBuilder { parent::setParameter($name, $value); } - /** - * Synchronizes a service change. - * - * This method is a copy of the ContainerBuilder of symfony. - * - * This method updates all services that depend on the given - * service by calling all methods referencing it. - * - * @param string $id A service id - */ - private function synchronize($id) { - foreach ($this->getDefinitions() as $definitionId => $definition) { - // only check initialized services - if (!$this->initialized($definitionId)) { - continue; - } - - foreach ($definition->getMethodCalls() as $call) { - foreach ($call[1] as $argument) { - if ($argument instanceof Reference && $id == (string) $argument) { - $this->callMethod($this->get($definitionId), $call); - } - } - } - } - } - /** * A 1to1 copy of parent::callMethod. */ diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 4b56db2d6..278826e0f 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -821,13 +821,6 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { // If there is no container and no cached container definition, build a new // one from scratch. if (!isset($container) && !isset($container_definition)) { - if (version_compare(phpversion(), '7.0.0-dev') >= 0) { - // The service graph implementation is prone to corruption during GC. - // Collect cycles now then disable the GC for the time of the compiler - // run. - // @see https://bugs.php.net/bug.php?id=70805 - gc_collect_cycles(); - } $container = $this->compileContainer(); // Only dump the container if dumping is allowed. This is useful for diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 2a52b79b3..6dd6e10cf 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -10,10 +10,10 @@ namespace Drupal\Core\Entity\Element; use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Tags; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\Textfield; use Drupal\Core\Site\Settings; -use Drupal\user\EntityOwnerInterface; /** * Provides an entity autocomplete form element. @@ -147,7 +147,7 @@ class EntityAutocomplete extends Textfield { 'handler_settings' => $element['#selection_settings'], ); $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options); - $autocreate = (bool) $element['#autocreate']; + $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface; $input_values = $element['#tags'] ? Tags::explode($element['#value']) : array($element['#value']); foreach ($input_values as $input) { @@ -167,13 +167,14 @@ class EntityAutocomplete extends Textfield { // Auto-create item. See an example of how this is handled in // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave(). $value[] = array( - 'entity' => static::createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']) + 'entity' => $handler->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']), ); } } // Check that the referenced entities are valid, if needed. - if ($element['#validate_reference'] && !$autocreate && !empty($value)) { + if ($element['#validate_reference'] && !empty($value)) { + // Validate existing entities. $ids = array_reduce($value, function ($return, $item) { if (isset($item['target_id'])) { $return[] = $item['target_id']; @@ -189,6 +190,30 @@ class EntityAutocomplete extends Textfield { } } } + + // Validate newly created entities. + $new_entities = array_reduce($value, function ($return, $item) { + if (isset($item['entity'])) { + $return[] = $item['entity']; + } + return $return; + }); + + if ($new_entities) { + if ($autocreate) { + $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities); + $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities); + } + else { + // If the selection handler does not support referencing newly + // created entities, all of them should be invalidated. + $invalid_new_entities = $new_entities; + } + + foreach ($invalid_new_entities as $entity) { + $form_state->setError($element, t('This entity (%type: %label) cannot be referenced.', array('%type' => $element['#target_type'], '%label' => $entity->label()))); + } + } } // Use only the last value if the form element does not support multiple @@ -310,37 +335,4 @@ class EntityAutocomplete extends Textfield { return $match; } - /** - * Creates a new entity from a label entered in the autocomplete input. - * - * @param string $entity_type_id - * The entity type ID. - * @param string $bundle - * The bundle name. - * @param string $label - * The entity label. - * @param int $uid - * The entity owner ID. - * - * @return \Drupal\Core\Entity\EntityInterface - */ - protected static function createNewEntity($entity_type_id, $bundle, $label, $uid) { - $entity_manager = \Drupal::entityManager(); - - $entity_type = $entity_manager->getDefinition($entity_type_id); - $bundle_key = $entity_type->getKey('bundle'); - $label_key = $entity_type->getKey('label'); - - $entity = $entity_manager->getStorage($entity_type_id)->create(array( - $bundle_key => $bundle, - $label_key => $label, - )); - - if ($entity instanceof EntityOwnerInterface) { - $entity->setOwnerId($uid); - } - - return $entity; - } - } diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 2a4b199ee..8164d7c1a 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -72,11 +72,26 @@ abstract class Entity implements EntityInterface { * Gets the entity manager. * * @return \Drupal\Core\Entity\EntityManagerInterface + * + * @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0. + * Use \Drupal::entityTypeManager() instead in most cases. If the needed + * method is not on \Drupal\Core\Entity\EntityTypeManagerInterface, see the + * deprecated \Drupal\Core\Entity\EntityManager to find the + * correct interface or service. */ protected function entityManager() { return \Drupal::entityManager(); } + /** + * Gets the entity type manager. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected function entityTypeManager() { + return \Drupal::entityTypeManager(); + } + /** * Gets the language manager. * @@ -158,6 +173,13 @@ abstract class Entity implements EntityInterface { * {@inheritdoc} */ public function urlInfo($rel = 'canonical', array $options = []) { + return $this->toUrl($rel, $options); + } + + /** + * {@inheritdoc} + */ + public function toUrl($rel = 'canonical', array $options = []) { if ($this->id() === NULL) { throw new EntityMalformedException(sprintf('The "%s" entity cannot have a URI as it does not have an ID', $this->getEntityTypeId())); } @@ -237,26 +259,33 @@ abstract class Entity implements EntityInterface { * {@inheritdoc} */ public function link($text = NULL, $rel = 'canonical', array $options = []) { - if (is_null($text)) { + return $this->toLink($text, $rel, $options)->toString(); + } + + /** + * {@inheritdoc} + */ + public function toLink($text = NULL, $rel = 'canonical', array $options = []) { + if (!isset($text)) { $text = $this->label(); } - $url = $this->urlInfo($rel); + $url = $this->toUrl($rel); $options += $url->getOptions(); $url->setOptions($options); - return (new Link($text, $url))->toString(); + return new Link($text, $url); } /** * {@inheritdoc} */ public function url($rel = 'canonical', $options = array()) { - // While self::urlInfo() will throw an exception if the entity is new, + // While self::toUrl() will throw an exception if the entity has no id, // the expected result for a URL is always a string. - if ($this->isNew() || !$this->hasLinkTemplate($rel)) { + if ($this->id() === NULL || !$this->hasLinkTemplate($rel)) { return ''; } - $uri = $this->urlInfo($rel); + $uri = $this->toUrl($rel); $options += $uri->getOptions(); $uri->setOptions($options); return $uri->toString(); diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index 1f476e583..62a8f4476 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -101,7 +101,29 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf public function label(); /** - * Gets the URI elements of the entity. + * Gets the URL object for the entity. + * + * @param string $rel + * The link relationship type, for example: canonical or edit-form. + * @param array $options + * See \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for + * the available options. + * + * @return \Drupal\Core\Url + * The URL object. + * + * @deprecated in Drupal 8.0.0, intended to be removed in Drupal 9.0.0 + * Use toUrl() instead. + * + * @see \Drupal\Core\Entity\EntityInterface::toUrl + */ + public function urlInfo($rel = 'canonical', array $options = array()); + + /** + * Gets the URL object for the entity. + * + * The entity must have an id already. Content entities usually get their IDs + * by saving them. * * URI templates might be set in the links array in an annotation, for * example: @@ -128,8 +150,12 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf * the available options. * * @return \Drupal\Core\Url + * The URL object. + * + * @throws \Drupal\Core\Entity\EntityMalformedException + * @throws \Drupal\Core\Entity\Exception\UndefinedLinkTemplateException */ - public function urlInfo($rel = 'canonical', array $options = array()); + public function toUrl($rel = 'canonical', array $options = array()); /** * Gets the public URL for this entity. @@ -142,9 +168,36 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf * * @return string * The URL for this entity. + * + * @deprecated in Drupal 8.0.0, intended to be removed in Drupal 9.0.0 + * Please use toUrl() instead. + * + * @see \Drupal\Core\Entity\EntityInterface::toUrl */ public function url($rel = 'canonical', $options = array()); + /** + * Deprecated way of generating a link to the entity. See toLink(). + * + * @param string|null $text + * (optional) The link text for the anchor tag as a translated string. + * If NULL, it will use the entity's label. Defaults to NULL. + * @param string $rel + * (optional) The link relationship type. Defaults to 'canonical'. + * @param array $options + * See \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for + * the available options. + * + * @return string + * An HTML string containing a link to the entity. + * + * @deprecated in Drupal 8.0.0, intended to be removed in Drupal 9.0.0 + * Please use toLink() instead. + * + * @see \Drupal\Core\Entity\EntityInterface::toLink + */ + public function link($text = NULL, $rel = 'canonical', array $options = []); + /** * Generates the HTML for a link to this entity. * @@ -157,10 +210,13 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf * See \Drupal\Core\Routing\UrlGeneratorInterface::generateFromRoute() for * the available options. * - * @return string - * An HTML string containing a link to the entity. + * @return \Drupal\Core\Link + * A Link to the entity. + * + * @throws \Drupal\Core\Entity\EntityMalformedException + * @throws \Drupal\Core\Entity\Exception\UndefinedLinkTemplateException */ - public function link($text = NULL, $rel = 'canonical', array $options = []); + public function toLink($text = NULL, $rel = 'canonical', array $options = []); /** * Indicates if a link template exists for a given key. diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php index 83120d195..132b9a7b2 100644 --- a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionInterface.php @@ -30,7 +30,7 @@ interface SelectionInterface extends PluginFormInterface { public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0); /** - * Counts entities that are referenceable by a given field. + * Counts entities that are referenceable. * * @return int * The number of referenceable entities. @@ -38,7 +38,7 @@ interface SelectionInterface extends PluginFormInterface { public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS'); /** - * Validates that entities can be referenced by this field. + * Validates which existing entities can be referenced. * * @return array * An array of valid entity IDs. diff --git a/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionWithAutocreateInterface.php b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionWithAutocreateInterface.php new file mode 100644 index 000000000..7010b91ad --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityReferenceSelection/SelectionWithAutocreateInterface.php @@ -0,0 +1,52 @@ +bundleInfo = $this->moduleHandler->invokeAll('entity_bundle_info'); - // First look for entity types that act as bundles for others, load them - // and add them as bundles. foreach ($this->entityTypeManager->getDefinitions() as $type => $entity_type) { - if ($entity_type->getBundleOf()) { - foreach ($this->entityTypeManager->getStorage($type)->loadMultiple() as $entity) { - $this->bundleInfo[$entity_type->getBundleOf()][$entity->id()]['label'] = $entity->label(); + // First look for entity types that act as bundles for others, load them + // and add them as bundles. + if ($bundle_entity_type = $entity_type->getBundleEntityType()) { + foreach ($this->entityTypeManager->getStorage($bundle_entity_type)->loadMultiple() as $entity) { + $this->bundleInfo[$type][$entity->id()]['label'] = $entity->label(); } } - } - foreach ($this->entityTypeManager->getDefinitions() as $type => $entity_type) { - // If no bundles are provided, use the entity type name and label. - if (!isset($this->bundleInfo[$type])) { + // If entity type bundles are not supported and + // hook_entity_bundle_info() has not already set up bundle + // information, use the entity type name and label. + elseif (!isset($this->bundleInfo[$type])) { $this->bundleInfo[$type][$type]['label'] = $entity_type->getLabel(); } } diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php index e25b92496..59d502bdb 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php +++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Form\FormStateInterface; @@ -18,6 +19,7 @@ use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Session\AccountInterface; +use Drupal\user\EntityOwnerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -40,7 +42,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * deriver = "Drupal\Core\Entity\Plugin\Derivative\DefaultSelectionDeriver" * ) */ -class DefaultSelection extends PluginBase implements SelectionInterface, ContainerFactoryPluginInterface { +class DefaultSelection extends PluginBase implements SelectionInterface, SelectionWithAutocreateInterface, ContainerFactoryPluginInterface { /** * The entity manager. @@ -288,6 +290,38 @@ class DefaultSelection extends PluginBase implements SelectionInterface, Contain return $result; } + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $entity_type = $this->entityManager->getDefinition($entity_type_id); + $bundle_key = $entity_type->getKey('bundle'); + $label_key = $entity_type->getKey('label'); + + $entity = $this->entityManager->getStorage($entity_type_id)->create(array( + $bundle_key => $bundle, + $label_key => $label, + )); + + if ($entity instanceof EntityOwnerInterface) { + $entity->setOwnerId($uid); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + return array_filter($entities, function ($entity) { + if (isset($this->configuration['handler_settings']['target_bundles'])) { + return in_array($entity->bundle(), $this->configuration['handler_settings']['target_bundles']); + } + return TRUE; + }); + } + /** * Builds an EntityQuery to get referenceable entities. * diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php index bf8658aa6..f27c22d17 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraint.php @@ -26,10 +26,24 @@ class ValidReferenceConstraint extends Constraint { * * @var string */ - public $message = 'The referenced entity (%type: %id) does not exist.'; + public $message = 'This entity (%type: %id) cannot be referenced.'; /** - * Validation message when the target_id is empty. + * Violation message when the entity does not exist. + * + * @var string + */ + public $nonExistingMessage = 'The referenced entity (%type: %id) does not exist.'; + + /** + * Violation message when a new entity ("autocreate") is invalid. + * + * @var string + */ + public $invalidAutocreateMessage = 'This entity (%type: %label) cannot be referenced.'; + + /** + * Violation message when the target_id is empty. * * @var string */ diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php index 8f704c1c7..f228bbcef 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/ValidReferenceConstraintValidator.php @@ -7,39 +7,142 @@ namespace Drupal\Core\Entity\Plugin\Validation\Constraint; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; +use Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; /** * Checks if referenced entities are valid. */ -class ValidReferenceConstraintValidator extends ConstraintValidator { +class ValidReferenceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { + + /** + * The selection plugin manager. + * + * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface + */ + protected $selectionManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a ValidReferenceConstraintValidator object. + * + * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager + * The selection plugin manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(SelectionPluginManagerInterface $selection_manager, EntityTypeManagerInterface $entity_type_manager) { + $this->selectionManager = $selection_manager; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.entity_reference_selection'), + $container->get('entity_type.manager') + ); + } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { - /** @var \Drupal\Core\Field\FieldItemInterface $value */ + /** @var \Drupal\Core\Field\FieldItemListInterface $value */ /** @var ValidReferenceConstraint $constraint */ if (!isset($value)) { return; } - // We don't use a regular NotNull constraint for the target_id property as - // a NULL value is valid if the entity property contains an unsaved entity. - // @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints - if (!$value->isEmpty() && $value->target_id === NULL && !$value->entity->isNew()) { - $this->context->addViolation($constraint->nullMessage); + + // Collect new entities and IDs of existing entities across the field items. + $new_entities = []; + $target_ids = []; + foreach ($value as $delta => $item) { + $target_id = $item->target_id; + // We don't use a regular NotNull constraint for the target_id property as + // NULL is allowed if the entity property contains an unsaved entity. + // @see \Drupal\Core\TypedData\DataReferenceTargetDefinition::getConstraints() + if (!$item->isEmpty() && $target_id === NULL) { + if (!$item->entity->isNew()) { + $this->context->buildViolation($constraint->nullMessage) + ->atPath((string) $delta) + ->addViolation(); + return; + } + $new_entities[$delta] = $item->entity; + } + + // '0' or NULL are considered valid empty references. + if (!empty($target_id)) { + $target_ids[$delta] = $target_id; + } + } + + // Early opt-out if nothing to validate. + if (!$new_entities && !$target_ids) { return; } - $id = $value->get('target_id')->getValue(); - // '0' or NULL are considered valid empty references. - if (empty($id)) { - return; + + /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler * */ + $handler = $this->selectionManager->getSelectionHandler($value->getFieldDefinition()); + $target_type_id = $value->getFieldDefinition()->getSetting('target_type'); + + // Add violations on deltas with a new entity that is not valid. + if ($new_entities) { + if ($handler instanceof SelectionWithAutocreateInterface) { + $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities); + $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities); + } + else { + // If the selection handler does not support referencing newly created + // entities, all of them should be invalidated. + $invalid_new_entities = $new_entities; + } + + foreach ($invalid_new_entities as $delta => $entity) { + $this->context->buildViolation($constraint->invalidAutocreateMessage) + ->setParameter('%type', $target_type_id) + ->setParameter('%label', $entity->label()) + ->atPath((string) $delta . '.entity') + ->setInvalidValue($entity) + ->addViolation(); + } } - $referenced_entity = $value->get('entity')->getValue(); - if (!$referenced_entity) { - $type = $value->getFieldDefinition()->getSetting('target_type'); - $this->context->addViolation($constraint->message, array('%type' => $type, '%id' => $id)); + + // Add violations on deltas with a target_id that is not valid. + if ($target_ids) { + $valid_target_ids = $handler->validateReferenceableEntities($target_ids); + if ($invalid_target_ids = array_diff($target_ids, $valid_target_ids)) { + // For accuracy of the error message, differentiate non-referenceable + // and non-existent entities. + $target_type = $this->entityTypeManager->getDefinition($target_type_id); + $existing_ids = $this->entityTypeManager->getStorage($target_type_id)->getQuery() + ->condition($target_type->getKey('id'), $invalid_target_ids, 'IN') + ->execute(); + foreach ($invalid_target_ids as $delta => $target_id) { + $message = in_array($target_id, $existing_ids) ? $constraint->message : $constraint->nonExistingMessage; + $this->context->buildViolation($message) + ->setParameter('%type', $target_type_id) + ->setParameter('%id', $target_id) + ->atPath((string) $delta . '.target_id') + ->setInvalidValue($target_id) + ->addViolation(); + } + } } } + } diff --git a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php index f0b5907de..6a8ed399a 100644 --- a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php +++ b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php @@ -7,7 +7,11 @@ namespace Drupal\Core\Entity\Routing; +use Drupal\Core\Entity\EntityHandlerInterface; +use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -24,7 +28,33 @@ use Symfony\Component\Routing\RouteCollection; * * @internal */ -class DefaultHtmlRouteProvider implements EntityRouteProviderInterface { +class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface { + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * Constructs a new DefaultHtmlRouteProvider. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + */ + public function __construct(EntityManagerInterface $entity_manager) { + $this->entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $container->get('entity.manager') + ); + } /** * {@inheritdoc} @@ -71,6 +101,12 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface { ->setOption('parameters', [ $entity_type_id => ['type' => 'entity:' . $entity_type_id], ]); + + // Entity types with serial IDs can specify this in their route + // requirements, improving the matching process. + if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') { + $route->setRequirement($entity_type_id, '\d+'); + } return $route; } } @@ -102,6 +138,12 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface { ->setOption('parameters', [ $entity_type_id => ['type' => 'entity:' . $entity_type_id], ]); + + // Entity types with serial IDs can specify this in their route + // requirements, improving the matching process. + if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') { + $route->setRequirement($entity_type_id, '\d+'); + } return $route; } } @@ -128,8 +170,33 @@ class DefaultHtmlRouteProvider implements EntityRouteProviderInterface { ->setOption('parameters', [ $entity_type_id => ['type' => 'entity:' . $entity_type_id], ]); + + // Entity types with serial IDs can specify this in their route + // requirements, improving the matching process. + if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') { + $route->setRequirement($entity_type_id, '\d+'); + } return $route; } } + /** + * Gets the type of the ID key for a given entity type. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * An entity type. + * + * @return string|null + * The type of the ID key for a given entity type, or NULL if the entity + * type does not support fields. + */ + protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) { + if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) { + return NULL; + } + + $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type->id()); + return $field_storage_definitions[$entity_type->getKey('id')]->getType(); + } + } diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index 8883f78d0..077c48473 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -1916,16 +1916,16 @@ function hook_entity_extra_field_info() { // Visibility of the ordering of the language selector is the same as on the // node/add form. if ($module_language_enabled) { - $configuration = ContentLanguageSettings::loadByEntityTypeBundle('node', $bundle->type); + $configuration = ContentLanguageSettings::loadByEntityTypeBundle('node', $bundle->id()); if ($configuration->isLanguageAlterable()) { - $extra['node'][$bundle->type]['form']['language'] = array( + $extra['node'][$bundle->id()]['form']['language'] = array( 'label' => t('Language'), 'description' => $description, 'weight' => 0, ); } } - $extra['node'][$bundle->type]['display']['language'] = array( + $extra['node'][$bundle->id()]['display']['language'] = array( 'label' => t('Language'), 'description' => $description, 'weight' => 0, @@ -1948,8 +1948,8 @@ function hook_entity_extra_field_info() { function hook_entity_extra_field_info_alter(&$info) { // Force node title to always be at the top of the list by default. foreach (NodeType::loadMultiple() as $bundle) { - if (isset($info['node'][$bundle->type]['form']['title'])) { - $info['node'][$bundle->type]['form']['title']['weight'] = -20; + if (isset($info['node'][$bundle->id()]['form']['title'])) { + $info['node'][$bundle->id()]['form']['title']['weight'] = -20; } } } diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php deleted file mode 100644 index f6f30fe9c..000000000 --- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php +++ /dev/null @@ -1,48 +0,0 @@ -getRequest(); - - if ($request->attributes->has('_form')) { - $request->attributes->set('_controller', 'controller.form:getContentResult'); - } - } - - /** - * Registers the methods in this class that should be listeners. - * - * @return array - * An array of event listener definitions. - */ - static function getSubscribedEvents() { - $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 25); - - return $events; - } - -} diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php index 770b96f79..2ae21143b 100644 --- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php @@ -147,7 +147,12 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase { } $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST); - $response->setStatusCode($status_code); + // Only 2xx responses should have their status code overridden; any + // other status code should be passed on: redirects (3xx), error (5xx)… + // @see https://www.drupal.org/node/2603788#comment-10504916 + if ($response->isSuccessful()) { + $response->setStatusCode($status_code); + } // Persist any special HTTP headers that were set on the exception. if ($exception instanceof HttpExceptionInterface) { diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php index 943b08bd1..c67f531cc 100644 --- a/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponsePlaceholderStrategySubscriber.php @@ -48,10 +48,6 @@ class HtmlResponsePlaceholderStrategySubscriber implements EventSubscriberInterf * The event to process. */ public function onRespond(FilterResponseEvent $event) { - if (!$event->isMasterRequest()) { - return; - } - $response = $event->getResponse(); if (!$response instanceof HtmlResponse) { return; diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php index 6fada86d8..be64602ad 100644 --- a/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlResponseSubscriber.php @@ -42,10 +42,6 @@ class HtmlResponseSubscriber implements EventSubscriberInterface { * The event to process. */ public function onRespond(FilterResponseEvent $event) { - if (!$event->isMasterRequest()) { - return; - } - $response = $event->getResponse(); if (!$response instanceof HtmlResponse) { return; diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index 1988a143d..c3d868428 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -483,4 +483,18 @@ class ThemeHandler implements ThemeHandlerInterface { throw new \InvalidArgumentException(sprintf('The theme %s does not exist.', $name)); } + /** + * {@inheritdoc} + */ + public function hasUi($name) { + $themes = $this->listInfo(); + if (isset($themes[$name])) { + if (!empty($themes[$name]->info['hidden'])) { + $theme_config = $this->configFactory->get('system.theme'); + return $name == $theme_config->get('default') || $name == $theme_config->get('admin'); + } + return TRUE; + } + return FALSE; + } } diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php index c46f96a00..8b59ae3b8 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php @@ -208,4 +208,18 @@ interface ThemeHandlerInterface { */ public function getTheme($name); + /** + * Determines if a theme should be shown in the user interface. + * + * To be shown in the UI the theme has to be installed. If the theme is hidden + * it will not be shown unless it is the default or admin theme. + * + * @param string $name + * The name of the theme to check. + * + * @return bool + * TRUE if the theme should be shown in the UI, FALSE if not. + */ + public function hasUi($name); + } diff --git a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php index 4217b2ea9..cd221d24b 100644 --- a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php +++ b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php @@ -15,6 +15,16 @@ use Drupal\Core\Form\FormStateInterface; */ class EntityReferenceFieldItemList extends FieldItemList implements EntityReferenceFieldItemListInterface { + /** + * {@inheritdoc} + */ + public function getConstraints() { + $constraints = parent::getConstraints(); + $constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager(); + $constraints[] = $constraint_manager->create('ValidReference', []); + return $constraints; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php index 9e76afbd2..9ad1a9d29 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -40,9 +40,6 @@ use Drupal\Core\Validation\Plugin\Validation\Constraint\AllowedValuesConstraint; * default_widget = "entity_reference_autocomplete", * default_formatter = "entity_reference_label", * list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList", - * default_widget = "entity_reference_autocomplete", - * default_formatter = "entity_reference_label", - * constraints = {"ValidReference" = {}} * ) */ class EntityReferenceItem extends FieldItemBase implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface { @@ -165,20 +162,6 @@ class EntityReferenceItem extends FieldItemBase implements OptionsProviderInterf unset($constraints[$key]); } } - list($current_handler) = explode(':', $this->getSetting('handler'), 2); - if ($current_handler === 'default') { - $handler_settings = $this->getSetting('handler_settings'); - if (isset($handler_settings['target_bundles'])) { - $constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager(); - $constraints[] = $constraint_manager->create('ComplexData', [ - 'entity' => [ - 'Bundle' => [ - 'bundle' => $handler_settings['target_bundles'], - ], - ], - ]); - } - } return $constraints; } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php index 1481d884e..9d62405a1 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UriItem.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Field\Plugin\Field\FieldType; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\TypedData\DataDefinition; @@ -77,4 +79,16 @@ class UriItem extends StringItem { return parent::isEmpty(); } + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + $values = parent::generateSampleValue($field_definition); + $suffix_length = $field_definition->getSetting('max_length') - 7; + foreach ($values as $key => $value) { + $values[$key] = 'http://' . Unicode::substr($value, 0, $suffix_length); + } + return $values; + } + } diff --git a/core/lib/Drupal/Core/Menu/LocalTaskManager.php b/core/lib/Drupal/Core/Menu/LocalTaskManager.php index 512ccfea9..1791efebe 100644 --- a/core/lib/Drupal/Core/Menu/LocalTaskManager.php +++ b/core/lib/Drupal/Core/Menu/LocalTaskManager.php @@ -304,7 +304,9 @@ class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerI } // Pre-fetch all routes involved in the tree. This reduces the number // of SQL queries that would otherwise be triggered by the access manager. - $routes = $route_names ? $this->routeProvider->getRoutesByNames($route_names) : array(); + if ($route_names) { + $this->routeProvider->getRoutesByNames($route_names); + } foreach ($tree as $level => $instances) { /** @var $instances \Drupal\Core\Menu\LocalTaskInterface[] */ diff --git a/core/lib/Drupal/Core/Menu/MenuLinkManager.php b/core/lib/Drupal/Core/Menu/MenuLinkManager.php index 106407b2b..beb0d95f7 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkManager.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkManager.php @@ -351,8 +351,11 @@ class MenuLinkManager implements MenuLinkManagerInterface { * {@inheritdoc} */ public function addDefinition($id, array $definition) { - if ($this->treeStorage->load($id) || $id === '') { - throw new PluginException("The ID $id already exists as a plugin definition or is not valid"); + if ($this->treeStorage->load($id)) { + throw new PluginException("The menu link ID $id already exists as a plugin definition"); + } + elseif ($id === '') { + throw new PluginException("The menu link ID cannot be empty"); } // Add defaults, so there is no requirement to specify everything. $this->processDefinition($definition, $id); diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php index 5b3aee85f..3c71930f3 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkTree.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php @@ -177,6 +177,7 @@ class MenuLinkTree implements MenuLinkTreeInterface { // Add the theme wrapper for outer markup. // Allow menu-specific theme overrides. $build['#theme'] = 'menu__' . strtr($menu_name, '-', '_'); + $build['#menu_name'] = $menu_name; $build['#items'] = $items; // Set cache tag. $build['#cache']['tags'][] = 'config:system.menu.' . $menu_name; diff --git a/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php index 899c39e08..8caae97e6 100644 --- a/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -11,9 +11,14 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Database\Query\Condition; /** * Provides a class for CRUD operations on path aliases. + * + * All queries perform case-insensitive matching on the 'source' and 'alias' + * fields, so the aliases '/test-alias' and '/test-Alias' are considered to be + * the same, and will both refer to the same internal system path. */ class AliasStorage implements AliasStorageInterface { /** @@ -98,7 +103,13 @@ class AliasStorage implements AliasStorageInterface { public function load($conditions) { $select = $this->connection->select('url_alias'); foreach ($conditions as $field => $value) { - $select->condition($field, $value); + if ($field == 'source' || $field == 'alias') { + // Use LIKE for case-insensitive matching. + $select->condition($field, $this->connection->escapeLike($value), 'LIKE'); + } + else { + $select->condition($field, $value); + } } return $select ->fields('url_alias') @@ -115,7 +126,13 @@ class AliasStorage implements AliasStorageInterface { $path = $this->load($conditions); $query = $this->connection->delete('url_alias'); foreach ($conditions as $field => $value) { - $query->condition($field, $value); + if ($field == 'source' || $field == 'alias') { + // Use LIKE for case-insensitive matching. + $query->condition($field, $this->connection->escapeLike($value), 'LIKE'); + } + else { + $query->condition($field, $value); + } } $deleted = $query->execute(); // @todo Switch to using an event for this instead of a hook. @@ -128,90 +145,101 @@ class AliasStorage implements AliasStorageInterface { * {@inheritdoc} */ public function preloadPathAlias($preloaded, $langcode) { - $args = array( - ':system[]' => $preloaded, - ':langcode' => $langcode, - ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - ); + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + $select = $this->connection->select('url_alias') + ->fields('url_alias', ['source', 'alias']); + + if (!empty($preloaded)) { + $conditions = new Condition('OR'); + foreach ($preloaded as $preloaded_item) { + $conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE'); + } + $select->condition($conditions); + } + // Always get the language-specific alias before the language-neutral one. // For example 'de' is less than 'und' so the order needs to be ASC, while // 'xx-lolspeak' is more than 'und' so the order needs to be DESC. We also // order by pid ASC so that fetchAllKeyed() returns the most recently // created alias for each source. Subsequent queries using fetchField() must - // use pid DESC to have the same effect. For performance reasons, the query - // builder is not used here. + // use pid DESC to have the same effect. if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - // Prevent PDO from complaining about a token the query doesn't use. - unset($args[':langcode']); - $result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode = :langcode_undetermined ORDER BY pid ASC', $args); + array_pop($langcode_list); } elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid ASC', $args); + $select->orderBy('langcode', 'ASC'); } else { - $result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid ASC', $args); + $select->orderBy('langcode', 'DESC'); } - return $result->fetchAllKeyed(); + $select->orderBy('pid', 'ASC'); + $select->condition('langcode', $langcode_list, 'IN'); + return $select->execute()->fetchAllKeyed(); } /** * {@inheritdoc} */ public function lookupPathAlias($path, $langcode) { - $args = array( - ':source' => $path, - ':langcode' => $langcode, - ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - ); - // See the queries above. + $source = $this->connection->escapeLike($path); + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + + // See the queries above. Use LIKE for case-insensitive matching. + $select = $this->connection->select('url_alias') + ->fields('url_alias', ['alias']) + ->condition('source', $source, 'LIKE'); if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - unset($args[':langcode']); - $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode = :langcode_undetermined ORDER BY pid DESC", $args)->fetchField(); + array_pop($langcode_list); } elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args)->fetchField(); + $select->orderBy('langcode', 'DESC'); } else { - $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args)->fetchField(); + $select->orderBy('langcode', 'ASC'); } - return $alias; + $select->orderBy('pid', 'DESC'); + $select->condition('langcode', $langcode_list, 'IN'); + return $select->execute()->fetchField(); } /** * {@inheritdoc} */ public function lookupPathSource($path, $langcode) { - $args = array( - ':alias' => $path, - ':langcode' => $langcode, - ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - ); - // See the queries above. + $alias = $this->connection->escapeLike($path); + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + + // See the queries above. Use LIKE for case-insensitive matching. + $select = $this->connection->select('url_alias') + ->fields('url_alias', ['source']) + ->condition('alias', $alias, 'LIKE'); if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - unset($args[':langcode']); - $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args); + array_pop($langcode_list); } elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args); + $select->orderBy('langcode', 'DESC'); } else { - $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args); + $select->orderBy('langcode', 'ASC'); } - return $result->fetchField(); + $select->orderBy('pid', 'DESC'); + $select->condition('langcode', $langcode_list, 'IN'); + return $select->execute()->fetchField(); } /** * {@inheritdoc} */ public function aliasExists($alias, $langcode, $source = NULL) { + // Use LIKE and NOT LIKE for case-insensitive matching. $query = $this->connection->select('url_alias') - ->condition('alias', $alias) + ->condition('alias', $this->connection->escapeLike($alias), 'LIKE') ->condition('langcode', $langcode); if (!empty($source)) { - $query->condition('source', $source, '<>'); + $query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE'); } $query->addExpression('1'); $query->range(0, 1); diff --git a/core/lib/Drupal/Core/Path/AliasStorageInterface.php b/core/lib/Drupal/Core/Path/AliasStorageInterface.php index 5ac77a3ef..3b9c4eecd 100644 --- a/core/lib/Drupal/Core/Path/AliasStorageInterface.php +++ b/core/lib/Drupal/Core/Path/AliasStorageInterface.php @@ -44,6 +44,9 @@ interface AliasStorageInterface { /** * Fetches a specific URL alias from the database. * + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. + * * @param array $conditions * An array of query conditions. * @@ -60,6 +63,9 @@ interface AliasStorageInterface { /** * Deletes a URL alias. * + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. + * * @param array $conditions * An array of criteria. */ @@ -82,6 +88,9 @@ interface AliasStorageInterface { /** * Returns an alias of Drupal system URL. * + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. + * * @param string $path * The path to investigate for corresponding path aliases. * @param string $langcode @@ -96,6 +105,9 @@ interface AliasStorageInterface { /** * Returns Drupal system URL of an alias. * + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. + * * @param string $path * The path to investigate for corresponding system URLs. * @param string $langcode @@ -110,6 +122,9 @@ interface AliasStorageInterface { /** * Checks if alias already exists. * + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. + * * @param string $alias * Alias to check against. * @param string $langcode @@ -135,8 +150,9 @@ interface AliasStorageInterface { * * @param array $header * Table header. - * @param string[]|null $keys - * (optional) Search keys. + * @param string|null $keys + * (optional) Search keyword that may include one or more '*' as wildcard + * values. * * @return array * Array of items to be displayed on the current page. diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php index 59f2d0c51..cad93c269 100644 --- a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php +++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php @@ -21,8 +21,27 @@ interface OutboundPathProcessorInterface { * @param string $path * The path to process, with a leading slash. * @param array $options - * An array of options such as would be passed to the generator's - * generateFromRoute() method. + * (optional) An associative array of additional options, with the following + * elements: + * - 'query': An array of query key/value-pairs (without any URL-encoding) + * to append to the URL. + * - 'fragment': A fragment identifier (named anchor) to append to the URL. + * Do not include the leading '#' character. + * - 'absolute': Defaults to FALSE. Whether to force the output to be an + * absolute link (beginning with http:). Useful for links that will be + * displayed outside the site, such as in an RSS feed. + * - 'language': An optional language object used to look up the alias + * for the URL. If $options['language'] is omitted, it defaults to the + * current language for the language type LanguageInterface::TYPE_URL. + * - 'https': Whether this URL should point to a secure location. If not + * defined, the current scheme is used, so the user stays on HTTP or HTTPS + * respectively. TRUE enforces HTTPS and FALSE enforces HTTP. + * - 'base_url': Only used internally by a path processor, for example, to + * modify the base URL when a language dependent URL requires so. + * - 'prefix': Only used internally, to modify the path when a language + * dependent URL requires so. + * - 'route': The route object for the given path. It will be set by + * \Drupal\Core\Routing\UrlGenerator::generateFromRoute(). * @param \Symfony\Component\HttpFoundation\Request $request * The HttpRequest object representing the current request. * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index 47c84024c..b6f95bcd6 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -269,6 +269,11 @@ abstract class RenderElement extends PluginBase implements ElementInterface { return $element; } + // Add a data attribute to disable automatic refocus after ajax call. + if (!empty($element['#ajax']['disable-refocus'])) { + $element['#attributes']['data-disable-refocus'] = "true"; + } + // Add a reasonable default event handler if none was specified. if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) { switch ($element['#type']) { diff --git a/core/lib/Drupal/Core/Routing/Enhancer/FormRouteEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/FormRouteEnhancer.php new file mode 100644 index 000000000..863accad9 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/FormRouteEnhancer.php @@ -0,0 +1,33 @@ +hasDefault('_form') && !$route->hasDefault('_controller'); + } + + /** + * {@inheritdoc} + */ + public function enhance(array $defaults, Request $request) { + $defaults['_controller'] = 'controller.form:getContentResult'; + return $defaults; + } + +} diff --git a/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php index 3c1d282ec..a476006e5 100644 --- a/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php +++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php @@ -44,8 +44,12 @@ class ParamConversionEnhancer implements RouteEnhancerInterface, EventSubscriber * {@inheritdoc} */ public function enhance(array $defaults, Request $request) { - $defaults['_raw_variables'] = $this->copyRawVariables($defaults); - return $this->paramConverterManager->convert($defaults); + // Just run the parameter conversion once per request. + if (!isset($defaults['_raw_variables'])) { + $defaults['_raw_variables'] = $this->copyRawVariables($defaults); + $defaults = $this->paramConverterManager->convert($defaults); + } + return $defaults; } /** diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php index 937cff71f..ac8188790 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -246,7 +246,7 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv * @return array * An array of outlines that could match the specified path parts. */ - public function getCandidateOutlines(array $parts) { + protected function getCandidateOutlines(array $parts) { $number_parts = count($parts); $ancestors = array(); $length = $number_parts - 1; @@ -355,7 +355,7 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv /** * Comparison function for usort on routes. */ - public function routeProviderRouteCompare(array $a, array $b) { + protected function routeProviderRouteCompare(array $a, array $b) { if ($a['fit'] == $b['fit']) { return strcmp($a['name'], $b['name']); } diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index f3d1275b2..62ece640d 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -309,6 +309,9 @@ class UrlGenerator implements UrlGeneratorInterface { $name = $this->getRouteDebugMessage($name); $this->processRoute($name, $route, $parameters, $generated_url); $path = $this->getInternalPathFromRoute($name, $route, $parameters, $query_params); + // Outbound path processors might need the route object for the path, e.g. + // to get the path pattern. + $options['route'] = $route; $path = $this->processPath($path, $options, $generated_url); if (!empty($options['prefix'])) { diff --git a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php index 7c33fd4b6..eb77445c8 100644 --- a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php +++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php @@ -77,6 +77,10 @@ interface UrlGeneratorInterface extends VersatileGeneratorInterface { * @throws \Symfony\Component\Routing\Exception\InvalidParameterException * Thrown when a parameter value for a placeholder is not correct because it * does not match the requirement. + * + * @internal + * Should not be used in user code. + * Use \Drupal\Core\Url instead. */ public function generateFromRoute($name, $parameters = array(), $options = array(), $collect_bubbleable_metadata = FALSE); diff --git a/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php b/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php index db8019b18..514cad3e0 100644 --- a/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php +++ b/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php @@ -47,28 +47,41 @@ class ReverseProxyMiddleware implements HttpKernelInterface { */ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { // Initialize proxy settings. - if ($this->settings->get('reverse_proxy', FALSE)) { - $ip_header = $this->settings->get('reverse_proxy_header', 'X_FORWARDED_FOR'); + static::setSettingsOnRequest($request, $this->settings); + return $this->httpKernel->handle($request, $type, $catch); + } + + /** + * Sets reverse proxy settings on Request object. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * A Request instance. + * @param \Drupal\Core\Site\Settings $settings + * The site settings. + */ + public static function setSettingsOnRequest(Request $request, Settings $settings) { + // Initialize proxy settings. + if ($settings->get('reverse_proxy', FALSE)) { + $ip_header = $settings->get('reverse_proxy_header', 'X_FORWARDED_FOR'); $request::setTrustedHeaderName($request::HEADER_CLIENT_IP, $ip_header); - $proto_header = $this->settings->get('reverse_proxy_proto_header', 'X_FORWARDED_PROTO'); + $proto_header = $settings->get('reverse_proxy_proto_header', 'X_FORWARDED_PROTO'); $request::setTrustedHeaderName($request::HEADER_CLIENT_PROTO, $proto_header); - $host_header = $this->settings->get('reverse_proxy_host_header', 'X_FORWARDED_HOST'); + $host_header = $settings->get('reverse_proxy_host_header', 'X_FORWARDED_HOST'); $request::setTrustedHeaderName($request::HEADER_CLIENT_HOST, $host_header); - $port_header = $this->settings->get('reverse_proxy_port_header', 'X_FORWARDED_PORT'); + $port_header = $settings->get('reverse_proxy_port_header', 'X_FORWARDED_PORT'); $request::setTrustedHeaderName($request::HEADER_CLIENT_PORT, $port_header); - $forwarded_header = $this->settings->get('reverse_proxy_forwarded_header', 'FORWARDED'); + $forwarded_header = $settings->get('reverse_proxy_forwarded_header', 'FORWARDED'); $request::setTrustedHeaderName($request::HEADER_FORWARDED, $forwarded_header); - $proxies = $this->settings->get('reverse_proxy_addresses', array()); + $proxies = $settings->get('reverse_proxy_addresses', array()); if (count($proxies) > 0) { $request::setTrustedProxies($proxies); } } - return $this->httpKernel->handle($request, $type, $catch); } } diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php index e9962d6eb..872d57f63 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php @@ -134,6 +134,11 @@ class TranslationManager implements TranslationInterface, TranslatorInterface { * The translated string. */ protected function doTranslate($string, array $options = array()) { + // If a NULL langcode has been provided, unset it. + if (!isset($options['langcode']) && array_key_exists('langcode', $options)) { + unset($options['langcode']); + } + // Merge in options defaults. $options = $options + [ 'langcode' => $this->defaultLangcode, diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index 26a1793cd..8aaac3bb0 100644 --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -117,10 +117,11 @@ class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface { * An AttributeValueBase representation of the attribute's value. */ protected function createAttributeValue($name, $value) { - // If the value is already an AttributeValueBase object, return it - // straight away. + // If the value is already an AttributeValueBase object, + // return a new instance of the same class, but with the new name. if ($value instanceof AttributeValueBase) { - return $value; + $class = get_class($value); + return new $class($name, $value->value()); } // An array value or 'class' attribute name are forced to always be an // AttributeArray value for consistency. diff --git a/core/lib/Drupal/Core/Theme/MissingThemeDependencyException.php b/core/lib/Drupal/Core/Theme/MissingThemeDependencyException.php new file mode 100644 index 000000000..0708450f8 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/MissingThemeDependencyException.php @@ -0,0 +1,47 @@ +theme = $theme; + } + + /** + * Gets the machine name of the missing theme. + * + * @return string + * The machine name of the theme that is missing. + */ + public function getMissingThemeName() { + return $this->theme; + } + +} diff --git a/core/lib/Drupal/Core/Theme/ThemeInitialization.php b/core/lib/Drupal/Core/Theme/ThemeInitialization.php index 95f27a9d0..d0c91662e 100644 --- a/core/lib/Drupal/Core/Theme/ThemeInitialization.php +++ b/core/lib/Drupal/Core/Theme/ThemeInitialization.php @@ -109,6 +109,16 @@ class ThemeInitialization implements ThemeInitializationInterface { $ancestor = $theme_name; while ($ancestor && isset($themes[$ancestor]->base_theme)) { $ancestor = $themes[$ancestor]->base_theme; + if (!$this->themeHandler->themeExists($ancestor)) { + if ($ancestor == 'stable') { + // Themes that depend on Stable will be fixed by system_update_8014(). + // There is no harm in not adding it as an ancestor since at worst + // some people might experience slight visual regressions on + // update.php. + continue; + } + throw new MissingThemeDependencyException(sprintf('Base theme %s has not been installed.', $ancestor), $ancestor); + } $base_themes[] = $themes[$ancestor]; } diff --git a/core/lib/Drupal/Core/Theme/ThemeInitializationInterface.php b/core/lib/Drupal/Core/Theme/ThemeInitializationInterface.php index f2b1547ed..a3b54df0b 100644 --- a/core/lib/Drupal/Core/Theme/ThemeInitializationInterface.php +++ b/core/lib/Drupal/Core/Theme/ThemeInitializationInterface.php @@ -34,6 +34,9 @@ interface ThemeInitializationInterface { * * @return \Drupal\Core\Theme\ActiveTheme * An active theme object instance for the given theme. + * + * @throws \Drupal\Core\Theme\MissingThemeDependencyException + * Thrown when base theme for installed theme is not installed. */ public function getActiveThemeByName($theme_name); @@ -54,8 +57,8 @@ interface ThemeInitializationInterface { * @param \Drupal\Core\Extension\Extension $theme * The theme extension object. * @param \Drupal\Core\Extension\Extension[] $base_themes - * An array of extension objects of base theme and its bases. It is ordered - * by 'oldest first', meaning the top level of the chain will be first. + * An array of extension objects of base theme and its bases. It is ordered + * by 'next parent first', meaning the top level of the chain will be first. * * @return \Drupal\Core\Theme\ActiveTheme * The active theme instance for the passed in $theme. diff --git a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php index 10aa8c6fc..8029d7093 100644 --- a/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php +++ b/core/lib/Drupal/Core/Utility/LinkGeneratorInterface.php @@ -72,6 +72,9 @@ interface LinkGeneratorInterface { * @throws \Symfony\Component\Routing\Exception\InvalidParameterException * Thrown when a parameter value for a placeholder is not correct because it * does not match the requirement. + * + * @internal + * Should not be used in user code. Use \Drupal\Core\Link instead. */ public function generate($text, Url $url); @@ -84,6 +87,10 @@ interface LinkGeneratorInterface { * @return \Drupal\Core\GeneratedLink * A GeneratedLink object containing a link to the given route and * parameters and bubbleable metadata. + * + * @internal + * Should not be used in user code. + * Use \Drupal\Core\Link instead. */ public function generateFromLink(Link $link); diff --git a/core/misc/ajax.js b/core/misc/ajax.js index 2a6616dc7..b63bc0344 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -735,9 +735,36 @@ } $(this.element).prop('disabled', false); + // Save element's ancestors tree so if the element is removed from the dom + // we can try to refocus one of its parents. Using addBack reverse the + // result array, meaning that index 0 is the highest parent in the hierarchy + // in this situation it is usually a
element. + var elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray(); + + // Track if any command is altering the focus so we can avoid changing the + // focus set by the Ajax command. + var focusChanged = false; for (var i in response) { if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) { this.commands[response[i].command](this, response[i], status); + if (response[i].command === 'invoke' && response[i].method === 'focus') { + focusChanged = true; + } + } + } + + // If the focus hasn't be changed by the ajax commands, try to refocus the + // triggering element or one of its parents if that element does not exist + // anymore. + if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) { + var target = false; + + for (var n = elementParents.length - 1; !target && n > 0; n--) { + target = document.querySelector('[data-drupal-selector="' + elementParents[n].getAttribute('data-drupal-selector') + '"]'); + } + + if (target) { + $(target).trigger('focus'); } } diff --git a/core/misc/drupalSettingsLoader.js b/core/misc/drupalSettingsLoader.js index da61b7edd..7ff292efb 100644 --- a/core/misc/drupalSettingsLoader.js +++ b/core/misc/drupalSettingsLoader.js @@ -7,7 +7,8 @@ 'use strict'; - var settingsElement = document.querySelector('script[type="application/json"][data-drupal-selector="drupal-settings-json"]'); + // Use direct child elements to harden against XSS exploits when CSP is on. + var settingsElement = document.querySelector('head > script[type="application/json"][data-drupal-selector="drupal-settings-json"], body > script[type="application/json"][data-drupal-selector="drupal-settings-json"]'); /** * Variable generated by Drupal with all the configuration created from PHP. diff --git a/core/misc/progress.js b/core/misc/progress.js index bf678f55b..26757518e 100644 --- a/core/misc/progress.js +++ b/core/misc/progress.js @@ -120,6 +120,7 @@ type: this.method, url: uri, data: '', + dataType: 'json', success: function (progress) { // Display errors. if (progress.status === 0) { diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index aa512acea..6a27d8fb2 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -236,14 +236,10 @@ // Add event bindings to the document. The self variable is passed along // as event handlers do not have direct access to the tableDrag object. - if (Modernizr.touchevents) { - $(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); }); - $(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); }); - } - else { - $(document).on('mousemove', function (event) { return self.dragRow(event, self); }); - $(document).on('mouseup', function (event) { return self.dropRow(event, self); }); - } + $(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); }); + $(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); }); + $(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); }); + $(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); }); // React to localStorage event showing or hiding weight columns. $(window).on('storage', $.proxy(function (e) { @@ -460,19 +456,13 @@ $item.find('td').eq(0).prepend(handle); } - if (Modernizr.touchevents) { - handle.on('touchstart', function (event) { - event.preventDefault(); + handle.on('mousedown touchstart pointerdown', function (event) { + event.preventDefault(); + if (event.originalEvent.type === 'touchstart') { event = event.originalEvent.touches[0]; - self.dragStart(event, self, item); - }); - } - else { - handle.on('mousedown', function (event) { - event.preventDefault(); - self.dragStart(event, self, item); - }); - } + } + self.dragStart(event, self, item); + }); // Prevent the anchor tag from jumping us to the top of the page. handle.on('click', function (e) { diff --git a/core/modules/aggregator/src/Tests/Migrate/MigrateAggregatorStubTest.php b/core/modules/aggregator/src/Tests/Migrate/MigrateAggregatorStubTest.php new file mode 100644 index 000000000..d620259a5 --- /dev/null +++ b/core/modules/aggregator/src/Tests/Migrate/MigrateAggregatorStubTest.php @@ -0,0 +1,63 @@ +installEntitySchema('aggregator_feed'); + $this->installEntitySchema('aggregator_item'); + } + + /** + * Tests creation of aggregator feed stubs. + */ + public function testFeedStub() { + $this->performStubTest('aggregator_feed'); + } + + /** + * Tests creation of aggregator feed items. + */ + public function testItemStub() { + try { + // We expect an exception, because there's no feed to reference. + $this->performStubTest('aggregator_item'); + $this->fail('Expected exception has not been thrown.'); + } + catch (MigrateException $e) { + $this->assertIdentical($e->getMessage(), + 'Stubbing failed, unable to generate value for field fid'); + } + + // The stub should pass when there's a feed to point to. + $this->createStub('aggregator_feed'); + $this->performStubTest('aggregator_item'); + } + +} diff --git a/core/modules/block/block.module b/core/modules/block/block.module index 29c6e16bf..0b6ca2631 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -89,7 +89,10 @@ function block_page_top(array &$page_top) { */ function block_themes_installed($theme_list) { foreach ($theme_list as $theme) { - block_theme_initialize($theme); + // Don't initialize themes that are not displayed in the UI. + if (\Drupal::service('theme_handler')->hasUi($theme)) { + block_theme_initialize($theme); + } } } diff --git a/core/modules/block/src/Controller/BlockController.php b/core/modules/block/src/Controller/BlockController.php index 2779a9b57..effa84266 100644 --- a/core/modules/block/src/Controller/BlockController.php +++ b/core/modules/block/src/Controller/BlockController.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Extension\ThemeHandlerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Controller routines for admin block routes. @@ -53,6 +54,10 @@ class BlockController extends ControllerBase { * A #type 'page' render array containing the block region demo. */ public function demo($theme) { + if (!$this->themeHandler->hasUi($theme)) { + throw new NotFoundHttpException(); + } + $page = [ '#title' => Html::escape($this->themeHandler->getName($theme)), '#type' => 'page', diff --git a/core/modules/block/src/Controller/BlockListController.php b/core/modules/block/src/Controller/BlockListController.php index 72aa445c0..6185c9097 100644 --- a/core/modules/block/src/Controller/BlockListController.php +++ b/core/modules/block/src/Controller/BlockListController.php @@ -8,13 +8,42 @@ namespace Drupal\block\Controller; use Drupal\Core\Entity\Controller\EntityListController; +use Drupal\Core\Extension\ThemeHandlerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Defines a controller to list blocks. */ class BlockListController extends EntityListController { + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** + * Constructs the BlockListController. + * + * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler + * The theme handler. + */ + public function __construct(ThemeHandlerInterface $theme_handler) { + $this->themeHandler = $theme_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('theme_handler') + ); + } + /** * Shows the block administration page. * @@ -28,6 +57,10 @@ class BlockListController extends EntityListController { */ public function listing($theme = NULL, Request $request = NULL) { $theme = $theme ?: $this->config('system.theme')->get('default'); + if (!$this->themeHandler->hasUi($theme)) { + throw new NotFoundHttpException(); + } + return $this->entityManager()->getListBuilder('block')->render($theme, $request); } diff --git a/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php b/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php index 4e337db7b..ed9d09544 100644 --- a/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php +++ b/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php @@ -50,7 +50,7 @@ class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface { $default_theme = $this->themeHandler->getDefault(); foreach ($this->themeHandler->listInfo() as $theme_name => $theme) { - if ($theme->status) { + if ($this->themeHandler->hasUi($theme_name)) { $this->derivatives[$theme_name] = $base_plugin_definition; $this->derivatives[$theme_name]['title'] = $theme->info['name']; $this->derivatives[$theme_name]['route_parameters'] = array('theme' => $theme_name); diff --git a/core/modules/block/src/Tests/BlockHiddenRegionTest.php b/core/modules/block/src/Tests/BlockHiddenRegionTest.php index a6af2ce5e..bb42dceea 100644 --- a/core/modules/block/src/Tests/BlockHiddenRegionTest.php +++ b/core/modules/block/src/Tests/BlockHiddenRegionTest.php @@ -56,7 +56,9 @@ class BlockHiddenRegionTest extends WebTestBase { // Install "block_test_theme" and set it as the default theme. $theme = 'block_test_theme'; - \Drupal::service('theme_handler')->install(array($theme)); + // We need to install a non-hidden theme so that there is more than one + // local task. + \Drupal::service('theme_handler')->install(array($theme, 'stark')); $this->config('system.theme') ->set('default', $theme) ->save(); diff --git a/core/modules/block/src/Tests/BlockTest.php b/core/modules/block/src/Tests/BlockTest.php index c98b042de..261c3ae6d 100644 --- a/core/modules/block/src/Tests/BlockTest.php +++ b/core/modules/block/src/Tests/BlockTest.php @@ -197,9 +197,9 @@ class BlockTest extends BlockTestBase { */ public function testBlockThemeSelector() { // Install all themes. - \Drupal::service('theme_handler')->install(array('bartik', 'seven')); + \Drupal::service('theme_handler')->install(['bartik', 'seven', 'stark']); $theme_settings = $this->config('system.theme'); - foreach (array('bartik', 'classy', 'seven') as $theme) { + foreach (['bartik', 'seven', 'stark'] as $theme) { $this->drupalGet('admin/structure/block/list/' . $theme); $this->assertTitle(t('Block layout') . ' | Drupal'); // Select the 'Powered by Drupal' block to be placed. diff --git a/core/modules/block/src/Tests/BlockUiTest.php b/core/modules/block/src/Tests/BlockUiTest.php index 25b7942a5..4d0f03a1e 100644 --- a/core/modules/block/src/Tests/BlockUiTest.php +++ b/core/modules/block/src/Tests/BlockUiTest.php @@ -90,6 +90,10 @@ class BlockUiTest extends WebTestBase { \Drupal::service('theme_handler')->install(array('test_theme')); $this->drupalGet('admin/structure/block/demo/test_theme'); $this->assertEscaped('Test theme'); + + \Drupal::service('theme_handler')->install(['stable']); + $this->drupalGet('admin/structure/block/demo/stable'); + $this->assertResponse(404, 'Hidden themes that are not the default theme are not supported by the block demo screen'); } /** @@ -136,6 +140,28 @@ class BlockUiTest extends WebTestBase { $this->drupalGet('admin/structure/block'); $element = $this->xpath('//tr[contains(@class, :class)]', [':class' => 'region-title-header']); $this->assertTrue(!empty($element)); + + // Ensure hidden themes do not appear in the UI. Enable another non base + // theme and place the local tasks block. + $this->assertTrue(\Drupal::service('theme_handler')->themeExists('classy'), 'The classy base theme is enabled'); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header']); + \Drupal::service('theme_installer')->install(['stable', 'stark']); + $this->drupalGet('admin/structure/block'); + $theme_handler = \Drupal::service('theme_handler'); + $this->assertLink($theme_handler->getName('classy')); + $this->assertLink($theme_handler->getName('stark')); + $this->assertNoLink($theme_handler->getName('stable')); + + $this->drupalGet('admin/structure/block/list/stable'); + $this->assertResponse(404, 'Placing blocks through UI is not possible for a hidden base theme.'); + + \Drupal::configFactory()->getEditable('system.theme')->set('admin', 'stable')->save(); + \Drupal::service('router.builder')->rebuildIfNeeded(); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header', 'theme' => 'stable']); + $this->drupalGet('admin/structure/block'); + $this->assertLink($theme_handler->getName('stable')); + $this->drupalGet('admin/structure/block/list/stable'); + $this->assertResponse(200, 'Placing blocks through UI is possible for a hidden base theme that is the admin theme.'); } /** diff --git a/core/modules/block/src/Tests/NewDefaultThemeBlocksTest.php b/core/modules/block/src/Tests/NewDefaultThemeBlocksTest.php index 68592c84f..6d14eedb6 100644 --- a/core/modules/block/src/Tests/NewDefaultThemeBlocksTest.php +++ b/core/modules/block/src/Tests/NewDefaultThemeBlocksTest.php @@ -65,6 +65,14 @@ class NewDefaultThemeBlocksTest extends WebTestBase { unset($new_blocks[str_replace($default_theme . '_', $new_theme . '_', $default_block_name)]); } $this->assertTrue(empty($new_blocks), 'The new theme has exactly the same blocks as the previous default theme.'); + + // Install a hidden base theme and ensure blocks are not copied. + $base_theme = 'test_basetheme'; + \Drupal::service('theme_handler')->install([$base_theme]); + $new_blocks = $this->container->get('entity.query')->get('block') + ->condition('theme', $base_theme) + ->execute(); + $this->assertTrue(empty($new_blocks), 'Installing a hidden base theme does not copy blocks from the default theme.'); } } diff --git a/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php b/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php index f70a57cf7..632091754 100644 --- a/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php +++ b/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php @@ -27,7 +27,11 @@ class BlockLocalTasksTest extends LocalTaskIntegrationTestBase { $themes = array(); $themes['test_a'] = (object) array( - 'status' => 0, + 'status' => 1, + 'info' => array( + 'name' => 'test_a', + 'hidden' => TRUE, + ), ); $themes['test_b'] = (object) array( 'status' => 1, @@ -45,6 +49,13 @@ class BlockLocalTasksTest extends LocalTaskIntegrationTestBase { $theme_handler->expects($this->any()) ->method('listInfo') ->will($this->returnValue($themes)); + $theme_handler->expects($this->any()) + ->method('hasUi') + ->willReturnMap([ + ['test_a', FALSE], + ['test_b', TRUE], + ['test_c', TRUE], + ]); $container = new ContainerBuilder(); $container->set('config.factory', $config_factory); diff --git a/core/modules/block_content/block_content.routing.yml b/core/modules/block_content/block_content.routing.yml index 8e4f649cc..75ea9b29d 100644 --- a/core/modules/block_content/block_content.routing.yml +++ b/core/modules/block_content/block_content.routing.yml @@ -44,6 +44,7 @@ entity.block_content.canonical: _admin_route: TRUE requirements: _entity_access: 'block_content.update' + block_content: \d+ entity.block_content.edit_form: path: '/block/{block_content}' @@ -53,6 +54,7 @@ entity.block_content.edit_form: _admin_route: TRUE requirements: _entity_access: 'block_content.update' + block_content: \d+ entity.block_content.delete_form: path: '/block/{block_content}/delete' @@ -63,6 +65,7 @@ entity.block_content.delete_form: _admin_route: TRUE requirements: _entity_access: 'block_content.delete' + block_content: \d+ block_content.type_add: path: '/admin/structure/block/block-content/types/add' diff --git a/core/modules/block_content/src/Tests/BlockContentTypeTest.php b/core/modules/block_content/src/Tests/BlockContentTypeTest.php index e669a72e3..d1405b9ee 100644 --- a/core/modules/block_content/src/Tests/BlockContentTypeTest.php +++ b/core/modules/block_content/src/Tests/BlockContentTypeTest.php @@ -185,17 +185,15 @@ class BlockContentTypeTest extends BlockContentTestBase { ->getStorage('block_content'); // Install all themes. - \Drupal::service('theme_handler')->install(array('bartik', 'seven')); - $themes = array('bartik', 'seven', 'classy'); + \Drupal::service('theme_handler')->install(['bartik', 'seven', 'stark']); $theme_settings = $this->config('system.theme'); - foreach ($themes as $default_theme) { + foreach (['bartik', 'seven', 'stark'] as $default_theme) { // Change the default theme. $theme_settings->set('default', $default_theme)->save(); \Drupal::service('router.builder')->rebuild(); // For each installed theme, go to its block page and test the redirects. - $themes = array('bartik', 'classy', 'seven'); - foreach ($themes as $theme) { + foreach (['bartik', 'seven', 'stark'] as $theme) { // Test that adding a block from the 'place blocks' form sends you to the // block configure form. $path = $theme == $default_theme ? 'admin/structure/block' : "admin/structure/block/list/$theme"; diff --git a/core/modules/block_content/src/Tests/Migrate/MigrateBlockContentStubTest.php b/core/modules/block_content/src/Tests/Migrate/MigrateBlockContentStubTest.php new file mode 100644 index 000000000..5b28c3705 --- /dev/null +++ b/core/modules/block_content/src/Tests/Migrate/MigrateBlockContentStubTest.php @@ -0,0 +1,63 @@ +installEntitySchema('block_content'); + } + + /** + * Tests creation of block content stubs with no block_content_type available. + */ + public function testStubFailure() { + $message = 'Expected MigrateException thrown when no bundles exist.'; + try { + $this->createStub('block_content'); + $this->fail($message); + } + catch (MigrateException $e) { + $this->pass($message); + $this->assertEqual('Stubbing failed, no bundles available for entity type: block_content', $e->getMessage()); + } + } + + /** + * Tests creation of block content stubs when there is a block_content_type. + */ + public function testStubSuccess() { + BlockContentType::create([ + 'id' => 'test_block_content_type', + 'label' => 'Test block content type', + ])->save(); + $this->performStubTest('block_content'); + } + +} diff --git a/core/modules/book/book.routing.yml b/core/modules/book/book.routing.yml index 9b8f32293..772a6cf39 100644 --- a/core/modules/book/book.routing.yml +++ b/core/modules/book/book.routing.yml @@ -29,6 +29,7 @@ book.export: requirements: _permission: 'access printer-friendly version' _entity_access: 'node.view' + node: \d+ entity.node.book_outline_form: path: '/node/{node}/outline' @@ -38,6 +39,7 @@ entity.node.book_outline_form: requirements: _permission: 'administer book outlines' _entity_access: 'node.view' + node: \d+ options: _node_operation_route: TRUE @@ -62,3 +64,4 @@ entity.node.book_remove_form: _permission: 'administer book outlines' _entity_access: 'node.view' _access_book_removable: 'TRUE' + node: \d+ diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.js index dfb7c875a..9f0abcd98 100644 --- a/core/modules/ckeditor/js/plugins/drupallink/plugin.js +++ b/core/modules/ckeditor/js/plugins/drupallink/plugin.js @@ -9,6 +9,27 @@ 'use strict'; + function parseAttributes(element) { + var parsedAttributes = {}; + + var domElement = element.$; + var attribute = null; + var attributeName; + for (var attrIndex = 0; attrIndex < domElement.attributes.length; attrIndex++) { + attribute = domElement.attributes.item(attrIndex); + attributeName = attribute.nodeName.toLowerCase(); + // Don't consider data-cke-saved- attributes; they're just there to work + // around browser quirks. + if (attributeName.substring(0, 15) === 'data-cke-saved-') { + continue; + } + // Store the value for this attribute, unless there's a data-cke-saved- + // alternative for it, which will contain the quirk-free, original value. + parsedAttributes[attributeName] = element.data('cke-saved-' + attributeName) || attribute.nodeValue; + } + return parsedAttributes; + } + CKEDITOR.plugins.add('drupallink', { init: function (editor) { // Add the commands for link and unlink. @@ -16,8 +37,7 @@ allowedContent: { a: { attributes: { - '!href': true, - 'target': true + '!href': true }, classes: {} } @@ -34,35 +54,16 @@ var drupalImageUtils = CKEDITOR.plugins.drupalimage; var focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor); var linkElement = getSelectedLink(editor); - var linkDOMElement = null; // Set existing values based on selected element. var existingValues = {}; if (linkElement && linkElement.$) { - linkDOMElement = linkElement.$; - - // Populate an array with the link's current attributes. - var attribute = null; - var attributeName; - for (var attrIndex = 0; attrIndex < linkDOMElement.attributes.length; attrIndex++) { - attribute = linkDOMElement.attributes.item(attrIndex); - attributeName = attribute.nodeName.toLowerCase(); - // Don't consider data-cke-saved- attributes; they're just there - // to work around browser quirks. - if (attributeName.substring(0, 15) === 'data-cke-saved-') { - continue; - } - // Store the value for this attribute, unless there's a - // data-cke-saved- alternative for it, which will contain the - // quirk-free, original value. - existingValues[attributeName] = linkElement.data('cke-saved-' + attributeName) || attribute.nodeValue; - } + existingValues = parseAttributes(linkElement); } // Or, if an image widget is focused, we're editing a link wrapping // an image widget. else if (focusedImageWidget && focusedImageWidget.data.link) { - var url = focusedImageWidget.data.link.url; - existingValues.href = url.protocol + url.url; + existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link); } // Prepare a save callback to be used upon saving the dialog. @@ -70,14 +71,7 @@ // If an image widget is focused, we're not editing an independent // link, but we're wrapping an image widget in a link. if (focusedImageWidget) { - var urlMatch = returnValues.attributes.href.match(urlRegex); - focusedImageWidget.setData('link', { - type: 'url', - url: { - protocol: urlMatch[1], - url: urlMatch[2] - } - }); + focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link)); editor.fire('saveSnapshot'); return; } @@ -97,11 +91,6 @@ range.selectNodeContents(text); } - // Ignore a disabled target attribute. - if (returnValues.attributes.target === 0) { - delete returnValues.attributes.target; - } - // Create the new link by applying a style to the new text. var style = new CKEDITOR.style({element: 'a', attributes: returnValues.attributes}); style.type = CKEDITOR.STYLE_INLINE; @@ -150,8 +139,7 @@ allowedContent: { a: { attributes: { - '!href': true, - 'target': true + '!href': true } } }, @@ -280,8 +268,6 @@ return null; } - var urlRegex = /^((?:http|https):\/\/)?(.*)$/; - /** * The image2 plugin is currently tightly coupled to the link plugin: it * calls CKEDITOR.plugins.link.parseLinkAttributes(). @@ -296,28 +282,20 @@ */ CKEDITOR.plugins.link = CKEDITOR.plugins.link || { parseLinkAttributes: function (editor, element) { - var href = (element && (element.data('cke-saved-href') || element.getAttribute('href'))) || ''; - var urlMatch = href.match(urlRegex); - return { - type: 'url', - url: { - protocol: urlMatch[1], - url: urlMatch[2] - } - }; + return parseAttributes(element); }, getLinkAttributes: function (editor, data) { var set = {}; - - var protocol = (data.url && typeof data.url.protocol !== 'undefined') ? data.url.protocol : 'http://'; - var url = (data.url && CKEDITOR.tools.trim(data.url.url)) || ''; - set['data-cke-saved-href'] = (url.indexOf('/') === 0) ? url : protocol + url; - - // Browser need the "href" fro copy/paste link to work. (#6641) - if (set['data-cke-saved-href']) { - set.href = set['data-cke-saved-href']; + for (var attributeName in data) { + if (data.hasOwnProperty(attributeName)) { + set[attributeName] = data[attributeName]; + } } + // CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute + // to work around browser quirks. We need to update it. + set['data-cke-saved-href'] = set.href; + // Remove all attributes which are not currently set. var removed = {}; for (var s in set) { diff --git a/core/modules/comment/comment.routing.yml b/core/modules/comment/comment.routing.yml index 967cb3f08..3d698b837 100644 --- a/core/modules/comment/comment.routing.yml +++ b/core/modules/comment/comment.routing.yml @@ -23,6 +23,7 @@ entity.comment.edit_form: _entity_form: 'comment.default' requirements: _entity_access: 'comment.update' + comment: \d+ comment.approve: path: '/comment/{comment}/approve' @@ -33,6 +34,7 @@ comment.approve: requirements: _entity_access: 'comment.approve' _csrf_token: 'TRUE' + comment: \d+ entity.comment.canonical: path: '/comment/{comment}' @@ -41,6 +43,7 @@ entity.comment.canonical: _controller: '\Drupal\comment\Controller\CommentController::commentPermalink' requirements: _entity_access: 'comment.view' + comment: \d+ entity.comment.delete_form: path: '/comment/{comment}/delete' @@ -49,6 +52,7 @@ entity.comment.delete_form: _entity_form: 'comment.delete' requirements: _entity_access: 'comment.delete' + comment: \d+ comment.reply: path: '/comment/reply/{entity_type}/{entity}/{field_name}/{pid}' @@ -77,6 +81,7 @@ comment.node_redirect: requirements: _entity_access: 'node.view' _module_dependencies: 'node' + node: \d+ entity.comment_type.collection: path: '/admin/structure/comment' diff --git a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php index c8bc4a701..b053170fc 100644 --- a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php +++ b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php @@ -39,6 +39,34 @@ class CommentSelection extends DefaultSelection { return $query; } + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $comment = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable comment, it needs to published. + /** @var \Drupal\comment\CommentInterface $comment */ + $comment->setPublished(TRUE); + + return $comment; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + // Mirror the conditions checked in buildEntityQuery(). + if (!$this->currentUser->hasPermission('administer comments')) { + $entities = array_filter($entities, function ($comment) { + /** @var \Drupal\comment\CommentInterface $comment */ + return $comment->isPublished(); + }); + } + return $entities; + } + /** * {@inheritdoc} */ diff --git a/core/modules/comment/src/Plugin/migrate/destination/EntityComment.php b/core/modules/comment/src/Plugin/migrate/destination/EntityComment.php index 477cdcd0c..7ebbc8338 100644 --- a/core/modules/comment/src/Plugin/migrate/destination/EntityComment.php +++ b/core/modules/comment/src/Plugin/migrate/destination/EntityComment.php @@ -10,6 +10,7 @@ namespace Drupal\comment\Plugin\migrate\destination; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\Query\QueryFactory; +use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\State\StateInterface; use Drupal\migrate\Entity\MigrationInterface; use Drupal\migrate\MigrateException; @@ -62,13 +63,15 @@ class EntityComment extends EntityContentBase { * The list of bundles this entity type has. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager service. + * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager + * The field type plugin manager service. * @param \Drupal\Core\State\StateInterface $state * The state storage object. * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query * The query object that can query the given entity type. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, StateInterface $state, QueryFactory $entity_query) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager); + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, StateInterface $state, QueryFactory $entity_query) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager); $this->state = $state; $this->entityQuery = $entity_query; } @@ -86,6 +89,7 @@ class EntityComment extends EntityContentBase { $container->get('entity.manager')->getStorage($entity_type), array_keys($container->get('entity.manager')->getBundleInfo($entity_type)), $container->get('entity.manager'), + $container->get('plugin.manager.field.field_type'), $container->get('state'), $container->get('entity.query') ); @@ -110,32 +114,9 @@ class EntityComment extends EntityContentBase { */ protected function processStubRow(Row $row) { parent::processStubRow($row); - $stub_commented_entity_type = $row->getDestinationProperty('entity_type'); - - // While parent::getEntity() fills the bundle property for stub entities - // if it's still empty, here we must also make sure entity_id/entity_type - // are filled (so $comment->getCommentedEntity() always returns a value). - if (empty($this->stubCommentedEntityIds[$stub_commented_entity_type])) { - // Fill stub entity id. Any id will do, as long as it exists. - $entity_type = $this->entityManager->getDefinition($stub_commented_entity_type); - $id_key = $entity_type->getKey('id'); - $result = $this->entityQuery - ->get($stub_commented_entity_type) - ->range(0, 1) - ->execute(); - if ($result) { - $this->stubCommentedEntityIds[$stub_commented_entity_type] = array_pop($result); - $row->setSourceProperty($id_key, $this->stubCommentedEntityIds[$stub_commented_entity_type]); - } - else { - throw new MigrateException(t('Could not find parent entity to use for comment %id', ['%id' => implode(':', $row->getSourceIdValues())]), MigrationInterface::MESSAGE_ERROR); - } - } - - $row->setDestinationProperty('entity_id', $this->stubCommentedEntityIds[$stub_commented_entity_type]); - $row->setDestinationProperty('entity_type', $stub_commented_entity_type); - $row->setDestinationProperty('created', REQUEST_TIME); - $row->setDestinationProperty('changed', REQUEST_TIME); + // Neither uid nor name is required in itself, but it is required to set one + // of them. + $row->setDestinationProperty('name', 'anonymous_stub'); } } diff --git a/core/modules/comment/src/Tests/CommentValidationTest.php b/core/modules/comment/src/Tests/CommentValidationTest.php index 62acec0b1..5c30d893d 100644 --- a/core/modules/comment/src/Tests/CommentValidationTest.php +++ b/core/modules/comment/src/Tests/CommentValidationTest.php @@ -39,7 +39,7 @@ class CommentValidationTest extends EntityUnitTestBase { */ public function testValidation() { // Add a user. - $user = User::create(array('name' => 'test')); + $user = User::create(array('name' => 'test', 'status' => TRUE)); $user->save(); // Add comment type. diff --git a/core/modules/comment/src/Tests/Migrate/MigrateCommentStubTest.php b/core/modules/comment/src/Tests/Migrate/MigrateCommentStubTest.php new file mode 100644 index 000000000..22818f585 --- /dev/null +++ b/core/modules/comment/src/Tests/Migrate/MigrateCommentStubTest.php @@ -0,0 +1,78 @@ +installEntitySchema('comment'); + $this->installEntitySchema('node'); + // Make sure uid 0 is created (default uid for comments is 0). + $storage = \Drupal::entityManager()->getStorage('user'); + // Insert a row for the anonymous user. + $storage + ->create(array( + 'uid' => 0, + 'status' => 0, + 'name' => '', + )) + ->save(); + // Need at least one node type and comment type present. + NodeType::create([ + 'type' => 'testnodetype', + 'name' => 'Test node type', + ])->save(); + CommentType::create([ + 'id' => 'testcommenttype', + 'label' => 'Test comment type', + 'target_entity_type_id' => 'node', + ])->save(); + } + + /** + * Tests creation of comment stubs. + */ + public function testStub() { + try { + // We expect an exception, because there's no node to reference. + $this->performStubTest('comment'); + $this->fail('Expected exception has not been thrown.'); + } + catch (MigrateException $e) { + $this->assertIdentical($e->getMessage(), + 'Stubbing failed, unable to generate value for field entity_id'); + } + + // The stub should pass when there's a node to point to. + $this->createStub('node'); + $this->performStubTest('comment'); + } + +} diff --git a/core/modules/comment/src/Tests/Migrate/d6/MigrateCommentTest.php b/core/modules/comment/src/Tests/Migrate/d6/MigrateCommentTest.php index 4545e4626..7219d9548 100644 --- a/core/modules/comment/src/Tests/Migrate/d6/MigrateCommentTest.php +++ b/core/modules/comment/src/Tests/Migrate/d6/MigrateCommentTest.php @@ -22,13 +22,7 @@ class MigrateCommentTest extends MigrateDrupal6TestBase { /** * {@inheritdoc} */ - public static $modules = [ - 'comment', - // Directly testing that a stub comment's entity_id is populated upon - // importing is not straightforward, but RDF module serves as an implicit - // test - its hook_comment_storage_load() references a stubbed comment. - 'rdf', - ]; + public static $modules = ['comment']; /** * {@inheritdoc} diff --git a/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php b/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php index 19ef2eec0..e15d3acbf 100644 --- a/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php +++ b/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php @@ -67,6 +67,7 @@ class ConfigImportInstallProfileTest extends WebTestBase { $core['module']['testing_config_import'] = 0; unset($core['module']['syslog']); unset($core['theme']['stark']); + $core['theme']['stable'] = 0; $core['theme']['classy'] = 0; $sync->write('core.extension', $core); $sync->deleteAll('syslog.'); diff --git a/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php b/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php index b5409bec6..e10dfe0b1 100644 --- a/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php +++ b/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php @@ -92,12 +92,12 @@ class ConfigInstallProfileOverrideTest extends WebTestBase { } // Install the config_test module and ensure that the override from the - // install profile is not used. Optional configuration can not override + // install profile is used. Optional configuration can override // configuration in a modules config/install directory. $this->container->get('module_installer')->install(['config_test']); $this->rebuildContainer(); $config_test_storage = \Drupal::entityManager()->getStorage('config_test'); - $this->assertEqual($config_test_storage->load('dotted.default')->label(), 'Default', 'The config_test entity is not overridden by the profile optional configuration.'); + $this->assertEqual($config_test_storage->load('dotted.default')->label(), 'Default install profile override', 'The config_test entity is overridden by the profile optional configuration.'); // Test that override of optional configuration does work. $this->assertEqual($config_test_storage->load('override')->label(), 'Override', 'The optional config_test entity is overridden by the profile optional configuration.'); // Test that override of optional configuration which introduces an unmet diff --git a/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php b/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php index e7e8f6cab..59683af60 100644 --- a/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php +++ b/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php @@ -95,6 +95,7 @@ class ConfigInstallProfileUnmetDependenciesTest extends InstallerTestBase { else { $this->fail('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown'); } + $this->assertErrorLogged('Configuration objects (system.action.user_block_user_action) provided by user have unmet dependencies in'); } } diff --git a/core/modules/contact/contact.routing.yml b/core/modules/contact/contact.routing.yml index d81bb9fc2..59779868d 100644 --- a/core/modules/contact/contact.routing.yml +++ b/core/modules/contact/contact.routing.yml @@ -54,3 +54,4 @@ entity.user.contact_form: _controller: '\Drupal\contact\Controller\ContactController::contactPersonalPage' requirements: _access_contact_personal_tab: 'TRUE' + user: \d+ diff --git a/core/modules/content_translation/content_translation.install b/core/modules/content_translation/content_translation.install index 0bfe4149e..d44b2d112 100644 --- a/core/modules/content_translation/content_translation.install +++ b/core/modules/content_translation/content_translation.install @@ -29,3 +29,19 @@ function content_translation_enable() { $message = t('Enable translation for content types, taxonomy vocabularies, accounts, or any other element you wish to translate.', $t_args); drupal_set_message($message, 'warning'); } + +/** + * @addtogroup updates-8.0.0-rc + * @{ + */ + +/** + * Rebuild the routes as the content translation routes have now new names. + */ +function content_translation_update_8001() { + \Drupal::service('router.builder')->rebuild(); +} + +/** + * @} End of "addtogroup updates-8.0.0-rc". + */ diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index ff1b34ddf..b354e9549 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -53,7 +53,7 @@ function content_translation_help($route_name, RouteMatchInterface $route_match) */ function content_translation_module_implements_alter(&$implementations, $hook) { switch ($hook) { - // Move some of our hook implementations to the end of the list. + // Move our hook_entity_type_alter() implementation to the end of the list. case 'entity_type_alter': $group = $implementations['content_translation']; unset($implementations['content_translation']); @@ -140,7 +140,11 @@ function content_translation_entity_type_alter(array &$entity_types) { if ($entity_type->hasLinkTemplate('canonical')) { // Provide default route names for the translation paths. if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) { - $entity_type->setLinkTemplate('drupal:content-translation-overview', $entity_type->getLinkTemplate('canonical') . '/translations'); + $translations_path = $entity_type->getLinkTemplate('canonical') . '/translations'; + $entity_type->setLinkTemplate('drupal:content-translation-overview', $translations_path); + $entity_type->setLinkTemplate('drupal:content-translation-add', $translations_path . '/add/{source}/{target}'); + $entity_type->setLinkTemplate('drupal:content-translation-edit', $translations_path . '/edit/{language}'); + $entity_type->setLinkTemplate('drupal:content-translation-delete', $translations_path . '/delete/{language}'); } // @todo Remove this as soon as menu access checks rely on the // controller. See https://www.drupal.org/node/2155787. diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index d7eb42ab3..1b8d77df9 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -652,7 +652,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E $source = $form_state->getValue(array('source_langcode', 'source')); $entity_type_id = $entity->getEntityTypeId(); - $form_state->setRedirect('content_translation.translation_add_' . $entity_type_id, array( + $form_state->setRedirect("entity.$entity_type_id.content_translation_add", array( $entity_type_id => $entity->id(), 'source' => $source, 'target' => $form_object->getFormLangcode($form_state), @@ -689,7 +689,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E $form_state->setRedirectUrl($entity->urlInfo('delete-form')); } else { - $form_state->setRedirect('content_translation.translation_delete_' . $entity_type_id, [ + $form_state->setRedirect("entity.$entity_type_id.content_translation_delete", [ $entity_type_id => $entity->id(), 'language' => $form_object->getFormLangcode($form_state), ]); diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php index 9c6879ac9..8c19de141 100644 --- a/core/modules/content_translation/src/Controller/ContentTranslationController.php +++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php @@ -127,7 +127,7 @@ class ContentTranslationController extends ControllerBase { $langcode = $language->getId(); $add_url = new Url( - 'content_translation.translation_add_' . $entity_type_id, + "entity.$entity_type_id.content_translation_add", array( 'source' => $original, 'target' => $language->getId(), @@ -138,7 +138,7 @@ class ContentTranslationController extends ControllerBase { ) ); $edit_url = new Url( - 'content_translation.translation_edit_' . $entity_type_id, + "entity.$entity_type_id.content_translation_edit", array( 'language' => $language->getId(), $entity_type_id => $entity->id(), @@ -148,7 +148,7 @@ class ContentTranslationController extends ControllerBase { ) ); $delete_url = new Url( - 'content_translation.translation_delete_' . $entity_type_id, + "entity.$entity_type_id.content_translation_delete", array( 'language' => $language->getId(), $entity_type_id => $entity->id(), diff --git a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php index a2b54a8a2..ebdf5626a 100644 --- a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php +++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php @@ -112,7 +112,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase { '_admin_route' => $is_admin, ) ); - $collection->add("content_translation.translation_add_$entity_type_id", $route); + $collection->add("entity.$entity_type_id.content_translation_add", $route); $route = new Route( $path . '/edit/{language}', @@ -137,7 +137,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase { '_admin_route' => $is_admin, ) ); - $collection->add("content_translation.translation_edit_$entity_type_id", $route); + $collection->add("entity.$entity_type_id.content_translation_edit", $route); $route = new Route( $path . '/delete/{language}', @@ -162,7 +162,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase { '_admin_route' => $is_admin, ) ); - $collection->add("content_translation.translation_delete_$entity_type_id", $route); + $collection->add("entity.$entity_type_id.content_translation_delete", $route); } } diff --git a/core/modules/content_translation/src/Tests/ContentTranslationEnableTest.php b/core/modules/content_translation/src/Tests/ContentTranslationEnableTest.php index 9af869807..926a60e4a 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationEnableTest.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationEnableTest.php @@ -19,7 +19,7 @@ class ContentTranslationEnableTest extends WebTestBase { /** * {@inheritdoc} */ - public static $modules = ['entity_test', 'menu_link_content']; + public static $modules = ['entity_test', 'menu_link_content', 'node']; /** * Tests that entity schemas are up-to-date after enabling translation. @@ -39,6 +39,9 @@ class ContentTranslationEnableTest extends WebTestBase { $requirement_value = $this->cssSelect("tr.system-status-report__entry th:contains('Entity/field definitions') + td"); $this->assertEqual(t('Up to date'), trim((string) $requirement_value[0])); + $this->drupalGet('admin/config/regional/content-language'); + // The node entity type should not be an option because it has no bundles. + $this->assertNoRaw('entity_types[node]'); // Enable content translation on entity types that have will have a // content_translation_uid. $edit = [ @@ -47,12 +50,23 @@ class ContentTranslationEnableTest extends WebTestBase { 'entity_types[entity_test_mul]' => TRUE, 'settings[entity_test_mul][entity_test_mul][translatable]' => TRUE, ]; - $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); + $this->drupalPostForm(NULL, $edit, t('Save configuration')); // No pending updates should be available. $this->drupalGet('admin/reports/status'); $requirement_value = $this->cssSelect("tr.system-status-report__entry th:contains('Entity/field definitions') + td"); $this->assertEqual(t('Up to date'), trim((string) $requirement_value[0])); + + // Create a node type and check the content translation settings are now + // available for nodes. + $edit = array( + 'name' => 'foo', + 'title_label' => 'title for foo', + 'type' => 'foo', + ); + $this->drupalPostForm('admin/structure/types/add', $edit, t('Save content type')); + $this->drupalGet('admin/config/regional/content-language'); + $this->assertRaw('entity_types[node]'); } } diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php index 33d936a46..1b79d1313 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php @@ -107,7 +107,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase { $language = ConfigurableLanguage::load($langcode); $values[$langcode] = $this->getNewEntityValues($langcode); - $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [ + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ $entity->getEntityTypeId() => $entity->id(), 'source' => $default_langcode, 'target' => $langcode @@ -167,7 +168,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase { $language = ConfigurableLanguage::load($langcode); $source_langcode = 'it'; $edit = array('source_langcode[source]' => $source_langcode); - $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [ + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ $entity->getEntityTypeId() => $entity->id(), 'source' => $default_langcode, 'target' => $langcode @@ -180,7 +182,8 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase { // Add another translation and mark the other ones as outdated. $values[$langcode] = $this->getNewEntityValues($langcode); $edit = $this->getEditValues($values, $langcode) + array('content_translation[retranslate]' => TRUE); - $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [ + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ $entity->getEntityTypeId() => $entity->id(), 'source' => $source_langcode, 'target' => $langcode @@ -207,13 +210,15 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase { */ protected function doTestTranslationOverview() { $entity = entity_load($this->entityTypeId, $this->entityId, TRUE); - $this->drupalGet($entity->urlInfo('drupal:content-translation-overview')); + $translate_url = $entity->urlInfo('drupal:content-translation-overview'); + $this->drupalGet($translate_url); + $translate_url->setAbsolute(FALSE); foreach ($this->langcodes as $langcode) { if ($entity->hasTranslation($langcode)) { $language = new Language(array('id' => $langcode)); - $view_path = $entity->url('canonical', array('language' => $language)); - $elements = $this->xpath('//table//a[@href=:href]', array(':href' => $view_path)); + $view_url = $entity->url('canonical', ['language' => $language]); + $elements = $this->xpath('//table//a[@href=:href]', [':href' => $view_url]); $this->assertEqual((string) $elements[0], $entity->getTranslation($langcode)->label(), format_string('Label correctly shown for %language translation.', array('%language' => $langcode))); $edit_path = $entity->url('edit-form', array('language' => $language)); $elements = $this->xpath('//table//ul[@class="dropbutton"]/li/a[@href=:href]', array(':href' => $edit_path)); @@ -343,7 +348,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase { // Check that the translator cannot delete the original translation. $args = [$this->entityTypeId => $entity->id(), 'language' => 'en']; - $this->drupalGet(Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, $args)); + $this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args)); $this->assertResponse(403); } diff --git a/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php b/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php index 9d8a052e4..63c1963d0 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationWorkflowsTest.php @@ -73,7 +73,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase { // Create a translation. $this->drupalLogin($this->translator); - $add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]); + $add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $this->langcodes[2]]); $this->drupalPostForm($add_translation_url, array(), t('Save')); $this->rebuildContainer(); } @@ -175,7 +175,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase { $this->assertResponse($expected_status['overview'], SafeMarkup::format('The @user_label has the expected translation overview access.', $args)); // Check whether the user is allowed to create a translation. - $add_translation_url = Url::fromRoute('content_translation.translation_add_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options); + $add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options); if ($expected_status['add_translation'] == 200) { $this->clickLink('Add'); $this->assertUrl($add_translation_url->toString(), [], 'The translation overview points to the translation form when creating translations.'); @@ -193,7 +193,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase { // Check whether the user is allowed to edit a translation. $langcode = $this->langcodes[2]; $options['language'] = $languages[$langcode]; - $edit_translation_url = Url::fromRoute('content_translation.translation_edit_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options); + $edit_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_edit", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options); if ($expected_status['edit_translation'] == 200) { $this->drupalGet($translations_url); $editor = $expected_status['edit'] == 200; @@ -221,7 +221,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase { // Check whether the user is allowed to delete a translation. $langcode = $this->langcodes[2]; $options['language'] = $languages[$langcode]; - $delete_translation_url = Url::fromRoute('content_translation.translation_delete_' . $this->entityTypeId, [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options); + $delete_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options); if ($expected_status['delete_translation'] == 200) { $this->drupalGet($translations_url); $editor = $expected_status['delete'] == 200; diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index e83d49a97..a2c3219e8 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -464,7 +464,11 @@ function _editor_get_file_uuids_by_field(EntityInterface $entity) { $formatted_text_fields = _editor_get_formatted_text_fields($entity); foreach ($formatted_text_fields as $formatted_text_field) { - $text = $entity->get($formatted_text_field)->value; + $text = ''; + $field_items = $entity->get($formatted_text_field); + foreach ($field_items as $field_item) { + $text .= $field_item->value; + } $uuids[$formatted_text_field] = _editor_parse_file_uuids($text); } return $uuids; diff --git a/core/modules/editor/src/Annotation/Editor.php b/core/modules/editor/src/Annotation/Editor.php index 58d2450c1..e0b28059c 100644 --- a/core/modules/editor/src/Annotation/Editor.php +++ b/core/modules/editor/src/Annotation/Editor.php @@ -14,11 +14,43 @@ use Drupal\Component\Annotation\Plugin; * * Plugin Namespace: Plugin\Editor * + * Text editor plugin implementations need to define a plugin definition array + * through annotation. These definition arrays may be altered through + * hook_editor_info_alter(). The definition includes the following keys: + * + * - id: The unique, system-wide identifier of the text editor. Typically named + * the same as the editor library. + * - label: The human-readable name of the text editor, translated. + * - supports_content_filtering: Whether the editor supports "allowed content + * only" filtering. + * - supports_inline_editing: Whether the editor supports the inline editing + * provided by the Edit module. + * - is_xss_safe: Whether this text editor is not vulnerable to XSS attacks. + * - supported_element_types: On which form element #types this text editor is + * capable of working. + * + * A complete sample plugin definition should be defined as in this example: + * + * @code + * @Editor( + * id = "myeditor", + * label = @Translation("My Editor"), + * supports_content_filtering = FALSE, + * supports_inline_editing = FALSE, + * is_xss_safe = FALSE, + * supported_element_types = { + * "textarea", + * "textfield", + * } + * ) + * @endcode + * * For a working example, see \Drupal\ckeditor\Plugin\Editor\CKEditor * * @see \Drupal\editor\Plugin\EditorPluginInterface * @see \Drupal\editor\Plugin\EditorBase * @see \Drupal\editor\Plugin\EditorManager + * @see hook_editor_info_alter() * @see plugin_api * * @Annotation diff --git a/core/modules/editor/src/EditorController.php b/core/modules/editor/src/EditorController.php index fd605cd78..73d5dad0e 100644 --- a/core/modules/editor/src/EditorController.php +++ b/core/modules/editor/src/EditorController.php @@ -60,6 +60,9 @@ class EditorController extends ControllerBase { * @return \Symfony\Component\HttpFoundation\JsonResponse * A JSON response containing the XSS-filtered value. * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * Thrown if no value to filter is specified. + * * @see editor_filter_xss() */ public function filterXss(Request $request, FilterFormatInterface $filter_format) { diff --git a/core/modules/editor/src/Plugin/EditorBase.php b/core/modules/editor/src/Plugin/EditorBase.php index cc39c18a4..514a32cd4 100644 --- a/core/modules/editor/src/Plugin/EditorBase.php +++ b/core/modules/editor/src/Plugin/EditorBase.php @@ -17,30 +17,8 @@ use Drupal\editor\Entity\Editor; * This class provides default implementations of the EditorPluginInterface so * that classes extending this one do not need to implement every method. * - * Plugins extending this class need to define a plugin definition array through - * annotation. These definition arrays may be altered through - * hook_editor_info_alter(). The definition includes the following keys: - * - * - id: The unique, system-wide identifier of the text editor. Typically named - * the same as the editor library. - * - label: The human-readable name of the text editor, translated. - * - supports_content_filtering: Whether the editor supports "allowed content - * only" filtering. - * - supports_inline_editing: Whether the editor supports the inline editing - * provided by the Edit module. - * - is_xss_safe: Whether this text editor is not vulnerable to XSS attacks. - * - * A complete sample plugin definition should be defined as in this example: - * - * @code - * @Editor( - * id = "myeditor", - * label = @Translation("My Editor"), - * supports_content_filtering = FALSE, - * supports_inline_editing = FALSE, - * is_xss_safe = FALSE - * ) - * @endcode + * Plugins extending this class need to specify an annotation containing the + * plugin definition so the plugin can be discovered. * * @see \Drupal\editor\Annotation\Editor * @see \Drupal\editor\Plugin\EditorPluginInterface diff --git a/core/modules/editor/src/Tests/EditorFileUsageTest.php b/core/modules/editor/src/Tests/EditorFileUsageTest.php index 7b5210e50..6e291d434 100644 --- a/core/modules/editor/src/Tests/EditorFileUsageTest.php +++ b/core/modules/editor/src/Tests/EditorFileUsageTest.php @@ -8,6 +8,8 @@ namespace Drupal\editor\Tests; use Drupal\system\Tests\Entity\EntityUnitTestBase; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Tests tracking of file usage by the Text Editor module. @@ -39,6 +41,11 @@ class EditorFileUsageTest extends EntityUnitTestBase { )); $filtered_html_format->save(); + // Set cardinality for body field. + FieldStorageConfig::loadByName('node', 'body') + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) + ->save(); + // Set up text editor. $editor = entity_create('editor', array( 'format' => 'filtered_html', @@ -56,33 +63,56 @@ class EditorFileUsageTest extends EntityUnitTestBase { * Tests the configurable text editor manager. */ public function testEditorEntityHooks() { - $image = entity_create('file'); - $image->setFileUri('core/misc/druplicon.png'); - $image->setFilename(drupal_basename($image->getFileUri())); - $image->save(); - $file_usage = $this->container->get('file.usage'); - $this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages.'); + $image_paths = array( + 0 => 'core/misc/druplicon.png', + 1 => 'core/misc/tree.png', + 2 => 'core/misc/help.png', + ); + + $image_entities = array(); + foreach ($image_paths as $key => $image_path) { + $image = entity_create('file'); + $image->setFileUri($image_path); + $image->setFilename(drupal_basename($image->getFileUri())); + $image->save(); + + $file_usage = $this->container->get('file.usage'); + $this->assertIdentical(array(), $file_usage->listUsage($image), 'The image ' . $image_paths[$key] . ' has zero usages.'); + + $image_entities[] = $image; + } + + $body = array(); + foreach ($image_entities as $key => $image_entity) { + // Don't be rude, say hello. + $body_value = '

Hello, world!

'; + // Test handling of a valid image entry. + $body_value .= ''; + // Test handling of an invalid data-entity-uuid attribute. + $body_value .= ''; + // Test handling of an invalid data-entity-type attribute. + $body_value .= ''; + // Test handling of a non-existing UUID. + $body_value .= ''; + + $body[] = array( + 'value' => $body_value, + 'format' => 'filtered_html', + ); + } - $body_value = '

Hello, world!

'; - // Test handling of an invalid data-entity-uuid attribute. - $body_value .= ''; - // Test handling of an invalid data-entity-type attribute. - $body_value .= ''; - // Test handling of a non-existing UUID. - $body_value .= ''; // Test editor_entity_insert(): increment. $this->createUser(); $node = entity_create('node', array( 'type' => 'page', 'title' => 'test', - 'body' => array( - 'value' => $body_value, - 'format' => 'filtered_html', - ), + 'body' => $body, 'uid' => 1, )); $node->save(); - $this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), $file_usage->listUsage($image), 'The image has 1 usage.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 1 usage.'); + } // Test editor_entity_update(): increment, twice, by creating new revisions. $node->setNewRevision(TRUE); @@ -90,45 +120,68 @@ class EditorFileUsageTest extends EntityUnitTestBase { $second_revision_id = $node->getRevisionId(); $node->setNewRevision(TRUE); $node->save(); - $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 3 usages.'); + } // Test hook_entity_update(): decrement, by modifying the last revision: // remove the data-entity-type attribute from the body field. - $body = $node->get('body')->first()->get('value'); - $original_value = $body->getValue(); - $new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value); - $body->setValue($new_value); + $original_values = array(); + for ($i = 0; $i < count($image_entities); $i++) { + $original_value = $node->body[$i]->value; + $new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value); + $node->body[$i]->value = $new_value; + $original_values[$i] = $original_value; + } $node->save(); - $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.'); + } // Test editor_entity_update(): increment again by creating a new revision: // read the data- attributes to the body field. $node->setNewRevision(TRUE); - $node->get('body')->first()->get('value')->setValue($original_value); + foreach ($original_values as $key => $original_value) { + $node->body[$key]->value = $original_value; + } $node->save(); - $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 3 usages.'); + } // Test hook_entity_update(): decrement, by modifying the last revision: // remove the data-entity-uuid attribute from the body field. - $body = $node->get('body')->first()->get('value'); - $new_value = str_replace('data-entity-uuid', 'data-entity-uuid-modified', $original_value); - $body->setValue($new_value); + foreach ($original_values as $key => $original_value) { + $original_value = $node->body[$key]->value; + $new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value); + $node->body[$key]->value = $new_value; + } $node->save(); - $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.'); + } // Test hook_entity_update(): increment, by modifying the last revision: // read the data- attributes to the body field. - $node->get('body')->first()->get('value')->setValue($original_value); + foreach ($original_values as $key => $original_value) { + $node->body[$key]->value = $original_value; + } $node->save(); - $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 3 usages.'); + } // Test editor_entity_revision_delete(): decrement, by deleting a revision. entity_revision_delete('node', $second_revision_id); - $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.'); + } // Test editor_entity_delete(). $node->delete(); - $this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages again.'); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array(), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has zero usages again.'); + } } } diff --git a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php index bddfc271b..923590319 100644 --- a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php +++ b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php @@ -40,7 +40,7 @@ class FieldInstance extends DrupalSqlBase { public function fields() { return array( 'field_name' => $this->t('The machine name of field.'), - 'type_name' => $this->t('Content type where is used this field.'), + 'type_name' => $this->t('Content type where this field is in use.'), 'weight' => $this->t('Weight.'), 'label' => $this->t('A name to show.'), 'widget_type' => $this->t('Widget type.'), diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php index f8a934855..f629ce8cc 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php @@ -7,6 +7,8 @@ namespace Drupal\field\Tests\EntityReference; +use Drupal\comment\Entity\Comment; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Unicode; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemInterface; @@ -17,8 +19,11 @@ use Drupal\entity_test\Entity\EntityTestStringId; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Tests\FieldUnitTestBase; +use Drupal\file\Entity\File; +use Drupal\node\Entity\Node; use Drupal\taxonomy\Entity\Term; use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\user\Entity\User; /** @@ -35,7 +40,7 @@ class EntityReferenceItemTest extends FieldUnitTestBase { * * @var array */ - public static $modules = ['taxonomy', 'text', 'filter', 'views', 'field']; + public static $modules = ['node', 'comment', 'file', 'taxonomy', 'text', 'filter', 'views', 'field']; /** * The taxonomy vocabulary to test with. @@ -66,6 +71,11 @@ class EntityReferenceItemTest extends FieldUnitTestBase { $this->installEntitySchema('entity_test_string_id'); $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('file'); + + $this->installSchema('comment', ['comment_entity_statistics']); $this->vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => $this->randomMachineName(), @@ -90,6 +100,10 @@ class EntityReferenceItemTest extends FieldUnitTestBase { $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_term', 'Test content entity reference', 'taxonomy_term'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_entity_test_string_id', 'Test content entity reference with string ID', 'entity_test_string_id'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_vocabulary', 'Test config entity reference', 'taxonomy_vocabulary'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_node', 'Test node entity reference', 'node'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_user', 'Test user entity reference', 'user'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_comment', 'Test comment entity reference', 'comment'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_file', 'Test file entity reference', 'file'); } /** @@ -333,9 +347,9 @@ class EntityReferenceItemTest extends FieldUnitTestBase { } /** - * Tests validation constraint. + * Tests ValidReferenceConstraint with newly created and unsaved entities. */ - public function testValidation() { + public function testAutocreateValidation() { // The term entity is unsaved here. $term = Term::create(array( 'name' => $this->randomMachineName(), @@ -367,6 +381,100 @@ class EntityReferenceItemTest extends FieldUnitTestBase { $entity->save(); $errors = $entity->validate(); $this->assertEqual(0, count($errors)); + + // Test with an unpublished and unsaved node. + $title = $this->randomString(); + $node = Node::create([ + 'title' => $title, + 'type' => 'node', + 'status' => NODE_NOT_PUBLISHED, + ]); + + $entity = EntityTest::create([ + 'field_test_node' => [ + 'entity' => $node, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'node', '%label' => $title])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_node.0.entity'); + + // Publish the node and try again. + $node->setPublished(TRUE); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); + + // Test with an unpublished and unsaved comment. + $title = $this->randomString(); + $comment = Comment::create([ + 'subject' => $title, + 'comment_type' => 'comment', + 'status' => 0, + ]); + + $entity = EntityTest::create([ + 'field_test_comment' => [ + 'entity' => $comment, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'comment', '%label' => $title])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_comment.0.entity'); + + // Publish the comment and try again. + $comment->setPublished(TRUE); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); + + // Test with an inactive and unsaved user. + $name = $this->randomString(); + $user = User::create([ + 'name' => $name, + 'status' => 0, + ]); + + $entity = EntityTest::create([ + 'field_test_user' => [ + 'entity' => $user, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'user', '%label' => $name])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_user.0.entity'); + + // Activate the user and try again. + $user->activate(); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); + + // Test with a temporary and unsaved file. + $filename = $this->randomMachineName() . '.txt'; + $file = File::create([ + 'filename' => $filename, + 'status' => 0, + ]); + + $entity = EntityTest::create([ + 'field_test_file' => [ + 'entity' => $file, + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'file', '%label' => $filename])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_file.0.entity'); + + // Set the file as permanent and try again. + $file->setPermanent(); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); } } diff --git a/core/modules/field/src/Tests/Uri/UriItemTest.php b/core/modules/field/src/Tests/Uri/UriItemTest.php index 46ffe0ad5..9dc50a2c6 100644 --- a/core/modules/field/src/Tests/Uri/UriItemTest.php +++ b/core/modules/field/src/Tests/Uri/UriItemTest.php @@ -68,6 +68,11 @@ class UriItemTest extends FieldUnitTestBase { 'type' => 'uri', ]) ->save(); + + // Test the generateSampleValue() method. + $entity = entity_create('entity_test'); + $entity->$field_name->generateSampleItems(); + $this->entityValidateAndSave($entity); } } diff --git a/core/modules/file/config/schema/file.destination.schema.yml b/core/modules/file/config/schema/file.destination.schema.yml index 5d494f701..c6b197ffe 100644 --- a/core/modules/file/config/schema/file.destination.schema.yml +++ b/core/modules/file/config/schema/file.destination.schema.yml @@ -5,3 +5,6 @@ migrate.destination.entity:file: source_path_property: type: string label: 'Source path' + urlencode: + type: boolean + label: 'Whether to urlencode incoming file paths' diff --git a/core/modules/file/file.field.inc b/core/modules/file/file.field.inc index 6c065758a..bbfc2b23e 100644 --- a/core/modules/file/file.field.inc +++ b/core/modules/file/file.field.inc @@ -9,27 +9,6 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldFilteredMarkup; use Drupal\Core\Render\Element; -/** - * Returns HTML for an individual file upload widget. - * - * Default template: file-widget.html.twig. - * - * @param array $variables - * An associative array containing: - * - element: A render element representing the file. - */ -function template_preprocess_file_widget(&$variables) { - $element = $variables['element']; - if (!empty($element['fids']['#value'])) { - // Add the file size after the file name. - $file = reset($element['#files']); - $element['file_' . $file->id()]['filename']['#suffix'] = ' (' . format_size($file->getSize()) . ') '; - } - $variables['element'] = $element; - // The "js-form-managed-file" class is required for proper Ajax functionality. - $variables['attributes'] = array('class' => array('file-widget', 'js-form-managed-file', 'form-managed-file', 'clearfix')); -} - /** * Prepares variables for multi file form widget templates. * diff --git a/core/modules/file/file.js b/core/modules/file/file.js index 30cf2450e..8ed377eec 100644 --- a/core/modules/file/file.js +++ b/core/modules/file/file.js @@ -110,10 +110,10 @@ */ Drupal.behaviors.filePreviewLinks = { attach: function (context) { - $(context).find('div.js-form-managed-file .file a, .file-widget .file a').on('click', Drupal.file.openInNewWindow); + $(context).find('div.js-form-managed-file .file a').on('click', Drupal.file.openInNewWindow); }, detach: function (context) { - $(context).find('div.js-form-managed-file .file a, .file-widget .file a').off('click', Drupal.file.openInNewWindow); + $(context).find('div.js-form-managed-file .file a').off('click', Drupal.file.openInNewWindow); } }; diff --git a/core/modules/file/file.module b/core/modules/file/file.module index d48b23209..26c35ac53 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -555,10 +555,6 @@ function file_theme() { ), // From file.field.inc. - 'file_widget' => array( - 'render element' => 'element', - 'file' => 'file.field.inc', - ), 'file_widget_multiple' => array( 'render element' => 'element', 'file' => 'file.field.inc', diff --git a/core/modules/file/migration_templates/d6_file.yml b/core/modules/file/migration_templates/d6_file.yml index 07a9cc2d3..a4693cf11 100644 --- a/core/modules/file/migration_templates/d6_file.yml +++ b/core/modules/file/migration_templates/d6_file.yml @@ -23,3 +23,4 @@ process: uid: uid destination: plugin: entity:file + urlencode: true diff --git a/core/modules/file/migration_templates/d7_file.yml b/core/modules/file/migration_templates/d7_file.yml index ea8f647f1..b10cca614 100644 --- a/core/modules/file/migration_templates/d7_file.yml +++ b/core/modules/file/migration_templates/d7_file.yml @@ -23,3 +23,4 @@ process: destination: plugin: entity:file source_path_property: filepath + urlencode: true diff --git a/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php b/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php index e789154d2..01fb8cd8a 100644 --- a/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php +++ b/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php @@ -27,8 +27,41 @@ class FileSelection extends DefaultSelection { */ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { $query = parent::buildEntityQuery($match, $match_operator); - $query->condition('status', FILE_STATUS_PERMANENT); + // Allow referencing : + // - files with status "permanent" + // - or files uploaded by the current user (since newly uploaded files only + // become "permanent" after the containing entity gets validated and + // saved.) + $query->condition($query->orConditionGroup() + ->condition('status', FILE_STATUS_PERMANENT) + ->condition('uid', $this->currentUser->id())); return $query; } + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $file = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable file, it needs to have a "permanent" + // status. + /** @var \Drupal\file\FileInterface $file */ + $file->setPermanent(); + + return $file; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + $entities = array_filter($entities, function ($file) { + /** @var \Drupal\file\FileInterface $file */ + return $file->isPermanent() || $file->getOwnerId() === $this->currentUser->id(); + }); + return $entities; + } + } diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php index 1633147b2..ba50903ae 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php @@ -28,7 +28,7 @@ use Drupal\Core\TypedData\DataDefinition; * default_widget = "file_generic", * default_formatter = "file_default", * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList", - * constraints = {"ValidReference" = {}, "ReferenceAccess" = {}} + * constraints = {"ReferenceAccess" = {}, "FileValidation" = {}} * ) */ class FileItem extends EntityReferenceItem { diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php index bd511c8cf..3570c3506 100644 --- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php +++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php @@ -18,8 +18,9 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\file\Element\ManagedFile; -use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\file\Entity\File; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Validator\ConstraintViolationListInterface; /** * Plugin implementation of the 'file_generic' widget. @@ -369,11 +370,6 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface { $item = $element['#value']; $item['fids'] = $element['fids']['#value']; - // Prevent the file widget from overriding the image widget. - if (!isset($element['#theme'])) { - $element['#theme'] = 'file_widget'; - } - // Add the display field if enabled. if ($element['#display_field']) { $element['display'] = array( @@ -575,4 +571,15 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface { static::setWidgetState($parents, $field_name, $form_state, $field_state); } + /** + * {@inheritdoc} + */ + public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { + // Never flag validation errors for the remove button. + $clicked_button = end($form_state->getTriggeringElement()['#parents']); + if ($clicked_button !== 'remove_button') { + parent::flagErrors($items, $violations, $form, $form_state); + } + } + } diff --git a/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php b/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php new file mode 100644 index 000000000..3b14feea0 --- /dev/null +++ b/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php @@ -0,0 +1,22 @@ +get('entity')->getTarget()->getValue(); + // Get the validators. + $validators = $value->getUploadValidators(); + // Checks that a file meets the criteria specified by the validators. + if ($errors = file_validate($file, $validators)) { + foreach ($errors as $error) { + $this->context->addViolation($error); + } + } + } + +} diff --git a/core/modules/file/src/Plugin/migrate/destination/EntityFile.php b/core/modules/file/src/Plugin/migrate/destination/EntityFile.php index c5e898948..38c3f8716 100644 --- a/core/modules/file/src/Plugin/migrate/destination/EntityFile.php +++ b/core/modules/file/src/Plugin/migrate/destination/EntityFile.php @@ -7,8 +7,11 @@ namespace Drupal\file\Plugin\migrate\destination; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Field\Plugin\Field\FieldType\UriItem; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\StreamWrapper\LocalStream; use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; @@ -41,7 +44,7 @@ class EntityFile extends EntityContentBase { /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system) { $configuration += array( 'source_base_path' => '', 'source_path_property' => 'filepath', @@ -49,7 +52,7 @@ class EntityFile extends EntityContentBase { 'move' => FALSE, 'urlencode' => FALSE, ); - parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager); + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager); $this->streamWrapperManager = $stream_wrappers; $this->fileSystem = $file_system; @@ -68,6 +71,7 @@ class EntityFile extends EntityContentBase { $container->get('entity.manager')->getStorage($entity_type), array_keys($container->get('entity.manager')->getBundleInfo($entity_type)), $container->get('entity.manager'), + $container->get('plugin.manager.field.field_type'), $container->get('stream_wrapper_manager'), $container->get('file_system') ); @@ -77,6 +81,12 @@ class EntityFile extends EntityContentBase { * {@inheritdoc} */ protected function getEntity(Row $row, array $old_destination_id_values) { + // For stub rows, there is no real file to deal with, let the stubbing + // process take its default path. + if ($row->isStub()) { + return parent::getEntity($row, $old_destination_id_values); + } + $destination = $row->getDestinationProperty($this->configuration['destination_path_property']); $entity = $this->storage->loadByProperties(['uri' => $destination]); if ($entity) { @@ -91,6 +101,12 @@ class EntityFile extends EntityContentBase { * {@inheritdoc} */ public function import(Row $row, array $old_destination_id_values = array()) { + // For stub rows, there is no real file to deal with, let the stubbing + // process create the stub entity. + if ($row->isStub()) { + return parent::import($row, $old_destination_id_values); + } + $file = $row->getSourceProperty($this->configuration['source_path_property']); $destination = $row->getDestinationProperty($this->configuration['destination_path_property']); $source = $this->configuration['source_base_path'] . $file; @@ -256,4 +272,30 @@ class EntityFile extends EntityContentBase { return $filename; } + /** + * {@inheritdoc} + */ + protected function processStubRow(Row $row) { + // We stub the uri value ourselves so we can create a real stub file for it. + if (!$row->getDestinationProperty('uri')) { + $field_definitions = $this->entityManager + ->getFieldDefinitions($this->storage->getEntityTypeId(), + $this->getKey('bundle')); + $value = UriItem::generateSampleValue($field_definitions['uri']); + if (empty($value)) { + throw new MigrateException('Stubbing failed, unable to generate value for field uri'); + } + // generateSampleValue() wraps the value in an array. + $value = reset($value); + // Make it into a proper public file uri, stripping off the existing + // scheme if present. + $value = 'public://' . preg_replace('|^[a-z]+://|i', '', $value); + $value = Unicode::substr($value, 0, $field_definitions['uri']->getSetting('max_length')); + // Create a real file, so File::preSave() can do filesize() on it. + touch($value); + $row->setDestinationProperty('uri', $value); + } + parent::processStubRow($row); + } + } diff --git a/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php b/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php index b464ee004..912371c25 100644 --- a/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php +++ b/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php @@ -24,6 +24,11 @@ class FileUri extends ProcessPluginBase { * {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + // If we're stubbing a file entity, return a uri of NULL so it will get + // stubbed by the general process. + if ($row->isStub()) { + return NULL; + } list($filepath, $file_directory_path, $temp_directory_path, $is_public) = $value; // Specific handling using $temp_directory_path for temporary files. diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/src/Tests/FileFieldValidateTest.php index 291c43297..12fe92552 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/src/Tests/FileFieldValidateTest.php @@ -159,4 +159,35 @@ class FileFieldValidateTest extends FileFieldTestBase { $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with extension checking.'); } + /** + * Checks that a file can always be removed if it does not pass validation. + */ + public function testFileRemoval() { + $node_storage = $this->container->get('entity.manager')->getStorage('node'); + $type_name = 'article'; + $field_name = 'file_test'; + $this->createFileField($field_name, 'node', $type_name); + + $test_file = $this->getTestFile('image'); + + // Disable extension checking. + $this->updateFileField($field_name, $type_name, array('file_extensions' => '')); + + // Check that the file can be uploaded with no extension checking. + $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); + $node_storage->resetCache(array($nid)); + $node = $node_storage->load($nid); + $node_file = File::load($node->{$field_name}->target_id); + $this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.'); + $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.'); + + // Enable extension checking for text files. + $this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt')); + + // Check that the file can still be removed. + $this->removeNodeFile($nid); + $this->assertNoText('Only files with the following extensions are allowed: txt.'); + $this->assertText('Article ' . $node->getTitle() . ' has been updated.'); + } + } diff --git a/core/modules/file/src/Tests/Migrate/MigrateFileStubTest.php b/core/modules/file/src/Tests/Migrate/MigrateFileStubTest.php new file mode 100644 index 000000000..ae7446c87 --- /dev/null +++ b/core/modules/file/src/Tests/Migrate/MigrateFileStubTest.php @@ -0,0 +1,42 @@ +installEntitySchema('file'); + } + + /** + * Tests creation of file stubs. + */ + public function testStub() { + $this->performStubTest('file'); + } + +} diff --git a/core/modules/file/templates/file-widget.html.twig b/core/modules/file/templates/file-widget.html.twig deleted file mode 100644 index 892ed3d83..000000000 --- a/core/modules/file/templates/file-widget.html.twig +++ /dev/null @@ -1,17 +0,0 @@ -{# -/** - * @file - * Default theme implementation to display a file widget. - * - * Available variables: - * - element: Form element for the managed file. - * - attributes: Remaining HTML attributes for the containing element. - * - * @see template_preprocess_file_widget() - * - * @ingroup themeable - */ -#} - - {{ element }} - diff --git a/core/modules/file/tests/src/Kernel/FileItemValidationTest.php b/core/modules/file/tests/src/Kernel/FileItemValidationTest.php new file mode 100644 index 000000000..fbfcbd18e --- /dev/null +++ b/core/modules/file/tests/src/Kernel/FileItemValidationTest.php @@ -0,0 +1,119 @@ +installEntitySchema('user'); + $this->installEntitySchema('file'); + $this->installSchema('file', 'file_usage'); + $this->installSchema('system', 'sequences'); + + $this->user = User::create([ + 'name' => 'username', + 'status' => 1, + ]); + $this->user->save(); + } + + /** + * @covers \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraint + * @covers \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator + * @dataProvider getFileTypes + */ + public function testFileValidationConstraint($file_type) { + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_test_file', + 'entity_type' => 'entity_test', + 'type' => $file_type, + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_name' => 'field_test_file', + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + 'settings' => [ + 'max_filesize' => '2k', + 'file_extensions' => 'jpg|png', + ], + ]); + $field->save(); + + vfsStream::setup('drupal_root'); + vfsStream::create([ + 'sites' => [ + 'default' => [ + 'files' => [ + 'test.txt' => str_repeat('a', 3000), + ] + ] + ] + ]); + + // Test for max filesize. + $file = File::create([ + 'uri' => 'vfs://drupal_root/sites/default/files/test.txt', + ]); + $file->setPermanent(); + $file->save(); + + $entity_test = EntityTest::create([ + 'uid' => $this->user->id(), + 'field_test_file' => [ + 'target_id' => $file->id(), + ] + ]); + $result = $entity_test->validate(); + $this->assertCount(2, $result); + + $this->assertEquals('field_test_file.0', $result->get(0)->getPropertyPath()); + $this->assertEquals('The file is 2.93 KB exceeding the maximum file size of 2 KB.', (string) $result->get(0)->getMessage()); + $this->assertEquals('field_test_file.0', $result->get(1)->getPropertyPath()); + $this->assertEquals('Only files with the following extensions are allowed: jpg|png.', (string) $result->get(1)->getMessage()); + } + + /** + * Provides a list of file types to test. + */ + public function getFileTypes() { + return [['file'], ['image']]; + } + +} diff --git a/core/modules/history/history.routing.yml b/core/modules/history/history.routing.yml index f4db92a27..ff01a7c33 100644 --- a/core/modules/history/history.routing.yml +++ b/core/modules/history/history.routing.yml @@ -11,3 +11,4 @@ history.read_node: _controller: '\Drupal\history\Controller\HistoryController::readNode' requirements: _entity_access: 'node.view' + node: \d+ diff --git a/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php b/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php index 221692732..9abfe5639 100644 --- a/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php +++ b/core/modules/history/src/Plugin/views/filter/HistoryUserTimestamp.php @@ -81,8 +81,7 @@ class HistoryUserTimestamp extends FilterPluginBase { $clause = ''; $clause2 = ''; - if (\Drupal::moduleHandler()->moduleExists('comment')) { - $ces = $this->query->ensureTable('comment_entity_statistics', $this->relationship); + if ($ces = $this->query->ensureTable('comment_entity_statistics', $this->relationship)) { $clause = ("OR $ces.last_comment_timestamp > (***CURRENT_TIME*** - $limit)"); $clause2 = "OR $field < $ces.last_comment_timestamp"; } diff --git a/core/modules/history/src/Tests/Views/HistoryTimestampTest.php b/core/modules/history/src/Tests/Views/HistoryTimestampTest.php index 44f67f958..a76a5e804 100644 --- a/core/modules/history/src/Tests/Views/HistoryTimestampTest.php +++ b/core/modules/history/src/Tests/Views/HistoryTimestampTest.php @@ -80,5 +80,14 @@ class HistoryTimestampTest extends ViewTestBase { $this->executeView($view); $this->assertEqual(count($view->result), 1); $this->assertIdenticalResultset($view, array(array('nid' => $nodes[0]->id())), $column_map); + + // Install Comment module and make sure that content types without comment + // field will not break the view. + // See \Drupal\history\Plugin\views\filter\HistoryUserTimestamp::query() + \Drupal::service('module_installer')->install(['comment']); + $view = Views::getView('test_history'); + $view->setDisplay('page_2'); + $this->executeView($view); + } } diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php index 442929fe7..9ec3b6794 100644 --- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php +++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php @@ -43,7 +43,7 @@ use Drupal\file\Plugin\Field\FieldType\FileItem; * }, * }, * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList", - * constraints = {"ValidReference" = {}, "ReferenceAccess" = {}} + * constraints = {"ReferenceAccess" = {}, "FileValidation" = {}} * ) */ class ImageItem extends FileItem { @@ -346,10 +346,10 @@ class ImageItem extends FileItem { if ($path = $random->image(drupal_realpath($destination), $min_resolution, $max_resolution)) { $image = File::create(); $image->setFileUri($path); - // $image->setOwner($account); + $image->setOwnerId(\Drupal::currentUser()->id()); $image->setMimeType('image/' . pathinfo($path, PATHINFO_EXTENSION)); $image->setFileName(drupal_basename($path)); - $destination_dir = $settings['uri_scheme'] . '://' . $settings['file_directory']; + $destination_dir = static::doGetUploadLocation($settings); file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY); $destination = $destination_dir . '/' . basename($path); $file = file_move($image, $destination, FILE_CREATE_DIRECTORY); diff --git a/core/modules/language/language.install b/core/modules/language/language.install new file mode 100644 index 000000000..dcec16ae6 --- /dev/null +++ b/core/modules/language/language.install @@ -0,0 +1,13 @@ +invalidateContainer(); +} diff --git a/core/modules/language/language.services.yml b/core/modules/language/language.services.yml index fc9bba1cf..19d0f21a9 100644 --- a/core/modules/language/language.services.yml +++ b/core/modules/language/language.services.yml @@ -9,7 +9,7 @@ services: - [initLanguageManager] language.config_subscriber: class: Drupal\language\EventSubscriber\ConfigSubscriber - arguments: ['@language_manager', '@language.default', '@config.factory'] + arguments: ['@language_manager', '@language.default', '@config.factory', '@language_negotiator'] tags: - { name: event_subscriber } language.config_factory_override: diff --git a/core/modules/language/src/EventSubscriber/ConfigSubscriber.php b/core/modules/language/src/EventSubscriber/ConfigSubscriber.php index e1875ce7d..f1c758234 100644 --- a/core/modules/language/src/EventSubscriber/ConfigSubscriber.php +++ b/core/modules/language/src/EventSubscriber/ConfigSubscriber.php @@ -14,6 +14,8 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Drupal\language\ConfigurableLanguageManager; +use Drupal\language\HttpKernel\PathProcessorLanguage; +use Drupal\language\LanguageNegotiatorInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -42,6 +44,20 @@ class ConfigSubscriber implements EventSubscriberInterface { */ protected $configFactory; + /** + * The language negotiator. + * + * @var \Drupal\language\LanguageNegotiatorInterface + */ + protected $languageNegotiator; + + /** + * The language path processor. + * + * @var \Drupal\language\HttpKernel\PathProcessorLanguage + */ + protected $pathProcessorLanguage; + /** * Constructs a new class object. * @@ -51,11 +67,14 @@ class ConfigSubscriber implements EventSubscriberInterface { * The default language. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The configuration factory. + * @param \Drupal\language\LanguageNegotiatorInterface $language_negotiator + * The language negotiator. */ - public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory) { + public function __construct(LanguageManagerInterface $language_manager, LanguageDefault $language_default, ConfigFactoryInterface $config_factory, LanguageNegotiatorInterface $language_negotiator) { $this->languageManager = $language_manager; $this->languageDefault = $language_default; $this->configFactory = $config_factory; + $this->languageNegotiator = $language_negotiator; } /** @@ -102,6 +121,25 @@ class ConfigSubscriber implements EventSubscriberInterface { // Trigger a container rebuild on the next request by invalidating it. ConfigurableLanguageManager::rebuildServices(); } + elseif ($saved_config->getName() == 'language.types' && $event->isChanged('negotiation')) { + // If the negotiation configuration changed the language negotiator and + // the language path processor have to be reset so that they regenerate + // the method instances and also sort them accordingly to the new config. + $this->languageNegotiator->reset(); + if (isset($this->pathProcessorLanguage)) { + $this->pathProcessorLanguage->reset(); + } + } + } + + /** + * Injects the language path processors on multilingual site configuration. + * + * @param \Drupal\language\HttpKernel\PathProcessorLanguage $path_processor_language + * The language path processor. + */ + public function setPathProcessorLanguage(PathProcessorLanguage $path_processor_language) { + $this->pathProcessorLanguage = $path_processor_language; } /** diff --git a/core/modules/language/src/Form/ContentLanguageSettingsForm.php b/core/modules/language/src/Form/ContentLanguageSettingsForm.php index ebbf0fd95..4b7d4a50a 100644 --- a/core/modules/language/src/Form/ContentLanguageSettingsForm.php +++ b/core/modules/language/src/Form/ContentLanguageSettingsForm.php @@ -63,7 +63,7 @@ class ContentLanguageSettingsForm extends FormBase { $bundles = $this->entityManager->getAllBundleInfo(); $language_configuration = array(); foreach ($entity_types as $entity_type_id => $entity_type) { - if (!$entity_type instanceof ContentEntityTypeInterface || !$entity_type->hasKey('langcode')) { + if (!$entity_type instanceof ContentEntityTypeInterface || !$entity_type->hasKey('langcode') || !isset($bundles[$entity_type_id])) { continue; } $labels[$entity_type_id] = $entity_type->getLabel() ?: $entity_type_id; diff --git a/core/modules/language/src/HttpKernel/PathProcessorLanguage.php b/core/modules/language/src/HttpKernel/PathProcessorLanguage.php index caddfc0a0..aa5870915 100644 --- a/core/modules/language/src/HttpKernel/PathProcessorLanguage.php +++ b/core/modules/language/src/HttpKernel/PathProcessorLanguage.php @@ -13,6 +13,7 @@ use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\language\ConfigurableLanguageManagerInterface; +use Drupal\language\EventSubscriber\ConfigSubscriber; use Drupal\language\LanguageNegotiatorInterface; use Symfony\Component\HttpFoundation\Request; use Drupal\Core\Session\AccountInterface; @@ -57,6 +58,14 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa */ protected $multilingual; + /** + * The language configuration event subscriber. + * + * @var \Drupal\language\EventSubscriber\ConfigSubscriber + */ + protected $configSubscriber; + + /** * Constructs a PathProcessorLanguage object. * @@ -68,12 +77,15 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa * The language negotiator. * @param \Drupal\Core\Session\AccountInterface $current_user * The current active user. + * @param \Drupal\language\EventSubscriber\ConfigSubscriber $config_subscriber + * The language configuration event subscriber. */ - public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user) { + public function __construct(ConfigFactoryInterface $config, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, AccountInterface $current_user, ConfigSubscriber $config_subscriber) { $this->config = $config; $this->languageManager = $language_manager; $this->negotiator = $negotiator; $this->negotiator->setCurrentUser($current_user); + $this->configSubscriber = $config_subscriber; } /** @@ -152,4 +164,22 @@ class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPa }); } + /** + * Initializes the injected event subscriber with the language path processor. + * + * The language path processor service is registered only on multilingual + * site configuration, thus we inject it in the event subscriber only when + * it is initialized. + */ + public function initConfigSubscriber() { + $this->configSubscriber->setPathProcessorLanguage($this); + } + + /** + * Resets the collected processors instances. + */ + public function reset() { + $this->processors = array(); + } + } diff --git a/core/modules/language/src/LanguageServiceProvider.php b/core/modules/language/src/LanguageServiceProvider.php index 2f1840c09..c3f1cd12d 100644 --- a/core/modules/language/src/LanguageServiceProvider.php +++ b/core/modules/language/src/LanguageServiceProvider.php @@ -39,7 +39,9 @@ class LanguageServiceProvider extends ServiceProviderBase { ->addArgument(new Reference('config.factory')) ->addArgument(new Reference('language_manager')) ->addArgument(new Reference('language_negotiator')) - ->addArgument(new Reference('current_user')); + ->addArgument(new Reference('current_user')) + ->addArgument(new Reference('language.config_subscriber')) + ->addMethodCall('initConfigSubscriber'); } } diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php new file mode 100644 index 000000000..4ce022dea --- /dev/null +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php @@ -0,0 +1,294 @@ +entityManager = $entity_manager; + $this->paths = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static($container->get('entity.manager')); + } + + /** + * {@inheritdoc} + */ + public function getLangcode(Request $request = NULL) { + $langcode = $request->get(static::QUERY_PARAMETER); + + $language_enabled = array_key_exists($langcode, $this->languageManager->getLanguages()); + return $language_enabled ? $langcode : NULL; + } + + /** + * {@inheritdoc} + */ + public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { + // If appropriate, process outbound to add a query parameter to the url and + // remove the language option, so that url negotiator does not rewrite the + // url. + + // First, check if processing conditions are met. + if (!($request && !empty($options['route']) && $this->hasLowerLanguageNegotiationWeight() && $this->meetsContentEntityRoutesCondition($options['route'], $request))) { + return $path; + } + + if (isset($options['language']) || $langcode = $this->getLangcode($request)) { + // If the language option is set, unset it, so that the url language + // negotiator does not rewrite the url. + if (isset($options['language'])) { + $langcode = $options['language']->getId(); + unset($options['language']); + } + + if (isset($options['query']) && is_string($options['query'])) { + $query = []; + parse_str($options['query'], $query); + $options['query'] = $query; + } + else { + $options['query'] = []; + } + + if (!isset($options['query'][static::QUERY_PARAMETER])) { + $query_addon = [static::QUERY_PARAMETER => $langcode]; + $options['query'] += $query_addon; + // @todo Remove this once https://www.drupal.org/node/2507005 lands. + $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($query_addon); + } + + if ($bubbleable_metadata) { + // Cached URLs that have been processed by this outbound path + // processor must be: + $bubbleable_metadata + // - varied by the content language query parameter. + ->addCacheContexts(['url.query_args:' . static::QUERY_PARAMETER]); + } + } + + return $path; + } + + /** + * {@inheritdoc} + */ + public function getLanguageSwitchLinks(Request $request, $type, Url $url) { + $links = []; + $query = []; + parse_str($request->getQueryString(), $query); + + foreach ($this->languageManager->getNativeLanguages() as $language) { + $langcode = $language->getId(); + $query[static::QUERY_PARAMETER] = $langcode; + $links[$langcode] = [ + 'url' => $url, + 'title' => $language->getName(), + 'attributes' => ['class' => ['language-link']], + 'query' => $query, + ]; + } + + return $links; + } + + /** + * Determines if content entity language negotiator has higher priority. + * + * The content entity language negotiator having higher priority than the url + * language negotiator, is a criteria in + * \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity::processOutbound(). + * + * @return bool + * TRUE if the the content entity language negotiator has higher priority + * than the url language negotiator, FALSE otherwise. + */ + protected function hasLowerLanguageNegotiationWeight() { + if (!isset($this->hasLowerLanguageNegotiationWeightResult)) { + // Only run if the LanguageNegotiationContentEntity outbound function is + // being executed before the outbound function of LanguageNegotiationUrl. + $content_method_weights = $this->config->get('language.types')->get('negotiation.language_content.enabled') ?: []; + + // Check if the content language is configured to be dependent on the + // url negotiator directly or indirectly over the interface negotiator. + if (isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) && ($content_method_weights[static::METHOD_ID] > $content_method_weights[LanguageNegotiationUrl::METHOD_ID])) { + $this->hasLowerLanguageNegotiationWeightResult = FALSE; + } + else { + $check_interface_method = FALSE; + if (isset($content_method_weights[LanguageNegotiationUI::METHOD_ID])) { + $interface_method_weights = $this->config->get('language.types')->get('negotiation.language_interface.enabled') ?: []; + $check_interface_method = isset($interface_method_weights[LanguageNegotiationUrl::METHOD_ID]); + } + if ($check_interface_method) { + $max_weight = $content_method_weights[LanguageNegotiationUI::METHOD_ID]; + $max_weight = isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) ? max($max_weight, $content_method_weights[LanguageNegotiationUrl::METHOD_ID]) : $max_weight; + } + else { + $max_weight = isset($content_method_weights[LanguageNegotiationUrl::METHOD_ID]) ? $content_method_weights[LanguageNegotiationUrl::METHOD_ID] : PHP_INT_MAX; + } + + $this->hasLowerLanguageNegotiationWeightResult = $content_method_weights[static::METHOD_ID] < $max_weight; + } + } + + return $this->hasLowerLanguageNegotiationWeightResult; + } + + /** + * Determines if content entity route condition is met. + * + * Requirements: currently being on an content entity route and processing + * outbound url pointing to the same content entity. + * + * @param \Symfony\Component\Routing\Route $outbound_route + * The route object for the current outbound url being processed. + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + * + * @return bool + * TRUE if the content entity route condition is met, FALSE otherwise. + */ + protected function meetsContentEntityRoutesCondition(Route $outbound_route, Request $request) { + $outbound_path_pattern = $outbound_route->getPath(); + $storage = isset($this->paths[$request]) ? $this->paths[$request] : []; + if (!isset($storage[$outbound_path_pattern])) { + $storage[$outbound_path_pattern] = FALSE; + + // Check if the outbound route points to the current entity. + if ($content_entity_type_id_for_current_route = $this->getContentEntityTypeIdForCurrentRequest($request)) { + if (!empty($this->getContentEntityPaths()[$outbound_path_pattern]) && $content_entity_type_id_for_current_route == $this->getContentEntityPaths()[$outbound_path_pattern]) { + $storage[$outbound_path_pattern] = TRUE; + } + } + + $this->paths[$request] = $storage; + } + + return $storage[$outbound_path_pattern]; + } + + /** + * Returns the content entity type ID from the current request for the route. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + * + * @return string + * The entity type ID for the route from the request. + */ + protected function getContentEntityTypeIdForCurrentRequest(Request $request) { + $content_entity_type_id_for_current_route = ''; + + if ($current_route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) { + $current_route_path = $current_route->getPath(); + $content_entity_type_id_for_current_route = isset($this->getContentEntityPaths()[$current_route_path]) ? $this->getContentEntityPaths()[$current_route_path] : ''; + } + + return $content_entity_type_id_for_current_route; + } + + /** + * Returns the paths for the link templates of all content entities. + * + * @return array + * An array of all content entity type IDs, keyed by the corresponding link + * template paths. + */ + protected function getContentEntityPaths() { + if (!isset($this->contentEntityPaths)) { + $this->contentEntityPaths = []; + $entity_types = $this->entityManager->getDefinitions(); + foreach ($entity_types as $entity_type_id => $entity_type) { + if ($entity_type->isSubclassOf(ContentEntityInterface::class)) { + $entity_paths = array_fill_keys($entity_type->getLinkTemplates(), $entity_type_id); + $this->contentEntityPaths = array_merge($this->contentEntityPaths, $entity_paths); + } + } + } + + return $this->contentEntityPaths; + } + +} diff --git a/core/modules/language/src/Tests/EntityUrlLanguageTest.php b/core/modules/language/src/Tests/EntityUrlLanguageTest.php index 915f7f605..f172b38f4 100644 --- a/core/modules/language/src/Tests/EntityUrlLanguageTest.php +++ b/core/modules/language/src/Tests/EntityUrlLanguageTest.php @@ -7,22 +7,34 @@ namespace Drupal\language\Tests; +use Drupal\Core\Language\LanguageInterface; use Drupal\entity_test\Entity\EntityTest; use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\simpletest\KernelTestBase; +use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationContentEntity; +use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Tests the language of entity URLs. * @group language */ -class EntityUrlLanguageTest extends KernelTestBase { +class EntityUrlLanguageTest extends LanguageTestBase { /** * Modules to enable. * * @var array */ - public static $modules = ['language', 'entity_test', 'user', 'system']; + public static $modules = ['entity_test', 'user']; + + /** + * The entity being used for testing. + * + * @var \Drupal\Core\Entity\ContentEntityInterface + */ + protected $entity; protected function setUp() { parent::setUp(); @@ -37,33 +49,93 @@ class EntityUrlLanguageTest extends KernelTestBase { ConfigurableLanguage::create(['id' => 'es'])->save(); ConfigurableLanguage::create(['id' => 'fr'])->save(); - $this->config('language.types')->setData([ - 'configurable' => ['language_interface'], - 'negotiation' => ['language_interface' => ['enabled' => ['language-url' => 0]]], - ])->save(); - $this->config('language.negotiation')->setData([ - 'url' => [ - 'source' => 'path_prefix', - 'prefixes' => ['en' => 'en', 'es' => 'es', 'fr' => 'fr'] - ], - ])->save(); + $config = $this->config('language.negotiation'); + $config->set('url.prefixes', ['en' => 'en', 'es' => 'es', 'fr' => 'fr']) + ->save(); $this->kernel->rebuildContainer(); - $this->container = $this->kernel->getContainer(); - \Drupal::setContainer($this->container); + + $this->createTranslatableEntity(); } /** * Ensures that entity URLs in a language have the right language prefix. */ public function testEntityUrlLanguage() { - $entity = EntityTest::create(); - $entity->addTranslation('es', ['name' => 'name spanish']); - $entity->addTranslation('fr', ['name' => 'name french']); - $entity->save(); + $this->assertTrue(strpos($this->entity->urlInfo()->toString(), '/en/entity_test/' . $this->entity->id()) !== FALSE); + $this->assertTrue(strpos($this->entity->getTranslation('es')->urlInfo()->toString(), '/es/entity_test/' . $this->entity->id()) !== FALSE); + $this->assertTrue(strpos($this->entity->getTranslation('fr')->urlInfo()->toString(), '/fr/entity_test/' . $this->entity->id()) !== FALSE); + } - $this->assertTrue(strpos($entity->urlInfo()->toString(), '/en/entity_test/' . $entity->id()) !== FALSE); - $this->assertTrue(strpos($entity->getTranslation('es')->urlInfo()->toString(), '/es/entity_test/' . $entity->id()) !== FALSE); - $this->assertTrue(strpos($entity->getTranslation('fr')->urlInfo()->toString(), '/fr/entity_test/' . $entity->id()) !== FALSE); + /** + * Ensures correct entity URLs with the method language-content-entity enabled. + * + * Test case with the method language-content-entity enabled and configured + * with higher and also with lower priority than the method language-url. + */ + public function testEntityUrlLanguageWithLanguageContentEnabled() { + // Define the method language-content-entity with a higher priority than + // language-url. + $config = $this->config('language.types'); + $config->set('configurable', [LanguageInterface::TYPE_INTERFACE, LanguageInterface::TYPE_CONTENT]); + $config->set('negotiation.language_content.enabled', [ + LanguageNegotiationContentEntity::METHOD_ID => 0, + LanguageNegotiationUrl::METHOD_ID => 1 + ]); + $config->save(); + + // Without being on an content entity route the default entity URL tests + // should still pass. + $this->testEntityUrlLanguage(); + + // Now switching to an entity route, so that the URL links are generated + // while being on an entity route. + $this->setCurrentRequestForRoute('/entity_test/{entity_test}', 'entity.entity_test.canonical'); + + // The method language-content-entity should run before language-url and + // append query parameter for the content language and prevent language-url + // from overwriting the url. + $this->assertTrue(strpos($this->entity->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=en') !== FALSE); + $this->assertTrue(strpos($this->entity->getTranslation('es')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=es') !== FALSE); + $this->assertTrue(strpos($this->entity->getTranslation('fr')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=fr') !== FALSE); + + // Define the method language-url with a higher priority than + // language-content-entity. This configuration should match the default one, + // where the language-content-entity is turned off. + $config->set('negotiation.language_content.enabled', [ + LanguageNegotiationUrl::METHOD_ID => 0, + LanguageNegotiationContentEntity::METHOD_ID => 1 + ]); + $config->save(); + + // The default entity URL tests should pass again with the current + // configuration. + $this->testEntityUrlLanguage(); + } + + /** + * Creates a translated entity. + */ + protected function createTranslatableEntity() { + $this->entity = EntityTest::create(); + $this->entity->addTranslation('es', ['name' => 'name spanish']); + $this->entity->addTranslation('fr', ['name' => 'name french']); + $this->entity->save(); + } + + /** + * Sets the current request to a specific path with the corresponding route. + * + * @param string $path + * The path for which the current request should be created. + * @param string $route_name + * The route name for which the route object for the request should be + * created. + */ + protected function setCurrentRequestForRoute($path, $route_name) { + $request = Request::create($path); + $request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name); + $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route($path)); + $this->container->get('request_stack')->push($request); } } diff --git a/core/modules/language/src/Tests/LanguageNegotiationContentEntityTest.php b/core/modules/language/src/Tests/LanguageNegotiationContentEntityTest.php new file mode 100644 index 000000000..5373096fc --- /dev/null +++ b/core/modules/language/src/Tests/LanguageNegotiationContentEntityTest.php @@ -0,0 +1,182 @@ + 'es'])->save(); + ConfigurableLanguage::create(['id' => 'fr'])->save(); + + // In order to reflect the changes for a multilingual site in the container + // we have to rebuild it. + $this->rebuildContainer(); + + $this->createTranslatableEntity(); + + $user = $this->drupalCreateUser(array('view test entity')); + $this->drupalLogin($user); + } + + /** + * Tests default with content language remaining same as interface language. + */ + public function testDefaultConfiguration() { + $translation = $this->entity; + $this->drupalGet($translation->urlInfo()); + $last = $this->container->get('state')->get('language_test.language_negotiation_last'); + $last_content_language = $last[LanguageInterface::TYPE_CONTENT]; + $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE]; + $this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()])); + + $translation = $this->entity->getTranslation('es'); + $this->drupalGet($translation->urlInfo()); + $last = $this->container->get('state')->get('language_test.language_negotiation_last'); + $last_content_language = $last[LanguageInterface::TYPE_CONTENT]; + $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE]; + $this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()])); + + $translation = $this->entity->getTranslation('fr'); + $this->drupalGet($translation->urlInfo()); + $last = $this->container->get('state')->get('language_test.language_negotiation_last'); + $last_content_language = $last[LanguageInterface::TYPE_CONTENT]; + $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE]; + $this->assertTrue(($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), new FormattableMarkup('Interface language %interface_language and Content language %content_language are the same as the translation language %translation_language of the entity.', ['%interface_language' => $last_interface_language, '%content_language' => $last_content_language, '%translation_language' => $translation->language()->getId()])); + } + + /** + * Tests enabling the language negotiator language_content_entity. + */ + public function testEnabledLanguageContentNegotiator() { + // Define the method language-url with a higher priority than + // language-content-entity. This configuration should match the default one, + // where the language-content-entity is turned off. + $config = $this->config('language.types'); + $config->set('configurable', [LanguageInterface::TYPE_INTERFACE, LanguageInterface::TYPE_CONTENT]); + $config->set('negotiation.language_content.enabled', [ + LanguageNegotiationUrl::METHOD_ID => 0, + LanguageNegotiationContentEntity::METHOD_ID => 1 + ]); + $config->save(); + + // In order to reflect the changes for a multilingual site in the container + // we have to rebuild it. + $this->rebuildContainer(); + + // The tests for the default configuration should still pass. + $this->testDefaultConfiguration(); + + // Define the method language-content-entity with a higher priority than + // language-url. + $config->set('negotiation.language_content.enabled', [ + LanguageNegotiationContentEntity::METHOD_ID => 0, + LanguageNegotiationUrl::METHOD_ID => 1 + ]); + $config->save(); + + // In order to reflect the changes for a multilingual site in the container + // we have to rebuild it. + $this->rebuildContainer(); + + // The method language-content-entity should run before language-url and + // append query parameter for the content language and prevent language-url + // from overwriting the URL. + $default_site_langcode = $this->config('system.site')->get('default_langcode'); + + // Now switching to an entity route, so that the URL links are generated + // while being on an entity route. + $this->setCurrentRequestForRoute('/entity_test/{entity_test}', 'entity.entity_test.canonical'); + + $translation = $this->entity; + $this->drupalGet($translation->urlInfo()); + $last = $this->container->get('state')->get('language_test.language_negotiation_last'); + $last_content_language = $last[LanguageInterface::TYPE_CONTENT]; + $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE]; + $this->assertTrue(($last_interface_language == $default_site_langcode) && ($last_interface_language == $last_content_language) && ($last_content_language == $translation->language()->getId()), 'Interface language and Content language are the same as the default translation language of the entity.'); + $this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.'); + $this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.'); + + $translation = $this->entity->getTranslation('es'); + $this->drupalGet($translation->urlInfo()); + $last = $this->container->get('state')->get('language_test.language_negotiation_last'); + $last_content_language = $last[LanguageInterface::TYPE_CONTENT]; + $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE]; + $this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.'); + $this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.'); + + $translation = $this->entity->getTranslation('fr'); + $this->drupalGet($translation->urlInfo()); + $last = $this->container->get('state')->get('language_test.language_negotiation_last'); + $last_content_language = $last[LanguageInterface::TYPE_CONTENT]; + $last_interface_language = $last[LanguageInterface::TYPE_INTERFACE]; + $this->assertTrue($last_interface_language == $default_site_langcode, 'Interface language did not change from the default site language.'); + $this->assertTrue($last_content_language == $translation->language()->getId(), 'Content language matches the current entity translation language.'); + } + + /** + * Creates a translated entity. + */ + protected function createTranslatableEntity() { + $this->entity = EntityTest::create(); + $this->entity->addTranslation('es', ['name' => 'name spanish']); + $this->entity->addTranslation('fr', ['name' => 'name french']); + $this->entity->save(); + } + + /** + * Sets the current request to a specific path with the corresponding route. + * + * @param string $path + * The path for which the current request should be created. + * @param string $route_name + * The route name for which the route object for the request should be + * created. + */ + protected function setCurrentRequestForRoute($path, $route_name) { + $request = Request::create($path); + $request->attributes->set(RouteObjectInterface::ROUTE_NAME, $route_name); + $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route($path)); + $this->container->get('request_stack')->push($request); + } + +} diff --git a/core/modules/language/src/Tests/LanguageSelectorTranslatableTest.php b/core/modules/language/src/Tests/LanguageSelectorTranslatableTest.php index 15290fdd2..7f9a72ecd 100644 --- a/core/modules/language/src/Tests/LanguageSelectorTranslatableTest.php +++ b/core/modules/language/src/Tests/LanguageSelectorTranslatableTest.php @@ -84,7 +84,7 @@ class LanguageSelectorTranslatableTest extends WebTestBase { $this->drupalGet($path); // Get en language from selector. - $elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => 'edit-settings-node-node-settings-language-langcode', ':option' => 'en')); + $elements = $this->xpath('//select[@id=:id]//option[@value=:option]', array(':id' => 'edit-settings-user-user-settings-language-langcode', ':option' => 'en')); // Check that the language text is translated. $this->assertEqual((string) $elements[0], $name_translation, 'Checking the option string English is translated to Spanish.'); diff --git a/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php b/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php index 4645d68bc..16ba13e46 100644 --- a/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php +++ b/core/modules/language/src/Tests/LanguageUILanguageNegotiationTest.php @@ -221,8 +221,13 @@ class LanguageUILanguageNegotiationTest extends WebTestBase { // Unknown language prefix should return 404. $definitions = \Drupal::languageManager()->getNegotiator()->getNegotiationMethods(); + // Enable only methods, which are either not limited to a specific language + // type or are supporting the interface language type. + $language_interface_method_definitions = array_filter($definitions, function ($method_definition) { + return !isset($method_definition['types']) || (isset($method_definition['types']) && in_array(LanguageInterface::TYPE_INTERFACE, $method_definition['types'])); + }); $this->config('language.types') - ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', array_flip(array_keys($definitions))) + ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', array_flip(array_keys($language_interface_method_definitions))) ->save(); $this->drupalGet("$langcode_unknown/admin/config", array(), $http_header_browser_fallback); $this->assertResponse(404, "Unknown language path prefix should return 404"); diff --git a/core/modules/language/templates/language-negotiation-configure-form.html.twig b/core/modules/language/templates/language-negotiation-configure-form.html.twig index 12528acd4..60717314b 100644 --- a/core/modules/language/templates/language-negotiation-configure-form.html.twig +++ b/core/modules/language/templates/language-negotiation-configure-form.html.twig @@ -1,24 +1,25 @@ {# /** -* @file -* Default theme implementation for a language negotiation configuration form. -* -* Available variables: -* - language_types: A list of language negotiation types. Each language type -* contains the following: -* - type: The machine name for the negotiation type. -* - title: The language negotiation type name. -* - description: A description for how the language negotiation type operates. -* - configurable: A radio element to toggle the table. -* - table: A draggable table for the language detection methods of this type. -* - children: Remaining form items for the group. -* - attributes: A list of HTML attributes for the wrapper element. -* - children: Remaining form items for all groups. -* -* @see template_preprocess_language_negotiation_configure_form() -* -* @ingroup themeable -*/ + * @file + * Default theme implementation for a language negotiation configuration form. + * + * Available variables: + * - language_types: A list of language negotiation types. Each language type + * contains the following: + * - type: The machine name for the negotiation type. + * - title: The language negotiation type name. + * - description: A description for how the language negotiation type + * operates. + * - configurable: A radio element to toggle the table. + * - table: A draggable table for the language detection methods of this type. + * - children: Remaining form items for the group. + * - attributes: A list of HTML attributes for the wrapper element. + * - children: Remaining form items for all groups. + * + * @see template_preprocess_language_negotiation_configure_form() + * + * @ingroup themeable + */ #} {% for language_type in language_types %} {% diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php index 5c21ace87..722a0c1de 100644 --- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php +++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php @@ -121,25 +121,30 @@ class LinkItem extends FieldItemBase implements LinkItemInterface { * {@inheritdoc} */ public static function generateSampleValue(FieldDefinitionInterface $field_definition) { - // Set of possible top-level domains. - $tlds = array('com', 'net', 'gov', 'org', 'edu', 'biz', 'info'); - // Set random length for the domain name. - $domain_length = mt_rand(7, 15); $random = new Random(); + if ($field_definition->getItemDefinition()->getSetting('link_type') & LinkItemInterface::LINK_EXTERNAL) { + // Set of possible top-level domains. + $tlds = array('com', 'net', 'gov', 'org', 'edu', 'biz', 'info'); + // Set random length for the domain name. + $domain_length = mt_rand(7, 15); - switch ($field_definition->getSetting('title')) { - case DRUPAL_DISABLED: - $values['title'] = ''; - break; - case DRUPAL_REQUIRED: - $values['title'] = $random->sentences(4); - break; - case DRUPAL_OPTIONAL: - // In case of optional title, randomize its generation. - $values['title'] = mt_rand(0,1) ? $random->sentences(4) : ''; - break; + switch ($field_definition->getSetting('title')) { + case DRUPAL_DISABLED: + $values['title'] = ''; + break; + case DRUPAL_REQUIRED: + $values['title'] = $random->sentences(4); + break; + case DRUPAL_OPTIONAL: + // In case of optional title, randomize its generation. + $values['title'] = mt_rand(0, 1) ? $random->sentences(4) : ''; + break; + } + $values['uri'] = 'http://www.' . $random->word($domain_length) . '.' . $tlds[mt_rand(0, (sizeof($tlds) - 1))]; + } + else { + $values['uri'] = 'base:' . $random->name(mt_rand(1, 64)); } - $values['uri'] = 'http://www.' . $random->word($domain_length) . '.' . $tlds[mt_rand(0, (sizeof($tlds)-1))]; return $values; } diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php index dad78ab69..98b29e0bd 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkNotExistingInternalConstraintValidator.php @@ -7,6 +7,8 @@ namespace Drupal\link\Plugin\Validation\Constraint; +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidatorInterface; @@ -50,9 +52,17 @@ class LinkNotExistingInternalConstraintValidator implements ConstraintValidatorI try { $url->toString(); } + // The following exceptions are all possible during URL generation, and + // should be considered as disallowed URLs. catch (RouteNotFoundException $e) { $allowed = FALSE; } + catch (InvalidParameterException $e) { + $allowed = FALSE; + } + catch (MissingMandatoryParametersException $e) { + $allowed = FALSE; + } if (!$allowed) { $this->context->addViolation($constraint->message, array('@uri' => $value->uri)); } diff --git a/core/modules/link/src/Tests/LinkFieldTest.php b/core/modules/link/src/Tests/LinkFieldTest.php index 494936a09..eada49430 100644 --- a/core/modules/link/src/Tests/LinkFieldTest.php +++ b/core/modules/link/src/Tests/LinkFieldTest.php @@ -129,8 +129,6 @@ class LinkFieldTest extends WebTestBase { 'entity:user/1' => '- Restricted access - (1)', // URI for an entity that doesn't exist, but with a valid ID. 'entity:user/999999' => 'entity:user/999999', - // URI for an entity that doesn't exist, with an invalid ID. - 'entity:user/invalid-parameter' => 'entity:user/invalid-parameter', ); // Define some invalid URLs. @@ -146,6 +144,8 @@ class LinkFieldTest extends WebTestBase { $invalid_internal_entries = array( 'no-leading-slash' => $validation_error_2, 'entity:non_existing_entity_type/yar' => $validation_error_1, + // URI for an entity that doesn't exist, with an invalid ID. + 'entity:user/invalid-parameter' => $validation_error_1, ); // Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC. diff --git a/core/modules/link/src/Tests/LinkItemTest.php b/core/modules/link/src/Tests/LinkItemTest.php index 382a464f1..1115983f9 100644 --- a/core/modules/link/src/Tests/LinkItemTest.php +++ b/core/modules/link/src/Tests/LinkItemTest.php @@ -10,7 +10,10 @@ namespace Drupal\link\Tests; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemInterface; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Tests\FieldUnitTestBase; +use Drupal\link\LinkItemInterface; /** * Tests the new entity API for the link field type. @@ -29,17 +32,40 @@ class LinkItemTest extends FieldUnitTestBase { protected function setUp() { parent::setUp(); - // Create a link field for validation. - entity_create('field_storage_config', array( + // Create a generic, external, and internal link fields for validation. + FieldStorageConfig::create([ 'field_name' => 'field_test', 'entity_type' => 'entity_test', 'type' => 'link', - ))->save(); - entity_create('field_config', array( + ])->save(); + FieldConfig::create([ 'entity_type' => 'entity_test', 'field_name' => 'field_test', 'bundle' => 'entity_test', - ))->save(); + 'settings' => ['link_type' => LinkItemInterface::LINK_GENERIC], + ])->save(); + FieldStorageConfig::create([ + 'field_name' => 'field_test_external', + 'entity_type' => 'entity_test', + 'type' => 'link', + ])->save(); + FieldConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_test_external', + 'bundle' => 'entity_test', + 'settings' => ['link_type' => LinkItemInterface::LINK_EXTERNAL], + ])->save(); + FieldStorageConfig::create([ + 'field_name' => 'field_test_internal', + 'entity_type' => 'entity_test', + 'type' => 'link', + ])->save(); + FieldConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_test_internal', + 'bundle' => 'entity_test', + 'settings' => ['link_type' => LinkItemInterface::LINK_INTERNAL], + ])->save(); } /** @@ -130,9 +156,12 @@ class LinkItemTest extends FieldUnitTestBase { $entity->field_test[0] = NULL; $this->assertNull($entity->field_test[0]->getValue()); - // Test the generateSampleValue() method. + // Test the generateSampleValue() method for generic, external, and internal + // link types. $entity = entity_create('entity_test'); $entity->field_test->generateSampleItems(); + $entity->field_test_external->generateSampleItems(); + $entity->field_test_internal->generateSampleItems(); $this->entityValidateAndSave($entity); } diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationImportTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationImportTest.php index b0450e55b..dbe831099 100644 --- a/core/modules/locale/src/Tests/LocaleConfigTranslationImportTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigTranslationImportTest.php @@ -78,6 +78,10 @@ class LocaleConfigTranslationImportTest extends WebTestBase { $this->container->get('module_installer')->install(['block', 'config_translation']); $this->resetAll(); + // The testing profile overrides locale.settings to disable translation + // import. Test that this override is in place. + $this->assertFalse($this->config('locale.settings')->get('translation.import_enabled'), 'Translations imports are disabled by default in the Testing profile.'); + $admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'administer permissions', 'translate configuration')); $this->drupalLogin($admin_user); diff --git a/core/modules/locale/src/Tests/LocalePluralFormatTest.php b/core/modules/locale/src/Tests/LocalePluralFormatTest.php index ba5cf84a2..a8dfa453e 100644 --- a/core/modules/locale/src/Tests/LocalePluralFormatTest.php +++ b/core/modules/locale/src/Tests/LocalePluralFormatTest.php @@ -144,6 +144,66 @@ class LocalePluralFormatTest extends WebTestBase { } } + /** + * Tests plural editing of DateFormatter strings + */ + public function testPluralEditDateFormatter() { + + // Import some .po files with formulas to set up the environment. + // These will also add the languages to the system. + $this->importPoFile($this->getPoFileWithSimplePlural(), array( + 'langcode' => 'fr', + )); + + // Set French as the site default language. + $this->config('system.site')->set('default_langcode', 'fr')->save(); + + // Visit User Info page before updating translation strings. + $this->drupalGet('user'); + + // Member for time should be translated. + $this->assertText("seconde", "'Member for' text is translated."); + + $path = 'admin/config/regional/translate/'; + $search = array( + 'langcode' => 'fr', + // Limit to only translated strings to ensure that database ordering does + // not break the test. + 'translation' => 'translated', + ); + $this->drupalPostForm($path, $search, t('Filter')); + // Plural values for the langcode fr. + $this->assertText('@count seconde'); + $this->assertText('@count secondes'); + + // Inject a plural source string to the database. We need to use a specific + // langcode here because the language will be English by default and will + // not save our source string for performance optimization if we do not ask + // specifically for a language. + \Drupal::translation()->formatPlural(1, '1 second', '@count seconds', array(), array('langcode' => 'fr'))->render(); + $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 second" . LOCALE_PLURAL_DELIMITER . "@count seconds"))->fetchField(); + // Look up editing page for this plural string and check fields. + $search = array( + 'string' => '1 second', + 'langcode' => 'fr', + ); + $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter')); + + // Save complete translations for the string in langcode fr. + $edit = array( + "strings[$lid][translations][0]" => '1 seconde updated', + "strings[$lid][translations][1]" => '@count secondes updated', + ); + $this->drupalPostForm($path, $edit, t('Save translations')); + + // User interface input for translating seconds should not be duplicated + $this->assertUniqueText('@count seconds', 'Interface translation input for @count seconds only appears once.'); + + // Member for time should be translated. + $this->drupalGet('user'); + $this->assertText("seconde", "'Member for' text is translated."); + } + /** * Tests plural editing and export functionality. */ @@ -308,6 +368,11 @@ msgid_plural "@count hours" msgstr[0] "@count heure" msgstr[1] "@count heures" +msgid "1 second" +msgid_plural "@count seconds" +msgstr[0] "@count seconde" +msgstr[1] "@count secondes" + msgid "Monday" msgstr "lundi" EOF; diff --git a/core/modules/menu_link_content/src/Tests/Migrate/MigrateMenuLinkContentStubTest.php b/core/modules/menu_link_content/src/Tests/Migrate/MigrateMenuLinkContentStubTest.php new file mode 100644 index 000000000..47e94d134 --- /dev/null +++ b/core/modules/menu_link_content/src/Tests/Migrate/MigrateMenuLinkContentStubTest.php @@ -0,0 +1,42 @@ +installEntitySchema('menu_link_content'); + } + + /** + * Tests creation of menu link content stubs. + */ + public function testStub() { + $this->performStubTest('menu_link_content'); + } + +} diff --git a/core/modules/migrate/src/MigrateExecutable.php b/core/modules/migrate/src/MigrateExecutable.php index 2749313d3..a3706239e 100644 --- a/core/modules/migrate/src/MigrateExecutable.php +++ b/core/modules/migrate/src/MigrateExecutable.php @@ -401,8 +401,8 @@ class MigrateExecutable implements MigrateExecutableInterface { $multiple = $multiple || $plugin->multiple(); } } - // No plugins means do not set. - if ($plugins) { + // No plugins or no value means do not set. + if ($plugins && !is_null($value)) { $row->setDestinationProperty($destination, $value); } // Reset the value. diff --git a/core/modules/migrate/src/Plugin/MigrateSourceInterface.php b/core/modules/migrate/src/Plugin/MigrateSourceInterface.php index efcf89861..3620e6777 100644 --- a/core/modules/migrate/src/Plugin/MigrateSourceInterface.php +++ b/core/modules/migrate/src/Plugin/MigrateSourceInterface.php @@ -44,10 +44,14 @@ interface MigrateSourceInterface extends \Countable, \Iterator, PluginInspection public function __toString(); /** - * Get the source ids. + * Defines the source fields uniquely identifying a source row. None of these + * fields should contain a NULL value - if necessary, use prepareRow() or + * hook_migrate_prepare_row() to rewrite NULL values to appropriate empty + * values (such as '' or 0). * * @return array - * The source ids. + * Array keyed by source field name, with values being a schema array + * describing the field (such as ['type' => 'string]). */ public function getIds(); diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php index 64123d6d4..f7a6a1e93 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php @@ -136,19 +136,6 @@ abstract class Entity extends DestinationBase implements ContainerFactoryPluginI return $row->getDestinationProperty($this->getKey('id')); } - /** - * Process the stub values. - * - * @param \Drupal\migrate\Row $row - * The row of data. - */ - protected function processStubRow(Row $row) { - $bundle_key = $this->getKey('bundle'); - if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) { - $row->setDestinationProperty($bundle_key, reset($this->bundles)); - } - } - /** * Returns a specific entity key. * diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php index ae3030a11..e2320cd75 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php @@ -7,12 +7,17 @@ namespace Drupal\migrate\Plugin\migrate\destination; +use Drupal\Component\Utility\Random; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\link\LinkItemInterface; use Drupal\migrate\Entity\MigrationInterface; +use Drupal\migrate\MigrateException; use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate\Row; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -29,6 +34,13 @@ class EntityContentBase extends Entity { */ protected $entityManager; + /** + * Field type plugin manager. + * + * @var \Drupal\Core\Field\FieldTypePluginManagerInterface + */ + protected $fieldTypeManager; + /** * Constructs a content entity. * @@ -46,10 +58,13 @@ class EntityContentBase extends Entity { * The list of bundles this entity type has. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager service. + * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager + * The field type plugin manager service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles); $this->entityManager = $entity_manager; + $this->fieldTypeManager = $field_type_manager; } /** @@ -64,7 +79,8 @@ class EntityContentBase extends Entity { $migration, $container->get('entity.manager')->getStorage($entity_type), array_keys($container->get('entity.manager')->getBundleInfo($entity_type)), - $container->get('entity.manager') + $container->get('entity.manager'), + $container->get('plugin.manager.field.field_type') ); } @@ -74,6 +90,9 @@ class EntityContentBase extends Entity { public function import(Row $row, array $old_destination_id_values = array()) { $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE; $entity = $this->getEntity($row, $old_destination_id_values); + if (!$entity) { + throw new MigrateException('Unable to get entity'); + } return $this->save($entity, $old_destination_id_values); } @@ -132,4 +151,47 @@ class EntityContentBase extends Entity { $this->setRollbackAction($row->getIdMap()); } + /** + * Do as much population of the stub row as we can. + * + * @param \Drupal\migrate\Row $row + * The row of data. + */ + protected function processStubRow(Row $row) { + $bundle_key = $this->getKey('bundle'); + if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) { + if (empty($this->bundles)) { + throw new MigrateException('Stubbing failed, no bundles available for entity type: ' . $this->storage->getEntityTypeId()); + } + $row->setDestinationProperty($bundle_key, reset($this->bundles)); + } + + // Populate any required fields not already populated. + $fields = $this->entityManager + ->getFieldDefinitions($this->storage->getEntityTypeId(), $bundle_key); + foreach ($fields as $field_name => $field_definition) { + if ($field_definition->isRequired() && is_null($row->getDestinationProperty($field_name))) { + // Use the configured default value for this specific field, if any. + if ($default_value = $field_definition->getDefaultValueLiteral()) { + $values[] = $default_value; + } + else { + // Otherwise, ask the field type to generate a sample value. + $field_type = $field_definition->getType(); + /** @var \Drupal\Core\Field\FieldItemInterface $field_type_class */ + $field_type_class = $this->fieldTypeManager + ->getPluginClass($field_definition->getType()); + $values = $field_type_class::generateSampleValue($field_definition); + if (is_null($values)) { + // Handle failure to generate a sample value. + throw new MigrateException('Stubbing failed, unable to generate value for field ' . $field_name); + break; + } + } + + $row->setDestinationProperty($field_name, $values); + } + } + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php index 1e47d1ae4..7f9746406 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -287,6 +287,7 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP foreach ($this->migration->getSourcePlugin()->getIds() as $id_definition) { $mapkey = 'sourceid' . $count++; $source_id_schema[$mapkey] = $this->getFieldSchema($id_definition); + $source_id_schema[$mapkey]['not null'] = TRUE; // With InnoDB, utf8mb4-based primary keys can't be over 191 characters. // Use ASCII-based primary keys instead. diff --git a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php index a7e3e89b9..a222eee32 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php +++ b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php @@ -136,6 +136,7 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter $this->skipCount = !empty($configuration['skip_count']); $this->cacheKey = !empty($configuration['cache_key']) ? !empty($configuration['cache_key']) : NULL; $this->trackChanges = !empty($configuration['track_changes']) ? $configuration['track_changes'] : FALSE; + $this->idMap = $this->migration->getIdMap(); // Pull out the current highwater mark if we have a highwater property. if ($this->highWaterProperty = $this->migration->get('highWaterProperty')) { @@ -256,7 +257,6 @@ abstract class SourcePluginBase extends PluginBase implements MigrateSourceInter * source records. */ public function rewind() { - $this->idMap = $this->migration->getIdMap(); $this->getIterator()->rewind(); $this->next(); } diff --git a/core/modules/migrate/src/Row.php b/core/modules/migrate/src/Row.php index e055c1d4c..4e788afec 100644 --- a/core/modules/migrate/src/Row.php +++ b/core/modules/migrate/src/Row.php @@ -73,7 +73,7 @@ class Row { * * @see getRawDestination() */ - protected $rawDestination; + protected $rawDestination = []; /** * TRUE when this row is a stub. @@ -222,6 +222,17 @@ class Row { NestedArray::setValue($this->destination, explode(static::PROPERTY_SEPARATOR, $property), $value, TRUE); } + /** + * Removes destination property. + * + * @param string $property + * The name of the destination property. + */ + public function removeDestinationProperty($property) { + unset($this->rawDestination[$property]); + NestedArray::unsetValue($this->destination, explode(static::PROPERTY_SEPARATOR, $property)); + } + /** * Returns the whole destination array. * diff --git a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php index 051e9174e..502c9e7aa 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php @@ -9,8 +9,12 @@ namespace Drupal\Tests\migrate\Unit; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\migrate\MigrateExecutable; +use Drupal\migrate\MigrateSkipRowException; +use Drupal\migrate\Plugin\migrate\source\SourcePluginBase; use Drupal\migrate\Plugin\MigrateIdMapInterface; +use Drupal\migrate\Row; /** * @coversDefaultClass \Drupal\migrate\Plugin\migrate\source\SourcePluginBase @@ -128,6 +132,7 @@ class MigrateSourceTest extends MigrateTestCase { } /** + * @covers ::__construct * @expectedException \Drupal\migrate\MigrateException */ public function testHighwaterTrackChangesIncompatible() { @@ -138,6 +143,8 @@ class MigrateSourceTest extends MigrateTestCase { /** * Test that the source count is correct. + * + * @covers ::count */ public function testCount() { // Mock the cache to validate set() receives appropriate arguments. @@ -221,6 +228,144 @@ class MigrateSourceTest extends MigrateTestCase { $this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), 'Incoming row timestamp is greater than current highwater mark so we have a row.'); } + /** + * Test basic row preparation. + * + * @covers ::prepareRow + */ + public function testPrepareRow() { + $this->migrationConfiguration['id'] = 'test_migration'; + + // Get a new migration with an id. + $migration = $this->getMigration(); + $source = new StubSourcePlugin([], '', [], $migration); + $row = new Row([], []); + + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) + ->willReturn([TRUE, TRUE]) + ->shouldBeCalled(); + $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) + ->willReturn([TRUE, TRUE]) + ->shouldBeCalled(); + $source->setModuleHandler($module_handler->reveal()); + + // Ensure we don't log this to the mapping table. + $this->idMap->expects($this->never()) + ->method('saveIdMapping'); + + $this->assertTrue($source->prepareRow($row)); + + // Track_changes... + $source = new StubSourcePlugin(['track_changes' => TRUE], '', [], $migration); + $row2 = $this->prophesize(Row::class); + $row2->rehash() + ->shouldBeCalled(); + $module_handler->invokeAll('migrate_prepare_row', [$row2, $source, $migration]) + ->willReturn([TRUE, TRUE]) + ->shouldBeCalled(); + $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row2, $source, $migration]) + ->willReturn([TRUE, TRUE]) + ->shouldBeCalled(); + $source->setModuleHandler($module_handler->reveal()); + $this->assertTrue($source->prepareRow($row2->reveal())); + } + + /** + * Test that global prepare hooks can skip rows. + * + * @covers ::prepareRow + */ + public function testPrepareRowGlobalPrepareSkip() { + $this->migrationConfiguration['id'] = 'test_migration'; + + $migration = $this->getMigration(); + $source = new StubSourcePlugin([], '', [], $migration); + $row = new Row([], []); + + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + // Return a failure from a prepare row hook. + $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) + ->willReturn([TRUE, FALSE, TRUE]) + ->shouldBeCalled(); + $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) + ->willReturn([TRUE, TRUE]) + ->shouldBeCalled(); + $source->setModuleHandler($module_handler->reveal()); + + $this->idMap->expects($this->once()) + ->method('saveIdMapping') + ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED); + + $this->assertFalse($source->prepareRow($row)); + } + + /** + * Test that migrate specific prepare hooks can skip rows. + * + * @covers ::prepareRow + */ + public function testPrepareRowMigratePrepareSkip() { + $this->migrationConfiguration['id'] = 'test_migration'; + + $migration = $this->getMigration(); + $source = new StubSourcePlugin([], '', [], $migration); + $row = new Row([], []); + + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + // Return a failure from a prepare row hook. + $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) + ->willReturn([TRUE, TRUE]) + ->shouldBeCalled(); + $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) + ->willReturn([TRUE, FALSE, TRUE]) + ->shouldBeCalled(); + $source->setModuleHandler($module_handler->reveal()); + + $this->idMap->expects($this->once()) + ->method('saveIdMapping') + ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED); + + $this->assertFalse($source->prepareRow($row)); + } + + /** + * Test that a skip exception during prepare hooks correctly skips. + * + * @covers ::prepareRow + */ + public function testPrepareRowPrepareException() { + $this->migrationConfiguration['id'] = 'test_migration'; + + $migration = $this->getMigration(); + $source = new StubSourcePlugin([], '', [], $migration); + $row = new Row([], []); + + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + // Return a failure from a prepare row hook. + $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) + ->willReturn([TRUE, TRUE]) + ->shouldBeCalled(); + $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) + ->willThrow(new MigrateSkipRowException()) + ->shouldBeCalled(); + $source->setModuleHandler($module_handler->reveal()); + + // This will only be called on the first prepare because the second + // explicitly avoids it. + $this->idMap->expects($this->once()) + ->method('saveIdMapping') + ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED); + $this->assertFalse($source->prepareRow($row)); + + // Throw an exception the second time that avoids mapping. + $e = new MigrateSkipRowException('', FALSE); + $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) + ->willThrow($e) + ->shouldBeCalled(); + $this->assertFalse($source->prepareRow($row)); + } + /** * Get a mock executable for the test. * @@ -239,3 +384,46 @@ class MigrateSourceTest extends MigrateTestCase { } } + +/** + * Stubbed source plugin for testing base class implementations. + */ +class StubSourcePlugin extends SourcePluginBase { + + /** + * Helper for setting internal module handler implementation. + * + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + */ + function setModuleHandler(ModuleHandlerInterface $module_handler) { + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function fields() { + return []; + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + return []; + } + + /** + * {@inheritdoc} + */ + protected function initializeIterator() { + return []; + } +} diff --git a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php index 768e0fcc7..e3a253a59 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php +++ b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php @@ -19,6 +19,11 @@ abstract class MigrateTestCase extends UnitTestCase { protected $migrationConfiguration = []; + /** + * @var \Drupal\migrate\Plugin\MigrateIdMapInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $idMap; + /** * Local store for mocking setStatus()/getStatus(). * diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php new file mode 100644 index 000000000..220cbe84e --- /dev/null +++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php @@ -0,0 +1,118 @@ +migration = $this->prophesize(MigrationInterface::class); + $this->storage = $this->prophesize(EntityStorageInterface::class); + $this->entityManager = $this->prophesize(EntityManagerInterface::class); + } + + /** + * Test basic entity save. + * + * @covers ::import + */ + public function testImport() { + $bundles = []; + $destination = new EntityTestDestination([], '', [], + $this->migration->reveal(), + $this->storage->reveal(), + $bundles, + $this->entityManager->reveal(), + $this->prophesize(FieldTypePluginManagerInterface::class)->reveal()); + $entity = $this->prophesize(ContentEntityInterface::class); + // Assert that save is called. + $entity->save() + ->shouldBeCalledTimes(1); + // Set an id for the entity + $entity->id() + ->willReturn(5); + $destination->setEntity($entity->reveal()); + // Ensure the id is saved entity id is returned from import. + $this->assertEquals([5], $destination->import(new Row([], []))); + // Assert that import set the rollback action. + $this->assertEquals(MigrateIdMapInterface::ROLLBACK_DELETE, $destination->rollbackAction()); + } + + /** + * Test row skipping when we can't get an entity to save. + * + * @covers ::import + * @expectedException \Drupal\migrate\MigrateException + * @expectedExceptionMessage Unable to get entity + */ + public function testImportEntityLoadFailure() { + $bundles = []; + $destination = new EntityTestDestination([], '', [], + $this->migration->reveal(), + $this->storage->reveal(), + $bundles, + $this->entityManager->reveal(), + $this->prophesize(FieldTypePluginManagerInterface::class)->reveal()); + $destination->setEntity(FALSE); + $destination->import(new Row([], [])); + } + +} + +/** + * Stub class for testing EntityContentBase methods. + * + * We want to test things without testing the base class implementations. + */ +class EntityTestDestination extends EntityContentBase { + + private $entity = NULL; + + public function setEntity($entity) { + $this->entity = $entity; + } + + protected function getEntity(Row $row, array $old_destination_id_values) { + return $this->entity; + } +} diff --git a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php index d5f71a072..2db5d9e8e 100644 --- a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php +++ b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php @@ -36,6 +36,11 @@ class EntityRevisionTest extends UnitTestCase { */ protected $entityManager; + /** + * @var \Drupal\Core\Field\FieldTypePluginManagerInterface + */ + protected $fieldTypeManager; + public function setUp() { parent::setUp(); @@ -43,6 +48,7 @@ class EntityRevisionTest extends UnitTestCase { $this->migration = $this->prophesize('\Drupal\migrate\Entity\MigrationInterface'); $this->storage = $this->prophesize('\Drupal\Core\Entity\EntityStorageInterface'); $this->entityManager = $this->prophesize('\Drupal\Core\Entity\EntityManagerInterface'); + $this->fieldTypeManager = $this->prophesize('\Drupal\Core\Field\FieldTypePluginManagerInterface'); } /** @@ -183,7 +189,9 @@ class EntityRevisionTest extends UnitTestCase { $this->migration->reveal(), $this->storage->reveal(), [], - $this->entityManager->reveal()); + $this->entityManager->reveal(), + $this->fieldTypeManager->reveal() + ); } } diff --git a/core/modules/migrate_drupal/src/Tests/StubTestTrait.php b/core/modules/migrate_drupal/src/Tests/StubTestTrait.php new file mode 100644 index 000000000..1e028bfff --- /dev/null +++ b/core/modules/migrate_drupal/src/Tests/StubTestTrait.php @@ -0,0 +1,80 @@ +createStub($entity_type_id); + $this->assertTrue($entity_id, 'Stub successfully created'); + if ($entity_id) { + $violations = $this->validateStub($entity_type_id, $entity_id); + if (!$this->assertIdentical(count($violations), 0, 'Stub is a valid entity')) { + foreach ($violations as $violation) { + $this->fail((string) $violation->getMessage()); + } + } + } + } + + /** + * Create a stub of the given entity type. + * + * @param string $entity_type_id + * The entity type we are stubbing. + * + * @return int + * ID of the created entity. + */ + protected function createStub($entity_type_id) { + // Create a dummy migration to pass to the destination plugin. + $config = [ + 'id' => 'dummy', + 'migration_tags' => ['Stub test'], + 'source' => ['plugin' => 'empty'], + 'process' => [], + 'destination' => ['plugin' => 'entity:' . $entity_type_id], + ]; + $migration = Migration::create($config); + $destination_plugin = $migration->getDestinationPlugin(TRUE); + $stub_row = new Row([], [], TRUE); + $destination_ids = $destination_plugin->import($stub_row); + return reset($destination_ids); + } + + /** + * Perform validation on a stub entity. + * + * @param string $entity_type_id + * The entity type we are stubbing. + * @param string $entity_id + * ID of the stubbed entity to validate. + * + * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface + * List of constraint violations identified. + */ + protected function validateStub($entity_type_id, $entity_id) { + $controller = \Drupal::entityManager()->getStorage($entity_type_id); + /** @var \Drupal\Core\Entity\ContentEntityInterface $stub_entity */ + $stub_entity = $controller->load($entity_id); + return $stub_entity->validate(); + } + +} diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal7.php b/core/modules/migrate_drupal/tests/fixtures/drupal7.php index c1480c186..78651e4be 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal7.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal7.php @@ -41539,10 +41539,30 @@ $connection->insert('variable') 'name' => 'tracker_batch_size', 'value' => 'i:999;', )) +->values(array( + 'name' => 'update_check_frequency', + 'value' => 'i:1;', +)) +->values(array( + 'name' => 'update_fetch_url', + 'value' => 's:23:"http://127.0.0.1/update";', +)) ->values(array( 'name' => 'update_last_check', 'value' => 'i:1444944973;', )) +->values(array( + 'name' => 'update_max_fetch_attempts', + 'value' => 'i:3;', +)) +->values(array( + 'name' => 'update_notification_threshold', + 'value' => 's:3:"all";', +)) +->values(array( + 'name' => 'update_notify_emails', + 'value' => 'a:1:{i:0;s:19:"webmaster@127.0.0.1";}', +)) ->values(array( 'name' => 'user_admin_role', 'value' => 's:1:"3";', diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 896c4f888..a54fc0a2c 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1064,6 +1064,14 @@ function node_query_node_access_alter(AlterableInterface $query) { // Update the query for the given storage method. \Drupal::service('node.grant_storage')->alterQuery($query, $tables, $op, $account, $base_table); + + // Bubble the 'user.node_grants:$op' cache context to the current render + // context. + $renderer = \Drupal::service('renderer'); + if ($renderer->hasRenderContext()) { + $build = ['#cache' => ['contexts' => ['user.node_grants:' . $op]]]; + $renderer->render($build); + } } /** diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml index 187e8521a..7c942a2c1 100644 --- a/core/modules/node/node.routing.yml +++ b/core/modules/node/node.routing.yml @@ -47,6 +47,7 @@ entity.node.version_history: _controller: '\Drupal\node\Controller\NodeController::revisionOverview' requirements: _access_node_revision: 'view' + node: \d+ options: _node_operation_route: TRUE @@ -57,6 +58,7 @@ entity.node.revision: _title_callback: '\Drupal\node\Controller\NodeController::revisionPageTitle' requirements: _access_node_revision: 'view' + node: \d+ node.revision_revert_confirm: path: '/node/{node}/revisions/{node_revision}/revert' @@ -65,6 +67,7 @@ node.revision_revert_confirm: _title: 'Revert to earlier revision' requirements: _access_node_revision: 'update' + node: \d+ options: _node_operation_route: TRUE @@ -75,6 +78,7 @@ node.revision_revert_translation_confirm: _title: 'Revert to earlier revision of a translation' requirements: _access_node_revision: 'update' + node: \d+ options: _node_operation_route: TRUE @@ -85,6 +89,7 @@ node.revision_delete_confirm: _title: 'Delete earlier revision' requirements: _access_node_revision: 'delete' + node: \d+ options: _node_operation_route: TRUE diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php index a43bb53a8..3c534f2ea 100644 --- a/core/modules/node/src/Controller/NodeController.php +++ b/core/modules/node/src/Controller/NodeController.php @@ -187,7 +187,7 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) { $username = [ '#theme' => 'username', - '#account' => $revision->uid->entity, + '#account' => $revision->getRevisionAuthor(), ]; // Use revision link to link to revisions that are not active. diff --git a/core/modules/node/src/Entity/NodeRouteProvider.php b/core/modules/node/src/Entity/NodeRouteProvider.php index fa1c02d7f..7bfc9820d 100644 --- a/core/modules/node/src/Entity/NodeRouteProvider.php +++ b/core/modules/node/src/Entity/NodeRouteProvider.php @@ -27,6 +27,7 @@ class NodeRouteProvider implements EntityRouteProviderInterface { '_controller' => '\Drupal\node\Controller\NodeViewController::view', '_title_callback' => '\Drupal\node\Controller\NodeViewController::title', ]) + ->setRequirement('node', '\d+') ->setRequirement('_entity_access', 'node.view'); $route_collection->add('entity.node.canonical', $route); @@ -35,6 +36,7 @@ class NodeRouteProvider implements EntityRouteProviderInterface { '_entity_form' => 'node.delete', '_title' => 'Delete', ]) + ->setRequirement('node', '\d+') ->setRequirement('_entity_access', 'node.delete') ->setOption('_node_operation_route', TRUE); $route_collection->add('entity.node.delete_form', $route); @@ -42,6 +44,7 @@ class NodeRouteProvider implements EntityRouteProviderInterface { $route = (new Route('/node/{node}/edit')) ->setDefault('_entity_form', 'node.edit') ->setRequirement('_entity_access', 'node.update') + ->setRequirement('node', '\d+') ->setOption('_node_operation_route', TRUE); $route_collection->add('entity.node.edit_form', $route); diff --git a/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php b/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php index 276c795b6..a42fa60ec 100644 --- a/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php +++ b/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php @@ -48,4 +48,32 @@ class NodeSelection extends DefaultSelection { return $query; } + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $node = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable node, it needs to published. + /** @var \Drupal\node\NodeInterface $node */ + $node->setPublished(TRUE); + + return $node; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + // Mirror the conditions checked in buildEntityQuery(). + if (!$this->currentUser->hasPermission('bypass node access') && !count($this->moduleHandler->getImplementations('node_grants'))) { + $entities = array_filter($entities, function ($node) { + /** @var \Drupal\node\NodeInterface $node */ + return $node->isPublished(); + }); + } + return $entities; + } + } diff --git a/core/modules/node/src/Plugin/views/wizard/Node.php b/core/modules/node/src/Plugin/views/wizard/Node.php index f7a43ecf8..5f560c568 100644 --- a/core/modules/node/src/Plugin/views/wizard/Node.php +++ b/core/modules/node/src/Plugin/views/wizard/Node.php @@ -199,7 +199,9 @@ class Node extends WizardPluginBase { protected function buildFilters(&$form, FormStateInterface $form_state) { parent::buildFilters($form, $form_state); - $selected_bundle = static::getSelected($form_state, array('show', 'type'), 'all', $form['displays']['show']['type']); + if (isset($form['displays']['show']['type'])) { + $selected_bundle = static::getSelected($form_state, array('show', 'type'), 'all', $form['displays']['show']['type']); + } // Add the "tagged with" filter to the view. diff --git a/core/modules/node/src/Tests/Migrate/MigrateNodeStubTest.php b/core/modules/node/src/Tests/Migrate/MigrateNodeStubTest.php new file mode 100644 index 000000000..22d140bf3 --- /dev/null +++ b/core/modules/node/src/Tests/Migrate/MigrateNodeStubTest.php @@ -0,0 +1,48 @@ +installEntitySchema('node'); + // Need at least one node type present. + NodeType::create([ + 'type' => 'testnodetype', + 'name' => 'Test node type', + ])->save(); + } + + /** + * Tests creation of node stubs. + */ + public function testStub() { + $this->performStubTest('node'); + } + +} diff --git a/core/modules/node/src/Tests/NodeAccessAutoBubblingTest.php b/core/modules/node/src/Tests/NodeAccessAutoBubblingTest.php new file mode 100644 index 000000000..908567239 --- /dev/null +++ b/core/modules/node/src/Tests/NodeAccessAutoBubblingTest.php @@ -0,0 +1,72 @@ +drupalCreateNode(); + $this->drupalCreateNode(); + $this->drupalCreateNode(); + $this->drupalCreateNode(); + } + + /** + * Tests that the node grants cache context is auto-added, only when needed. + * + * @see node_query_node_access_alter() + */ + public function testNodeAccessCacheabilitySafeguard() { + $this->dumpHeaders = TRUE; + + // The node grants cache context should be added automatically. + $this->drupalGet(new Url('node_access_test_auto_bubbling')); + $this->assertCacheContext('user.node_grants:view'); + + // The root user has the 'bypass node access' permission, which means the + // node grants cache context is not necessary. + $this->drupalLogin($this->rootUser); + $this->drupalGet(new Url('node_access_test_auto_bubbling')); + $this->assertNoCacheContext('user.node_grants:view'); + $this->drupalLogout(); + + // Uninstall the module with the only hook_node_grants() implementation. + $this->container->get('module_installer')->uninstall(['node_access_test']); + $this->rebuildContainer(); + + // Because there are no node grants defined, there also is no need for the + // node grants cache context to be bubbled. + $this->drupalGet(new Url('node_access_test_auto_bubbling')); + $this->assertNoCacheContext('user.node_grants:view'); + } + +} diff --git a/core/modules/node/src/Tests/NodeRevisionsTest.php b/core/modules/node/src/Tests/NodeRevisionsTest.php index 2e3114576..45fa06d58 100644 --- a/core/modules/node/src/Tests/NodeRevisionsTest.php +++ b/core/modules/node/src/Tests/NodeRevisionsTest.php @@ -21,7 +21,19 @@ use Drupal\node\NodeInterface; * @group node */ class NodeRevisionsTest extends NodeTestBase { + + /** + * An array of node revisions. + * + * @var \Drupal\node\NodeInterface[] + */ protected $nodes; + + /** + * Revision log messages. + * + * @var array + */ protected $revisionLogs; /** @@ -93,6 +105,16 @@ class NodeRevisionsTest extends NodeTestBase { ); $node->untranslatable_string_field->value = $this->randomString(); $node->setNewRevision(); + + // Edit the 2nd revision with a different user. + if ($i == 1) { + $editor = $this->drupalCreateUser(); + $node->setRevisionAuthorId($editor->id()); + } + else { + $node->setRevisionAuthorId($web_user->id()); + } + $node->save(); $node = Node::load($node->id()); // Make sure we get revision information. @@ -123,6 +145,11 @@ class NodeRevisionsTest extends NodeTestBase { foreach ($logs as $revision_log) { $this->assertText($revision_log, 'Revision log message found.'); } + // Original author, and editor names should appear on revisions overview. + $web_user = $nodes[0]->revision_uid->entity; + $this->assertText(t('by @name', ['@name' => $web_user->getAccountName()])); + $editor = $nodes[2]->revision_uid->entity; + $this->assertText(t('by @name', ['@name' => $editor->getAccountName()])); // Confirm that this is the default revision. $this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.'); diff --git a/core/modules/node/src/Tests/NodeTranslationUITest.php b/core/modules/node/src/Tests/NodeTranslationUITest.php index b42eb76b4..0bb81c071 100644 --- a/core/modules/node/src/Tests/NodeTranslationUITest.php +++ b/core/modules/node/src/Tests/NodeTranslationUITest.php @@ -94,7 +94,8 @@ class NodeTranslationUITest extends ContentTranslationUITestBase { $language = ConfigurableLanguage::load($langcode); $values[$langcode] = array('title' => array(array('value' => $this->randomMachineName()))); - $add_url = Url::fromRoute('content_translation.translation_add_' . $entity->getEntityTypeId(), [ + $entity_type_id = $entity->getEntityTypeId(); + $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [ $entity->getEntityTypeId() => $entity->id(), 'source' => $default_langcode, 'target' => $langcode diff --git a/core/modules/node/src/Tests/NodeTypeTest.php b/core/modules/node/src/Tests/NodeTypeTest.php index a8fa54d98..07117f709 100644 --- a/core/modules/node/src/Tests/NodeTypeTest.php +++ b/core/modules/node/src/Tests/NodeTypeTest.php @@ -218,6 +218,9 @@ class NodeTypeTest extends NodeTestBase { * Tests for when there are no content types defined. */ public function testNodeTypeNoContentType() { + /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */ + $bundle_info = \Drupal::service('entity_type.bundle.info'); + $this->assertEqual(2, count($bundle_info->getBundleInfo('node')), 'The bundle information service has 2 bundles for the Node entity type.'); $web_user = $this->drupalCreateUser(['administer content types']); $this->drupalLogin($web_user); @@ -231,6 +234,9 @@ class NodeTypeTest extends NodeTestBase { $this->assertRaw(t('No content types available. Add content type.', [ ':link' => Url::fromRoute('node.type_add')->toString() ]), 'Empty text when there are no content types in the system is correct.'); + + $bundle_info->clearCachedBundles(); + $this->assertEqual(0, count($bundle_info->getBundleInfo('node')), 'The bundle information service has 0 bundles for the Node entity type.'); } } diff --git a/core/modules/node/tests/node_access_test_auto_bubbling/node_access_test_auto_bubbling.info.yml b/core/modules/node/tests/node_access_test_auto_bubbling/node_access_test_auto_bubbling.info.yml new file mode 100644 index 000000000..49a990d75 --- /dev/null +++ b/core/modules/node/tests/node_access_test_auto_bubbling/node_access_test_auto_bubbling.info.yml @@ -0,0 +1,6 @@ +name: 'Node module access automatic cacheability bubbling tests' +type: module +description: 'Support module for node permission testing. Provides a route which does a node access query without explicitly specifying the corresponding cache context.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/node/tests/node_access_test_auto_bubbling/node_access_test_auto_bubbling.routing.yml b/core/modules/node/tests/node_access_test_auto_bubbling/node_access_test_auto_bubbling.routing.yml new file mode 100644 index 000000000..34fd420b3 --- /dev/null +++ b/core/modules/node/tests/node_access_test_auto_bubbling/node_access_test_auto_bubbling.routing.yml @@ -0,0 +1,6 @@ +node_access_test_auto_bubbling: + path: '/node_access_test_auto_bubbling' + defaults: + _controller: '\Drupal\node_access_test_auto_bubbling\Controller\NodeAccessTestAutoBubblingController::latest' + requirements: + _access: 'TRUE' diff --git a/core/modules/node/tests/node_access_test_auto_bubbling/src/Controller/NodeAccessTestAutoBubblingController.php b/core/modules/node/tests/node_access_test_auto_bubbling/src/Controller/NodeAccessTestAutoBubblingController.php new file mode 100644 index 000000000..1fcaa4b03 --- /dev/null +++ b/core/modules/node/tests/node_access_test_auto_bubbling/src/Controller/NodeAccessTestAutoBubblingController.php @@ -0,0 +1,61 @@ +entityQuery = $entity_query; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.query') + ); + } + + /** + * Lists the three latest published node IDs. + * + * @return array + * A render array. + */ + public function latest() { + $nids = $this->entityQuery->get('node') + ->condition('status', NODE_PUBLISHED) + ->sort('created', 'DESC') + ->range(0, 3) + ->execute(); + return ['#markup' => $this->t('The three latest nodes are: @nids.', ['@nids' => implode(', ', $nids)])]; + } + +} diff --git a/core/modules/path/src/Form/PathFormBase.php b/core/modules/path/src/Form/PathFormBase.php index 8c8484167..95d261ac7 100644 --- a/core/modules/path/src/Form/PathFormBase.php +++ b/core/modules/path/src/Form/PathFormBase.php @@ -180,8 +180,22 @@ abstract class PathFormBase extends FormBase { $langcode = $form_state->getValue('langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED); if ($this->aliasStorage->aliasExists($alias, $langcode, $this->path['source'])) { - $form_state->setErrorByName('alias', t('The alias %alias is already in use in this language.', array('%alias' => $alias))); + $stored_alias = $this->aliasStorage->load(['alias' => $alias, 'langcode' => $langcode]); + if ($stored_alias['alias'] !== $alias) { + // The alias already exists with different capitalization as the default + // implementation of AliasStorageInterface::aliasExists is + // case-insensitive. + $form_state->setErrorByName('alias', t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [ + '%alias' => $alias, + '%stored_alias' => $stored_alias['alias'], + ])); + } + else { + $form_state->setErrorByName('alias', t('The alias %alias is already in use in this language.', ['%alias' => $alias])); + } } + + if (!$this->pathValidator->isValid(trim($source, '/'))) { $form_state->setErrorByName('source', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $source))); } diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php index 651c11f9e..7e85cda03 100644 --- a/core/modules/path/src/Tests/PathAliasTest.php +++ b/core/modules/path/src/Tests/PathAliasTest.php @@ -7,7 +7,9 @@ namespace Drupal\path\Tests; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; +use Drupal\Core\Database\Database; /** * Add, edit, delete, and change alias and verify its consistency in the @@ -75,25 +77,45 @@ class PathAliasTest extends PathTestBase { // Create alias. $edit = array(); $edit['source'] = '/node/' . $node1->id(); - $edit['alias'] = '/' . $this->randomMachineName(8); + $edit['alias'] = '/' . $this->getRandomGenerator()->word(8); $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Confirm that the alias works. $this->drupalGet($edit['alias']); $this->assertText($node1->label(), 'Alias works.'); $this->assertResponse(200); + // Confirm that the alias works in a case-insensitive way. + $this->assertTrue(ctype_lower(ltrim($edit['alias'], '/'))); + $this->drupalGet($edit['alias']); + $this->assertText($node1->label(), 'Alias works lower case.'); + $this->assertResponse(200); + $this->drupalGet(Unicode::strtoupper($edit['alias'])); + $this->assertText($node1->label(), 'Alias works upper case.'); + $this->assertResponse(200); // Change alias to one containing "exotic" characters. $pid = $this->getPID($edit['alias']); $previous = $edit['alias']; - $edit['alias'] = "/- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. - "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. - "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. + $edit['alias'] = '/alias' . // Lower-case letters. + // "Special" ASCII characters. + "- ._~!$'\"()*@[]?&+%#,;=:" . + // Characters that look like a percent-escaped string. + "%23%25%26%2B%2F%3F" . + // Characters from various non-ASCII alphabets. + "中國書۞"; + $connection = Database::getConnection(); + if ($connection->databaseType() != 'sqlite') { + // When using LIKE for case-insensitivity, the SQLite driver is + // currently unable to find the upper-case versions of non-ASCII + // characters. + // @todo fix this in https://www.drupal.org/node/2607432 + $edit['alias'] .= "ïвβéø"; + } $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet($edit['alias']); + $this->drupalGet(Unicode::strtoupper($edit['alias'])); $this->assertText($node1->label(), 'Changed alias works.'); $this->assertResponse(200); @@ -114,6 +136,14 @@ class PathAliasTest extends PathTestBase { // Confirm no duplicate was created. $this->assertRaw(t('The alias %alias is already in use in this language.', array('%alias' => $edit['alias'])), 'Attempt to move alias was rejected.'); + $edit_upper = $edit; + $edit_upper['alias'] = Unicode::strtoupper($edit['alias']); + $this->drupalPostForm('admin/config/search/path/add', $edit_upper, t('Save')); + $this->assertRaw(t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [ + '%alias' => $edit_upper['alias'], + '%stored_alias' => $edit['alias'], + ]), 'Attempt to move upper-case alias was rejected.'); + // Delete alias. $this->drupalPostForm('admin/config/search/path/edit/' . $pid, array(), t('Delete')); $this->drupalPostForm(NULL, array(), t('Confirm')); @@ -217,15 +247,27 @@ class PathAliasTest extends PathTestBase { $elements = $this->xpath("//link[contains(@rel, 'shortlink') and contains(@href, 'node/" . $node1->id() . "')]"); $this->assertTrue(!empty($elements), 'Page contains shortlink URL.'); - // Change alias to one containing "exotic" characters. $previous = $edit['path[0][alias]']; - $edit['path[0][alias]'] = "/- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. - "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. - "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. + // Change alias to one containing "exotic" characters. + $edit['path[0][alias]'] = '/alias' . // Lower-case letters. + // "Special" ASCII characters. + "- ._~!$'\"()*@[]?&+%#,;=:" . + // Characters that look like a percent-escaped string. + "%23%25%26%2B%2F%3F" . + // Characters from various non-ASCII alphabets. + "中國書۞"; + $connection = Database::getConnection(); + if ($connection->databaseType() != 'sqlite') { + // When using LIKE for case-insensitivity, the SQLite driver is + // currently unable to find the upper-case versions of non-ASCII + // characters. + // @todo fix this in https://www.drupal.org/node/2607432 + $edit['path[0][alias]'] .= "ïвβéø"; + } $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet($edit['path[0][alias]']); + $this->drupalGet(Unicode::strtoupper($edit['path[0][alias]'])); $this->assertText($node1->label(), 'Changed alias works.'); $this->assertResponse(200); diff --git a/core/modules/rest/src/Plugin/views/display/RestExport.php b/core/modules/rest/src/Plugin/views/display/RestExport.php index 427becfac..68bd407cd 100644 --- a/core/modules/rest/src/Plugin/views/display/RestExport.php +++ b/core/modules/rest/src/Plugin/views/display/RestExport.php @@ -151,7 +151,7 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac * {@inheritdoc} */ public function usesExposed() { - return FALSE; + return TRUE; } /** diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php index 5f378ee2f..e3b8d263c 100644 --- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php +++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php @@ -50,7 +50,7 @@ class StyleSerializerTest extends PluginTestBase { * * @var array */ - public static $testViews = array('test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_node_display_field'); + public static $testViews = array('test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_node_display_field', 'test_serializer_node_exposed_filter'); /** * A user with administrative privileges to look at test entity and configure views. @@ -609,4 +609,64 @@ class StyleSerializerTest extends PluginTestBase { } $this->assertEqual($serializer->serialize($expected, 'json'), (string) $renderer->renderRoot($build)); } + + /** + * Tests the exposed filter works. + * + * There is an exposed filter on the title field which takes a title query + * parameter. This is set to filter nodes by those whose title starts with + * the value provided. + */ + public function testRestViewExposedFilter() { + $this->drupalCreateContentType(array('type' => 'page')); + $node0 = $this->drupalCreateNode(array('title' => 'Node 1')); + $node1 = $this->drupalCreateNode(array('title' => 'Node 11')); + $node2 = $this->drupalCreateNode(array('title' => 'Node 111')); + + // Test that no filter brings back all three nodes. + $result = $this->drupalGetJSON('test/serialize/node-exposed-filter'); + + $expected = array( + 0 => array( + 'nid' => $node0->id(), + 'body' => $node0->body->processed, + ), + 1 => array( + 'nid' => $node1->id(), + 'body' => $node1->body->processed, + ), + 2 => array( + 'nid' => $node2->id(), + 'body' => $node2->body->processed, + ), + ); + + $this->assertEqual($result, $expected, 'Querying a view with no exposed filter returns all nodes.'); + + // Test that title starts with 'Node 11' query finds 2 of the 3 nodes. + $result = $this->drupalGetJSON('test/serialize/node-exposed-filter', ['query' => ['title' => 'Node 11']]); + + $expected = array( + 0 => array( + 'nid' => $node1->id(), + 'body' => $node1->body->processed, + ), + 1 => array( + 'nid' => $node2->id(), + 'body' => $node2->body->processed, + ), + ); + + $cache_contexts = [ + 'languages:language_content', + 'languages:language_interface', + 'theme', + 'request_format', + 'user.node_grants:view', + 'url', + ]; + + $this->assertEqual($result, $expected, 'Querying a view with a starts with exposed filter on the title returns nodes whose title starts with value provided.'); + $this->assertCacheContexts($cache_contexts); + } } diff --git a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_exposed_filter.yml b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_exposed_filter.yml new file mode 100644 index 000000000..b5af1e96b --- /dev/null +++ b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_exposed_filter.yml @@ -0,0 +1,172 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.body + module: + - field + - node + - rest + - rest_test_views + - user +id: test_serializer_node_exposed_filter +label: 'Test serializer display for exposed filters' +module: rest_test_views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: null + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + query: + type: views_query + exposed_form: + type: basic + style: + type: serializer + row: + type: data_field + fields: + nid: + id: nid + table: node + field: nid + plugin_id: field + entity_type: node + entity_field: nid + body: + id: body + table: node__body + field: body + relationship: none + group_type: group + admin_label: '' + label: Body + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: text_default + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + plugin_id: field + entity_type: node + entity_field: body + filters: + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + operator: starts + value: '' + group: 1 + exposed: true + expose: + operator_id: title_op + label: Title + description: '' + use_operator: false + operator: title_op + identifier: title + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: title + plugin_id: string + title: 'Test serialize' + arguments: { } + rest_export_1: + display_plugin: rest_export + id: rest_export_1 + display_title: serializer + position: null + display_options: + defaults: + access: false + style: false + row: false + path: test/serialize/node-exposed-filter + access: + type: none + style: + type: serializer + row: + type: data_field diff --git a/core/modules/search/migration_templates/d7_search_settings.yml b/core/modules/search/migration_templates/d7_search_settings.yml index ec272141a..57db8b945 100644 --- a/core/modules/search/migration_templates/d7_search_settings.yml +++ b/core/modules/search/migration_templates/d7_search_settings.yml @@ -12,6 +12,7 @@ source: - search_cron_limit - search_tag_weights - search_and_or_limit + - search_default_module process: 'index/minimum_word_size': minimum_word_size 'index/overlap_cjk': overlap_cjk @@ -19,6 +20,13 @@ process: 'index/tag_weights': search_tag_weights and_or_limit: search_and_or_limit logging: 'constants/status' + default_page: + plugin: static_map + source: + - search_default_module + map: + node: node_search + user: user_search destination: plugin: config config_name: search.settings diff --git a/core/modules/search/src/Tests/Migrate/d7/MigrateSearchSettingsTest.php b/core/modules/search/src/Tests/Migrate/d7/MigrateSearchSettingsTest.php index 5adff4108..ac0e4c0ea 100644 --- a/core/modules/search/src/Tests/Migrate/d7/MigrateSearchSettingsTest.php +++ b/core/modules/search/src/Tests/Migrate/d7/MigrateSearchSettingsTest.php @@ -31,6 +31,7 @@ class MigrateSearchSettingsTest extends MigrateDrupal7TestBase { */ public function testSearchSettings() { $config = $this->config('search.settings'); + $this->assertIdentical('node_search', $config->get('default_page')); $this->assertIdentical(4, $config->get('index.minimum_word_size')); $this->assertTrue($config->get('index.overlap_cjk')); $this->assertIdentical(100, $config->get('index.cron_limit')); diff --git a/core/modules/serialization/src/Tests/NormalizerTestBase.php b/core/modules/serialization/src/Tests/NormalizerTestBase.php index c38c55433..50e3c870c 100644 --- a/core/modules/serialization/src/Tests/NormalizerTestBase.php +++ b/core/modules/serialization/src/Tests/NormalizerTestBase.php @@ -7,7 +7,9 @@ namespace Drupal\serialization\Tests; -use Drupal\simpletest\KernelTestBase; +use Drupal\KernelTests\KernelTestBase; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; abstract class NormalizerTestBase extends KernelTestBase { @@ -29,14 +31,14 @@ abstract class NormalizerTestBase extends KernelTestBase { \Drupal::moduleHandler()->invoke('rest', 'install'); // Auto-create a field for testing. - entity_create('field_storage_config', array( + FieldstorageConfig::create(array( 'entity_type' => 'entity_test_mulrev', 'field_name' => 'field_test_text', 'type' => 'text', 'cardinality' => 1, 'translatable' => FALSE, ))->save(); - entity_create('field_config', array( + FieldConfig::create(array( 'entity_type' => 'entity_test_mulrev', 'field_name' => 'field_test_text', 'bundle' => 'entity_test_mulrev', diff --git a/core/modules/shortcut/shortcut.routing.yml b/core/modules/shortcut/shortcut.routing.yml index d6c0e1ad2..113335756 100644 --- a/core/modules/shortcut/shortcut.routing.yml +++ b/core/modules/shortcut/shortcut.routing.yml @@ -61,6 +61,7 @@ entity.shortcut.canonical: _title: 'Edit' requirements: _entity_access: 'shortcut.update' + shortcut: \d+ entity.shortcut.edit_form: path: '/admin/config/user-interface/shortcut/link/{shortcut}' @@ -69,6 +70,7 @@ entity.shortcut.edit_form: _title: 'Edit' requirements: _entity_access: 'shortcut.update' + shortcut: \d+ entity.shortcut.link_delete_inline: path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete-inline' @@ -77,6 +79,7 @@ entity.shortcut.link_delete_inline: requirements: _entity_access: 'shortcut.delete' _csrf_token: 'TRUE' + shortcut: \d+ entity.shortcut.delete_form: path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete' @@ -85,6 +88,7 @@ entity.shortcut.delete_form: _title: 'Delete' requirements: _entity_access: 'shortcut.delete' + shortcut: \d+ shortcut.set_switch: path: '/user/{user}/shortcuts' @@ -95,3 +99,4 @@ shortcut.set_switch: _custom_access: 'Drupal\shortcut\Form\SwitchShortcutSet::checkAccess' options: _admin_route: TRUE + user: \d+ diff --git a/core/modules/shortcut/src/Tests/Migrate/MigrateShortcutStubTest.php b/core/modules/shortcut/src/Tests/Migrate/MigrateShortcutStubTest.php new file mode 100644 index 000000000..ba9a0002e --- /dev/null +++ b/core/modules/shortcut/src/Tests/Migrate/MigrateShortcutStubTest.php @@ -0,0 +1,44 @@ +installEntitySchema('shortcut'); + // Make sure the 'default' shortcut_set is installed. + $this->installConfig(['shortcut']); + } + + /** + * Tests creation of shortcut stubs. + */ + public function testStub() { + $this->performStubTest('shortcut'); + } + +} diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index b99435222..74d348e92 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -170,17 +170,20 @@ function simpletest_run_tests($test_list) { * @param $unescaped_test_classnames * An array of test class names, including full namespaces, to be passed as * a regular expression to PHPUnit's --filter option. + * @param int $status + * (optional) The exit status code of the PHPUnit process will be assigned to + * this variable. * * @return array * The parsed results of PHPUnit's JUnit XML output, in the format of * {simpletest}'s schema. */ -function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames) { +function simpletest_run_phpunit_tests($test_id, array $unescaped_test_classnames, &$status = NULL) { $phpunit_file = simpletest_phpunit_xml_filepath($test_id); - $ret = simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file); - // A return value of 0 = passed test, 1 = failed test, > 1 indicates segfault + simpletest_phpunit_run_command($unescaped_test_classnames, $phpunit_file, $status); + // A $status of 0 = passed test, 1 = failed test, > 1 indicates segfault // timeout, or other type of failure. - if ($ret > 1) { + if ($status > 1) { // Something broke during the execution of phpunit. // Return an error record of all failed classes. $rows[] = [ @@ -251,11 +254,14 @@ function simpletest_phpunit_configuration_filepath() { * a regular expression to PHPUnit's --filter option. * @param string $phpunit_file * A filepath to use for PHPUnit's --log-junit option. + * @param int $status + * (optional) The exit status code of the PHPUnit process will be assigned to + * this variable. * * @return string * The results as returned by exec(). */ -function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file) { +function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpunit_file, &$status = NULL) { // Setup an environment variable containing the database connection so that // functional tests can connect to the database. putenv('SIMPLETEST_DB=' . Database::getConnectionInfoAsUrl()); @@ -292,7 +298,8 @@ function simpletest_phpunit_run_command(array $unescaped_test_classnames, $phpun // exec in a subshell so that the environment is isolated when running tests // via the simpletest UI. - exec(join($command, " "), $output, $ret); + $ret = exec(join($command, " "), $output, $status); + chdir($old_cwd); putenv('SIMPLETEST_DB='); return $ret; diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index b255dea49..60ad26095 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -821,7 +821,7 @@ abstract class TestBase { * @return bool * TRUE if the assertion succeeded, FALSE otherwise. * - * @see TestBase::prepareEnvironment() + * @see \Drupal\simpletest\TestBase::prepareEnvironment() * @see \Drupal\Core\DrupalKernel::bootConfiguration() */ protected function assertNoErrorsLogged() { @@ -830,6 +830,42 @@ abstract class TestBase { return $this->assertFalse(file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.'); } + /** + * Asserts that a specific error has been logged to the PHP error log. + * + * @param string $error_message + * The expected error message. + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + * + * @see \Drupal\simpletest\TestBase::prepareEnvironment() + * @see \Drupal\Core\DrupalKernel::bootConfiguration() + */ + protected function assertErrorLogged($error_message) { + $error_log_filename = DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'; + if (!file_exists($error_log_filename)) { + $this->error('No error logged yet.'); + } + + $content = file_get_contents($error_log_filename); + $rows = explode(PHP_EOL, $content); + + // We iterate over the rows in order to be able to remove the logged error + // afterwards. + $found = FALSE; + foreach ($rows as $row_index => $row) { + if (strpos($content, $error_message) !== FALSE) { + $found = TRUE; + unset($rows[$row_index]); + } + } + + file_put_contents($error_log_filename, implode("\n", $rows)); + + return $this->assertTrue($found, sprintf('The %s error message was logged.', $error_message)); + } + /** * Fire an assertion that is always positive. * diff --git a/core/modules/system/src/Controller/ThemeController.php b/core/modules/system/src/Controller/ThemeController.php index 5f56052b3..d54cd9141 100644 --- a/core/modules/system/src/Controller/ThemeController.php +++ b/core/modules/system/src/Controller/ThemeController.php @@ -29,27 +29,17 @@ class ThemeController extends ControllerBase { */ protected $themeHandler; - /** - * The route builder service. - * - * @var \Drupal\Core\Routing\RouteBuilderInterface - */ - protected $routeBuilder; - /** * Constructs a new ThemeController. * * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler * The theme handler. - * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder - * The route builder. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. */ - public function __construct(ThemeHandlerInterface $theme_handler, RouteBuilderInterface $route_builder, ConfigFactoryInterface $config_factory) { + public function __construct(ThemeHandlerInterface $theme_handler,ConfigFactoryInterface $config_factory) { $this->themeHandler = $theme_handler; $this->configFactory = $config_factory; - $this->routeBuilder = $route_builder; } /** @@ -58,7 +48,6 @@ class ThemeController extends ControllerBase { public static function create(ContainerInterface $container) { return new static( $container->get('theme_handler'), - $container->get('router.builder'), $container->get('config.factory') ); } @@ -183,8 +172,6 @@ class ThemeController extends ControllerBase { // Set the default theme. $config->set('default', $theme)->save(); - $this->routeBuilder->setRebuildNeeded(); - // The status message depends on whether an admin theme is currently in // use: a value of 0 means the admin theme is set to be the default // theme. diff --git a/core/modules/system/src/Form/ThemeSettingsForm.php b/core/modules/system/src/Form/ThemeSettingsForm.php index 395c74359..7f02517d0 100644 --- a/core/modules/system/src/Form/ThemeSettingsForm.php +++ b/core/modules/system/src/Form/ThemeSettingsForm.php @@ -108,13 +108,11 @@ class ThemeSettingsForm extends ConfigFormBase { $themes = $this->themeHandler->listInfo(); - // Deny access if the theme is not installed or not found. - if (!empty($theme) && (empty($themes[$theme]) || !$themes[$theme]->status)) { - throw new NotFoundHttpException(); - } - // Default settings are defined in theme_get_setting() in includes/theme.inc if ($theme) { + if (!$this->themeHandler->hasUi($theme)) { + throw new NotFoundHttpException(); + } $var = 'theme_' . $theme . '_settings'; $config_key = $theme . '.settings'; $themes = $this->themeHandler->listInfo(); diff --git a/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php b/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php index 9c9de198e..7193ea5c4 100644 --- a/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php +++ b/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php @@ -48,7 +48,7 @@ class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface { */ public function getDerivativeDefinitions($base_plugin_definition) { foreach ($this->themeHandler->listInfo() as $theme_name => $theme) { - if ($theme->status) { + if ($this->themeHandler->hasUi($theme_name)) { $this->derivatives[$theme_name] = $base_plugin_definition; $this->derivatives[$theme_name]['title'] = $theme->info['name']; $this->derivatives[$theme_name]['route_parameters'] = array('theme' => $theme_name); diff --git a/core/modules/system/src/SystemConfigSubscriber.php b/core/modules/system/src/SystemConfigSubscriber.php index 204d68538..76ae5731e 100644 --- a/core/modules/system/src/SystemConfigSubscriber.php +++ b/core/modules/system/src/SystemConfigSubscriber.php @@ -7,8 +7,10 @@ namespace Drupal\system; +use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigImporterEvent; +use Drupal\Core\Routing\RouteBuilderInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -18,6 +20,35 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; class SystemConfigSubscriber implements EventSubscriberInterface { use StringTranslationTrait; + /** + * The router builder. + * + * @var \Drupal\Core\Routing\RouteBuilderInterface + */ + protected $routerBuilder; + + /** + * Constructs the SystemConfigSubscriber. + * + * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder + * The router builder service. + */ + public function __construct(RouteBuilderInterface $router_builder) { + $this->routerBuilder = $router_builder; + } + + /** + * Rebuilds the router when the default or admin theme is changed. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + */ + public function onConfigSave(ConfigCrudEvent $event) { + $saved_config = $event->getConfig(); + if ($saved_config->getName() == 'system.theme' && ($event->isChanged('admin') || $event->isChanged('default'))) { + $this->routerBuilder->setRebuildNeeded(); + } + } + /** * Checks that the configuration synchronization is valid. * @@ -55,6 +86,7 @@ class SystemConfigSubscriber implements EventSubscriberInterface { * {@inheritdoc} */ public static function getSubscribedEvents() { + $events[ConfigEvents::SAVE][] = array('onConfigSave', 0); // The empty check has a high priority so that is can stop propagation if // there is no configuration to import. $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateNotEmpty', 512); diff --git a/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php index 0fdd9af97..3e0386ce4 100644 --- a/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php +++ b/core/modules/system/src/Tests/Entity/Element/EntityAutocompleteElementFormTest.php @@ -137,6 +137,7 @@ class EntityAutocompleteElementFormTest extends EntityUnitTestBase implements Fo $form['single_autocreate_no_validate'] = array( '#type' => 'entity_autocomplete', '#target_type' => 'entity_test', + '#validate_reference' => FALSE, '#autocreate' => array( 'bundle' => 'entity_test', ), diff --git a/core/modules/system/src/Tests/Entity/EntityFieldTest.php b/core/modules/system/src/Tests/Entity/EntityFieldTest.php index 6fae79b31..5c9161e3c 100644 --- a/core/modules/system/src/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php @@ -691,8 +691,8 @@ class EntityFieldTest extends EntityUnitTestBase { ->setSetting('target_type', 'node') ->setSetting('handler_settings', ['target_bundles' => ['article' => 'article']]); $reference_field = \Drupal::TypedDataManager()->create($definition); - $reference = $reference_field->appendItem(array('entity' => $node)); - $violations = $reference->validate(); + $reference_field->appendItem(array('entity' => $node)); + $violations = $reference_field->validate(); $this->assertEqual($violations->count(), 1); $node = entity_create('node', array( @@ -701,8 +701,8 @@ class EntityFieldTest extends EntityUnitTestBase { 'title' => $this->randomString(), )); $node->save(); - $reference->setValue($node); - $violations = $reference->validate(); + $reference_field->entity = $node; + $violations = $reference_field->validate(); $this->assertEqual($violations->count(), 0); } diff --git a/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php b/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php index 8e64386a5..aa49b84de 100644 --- a/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php +++ b/core/modules/system/src/Tests/Entity/EntityReferenceFieldTest.php @@ -113,7 +113,7 @@ class EntityReferenceFieldTest extends EntityUnitTestBase { $entity->{$this->fieldName}->target_id = $referenced_entity->id(); $violations = $entity->{$this->fieldName}->validate(); $this->assertEqual($violations->count(), 1, 'Validation throws a violation.'); - $this->assertEqual($violations[0]->getMessage(), t('The entity must be of bundle %bundle.', array('%bundle' => $this->bundle))); + $this->assertEqual($violations[0]->getMessage(), t('This entity (%type: %id) cannot be referenced.', array('%type' => $this->referencedEntityType, '%id' => $referenced_entity->id()))); } /** diff --git a/core/modules/system/src/Tests/File/HtaccessUnitTest.php b/core/modules/system/src/Tests/File/HtaccessUnitTest.php index c8599b3af..9f5273c25 100644 --- a/core/modules/system/src/Tests/File/HtaccessUnitTest.php +++ b/core/modules/system/src/Tests/File/HtaccessUnitTest.php @@ -40,8 +40,7 @@ class HtaccessUnitTest extends KernelTestBase { $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE); $this->assertFalse(strpos($content, "Require all denied") !== FALSE); $this->assertFalse(strpos($content, "Deny from all") !== FALSE); - $this->assertTrue(strpos($content, "Options None") !== FALSE); - $this->assertTrue(strpos($content, "Options +FollowSymLinks") !== FALSE); + $this->assertTrue(strpos($content, "Options -Indexes -ExecCGI -Includes -MultiViews") !== FALSE); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE); $this->assertFilePermissions($public . '/.htaccess', 0444); @@ -54,8 +53,7 @@ class HtaccessUnitTest extends KernelTestBase { $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE); $this->assertTrue(strpos($content, "Require all denied") !== FALSE); $this->assertTrue(strpos($content, "Deny from all") !== FALSE); - $this->assertTrue(strpos($content, "Options None") !== FALSE); - $this->assertTrue(strpos($content, "Options +FollowSymLinks") !== FALSE); + $this->assertTrue(strpos($content, "Options -Indexes -ExecCGI -Includes -MultiViews") !== FALSE); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE); $this->assertFilePermissions($private . '/.htaccess', 0444); @@ -68,8 +66,7 @@ class HtaccessUnitTest extends KernelTestBase { $this->assertTrue(strpos($content,"SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE); $this->assertTrue(strpos($content, "Require all denied") !== FALSE); $this->assertTrue(strpos($content,"Deny from all") !== FALSE); - $this->assertTrue(strpos($content,"Options None") !== FALSE); - $this->assertTrue(strpos($content,"Options +FollowSymLinks") !== FALSE); + $this->assertTrue(strpos($content,"Options -Indexes -ExecCGI -Includes -MultiViews") !== FALSE); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE); $this->assertFilePermissions($stream . '/.htaccess', 0444); diff --git a/core/modules/system/src/Tests/Installer/StandardInstallerTest.php b/core/modules/system/src/Tests/Installer/StandardInstallerTest.php index 184e07b65..dfe920b9c 100644 --- a/core/modules/system/src/Tests/Installer/StandardInstallerTest.php +++ b/core/modules/system/src/Tests/Installer/StandardInstallerTest.php @@ -40,6 +40,19 @@ class StandardInstallerTest extends ConfigAfterInstallerTestBase { parent::setUpSite(); } + /** + * {@inheritdoc} + */ + protected function curlExec($curl_options, $redirect = FALSE) { + // Ensure that we see the classy progress CSS on the batch page. + // Batch processing happens as part of HTTP redirects, so we can access the + // HTML of the batch page. + if (strpos($curl_options[CURLOPT_URL], '&id=1&op=do_nojs') !== FALSE) { + $this->assertRaw('themes/classy/css/components/progress.css'); + } + return parent::curlExec($curl_options, $redirect); + } + /** * Ensures that the exported standard configuration is up to date. */ diff --git a/core/modules/system/src/Tests/Menu/MenuRouterTest.php b/core/modules/system/src/Tests/Menu/MenuRouterTest.php index 42332e06c..7ea4e0f5c 100644 --- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php @@ -204,6 +204,7 @@ class MenuRouterTest extends WebTestBase { "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. $this->drupalGet($path); $this->assertRaw('This is the menuTestCallback content.'); + $this->assertNoText(t('The website encountered an unexpected error. Please try again later.')); } /** diff --git a/core/modules/system/src/Tests/Routing/RouteProviderTest.php b/core/modules/system/src/Tests/Routing/RouteProviderTest.php index c1d24ff33..cbe8b0981 100644 --- a/core/modules/system/src/Tests/Routing/RouteProviderTest.php +++ b/core/modules/system/src/Tests/Routing/RouteProviderTest.php @@ -115,7 +115,7 @@ class RouteProviderTest extends KernelTestBase { public function testCandidateOutlines() { $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + $provider = new TestRouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); $parts = array('node', '5', 'edit'); @@ -532,7 +532,7 @@ class RouteProviderTest extends KernelTestBase { */ public function testGetRoutesByPatternWithLongPatterns() { $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + $provider = new TestRouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); $this->fixtures->createTables($connection); // This pattern has only 3 parts, so we will get candidates, but no routes, @@ -613,3 +613,11 @@ class RouteProviderTest extends KernelTestBase { } } + +class TestRouteProvider extends RouteProvider { + + public function getCandidateOutlines(array $parts) { + return parent::getCandidateOutlines($parts); + } + +} diff --git a/core/modules/system/src/Tests/System/DefaultMobileMetaTagsTest.php b/core/modules/system/src/Tests/System/DefaultMobileMetaTagsTest.php index 31d404d40..ae2d3f4ff 100644 --- a/core/modules/system/src/Tests/System/DefaultMobileMetaTagsTest.php +++ b/core/modules/system/src/Tests/System/DefaultMobileMetaTagsTest.php @@ -44,11 +44,6 @@ class DefaultMobileMetaTagsTest extends WebTestBase { * Verifies that the default mobile meta tags can be removed. */ public function testRemovingDefaultMetaTags() { - // @todo remove once PHP7 on Drupal CI has the fix. - if (version_compare(phpversion(), '7.0.0-dev') >= 0) { - // @see https://bugs.php.net/bug.php?id=70808 - return; - } \Drupal::service('module_installer')->install(array('system_module_test')); $this->drupalGet(''); foreach ($this->defaultMetaTags as $name => $metatag) { diff --git a/core/modules/system/src/Tests/System/ErrorHandlerTest.php b/core/modules/system/src/Tests/System/ErrorHandlerTest.php index 2cea2b766..07f2aacfb 100644 --- a/core/modules/system/src/Tests/System/ErrorHandlerTest.php +++ b/core/modules/system/src/Tests/System/ErrorHandlerTest.php @@ -100,6 +100,7 @@ class ErrorHandlerTest extends WebTestBase { $this->assertErrorMessage($error_warning); $this->assertErrorMessage($error_user_notice); $this->assertNoRaw('
', 'Did not find pre element with backtrace class.');
+    $this->assertErrorLogged($fatal_error['@message']);
 
     // Set error reporting to not collect notices.
     $config->set('error_level', ERROR_REPORTING_DISPLAY_SOME)->save();
diff --git a/core/modules/system/src/Tests/System/ThemeTest.php b/core/modules/system/src/Tests/System/ThemeTest.php
index 8b2836b75..278353db5 100644
--- a/core/modules/system/src/Tests/System/ThemeTest.php
+++ b/core/modules/system/src/Tests/System/ThemeTest.php
@@ -52,6 +52,9 @@ class ThemeTest extends WebTestBase {
     $this->assertResponse(404, 'The theme settings form URL for a uninstalled theme could not be found.');
     $this->drupalGet('admin/appearance/settings/' . $this->randomMachineName());
     $this->assertResponse(404, 'The theme settings form URL for a non-existent theme could not be found.');
+    $this->assertTrue(\Drupal::service('theme_installer')->install(['stable']));
+    $this->drupalGet('admin/appearance/settings/stable');
+    $this->assertResponse(404, 'The theme settings form URL for a hidden theme is unavailable.');
 
     // Specify a filesystem path to be used for the logo.
     $file = current($this->drupalGetTestFiles('image'));
@@ -190,6 +193,23 @@ class ThemeTest extends WebTestBase {
     // The logo field should only be present on the global theme settings form.
     $this->assertNoFieldByName('logo_path');
     $this->drupalPostForm(NULL, [], t('Save configuration'));
+
+    // Ensure only valid themes are listed in the local tasks.
+    $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header']);
+    $this->drupalGet('admin/appearance/settings');
+    $theme_handler = \Drupal::service('theme_handler');
+    $this->assertLink($theme_handler->getName('classy'));
+    $this->assertLink($theme_handler->getName('bartik'));
+    $this->assertNoLink($theme_handler->getName('stable'));
+
+    // If a hidden theme is an admin theme it should be viewable.
+    \Drupal::configFactory()->getEditable('system.theme')->set('admin', 'stable')->save();
+    \Drupal::service('router.builder')->rebuildIfNeeded();
+    $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header', 'theme' => 'stable']);
+    $this->drupalGet('admin/appearance/settings');
+    $this->assertLink($theme_handler->getName('stable'));
+    $this->drupalGet('admin/appearance/settings/stable');
+    $this->assertResponse(200, 'The theme settings form URL for a hidden theme that is the admin theme is available.');
   }
 
   /**
@@ -255,8 +275,14 @@ class ThemeTest extends WebTestBase {
    * Test switching the default theme.
    */
   function testSwitchDefaultTheme() {
+    /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */
+    $theme_handler = \Drupal::service('theme_handler');
+    // First, install Stark and set it as the default theme programmatically.
+    $theme_handler->install(array('stark'));
+    $theme_handler->setDefault('stark');
+
     // Install Bartik and set it as the default theme.
-    \Drupal::service('theme_handler')->install(array('bartik'));
+    $theme_handler->install(array('bartik'));
     $this->drupalGet('admin/appearance');
     $this->clickLink(t('Set as default'));
     $this->assertEqual($this->config('system.theme')->get('default'), 'bartik');
@@ -266,10 +292,10 @@ class ThemeTest extends WebTestBase {
     $this->assertText('Bartik(' . t('active tab') . ')', 'Default local task on blocks admin page is the default theme.');
     // Switch back to Stark and test again to test that the menu cache is cleared.
     $this->drupalGet('admin/appearance');
-    // Classy is the first 'Set as default' link.
-    $this->clickLink(t('Set as default'), 0);
+    // Stark is the first 'Set as default' link.
+    $this->clickLink(t('Set as default'));
     $this->drupalGet('admin/structure/block');
-    $this->assertText('Classy(' . t('active tab') . ')', 'Default local task on blocks admin page has changed.');
+    $this->assertText('Stark(' . t('active tab') . ')', 'Default local task on blocks admin page has changed.');
   }
 
   /**
@@ -328,8 +354,8 @@ class ThemeTest extends WebTestBase {
     // base theme of bartik.
     $this->assertNoRaw('Uninstall Classy theme', 'A link to uninstall the Classy theme does not appear on the theme settings page.');
 
-    // Change the default theme to stark, stark is third in the list.
-    $this->clickLink(t('Set as default'), 2);
+    // Change the default theme to stark, stark is second in the list.
+    $this->clickLink(t('Set as default'), 1);
 
     // Check that bartik can be uninstalled now.
     $this->assertRaw('Uninstall Bartik theme', 'A link to uninstall the Bartik theme does appear on the theme settings page.');
@@ -344,9 +370,9 @@ class ThemeTest extends WebTestBase {
     // Seven is the second in the list.
     $this->clickLink(t('Uninstall'));
     $this->assertRaw('The Seven theme has been uninstalled');
-    // Now uninstall classy.
-    $this->clickLink(t('Uninstall'));
-    $this->assertRaw('The Classy theme has been uninstalled');
+
+    // Check that the classy theme still can't be uninstalled as it is hidden.
+    $this->assertNoRaw('Uninstall Classy theme', 'A link to uninstall the Classy theme does not appear on the theme settings page.');
   }
 
   /**
diff --git a/core/modules/system/src/Tests/System/UncaughtExceptionTest.php b/core/modules/system/src/Tests/System/UncaughtExceptionTest.php
index 8c87ebf46..3a67ae873 100644
--- a/core/modules/system/src/Tests/System/UncaughtExceptionTest.php
+++ b/core/modules/system/src/Tests/System/UncaughtExceptionTest.php
@@ -87,6 +87,7 @@ class UncaughtExceptionTest extends WebTestBase {
     $this->assertResponse(500);
     $this->assertText('The website encountered an unexpected error. Please try again later.');
     $this->assertText($this->expectedExceptionMessage);
+    $this->assertErrorLogged($this->expectedExceptionMessage);
   }
 
   /**
@@ -122,6 +123,7 @@ class UncaughtExceptionTest extends WebTestBase {
 
     $this->assertRaw('The website encountered an unexpected error.');
     $this->assertRaw($this->expectedExceptionMessage);
+    $this->assertErrorLogged($this->expectedExceptionMessage);
   }
 
   /**
@@ -178,6 +180,7 @@ class UncaughtExceptionTest extends WebTestBase {
     $this->assertResponse(500);
 
     $this->assertRaw($this->expectedExceptionMessage);
+    $this->assertErrorLogged($this->expectedExceptionMessage);
   }
 
   /**
@@ -199,6 +202,7 @@ class UncaughtExceptionTest extends WebTestBase {
 
     $this->assertRaw('The website encountered an unexpected error');
     $this->assertRaw($this->expectedExceptionMessage);
+    $this->assertErrorLogged($this->expectedExceptionMessage);
   }
 
   /**
@@ -233,6 +237,7 @@ class UncaughtExceptionTest extends WebTestBase {
     $this->drupalGet('');
     $this->assertResponse(500);
     $this->assertRaw('PDOException');
+    $this->assertErrorLogged($this->expectedExceptionMessage);
   }
 
   /**
@@ -252,7 +257,8 @@ class UncaughtExceptionTest extends WebTestBase {
 
     // Find fatal error logged to the simpletest error.log
     $errors = file(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
-    $this->assertIdentical(count($errors), 1, 'Exactly one line logged to the PHP error log');
+    $this->assertIdentical(count($errors), 2, 'The error + the error that the logging service is broken has been written to the error log.');
+    $this->assertTrue(strpos($errors[0], 'Failed to log error') !== FALSE, 'The error handling logs when an error could not be logged to the logger.');
 
     $expected_path = \Drupal::root() . '/core/modules/system/tests/modules/error_service_test/src/MonkeysInTheControlRoom.php';
     $expected_line = 63;
diff --git a/core/modules/system/src/Tests/Theme/StableLibraryOverrideTest.php b/core/modules/system/src/Tests/Theme/StableLibraryOverrideTest.php
new file mode 100644
index 000000000..526be51c1
--- /dev/null
+++ b/core/modules/system/src/Tests/Theme/StableLibraryOverrideTest.php
@@ -0,0 +1,185 @@
+themeManager = $this->container->get('theme.manager');
+    $this->themeInitialization = $this->container->get('theme.initialization');
+    $this->libraryDiscovery = $this->container->get('library.discovery');
+
+    $this->container->get('theme_installer')->install(['stable']);
+
+    // Enable all core modules.
+    $all_modules = system_rebuild_module_data();
+    $all_modules = array_filter($all_modules, function ($module) {
+      // Filter contrib, hidden, already enabled modules and modules in the
+      // Testing package.
+      if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') {
+        return FALSE;
+      }
+      return TRUE;
+    });
+    $this->allModules = array_keys($all_modules);
+    sort($this->allModules);
+    $this->enableModules($this->allModules);
+  }
+
+  /**
+   * Ensures that Stable overrides all relevant core library assets.
+   */
+  public function testStableLibraryOverrides() {
+    // First get the clean library definitions with no active theme.
+    $libraries_before = $this->getAllLibraries();
+    $libraries_before = $this->removeVendorAssets($libraries_before);
+
+    $this->themeManager->setActiveTheme($this->themeInitialization->getActiveThemeByName('stable'));
+    $this->libraryDiscovery->clearCachedDefinitions();
+
+    // Now get the library definitions with Stable as the active theme.
+    $libraries_after = $this->getAllLibraries();
+    $libraries_after = $this->removeVendorAssets($libraries_after);
+
+    $root = \Drupal::root();
+    foreach ($libraries_before as $extension => $libraries) {
+      foreach ($libraries as $library_name => $library) {
+        // Allow skipping libraries.
+        if (in_array("$extension/$library_name", $this->librariesToSkip)) {
+          continue;
+        }
+        $library_after = $libraries_after[$extension][$library_name];
+
+        // Check that all the CSS assets are overridden.
+        foreach ($library['css'] as $index => $asset) {
+          $clean_path = $asset['data'];
+          $stable_path = $library_after['css'][$index]['data'];
+          // Make core/misc assets look like they are coming from a "core"
+          // module.
+          $replacements = [
+            'core/misc/' => "core/modules/core/css/",
+          ];
+          $expected_path = strtr($clean_path, $replacements);
+
+          // Adjust the module asset paths to correspond with the Stable folder
+          // structure.
+          $expected_path = str_replace("core/modules/$extension/css/", "core/themes/stable/css/$extension/", $expected_path);
+          $assert_path = str_replace("core/modules/$extension/", '', $clean_path);
+
+          $this->assertEqual($expected_path, $stable_path, "$assert_path from the $extension/$library_name library is overridden in Stable.");
+        }
+      }
+    }
+  }
+
+  /**
+   * Removes all vendor libraries and assets from the library definitions.
+   *
+   * @param array[] $all_libraries
+   *   An associative array of libraries keyed by extension, then by library
+   *   name, and so on.
+   *
+   * @return array[]
+   *   The reduced array of libraries.
+   */
+  protected function removeVendorAssets($all_libraries) {
+    foreach ($all_libraries as $extension => $libraries) {
+      foreach ($libraries as $library_name => $library) {
+        if (isset($library['remote'])) {
+          unset($all_libraries[$extension][$library_name]);
+        }
+        foreach (['css', 'js'] as $asset_type) {
+          foreach ($library[$asset_type] as $index => $asset) {
+            if (strpos($asset['data'], 'core/assets/vendor') !== FALSE) {
+              unset($all_libraries[$extension][$library_name][$asset_type][$index]);
+              // Re-key the array of assets. This is needed because
+              // libraries-override doesn't always preserve the order.
+              if (!empty($all_libraries[$extension][$library_name][$asset_type])) {
+                $all_libraries[$extension][$library_name][$asset_type] = array_values($all_libraries[$extension][$library_name][$asset_type]);
+              }
+            }
+          }
+        }
+      }
+    }
+    return $all_libraries;
+  }
+
+  /**
+   * Gets all libraries for core and all installed modules.
+   *
+   * @return array[]
+   *   An associative array of libraries keyed by extension, then by library
+   *   name, and so on.
+   */
+  protected function getAllLibraries() {
+    $modules = \Drupal::moduleHandler()->getModuleList();
+    $module_list = array_keys($modules);
+    sort($module_list);
+    $this->assertEqual($this->allModules, $module_list, 'All core modules are installed.');
+
+    $libraries['core'] = $this->libraryDiscovery->getLibrariesByExtension('core');
+
+    $root = \Drupal::root();
+    foreach ($modules as $module_name => $module) {
+      $library_file = $module->getPath() . '/' . $module_name . '.libraries.yml';
+      if (is_file($root . '/' . $library_file)) {
+        $libraries[$module_name] = $this->libraryDiscovery->getLibrariesByExtension($module_name);
+      }
+    }
+    return $libraries;
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
index 956849c0e..fa1499970 100644
--- a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
+++ b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
@@ -30,8 +30,7 @@ class UpdatePathRC1TestBaseFilledTest extends UpdatePathRC1TestBaseTest {
    * Tests that the content and configuration were properly updated.
    */
   public function testUpdatedSite() {
-    // @todo there are no updates to run.
-    //$this->runUpdates();
+    $this->runUpdates();
 
     $spanish = \Drupal::languageManager()->getLanguage('es');
 
diff --git a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseTest.php b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseTest.php
index 47e70b17e..54c2d1e93 100644
--- a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseTest.php
+++ b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseTest.php
@@ -34,6 +34,8 @@ class UpdatePathRC1TestBaseTest extends UpdatePathTestBase {
    * Tests that the database was properly loaded.
    */
   public function testDatabaseLoaded() {
+    $extensions = \Drupal::service('config.storage')->read('core.extension');
+    $this->assertFalse(isset($extensions['theme']['stable']), 'Stable is not installed before updating.');
     $hook_updates = [
       'user' => '8000',
       'node' => '8003',
@@ -57,11 +59,14 @@ class UpdatePathRC1TestBaseTest extends UpdatePathTestBase {
       $this->assertEqual($existing_updates[$expected_update], 1, new FormattableMarkup("@expected_update exists in 'existing_updates' key and only appears once.", ['@expected_update' => $expected_update]));
     }
 
-    // @todo there are no updates to run.
-    // $this->runUpdates();
+    $this->runUpdates();
     $this->assertEqual(\Drupal::config('system.site')->get('name'), 'Site-Install');
     $this->drupalGet('');
     $this->assertText('Site-Install');
+    $extensions = \Drupal::service('config.storage')->read('core.extension');
+    $this->assertTrue(isset($extensions['theme']['stable']), 'Stable is installed after updating.');
+    $blocks = \Drupal::entityManager()->getStorage('block')->loadByProperties(['theme' => 'stable']);
+    $this->assertTrue(empty($blocks), 'No blocks have been placed for Stable.');
   }
 
 }
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 69044a1ed..c9e81754e 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1464,6 +1464,7 @@ function system_update_8005() {
         break;
 
       case 'classy':
+      case 'stable':
         // Don't place any blocks or trigger custom themes installed warning.
         break;
 
@@ -1541,6 +1542,7 @@ function system_update_8006() {
 
       case 'seven':
       case 'classy':
+      case 'stable':
         // Don't place any blocks or trigger custom themes installed warning.
         break;
       default:
@@ -1805,23 +1807,6 @@ function system_update_8011() {
   _system_update_create_block($name, $theme_name, $values);
 }
 
-/**
- * Install the Stable base theme if needed.
- */
-function system_update_8012() {
-  $theme_handler = \Drupal::service('theme_handler');
-  // Ensure we have fresh info.
-  $theme_handler->rebuildThemeData();
-  foreach ($theme_handler->listInfo() as $theme) {
-    // We first check that a base theme is set because if it's set to false then
-    // it's unset in \Drupal\Core\Extension\ThemeHandler::rebuildThemeData().
-    if (isset($theme->info['base theme']) && $theme->info['base theme'] == 'stable') {
-      $theme_handler->install(['stable']);
-      return;
-    }
-  }
-}
-
 /**
  * Enable automated cron module and move the config into it.
  */
@@ -1847,3 +1832,31 @@ function system_update_8013() {
 /**
  * @} End of "addtogroup updates-8.0.0-beta".
  */
+
+/**
+ * @addtogroup updates-8.0.0-rc
+ * @{
+ */
+
+/**
+ * Install the Stable base theme if needed.
+ */
+function system_update_8014() {
+  $theme_handler = \Drupal::service('theme_handler');
+  if ($theme_handler->themeExists('stable')) {
+    return;
+  }
+  $theme_handler->refreshInfo();
+  foreach ($theme_handler->listInfo() as $theme) {
+    // We first check that a base theme is set because if it's set to false then
+    // it's unset in \Drupal\Core\Extension\ThemeHandler::rebuildThemeData().
+    if (isset($theme->info['base theme']) && $theme->info['base theme'] == 'stable') {
+      $theme_handler->install(['stable']);
+      return;
+    }
+  }
+}
+
+/**
+ * @} End of "addtogroup updates-8.0.0-rc".
+ */
diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml
index 0f816d6fe..c70889d3f 100644
--- a/core/modules/system/system.services.yml
+++ b/core/modules/system/system.services.yml
@@ -35,6 +35,7 @@ services:
       - { name: theme_negotiator, priority: 100 }
   system.config_subscriber:
     class: Drupal\system\SystemConfigSubscriber
+    arguments: ['@router.builder']
     tags:
       - { name: event_subscriber }
   system.config_cache_tag:
diff --git a/core/modules/system/templates/block--local-actions-block.html.twig b/core/modules/system/templates/block--local-actions-block.html.twig
index 65d57be1f..3e660c514 100644
--- a/core/modules/system/templates/block--local-actions-block.html.twig
+++ b/core/modules/system/templates/block--local-actions-block.html.twig
@@ -1,4 +1,4 @@
-{% extends "@block/block.html.twig" %}
+{% extends "block.html.twig" %}
 {#
 /**
  * @file
diff --git a/core/modules/system/tests/fixtures/update/drupal-8-rc1.bare.standard.php.gz b/core/modules/system/tests/fixtures/update/drupal-8-rc1.bare.standard.php.gz
new file mode 100644
index 000000000..db046bea9
Binary files /dev/null and b/core/modules/system/tests/fixtures/update/drupal-8-rc1.bare.standard.php.gz differ
diff --git a/core/modules/system/tests/fixtures/update/drupal-8-rc1.filled.standard.php.gz b/core/modules/system/tests/fixtures/update/drupal-8-rc1.filled.standard.php.gz
new file mode 100644
index 000000000..615b88a7d
Binary files /dev/null and b/core/modules/system/tests/fixtures/update/drupal-8-rc1.filled.standard.php.gz differ
diff --git a/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php b/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php
index 5e2835e6a..7c817c2ce 100644
--- a/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php
+++ b/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php
@@ -6,6 +6,7 @@
  */
 
 namespace Drupal\test_page_test\Controller;
+use Symfony\Component\HttpKernel\Exception\HttpException;
 
 /**
  * Defines a test controller for page titles.
@@ -75,4 +76,14 @@ class Test {
     );
   }
 
+  /**
+   * Throws a HTTP exception.
+   *
+   * @param int $code
+   *   The status code.
+   */
+  public function httpResponseException($code) {
+    throw new HttpException($code);
+  }
+
 }
diff --git a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml
index 240842349..a00d0844c 100644
--- a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml
+++ b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml
@@ -42,3 +42,11 @@ test_page_test.admin_render_title:
     _controller: '\Drupal\test_page_test\Controller\Test::renderTitle'
   requirements:
     _access: 'TRUE'
+
+test_page_test.http_response_exception:
+  path: '/test-http-response-exception/{code}'
+  defaults:
+    _controller: '\Drupal\test_page_test\Controller\Test::httpResponseException'
+    code: 200
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php b/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php
index ac6af1491..d51f6e3a7 100644
--- a/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php
+++ b/core/modules/system/tests/src/Unit/Menu/MenuLinkTreeTest.php
@@ -135,6 +135,7 @@ class MenuLinkTreeTest extends UnitTestCase {
         'max-age' => Cache::PERMANENT,
       ],
       '#sorted' => TRUE,
+      '#menu_name' => 'mock',
       '#theme' => 'menu__mock',
       '#items' => [
         // To be filled when generating test cases, using $get_built_element().
diff --git a/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php b/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php
index 802f5ef84..54e67902f 100644
--- a/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php
+++ b/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php
@@ -44,6 +44,10 @@ class SystemLocalTasksTest extends LocalTaskIntegrationTestBase {
       ->will($this->returnValue(array(
         'bartik' => $theme,
       )));
+    $this->themeHandler->expects($this->any())
+      ->method('hasUi')
+      ->with('bartik')
+      ->willReturn(TRUE);
     $this->container->set('theme_handler', $this->themeHandler);
   }
 
diff --git a/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml b/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml
index afa55cb55..3efb45f3d 100644
--- a/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml
+++ b/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml
@@ -4,6 +4,8 @@ description: 'Test theme which acts as a base theme for other test subthemes.'
 version: VERSION
 core: 8.x
 base theme: false
+hidden: true
+
 libraries:
   - test_basetheme/global-styling
 stylesheets-remove:
diff --git a/core/modules/system/tests/themes/test_theme/test_theme.info.yml b/core/modules/system/tests/themes/test_theme/test_theme.info.yml
index 4c1568c4c..fca49c240 100644
--- a/core/modules/system/tests/themes/test_theme/test_theme.info.yml
+++ b/core/modules/system/tests/themes/test_theme/test_theme.info.yml
@@ -44,12 +44,12 @@ libraries-override:
   core/drupal.dropbutton:
     css:
       component:
-        misc/dropbutton/dropbutton.css: /themes/my_theme/css/dropbutton.css
+        /core/themes/stable/css/core/dropbutton/dropbutton.css: /themes/my_theme/css/dropbutton.css
   # Use stream wrappers.
   core/drupal.vertical-tabs:
     css:
       component:
-        misc/vertical-tabs.css: public://my_css/vertical-tabs.css
+        /core/themes/stable/css/core/vertical-tabs.css: public://my_css/vertical-tabs.css
   # Use a protocol-relative URI.
   core/jquery.ui:
     css:
diff --git a/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml b/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml
index b4aca4899..bda89d41c 100644
--- a/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml
+++ b/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml
@@ -19,6 +19,7 @@ process:
     -
       plugin: skip_on_empty
       method: row
+  label: name
   'settings/handler': 'constants/selection_handler'
   'settings/handler_settings/target_bundles/0': '@field_name'
   'settings/handler_settings/auto_create': 'constants/auto_create'
diff --git a/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyTerm.php b/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyTerm.php
deleted file mode 100644
index 0a28f5ba7..000000000
--- a/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyTerm.php
+++ /dev/null
@@ -1,30 +0,0 @@
-isStub()) {
-      $row->setDestinationProperty('name', $this->t('Stub name for source tid:') . $row->getSourceProperty('tid'));
-    }
-    return parent::getEntity($row, $old_destination_id_values);
-  }
-
-}
diff --git a/core/modules/taxonomy/src/Tests/Migrate/MigrateTaxonomyTermStubTest.php b/core/modules/taxonomy/src/Tests/Migrate/MigrateTaxonomyTermStubTest.php
new file mode 100644
index 000000000..a7cd9ca7f
--- /dev/null
+++ b/core/modules/taxonomy/src/Tests/Migrate/MigrateTaxonomyTermStubTest.php
@@ -0,0 +1,118 @@
+installEntitySchema('taxonomy_term');
+  }
+
+  /**
+   * Tests creation of taxonomy term stubs.
+   */
+  public function testStub() {
+    Vocabulary::create([
+      'vid' => 'test_vocabulary',
+      'name' => 'Test vocabulary',
+    ])->save();
+    $this->performStubTest('taxonomy_term');
+  }
+
+  /**
+   * Tests creation of stubs when weight is mapped.
+   */
+  public function testStubWithWeightMapping() {
+    // Create a vocabulary via migration for the terms to reference.
+    $vocabulary_data_rows = [
+      ['id' => '1', 'name' => 'tags'],
+    ];
+    $ids = ['id' => ['type' => 'integer']];
+    $config = [
+      'id' => 'vocabularies',
+      'migration_tags' => ['Stub test'],
+      'source' => [
+        'plugin' => 'embedded_data',
+        'data_rows' => $vocabulary_data_rows,
+        'ids' => $ids,
+      ],
+      'process' => [
+        'vid' => 'id',
+        'name' => 'name',
+      ],
+      'destination' => ['plugin' => 'entity:taxonomy_vocabulary'],
+    ];
+    $vocabulary_migration = Migration::create($config);
+    $vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this);
+    $vocabulary_executable->import();
+
+    // We have a term referencing an unmigrated parent, forcing a stub to be
+    // created.
+    $term_data_rows = [
+      ['id' => '1', 'vocab' => '1', 'name' => 'music', 'parent' => '2'],
+    ];
+    $ids = ['id' => ['type' => 'integer']];
+    $config = [
+      'id' => 'terms',
+      'migration_tags' => ['Import and rollback test'],
+      'source' => [
+        'plugin' => 'embedded_data',
+        'data_rows' => $term_data_rows,
+        'ids' => $ids,
+      ],
+      'process' => [
+        'tid' => 'id',
+        'vid' => 'vocab',
+        'name' => 'name',
+        'weight' => 'weight',
+        'parent' => [
+          'plugin' => 'migration',
+          'migration' => 'terms',
+          'source' => 'parent',
+        ],
+      ],
+      'destination' => ['plugin' => 'entity:taxonomy_term'],
+      'migration_dependencies' => ['required' => ['vocabularies']],
+    ];
+
+    $term_migration = Migration::create($config);
+    $term_migration->save();
+    $term_executable = new MigrateExecutable($term_migration, $this);
+    $term_executable->import();
+    // Load the referenced term, which should exist as a stub.
+    /** @var \Drupal\Core\Entity\ContentEntityBase $stub_entity */
+    $stub_entity = Term::load(2);
+    $this->assertTrue($stub_entity, 'Stub successfully created');
+    if ($stub_entity) {
+      $this->assertIdentical(count($stub_entity->validate()), 0, 'Stub is a valid entity');
+    }
+  }
+}
diff --git a/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php b/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php
index 903957fc3..967dc6824 100644
--- a/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php
+++ b/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php
@@ -39,11 +39,13 @@ class MigrateVocabularyFieldInstanceTest extends MigrateDrupal6TestBase {
     $field_id = 'node.article.tags';
     $field = FieldConfig::load($field_id);
     $this->assertIdentical($field_id, $field->id(), 'Field instance exists on article bundle.');
+    $this->assertIdentical('Tags', $field->label());
 
     // Test the page bundle as well.
     $field_id = 'node.page.tags';
     $field = FieldConfig::load($field_id);
     $this->assertIdentical($field_id, $field->id(), 'Field instance exists on page bundle.');
+    $this->assertIdentical('Tags', $field->label());
 
     $settings = $field->getSettings();
     $this->assertIdentical('default:taxonomy_term', $settings['handler'], 'The handler plugin ID is correct.');
diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml
index a7a75f30f..8a3bd1a58 100644
--- a/core/modules/taxonomy/taxonomy.routing.yml
+++ b/core/modules/taxonomy/taxonomy.routing.yml
@@ -23,6 +23,7 @@ entity.taxonomy_term.edit_form:
     _admin_route: TRUE
   requirements:
     _entity_access: 'taxonomy_term.update'
+    taxonomy_term: \d+
 
 entity.taxonomy_term.delete_form:
   path: '/taxonomy/term/{taxonomy_term}/delete'
@@ -33,6 +34,7 @@ entity.taxonomy_term.delete_form:
     _admin_route: TRUE
   requirements:
     _entity_access: 'taxonomy_term.delete'
+    taxonomy_term: \d+
 
 entity.taxonomy_vocabulary.add_form:
   path: '/admin/structure/taxonomy/add'
@@ -82,3 +84,4 @@ entity.taxonomy_term.canonical:
     _title_callback: '\Drupal\taxonomy\Controller\TaxonomyController::termTitle'
   requirements:
     _entity_access: 'taxonomy_term.view'
+    taxonomy_term: \d+
diff --git a/core/modules/toolbar/js/views/ToolbarVisualView.js b/core/modules/toolbar/js/views/ToolbarVisualView.js
index 7e0401926..c3b0c85cb 100644
--- a/core/modules/toolbar/js/views/ToolbarVisualView.js
+++ b/core/modules/toolbar/js/views/ToolbarVisualView.js
@@ -124,7 +124,7 @@
       var orientation = this.model.get('orientation');
       // Determine the toggle-to orientation.
       var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
-      var locked = (antiOrientation === 'vertical') ? true : false;
+      var locked = antiOrientation === 'vertical';
       // Remember the locked state.
       if (locked) {
         localStorage.setItem('Drupal.toolbar.trayVerticalLocked', 'true');
diff --git a/core/modules/toolbar/src/Menu/ToolbarMenuLinkTree.php b/core/modules/toolbar/src/Menu/ToolbarMenuLinkTree.php
index 6742e6c61..d1e22a9e4 100644
--- a/core/modules/toolbar/src/Menu/ToolbarMenuLinkTree.php
+++ b/core/modules/toolbar/src/Menu/ToolbarMenuLinkTree.php
@@ -30,6 +30,7 @@ class ToolbarMenuLinkTree extends MenuLinkTree {
       $menu_name = $first_link->getMenuName();
       // Add a more specific theme suggestion to differentiate this rendered
       // menu from others.
+      $build['#menu_name'] = $menu_name;
       $build['#theme'] = 'menu__toolbar__' . strtr($menu_name, '-', '_');
       return $build;
     }
diff --git a/core/modules/tracker/tracker.routing.yml b/core/modules/tracker/tracker.routing.yml
index d227f6d97..ed2f8681c 100644
--- a/core/modules/tracker/tracker.routing.yml
+++ b/core/modules/tracker/tracker.routing.yml
@@ -14,6 +14,7 @@ tracker.users_recent_content:
   requirements:
     _permission: 'access content'
     _access_tracker_own_information: 'TRUE'
+    user: \d+
 
 tracker.user_tab:
   path: '/user/{user}/activity'
@@ -23,4 +24,5 @@ tracker.user_tab:
   requirements:
     _permission: 'access content'
     _entity_access: 'user.view'
+    user: \d+
 
diff --git a/core/modules/update/migration_templates/d6_update_settings.yml b/core/modules/update/migration_templates/update_settings.yml
similarity index 94%
rename from core/modules/update/migration_templates/d6_update_settings.yml
rename to core/modules/update/migration_templates/update_settings.yml
index 37e0ef1ee..ad2247235 100644
--- a/core/modules/update/migration_templates/d6_update_settings.yml
+++ b/core/modules/update/migration_templates/update_settings.yml
@@ -1,7 +1,8 @@
-id: d6_update_settings
+id: update_settings
 label: Update configuration
 migration_tags:
   - Drupal 6
+  - Drupal 7
 source:
   plugin: variable
   variables:
diff --git a/core/modules/update/src/Tests/Migrate/d6/MigrateUpdateConfigsTest.php b/core/modules/update/src/Tests/Migrate/d6/MigrateUpdateConfigsTest.php
index 679350a85..29cf50293 100644
--- a/core/modules/update/src/Tests/Migrate/d6/MigrateUpdateConfigsTest.php
+++ b/core/modules/update/src/Tests/Migrate/d6/MigrateUpdateConfigsTest.php
@@ -29,7 +29,7 @@ class MigrateUpdateConfigsTest extends MigrateDrupal6TestBase {
    */
   protected function setUp() {
     parent::setUp();
-    $this->executeMigration('d6_update_settings');
+    $this->executeMigration('update_settings');
   }
 
   /**
diff --git a/core/modules/user/config/schema/user.source.schema.yml b/core/modules/user/config/schema/user.source.schema.yml
index 19990aa0b..81d3a38c6 100644
--- a/core/modules/user/config/schema/user.source.schema.yml
+++ b/core/modules/user/config/schema/user.source.schema.yml
@@ -17,7 +17,7 @@ migrate.source.d6_user:
 
 migrate.source.d6_user_picture_file:
   type: migrate_source_sql
-  label: 'Drupal 6 user picure display'
+  label: 'Drupal 6 user picture display'
   mapping:
     constants:
       type: mapping
@@ -29,7 +29,7 @@ migrate.source.d6_user_picture_file:
 
 migrate.source.d6_user_picture_instance:
   type: migrate_source_sql
-  label: 'Drupal 6 user picure display'
+  label: 'Drupal 6 user picture display'
   mapping:
     provider:
       type: string
diff --git a/core/modules/user/src/Entity/UserRouteProvider.php b/core/modules/user/src/Entity/UserRouteProvider.php
index f4cfd7928..fb2ea6918 100644
--- a/core/modules/user/src/Entity/UserRouteProvider.php
+++ b/core/modules/user/src/Entity/UserRouteProvider.php
@@ -27,6 +27,7 @@ class UserRouteProvider implements EntityRouteProviderInterface {
         '_entity_view' => 'user.full',
         '_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
       ])
+      ->setRequirement('user', '\d+')
       ->setRequirement('_entity_access', 'user.view');
     $route_collection->add('entity.user.canonical', $route);
 
@@ -36,6 +37,7 @@ class UserRouteProvider implements EntityRouteProviderInterface {
         '_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
       ])
       ->setOption('_admin_route', TRUE)
+      ->setRequirement('user', '\d+')
       ->setRequirement('_entity_access', 'user.update');
     $route_collection->add('entity.user.edit_form', $route);
 
@@ -45,6 +47,7 @@ class UserRouteProvider implements EntityRouteProviderInterface {
         '_entity_form' => 'user.cancel',
       ])
       ->setOption('_admin_route', TRUE)
+      ->setRequirement('user', '\d+')
       ->setRequirement('_entity_access', 'user.delete');
     $route_collection->add('entity.user.cancel_form', $route);
 
diff --git a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php
index 160c50efd..3964f29db 100644
--- a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php
+++ b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php
@@ -168,6 +168,42 @@ class UserSelection extends DefaultSelection {
     return $query;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
+    $user = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
+
+    // In order to create a referenceable user, it needs to be active.
+    if (!$this->currentUser->hasPermission('administer users')) {
+      /** @var \Drupal\user\UserInterface $user */
+      $user->activate();
+    }
+
+    return $user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateReferenceableNewEntities(array $entities) {
+    $entities = parent::validateReferenceableNewEntities($entities);
+    // Mirror the conditions checked in buildEntityQuery().
+    if (!empty($this->configuration['handler_settings']['filter']['role'])) {
+      $entities = array_filter($entities, function ($user) {
+        /** @var \Drupal\user\UserInterface $user */
+        return !empty(array_intersect($user->getRoles(), $this->configuration['handler_settings']['filter']['role']));
+      });
+    }
+    if (!$this->currentUser->hasPermission('administer users')) {
+      $entities = array_filter($entities, function ($user) {
+        /** @var \Drupal\user\UserInterface $user */
+        return $user->isActive();
+      });
+    }
+    return $entities;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php
index 53520921e..beda7c806 100644
--- a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php
+++ b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php
@@ -7,8 +7,11 @@
 
 namespace Drupal\user\Plugin\migrate\destination;
 
+use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\EmailItem;
 use Drupal\Core\Password\PasswordInterface;
 use Drupal\migrate\Entity\MigrationInterface;
 use Drupal\migrate\MigrateException;
@@ -50,11 +53,13 @@ class EntityUser extends EntityContentBase {
    *   The migrate plugin manager.
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager service.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type plugin manager service.
    * @param \Drupal\Core\Password\PasswordInterface $password
    *   The password service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, PasswordInterface $password) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager);
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, PasswordInterface $password) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager);
     if (isset($configuration['md5_passwords'])) {
       $this->password = $password;
     }
@@ -73,6 +78,7 @@ class EntityUser extends EntityContentBase {
       $container->get('entity.manager')->getStorage($entity_type),
       array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
       $container->get('entity.manager'),
+      $container->get('plugin.manager.field.field_type'),
       $container->get('password')
     );
   }
@@ -90,6 +96,10 @@ class EntityUser extends EntityContentBase {
         throw new MigrateException('Password service has been altered by another module, aborting.');
       }
     }
+    // Do not overwrite the root account password.
+    if ($row->getDestinationProperty('uid') == 1) {
+      $row->removeDestinationProperty('pass');
+    }
     $ids = parent::import($row, $old_destination_id_values);
     if ($this->password) {
       $this->password->disableMd5Prefixing();
@@ -98,4 +108,28 @@ class EntityUser extends EntityContentBase {
     return $ids;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function processStubRow(Row $row) {
+    parent::processStubRow($row);
+    // Email address is not defined as required in the base field definition but
+    // is effectively required by the UserMailRequired constraint. This means
+    // that Entity::processStubRow() did not populate it - we do it here.
+    $field_definitions = $this->entityManager
+      ->getFieldDefinitions($this->storage->getEntityTypeId(),
+        $this->getKey('bundle'));
+    $mail = EmailItem::generateSampleValue($field_definitions['mail']);
+    $row->setDestinationProperty('mail', reset($mail));
+
+    // @todo Work-around for https://www.drupal.org/node/2602066.
+    $name = $row->getDestinationProperty('name');
+    if (is_array($name)) {
+      $name = reset($name);
+    }
+    if (Unicode::strlen($name) > USERNAME_MAX_LENGTH) {
+      $row->setDestinationProperty('name', Unicode::substr($name, 0, USERNAME_MAX_LENGTH));
+    }
+  }
+
 }
diff --git a/core/modules/user/src/Tests/Migrate/MigrateUserAdminPassTest.php b/core/modules/user/src/Tests/Migrate/MigrateUserAdminPassTest.php
new file mode 100644
index 000000000..58496508f
--- /dev/null
+++ b/core/modules/user/src/Tests/Migrate/MigrateUserAdminPassTest.php
@@ -0,0 +1,116 @@
+container->get('module_handler')->loadInclude('user', 'install');
+    $this->installEntitySchema('user');
+    user_install();
+    /** @var \Drupal\user\Entity\User $admin_account */
+    $admin_account = User::load(1);
+    $admin_account->setPassword('original');
+    $admin_account->save();
+    $this->originalPasswords[1] = $admin_account->getPassword();
+
+    /** @var \Drupal\user\Entity\User $user_account */
+    $user_account = User::create([
+      'uid' => 2,
+      'name' => 'original_username',
+      'mail' => 'original_email@example.com',
+      'pass' => 'original_password',
+    ]);
+    $user_account->save();
+    $this->originalPasswords[2] = $user_account->getPassword();
+  }
+
+  /**
+   * Tests preserving the admin user's password.
+   */
+  public function testAdminPasswordPreserved() {
+    $user_data_rows = [
+      [
+        'id' => '1',
+        'username' => 'site_admin',
+        'password' => 'new_password',
+        'email' => 'site_admin@example.com',
+      ],
+      [
+        'id' => '2',
+        'username' => 'random_user',
+        'password' => 'random_password',
+        'email' => 'random_user@example.com',
+      ],
+    ];
+    $ids = ['id' => ['type' => 'integer']];
+    $config = [
+      'id' => 'users',
+      'migration_tags' => ['Admin password test'],
+      'source' => [
+        'plugin' => 'embedded_data',
+        'data_rows' => $user_data_rows,
+        'ids' => $ids,
+      ],
+      'process' => [
+        'uid' => 'id',
+        'name' => 'username',
+        'mail' => 'email',
+        'pass' => 'password',
+      ],
+      'destination' => ['plugin' => 'entity:user'],
+    ];
+    $migration = Migration::create($config);
+    $this->executeMigration($migration);
+
+    // Verify that admin username and email were changed, but password was not.
+    /** @var \Drupal\user\Entity\User $admin_account */
+    $admin_account = User::load(1);
+    $this->assertIdentical($admin_account->getUsername(), 'site_admin');
+    $this->assertIdentical($admin_account->getEmail(), 'site_admin@example.com');
+    $this->assertIdentical($admin_account->getPassword(), $this->originalPasswords[1]);
+
+    // Verify that everything changed for the regular user.
+    /** @var \Drupal\user\Entity\User $user_account */
+    $user_account = User::load(2);
+    $this->assertIdentical($user_account->getUsername(), 'random_user');
+    $this->assertIdentical($user_account->getEmail(), 'random_user@example.com');
+    $this->assertNotIdentical($user_account->getPassword(), $this->originalPasswords[2]);
+  }
+
+}
diff --git a/core/modules/user/src/Tests/Migrate/MigrateUserStubTest.php b/core/modules/user/src/Tests/Migrate/MigrateUserStubTest.php
new file mode 100644
index 000000000..0a65ddf2d
--- /dev/null
+++ b/core/modules/user/src/Tests/Migrate/MigrateUserStubTest.php
@@ -0,0 +1,43 @@
+installEntitySchema('user');
+    $this->installSchema('system', ['sequences']);
+  }
+
+  /**
+   * Tests creation of user stubs.
+   */
+  public function testStub() {
+    $this->performStubTest('user');
+  }
+
+}
diff --git a/core/modules/user/src/Tests/Migrate/d6/MigrateUserTest.php b/core/modules/user/src/Tests/Migrate/d6/MigrateUserTest.php
index ddb93cf4f..eb94627c6 100644
--- a/core/modules/user/src/Tests/Migrate/d6/MigrateUserTest.php
+++ b/core/modules/user/src/Tests/Migrate/d6/MigrateUserTest.php
@@ -119,8 +119,11 @@ class MigrateUserTest extends MigrateDrupal6TestBase {
       }
 
       // Use the API to check if the password has been salted and re-hashed to
-      // conform the Drupal >= 7.
-      $this->assertTrue(\Drupal::service('password')->check($source->pass_plain, $user->getPassword()));
+      // conform to Drupal >= 7 for non-admin users.
+      if ($user->id() != 1) {
+        $this->assertTrue(\Drupal::service('password')
+          ->check($source->pass_plain, $user->getPassword()));
+      }
     }
     // Rollback the migration and make sure everything is deleted but uid 1.
     (new MigrateExecutable($this->migration, $this))->rollback();
diff --git a/core/modules/user/src/Tests/UserValidationTest.php b/core/modules/user/src/Tests/UserValidationTest.php
index 4891d5a15..dfa8a2114 100644
--- a/core/modules/user/src/Tests/UserValidationTest.php
+++ b/core/modules/user/src/Tests/UserValidationTest.php
@@ -176,7 +176,7 @@ class UserValidationTest extends KernelTestBase {
     $user->roles[1]->target_id = 'unknown_role';
     $violations = $user->validate();
     $this->assertEqual(count($violations), 1);
-    $this->assertEqual($violations[0]->getPropertyPath(), 'roles.1');
+    $this->assertEqual($violations[0]->getPropertyPath(), 'roles.1.target_id');
     $this->assertEqual($violations[0]->getMessage(), t('The referenced entity (%entity_type: %name) does not exist.', array('%entity_type' => 'user_role', '%name' => 'unknown_role')));
   }
 
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 6eb709fe5..6eea7ecec 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -138,6 +138,7 @@ user.cancel_confirm:
     hashed_pass: ''
   requirements:
     _entity_access: 'user.delete'
+    user: \d+
 
 user.reset:
   path: '/user/reset/{uid}/{timestamp}/{hash}'
diff --git a/core/modules/views/src/Plugin/views/argument_default/Raw.php b/core/modules/views/src/Plugin/views/argument_default/Raw.php
index b94a2db0f..b4efe9cf6 100644
--- a/core/modules/views/src/Plugin/views/argument_default/Raw.php
+++ b/core/modules/views/src/Plugin/views/argument_default/Raw.php
@@ -112,11 +112,15 @@ class Raw extends ArgumentDefaultPluginBase implements CacheableDependencyInterf
    * {@inheritdoc}
    */
   public function getArgument() {
-    $path = trim($this->currentPath->getPath($this->view->getRequest()), '/');
+    // Don't trim the leading slash since getAliasByPath() requires it.
+    $path = rtrim($this->currentPath->getPath($this->view->getRequest()), '/');
     if ($this->options['use_alias']) {
       $path = $this->aliasManager->getAliasByPath($path);
     }
     $args = explode('/', $path);
+    // Drop the empty first element created by the leading slash since the path
+    // component index doesn't take it into account.
+    array_shift($args);
     if (isset($args[$this->options['index']])) {
       return $args[$this->options['index']];
     }
diff --git a/core/modules/views/src/Tests/DefaultViewsTest.php b/core/modules/views/src/Tests/DefaultViewsTest.php
index 23e7c97e5..197d25e5a 100644
--- a/core/modules/views/src/Tests/DefaultViewsTest.php
+++ b/core/modules/views/src/Tests/DefaultViewsTest.php
@@ -210,6 +210,13 @@ class DefaultViewsTest extends ViewTestBase {
       ),
     );
     $this->assertIdenticalResultset($view, $expected_result, $column_map);
+
+    $view->storage->setStatus(TRUE);
+    $view->save();
+    \Drupal::service('router.builder')->rebuild();
+
+    $this->drupalGet('archive');
+    $this->assertResponse(200);
   }
 
 }
diff --git a/core/modules/views/src/Tests/Plugin/StyleGridTest.php b/core/modules/views/src/Tests/Plugin/StyleGridTest.php
index 6dbac6bd9..698d2ba10 100644
--- a/core/modules/views/src/Tests/Plugin/StyleGridTest.php
+++ b/core/modules/views/src/Tests/Plugin/StyleGridTest.php
@@ -53,7 +53,7 @@ class StyleGridTest extends PluginTestBase {
 
     // Ensure styles are properly added for grid views.
     $this->drupalGet('test-grid');
-    $this->assertRaw('views/css/views.module.css');
+    $this->assertRaw('stable/css/views/views.module.css');
   }
 
   /**
diff --git a/core/modules/views/src/Tests/Plugin/StyleSummaryTest.php b/core/modules/views/src/Tests/Plugin/StyleSummaryTest.php
new file mode 100644
index 000000000..cea8fba1b
--- /dev/null
+++ b/core/modules/views/src/Tests/Plugin/StyleSummaryTest.php
@@ -0,0 +1,77 @@
+entities[] = $entity = EntityTest::create([
+          'name' => 'Entity ' . ($i * 5 + $j),
+          'type' => 'type' . $i,
+        ]);
+        $entity->save();
+      }
+    }
+  }
+
+  /**
+   * Tests a summary view.
+   */
+  public function testSummaryView() {
+    $this->drupalGet('test-summary');
+
+    $summary_list = $this->cssSelect('ul.views-summary li');
+    $this->assertEqual(4, count($summary_list));
+
+    foreach ($summary_list as $summary_list_item) {
+      $this->assertEqual('(5)', trim((string) $summary_list_item));
+    }
+
+    $summary_links = $this->cssSelect('ul.views-summary a');
+    $this->assertEqual(4, count($summary_links));
+    foreach ($summary_links as $index => $summary_link) {
+      $this->assertEqual('type' . $index, trim((string) $summary_link));
+    }
+
+    $this->clickLink('type1');
+    $entries = $this->cssSelect('div.view-content div.views-row');
+    $this->assertEqual(2, count($entries));
+  }
+
+}
diff --git a/core/modules/views/src/Tests/Plugin/StyleTestBase.php b/core/modules/views/src/Tests/Plugin/StyleTestBase.php
index 38db4d903..508fc1214 100644
--- a/core/modules/views/src/Tests/Plugin/StyleTestBase.php
+++ b/core/modules/views/src/Tests/Plugin/StyleTestBase.php
@@ -18,7 +18,7 @@ abstract class StyleTestBase extends ViewKernelTestBase {
   /**
    * Stores the SimpleXML representation of the output.
    *
-   * @var SimpleXMLElement
+   * @var \SimpleXMLElement
    */
   protected $elements;
 
diff --git a/core/modules/views/src/Tests/Wizard/BasicTest.php b/core/modules/views/src/Tests/Wizard/BasicTest.php
index 8a64df8cb..b87bca611 100644
--- a/core/modules/views/src/Tests/Wizard/BasicTest.php
+++ b/core/modules/views/src/Tests/Wizard/BasicTest.php
@@ -189,7 +189,10 @@ class BasicTest extends WizardTestBase {
     $this->drupalPostAjaxForm(NULL, array('show[wizard_key]' => 'users'), 'show[wizard_key]');
     $this->assertNoFieldByName('show[type]', NULL, 'The "of type" filter is not added for users.');
     $this->drupalPostAjaxForm(NULL, array('show[wizard_key]' => 'node'), 'show[wizard_key]');
-    $this->assertFieldByName('show[type]', 'all', 'The "of type" filter is added for nodes.');
+    $this->assertNoFieldByName('show[type]', 'all', 'The "of type" filter is not added for nodes when there are no node types.');
+    $this->drupalCreateContentType(array('type' => 'page'));
+    $this->drupalPostAjaxForm(NULL, array('show[wizard_key]' => 'node'), 'show[wizard_key]');
+    $this->assertFieldByName('show[type]', 'all', 'The "of type" filter is added for nodes when there is at least one node type.');
   }
 
   /**
diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php
index 9f5d32a88..3ac4e1792 100644
--- a/core/modules/views/src/ViewExecutable.php
+++ b/core/modules/views/src/ViewExecutable.php
@@ -553,7 +553,11 @@ class ViewExecutable implements \Serializable {
    *   The items per page.
    */
   public function setItemsPerPage($items_per_page) {
-    $this->element['#cache']['keys'][] = 'items_per_page:' . $items_per_page;
+    // Check whether the element is pre rendered. At that point, the cache keys
+    // cannot longer be manipulated.
+    if (empty($this->element['#pre_rendered'])) {
+      $this->element['#cache']['keys'][] = 'items_per_page:' . $items_per_page;
+    }
     $this->items_per_page = $items_per_page;
 
     // If the pager is already initialized, pass it through to the pager.
@@ -583,9 +587,15 @@ class ViewExecutable implements \Serializable {
    *   The pager offset.
    */
   public function setOffset($offset) {
-    $this->element['#cache']['keys'][] = 'offset:' . $offset;
+    // Check whether the element is pre rendered. At that point, the cache keys
+    // cannot longer be manipulated.
+    if (empty($this->element['#pre_rendered'])) {
+      $this->element['#cache']['keys'][] = 'offset:' . $offset;
+    }
+
     $this->offset = $offset;
 
+
     // If the pager is already initialized, pass it through to the pager.
     if (!empty($this->pager)) {
       $this->pager->setOffset($offset);
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_summary.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_summary.yml
new file mode 100644
index 000000000..daf92fa2a
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_summary.yml
@@ -0,0 +1,127 @@
+langcode: en
+status: true
+id: test_summary
+label: Test Summary
+module: views
+description: ''
+tag: default
+base_table: entity_test
+base_field: id
+core: '8'
+display:
+  default:
+    id: default
+    display_title: Master
+    display_plugin: default
+    position: 0
+    display_options:
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      title: 'Summary test'
+      access:
+        type: none
+      cache:
+        type: tag
+        options: {  }
+      pager:
+        type: mini
+        options:
+          items_per_page: 2
+          offset: 0
+          id: 0
+          total_pages: 0
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '1, 2, 3, 4'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: ‹‹
+            next: ››
+      sorts:
+        id:
+          id: id
+          table: entity_test
+          field: id
+          order: ASC
+          plugin_id: standard
+          relationship: none
+      arguments:
+        type:
+          id: type
+          field: type
+          table: entity_test
+          default_action: summary
+          exception:
+            title_enable: true
+          title_enable: true
+          title: '{{ arguments.type }}'
+          default_argument_type: fixed
+          summary:
+            sort_order: asc
+            format: default_summary
+          summary_options:
+            override: true
+            items_per_page: 4
+          specify_validation: true
+          plugin_id: string
+          entity_type: entity_test
+      fields:
+        id:
+          id: id
+          field: id
+          table: entity_test
+          plugin_id: field
+          entity_type: entity_test
+      style:
+        type: default
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          uses_fields: false
+      row:
+        type: fields
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      fields: {  }
+      display_extenders: {  }
+    cache_metadata:
+      contexts:
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user.permissions
+      max-age: -1
+      tags: {  }
+  page_1:
+    id: page_1
+    display_title: Page
+    display_plugin: page
+    position: 2
+    display_options:
+      query:
+        type: views_query
+        options: {  }
+      path: test-summary
+      display_extenders: {  }
+    cache_metadata:
+      contexts:
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      max-age: -1
+      tags: {  }
diff --git a/core/modules/views/tests/src/Unit/Plugin/argument_default/QueryParameterTest.php b/core/modules/views/tests/src/Unit/Plugin/argument_default/QueryParameterTest.php
index 0df15f2ca..f19e20b3b 100644
--- a/core/modules/views/tests/src/Unit/Plugin/argument_default/QueryParameterTest.php
+++ b/core/modules/views/tests/src/Unit/Plugin/argument_default/QueryParameterTest.php
@@ -48,41 +48,33 @@ class QueryParameterTest extends UnitTestCase {
    *   - third entry: the expected default argument value.
    */
   public function providerGetArgument() {
-    $data = array();
+    $data = [];
 
-    $single[] = array(
-      'query_param' => 'test',
-    );
-    $single[] = new Request(array('test' => 'data'));
-    $single[] = 'data';
-    $data[] = $single;
+    $data[] = [
+      ['query_param' => 'test'],
+      new Request(['test' => 'data']),
+      'data',
+    ];
 
-    $single[] = array(
-      'query_param' => 'test',
-      'multiple' => 'AND'
-    );
-    $single[] = new Request(array('test' => array('data1', 'data2')));
-    $single[] = 'data1+data2';
-    $data[] = $single;
+    $data[] = [
+      ['query_param' => 'test', 'multiple' => 'and'],
+      new Request(['test' => ['data1', 'data2']]),
+      'data1,data2',
+    ];
 
-    $single[] = array(
-      'query_param' => 'test',
-      'multiple' => 'OR'
-    );
-    $single[] = new Request(array('test' => array('data1', 'data2')));
-    $single[] = 'data1,data2';
-    $data[] = $single;
+    $data[] = [
+      ['query_param' => 'test', 'multiple' => 'or'],
+      new Request(['test' => ['data1', 'data2']]),
+      'data1+data2',
+    ];
 
-    $single[] = array(
-      'query_param' => 'test',
-      'fallback' => 'blub',
-    );
-    $single[] = new Request(array());
-    $single[] = 'blub';
-    $data[] = $single;
+    $data[] = [
+      ['query_param' => 'test', 'fallback' => 'blub'],
+      new Request([]),
+      'blub',
+    ];
 
     return $data;
   }
 
 }
-
diff --git a/core/modules/views/tests/src/Unit/Plugin/argument_default/RawTest.php b/core/modules/views/tests/src/Unit/Plugin/argument_default/RawTest.php
index f50b928f4..f33cbaa2f 100644
--- a/core/modules/views/tests/src/Unit/Plugin/argument_default/RawTest.php
+++ b/core/modules/views/tests/src/Unit/Plugin/argument_default/RawTest.php
@@ -42,7 +42,15 @@ class RawTest extends UnitTestCase {
     $alias_manager->expects($this->never())
       ->method('getAliasByPath');
 
-    // Don't use aliases.
+    // Don't use aliases. Check against NULL and nonexistent path component
+    // values in addition to valid ones.
+    $raw = new Raw(array(), 'raw', array(), $alias_manager, $current_path);
+    $options = array(
+      'use_alias' => FALSE,
+    );
+    $raw->init($view, $display_plugin, $options);
+    $this->assertEquals(NULL, $raw->getArgument());
+
     $raw = new Raw(array(), 'raw', array(), $alias_manager, $current_path);
     $options = array(
       'use_alias' => FALSE,
@@ -59,12 +67,27 @@ class RawTest extends UnitTestCase {
     $raw->init($view, $display_plugin, $options);
     $this->assertEquals('example', $raw->getArgument());
 
+    $raw = new Raw(array(), 'raw', array(), $alias_manager, $current_path);
+    $options = array(
+      'use_alias' => FALSE,
+      'index' => 2,
+    );
+    $raw->init($view, $display_plugin, $options);
+    $this->assertEquals(NULL, $raw->getArgument());
+
     // Setup an alias manager with a path alias.
     $alias_manager = $this->getMock('Drupal\Core\Path\AliasManagerInterface');
     $alias_manager->expects($this->any())
       ->method('getAliasByPath')
-      ->with($this->equalTo('test/example'))
-      ->will($this->returnValue('other/example'));
+      ->with($this->equalTo('/test/example'))
+      ->will($this->returnValue('/other/example'));
+
+    $raw = new Raw(array(), 'raw', array(), $alias_manager, $current_path);
+    $options = array(
+      'use_alias' => TRUE,
+    );
+    $raw->init($view, $display_plugin, $options);
+    $this->assertEquals(NULL, $raw->getArgument());
 
     $raw = new Raw(array(), 'raw', array(), $alias_manager, $current_path);
     $options = array(
@@ -82,6 +105,13 @@ class RawTest extends UnitTestCase {
     $raw->init($view, $display_plugin, $options);
     $this->assertEquals('example', $raw->getArgument());
 
+    $raw = new Raw(array(), 'raw', array(), $alias_manager, $current_path);
+    $options = array(
+      'use_alias' => TRUE,
+      'index' => 2,
+    );
+    $raw->init($view, $display_plugin, $options);
+    $this->assertEquals(NULL, $raw->getArgument());
   }
 
 }
diff --git a/core/modules/views/tests/src/Unit/ViewExecutableTest.php b/core/modules/views/tests/src/Unit/ViewExecutableTest.php
index fea412b63..049df8d9e 100644
--- a/core/modules/views/tests/src/Unit/ViewExecutableTest.php
+++ b/core/modules/views/tests/src/Unit/ViewExecutableTest.php
@@ -469,4 +469,100 @@ class ViewExecutableTest extends UnitTestCase {
     return array($view, $display);
   }
 
+  /**
+   * @covers ::setItemsPerPage
+   * @covers ::getItemsPerPage
+   */
+  public function testSetItemsPerPageBeforePreRender() {
+    /** @var \Drupal\views\ViewExecutable|\PHPUnit_Framework_MockObject_MockObject $view */
+    /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase|\PHPUnit_Framework_MockObject_MockObject $display */
+    list($view, $display) = $this->setupBaseViewAndDisplay();
+
+    $view->setItemsPerPage(12);
+    $this->assertEquals(12, $view->getItemsPerPage());
+    $this->assertContains('items_per_page:12', $view->element['#cache']['keys']);
+  }
+
+  /**
+   * @covers ::setItemsPerPage
+   * @covers ::getItemsPerPage
+   */
+  public function testSetItemsPerPageDuringPreRender() {
+    /** @var \Drupal\views\ViewExecutable|\PHPUnit_Framework_MockObject_MockObject $view */
+    /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase|\PHPUnit_Framework_MockObject_MockObject $display */
+    list($view, $display) = $this->setupBaseViewAndDisplay();
+
+    $elements = &$view->element;
+    $elements['#cache'] += ['keys' => []];
+    $elements['#pre_rendered'] = TRUE;
+
+    $view->setItemsPerPage(12);
+    $this->assertEquals(12, $view->getItemsPerPage());
+    $this->assertNotContains('items_per_page:12', $view->element['#cache']['keys']);
+  }
+
+  /**
+   * @covers ::setOffset
+   * @covers ::getOffset
+   */
+  public function testSetOffsetBeforePreRender() {
+    /** @var \Drupal\views\ViewExecutable|\PHPUnit_Framework_MockObject_MockObject $view */
+    /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase|\PHPUnit_Framework_MockObject_MockObject $display */
+    list($view, $display) = $this->setupBaseViewAndDisplay();
+
+    $view->setOffset(12);
+    $this->assertEquals(12, $view->getOffset());
+    $this->assertContains('offset:12', $view->element['#cache']['keys']);
+  }
+
+  /**
+   * @covers ::setOffset
+   * @covers ::getOffset
+   */
+  public function testSetOffsetDuringPreRender() {
+    /** @var \Drupal\views\ViewExecutable|\PHPUnit_Framework_MockObject_MockObject $view */
+    /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase|\PHPUnit_Framework_MockObject_MockObject $display */
+    list($view, $display) = $this->setupBaseViewAndDisplay();
+
+    $elements = &$view->element;
+    $elements['#cache'] += ['keys' => []];
+    $elements['#pre_rendered'] = TRUE;
+
+    $view->setOffset(12);
+    $this->assertEquals(12, $view->getOffset());
+    $this->assertNotContains('offset:12', $view->element['#cache']['keys']);
+  }
+
+  /**
+   * @covers ::setCurrentPage
+   * @covers ::getCurrentPage
+   */
+  public function testSetCurrentPageBeforePreRender() {
+    /** @var \Drupal\views\ViewExecutable|\PHPUnit_Framework_MockObject_MockObject $view */
+    /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase|\PHPUnit_Framework_MockObject_MockObject $display */
+    list($view, $display) = $this->setupBaseViewAndDisplay();
+
+    $view->setCurrentPage(12);
+    $this->assertEquals(12, $view->getCurrentPage());
+    $this->assertContains('page:12', $view->element['#cache']['keys']);
+  }
+
+  /**
+   * @covers ::setCurrentPage
+   * @covers ::getCurrentPage
+   */
+  public function testSetCurrentPageDuringPreRender() {
+    /** @var \Drupal\views\ViewExecutable|\PHPUnit_Framework_MockObject_MockObject $view */
+    /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase|\PHPUnit_Framework_MockObject_MockObject $display */
+    list($view, $display) = $this->setupBaseViewAndDisplay();
+
+    $elements = &$view->element;
+    $elements['#cache'] += ['keys' => []];
+    $elements['#pre_rendered'] = TRUE;
+
+    $view->setCurrentPage(12);
+    $this->assertEquals(12, $view->getCurrentPage());
+    $this->assertNotContains('page:12', $view->element['#cache']['keys']);
+  }
+
 }
diff --git a/core/modules/views_ui/src/ViewPreviewForm.php b/core/modules/views_ui/src/ViewPreviewForm.php
index 7ec8f4de1..d87f593d1 100644
--- a/core/modules/views_ui/src/ViewPreviewForm.php
+++ b/core/modules/views_ui/src/ViewPreviewForm.php
@@ -94,6 +94,7 @@ class ViewPreviewForm extends ViewFormBase {
           'event' => 'click',
           'progress' => array('type' => 'fullscreen'),
           'method' => 'replaceWith',
+          'disable-refocus' => TRUE,
         ),
       ),
     );
diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php
index 47b8e88fe..fe3198b26 100644
--- a/core/modules/views_ui/src/ViewUI.php
+++ b/core/modules/views_ui/src/ViewUI.php
@@ -1031,6 +1031,13 @@ class ViewUI implements ViewEntityInterface {
     return $this->storage->urlInfo($rel, $options);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function toUrl($rel = 'edit-form', array $options = []) {
+    return $this->storage->toUrl($rel, $options);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -1038,6 +1045,13 @@ class ViewUI implements ViewEntityInterface {
     return $this->storage->link($text, $rel, $options);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function toLink($text = NULL, $rel = 'edit-form', array $options = []) {
+    return $this->storage->toLink($text, $rel, $options);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/profiles/standard/config/install/block.block.classy_page_title.yml b/core/profiles/standard/config/install/block.block.classy_page_title.yml
deleted file mode 100644
index 42362242b..000000000
--- a/core/profiles/standard/config/install/block.block.classy_page_title.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  theme:
-    - classy
-id: classy_page_title
-theme: classy
-region: content
-weight: -50
-provider: null
-plugin: page_title_block
-settings:
-  id: page_title_block
-  label: 'Page title'
-  provider: core
-  label_display: '0'
-visibility: {  }
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 9ca309f48..bf46a4018 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -12,6 +12,7 @@ use Drupal\Core\Database\Database;
 use Drupal\Core\StreamWrapper\PublicStream;
 use Drupal\Core\Test\TestRunnerKernel;
 use Drupal\simpletest\Form\SimpletestResultsForm;
+use Drupal\simpletest\TestBase;
 use Symfony\Component\HttpFoundation\Request;
 
 $autoloader = require_once __DIR__ . '/../../autoload.php';
@@ -23,46 +24,68 @@ const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; // Brown.
 // Restricting the chunk of queries prevents memory exhaustion.
 const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
 
+const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
+const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
+const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;
+
 // Set defaults and get overrides.
 list($args, $count) = simpletest_script_parse_args();
 
 if ($args['help'] || $count == 0) {
   simpletest_script_help();
-  exit;
+  exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);
 }
 
 simpletest_script_init();
 
-$request = Request::createFromGlobals();
-$kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
-$kernel->prepareLegacyRequest($request);
+try {
+  $request = Request::createFromGlobals();
+  $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
+  $kernel->prepareLegacyRequest($request);
+}
+catch (Exception $e) {
+  echo (string) $e;
+  exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+}
 
 if ($args['execute-test']) {
   simpletest_script_setup_database();
   simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
   // Sub-process exited already; this is just for clarity.
-  exit;
+  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
 }
 
 if ($args['list']) {
   // Display all available tests.
   echo "\nAvailable test groups & classes\n";
   echo   "-------------------------------\n\n";
-  $groups = simpletest_test_get_all($args['module']);
+  try {
+    $groups = simpletest_test_get_all($args['module']);
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
   foreach ($groups as $group => $tests) {
     echo $group . "\n";
     foreach ($tests as $class => $info) {
       echo " - $class\n";
     }
   }
-  exit;
+  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
 }
 
 simpletest_script_setup_database(TRUE);
 
 if ($args['clean']) {
   // Clean up left-over tables and directories.
-  simpletest_clean_environment();
+  try {
+    simpletest_clean_environment();
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
   echo "\nEnvironment cleaned.\n";
 
   // Get the status messages and print them.
@@ -70,7 +93,7 @@ if ($args['clean']) {
   foreach ($messages['status'] as $text) {
     echo " - " . $text . "\n";
   }
-  exit;
+  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
 }
 
 $test_list = simpletest_script_get_test_list();
@@ -85,7 +108,7 @@ for ($i = 0; $i < $args['repeat']; $i++) {
 }
 
 // Execute tests.
-simpletest_script_execute_batch($tests_to_run);
+$status = simpletest_script_execute_batch($tests_to_run);
 
 // Stop the timer.
 simpletest_script_reporter_timer_stop();
@@ -104,11 +127,17 @@ if ($args['xml']) {
 
 // Clean up all test results.
 if (!$args['keep-results']) {
-  simpletest_clean_results_table();
+  try {
+    simpletest_clean_results_table();
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
 }
 
 // Test complete, exit.
-exit;
+exit($status);
 
 /**
  * Print help text.
@@ -295,7 +324,7 @@ function simpletest_script_parse_args() {
       else {
         // Argument not found in list.
         simpletest_script_print_error("Unknown argument '$arg'.");
-        exit;
+        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
       }
     }
     else {
@@ -308,7 +337,7 @@ function simpletest_script_parse_args() {
   // Validate the concurrency argument
   if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
     simpletest_script_print_error("--concurrency must be a strictly positive integer.");
-    exit;
+    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
   }
 
   if ($args['browser']) {
@@ -343,7 +372,7 @@ function simpletest_script_init() {
   else {
     simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
     simpletest_script_help();
-    exit();
+    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
   }
 
   // Get URL from arguments.
@@ -441,7 +470,7 @@ function simpletest_script_setup_database($new = FALSE) {
     }
     catch (\InvalidArgumentException $e) {
       simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
-      exit(1);
+      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
     }
   }
   // Otherwise, use the default database connection from settings.php.
@@ -452,7 +481,7 @@ function simpletest_script_setup_database($new = FALSE) {
   // If there is no default database connection for tests, we cannot continue.
   if (!isset($databases['default']['default'])) {
     simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
-    exit(1);
+    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
   }
   Database::addConnectionInfo('default', 'default', $databases['default']['default']);
 
@@ -496,21 +525,33 @@ function simpletest_script_setup_database($new = FALSE) {
   }
   catch (\PDOException $e) {
     simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
-    exit(1);
+    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
   }
   if ($new && $sqlite) {
     require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
     foreach (simpletest_schema() as $name => $table_spec) {
-      if ($schema->tableExists($name)) {
-        $schema->dropTable($name);
+      try {
+        if ($schema->tableExists($name)) {
+          $schema->dropTable($name);
+        }
+        $schema->createTable($name, $table_spec);
+      }
+      catch (Exception $e) {
+        echo (string) $e;
+        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
       }
-      $schema->createTable($name, $table_spec);
     }
   }
   // Verify that the Simpletest database schema exists by checking one table.
-  if (!$schema->tableExists('simpletest')) {
-    simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
-    exit(1);
+  try {
+    if (!$schema->tableExists('simpletest')) {
+      simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
+      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+    }
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
   }
 }
 
@@ -520,6 +561,8 @@ function simpletest_script_setup_database($new = FALSE) {
 function simpletest_script_execute_batch($test_classes) {
   global $args, $test_ids;
 
+  $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
+
   // Multi-process execution.
   $children = array();
   while (!empty($test_classes) || !empty($children)) {
@@ -528,8 +571,16 @@ function simpletest_script_execute_batch($test_classes) {
         break;
       }
 
-      $test_id = Database::getConnection('default', 'test-runner')
-        ->insert('simpletest_test_id')->useDefaults(array('test_id'))->execute();
+      try {
+        $test_id = Database::getConnection('default', 'test-runner')
+          ->insert('simpletest_test_id')
+          ->useDefaults(array('test_id'))
+          ->execute();
+      }
+      catch (Exception $e) {
+        echo (string) $e;
+        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+      }
       $test_ids[] = $test_id;
 
       $test_class = array_shift($test_classes);
@@ -539,7 +590,7 @@ function simpletest_script_execute_batch($test_classes) {
 
       if (!is_resource($process)) {
         echo "Unable to fork test process. Aborting.\n";
-        exit;
+        exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
       }
 
       // Register our new child.
@@ -560,8 +611,16 @@ function simpletest_script_execute_batch($test_classes) {
       if (empty($status['running'])) {
         // The child exited, unregister it.
         proc_close($child['process']);
-        if ($status['exitcode']) {
-          echo 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
+        if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
+          $total_status = max($status['exitcode'], $total_status);
+        }
+        elseif ($status['exitcode']) {
+          $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').';
+          echo $message . "\n";
+          // Insert a fail for xml results.
+          TestBase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
+          /// Ensure that an error line is displayed for the class.
+          simpletest_script_reporter_display_summary($child['class'], ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]);
           if ($args['die-on-fail']) {
             list($db_prefix, ) = simpletest_last_test_get($child['test_id']);
             $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
@@ -581,19 +640,19 @@ function simpletest_script_execute_batch($test_classes) {
       }
     }
   }
+  return $total_status;
 }
 
 /**
  * Run a group of phpunit tests.
  */
 function simpletest_script_run_phpunit($test_id, $class) {
-
   $reflection = new \ReflectionClass($class);
   if ($reflection->hasProperty('runLimit')) {
     set_time_limit($reflection->getStaticPropertyValue('runLimit'));
   }
 
-  $results = simpletest_run_phpunit_tests($test_id, array($class));
+  $results = simpletest_run_phpunit_tests($test_id, array($class), $status);
   simpletest_process_phpunit_results($results);
 
   // Map phpunit results to a data structure we can pass to
@@ -628,6 +687,7 @@ function simpletest_script_run_phpunit($test_id, $class) {
   foreach ($summaries as $class => $summary) {
     simpletest_script_reporter_display_summary($class, $summary);
   }
+  return $status;
 }
 
 /**
@@ -648,23 +708,28 @@ function simpletest_script_run_one_test($test_id, $test_class) {
     }
     $test = new $class_name($test_id);
     if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
-      simpletest_script_run_phpunit($test_id, $test_class);
+      $status = simpletest_script_run_phpunit($test_id, $test_class);
     }
     else {
       $test->dieOnFail = (bool) $args['die-on-fail'];
       $test->verbose = (bool) $args['verbose'];
       $test->run($methods);
       simpletest_script_reporter_display_summary($test_class, $test->results);
+
+      $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
+      // Finished, kill this runner.
+      if ($test->results['#fail'] || $test->results['#exception']) {
+        $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
+      }
     }
 
-    // Finished, kill this runner.
-    exit(0);
+    exit($status);
   }
   // DrupalTestCase::run() catches exceptions already, so this is only reached
   // when an exception is thrown in the wrapping test runner environment.
   catch (Exception $e) {
     echo (string) $e;
-    exit(1);
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
   }
 }
 
@@ -726,7 +791,13 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
     return;
   }
   // Retrieve the last database prefix used for testing.
-  list($db_prefix, ) = simpletest_last_test_get($test_id);
+  try {
+    list($db_prefix,) = simpletest_last_test_get($test_id);
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
 
   // If no database prefix was found, then the test was not set up correctly.
   if (empty($db_prefix)) {
@@ -741,7 +812,13 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
   $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
 
   // Read the log file in case any fatal errors caused the test to crash.
-  simpletest_log_read($test_id, $db_prefix, $test_class);
+  try {
+    simpletest_log_read($test_id, $db_prefix, $test_class);
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
 
   // Check whether a test site directory was setup already.
   // @see \Drupal\simpletest\TestBase::prepareEnvironment()
@@ -763,12 +840,19 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
   }
 
   // Clear out all database tables from the test.
-  $schema = Database::getConnection('default', 'default')->schema();
-  $count = 0;
-  foreach ($schema->findTables($db_prefix . '%') as $table) {
-    $schema->dropTable($table);
-    $count++;
+  try {
+    $schema = Database::getConnection('default', 'default')->schema();
+    $count = 0;
+    foreach ($schema->findTables($db_prefix . '%') as $table) {
+      $schema->dropTable($table);
+      $count++;
+    }
   }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
+
   if ($count) {
     $messages[] = "- Removed $count leftover tables.";
   }
@@ -792,7 +876,13 @@ function simpletest_script_get_test_list() {
 
   $test_list = array();
   if ($args['all'] || $args['module']) {
-    $groups = simpletest_test_get_all($args['module']);
+    try {
+      $groups = simpletest_test_get_all($args['module']);
+    }
+    catch (Exception $e) {
+      echo (string) $e;
+      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
     $all_tests = array();
     foreach ($groups as $group => $tests) {
       $all_tests = array_merge($all_tests, array_keys($tests));
@@ -808,14 +898,20 @@ function simpletest_script_get_test_list() {
           $test_list[] = $test_class;
         }
         else {
-          $groups = simpletest_test_get_all();
+          try {
+            $groups = simpletest_test_get_all();
+          }
+          catch (Exception $e) {
+            echo (string) $e;
+            exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+          }
           $all_classes = array();
           foreach ($groups as $group) {
             $all_classes = array_merge($all_classes, array_keys($group));
           }
           simpletest_script_print_error('Test class not found: ' . $class_name);
           simpletest_script_print_alternatives($class_name, $all_classes, 6);
-          exit(1);
+          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
         }
       }
     }
@@ -824,7 +920,7 @@ function simpletest_script_get_test_list() {
       foreach ($args['test_names'] as $file) {
         if (!file_exists($file)) {
           simpletest_script_print_error('File not found: ' . $file);
-          exit;
+          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
         }
         $content = file_get_contents($file);
         // Extract a potential namespace.
@@ -903,7 +999,13 @@ function simpletest_script_get_test_list() {
       }
     }
     else {
-      $groups = simpletest_test_get_all();
+      try {
+        $groups = simpletest_test_get_all();
+      }
+      catch (Exception $e) {
+        echo (string) $e;
+        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+      }
       foreach ($args['test_names'] as $group_name) {
         if (isset($groups[$group_name])) {
           $test_list = array_merge($test_list, array_keys($groups[$group_name]));
@@ -911,7 +1013,7 @@ function simpletest_script_get_test_list() {
         else {
           simpletest_script_print_error('Test group not found: ' . $group_name);
           simpletest_script_print_alternatives($group_name, array_keys($groups));
-          exit(1);
+          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
         }
       }
     }
@@ -919,7 +1021,7 @@ function simpletest_script_get_test_list() {
 
   if (empty($test_list)) {
     simpletest_script_print_error('No valid tests were specified.');
-    exit;
+    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
   }
   return $test_list;
 }
@@ -993,7 +1095,13 @@ function simpletest_script_reporter_display_summary($class, $results) {
 function simpletest_script_reporter_write_xml_results() {
   global $args, $test_ids, $results_map;
 
-  $results = simpletest_script_load_messages_by_test_id($test_ids);
+  try {
+    $results = simpletest_script_load_messages_by_test_id($test_ids);
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
 
   $test_class = '';
   $xml_files = array();
@@ -1083,7 +1191,13 @@ function simpletest_script_reporter_display_results() {
     echo "Detailed test results\n";
     echo "---------------------\n";
 
-    $results = simpletest_script_load_messages_by_test_id($test_ids);
+    try {
+      $results = simpletest_script_load_messages_by_test_id($test_ids);
+    }
+    catch (Exception $e) {
+      echo (string) $e;
+      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
     $test_class = '';
     foreach ($results as $result) {
       if (isset($results_map[$result->status])) {
@@ -1231,10 +1345,16 @@ function simpletest_script_load_messages_by_test_id($test_ids) {
   }
 
   foreach ($test_id_chunks as $test_id_chunk) {
-    $result_chunk = Database::getConnection('default', 'test-runner')
-      ->query("SELECT * FROM {simpletest} WHERE test_id IN ( :test_ids[] ) ORDER BY test_class, message_id", array(
-        ':test_ids[]' => $test_id_chunk,
-      ))->fetchAll();
+    try {
+      $result_chunk = Database::getConnection('default', 'test-runner')
+        ->query("SELECT * FROM {simpletest} WHERE test_id IN ( :test_ids[] ) ORDER BY test_class, message_id", array(
+          ':test_ids[]' => $test_id_chunk,
+        ))->fetchAll();
+    }
+    catch (Exception $e) {
+      echo (string) $e;
+      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
     if ($result_chunk) {
       $results = array_merge($results, $result_chunk);
     }
@@ -1249,14 +1369,20 @@ function simpletest_script_load_messages_by_test_id($test_ids) {
 function simpletest_script_open_browser() {
   global $test_ids;
 
-  $connection = Database::getConnection('default', 'test-runner');
-  $results = $connection->select('simpletest')
-    ->fields('simpletest')
-    ->condition('test_id', $test_ids, 'IN')
-    ->orderBy('test_class')
-    ->orderBy('message_id')
-    ->execute()
-    ->fetchAll();
+  try {
+    $connection = Database::getConnection('default', 'test-runner');
+    $results = $connection->select('simpletest')
+      ->fields('simpletest')
+      ->condition('test_id', $test_ids, 'IN')
+      ->orderBy('test_class')
+      ->orderBy('message_id')
+      ->execute()
+      ->fetchAll();
+  }
+  catch (Exception $e) {
+    echo (string) $e;
+    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+  }
 
   // Get the results form.
   $form = array();
diff --git a/core/tests/Drupal/KernelTests/Core/Cache/CacheCollectorTest.php b/core/tests/Drupal/KernelTests/Core/Cache/CacheCollectorTest.php
new file mode 100644
index 000000000..e9ca67a0a
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Cache/CacheCollectorTest.php
@@ -0,0 +1,79 @@
+installSchema('system', ['semaphore']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    parent::register($container);
+    // Change container to database cache backends.
+    $container
+      ->register('cache_factory', 'Drupal\Core\Cache\CacheFactory')
+      ->addArgument(new Reference('settings'))
+      ->addMethodCall('setContainer', [new Reference('service_container')]);
+
+    // Change container to use database lock backends.
+    $container
+      ->register('lock', 'Drupal\Core\Lock\DatabaseLockBackend')
+      ->addArgument(new Reference('database'));
+  }
+
+  /**
+   * Tests setting and invalidating
+   *
+   * @dataProvider providerTestInvalidCharacters
+   */
+  public function testCacheCollector($cid, $key, $value) {
+    $collector = new CacheCollectorHelper($cid, $this->container->get('cache.default'), $this->container->get('lock'));
+    $this->assertNull($collector->get($key));
+    $collector->set($key, $value);
+    $this->assertEquals($value, $collector->get($key));
+    $collector->destruct();
+    // @todo Shouldn't this be empty after destruction?
+    $this->assertEquals($value, $collector->get($key));
+  }
+
+  /**
+   * Data provider for ::testCacheCollector().
+   */
+  public function providerTestInvalidCharacters() {
+    return [
+      // Nothing special.
+      ['foo', 'bar', 'baz'],
+      // Invalid characters in CID.
+      ['éøïвβ中國書۞', 'foo', 'bar'],
+      // Really long CID.
+      [$this->randomString(1024), 'foo', 'bar'],
+    ];
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php
new file mode 100644
index 000000000..98180e3a3
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php
@@ -0,0 +1,86 @@
+installSchema('system', 'url_alias');
+    $this->storage = $this->container->get('path.alias_storage');
+  }
+
+  /**
+   * @covers ::load
+   */
+  public function testLoad() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+
+    $expected = [
+      'pid' => 1,
+      'alias' => '/test-alias-Case',
+      'source' => '/test-source-Case',
+      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+    ];
+
+    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-Case']));
+    $this->assertEquals($expected, $this->storage->load(['alias' => '/test-alias-case']));
+    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-Case']));
+    $this->assertEquals($expected, $this->storage->load(['source' => '/test-source-case']));
+  }
+
+  /**
+   * @covers ::lookupPathAlias
+   */
+  public function testLookupPathAlias() {
+    $this->storage->save('/test-source-Case', '/test-alias');
+
+    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertEquals('/test-alias', $this->storage->lookupPathAlias('/test-source-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::lookupPathSource
+   */
+  public function testLookupPathSource() {
+    $this->storage->save('/test-source', '/test-alias-Case');
+
+    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertEquals('/test-source', $this->storage->lookupPathSource('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+  /**
+   * @covers ::aliasExists
+   */
+  public function testAliasExists() {
+    $this->storage->save('/test-source-Case', '/test-alias-Case');
+
+    $this->assertTrue($this->storage->aliasExists('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->assertTrue($this->storage->aliasExists('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED));
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/MaintenanceThemeTest.php b/core/tests/Drupal/KernelTests/Core/Theme/MaintenanceThemeTest.php
new file mode 100644
index 000000000..7cadffa25
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Theme/MaintenanceThemeTest.php
@@ -0,0 +1,43 @@
+setSetting('maintenance_theme', 'seven');
+    // Get the maintenance theme loaded.
+    drupal_maintenance_theme();
+
+    // Do we have an active theme?
+    $this->assertTrue(\Drupal::theme()->hasActiveTheme());
+
+    $active_theme = \Drupal::theme()->getActiveTheme();
+    $this->assertEquals('seven', $active_theme->getName());
+
+    $base_themes = $active_theme->getBaseThemes();
+    $base_theme_names = array_keys($base_themes);
+    $this->assertSame(['classy', 'stable'], $base_theme_names);
+
+    // Ensure Classy has the correct base themes and amount of base themes.
+    $classy_base_themes = $base_themes['classy']->getBaseThemes();
+    $classy_base_theme_names = array_keys($classy_base_themes);
+    $this->assertSame(['stable'], $classy_base_theme_names);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php
new file mode 100644
index 000000000..69d9e8c61
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php
@@ -0,0 +1,108 @@
+themeHandler = $this->container->get('theme_handler');
+
+    $this->container->get('theme_installer')->install(['stable']);
+
+    $this->installSchema('system', 'router');
+    $this->installAllModules();
+  }
+
+  /**
+   * Installs all core modules.
+   */
+  protected function installAllModules() {
+    // Needed for system_rebuild_module_data().
+    include_once $this->root . '/core/modules/system/system.module';
+
+    // Enable all core modules.
+    $all_modules = system_rebuild_module_data();
+    $all_modules = array_filter($all_modules, function ($module) {
+      // Filter contrib, hidden, already enabled modules and modules in the
+      // Testing package.
+      if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') {
+        return FALSE;
+      }
+      return TRUE;
+    });
+    $this->allModules = array_keys($all_modules);
+    sort($this->allModules);
+
+    $module_installer = $this->container->get('module_installer');
+    $module_installer->install($this->allModules);
+
+    $this->installConfig(['system', 'user']);
+  }
+
+  /**
+   * Ensures that Stable overrides all relevant core templates.
+   */
+  public function testStableTemplateOverrides() {
+    $registry = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $this->themeHandler, \Drupal::service('theme.initialization'), 'stable');
+    $registry->setThemeManager(\Drupal::theme());
+
+    $registry_full = $registry->get();
+
+    foreach ($registry_full as $hook => $info) {
+      if (isset($info['template'])) {
+        // Allow skipping templates.
+        if (in_array($info['template'], $this->templatesToSkip)) {
+          continue;
+        }
+
+        $this->assertEquals('core/themes/stable', $info['theme path'], $info['template'] . '.html.twig overridden in Stable.');
+      }
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/RequestProcessing/RedirectOnExceptionTest.php b/core/tests/Drupal/KernelTests/RequestProcessing/RedirectOnExceptionTest.php
new file mode 100644
index 000000000..4c7a6860f
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/RequestProcessing/RedirectOnExceptionTest.php
@@ -0,0 +1,50 @@
+installSchema('system', ['router', 'url_alias']);
+    \Drupal::service('router.builder')->rebuild();
+  }
+
+  public function testRedirectOn404() {
+    \Drupal::configFactory()->getEditable('system.site')
+      ->set('page.404', '/test-http-response-exception/' . Response::HTTP_PERMANENTLY_REDIRECT)
+      ->save();
+
+    /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */
+    $http_kernel = \Drupal::service('http_kernel');
+
+    // Foo doesn't exist, so this triggers the 404 page.
+    $request = Request::create('/foo');
+    $response = $http_kernel->handle($request);
+    $this->assertEquals(Response::HTTP_PERMANENTLY_REDIRECT, $response->getStatusCode());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php
new file mode 100644
index 000000000..8b1b3399f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php
@@ -0,0 +1,168 @@
+libraryDiscovery = $this->getMockBuilder('Drupal\Core\Asset\LibraryDiscovery')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->libraryDependencyResolver = $this->getMock('\Drupal\Core\Asset\LibraryDependencyResolverInterface');
+    $this->libraryDependencyResolver->expects($this->any())
+      ->method('getLibrariesWithDependencies')
+      ->willReturnArgument(0);
+    $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->themeManager = $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface');
+    $active_theme = $this->getMockBuilder('\Drupal\Core\Theme\ActiveTheme')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $active_theme->expects($this->any())
+      ->method('getName')
+      ->willReturn('bartik');
+    $this->themeManager->expects($this->any())
+      ->method('getActiveTheme')
+      ->willReturn($active_theme);
+
+    $this->languageManager = $this->getMock('\Drupal\Core\Language\LanguageManagerInterface');
+    $english = $this->getMock('\Drupal\Core\Language\LanguageInterface');
+    $english->expects($this->any())
+      ->method('getId')
+      ->willReturn('en');
+    $japanese = $this->getMock('\Drupal\Core\Language\LanguageInterface');
+    $japanese->expects($this->any())
+      ->method('getId')
+      ->willReturn('jp');
+    $this->languageManager = $this->getMock('\Drupal\Core\Language\LanguageManagerInterface');
+    $this->languageManager->expects($this->any())
+      ->method('getCurrentLanguage')
+      ->will($this->onConsecutiveCalls($english, $english, $japanese, $japanese));
+    $this->cache = new TestMemoryBackend('llama');
+
+    $this->assetResolver = new AssetResolver($this->libraryDiscovery, $this->libraryDependencyResolver, $this->moduleHandler, $this->themeManager, $this->languageManager, $this->cache);
+  }
+
+  /**
+   * @covers ::getCssAssets
+   * @dataProvider providerAttachedAssets
+   */
+  public function testGetCssAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) {
+    $this->assetResolver->getCssAssets($assets_a, FALSE);
+    $this->assetResolver->getCssAssets($assets_b, FALSE);
+    $this->assertCount($expected_cache_item_count, $this->cache->getAllCids());
+  }
+
+  /**
+   * @covers ::getJsAssets
+   * @dataProvider providerAttachedAssets
+   */
+  public function testGetJsAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) {
+    $this->assetResolver->getJsAssets($assets_a, FALSE);
+    $this->assetResolver->getJsAssets($assets_b, FALSE);
+    $this->assertCount($expected_cache_item_count, $this->cache->getAllCids());
+
+    $this->assetResolver->getJsAssets($assets_a, FALSE);
+    $this->assetResolver->getJsAssets($assets_b, FALSE);
+    $this->assertCount($expected_cache_item_count * 2, $this->cache->getAllCids());
+  }
+
+  public function providerAttachedAssets() {
+    $time = time();
+    return [
+      'same libraries, different timestamps' => [
+        (new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal'])->setSettings(['currentTime' => $time]),
+        (new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal'])->setSettings(['currentTime' => $time + 100]),
+        1
+      ],
+      'different libraries, same timestamps' => [
+        (new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal'])->setSettings(['currenttime' => $time]),
+        (new AttachedAssets())->setAlreadyLoadedLibraries([])->setLibraries(['core/drupal', 'core/jquery'])->setSettings(['currentTime' => $time]),
+        2
+      ],
+    ];
+  }
+
+}
+
+if (!defined('CSS_AGGREGATE_DEFAULT')) {
+  define('CSS_AGGREGATE_DEFAULT', 0);
+}
+
+if (!defined('JS_DEFAULT')) {
+  define('JS_DEFAULT', 0);
+}
+
+class TestMemoryBackend extends MemoryBackend {
+  public function getAllCids() {
+    return array_keys($this->cache);
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
index 2f5ed21ba..ed0c9ceec 100644
--- a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
@@ -10,20 +10,6 @@ namespace Drupal\Tests\Core\Asset;
 use Drupal\Core\Asset\LibraryDiscoveryParser;
 use Drupal\Tests\UnitTestCase;
 
-if (!defined('CSS_AGGREGATE_DEFAULT')) {
-  define('CSS_AGGREGATE_DEFAULT', 0);
-  define('CSS_AGGREGATE_THEME', 100);
-  define('CSS_BASE', -200);
-  define('CSS_LAYOUT', -100);
-  define('CSS_COMPONENT', 0);
-  define('CSS_STATE', 100);
-  define('CSS_THEME', 200);
-  define('JS_SETTING', -200);
-  define('JS_LIBRARY', -100);
-  define('JS_DEFAULT', 0);
-  define('JS_THEME', 100);
-}
-
 /**
  * @coversDefaultClass \Drupal\Core\Asset\LibraryDiscoveryParser
  * @group Asset
@@ -575,3 +561,37 @@ class TestLibraryDiscoveryParser extends LibraryDiscoveryParser {
   }
 
 }
+
+if (!defined('CSS_AGGREGATE_DEFAULT')) {
+  define('CSS_AGGREGATE_DEFAULT', 0);
+}
+if (!defined('CSS_AGGREGATE_THEME')) {
+  define('CSS_AGGREGATE_THEME', 100);
+}
+if (!defined('CSS_BASE')) {
+  define('CSS_BASE', -200);
+}
+if (!defined('CSS_LAYOUT')) {
+  define('CSS_LAYOUT', -100);
+}
+if (!defined('CSS_COMPONENT')) {
+  define('CSS_COMPONENT', 0);
+}
+if (!defined('CSS_STATE')) {
+  define('CSS_STATE', 100);
+}
+if (!defined('CSS_THEME')) {
+  define('CSS_THEME', 200);
+}
+if (!defined('JS_SETTING')) {
+  define('JS_SETTING', -200);
+}
+if (!defined('JS_LIBRARY')) {
+  define('JS_LIBRARY', -100);
+}
+if (!defined('JS_DEFAULT')) {
+  define('JS_DEFAULT', 0);
+}
+if (!defined('JS_THEME')) {
+  define('JS_THEME', 100);
+}
diff --git a/core/tests/Drupal/Tests/Core/Common/AttributesTest.php b/core/tests/Drupal/Tests/Core/Common/AttributesTest.php
index 1dafef988..4fc57ae6f 100644
--- a/core/tests/Drupal/Tests/Core/Common/AttributesTest.php
+++ b/core/tests/Drupal/Tests/Core/Common/AttributesTest.php
@@ -71,4 +71,20 @@ class AttributesTest extends UnitTestCase {
     }
   }
 
+  /**
+   * Test AttributeValueBase copy.
+   */
+  public function testAttributeValueBaseCopy() {
+    $original_attributes = new Attribute([
+      'checked' => TRUE,
+      'class' => ['who', 'is', 'on'],
+      'id' => 'first',
+    ]);
+    $attributes['selected'] = $original_attributes['checked'];
+    $attributes['id'] = $original_attributes['id'];
+    $attributes = new Attribute($attributes);
+    $this->assertSame((string) $original_attributes, ' checked class="who is on" id="first"', 'Original boolean value used with original name.');
+    $this->assertSame((string) $attributes, ' selected id="first"', 'Original boolean value used with new name.');
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Datetime/DateHelperTest.php b/core/tests/Drupal/Tests/Core/Datetime/DateHelperTest.php
new file mode 100644
index 000000000..a88861ca5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Datetime/DateHelperTest.php
@@ -0,0 +1,112 @@
+ ['first_day' => $first_day]];
+    $container->set('config.factory', $this->getConfigFactoryStub($config));
+    \Drupal::setContainer($container);
+
+    $weekdays = DateHelper::weekDaysUntranslated();
+    // self::assertSame() MUST be used here as it checks for array key order.
+    $this->assertSame($expected, DateHelper::weekDaysOrdered($weekdays));
+  }
+
+  public function providerTestWeekDaysOrdered() {
+    $data = [];
+    $data[] = [0, [
+      0 => 'Sunday',
+      1 => 'Monday',
+      2 => 'Tuesday',
+      3 => 'Wednesday',
+      4 => 'Thursday',
+      5 => 'Friday',
+      6 => 'Saturday',
+    ]];
+    $data[] = [1, [
+      1 => 'Monday',
+      2 => 'Tuesday',
+      3 => 'Wednesday',
+      4 => 'Thursday',
+      5 => 'Friday',
+      6 => 'Saturday',
+      0 => 'Sunday',
+    ]];
+    $data[] = [2, [
+      2 => 'Tuesday',
+      3 => 'Wednesday',
+      4 => 'Thursday',
+      5 => 'Friday',
+      6 => 'Saturday',
+      0 => 'Sunday',
+      1 => 'Monday',
+    ]];
+    $data[] = [3, [
+      3 => 'Wednesday',
+      4 => 'Thursday',
+      5 => 'Friday',
+      6 => 'Saturday',
+      0 => 'Sunday',
+      1 => 'Monday',
+      2 => 'Tuesday',
+    ]];
+    $data[] = [4, [
+      4 => 'Thursday',
+      5 => 'Friday',
+      6 => 'Saturday',
+      0 => 'Sunday',
+      1 => 'Monday',
+      2 => 'Tuesday',
+      3 => 'Wednesday',
+    ]];
+    $data[] = [5, [
+      5 => 'Friday',
+      6 => 'Saturday',
+      0 => 'Sunday',
+      1 => 'Monday',
+      2 => 'Tuesday',
+      3 => 'Wednesday',
+      4 => 'Thursday',
+    ]];
+    $data[] = [6, [
+      6 => 'Saturday',
+      0 => 'Sunday',
+      1 => 'Monday',
+      2 => 'Tuesday',
+      3 => 'Wednesday',
+      4 => 'Thursday',
+      5 => 'Friday',
+    ]];
+    $data[] = [7, [
+      0 => 'Sunday',
+      1 => 'Monday',
+      2 => 'Tuesday',
+      3 => 'Wednesday',
+      4 => 'Thursday',
+      5 => 'Friday',
+      6 => 'Saturday',
+    ]];
+    return $data;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/DrupalTest.php b/core/tests/Drupal/Tests/Core/DrupalTest.php
index 315ce38c0..d22ed78b5 100644
--- a/core/tests/Drupal/Tests/Core/DrupalTest.php
+++ b/core/tests/Drupal/Tests/Core/DrupalTest.php
@@ -86,6 +86,16 @@ class DrupalTest extends UnitTestCase {
     $this->assertNotNull(\Drupal::entityManager());
   }
 
+  /**
+   * Tests the entityTypeManager() method.
+   *
+   * @covers ::entityTypeManager
+   */
+  public function testEntityTypeManager() {
+    $this->setMockContainerService('entity_type.manager');
+    $this->assertNotNull(\Drupal::entityTypeManager());
+  }
+
   /**
    * Tests the database() method.
    *
diff --git a/core/tests/Drupal/Tests/Core/Enhancer/ParamConversionEnhancerTest.php b/core/tests/Drupal/Tests/Core/Enhancer/ParamConversionEnhancerTest.php
index a4c8af62e..41b2104bc 100644
--- a/core/tests/Drupal/Tests/Core/Enhancer/ParamConversionEnhancerTest.php
+++ b/core/tests/Drupal/Tests/Core/Enhancer/ParamConversionEnhancerTest.php
@@ -59,7 +59,7 @@ class ParamConversionEnhancerTest extends UnitTestCase {
     $expected['id'] = 'something_better!';
     $expected['_raw_variables'] = new ParameterBag($raw_variables);
 
-    $this->paramConverterManager->expects($this->any())
+    $this->paramConverterManager->expects($this->once())
       ->method('convert')
       ->with($this->isType('array'))
       ->will($this->returnValue($expected));
@@ -67,6 +67,12 @@ class ParamConversionEnhancerTest extends UnitTestCase {
     $result = $this->paramConversionEnhancer->enhance($defaults, new Request());
 
     $this->assertEquals($expected, $result);
+
+    // Now run with the results as the new defaults to ensure that the
+    // conversion is just run once.
+    $result = $this->paramConversionEnhancer->enhance($result, new Request());
+
+    $this->assertEquals($expected, $result);
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php
index 9e54ebedb..efcf1b89a 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php
@@ -57,6 +57,8 @@ class EntityLinkTest extends UnitTestCase {
   }
 
   /**
+   * Tests for the Entity::link() method
+   *
    * @covers ::link
    *
    * @dataProvider providerTestLink
@@ -98,7 +100,7 @@ class EntityLinkTest extends UnitTestCase {
     /** @var \Drupal\Core\Entity\Entity $entity */
     $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', [
       ['id' => $entity_id, 'label' => $entity_label, 'langcode' => 'es'],
-      $entity_type_id
+      $entity_type_id,
     ]);
 
     $expected_link = Link::createFromRoute(
@@ -116,6 +118,64 @@ class EntityLinkTest extends UnitTestCase {
     $this->assertSame($expected, $entity->link($link_text, $link_rel, $link_options));
   }
 
+  /**
+   * Tests for the Entity::toLink() method
+   *
+   * @covers ::toLink
+   *
+   * @dataProvider providerTestLink
+   */
+  public function testToLink($entity_label, $link_text, $expected_text, $link_rel = 'canonical', array $link_options = []) {
+    $language = new Language(['id' => 'es']);
+    $link_options += ['language' => $language];
+    $this->languageManager->expects($this->any())
+      ->method('getLanguage')
+      ->with('es')
+      ->willReturn($language);
+
+    $route_name_map = [
+      'canonical' => 'entity.test_entity_type.canonical',
+      'edit-form' => 'entity.test_entity_type.edit_form',
+    ];
+    $route_name = $route_name_map[$link_rel];
+    $entity_id = 'test_entity_id';
+    $entity_type_id = 'test_entity_type';
+    $expected = '' . $expected_text . '';
+
+    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+    $entity_type->expects($this->once())
+      ->method('getLinkTemplates')
+      ->willReturn($route_name_map);
+    $entity_type->expects($this->any())
+      ->method('getKey')
+      ->willReturnMap([
+        ['label', 'label'],
+        ['langcode', 'langcode'],
+      ]);
+
+    $this->entityManager
+      ->expects($this->any())
+      ->method('getDefinition')
+      ->with($entity_type_id)
+      ->will($this->returnValue($entity_type));
+
+    /** @var \Drupal\Core\Entity\Entity $entity */
+    $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', [
+      ['id' => $entity_id, 'label' => $entity_label, 'langcode' => 'es'],
+      $entity_type_id,
+    ]);
+
+    $expected_link = Link::createFromRoute(
+      $expected_text,
+      $route_name,
+      [$entity_type_id => $entity_id],
+      ['entity_type' => $entity_type_id, 'entity' => $entity] + $link_options
+    );
+
+    $result_link = $entity->toLink($link_text, $link_rel, $link_options);
+    $this->assertEquals($expected_link, $result_link);
+  }
+
   /**
    * Provides test data for testLink().
    */
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php
index 82626ec74..9003bc581 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php
@@ -175,11 +175,11 @@ class EntityTypeBundleInfoTest extends UnitTestCase {
 
     $apple = $this->prophesize(EntityTypeInterface::class);
     $apple->getLabel()->willReturn('Apple');
-    $apple->getBundleOf()->willReturn(NULL);
+    $apple->getBundleEntityType()->willReturn(NULL);
 
     $banana = $this->prophesize(EntityTypeInterface::class);
     $banana->getLabel()->willReturn('Banana');
-    $banana->getBundleOf()->willReturn(NULL);
+    $banana->getBundleEntityType()->willReturn(NULL);
 
     $this->setUpEntityTypeDefinitions([
       'apple' => $apple,
@@ -223,11 +223,11 @@ class EntityTypeBundleInfoTest extends UnitTestCase {
 
     $apple = $this->prophesize(EntityTypeInterface::class);
     $apple->getLabel()->willReturn('Apple');
-    $apple->getBundleOf()->willReturn(NULL);
+    $apple->getBundleEntityType()->willReturn(NULL);
 
     $banana = $this->prophesize(EntityTypeInterface::class);
     $banana->getLabel()->willReturn('Banana');
-    $banana->getBundleOf()->willReturn(NULL);
+    $banana->getBundleEntityType()->willReturn(NULL);
 
     $this->setUpEntityTypeDefinitions([
       'apple' => $apple,
@@ -271,4 +271,49 @@ class EntityTypeBundleInfoTest extends UnitTestCase {
     $this->assertSame('cached data', $bundle_info);
   }
 
+  /**
+   * @covers ::getAllBundleInfo
+   */
+  public function testGetAllBundleInfoWithEntityBundleInfo() {
+    // Ensure that EntityTypeBundleInfo::getAllBundleInfo() does not add
+    // additional bundles if hook_entity_bundle_info() defines some and the
+    // entity_type does not define a bundle entity type.
+    $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([
+      'banana' => [
+        'fig' => [
+          'label' => 'Fig banana',
+        ],
+      ],
+    ]);
+    $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL);
+
+    $apple = $this->prophesize(EntityTypeInterface::class);
+    $apple->getLabel()->willReturn('Apple');
+    $apple->getBundleEntityType()->willReturn(NULL);
+
+    $banana = $this->prophesize(EntityTypeInterface::class);
+    $banana->getLabel()->willReturn('Banana');
+    $banana->getBundleEntityType()->willReturn(NULL);
+
+    $this->setUpEntityTypeDefinitions([
+      'apple' => $apple,
+      'banana' => $banana,
+    ]);
+
+    $expected = [
+      'banana' => [
+        'fig' => [
+          'label' => 'Fig banana',
+        ],
+      ],
+      'apple' => [
+        'apple' => [
+          'label' => 'Apple',
+        ],
+      ],
+    ];
+    $bundle_info = $this->entityTypeBundleInfo->getAllBundleInfo();
+    $this->assertSame($expected, $bundle_info);
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
index d19f473d1..c60a18fd7 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
@@ -28,6 +28,8 @@ class EntityUrlTest extends UnitTestCase {
   protected $entityManager;
 
   /**
+   * The mocked URL generator.
+   *
    * @var \Drupal\Core\Routing\UrlGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
    */
   protected $urlGenerator;
@@ -50,12 +52,15 @@ class EntityUrlTest extends UnitTestCase {
   /**
    * Tests the urlInfo() method.
    *
+   * Note that urlInfo() is a deprecated alias for toUrl().
+   * See testToUrl().
+   *
    * @covers ::urlInfo
    *
-   * @dataProvider providerTestUrlInfo
+   * @dataProvider providerTestToUrl
    */
   public function testUrlInfo($entity_class, $link_template, $expected, $langcode = NULL) {
-    /** @var $entity \Drupal\Core\Entity\EntityInterface */
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
     $entity = $this->getMockForAbstractClass($entity_class, array(array('id' => 'test_entity_id'), 'test_entity_type'));
     $uri = $this->getTestUrlInfo($entity, $link_template, [], $langcode);
 
@@ -75,27 +80,57 @@ class EntityUrlTest extends UnitTestCase {
       }
     }
   }
+  /**
+   * Tests the toUrl() method.
+   *
+   * @covers ::toUrl
+   *
+   * @dataProvider providerTestToUrl
+   */
+  public function testToUrl($entity_class, $link_template, $expected, $langcode = NULL) {
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    $entity = $this->getMockForAbstractClass($entity_class, array(array('id' => 'test_entity_id'), 'test_entity_type'));
+    $uri = $this->getTestToUrl($entity, $link_template, [], $langcode);
+
+    $this->assertSame($expected, $uri->getRouteName());
+    $this->assertSame($entity, $uri->getOption('entity'));
+
+    if ($langcode) {
+      $this->assertEquals($langcode, $uri->getOption('language')->getId());
+    }
+    else {
+      if ($entity instanceof ConfigEntityInterface) {
+        // Config entities do not provide a language with their URIs.
+        $this->assertEquals(NULL, $uri->getOption('language'));
+      }
+      else {
+        $this->assertEquals(LanguageInterface::LANGCODE_NOT_SPECIFIED, $uri->getOption('language')->getId());
+      }
+    }
+  }
 
   /**
-   * @covers ::urlInfo
+   * Tests for Entity::toUrl() exercising different language options.
+   *
+   * @covers ::toUrl
    */
-  public function testUrlInfoWithSpecificLanguageInOptions() {
-    /** @var $entity \Drupal\Core\Entity\EntityInterface */
+  public function testToUrlWithSpecificLanguageInOptions() {
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
     $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array('id' => 'test_entity_id'), 'test_entity_type'));
 
     // Ensure that a specified language overrides the current translation
     // language.
-    $uri = $this->getTestUrlInfo($entity, 'edit-form', [], 'en');
+    $uri = $this->getTestToUrl($entity, 'edit-form', [], 'en');
     $this->assertEquals('en', $uri->getOption('language')->getId());
 
-    $uri = $this->getTestUrlInfo($entity, 'edit-form', ['language' => new Language(['id' => 'fr'])], 'en');
+    $uri = $this->getTestToUrl($entity, 'edit-form', ['language' => new Language(['id' => 'fr'])], 'en');
     $this->assertEquals('fr', $uri->getOption('language')->getId());
   }
 
   /**
    * Provides test data for testUrlInfo().
    */
-  public function providerTestUrlInfo() {
+  public function providerTestToUrl() {
     return array(
       array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form', NULL),
       // Specify a langcode.
@@ -108,19 +143,20 @@ class EntityUrlTest extends UnitTestCase {
   }
 
   /**
-   * Tests the urlInfo() method with an invalid link template.
+   * Tests the toUrl() method with an invalid link template.
    *
-   * @covers ::urlInfo
+   * @covers ::toUrl
    *
    * @expectedException \Drupal\Core\Entity\Exception\UndefinedLinkTemplateException
+   *
    * @expectedExceptionMessage No link template 'canonical' found for the 'test_entity_type' entity type
    *
-   * @dataProvider providerTestUrlInfoForInvalidLinkTemplate
+   * @dataProvider providerTestToUrlForInvalidLinkTemplate
    */
-  public function testUrlInfoForInvalidLinkTemplate($entity_class, $link_template) {
-    /** @var $entity \Drupal\Core\Entity\EntityInterface */
+  public function testToUrlForInvalidLinkTemplate($entity_class, $link_template) {
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
     $entity = $this->getMockForAbstractClass($entity_class, array(array('id' => 'test_entity_id'), 'test_entity_type'));
-    $uri = $this->getTestUrlInfo($entity, $link_template);
+    $uri = $this->getTestToUrl($entity, $link_template);
 
     $this->assertEmpty($uri);
   }
@@ -128,7 +164,7 @@ class EntityUrlTest extends UnitTestCase {
   /**
    * Provides test data for testUrlInfoForInvalidLinkTemplate().
    */
-  public function providerTestUrlInfoForInvalidLinkTemplate() {
+  public function providerTestToUrlForInvalidLinkTemplate() {
     return array(
       array('Drupal\Core\Entity\Entity', 'canonical'),
       array('Drupal\Core\Entity\Entity', FALSE),
@@ -139,6 +175,9 @@ class EntityUrlTest extends UnitTestCase {
   /**
    * Creates a \Drupal\Core\Url object based on the entity and link template.
    *
+   * Method urlInfo() is deprecated and replaced with toUrl().
+   * See also getTestToUrl().
+   *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The test entity.
    * @param string $link_template
@@ -184,17 +223,64 @@ class EntityUrlTest extends UnitTestCase {
   }
 
   /**
-   * Tests the urlInfo() method when an entity is still "new".
+   * Creates a \Drupal\Core\Url object based on the entity and link template.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The test entity.
+   * @param string $link_template
+   *   The link template.
+   * @param string $langcode
+   *   The langcode.
+   *
+   * @return \Drupal\Core\Url
+   *   The URL for this entity's link template.
+   */
+  protected function getTestToUrl(EntityInterface $entity, $link_template, array $options = [], $langcode = NULL) {
+    $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+    $entity_type->expects($this->any())
+      ->method('getLinkTemplates')
+      ->will($this->returnValue(array(
+        'edit-form' => 'test_entity_type.edit',
+      )));
+
+    if ($langcode) {
+      $entity->langcode = $langcode;
+    }
+
+    $this->entityManager
+      ->expects($this->any())
+      ->method('getDefinition')
+      ->with('test_entity_type')
+      ->will($this->returnValue($entity_type));
+
+    // If no link template is given, call without a value to test the default.
+    if ($link_template) {
+      $uri = $entity->toUrl($link_template, $options);
+    }
+    else {
+      if ($entity instanceof ConfigEntityInterface) {
+        $uri = $entity->toUrl('edit-form', $options);
+      }
+      else {
+        $uri = $entity->toUrl('canonical', $options);
+      }
+    }
+
+    return $uri;
+  }
+
+  /**
+   * Tests the toUrl() method when an entity is still "new".
    *
    * @see \Drupal\Core\Entity\EntityInterface::isNew()
    *
-   * @covers ::urlInfo
+   * @covers ::toUrl
    *
    * @expectedException \Drupal\Core\Entity\EntityMalformedException
    */
-  public function testUrlInfoForNewEntity() {
+  public function testToUrlForNewEntity() {
     $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array(), 'test_entity_type'));
-    $entity->urlInfo();
+    $entity->toUrl();
   }
 
   /**
@@ -236,7 +322,7 @@ class EntityUrlTest extends UnitTestCase {
         if ($route_name === 'entity.test_entity_type.canonical' && $route_parameters === array('test_entity_type' => 'test_entity_id') && array_keys($options) === ['absolute', 'entity_type', 'entity', 'language'] && $options['language'] == $language) {
           return 'http://drupal/entity/test_entity_type/test_entity_id';
         }
-    });
+      });
 
     $this->assertSame('/entity/test_entity_type/test_entity_id', $valid_entity->url());
     $this->assertSame('http://drupal/entity/test_entity_type/test_entity_id', $valid_entity->url('canonical', array('absolute' => TRUE)));
diff --git a/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php
new file mode 100644
index 000000000..cdfd0f292
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php
@@ -0,0 +1,155 @@
+prophesize(EntityManagerInterface::class);
+    $route_provider = new TestDefaultHtmlRouteProvider($entity_manager->reveal());
+
+    $entity_type = $this->prophesize(EntityTypeInterface::class);
+    $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE);
+    $entity_type_id = 'the_entity_type_id';
+    $entity_type->id()->willReturn($entity_type_id);
+    $entity_type->getKey('id')->willReturn('id');
+
+    $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
+    $field_storage_definition->getType()->willReturn('integer');
+    $entity_manager->getFieldStorageDefinitions($entity_type_id)->willReturn(['id' => $field_storage_definition]);
+
+    $type = $route_provider->getEntityTypeIdKeyType($entity_type->reveal());
+    $this->assertSame('integer', $type);
+  }
+
+  /**
+   * @covers ::getEntityTypeIdKeyType
+   */
+  public function testGetEntityTypeIdKeyTypeNotFieldable() {
+    $entity_manager = $this->prophesize(EntityManagerInterface::class);
+    $route_provider = new TestDefaultHtmlRouteProvider($entity_manager->reveal());
+
+    $entity_type = $this->prophesize(EntityTypeInterface::class);
+    $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE);
+    $entity_manager->getFieldStorageDefinitions(Argument::any())->shouldNotBeCalled();
+
+    $type = $route_provider->getEntityTypeIdKeyType($entity_type->reveal());
+    $this->assertNull($type);
+  }
+
+  /**
+   * @covers ::getCanonicalRoute
+   * @dataProvider providerTestGetCanonicalRoute
+   */
+  public function testGetCanonicalRoute($entity_type_prophecy, $expected, $field_storage_definition = NULL) {
+    $entity_manager = $this->prophesize(EntityManagerInterface::class);
+    $route_provider = new TestDefaultHtmlRouteProvider($entity_manager->reveal());
+    $entity_type = $entity_type_prophecy->reveal();
+
+    if ($field_storage_definition) {
+      $entity_manager->getFieldStorageDefinitions($entity_type->id())
+        ->willReturn([$entity_type->getKey('id') => $field_storage_definition]);
+    }
+
+    $route = $route_provider->getCanonicalRoute($entity_type);
+    $this->assertEquals($expected, $route);
+  }
+
+  public function providerTestGetCanonicalRoute() {
+    $data = [];
+
+    $entity_type1 = $this->prophesize(EntityTypeInterface::class);
+    $entity_type1->hasLinkTemplate('canonical')->willReturn(FALSE);
+    $data['no_canonical_link_template'] = [$entity_type1, NULL];
+
+    $entity_type2 = $this->prophesize(EntityTypeInterface::class);
+    $entity_type2->hasLinkTemplate('canonical')->willReturn(TRUE);
+    $entity_type2->hasViewBuilderClass()->willReturn(FALSE);
+    $data['no_view_builder'] = [$entity_type2, NULL];
+
+    $entity_type3 = $this->prophesize(EntityTypeInterface::class);
+    $entity_type3->hasLinkTemplate('canonical')->willReturn(TRUE);
+    $entity_type3->hasViewBuilderClass()->willReturn(TRUE);
+    $entity_type3->id()->willReturn('the_entity_type_id');
+    $entity_type3->getLinkTemplate('canonical')->willReturn('/the/canonical/link/template');
+    $entity_type3->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE);
+    $route3 = (new Route('/the/canonical/link/template'))
+      ->setDefaults([
+        '_entity_view' => 'the_entity_type_id.full',
+        '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
+      ])
+      ->setRequirements([
+        '_entity_access' => 'the_entity_type_id.view',
+      ])
+      ->setOptions([
+        'parameters' => [
+          'the_entity_type_id' => [
+            'type' => 'entity:the_entity_type_id',
+          ],
+        ],
+      ]);
+    $data['id_key_type_null'] = [$entity_type3, $route3];
+
+    $entity_type4 = $this->prophesize(EntityTypeInterface::class);
+    $entity_type4->hasLinkTemplate('canonical')->willReturn(TRUE);
+    $entity_type4->hasViewBuilderClass()->willReturn(TRUE);
+    $entity_type4->id()->willReturn('the_entity_type_id');
+    $entity_type4->getLinkTemplate('canonical')->willReturn('/the/canonical/link/template');
+    $entity_type4->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE);
+    $entity_type4->getKey('id')->willReturn('id');
+    $route4 = (new Route('/the/canonical/link/template'))
+      ->setDefaults([
+        '_entity_view' => 'the_entity_type_id.full',
+        '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
+      ])
+      ->setRequirements([
+        '_entity_access' => 'the_entity_type_id.view',
+        'the_entity_type_id' => '\d+',
+      ])
+      ->setOptions([
+        'parameters' => [
+          'the_entity_type_id' => [
+            'type' => 'entity:the_entity_type_id',
+          ],
+        ],
+      ]);
+    $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
+    $field_storage_definition->getType()->willReturn('integer');
+    $data['id_key_type_integer'] = [$entity_type4, $route4, $field_storage_definition];
+
+    return $data;
+  }
+
+}
+
+class TestDefaultHtmlRouteProvider extends DefaultHtmlRouteProvider {
+
+  public function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
+    return parent::getEntityTypeIdKeyType($entity_type);
+  }
+  public function getCanonicalRoute(EntityTypeInterface $entity_type) {
+    return parent::getCanonicalRoute($entity_type);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php
index d01a1f6ef..da61f5fc6 100644
--- a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php
+++ b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php
@@ -150,11 +150,16 @@ class PathProcessorTest extends UnitTestCase {
     $current_user = $this->getMockBuilder('Drupal\Core\Session\AccountInterface')
       ->getMock();
 
+    // Create a config event subscriber stub.
+    $config_subscriber = $this->getMockBuilder('Drupal\language\EventSubscriber\ConfigSubscriber')
+      ->disableOriginalConstructor()
+      ->getMock();
+
     // Create the processors.
     $alias_processor = new PathProcessorAlias($alias_manager);
     $decode_processor = new PathProcessorDecode();
     $front_processor = new PathProcessorFront($config_factory_stub);
-    $language_processor = new PathProcessorLanguage($config_factory_stub, $this->languageManager, $negotiator, $current_user);
+    $language_processor = new PathProcessorLanguage($config_factory_stub, $this->languageManager, $negotiator, $current_user, $config_subscriber);
 
     // First, test the processor manager with the processors in the incorrect
     // order. The alias processor will run before the language processor, meaning
diff --git a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php
index 1a99bbf3a..1a1b3904e 100644
--- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php
@@ -43,6 +43,8 @@ class TranslationManagerTest extends UnitTestCase {
       // @todo support locale_get_plural
       [2, 'Singular', '@count @arg', array('@arg' => '