From 5c71ef4c466bced392d61289dff36d10338d6457 Mon Sep 17 00:00:00 2001 From: Bachir Soussi Chiadmi Date: Tue, 23 Oct 2018 11:50:40 +0200 Subject: [PATCH] updated core to 8.6.2 --- composer.lock | 486 +++++++++--------- core/lib/Drupal.php | 2 +- .../Drupal/Component/Utility/UrlHelper.php | 10 + .../RedirectResponseSubscriber.php | 32 -- .../Drupal/Core/Mail/Plugin/Mail/PhpMail.php | 48 +- .../Core/PathProcessor/PathProcessorAlias.php | 9 + core/lib/Drupal/Core/Routing/UrlGenerator.php | 5 + .../Drupal/Core/Security/RequestSanitizer.php | 13 +- .../src/Functional/Views/DisplayBlockTest.php | 10 +- .../Constraint/ModerationStateConstraint.php | 1 + .../ModerationStateConstraintValidator.php | 90 +++- .../src/StateTransitionValidation.php | 10 + .../StateTransitionValidationInterface.php | 19 + .../Functional/ModerationStateNodeTest.php | 23 +- .../EntityStateChangeValidationTest.php | 107 ++++ core/modules/contextual/contextual.module | 8 +- .../contextual/contextual.post_update.php | 14 + core/modules/contextual/js/contextual.es6.js | 21 +- core/modules/contextual/js/contextual.js | 20 +- .../contextual/src/ContextualController.php | 12 +- .../Element/ContextualLinksPlaceholder.php | 9 +- .../ContextualDynamicContextTest.php | 91 +++- .../Tests/Views/NodeContextualLinksTest.php | 118 ----- .../src/Functional}/NodeRevisionsTest.php | 22 +- .../src/Functional}/NodeTypeTest.php | 25 +- .../src/Functional}/PagePreviewTest.php | 17 +- .../Views/NodeContextualLinksTest.php | 47 ++ .../ContextualLinksTest.php | 117 +++++ .../tests/src/Functional/PathAliasTest.php | 31 +- .../system/src/Tests/Routing/RouterTest.php | 7 + .../Tests/Component/Utility/UrlHelperTest.php | 4 + .../RedirectResponseSubscriberTest.php | 71 --- .../Tests/Core/Mail/MailManagerTest.php | 9 + .../Core/Security/RequestSanitizerTest.php | 141 +++++ 34 files changed, 1083 insertions(+), 566 deletions(-) create mode 100644 core/modules/contextual/contextual.post_update.php delete mode 100644 core/modules/node/src/Tests/Views/NodeContextualLinksTest.php rename core/modules/node/{src/Tests => tests/src/Functional}/NodeRevisionsTest.php (92%) rename core/modules/node/{src/Tests => tests/src/Functional}/NodeTypeTest.php (94%) rename core/modules/node/{src/Tests => tests/src/Functional}/PagePreviewTest.php (97%) create mode 100644 core/modules/node/tests/src/Functional/Views/NodeContextualLinksTest.php create mode 100644 core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php diff --git a/composer.lock b/composer.lock index 01cb982e6..d3ff4d2b1 100644 --- a/composer.lock +++ b/composer.lock @@ -124,19 +124,20 @@ }, { "name": "chi-teck/drupal-code-generator", - "version": "1.26.0", + "version": "1.27.0", "source": { "type": "git", "url": "https://github.com/Chi-teck/drupal-code-generator.git", - "reference": "3090fabdbf3dd4b66e0fa0e4ed5d8adab6deb974" + "reference": "a839bc89d385087d8a7a96a9c1c4bd470ffb627e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Chi-teck/drupal-code-generator/zipball/3090fabdbf3dd4b66e0fa0e4ed5d8adab6deb974", - "reference": "3090fabdbf3dd4b66e0fa0e4ed5d8adab6deb974", + "url": "https://api.github.com/repos/Chi-teck/drupal-code-generator/zipball/a839bc89d385087d8a7a96a9c1c4bd470ffb627e", + "reference": "a839bc89d385087d8a7a96a9c1c4bd470ffb627e", "shasum": "" }, "require": { + "ext-json": "*", "php": ">=5.5.9", "symfony/console": "~2.7|^3", "symfony/filesystem": "~2.7|^3", @@ -146,6 +147,11 @@ "bin/dcg" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "files": [ "src/bootstrap.php" @@ -159,7 +165,7 @@ "GPL-2.0-or-later" ], "description": "Drupal code generator", - "time": "2018-08-06T08:54:16+00:00" + "time": "2018-10-11T08:05:59+00:00" }, { "name": "composer/installers", @@ -345,16 +351,16 @@ }, { "name": "consolidation/annotated-command", - "version": "2.8.5", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "1e8ff512072422b850b44aa721b5b303e4a5ebb3" + "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/1e8ff512072422b850b44aa721b5b303e4a5ebb3", - "reference": "1e8ff512072422b850b44aa721b5b303e4a5ebb3", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", + "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", "shasum": "" }, "require": { @@ -393,7 +399,7 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-08-18T23:51:49+00:00" + "time": "2018-09-19T17:47:18+00:00" }, { "name": "consolidation/config", @@ -500,19 +506,20 @@ }, { "name": "consolidation/output-formatters", - "version": "3.2.1", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5" + "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/a942680232094c4a5b21c0b7e54c20cce623ae19", + "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19", "shasum": "" }, "require": { + "dflydev/dot-access-data": "^1.1.0", "php": ">=5.4.0", "symfony/console": "^2.8|^3|^4", "symfony/finder": "^2.5|^3|^4" @@ -551,7 +558,7 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-05-25T18:02:34+00:00" + "time": "2018-10-19T22:35:38+00:00" }, { "name": "consolidation/robo", @@ -636,16 +643,16 @@ }, { "name": "consolidation/self-update", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/consolidation/self-update.git", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318" + "reference": "4422e52d3fabeca9129ecb1780f198f202debdce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/self-update/zipball/de33822f907e0beb0ffad24cf4b1b4fae5ada318", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318", + "url": "https://api.github.com/repos/consolidation/self-update/zipball/4422e52d3fabeca9129ecb1780f198f202debdce", + "reference": "4422e52d3fabeca9129ecb1780f198f202debdce", "shasum": "" }, "require": { @@ -682,20 +689,20 @@ } ], "description": "Provides a self:update command for Symfony Console applications.", - "time": "2018-08-24T17:01:46+00:00" + "time": "2018-10-21T20:17:55+00:00" }, { "name": "consolidation/site-alias", - "version": "1.1.2", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/consolidation/site-alias.git", - "reference": "d6fa92e4aaf5ba95cde4454be7ea3165e7e2f17a" + "reference": "247f3e52604b9d76a075e6c06d5d1dd73bbae892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/site-alias/zipball/d6fa92e4aaf5ba95cde4454be7ea3165e7e2f17a", - "reference": "d6fa92e4aaf5ba95cde4454be7ea3165e7e2f17a", + "url": "https://api.github.com/repos/consolidation/site-alias/zipball/247f3e52604b9d76a075e6c06d5d1dd73bbae892", + "reference": "247f3e52604b9d76a075e6c06d5d1dd73bbae892", "shasum": "" }, "require": { @@ -738,7 +745,7 @@ } ], "description": "Template project for PHP libraries.", - "time": "2018-08-22T01:07:08+00:00" + "time": "2018-09-22T04:11:38+00:00" }, { "name": "container-interop/container-interop", @@ -1632,16 +1639,16 @@ }, { "name": "drupal/core", - "version": "8.6.1", + "version": "8.6.2", "source": { "type": "git", "url": "https://github.com/drupal/core.git", - "reference": "1c8b96c89288daf4d145c6dbcf1e8f0d0c9d7970" + "reference": "356292934802bb1aecc478e88a3cba77442d7c62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drupal/core/zipball/1c8b96c89288daf4d145c6dbcf1e8f0d0c9d7970", - "reference": "1c8b96c89288daf4d145c6dbcf1e8f0d0c9d7970", + "url": "https://api.github.com/repos/drupal/core/zipball/356292934802bb1aecc478e88a3cba77442d7c62", + "reference": "356292934802bb1aecc478e88a3cba77442d7c62", "shasum": "" }, "require": { @@ -1866,57 +1873,90 @@ "GPL-2.0-or-later" ], "description": "Drupal is an open source content management platform powering millions of websites and applications.", - "time": "2018-09-10T11:50:07+00:00" + "time": "2018-10-17T22:19:50+00:00" }, { "name": "drush/drush", - "version": "9.4.0", + "version": "9.5.2", "source": { "type": "git", "url": "https://github.com/drush-ops/drush.git", - "reference": "9d46a2a67554ae8b6f6edec234a1272c3b4c6a9e" + "reference": "17f0106706391675a281c6d212850853bdbe90f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/drush-ops/drush/zipball/9d46a2a67554ae8b6f6edec234a1272c3b4c6a9e", - "reference": "9d46a2a67554ae8b6f6edec234a1272c3b4c6a9e", + "url": "https://api.github.com/repos/drush-ops/drush/zipball/17f0106706391675a281c6d212850853bdbe90f9", + "reference": "17f0106706391675a281c6d212850853bdbe90f9", "shasum": "" }, "require": { - "chi-teck/drupal-code-generator": "^1.24.0", + "chi-teck/drupal-code-generator": "^1.27.0", "composer/semver": "^1.4", - "consolidation/annotated-command": "^2.8.1", + "consolidation/annotated-command": "^2.9.1", "consolidation/config": "^1.1.0", "consolidation/output-formatters": "^3.1.12", "consolidation/robo": "^1.1.5", - "consolidation/site-alias": "^1.1.2", + "consolidation/site-alias": "^1.1.5", "ext-dom": "*", "grasmash/yaml-expander": "^1.1.1", "league/container": "~2", "php": ">=5.6.0", "psr/log": "~1.0", "psy/psysh": "~0.6", - "symfony/config": "~2.2|^3", - "symfony/console": "~2.7|^3", - "symfony/event-dispatcher": "~2.7|^3", - "symfony/finder": "~2.7|^3", - "symfony/process": "~2.7|^3", - "symfony/var-dumper": "~2.7|^3|^4", - "symfony/yaml": "~2.3|^3", + "symfony/config": "^3.4", + "symfony/console": "^3.4", + "symfony/event-dispatcher": "^3.4", + "symfony/finder": "^3.4", + "symfony/process": "^3.4", + "symfony/var-dumper": "^3.4", + "symfony/yaml": "^3.4", "webflo/drupal-finder": "^1.1", "webmozart/path-util": "^2.1.0" }, "require-dev": { + "composer/installers": "^1.2", + "cweagans/composer-patches": "~1.0", + "drupal/alinks": "1.0.0", + "drupal/devel": "^1.0@RC", + "drupal/empty_theme": "1.0", "g1a/composer-test-scenarios": "^2.2.0", "lox/xhprof": "dev-master", - "phpunit/phpunit": "^4.8.36|^5.5.4", - "squizlabs/php_codesniffer": "^2.7" + "phpunit/phpunit": "^4.8.36 || ^6.1", + "squizlabs/php_codesniffer": "^2.7", + "vlucas/phpdotenv": "^2.4", + "webflo/drupal-core-strict": "8.6.x-dev" }, "bin": [ "drush" ], "type": "library", "extra": { + "installer-paths": { + "sut/core": [ + "type:drupal-core" + ], + "sut/libraries/{$name}": [ + "type:drupal-library" + ], + "sut/modules/unish/{$name}": [ + "drupal/devel" + ], + "sut/themes/unish/{$name}": [ + "drupal/empty_theme" + ], + "sut/modules/contrib/{$name}": [ + "type:drupal-module" + ], + "sut/profiles/contrib/{$name}": [ + "type:drupal-profile" + ], + "sut/themes/contrib/{$name}": [ + "type:drupal-theme" + ], + "sut/drush/contrib/{$name}": [ + "type:drupal-drush" + ] + }, "branch-alias": { "dev-master": "9.x-dev" } @@ -1968,7 +2008,7 @@ ], "description": "Drush is a command line shell and scripting interface for Drupal, a veritable Swiss Army knife designed to make life easier for those of us who spend some of our working hours hacking away at the command prompt.", "homepage": "http://www.drush.org", - "time": "2018-09-04T17:24:36+00:00" + "time": "2018-10-17T18:37:53+00:00" }, { "name": "easyrdf/easyrdf", @@ -2034,16 +2074,16 @@ }, { "name": "egulias/email-validator", - "version": "1.2.14", + "version": "1.2.15", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "5642614492f0ca2064c01d60cc33284cc2f731a9" + "reference": "758a77525bdaabd6c0f5669176bd4361cb2dda9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/5642614492f0ca2064c01d60cc33284cc2f731a9", - "reference": "5642614492f0ca2064c01d60cc33284cc2f731a9", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/758a77525bdaabd6c0f5669176bd4361cb2dda9e", + "reference": "758a77525bdaabd6c0f5669176bd4361cb2dda9e", "shasum": "" }, "require": { @@ -2082,7 +2122,7 @@ "validation", "validator" ], - "time": "2017-02-03T22:48:59+00:00" + "time": "2018-09-25T20:59:41+00:00" }, { "name": "g1a/composer-test-scenarios", @@ -2395,32 +2435,32 @@ }, { "name": "jakub-onderka/php-console-color", - "version": "0.1", + "version": "v0.2", "source": { "type": "git", "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", - "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1" + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/e0b393dacf7703fc36a4efc3df1435485197e6c1", - "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191", + "reference": "d5deaecff52a0d61ccb613bb3804088da0307191", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": ">=5.4.0" }, "require-dev": { "jakub-onderka/php-code-style": "1.0", - "jakub-onderka/php-parallel-lint": "0.*", + "jakub-onderka/php-parallel-lint": "1.0", "jakub-onderka/php-var-dump-check": "0.*", - "phpunit/phpunit": "3.7.*", + "phpunit/phpunit": "~4.3", "squizlabs/php_codesniffer": "1.*" }, "type": "library", "autoload": { - "psr-0": { - "JakubOnderka\\PhpConsoleColor": "src/" + "psr-4": { + "JakubOnderka\\PhpConsoleColor\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2430,41 +2470,41 @@ "authors": [ { "name": "Jakub Onderka", - "email": "jakub.onderka@gmail.com", - "homepage": "http://www.acci.cz" + "email": "jakub.onderka@gmail.com" } ], - "time": "2014-04-08T15:00:19+00:00" + "time": "2018-09-29T17:23:10+00:00" }, { "name": "jakub-onderka/php-console-highlighter", - "version": "v0.3.2", + "version": "v0.4", "source": { "type": "git", "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", - "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5" + "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5", - "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547", + "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547", "shasum": "" }, "require": { - "jakub-onderka/php-console-color": "~0.1", - "php": ">=5.3.0" + "ext-tokenizer": "*", + "jakub-onderka/php-console-color": "~0.2", + "php": ">=5.4.0" }, "require-dev": { "jakub-onderka/php-code-style": "~1.0", - "jakub-onderka/php-parallel-lint": "~0.5", + "jakub-onderka/php-parallel-lint": "~1.0", "jakub-onderka/php-var-dump-check": "~0.1", "phpunit/phpunit": "~4.0", "squizlabs/php_codesniffer": "~1.5" }, "type": "library", "autoload": { - "psr-0": { - "JakubOnderka\\PhpConsoleHighlighter": "src/" + "psr-4": { + "JakubOnderka\\PhpConsoleHighlighter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2478,7 +2518,8 @@ "homepage": "http://www.acci.cz/" } ], - "time": "2015-04-20T18:58:01+00:00" + "description": "Highlight PHP code in terminal", + "time": "2018-09-29T18:48:56+00:00" }, { "name": "league/container", @@ -2547,16 +2588,16 @@ }, { "name": "masterminds/html5", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "2c37c6c520b995b761674de3be8455a381679067" + "reference": "33f8d475d28741398be26cdff7a10a63003324a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/2c37c6c520b995b761674de3be8455a381679067", - "reference": "2c37c6c520b995b761674de3be8455a381679067", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/33f8d475d28741398be26cdff7a10a63003324a3", + "reference": "33f8d475d28741398be26cdff7a10a63003324a3", "shasum": "" }, "require": { @@ -2608,20 +2649,20 @@ "serializer", "xml" ], - "time": "2017-09-04T12:26:28+00:00" + "time": "2018-10-22T16:58:34+00:00" }, { "name": "nikic/php-parser", - "version": "v4.0.3", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "bd088dc940a418f09cda079a9b5c7c478890fb8d" + "reference": "d0230c5c77a7e3cfa69446febf340978540958c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bd088dc940a418f09cda079a9b5c7c478890fb8d", - "reference": "bd088dc940a418f09cda079a9b5c7c478890fb8d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/d0230c5c77a7e3cfa69446febf340978540958c0", + "reference": "d0230c5c77a7e3cfa69446febf340978540958c0", "shasum": "" }, "require": { @@ -2637,7 +2678,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -2659,7 +2700,7 @@ "parser", "php" ], - "time": "2018-07-15T17:25:16+00:00" + "time": "2018-10-10T09:24:14+00:00" }, { "name": "paragonie/random_compat", @@ -2858,23 +2899,23 @@ }, { "name": "psy/psysh", - "version": "v0.9.8", + "version": "v0.9.9", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "ed3c32c4304e1a678a6e0f9dc11dd2d927d89555" + "reference": "9aaf29575bb8293206bb0420c1e1c87ff2ffa94e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ed3c32c4304e1a678a6e0f9dc11dd2d927d89555", - "reference": "ed3c32c4304e1a678a6e0f9dc11dd2d927d89555", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/9aaf29575bb8293206bb0420c1e1c87ff2ffa94e", + "reference": "9aaf29575bb8293206bb0420c1e1c87ff2ffa94e", "shasum": "" }, "require": { "dnoegel/php-xdg-base-dir": "0.1", "ext-json": "*", "ext-tokenizer": "*", - "jakub-onderka/php-console-highlighter": "0.3.*", + "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*", "nikic/php-parser": "~1.3|~2.0|~3.0|~4.0", "php": ">=5.4.0", "symfony/console": "~2.3.10|^2.4.2|~3.0|~4.0", @@ -2928,7 +2969,7 @@ "interactive", "shell" ], - "time": "2018-09-05T11:40:09+00:00" + "time": "2018-10-13T15:16:03+00:00" }, { "name": "stack/builder", @@ -3085,16 +3126,16 @@ }, { "name": "symfony/class-loader", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "31db283fc86d3143e7ff87e922177b457d909c30" + "reference": "f31333bdff54c7595f834d510a6d2325573ddb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/31db283fc86d3143e7ff87e922177b457d909c30", - "reference": "31db283fc86d3143e7ff87e922177b457d909c30", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/f31333bdff54c7595f834d510a6d2325573ddb36", + "reference": "f31333bdff54c7595f834d510a6d2325573ddb36", "shasum": "" }, "require": { @@ -3137,20 +3178,20 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/config", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "7b08223b7f6abd859651c56bcabf900d1627d085" + "reference": "e5389132dc6320682de3643091121c048ff796b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/7b08223b7f6abd859651c56bcabf900d1627d085", - "reference": "7b08223b7f6abd859651c56bcabf900d1627d085", + "url": "https://api.github.com/repos/symfony/config/zipball/e5389132dc6320682de3643091121c048ff796b3", + "reference": "e5389132dc6320682de3643091121c048ff796b3", "shasum": "" }, "require": { @@ -3201,20 +3242,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-09-08T13:15:14+00:00" }, { "name": "symfony/console", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6b217594552b9323bcdcfc14f8a0ce126e84cd73" + "reference": "3b2b415d4c48fbefca7dc742aa0a0171bfae4e0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6b217594552b9323bcdcfc14f8a0ce126e84cd73", - "reference": "6b217594552b9323bcdcfc14f8a0ce126e84cd73", + "url": "https://api.github.com/repos/symfony/console/zipball/3b2b415d4c48fbefca7dc742aa0a0171bfae4e0b", + "reference": "3b2b415d4c48fbefca7dc742aa0a0171bfae4e0b", "shasum": "" }, "require": { @@ -3270,20 +3311,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/css-selector", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416" + "reference": "3503415d4aafabc31cd08c3a4ebac7f43fde8feb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/edda5a6155000ff8c3a3f85ee5c421af93cca416", - "reference": "edda5a6155000ff8c3a3f85ee5c421af93cca416", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/3503415d4aafabc31cd08c3a4ebac7f43fde8feb", + "reference": "3503415d4aafabc31cd08c3a4ebac7f43fde8feb", "shasum": "" }, "require": { @@ -3323,20 +3364,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-07-26T09:06:28+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/debug", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "c4625e75341e4fb309ce0c049cbf7fb84b8897cd" + "reference": "0a612e9dfbd2ccce03eb174365f31ecdca930ff6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/c4625e75341e4fb309ce0c049cbf7fb84b8897cd", - "reference": "c4625e75341e4fb309ce0c049cbf7fb84b8897cd", + "url": "https://api.github.com/repos/symfony/debug/zipball/0a612e9dfbd2ccce03eb174365f31ecdca930ff6", + "reference": "0a612e9dfbd2ccce03eb174365f31ecdca930ff6", "shasum": "" }, "require": { @@ -3379,20 +3420,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-08-03T10:42:44+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/dependency-injection", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "09d7df7bf06c1393b6afc85875993cbdbdf897a0" + "reference": "aea20fef4e92396928b5db175788b90234c0270d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/09d7df7bf06c1393b6afc85875993cbdbdf897a0", - "reference": "09d7df7bf06c1393b6afc85875993cbdbdf897a0", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/aea20fef4e92396928b5db175788b90234c0270d", + "reference": "aea20fef4e92396928b5db175788b90234c0270d", "shasum": "" }, "require": { @@ -3450,20 +3491,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-08-08T11:42:34+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "452bfc854b60134438e3824b159b0d24a5892331" + "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/452bfc854b60134438e3824b159b0d24a5892331", - "reference": "452bfc854b60134438e3824b159b0d24a5892331", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c705bee03ade5b47c087807dd9ffaaec8dda2722", + "reference": "c705bee03ade5b47c087807dd9ffaaec8dda2722", "shasum": "" }, "require": { @@ -3507,11 +3548,11 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-07-26T10:03:52+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3574,16 +3615,16 @@ }, { "name": "symfony/filesystem", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c" + "reference": "d69930fc337d767607267d57c20a7403d0a822a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c", - "reference": "285ce5005cb01a0aeaa5b0cf590bd0cc40bb631c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d69930fc337d767607267d57c20a7403d0a822a4", + "reference": "d69930fc337d767607267d57c20a7403d0a822a4", "shasum": "" }, "require": { @@ -3620,20 +3661,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-08-10T07:29:05+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/finder", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8a84fcb207451df0013b2c74cbbf1b62d47b999a" + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8a84fcb207451df0013b2c74cbbf1b62d47b999a", - "reference": "8a84fcb207451df0013b2c74cbbf1b62d47b999a", + "url": "https://api.github.com/repos/symfony/finder/zipball/54ba444dddc5bd5708a34bd095ea67c6eb54644d", + "reference": "54ba444dddc5bd5708a34bd095ea67c6eb54644d", "shasum": "" }, "require": { @@ -3669,20 +3710,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-10-03T08:46:40+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "2fb33cb6eefe6e790e4023f7c534a9e4214252fc" + "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/2fb33cb6eefe6e790e4023f7c534a9e4214252fc", - "reference": "2fb33cb6eefe6e790e4023f7c534a9e4214252fc", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3a4498236ade473c52b92d509303e5fd1b211ab1", + "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1", "shasum": "" }, "require": { @@ -3723,20 +3764,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-08-27T17:45:33+00:00" + "time": "2018-10-03T08:48:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "2819693b25f480966cbfa13b651abccfed4871ca" + "reference": "a0944a9a1d8845da724236cde9a310964acadb1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2819693b25f480966cbfa13b651abccfed4871ca", - "reference": "2819693b25f480966cbfa13b651abccfed4871ca", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/a0944a9a1d8845da724236cde9a310964acadb1c", + "reference": "a0944a9a1d8845da724236cde9a310964acadb1c", "shasum": "" }, "require": { @@ -3812,7 +3853,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-08-28T06:06:12+00:00" + "time": "2018-10-03T12:03:34+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4051,16 +4092,16 @@ }, { "name": "symfony/process", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4d6b125d5293cbceedc2aa10f2c71617e76262e7" + "reference": "1dc2977afa7d70f90f3fefbcd84152813558910e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4d6b125d5293cbceedc2aa10f2c71617e76262e7", - "reference": "4d6b125d5293cbceedc2aa10f2c71617e76262e7", + "url": "https://api.github.com/repos/symfony/process/zipball/1dc2977afa7d70f90f3fefbcd84152813558910e", + "reference": "1dc2977afa7d70f90f3fefbcd84152813558910e", "shasum": "" }, "require": { @@ -4096,7 +4137,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-08-03T10:42:44+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -4161,16 +4202,16 @@ }, { "name": "symfony/routing", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "e20f4bb79502c3c0db86d572f7683a30d4143911" + "reference": "585f6e2d740393d546978769dd56e496a6233e0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/e20f4bb79502c3c0db86d572f7683a30d4143911", - "reference": "e20f4bb79502c3c0db86d572f7683a30d4143911", + "url": "https://api.github.com/repos/symfony/routing/zipball/585f6e2d740393d546978769dd56e496a6233e0b", + "reference": "585f6e2d740393d546978769dd56e496a6233e0b", "shasum": "" }, "require": { @@ -4234,20 +4275,20 @@ "uri", "url" ], - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/serializer", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "40031683816470610af87c2d03ea86d1cf0f0104" + "reference": "8bc00ef47a428bfebc4641f29d158e7c56137fcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/40031683816470610af87c2d03ea86d1cf0f0104", - "reference": "40031683816470610af87c2d03ea86d1cf0f0104", + "url": "https://api.github.com/repos/symfony/serializer/zipball/8bc00ef47a428bfebc4641f29d158e7c56137fcb", + "reference": "8bc00ef47a428bfebc4641f29d158e7c56137fcb", "shasum": "" }, "require": { @@ -4313,20 +4354,20 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:58:24+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "symfony/translation", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "9749930bfc825139aadd2d28461ddbaed6577862" + "reference": "94bc3a79008e6640defedf5e14eb3b4f20048352" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/9749930bfc825139aadd2d28461ddbaed6577862", - "reference": "9749930bfc825139aadd2d28461ddbaed6577862", + "url": "https://api.github.com/repos/symfony/translation/zipball/94bc3a79008e6640defedf5e14eb3b4f20048352", + "reference": "94bc3a79008e6640defedf5e14eb3b4f20048352", "shasum": "" }, "require": { @@ -4381,20 +4422,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/validator", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "5a9ca502663e32aed3302b00121f978d70a09ab9" + "reference": "9f8dbf0dceb03815c3160a279bf8cf4f8018a1c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/5a9ca502663e32aed3302b00121f978d70a09ab9", - "reference": "5a9ca502663e32aed3302b00121f978d70a09ab9", + "url": "https://api.github.com/repos/symfony/validator/zipball/9f8dbf0dceb03815c3160a279bf8cf4f8018a1c5", + "reference": "9f8dbf0dceb03815c3160a279bf8cf4f8018a1c5", "shasum": "" }, "require": { @@ -4466,20 +4507,20 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2018-08-07T09:33:53+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "f62a394bd3de96f2f5e8f4c7d685035897fb3cb3" + "reference": "ff8ac19e97e5c7c3979236b584719a1190f84181" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f62a394bd3de96f2f5e8f4c7d685035897fb3cb3", - "reference": "f62a394bd3de96f2f5e8f4c7d685035897fb3cb3", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ff8ac19e97e5c7c3979236b584719a1190f84181", + "reference": "ff8ac19e97e5c7c3979236b584719a1190f84181", "shasum": "" }, "require": { @@ -4535,20 +4576,20 @@ "debug", "dump" ], - "time": "2018-07-26T11:19:56+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c2f4812ead9f847cb69e90917ca7502e6892d6b8" + "reference": "640b6c27fed4066d64b64d5903a86043f4a4de7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c2f4812ead9f847cb69e90917ca7502e6892d6b8", - "reference": "c2f4812ead9f847cb69e90917ca7502e6892d6b8", + "url": "https://api.github.com/repos/symfony/yaml/zipball/640b6c27fed4066d64b64d5903a86043f4a4de7f", + "reference": "640b6c27fed4066d64b64d5903a86043f4a4de7f", "shasum": "" }, "require": { @@ -4594,7 +4635,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-08-10T07:34:36+00:00" + "time": "2018-10-02T16:33:53+00:00" }, { "name": "twig/twig", @@ -5238,12 +5279,12 @@ "source": { "type": "git", "url": "https://github.com/minkphp/MinkSelenium2Driver.git", - "reference": "f4efaf52fed6a98bf6037a04f3fdef94dc0c8841" + "reference": "8684ee4e634db7abda9039ea53545f86fc1e105a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/f4efaf52fed6a98bf6037a04f3fdef94dc0c8841", - "reference": "f4efaf52fed6a98bf6037a04f3fdef94dc0c8841", + "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/8684ee4e634db7abda9039ea53545f86fc1e105a", + "reference": "8684ee4e634db7abda9039ea53545f86fc1e105a", "shasum": "" }, "require": { @@ -5291,7 +5332,7 @@ "testing", "webdriver" ], - "time": "2018-07-23T10:51:47+00:00" + "time": "2018-10-10T12:39:06+00:00" }, { "name": "doctrine/instantiator", @@ -5349,22 +5390,28 @@ }, { "name": "drupal/coder", - "version": "8.2.12", + "version": "8.3.1", "source": { "type": "git", "url": "https://git.drupal.org/project/coder.git", - "reference": "984c54a7b1e8f27ff1c32348df69712afd86b17f" + "reference": "29a25627e7148b3119c84f18e087fc3b8c85b959" }, "require": { "ext-mbstring": "*", "php": ">=5.4.0", - "squizlabs/php_codesniffer": ">=2.8.1 <3.0", + "squizlabs/php_codesniffer": "^3.0.1", "symfony/yaml": ">=2.0.0" }, "require-dev": { "phpunit/phpunit": ">=3.7 <6" }, "type": "phpcodesniffer-standard", + "autoload": { + "psr-0": { + "Drupal\\": "coder_sniffer/Drupal/", + "DrupalPractice\\": "coder_sniffer/Drupal/" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0+" @@ -5376,7 +5423,7 @@ "phpcs", "standards" ], - "time": "2017-03-18T10:28:49+00:00" + "time": "2018-09-21T14:22:49+00:00" }, { "name": "fabpot/goutte", @@ -6968,64 +7015,37 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "2.9.1", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "dcbed1074f8244661eecddfc2a675430d8d33f62" + "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dcbed1074f8244661eecddfc2a675430d8d33f62", - "reference": "dcbed1074f8244661eecddfc2a675430d8d33f62", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6ad28354c04b364c3c71a34e4a18b629cc3b231e", + "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e", "shasum": "" }, "require": { "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": ">=5.1.2" + "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "bin": [ - "scripts/phpcs", - "scripts/phpcbf" + "bin/phpcs", + "bin/phpcbf" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "3.x-dev" } }, - "autoload": { - "classmap": [ - "CodeSniffer.php", - "CodeSniffer/CLI.php", - "CodeSniffer/Exception.php", - "CodeSniffer/File.php", - "CodeSniffer/Fixer.php", - "CodeSniffer/Report.php", - "CodeSniffer/Reporting.php", - "CodeSniffer/Sniff.php", - "CodeSniffer/Tokens.php", - "CodeSniffer/Reports/", - "CodeSniffer/Tokenizers/", - "CodeSniffer/DocGenerators/", - "CodeSniffer/Standards/AbstractPatternSniff.php", - "CodeSniffer/Standards/AbstractScopeSniff.php", - "CodeSniffer/Standards/AbstractVariableSniff.php", - "CodeSniffer/Standards/IncorrectPatternException.php", - "CodeSniffer/Standards/Generic/Sniffs/", - "CodeSniffer/Standards/MySource/Sniffs/", - "CodeSniffer/Standards/PEAR/Sniffs/", - "CodeSniffer/Standards/PSR1/Sniffs/", - "CodeSniffer/Standards/PSR2/Sniffs/", - "CodeSniffer/Standards/Squiz/Sniffs/", - "CodeSniffer/Standards/Zend/Sniffs/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -7042,11 +7062,11 @@ "phpcs", "standards" ], - "time": "2017-05-22T02:43:20+00:00" + "time": "2018-09-23T23:08:17+00:00" }, { "name": "symfony/browser-kit", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -7103,16 +7123,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v3.4.15", + "version": "v3.4.17", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "f4fde1ede82c7ca2a4f06cf48521a185b26c0fed" + "reference": "76e013a98031356604e5a730c9eb22713dc4dda4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/f4fde1ede82c7ca2a4f06cf48521a185b26c0fed", - "reference": "f4fde1ede82c7ca2a4f06cf48521a185b26c0fed", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/76e013a98031356604e5a730c9eb22713dc4dda4", + "reference": "76e013a98031356604e5a730c9eb22713dc4dda4", "shasum": "" }, "require": { @@ -7165,7 +7185,7 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", - "time": "2018-08-27T15:17:06+00:00" + "time": "2018-10-02T12:28:39+00:00" }, { "name": "theseer/tokenizer", diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 575ffbd3c..130183e73 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -82,7 +82,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '8.6.1'; + const VERSION = '8.6.2'; /** * Core API compatibility. diff --git a/core/lib/Drupal/Component/Utility/UrlHelper.php b/core/lib/Drupal/Component/Utility/UrlHelper.php index 1d133c9d6..9e2365c1a 100644 --- a/core/lib/Drupal/Component/Utility/UrlHelper.php +++ b/core/lib/Drupal/Component/Utility/UrlHelper.php @@ -248,6 +248,16 @@ class UrlHelper { * Exception thrown when a either $url or $bath_url are not fully qualified. */ public static function externalIsLocal($url, $base_url) { + // Some browsers treat \ as / so normalize to forward slashes. + $url = str_replace('\\', '/', $url); + + // Leading control characters may be ignored or mishandled by browsers, so + // assume such a path may lead to an non-local location. The \p{C} character + // class matches all UTF-8 control, unassigned, and private characters. + if (preg_match('/^\p{C}/u', $url) !== 0) { + return FALSE; + } + $url_parts = parse_url($url); $base_parts = parse_url($base_url); diff --git a/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php index 8397bdef4..67a4aae42 100644 --- a/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/RedirectResponseSubscriber.php @@ -8,7 +8,6 @@ use Drupal\Core\Routing\LocalRedirectResponse; use Drupal\Core\Routing\RequestContext; use Drupal\Core\Utility\UnroutedUrlAssemblerInterface; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -129,36 +128,6 @@ class RedirectResponseSubscriber implements EventSubscriberInterface { return $destination; } - /** - * Sanitize the destination parameter to prevent open redirect attacks. - * - * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event - * The Event to process. - */ - public function sanitizeDestination(GetResponseEvent $event) { - $request = $event->getRequest(); - // Sanitize the destination parameter (which is often used for redirects) to - // prevent open redirect attacks leading to other domains. Sanitize both - // $_GET['destination'] and $_REQUEST['destination'] to protect code that - // relies on either, but do not sanitize $_POST to avoid interfering with - // unrelated form submissions. The sanitization happens here because - // url_is_external() requires the variable system to be available. - $query_info = $request->query; - $request_info = $request->request; - if ($query_info->has('destination') || $request_info->has('destination')) { - // If the destination is an external URL, remove it. - if ($query_info->has('destination') && UrlHelper::isExternal($query_info->get('destination'))) { - $query_info->remove('destination'); - $request_info->remove('destination'); - } - // If there's still something in $_REQUEST['destination'] that didn't come - // from $_GET, check it too. - if ($request_info->has('destination') && (!$query_info->has('destination') || $request_info->get('destination') != $query_info->get('destination')) && UrlHelper::isExternal($request_info->get('destination'))) { - $request_info->remove('destination'); - } - } - } - /** * Registers the methods in this class that should be listeners. * @@ -167,7 +136,6 @@ class RedirectResponseSubscriber implements EventSubscriberInterface { */ public static function getSubscribedEvents() { $events[KernelEvents::RESPONSE][] = ['checkRedirectUrl']; - $events[KernelEvents::REQUEST][] = ['sanitizeDestination', 100]; return $events; } diff --git a/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php b/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php index e5361492b..27e5c76e5 100644 --- a/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php +++ b/core/lib/Drupal/Core/Mail/Plugin/Mail/PhpMail.php @@ -18,6 +18,20 @@ use Drupal\Core\Site\Settings; */ class PhpMail implements MailInterface { + /** + * The configuration factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * PhpMail constructor. + */ + public function __construct() { + $this->configFactory = \Drupal::configFactory(); + } + /** * Concatenates and wraps the email body for plain-text mails. * @@ -86,7 +100,10 @@ class PhpMail implements MailInterface { // On most non-Windows systems, the "-f" option to the sendmail command // is used to set the Return-Path. There is no space between -f and // the value of the return path. - $additional_headers = isset($message['Return-Path']) ? '-f' . $message['Return-Path'] : ''; + // We validate the return path, unless it is equal to the site mail, which + // we assume to be safe. + $site_mail = $this->configFactory->get('system.site')->get('mail'); + $additional_headers = isset($message['Return-Path']) && ($site_mail === $message['Return-Path'] || static::_isShellSafe($message['Return-Path'])) ? '-f' . $message['Return-Path'] : ''; $mail_result = @mail( $message['to'], $mail_subject, @@ -112,4 +129,33 @@ class PhpMail implements MailInterface { return $mail_result; } + /** + * Disallows potentially unsafe shell characters. + * + * Functionally similar to PHPMailer::isShellSafe() which resulted from + * CVE-2016-10045. Note that escapeshellarg and escapeshellcmd are inadequate + * for this purpose. + * + * @param string $string + * The string to be validated. + * + * @return bool + * True if the string is shell-safe. + * + * @see https://github.com/PHPMailer/PHPMailer/issues/924 + * @see https://github.com/PHPMailer/PHPMailer/blob/v5.2.21/class.phpmailer.php#L1430 + * + * @todo Rename to ::isShellSafe() and/or discuss whether this is the correct + * location for this helper. + */ + protected static function _isShellSafe($string) { + if (escapeshellcmd($string) !== $string || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])) { + return FALSE; + } + if (preg_match('/[^a-zA-Z0-9@_\-.]/', $string) !== 0) { + return FALSE; + } + return TRUE; + } + } diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php index b85737f3c..d0690fee2 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php @@ -43,6 +43,15 @@ class PathProcessorAlias implements InboundPathProcessorInterface, OutboundPathP if (empty($options['alias'])) { $langcode = isset($options['language']) ? $options['language']->getId() : NULL; $path = $this->aliasManager->getAliasByPath($path, $langcode); + // Ensure the resulting path has at most one leading slash, to prevent it + // becoming an external URL without a protocol like //example.com. This + // is done in \Drupal\Core\Routing\UrlGenerator::generateFromRoute() + // also, to protect against this problem in arbitrary path processors, + // but it is duplicated here to protect any other URL generation code + // that might call this method separately. + if (strpos($path, '//') === 0) { + $path = '/' . ltrim($path, '/'); + } } return $path; } diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index 3b5c8d256..853a5f7b4 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -297,6 +297,11 @@ class UrlGenerator implements UrlGeneratorInterface { if ($options['path_processing']) { $path = $this->processPath($path, $options, $generated_url); } + // Ensure the resulting path has at most one leading slash, to prevent it + // becoming an external URL without a protocol like //example.com. + if (strpos($path, '//') === 0) { + $path = '/' . ltrim($path, '/'); + } // The contexts base URL is already encoded // (see Symfony\Component\HttpFoundation\Request). $path = str_replace($this->decodedChars[0], $this->decodedChars[1], rawurlencode($path)); diff --git a/core/lib/Drupal/Core/Security/RequestSanitizer.php b/core/lib/Drupal/Core/Security/RequestSanitizer.php index 3f48f3d59..e1626ed38 100644 --- a/core/lib/Drupal/Core/Security/RequestSanitizer.php +++ b/core/lib/Drupal/Core/Security/RequestSanitizer.php @@ -90,7 +90,8 @@ class RequestSanitizer { } if ($bag->has('destination')) { - $destination_dangerous_keys = static::checkDestination($bag->get('destination'), $whitelist); + $destination = $bag->get('destination'); + $destination_dangerous_keys = static::checkDestination($destination, $whitelist); if (!empty($destination_dangerous_keys)) { // The destination is removed rather than sanitized because the URL // generator service is not available and this method is called very @@ -101,6 +102,16 @@ class RequestSanitizer { trigger_error(sprintf('Potentially unsafe destination removed from %s parameter bag because it contained the following keys: %s', $bag_name, implode(', ', $destination_dangerous_keys))); } } + // Sanitize the destination parameter (which is often used for redirects) + // to prevent open redirect attacks leading to other domains. + if (UrlHelper::isExternal($destination)) { + // The destination is removed because it is an external URL. + $bag->remove('destination'); + $sanitized = TRUE; + if ($log_sanitized_keys) { + trigger_error(sprintf('Potentially unsafe destination removed from %s parameter bag because it points to an external URL.', $bag_name)); + } + } } return $sanitized; } diff --git a/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php b/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php index 0a18d7fa0..cdb199824 100644 --- a/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php +++ b/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php @@ -3,6 +3,8 @@ namespace Drupal\Tests\block\Functional\Views; use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Crypt; +use Drupal\Core\Site\Settings; use Drupal\Core\Url; use Drupal\Tests\block\Functional\AssertBlockAppearsTrait; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; @@ -360,14 +362,16 @@ class DisplayBlockTest extends ViewTestBase { $this->drupalGet('test-page'); $id = 'block:block=' . $block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en'; + $id_token = Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get()); $cached_id = 'block:block=' . $cached_block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en'; + $cached_id_token = Crypt::hmacBase64($cached_id, Settings::getHashSalt() . $this->container->get('private_key')->get()); // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder() - $this->assertRaw(' $id]) . '>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $id])); - $this->assertRaw(' $cached_id]) . '>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $cached_id])); + $this->assertRaw(' $id, 'data-contextual-token' => $id_token]) . '>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $id])); + $this->assertRaw(' $cached_id, 'data-contextual-token' => $cached_id_token]) . '>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $cached_id])); // Get server-rendered contextual links. // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks() - $post = ['ids[0]' => $id, 'ids[1]' => $cached_id]; + $post = ['ids[0]' => $id, 'ids[1]' => $cached_id, 'tokens[0]' => $id_token, 'tokens[1]' => $cached_id_token]; $url = 'contextual/render?_format=json,destination=test-page'; $this->getSession()->getDriver()->getClient()->request('POST', $url, $post); $this->assertResponse(200); diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php index 7f7c756b6..4fcde3605 100644 --- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php +++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php @@ -16,5 +16,6 @@ class ModerationStateConstraint extends Constraint { public $message = 'Invalid state transition from %from to %to'; public $invalidStateMessage = 'State %state does not exist on %workflow workflow'; + public $invalidTransitionAccess = 'You do not have access to transition from %original_state to %new_state'; } diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php index 65fc2a0c5..c3b9c815f 100644 --- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php +++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php @@ -2,10 +2,13 @@ namespace Drupal\content_moderation\Plugin\Validation\Constraint; +use Drupal\content_moderation\StateTransitionValidationInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\content_moderation\ModerationInformationInterface; +use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -29,6 +32,20 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements */ protected $moderationInformation; + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The state transition validation service. + * + * @var \Drupal\content_moderation\StateTransitionValidationInterface + */ + protected $stateTransitionValidation; + /** * Creates a new ModerationStateConstraintValidator instance. * @@ -36,10 +53,16 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements * The entity type manager. * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information * The moderation information. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + * @param \Drupal\content_moderation\StateTransitionValidationInterface $state_transition_validation + * The state transition validation service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information, AccountInterface $current_user, StateTransitionValidationInterface $state_transition_validation) { $this->entityTypeManager = $entity_type_manager; $this->moderationInformation = $moderation_information; + $this->currentUser = $current_user; + $this->stateTransitionValidation = $state_transition_validation; } /** @@ -48,7 +71,9 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), - $container->get('content_moderation.moderation_information') + $container->get('content_moderation.moderation_information'), + $container->get('current_user'), + $container->get('content_moderation.state_transition_validation') ); } @@ -76,32 +101,59 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements return; } + $new_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value); + $original_state = $this->getOriginalOrInitialState($entity); + // If a new state is being set and there is an existing state, validate // there is a valid transition between them. + if (!$original_state->canTransitionTo($new_state->id())) { + $this->context->addViolation($constraint->message, [ + '%from' => $original_state->label(), + '%to' => $new_state->label(), + ]); + } + else { + // If we're sure the transition exists, make sure the user has permission + // to use it. + if (!$this->stateTransitionValidation->isTransitionValid($workflow, $original_state, $new_state, $this->currentUser)) { + $this->context->addViolation($constraint->invalidTransitionAccess, [ + '%original_state' => $original_state->label(), + '%new_state' => $new_state->label(), + ]); + } + } + } + + /** + * Gets the original or initial state of the given entity. + * + * When a state is being validated, the original state is used to validate + * that a valid transition exists for target state and the user has access + * to the transition between those two states. If the entity has been + * moderated before, we can load the original unmodified revision and + * translation for this state. + * + * If the entity is new we need to load the initial state from the workflow. + * Even if a value was assigned to the moderation_state field, the initial + * state is used to compute an appropriate transition for the purposes of + * validation. + * + * @return \Drupal\workflows\StateInterface + * The original or default moderation state. + */ + protected function getOriginalOrInitialState(ContentEntityInterface $entity) { + $state = NULL; + $workflow_type = $this->moderationInformation->getWorkflowForEntity($entity)->getTypePlugin(); if (!$entity->isNew() && !$this->isFirstTimeModeration($entity)) { $original_entity = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadRevision($entity->getLoadedRevisionId()); if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) { $original_entity = $original_entity->getTranslation($entity->language()->getId()); } - - // If the state of the original entity doesn't exist on the workflow, - // we cannot do any further validation of transitions, because none will - // be setup for a state that doesn't exist. Instead allow any state to - // take its place. - if (!$workflow->getTypePlugin()->hasState($original_entity->moderation_state->value)) { - return; - } - - $new_state = $workflow->getTypePlugin()->getState($entity->moderation_state->value); - $original_state = $workflow->getTypePlugin()->getState($original_entity->moderation_state->value); - - if (!$original_state->canTransitionTo($new_state->id())) { - $this->context->addViolation($constraint->message, [ - '%from' => $original_state->label(), - '%to' => $new_state->label(), - ]); + if ($workflow_type->hasState($original_entity->moderation_state->value)) { + $state = $workflow_type->getState($original_entity->moderation_state->value); } } + return $state ?: $workflow_type->getInitialState($entity); } /** diff --git a/core/modules/content_moderation/src/StateTransitionValidation.php b/core/modules/content_moderation/src/StateTransitionValidation.php index 01b2ad845..35d657e55 100644 --- a/core/modules/content_moderation/src/StateTransitionValidation.php +++ b/core/modules/content_moderation/src/StateTransitionValidation.php @@ -4,7 +4,9 @@ namespace Drupal\content_moderation; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\workflows\StateInterface; use Drupal\workflows\Transition; +use Drupal\workflows\WorkflowInterface; /** * Validates whether a certain state transition is allowed. @@ -47,4 +49,12 @@ class StateTransitionValidation implements StateTransitionValidationInterface { }); } + /** + * {@inheritdoc} + */ + public function isTransitionValid(WorkflowInterface $workflow, StateInterface $original_state, StateInterface $new_state, AccountInterface $user) { + $transition = $workflow->getTypePlugin()->getTransitionFromStateToState($original_state->id(), $new_state->id()); + return $user->hasPermission('use ' . $workflow->id() . ' transition ' . $transition->id()); + } + } diff --git a/core/modules/content_moderation/src/StateTransitionValidationInterface.php b/core/modules/content_moderation/src/StateTransitionValidationInterface.php index 1acbf052f..c793fe53e 100644 --- a/core/modules/content_moderation/src/StateTransitionValidationInterface.php +++ b/core/modules/content_moderation/src/StateTransitionValidationInterface.php @@ -4,6 +4,8 @@ namespace Drupal\content_moderation; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\workflows\StateInterface; +use Drupal\workflows\WorkflowInterface; /** * Validates whether a certain state transition is allowed. @@ -23,4 +25,21 @@ interface StateTransitionValidationInterface { */ public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user); + /** + * Checks if a transition between two states if valid for the given user. + * + * @param \Drupal\workflows\WorkflowInterface $workflow + * The workflow entity. + * @param \Drupal\workflows\StateInterface $original_state + * The original workflow state. + * @param \Drupal\workflows\StateInterface $new_state + * The new workflow state. + * @param \Drupal\Core\Session\AccountInterface $user + * The user to validate. + * + * @return bool + * Returns TRUE if transition is valid, otherwise FALSE. + */ + public function isTransitionValid(WorkflowInterface $workflow, StateInterface $original_state, StateInterface $new_state, AccountInterface $user); + } diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php index 11deaa72c..5fd168d0b 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php @@ -158,32 +158,15 @@ class ModerationStateNodeTest extends ModerationStateTestBase { ]); $this->drupalLogin($limited_user); - // Check the user can add content, but can't see the moderation state - // select. + // Check the user can see the content entity form, but can't see the + // moderation state select or save the entity form. $this->drupalGet('node/add/moderated_content'); $session_assert->statusCodeEquals(200); $session_assert->fieldNotExists('moderation_state[0][state]'); $this->drupalPostForm(NULL, [ 'title[0][value]' => 'moderated content', ], 'Save'); - - // Manually move the content to archived because the user doesn't have - // permission to do this. - $node = $this->getNodeByTitle('moderated content'); - $node->moderation_state->value = 'archived'; - $node->save(); - - // Check the user can see the current state but not the select. - $this->drupalGet('node/' . $node->id() . '/edit'); - $session_assert->statusCodeEquals(200); - $session_assert->pageTextContains('Archived'); - $session_assert->fieldNotExists('moderation_state[0][state]'); - $this->drupalPostForm(NULL, [], 'Save'); - - // When saving they should still be on the edit form, and see the validation - // error message. - $session_assert->pageTextContains('Edit Moderated content moderated content'); - $session_assert->pageTextContains('Invalid state transition from Archived to Archived'); + $session_assert->pageTextContains('You do not have access to transition from Draft to Draft'); } } diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php index dc1e7f691..df90bf63c 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php @@ -7,6 +7,7 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; /** * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator @@ -15,6 +16,7 @@ use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; class EntityStateChangeValidationTest extends KernelTestBase { use ContentModerationTestTrait; + use UserCreationTrait; /** * {@inheritdoc} @@ -29,6 +31,13 @@ class EntityStateChangeValidationTest extends KernelTestBase { 'workflows', ]; + /** + * An admin user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $adminUser; + /** * {@inheritdoc} */ @@ -40,6 +49,9 @@ class EntityStateChangeValidationTest extends KernelTestBase { $this->installEntitySchema('user'); $this->installEntitySchema('content_moderation_state'); $this->installConfig('content_moderation'); + $this->installSchema('system', ['sequences']); + + $this->adminUser = $this->createUser(array_keys($this->container->get('user.permissions')->getPermissions())); } /** @@ -48,6 +60,8 @@ class EntityStateChangeValidationTest extends KernelTestBase { * @covers ::validate */ public function testValidTransition() { + $this->setCurrentUser($this->adminUser); + $node_type = NodeType::create([ 'type' => 'example', ]); @@ -76,6 +90,8 @@ class EntityStateChangeValidationTest extends KernelTestBase { * @covers ::validate */ public function testInvalidTransition() { + $this->setCurrentUser($this->adminUser); + $node_type = NodeType::create([ 'type' => 'example', ]); @@ -125,6 +141,7 @@ class EntityStateChangeValidationTest extends KernelTestBase { * Test validation with content that has no initial state or an invalid state. */ public function testInvalidStateWithoutExisting() { + $this->setCurrentUser($this->adminUser); // Create content without moderation enabled for the content type. $node_type = NodeType::create([ 'type' => 'example', @@ -156,15 +173,24 @@ class EntityStateChangeValidationTest extends KernelTestBase { // validating. $workflow->getTypePlugin()->deleteState('deleted_state'); $workflow->save(); + + // When there is an invalid state, the content will revert to "draft". This + // will allow a draft to draft transition. $node->moderation_state->value = 'draft'; $violations = $node->validate(); $this->assertCount(0, $violations); + // This will disallow a draft to archived transition. + $node->moderation_state->value = 'archived'; + $violations = $node->validate(); + $this->assertCount(1, $violations); } /** * Test state transition validation with multiple languages. */ public function testInvalidStateMultilingual() { + $this->setCurrentUser($this->adminUser); + ConfigurableLanguage::createFromLangcode('fr')->save(); $node_type = NodeType::create([ 'type' => 'example', @@ -220,6 +246,8 @@ class EntityStateChangeValidationTest extends KernelTestBase { * Tests that content without prior moderation information can be moderated. */ public function testExistingContentWithNoModeration() { + $this->setCurrentUser($this->adminUser); + $node_type = NodeType::create([ 'type' => 'example', ]); @@ -254,6 +282,8 @@ class EntityStateChangeValidationTest extends KernelTestBase { * Tests that content without prior moderation information can be translated. */ public function testExistingMultilingualContentWithNoModeration() { + $this->setCurrentUser($this->adminUser); + // Enable French. ConfigurableLanguage::createFromLangcode('fr')->save(); @@ -293,4 +323,81 @@ class EntityStateChangeValidationTest extends KernelTestBase { $node_fr->save(); } + /** + * @dataProvider transitionAccessValidationTestCases + */ + public function testTransitionAccessValidation($permissions, $target_state, $messages) { + $node_type = NodeType::create([ + 'type' => 'example', + ]); + $node_type->save(); + $workflow = $this->createEditorialWorkflow(); + $workflow->getTypePlugin()->addState('foo', 'Foo'); + $workflow->getTypePlugin()->addTransition('draft_to_foo', 'Draft to foo', ['draft'], 'foo'); + $workflow->getTypePlugin()->addTransition('foo_to_foo', 'Foo to foo', ['foo'], 'foo'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); + + $this->setCurrentUser($this->createUser($permissions)); + + $node = Node::create([ + 'type' => 'example', + 'title' => 'Test content', + 'moderation_state' => $target_state, + ]); + $this->assertTrue($node->isNew()); + $violations = $node->validate(); + $this->assertCount(count($messages), $violations); + foreach ($messages as $i => $message) { + $this->assertEquals($message, $violations->get($i)->getMessage()); + } + } + + /** + * Test cases for ::testTransitionAccessValidation. + */ + public function transitionAccessValidationTestCases() { + return [ + 'Invalid transition, no permissions validated' => [ + [], + 'archived', + ['Invalid state transition from Draft to Archived'], + ], + 'Valid transition, missing permission' => [ + [], + 'published', + ['You do not have access to transition from Draft to Published'], + ], + 'Valid transition, granted published permission' => [ + ['use editorial transition publish'], + 'published', + [], + ], + 'Valid transition, granted draft permission' => [ + ['use editorial transition create_new_draft'], + 'draft', + [], + ], + 'Valid transition, incorrect permission granted' => [ + ['use editorial transition create_new_draft'], + 'published', + ['You do not have access to transition from Draft to Published'], + ], + // Test with an additional state and set of transitions, since the + // "published" transition can start from either "draft" or "published", it + // does not capture bugs that fail to correctly distinguish the initial + // workflow state from the set state of a new entity. + 'Valid transition, granted foo permission' => [ + ['use editorial transition draft_to_foo'], + 'foo', + [], + ], + 'Valid transition, incorrect foo permission granted' => [ + ['use editorial transition foo_to_foo'], + 'foo', + ['You do not have access to transition from Draft to Foo'], + ], + ]; + } + } diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index b9d61b76d..8b9fc36fd 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -191,13 +191,19 @@ function _contextual_links_to_id($contextual_links) { /** * Unserializes the result of _contextual_links_to_id(). * - * @see _contextual_links_to_id + * Note that $id is user input. Before calling this method the ID should be + * checked against the token stored in the 'data-contextual-token' attribute + * which is passed via the 'tokens' request parameter to + * \Drupal\contextual\ContextualController::render(). * * @param string $id * A serialized representation of a #contextual_links property value array. * * @return array * The value for a #contextual_links property. + * + * @see _contextual_links_to_id() + * @see \Drupal\contextual\ContextualController::render() */ function _contextual_id_to_links($id) { $contextual_links = []; diff --git a/core/modules/contextual/contextual.post_update.php b/core/modules/contextual/contextual.post_update.php new file mode 100644 index 000000000..8decad05f --- /dev/null +++ b/core/modules/contextual/contextual.post_update.php @@ -0,0 +1,14 @@ + { - const html = storage.getItem(`Drupal.contextual.${contextualID}`); + const uncachedIDs = []; + const uncachedTokens = []; + ids.forEach(contextualID => { + const html = storage.getItem(`Drupal.contextual.${contextualID.id}`); if (html && html.length) { // Initialize after the current execution cycle, to make the AJAX // request for retrieving the uncached contextual links as soon as @@ -182,13 +186,14 @@ // Drupal.contextual.collection. window.setTimeout(() => { initContextual( - $context.find(`[data-contextual-id="${contextualID}"]`), + $context.find(`[data-contextual-id="${contextualID.id}"]`), html, ); }); - return false; + return; } - return true; + uncachedIDs.push(contextualID.id); + uncachedTokens.push(contextualID.token); }); // Perform an AJAX request to let the server render the contextual links @@ -197,7 +202,7 @@ $.ajax({ url: Drupal.url('contextual/render'), type: 'POST', - data: { 'ids[]': uncachedIDs }, + data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens }, dataType: 'json', success(results) { _.each(results, (html, contextualID) => { diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js index 049233b4e..d51eba21a 100644 --- a/core/modules/contextual/js/contextual.js +++ b/core/modules/contextual/js/contextual.js @@ -95,25 +95,31 @@ var ids = []; $placeholders.each(function () { - ids.push($(this).attr('data-contextual-id')); + ids.push({ + id: $(this).attr('data-contextual-id'), + token: $(this).attr('data-contextual-token') + }); }); - var uncachedIDs = _.filter(ids, function (contextualID) { - var html = storage.getItem('Drupal.contextual.' + contextualID); + var uncachedIDs = []; + var uncachedTokens = []; + ids.forEach(function (contextualID) { + var html = storage.getItem('Drupal.contextual.' + contextualID.id); if (html && html.length) { window.setTimeout(function () { - initContextual($context.find('[data-contextual-id="' + contextualID + '"]'), html); + initContextual($context.find('[data-contextual-id="' + contextualID.id + '"]'), html); }); - return false; + return; } - return true; + uncachedIDs.push(contextualID.id); + uncachedTokens.push(contextualID.token); }); if (uncachedIDs.length > 0) { $.ajax({ url: Drupal.url('contextual/render'), type: 'POST', - data: { 'ids[]': uncachedIDs }, + data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens }, dataType: 'json', success: function success(results) { _.each(results, function (html, contextualID) { diff --git a/core/modules/contextual/src/ContextualController.php b/core/modules/contextual/src/ContextualController.php index 58e42ecd6..d05c6a852 100644 --- a/core/modules/contextual/src/ContextualController.php +++ b/core/modules/contextual/src/ContextualController.php @@ -2,8 +2,10 @@ namespace Drupal\contextual; +use Drupal\Component\Utility\Crypt; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Site\Settings; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -63,8 +65,16 @@ class ContextualController implements ContainerInjectionInterface { throw new BadRequestHttpException(t('No contextual ids specified.')); } + $tokens = $request->request->get('tokens'); + if (!isset($tokens)) { + throw new BadRequestHttpException(t('No contextual ID tokens specified.')); + } + $rendered = []; - foreach ($ids as $id) { + foreach ($ids as $key => $id) { + if (!isset($tokens[$key]) || !Crypt::hashEquals($tokens[$key], Crypt::hmacBase64($id, Settings::getHashSalt() . \Drupal::service('private_key')->get()))) { + throw new BadRequestHttpException('Invalid contextual ID specified.'); + } $element = [ '#type' => 'contextual_links', '#contextual_links' => _contextual_id_to_links($id), diff --git a/core/modules/contextual/src/Element/ContextualLinksPlaceholder.php b/core/modules/contextual/src/Element/ContextualLinksPlaceholder.php index 97afde9a2..5e993941a 100644 --- a/core/modules/contextual/src/Element/ContextualLinksPlaceholder.php +++ b/core/modules/contextual/src/Element/ContextualLinksPlaceholder.php @@ -2,6 +2,8 @@ namespace Drupal\contextual\Element; +use Drupal\Component\Utility\Crypt; +use Drupal\Core\Site\Settings; use Drupal\Core\Template\Attribute; use Drupal\Core\Render\Element\RenderElement; use Drupal\Component\Render\FormattableMarkup; @@ -43,7 +45,12 @@ class ContextualLinksPlaceholder extends RenderElement { * @see _contextual_links_to_id() */ public static function preRenderPlaceholder(array $element) { - $element['#markup'] = new FormattableMarkup('', ['@attributes' => new Attribute(['data-contextual-id' => $element['#id']])]); + $token = Crypt::hmacBase64($element['#id'], Settings::getHashSalt() . \Drupal::service('private_key')->get()); + $attribute = new Attribute([ + 'data-contextual-id' => $element['#id'], + 'data-contextual-token' => $token, + ]); + $element['#markup'] = new FormattableMarkup('', ['@attributes' => $attribute]); return $element; } diff --git a/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php b/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php index 340b60821..74a6d504e 100644 --- a/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php +++ b/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php @@ -3,9 +3,10 @@ namespace Drupal\Tests\contextual\Functional; use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Crypt; +use Drupal\Core\Site\Settings; use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\Core\Template\Attribute; use Drupal\Tests\BrowserTestBase; /** @@ -140,17 +141,76 @@ class ContextualDynamicContextTest extends BrowserTestBase { $this->assertRaw(''); } + /** + * Tests the contextual placeholder content is protected by a token. + */ + public function testTokenProtection() { + $this->drupalLogin($this->editorUser); + + // Create a node that will have a contextual link. + $node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]); + + // Now, on the front page, all article nodes should have contextual links + // placeholders, as should the view that contains them. + $id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en'; + + // Editor user: can access contextual links and can edit articles. + $this->drupalGet('node'); + $this->assertContextualLinkPlaceHolder($id); + + $http_client = $this->getHttpClient(); + $url = Url::fromRoute('contextual.render', [], [ + 'query' => [ + '_format' => 'json', + 'destination' => 'node', + ], + ])->setAbsolute()->toString(); + + $response = $http_client->request('POST', $url, [ + 'cookies' => $this->getSessionCookies(), + 'form_params' => ['ids' => [$id], 'tokens' => []], + 'http_errors' => FALSE, + ]); + $this->assertEquals('400', $response->getStatusCode()); + $this->assertContains('No contextual ID tokens specified.', (string) $response->getBody()); + + $response = $http_client->request('POST', $url, [ + 'cookies' => $this->getSessionCookies(), + 'form_params' => ['ids' => [$id], 'tokens' => ['wrong_token']], + 'http_errors' => FALSE, + ]); + $this->assertEquals('400', $response->getStatusCode()); + $this->assertContains('Invalid contextual ID specified.', (string) $response->getBody()); + + $response = $http_client->request('POST', $url, [ + 'cookies' => $this->getSessionCookies(), + 'form_params' => ['ids' => [$id], 'tokens' => ['wrong_key' => $this->createContextualIdToken($id)]], + 'http_errors' => FALSE, + ]); + $this->assertEquals('400', $response->getStatusCode()); + $this->assertContains('Invalid contextual ID specified.', (string) $response->getBody()); + + $response = $http_client->request('POST', $url, [ + 'cookies' => $this->getSessionCookies(), + 'form_params' => ['ids' => [$id], 'tokens' => [$this->createContextualIdToken($id)]], + 'http_errors' => FALSE, + ]); + $this->assertEquals('200', $response->getStatusCode()); + } + /** * Asserts that a contextual link placeholder with the given id exists. * * @param string $id * A contextual link id. - * - * @return bool - * The result of the assertion. */ protected function assertContextualLinkPlaceHolder($id) { - return $this->assertRaw(' $id]) . '>', format_string('Contextual link placeholder with id @id exists.', ['@id' => $id])); + $this->assertSession()->elementAttributeContains( + 'css', + 'div[data-contextual-id="' . $id . '"]', + 'data-contextual-token', + $this->createContextualIdToken($id) + ); } /** @@ -158,12 +218,9 @@ class ContextualDynamicContextTest extends BrowserTestBase { * * @param string $id * A contextual link id. - * - * @return bool - * The result of the assertion. */ protected function assertNoContextualLinkPlaceHolder($id) { - return $this->assertNoRaw(' $id]) . '>', format_string('Contextual link placeholder with id @id does not exist.', ['@id' => $id])); + $this->assertSession()->elementNotExists('css', 'div[data-contextual-id="' . $id . '"]'); } /** @@ -178,6 +235,7 @@ class ContextualDynamicContextTest extends BrowserTestBase { * The response object. */ protected function renderContextualLinks($ids, $current_path) { + $tokens = array_map([$this, 'createContextualIdToken'], $ids); $http_client = $this->getHttpClient(); $url = Url::fromRoute('contextual.render', [], [ 'query' => [ @@ -188,9 +246,22 @@ class ContextualDynamicContextTest extends BrowserTestBase { return $http_client->request('POST', $this->buildUrl($url), [ 'cookies' => $this->getSessionCookies(), - 'form_params' => ['ids' => $ids], + 'form_params' => ['ids' => $ids, 'tokens' => $tokens], 'http_errors' => FALSE, ]); } + /** + * Creates a contextual ID token. + * + * @param string $id + * The contextual ID to create a token for. + * + * @return string + * The contextual ID token. + */ + protected function createContextualIdToken($id) { + return Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get()); + } + } diff --git a/core/modules/node/src/Tests/Views/NodeContextualLinksTest.php b/core/modules/node/src/Tests/Views/NodeContextualLinksTest.php deleted file mode 100644 index dc23d0ce5..000000000 --- a/core/modules/node/src/Tests/Views/NodeContextualLinksTest.php +++ /dev/null @@ -1,118 +0,0 @@ -drupalCreateContentType(['type' => 'page']); - $this->drupalCreateNode(['promote' => 1]); - $this->drupalGet('node'); - - $user = $this->drupalCreateUser(['administer nodes', 'access contextual links']); - $this->drupalLogin($user); - - $response = $this->renderContextualLinks(['node:node=1:'], 'node'); - $this->assertResponse(200); - $json = Json::decode($response); - $this->setRawContent($json['node:node=1:']); - - // @todo Add these back when the functionality for making Views displays - // appear in contextual links is working again. - // $this->assertLinkByHref('node/1/contextual-links', 0, 'The contextual link to the view was found.'); - // $this->assertLink('Test contextual link', 0, 'The contextual link to the view was found.'); - } - - /** - * Get server-rendered contextual links for the given contextual link ids. - * - * Copied from \Drupal\contextual\Tests\ContextualDynamicContextTest::renderContextualLinks(). - * - * @param array $ids - * An array of contextual link ids. - * @param string $current_path - * The Drupal path for the page for which the contextual links are rendered. - * - * @return string - * The response body. - */ - protected function renderContextualLinks($ids, $current_path) { - // Build POST values. - $post = []; - for ($i = 0; $i < count($ids); $i++) { - $post['ids[' . $i . ']'] = $ids[$i]; - } - - // Serialize POST values. - foreach ($post as $key => $value) { - // Encode according to application/x-www-form-urlencoded - // Both names and values needs to be urlencoded, according to - // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 - $post[$key] = urlencode($key) . '=' . urlencode($value); - } - $post = implode('&', $post); - - // Perform HTTP request. - return $this->curlExec([ - CURLOPT_URL => \Drupal::url('contextual.render', [], ['absolute' => TRUE, 'query' => ['destination' => $current_path]]), - CURLOPT_POST => TRUE, - CURLOPT_POSTFIELDS => $post, - CURLOPT_HTTPHEADER => [ - 'Accept: application/json', - 'Content-Type: application/x-www-form-urlencoded', - ], - ]); - } - - /** - * Tests if the node page works if Contextual Links is disabled. - * - * All views have Contextual links enabled by default, even with the - * Contextual links module disabled. This tests if no calls are done to the - * Contextual links module by views when it is disabled. - * - * @see https://www.drupal.org/node/2379811 - */ - public function testPageWithDisabledContextualModule() { - \Drupal::service('module_installer')->uninstall(['contextual']); - \Drupal::service('module_installer')->install(['views_ui']); - - // Ensure that contextual links don't get called for admin users. - $admin_user = User::load(1); - $admin_user->setPassword('new_password'); - $admin_user->pass_raw = 'new_password'; - $admin_user->save(); - - $this->drupalCreateContentType(['type' => 'page']); - $this->drupalCreateNode(['promote' => 1]); - - $this->drupalLogin($admin_user); - $this->drupalGet('node'); - } - -} diff --git a/core/modules/node/src/Tests/NodeRevisionsTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsTest.php similarity index 92% rename from core/modules/node/src/Tests/NodeRevisionsTest.php rename to core/modules/node/tests/src/Functional/NodeRevisionsTest.php index fdc929a84..6b16ce8bd 100644 --- a/core/modules/node/src/Tests/NodeRevisionsTest.php +++ b/core/modules/node/tests/src/Functional/NodeRevisionsTest.php @@ -1,6 +1,6 @@ assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.'); - // Confirm that the "Edit" and "Delete" contextual links appear for the - // default revision. - $ids = ['node:node=' . $node->id() . ':changed=' . $node->getChangedTime()]; - $json = $this->renderContextualLinks($ids, 'node/' . $node->id()); - $this->verbose($json[$ids[0]]); - - $expected = '
  • Edit
  • '; - $this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Edit" contextual link is shown for the default revision.'); - $expected = '
  • Delete
  • '; - $this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Delete" contextual link is shown for the default revision.'); - // Confirm that revisions revert properly. $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert", [], t('Revert')); $this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [ @@ -188,15 +177,6 @@ class NodeRevisionsTest extends NodeTestBase { $node = node_revision_load($node->getRevisionId()); $this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.'); - // Confirm that "Edit" and "Delete" contextual links don't appear for - // non-default revision. - $ids = ['node_revision::node=' . $node->id() . '&node_revision=' . $node->getRevisionId() . ':']; - $json = $this->renderContextualLinks($ids, 'node/' . $node->id() . '/revisions/' . $node->getRevisionId() . '/view'); - $this->verbose($json[$ids[0]]); - - $this->assertFalse(strstr($json[$ids[0]], '
  • '), 'The "Edit" contextual link is not shown for a non-default revision.'); - $this->assertFalse(strstr($json[$ids[0]], '
  • '), 'The "Delete" contextual link is not shown for a non-default revision.'); - // Confirm revisions delete properly. $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete", [], t('Delete')); $this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', [ diff --git a/core/modules/node/src/Tests/NodeTypeTest.php b/core/modules/node/tests/src/Functional/NodeTypeTest.php similarity index 94% rename from core/modules/node/src/Tests/NodeTypeTest.php rename to core/modules/node/tests/src/Functional/NodeTypeTest.php index 9938bb0ab..84c549e8d 100644 --- a/core/modules/node/src/Tests/NodeTypeTest.php +++ b/core/modules/node/tests/src/Functional/NodeTypeTest.php @@ -1,11 +1,12 @@ assertSession(); $this->drupalPlaceBlock('system_breadcrumb_block'); $web_user = $this->drupalCreateUser(['bypass node access', 'administer content types', 'administer node fields']); $this->drupalLogin($web_user); @@ -96,8 +99,8 @@ class NodeTypeTest extends NodeTestBase { // Verify that title and body fields are displayed. $this->drupalGet('node/add/page'); - $this->assertRaw('Title', 'Title field was found.'); - $this->assertRaw('Body', 'Body field was found.'); + $assert->pageTextContains('Title'); + $assert->pageTextContains('Body'); // Rename the title field. $edit = [ @@ -106,8 +109,8 @@ class NodeTypeTest extends NodeTestBase { $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); $this->drupalGet('node/add/page'); - $this->assertRaw('Foo', 'New title label was displayed.'); - $this->assertNoRaw('Title', 'Old title label was not displayed.'); + $assert->pageTextContains('Foo'); + $assert->pageTextNotContains('Title'); // Change the name and the description. $edit = [ @@ -117,11 +120,11 @@ class NodeTypeTest extends NodeTestBase { $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); $this->drupalGet('node/add'); - $this->assertRaw('Bar', 'New name was displayed.'); - $this->assertRaw('Lorem ipsum', 'New description was displayed.'); + $assert->pageTextContains('Bar'); + $assert->pageTextContains('Lorem ipsum'); $this->clickLink('Bar'); - $this->assertRaw('Foo', 'Title field was found.'); - $this->assertRaw('Body', 'Body field was found.'); + $assert->pageTextContains('Foo'); + $assert->pageTextContains('Body'); // Change the name through the API /** @var \Drupal\node\NodeTypeInterface $node_type */ @@ -146,7 +149,7 @@ class NodeTypeTest extends NodeTestBase { ]); // Check that the body field doesn't exist. $this->drupalGet('node/add/page'); - $this->assertNoRaw('Body', 'Body field was not found.'); + $assert->pageTextNotContains('Body'); } /** diff --git a/core/modules/node/src/Tests/PagePreviewTest.php b/core/modules/node/tests/src/Functional/PagePreviewTest.php similarity index 97% rename from core/modules/node/src/Tests/PagePreviewTest.php rename to core/modules/node/tests/src/Functional/PagePreviewTest.php index 2bc9cd3ce..70305349d 100644 --- a/core/modules/node/src/Tests/PagePreviewTest.php +++ b/core/modules/node/tests/src/Functional/PagePreviewTest.php @@ -1,6 +1,6 @@ drupalPostForm(NULL, ['field_image[0][alt]' => 'Picture of llamas'], t('Preview')); // Check that the preview is displaying the title, body and term. - $this->assertTitle(t('@title | Drupal', ['@title' => $edit[$title_key]]), 'Basic page title is preview.'); + $expected_title = $edit[$title_key] . ' | Drupal'; + $this->assertSession()->titleEquals($expected_title); $this->assertEscaped($edit[$title_key], 'Title displayed and escaped.'); $this->assertText($edit[$body_key], 'Body displayed.'); $this->assertText($edit[$term_key], 'Term displayed.'); @@ -210,13 +215,13 @@ class PagePreviewTest extends NodeTestBase { $this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.'); $this->assertFieldByName($term_key, $edit[$term_key], 'Term field displayed.'); $this->assertFieldByName('field_image[0][alt]', 'Picture of llamas'); - $this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')], NULL, [], [], 'node-page-form'); + $this->getSession()->getPage()->pressButton('Add another item'); $this->assertFieldByName('field_test_multi[0][value]'); $this->assertFieldByName('field_test_multi[1][value]'); // Return to page preview to check everything is as expected. $this->drupalPostForm(NULL, [], t('Preview')); - $this->assertTitle(t('@title | Drupal', ['@title' => $edit[$title_key]]), 'Basic page title is preview.'); + $this->assertSession()->titleEquals($expected_title); $this->assertEscaped($edit[$title_key], 'Title displayed and escaped.'); $this->assertText($edit[$body_key], 'Body displayed.'); $this->assertText($edit[$term_key], 'Term displayed.'); @@ -353,8 +358,8 @@ class PagePreviewTest extends NodeTestBase { $this->assertText('Basic page ' . $title . ' has been created.'); $node = $this->drupalGetNodeByTitle($title); $this->drupalGet('node/' . $node->id() . '/edit'); - $this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')]); - $this->drupalPostAjaxForm(NULL, [], ['field_test_multi_add_more' => t('Add another item')]); + $this->getSession()->getPage()->pressButton('Add another item'); + $this->getSession()->getPage()->pressButton('Add another item'); $edit = [ 'field_test_multi[1][value]' => $example_text_2, 'field_test_multi[2][value]' => $example_text_3, diff --git a/core/modules/node/tests/src/Functional/Views/NodeContextualLinksTest.php b/core/modules/node/tests/src/Functional/Views/NodeContextualLinksTest.php new file mode 100644 index 000000000..73ccfef75 --- /dev/null +++ b/core/modules/node/tests/src/Functional/Views/NodeContextualLinksTest.php @@ -0,0 +1,47 @@ +uninstall(['contextual']); + \Drupal::service('module_installer')->install(['views_ui']); + + // Ensure that contextual links don't get called for admin users. + $admin_user = User::load(1); + $admin_user->setPassword('new_password'); + $admin_user->passRaw = 'new_password'; + $admin_user->save(); + + $this->drupalCreateContentType(['type' => 'page']); + $this->drupalCreateNode(['promote' => 1]); + + $this->drupalLogin($admin_user); + $this->drupalGet('node'); + } + +} diff --git a/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php new file mode 100644 index 000000000..98051262e --- /dev/null +++ b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php @@ -0,0 +1,117 @@ +drupalCreateContentType([ + 'type' => 'page', + 'name' => 'Basic page', + 'display_submitted' => FALSE, + ]); + + // Create initial node. + $node = $this->drupalCreateNode(); + + $nodes = []; + + // Get original node. + $nodes[] = clone $node; + + // Create two revisions. + $revision_count = 2; + for ($i = 0; $i < $revision_count; $i++) { + + // Create revision with a random title and body and update variables. + $node->title = $this->randomMachineName(); + $node->body = [ + 'value' => $this->randomMachineName(32), + 'format' => filter_default_format(), + ]; + $node->setNewRevision(); + + $node->save(); + + // Make sure we get revision information. + $node = Node::load($node->id()); + $nodes[] = clone $node; + } + + $this->nodes = $nodes; + + $this->drupalLogin($this->createUser( + [ + 'view page revisions', + 'revert page revisions', + 'delete page revisions', + 'edit any page content', + 'delete any page content', + 'access contextual links', + 'administer content types', + ] + )); + } + + /** + * Tests the contextual links on revisions. + */ + public function testRevisionContextualLinks() { + // Confirm that the "Edit" and "Delete" contextual links appear for the + // default revision. + $this->drupalGet('node/' . $this->nodes[0]->id()); + $page = $this->getSession()->getPage(); + $page->waitFor(10, function () use ($page) { + return $page->find('css', "main .contextual"); + }); + + $this->toggleContextualTriggerVisibility('main'); + $page->find('css', 'main .contextual button')->press(); + $links = $page->findAll('css', "main .contextual-links li a"); + + $this->assertEquals('Edit', $links[0]->getText()); + $this->assertEquals('Delete', $links[1]->getText()); + + // Confirm that "Edit" and "Delete" contextual links don't appear for + // non-default revision. + $this->drupalGet("node/" . $this->nodes[0]->id() . "/revisions/" . $this->nodes[1]->getRevisionId() . "/view"); + $this->assertSession()->pageTextContains($this->nodes[1]->getTitle()); + $page->waitFor(10, function () use ($page) { + return $page->find('css', "main .contextual"); + }); + + $this->toggleContextualTriggerVisibility('main'); + $contextual_button = $page->find('css', 'main .contextual button'); + $this->assertEmpty(0, $contextual_button); + } + +} diff --git a/core/modules/path/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php index b8ac5968d..19115cd27 100644 --- a/core/modules/path/tests/src/Functional/PathAliasTest.php +++ b/core/modules/path/tests/src/Functional/PathAliasTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\path\Functional; use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Database; +use Drupal\Core\Url; /** * Add, edit, delete, and change alias and verify its consistency in the @@ -24,7 +25,7 @@ class PathAliasTest extends PathTestBase { parent::setUp(); // Create test user and log in. - $web_user = $this->drupalCreateUser(['create page content', 'edit own page content', 'administer url aliases', 'create url aliases']); + $web_user = $this->drupalCreateUser(['create page content', 'edit own page content', 'administer url aliases', 'create url aliases', 'access content overview']); $this->drupalLogin($web_user); } @@ -327,6 +328,34 @@ class PathAliasTest extends PathTestBase { $node5->delete(); $path_alias = \Drupal::service('path.alias_storage')->lookupPathAlias('/node/' . $node5->id(), $node5->language()->getId()); $this->assertFalse($path_alias, 'Alias was successfully deleted when the referenced node was deleted.'); + + // Create sixth test node. + $node6 = $this->drupalCreateNode(); + + // Create an invalid alias with two leading slashes and verify that the + // extra slash is removed when the link is generated. This ensures that URL + // aliases cannot be used to inject external URLs. + // @todo The user interface should either display an error message or + // automatically trim these invalid aliases, rather than allowing them to + // be silently created, at which point the functional aspects of this + // test will need to be moved elsewhere and switch to using a + // programmatically-created alias instead. + $alias = $this->randomMachineName(8); + $edit = ['path[0][alias]' => '//' . $alias]; + $this->drupalPostForm($node6->toUrl('edit-form'), $edit, t('Save')); + $this->drupalGet(Url::fromRoute('system.admin_content')); + // This checks the link href before clicking it, rather than using + // \Drupal\Tests\BrowserTestBase::assertSession()->addressEquals() after + // clicking it, because the test browser does not always preserve the + // correct number of slashes in the URL when it visits internal links; + // using \Drupal\Tests\BrowserTestBase::assertSession()->addressEquals() + // would actually make the test pass unconditionally on the testbot (or + // anywhere else where Drupal is installed in a subdirectory). + $link_xpath = $this->xpath('//a[normalize-space(text())=:label]', [':label' => $node6->getTitle()]); + $link_href = $link_xpath[0]->getAttribute('href'); + $this->assertEquals($link_href, base_path() . $alias); + $this->clickLink($node6->getTitle()); + $this->assertResponse(404); } /** diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php index 83a9c55b3..8d7c43e86 100644 --- a/core/modules/system/src/Tests/Routing/RouterTest.php +++ b/core/modules/system/src/Tests/Routing/RouterTest.php @@ -320,6 +320,13 @@ class RouterTest extends WebTestBase { $this->drupalGet($url); $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url); $this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test'); + + // Ensure that external URLs in destination query params are not redirected + // to. + $url = $request->getUriForPath('/////////////////////////////////////////////////router_test/test1') . '?qs=test&destination=http://www.example.com%5c@drupal8alt.test'; + $this->drupalGet($url); + $this->assertEqual(1, $this->redirectCount, $url . " redirected to " . $this->url); + $this->assertUrl($request->getUriForPath('/router_test/test1') . '?qs=test'); } } diff --git a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php index d185219c9..beaa472c2 100644 --- a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php @@ -563,6 +563,10 @@ class UrlHelperTest extends TestCase { ['http://example.com/foo', 'http://example.com/bar', FALSE], ['http://example.com', 'http://example.com/bar', FALSE], ['http://example.com/bar', 'http://example.com/bar/', FALSE], + // Ensure \ is normalised to / since some browsers do that. + ['http://www.example.ca\@example.com', 'http://example.com', FALSE], + // Some browsers ignore or strip leading control characters. + ["\x00//www.example.ca", 'http://example.com', FALSE], ]; } diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php index 8659a6f12..85b3da313 100644 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php @@ -11,7 +11,6 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; @@ -192,74 +191,4 @@ class RedirectResponseSubscriberTest extends UnitTestCase { return $data; } - /** - * Tests that $_GET only contain internal URLs. - * - * @covers ::sanitizeDestination - * - * @dataProvider providerTestSanitizeDestination - * - * @see \Drupal\Component\Utility\UrlHelper::isExternal - */ - public function testSanitizeDestinationForGet($input, $output) { - $request = new Request(); - $request->query->set('destination', $input); - - $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext); - $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); - $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); - - $dispatcher = new EventDispatcher(); - $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'sanitizeDestination'], 100); - $dispatcher->dispatch(KernelEvents::REQUEST, $event); - - $this->assertEquals($output, $request->query->get('destination')); - } - - /** - * Tests that $_REQUEST['destination'] only contain internal URLs. - * - * @covers ::sanitizeDestination - * - * @dataProvider providerTestSanitizeDestination - * - * @see \Drupal\Component\Utility\UrlHelper::isExternal - */ - public function testSanitizeDestinationForPost($input, $output) { - $request = new Request(); - $request->request->set('destination', $input); - - $listener = new RedirectResponseSubscriber($this->urlAssembler, $this->requestContext); - $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); - $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); - - $dispatcher = new EventDispatcher(); - $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'sanitizeDestination'], 100); - $dispatcher->dispatch(KernelEvents::REQUEST, $event); - - $this->assertEquals($output, $request->request->get('destination')); - } - - /** - * Data provider for testSanitizeDestination(). - */ - public function providerTestSanitizeDestination() { - $data = []; - // Standard internal example node path is present in the 'destination' - // parameter. - $data[] = ['node', 'node']; - // Internal path with one leading slash is allowed. - $data[] = ['/example.com', '/example.com']; - // External URL without scheme is not allowed. - $data[] = ['//example.com/test', '']; - // Internal URL using a colon is allowed. - $data[] = ['example:test', 'example:test']; - // External URL is not allowed. - $data[] = ['http://example.com', '']; - // Javascript URL is allowed because it is treated as an internal URL. - $data[] = ['javascript:alert(0)', 'javascript:alert(0)']; - - return $data; - } - } diff --git a/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php b/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php index ea523028a..de2e3d943 100644 --- a/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Mail/MailManagerTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Mail; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\Tests\UnitTestCase; @@ -103,6 +104,9 @@ class MailManagerTest extends UnitTestCase { 'system.mail' => [ 'interface' => $interface, ], + 'system.site' => [ + 'mail' => 'test@example.com', + ], ]); $logger_factory = $this->getMock('\Drupal\Core\Logger\LoggerChannelFactoryInterface'); $string_translation = $this->getStringTranslationStub(); @@ -110,6 +114,11 @@ class MailManagerTest extends UnitTestCase { // Construct the manager object and override its discovery. $this->mailManager = new TestMailManager(new \ArrayObject(), $this->cache, $this->moduleHandler, $this->configFactory, $logger_factory, $string_translation, $this->renderer); $this->mailManager->setDiscovery($this->discovery); + + // @see \Drupal\Core\Plugin\Factory\ContainerFactory::createInstance() + $container = new ContainerBuilder(); + $container->set('config.factory', $this->configFactory); + \Drupal::setContainer($container); } /** diff --git a/core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php b/core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php index 53147f3b7..e828de086 100644 --- a/core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php +++ b/core/tests/Drupal/Tests/Core/Security/RequestSanitizerTest.php @@ -197,6 +197,147 @@ class RequestSanitizerTest extends UnitTestCase { return $tests; } + /** + * Tests acceptable destinations are not removed from GET requests. + * + * @param string $destination + * The destination string to test. + * + * @dataProvider providerTestAcceptableDestinations + */ + public function testAcceptableDestinationGet($destination) { + // Set up a GET request. + $request = $this->createRequestForTesting(['destination' => $destination]); + + $request = RequestSanitizer::sanitize($request, [], TRUE); + + $this->assertSame($destination, $request->query->get('destination', NULL)); + $this->assertNull($request->request->get('destination', NULL)); + $this->assertSame($destination, $_GET['destination']); + $this->assertSame($destination, $_REQUEST['destination']); + $this->assertArrayNotHasKey('destination', $_POST); + $this->assertEquals([], $this->errors); + } + + /** + * Tests unacceptable destinations are removed from GET requests. + * + * @param string $destination + * The destination string to test. + * + * @dataProvider providerTestSanitizedDestinations + */ + public function testSanitizedDestinationGet($destination) { + // Set up a GET request. + $request = $this->createRequestForTesting(['destination' => $destination]); + + $request = RequestSanitizer::sanitize($request, [], TRUE); + + $this->assertNull($request->request->get('destination', NULL)); + $this->assertNull($request->query->get('destination', NULL)); + $this->assertArrayNotHasKey('destination', $_POST); + $this->assertArrayNotHasKey('destination', $_REQUEST); + $this->assertArrayNotHasKey('destination', $_GET); + $this->assertError('Potentially unsafe destination removed from query parameter bag because it points to an external URL.', E_USER_NOTICE); + } + + /** + * Tests acceptable destinations are not removed from POST requests. + * + * @param string $destination + * The destination string to test. + * + * @dataProvider providerTestAcceptableDestinations + */ + public function testAcceptableDestinationPost($destination) { + // Set up a POST request. + $request = $this->createRequestForTesting([], ['destination' => $destination]); + + $request = RequestSanitizer::sanitize($request, [], TRUE); + + $this->assertSame($destination, $request->request->get('destination', NULL)); + $this->assertNull($request->query->get('destination', NULL)); + $this->assertSame($destination, $_POST['destination']); + $this->assertSame($destination, $_REQUEST['destination']); + $this->assertArrayNotHasKey('destination', $_GET); + $this->assertEquals([], $this->errors); + } + + /** + * Tests unacceptable destinations are removed from GET requests. + * + * @param string $destination + * The destination string to test. + * + * @dataProvider providerTestSanitizedDestinations + */ + public function testSanitizedDestinationPost($destination) { + // Set up a POST request. + $request = $this->createRequestForTesting([], ['destination' => $destination]); + + $request = RequestSanitizer::sanitize($request, [], TRUE); + + $this->assertNull($request->request->get('destination', NULL)); + $this->assertNull($request->query->get('destination', NULL)); + $this->assertArrayNotHasKey('destination', $_POST); + $this->assertArrayNotHasKey('destination', $_REQUEST); + $this->assertArrayNotHasKey('destination', $_GET); + $this->assertError('Potentially unsafe destination removed from request parameter bag because it points to an external URL.', E_USER_NOTICE); + } + + /** + * Creates a request and sets PHP globals for testing. + * + * @param array $query + * (optional) The GET parameters. + * @param array $request + * (optional) The POST parameters. + * + * @return \Symfony\Component\HttpFoundation\Request + * The request object. + */ + protected function createRequestForTesting(array $query = [], array $request = []) { + $request = new Request($query, $request); + + // Set up globals. + $_GET = $request->query->all(); + $_POST = $request->request->all(); + $_COOKIE = $request->cookies->all(); + $_REQUEST = array_merge($request->query->all(), $request->request->all()); + $request->server->set('QUERY_STRING', http_build_query($request->query->all())); + $_SERVER['QUERY_STRING'] = $request->server->get('QUERY_STRING'); + return $request; + } + + /** + * Data provider for testing acceptable destinations. + */ + public function providerTestAcceptableDestinations() { + $data = []; + // Standard internal example node path is present in the 'destination' + // parameter. + $data[] = ['node']; + // Internal path with one leading slash is allowed. + $data[] = ['/example.com']; + // Internal URL using a colon is allowed. + $data[] = ['example:test']; + // Javascript URL is allowed because it is treated as an internal URL. + $data[] = ['javascript:alert(0)']; + return $data; + } + + /** + * Data provider for testing sanitized destinations. + */ + public function providerTestSanitizedDestinations() { + $data = []; + // External URL without scheme is not allowed. + $data[] = ['//example.com/test']; + // External URL is not allowed. + $data[] = ['http://example.com']; + return $data; + } + /** * Catches and logs errors to $this->errors. *