diff --git a/.ht.router.php b/.ht.router.php index 4086277..3da80a1 100644 --- a/.ht.router.php +++ b/.ht.router.php @@ -30,10 +30,26 @@ return FALSE; } -// The use of a router script means that a number of $_SERVER variables have to -// be updated to point to the index-file. -$index_file_absolute = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'index.php'; -$index_file_relative = DIRECTORY_SEPARATOR . 'index.php'; +// Work around the PHP bug. +$path = $url['path']; +$script = 'index.php'; +if (strpos($path, '.php') !== FALSE) { + // Work backwards through the path to check if a script exists. Otherwise + // fallback to index.php. + do { + $path = dirname($path); + if (preg_match('/\.php$/', $path) && is_file('.' . $path)) { + // Discovered that the path contains an existing PHP file. Use that as the + // script to include. + $script = ltrim($path, '/'); + break; + } + } while ($path !== '/' && $path !== '.'); +} + +// Update $_SERVER variables to point to the correct index-file. +$index_file_absolute = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $script; +$index_file_relative = DIRECTORY_SEPARATOR . $script; // SCRIPT_FILENAME will point to the router script itself, it should point to // the full path of index.php. @@ -45,5 +61,5 @@ $_SERVER['SCRIPT_NAME'] = $index_file_relative; $_SERVER['PHP_SELF'] = $index_file_relative; -// Require the main index.php and let core take over. -require $index_file_absolute; +// Require the script and let core take over. +require $_SERVER['SCRIPT_FILENAME']; diff --git a/composer.json b/composer.json index 1736919..3000a86 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "include": [ "core/composer.json" ], - "recurse": false, + "recurse": true, "replace": false, "merge-extra": false }, @@ -52,7 +52,7 @@ "post-autoload-dump": "Drupal\\Core\\Composer\\Composer::ensureHtaccess", "post-package-install": "Drupal\\Core\\Composer\\Composer::vendorTestCodeCleanup", "post-package-update": "Drupal\\Core\\Composer\\Composer::vendorTestCodeCleanup", - "post-install-cmd": "Drupal\\Core\\Composer\\Composer::upgradePHPUnit", + "drupal-phpunit-upgrade-check": "Drupal\\Core\\Composer\\Composer::upgradePHPUnit", "drupal-phpunit-upgrade": "@composer update phpunit/phpunit --with-dependencies --no-progress", "phpcs": "phpcs --standard=core/phpcs.xml.dist --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer --", "phpcbf": "phpcbf --standard=core/phpcs.xml.dist --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer --" diff --git a/composer.lock b/composer.lock index e298fe0..29d4ecb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,26 +4,26 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "fdc8e2afb49b5917c7bb593a3a8746d3", + "content-hash": "e64d8398c963abb6a608286320ff0d0c", "packages": [ { "name": "asm89/stack-cors", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/asm89/stack-cors.git", - "reference": "65ccbd455370f043c2e3b93482a3813603d68731" + "reference": "c163e2b614550aedcf71165db2473d936abbced6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/asm89/stack-cors/zipball/65ccbd455370f043c2e3b93482a3813603d68731", - "reference": "65ccbd455370f043c2e3b93482a3813603d68731", + "url": "https://api.github.com/repos/asm89/stack-cors/zipball/c163e2b614550aedcf71165db2473d936abbced6", + "reference": "c163e2b614550aedcf71165db2473d936abbced6", "shasum": "" }, "require": { "php": ">=5.5.9", - "symfony/http-foundation": "~2.7|~3.0", - "symfony/http-kernel": "~2.7|~3.0" + "symfony/http-foundation": "~2.7|~3.0|~4.0", + "symfony/http-kernel": "~2.7|~3.0|~4.0" }, "require-dev": { "phpunit/phpunit": "^5.0 || ^4.8.10", @@ -32,7 +32,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -56,20 +56,20 @@ "cors", "stack" ], - "time": "2017-04-11T20:03:41+00:00" + "time": "2017-12-20T14:37:45+00:00" }, { "name": "composer/installers", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", - "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b" + "reference": "049797d727261bf27f2690430d935067710049c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b", - "reference": "9ce17fb70e9a38dd8acff0636a29f5cf4d575c1b", + "url": "https://api.github.com/repos/composer/installers/zipball/049797d727261bf27f2690430d935067710049c2", + "reference": "049797d727261bf27f2690430d935067710049c2", "shasum": "" }, "require": { @@ -81,7 +81,7 @@ }, "require-dev": { "composer/composer": "1.0.*@dev", - "phpunit/phpunit": "4.1.*" + "phpunit/phpunit": "^4.8.36" }, "type": "composer-plugin", "extra": { @@ -152,15 +152,18 @@ "lavalite", "lithium", "magento", + "majima", "mako", "mediawiki", "modulework", + "modx", "moodle", "osclass", "phpbb", "piwik", "ppi", "puppet", + "pxcms", "reindex", "roundcube", "shopware", @@ -173,7 +176,7 @@ "zend", "zikula" ], - "time": "2017-08-09T07:53:48+00:00" + "time": "2017-12-29T09:13:20+00:00" }, { "name": "composer/semver", @@ -997,16 +1000,16 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.10", + "version": "v2.0.11", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/634bae8e911eefa89c1abfbf1b66da679ac8f54d", - "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", "shasum": "" }, "require": { @@ -1041,7 +1044,7 @@ "pseudorandom", "random" ], - "time": "2017-03-13T16:27:32+00:00" + "time": "2017-09-27T21:40:39+00:00" }, { "name": "psr/container", @@ -1191,22 +1194,22 @@ }, { "name": "stack/builder", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/stackphp/builder.git", - "reference": "59fcc9b448a8ce5e338a04c4e2e4aca893e83425" + "reference": "fb3d136d04c6be41120ebf8c0cc71fe9507d750a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stackphp/builder/zipball/59fcc9b448a8ce5e338a04c4e2e4aca893e83425", - "reference": "59fcc9b448a8ce5e338a04c4e2e4aca893e83425", + "url": "https://api.github.com/repos/stackphp/builder/zipball/fb3d136d04c6be41120ebf8c0cc71fe9507d750a", + "reference": "fb3d136d04c6be41120ebf8c0cc71fe9507d750a", "shasum": "" }, "require": { "php": ">=5.3.0", - "symfony/http-foundation": "~2.1|~3.0", - "symfony/http-kernel": "~2.1|~3.0" + "symfony/http-foundation": "~2.1|~3.0|~4.0", + "symfony/http-kernel": "~2.1|~3.0|~4.0" }, "require-dev": { "silex/silex": "~1.0" @@ -1236,7 +1239,7 @@ "keywords": [ "stack" ], - "time": "2016-06-02T06:58:42+00:00" + "time": "2017-11-18T14:57:29+00:00" }, { "name": "symfony-cmf/routing", @@ -1299,7 +1302,7 @@ }, { "name": "symfony/class-loader", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -1355,16 +1358,16 @@ }, { "name": "symfony/console", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8394c8ef121949e8f858f13bc1e34f05169e4e7d" + "reference": "26b6f419edda16c19775211987651cb27baea7f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8394c8ef121949e8f858f13bc1e34f05169e4e7d", - "reference": "8394c8ef121949e8f858f13bc1e34f05169e4e7d", + "url": "https://api.github.com/repos/symfony/console/zipball/26b6f419edda16c19775211987651cb27baea7f1", + "reference": "26b6f419edda16c19775211987651cb27baea7f1", "shasum": "" }, "require": { @@ -1420,20 +1423,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-01-29T09:03:43+00:00" }, { "name": "symfony/debug", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "603b95dda8b00020e4e6e60dc906e7b715b1c245" + "reference": "53f6af2805daf52a43b393b93d2f24925d35c937" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/603b95dda8b00020e4e6e60dc906e7b715b1c245", - "reference": "603b95dda8b00020e4e6e60dc906e7b715b1c245", + "url": "https://api.github.com/repos/symfony/debug/zipball/53f6af2805daf52a43b393b93d2f24925d35c937", + "reference": "53f6af2805daf52a43b393b93d2f24925d35c937", "shasum": "" }, "require": { @@ -1476,20 +1479,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-01-03T17:14:19+00:00" + "time": "2018-01-18T22:16:57+00:00" }, { "name": "symfony/dependency-injection", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "35f957ca171a431710966bec6e2f8636d3b019c4" + "reference": "4b2717ee2499390e371e1fc7abaf886c1c83e83d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/35f957ca171a431710966bec6e2f8636d3b019c4", - "reference": "35f957ca171a431710966bec6e2f8636d3b019c4", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4b2717ee2499390e371e1fc7abaf886c1c83e83d", + "reference": "4b2717ee2499390e371e1fc7abaf886c1c83e83d", "shasum": "" }, "require": { @@ -1547,11 +1550,11 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-01-04T15:56:45+00:00" + "time": "2018-01-29T09:16:57+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -1614,16 +1617,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "4a213be1cc8598089b8c7451529a2927b49b5d26" + "reference": "8c39071ac9cc7e6d8dab1d556c990dc0d2cc3d30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4a213be1cc8598089b8c7451529a2927b49b5d26", - "reference": "4a213be1cc8598089b8c7451529a2927b49b5d26", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8c39071ac9cc7e6d8dab1d556c990dc0d2cc3d30", + "reference": "8c39071ac9cc7e6d8dab1d556c990dc0d2cc3d30", "shasum": "" }, "require": { @@ -1664,20 +1667,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-01-03T17:14:19+00:00" + "time": "2018-01-29T09:03:43+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "1c2a82d6a8ec9b354fe4ef48ad1ad3f1a4f7db0e" + "reference": "911d2e5dd4beb63caad9a72e43857de984301907" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1c2a82d6a8ec9b354fe4ef48ad1ad3f1a4f7db0e", - "reference": "1c2a82d6a8ec9b354fe4ef48ad1ad3f1a4f7db0e", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/911d2e5dd4beb63caad9a72e43857de984301907", + "reference": "911d2e5dd4beb63caad9a72e43857de984301907", "shasum": "" }, "require": { @@ -1685,7 +1688,7 @@ "psr/log": "~1.0", "symfony/debug": "~2.8|~3.0|~4.0", "symfony/event-dispatcher": "~2.8|~3.0|~4.0", - "symfony/http-foundation": "^3.3.11|~4.0" + "symfony/http-foundation": "^3.4.4|^4.0.4" }, "conflict": { "symfony/config": "<2.8", @@ -1752,7 +1755,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-01-05T08:33:00+00:00" + "time": "2018-01-29T12:29:46+00:00" }, { "name": "symfony/polyfill-iconv", @@ -1933,16 +1936,16 @@ }, { "name": "symfony/process", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "ff69f110c6b33fd33cd2089ba97d6112f44ef0ba" + "reference": "09a5172057be8fc677840e591b17f385e58c7c0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/ff69f110c6b33fd33cd2089ba97d6112f44ef0ba", - "reference": "ff69f110c6b33fd33cd2089ba97d6112f44ef0ba", + "url": "https://api.github.com/repos/symfony/process/zipball/09a5172057be8fc677840e591b17f385e58c7c0d", + "reference": "09a5172057be8fc677840e591b17f385e58c7c0d", "shasum": "" }, "require": { @@ -1978,7 +1981,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-01-29T09:03:43+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -2042,16 +2045,16 @@ }, { "name": "symfony/routing", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "e2b6d6fe7b090c7af720b75c7722c6dfa7a52658" + "reference": "235d01730d553a97732990588407eaf6779bb4b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/e2b6d6fe7b090c7af720b75c7722c6dfa7a52658", - "reference": "e2b6d6fe7b090c7af720b75c7722c6dfa7a52658", + "url": "https://api.github.com/repos/symfony/routing/zipball/235d01730d553a97732990588407eaf6779bb4b2", + "reference": "235d01730d553a97732990588407eaf6779bb4b2", "shasum": "" }, "require": { @@ -2116,20 +2119,20 @@ "uri", "url" ], - "time": "2018-01-04T15:09:34+00:00" + "time": "2018-01-16T18:03:57+00:00" }, { "name": "symfony/serializer", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "054e20557e48276064a5698e3444d3eb6beef139" + "reference": "abd3c5bd03fb5634d855293c47de059f72981830" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/054e20557e48276064a5698e3444d3eb6beef139", - "reference": "054e20557e48276064a5698e3444d3eb6beef139", + "url": "https://api.github.com/repos/symfony/serializer/zipball/abd3c5bd03fb5634d855293c47de059f72981830", + "reference": "abd3c5bd03fb5634d855293c47de059f72981830", "shasum": "" }, "require": { @@ -2194,20 +2197,20 @@ ], "description": "Symfony Serializer Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-01-19T11:19:37+00:00" }, { "name": "symfony/translation", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "17b5962d252b2d6d1d37a2485ebb7ddc5b2bef0a" + "reference": "10b32cf0eae28b9b39fe26c456c42b19854c4b84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/17b5962d252b2d6d1d37a2485ebb7ddc5b2bef0a", - "reference": "17b5962d252b2d6d1d37a2485ebb7ddc5b2bef0a", + "url": "https://api.github.com/repos/symfony/translation/zipball/10b32cf0eae28b9b39fe26c456c42b19854c4b84", + "reference": "10b32cf0eae28b9b39fe26c456c42b19854c4b84", "shasum": "" }, "require": { @@ -2262,20 +2265,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-01-18T22:16:57+00:00" }, { "name": "symfony/validator", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "10828736a48411f2c4d87a7fe61c2d02ccb922be" + "reference": "179a0136e155f206c12b6a6a28d3a8f97d65d0a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/10828736a48411f2c4d87a7fe61c2d02ccb922be", - "reference": "10828736a48411f2c4d87a7fe61c2d02ccb922be", + "url": "https://api.github.com/repos/symfony/validator/zipball/179a0136e155f206c12b6a6a28d3a8f97d65d0a9", + "reference": "179a0136e155f206c12b6a6a28d3a8f97d65d0a9", "shasum": "" }, "require": { @@ -2346,20 +2349,20 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2018-01-03T17:14:19+00:00" + "time": "2018-01-21T19:05:02+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "25c192f25721a74084272671f658797d9e0e0146" + "reference": "eab73b6c21d27ae4cd037c417618dfd4befb0bfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/25c192f25721a74084272671f658797d9e0e0146", - "reference": "25c192f25721a74084272671f658797d9e0e0146", + "url": "https://api.github.com/repos/symfony/yaml/zipball/eab73b6c21d27ae4cd037c417618dfd4befb0bfe", + "reference": "eab73b6c21d27ae4cd037c417618dfd4befb0bfe", "shasum": "" }, "require": { @@ -2404,7 +2407,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-01-21T19:05:02+00:00" }, { "name": "twig/twig", @@ -2894,6 +2897,67 @@ "time": "2016-03-05T09:04:22+00:00" }, { + "name": "behat/mink-selenium2-driver", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/minkphp/MinkSelenium2Driver.git", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/473a9f3ebe0c134ee1e623ce8a9c852832020288", + "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288", + "shasum": "" + }, + "require": { + "behat/mink": "~1.7@dev", + "instaclick/php-webdriver": "~1.1", + "php": ">=5.3.1" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Mink\\Driver\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Pete Otaqui", + "email": "pete@otaqui.com", + "homepage": "https://github.com/pete-otaqui" + } + ], + "description": "Selenium2 (WebDriver) driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "javascript", + "selenium", + "testing", + "webdriver" + ], + "time": "2016-03-05T09:10:18+00:00" + }, + { "name": "doctrine/instantiator", "version": "1.0.5", "source": { @@ -2955,6 +3019,12 @@ "url": "https://git.drupal.org/project/coder.git", "reference": "984c54a7b1e8f27ff1c32348df69712afd86b17f" }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/klausi/coder/zipball/984c54a7b1e8f27ff1c32348df69712afd86b17f", + "reference": "984c54a7b1e8f27ff1c32348df69712afd86b17f", + "shasum": "" + }, "require": { "ext-mbstring": "*", "php": ">=5.4.0", @@ -3028,6 +3098,65 @@ "time": "2017-01-03T13:21:43+00:00" }, { + "name": "instaclick/php-webdriver", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/instaclick/php-webdriver.git", + "reference": "6fa959452e774dcaed543faad3a9d1a37d803327" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/6fa959452e774dcaed543faad3a9d1a37d803327", + "reference": "6fa959452e774dcaed543faad3a9d1a37d803327", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "^4.8", + "satooshi/php-coveralls": "^1.0||^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "WebDriver": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Justin Bishop", + "email": "jubishop@gmail.com", + "role": "Developer" + }, + { + "name": "Anthon Pang", + "email": "apang@softwaredevelopment.ca", + "role": "Fork Maintainer" + } + ], + "description": "PHP WebDriver for Selenium 2", + "homepage": "http://instaclick.com/", + "keywords": [ + "browser", + "selenium", + "webdriver", + "webtest" + ], + "time": "2017-06-30T04:02:48+00:00" + }, + { "name": "ircmaxell/password-compat", "version": "v1.0.4", "source": { @@ -4170,7 +4299,7 @@ }, { "name": "symfony/browser-kit", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -4227,7 +4356,7 @@ }, { "name": "symfony/css-selector", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -4280,7 +4409,7 @@ }, { "name": "symfony/dom-crawler", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", @@ -4336,16 +4465,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v3.4.3", + "version": "v3.4.4", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "24ffb71a115c25f5ee56cbfd38e56ed2cdbeb0a9" + "reference": "6a65b09b666f975dd70ec2bb9e9b1a87dbb02aca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/24ffb71a115c25f5ee56cbfd38e56ed2cdbeb0a9", - "reference": "24ffb71a115c25f5ee56cbfd38e56ed2cdbeb0a9", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/6a65b09b666f975dd70ec2bb9e9b1a87dbb02aca", + "reference": "6a65b09b666f975dd70ec2bb9e9b1a87dbb02aca", "shasum": "" }, "require": { @@ -4398,7 +4527,7 @@ ], "description": "Symfony PHPUnit Bridge", "homepage": "https://symfony.com", - "time": "2018-01-04T17:19:23+00:00" + "time": "2018-01-21T19:05:02+00:00" } ], "aliases": [], diff --git a/core/.stylelintrc.json b/core/.stylelintrc.json index 3f0769b..51d4bcc 100644 --- a/core/.stylelintrc.json +++ b/core/.stylelintrc.json @@ -4,16 +4,12 @@ "stylelint-no-browser-hacks/lib" ], "rules": { - "color-hex-case": null, - "color-hex-length": null, "comment-empty-line-before": null, "declaration-block-no-duplicate-properties": null, - "declaration-block-no-shorthand-property-overrides": null, "function-linear-gradient-no-nonstandard-direction": null, "function-whitespace-after": null, - "no-empty-source": null, "no-unknown-animations": true, - "number-leading-zero": null, + "number-leading-zero": "always", "plugin/no-browser-hacks": [true, { "browsers": [ "ie >= 9", diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 0867a84..5127d58 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -82,7 +82,7 @@ Basic Auth - Juampy Novillo Requena 'juampy' https://www.drupal.org/u/juampy Batch API -- ? +- John Cook 'John Cook' https://www.drupal.org/u/john-cook BigPipe - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers @@ -245,7 +245,6 @@ Interface Translation (locale) JavaScript - Théodore Biadala 'nod_' https://www.drupal.org/u/nod_ -- Kay Leung 'droplet' https://www.drupal.org/u/droplet - Matthew Grill 'drpal' https://www.drupal.org/u/drpal - Sally Young 'justafish' https://www.drupal.org/u/justafish @@ -471,6 +470,11 @@ API-first Initiative - Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers - Mateu Aguiló Bosch 'e0ipso' https://www.drupal.org/u/e0ipso +JavaScript Modernization Initiative +- Angela Byron 'webchick' https://www.drupal.org/u/webchick +- Matthew Grill 'drpal' https://www.drupal.org/u/drpal +- Sally Young 'justafish' https://www.drupal.org/u/justafish + Layout Initiative - Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett - Emilie Nouveau 'DyanneNova' https://www.drupal.org/u/dyannenova diff --git a/core/assets/vendor/jquery-form/jquery.form.min.js b/core/assets/vendor/jquery-form/jquery.form.min.js index 4c5ee0e..9a447f7 100644 --- a/core/assets/vendor/jquery-form/jquery.form.min.js +++ b/core/assets/vendor/jquery-form/jquery.form.min.js @@ -1,11 +1,23 @@ /*! * jQuery Form Plugin - * version: 3.51.0-2014.06.20 - * Requires jQuery v1.5 or later - * Copyright (c) 2014 M. Alsup - * Examples and documentation at: http://malsup.com/jquery/form/ - * Project repository: https://github.com/malsup/form - * Dual licensed under the MIT and GPL licenses. - * https://github.com/malsup/form#copyright-and-license + * version: 4.2.2 + * Requires jQuery v1.7.2 or later + * Project repository: https://github.com/jquery-form/form + + * Copyright 2017 Kevin Morris + * Copyright 2006 M. Alsup + + * Dual licensed under the LGPL-2.1+ or MIT licenses + * https://github.com/jquery-form/form#license + + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. */ -!function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):e("undefined"!=typeof jQuery?jQuery:window.Zepto)}(function(e){"use strict";function t(t){var r=t.data;t.isDefaultPrevented()||(t.preventDefault(),e(t.target).ajaxSubmit(r))}function r(t){var r=t.target,a=e(r);if(!a.is("[type=submit],[type=image]")){var n=a.closest("[type=submit]");if(0===n.length)return;r=n[0]}var i=this;if(i.clk=r,"image"==r.type)if(void 0!==t.offsetX)i.clk_x=t.offsetX,i.clk_y=t.offsetY;else if("function"==typeof e.fn.offset){var o=a.offset();i.clk_x=t.pageX-o.left,i.clk_y=t.pageY-o.top}else i.clk_x=t.pageX-r.offsetLeft,i.clk_y=t.pageY-r.offsetTop;setTimeout(function(){i.clk=i.clk_x=i.clk_y=null},100)}function a(){if(e.fn.ajaxSubmit.debug){var t="[jquery.form] "+Array.prototype.join.call(arguments,"");window.console&&window.console.log?window.console.log(t):window.opera&&window.opera.postError&&window.opera.postError(t)}}var n={};n.fileapi=void 0!==e("").get(0).files,n.formdata=void 0!==window.FormData;var i=!!e.fn.prop;e.fn.attr2=function(){if(!i)return this.attr.apply(this,arguments);var e=this.prop.apply(this,arguments);return e&&e.jquery||"string"==typeof e?e:this.attr.apply(this,arguments)},e.fn.ajaxSubmit=function(t){function r(r){var a,n,i=e.param(r,t.traditional).split("&"),o=i.length,s=[];for(a=0;o>a;a++)i[a]=i[a].replace(/\+/g," "),n=i[a].split("="),s.push([decodeURIComponent(n[0]),decodeURIComponent(n[1])]);return s}function o(a){for(var n=new FormData,i=0;i').val(m.extraData[d].value).appendTo(w)[0]:e('').val(m.extraData[d]).appendTo(w)[0]);m.iframeTarget||v.appendTo("body"),g.attachEvent?g.attachEvent("onload",s):g.addEventListener("load",s,!1),setTimeout(t,15);try{w.submit()}catch(h){var x=document.createElement("form").submit;x.apply(w)}}finally{w.setAttribute("action",i),w.setAttribute("enctype",c),r?w.setAttribute("target",r):f.removeAttr("target"),e(l).remove()}}function s(t){if(!x.aborted&&!F){if(M=n(g),M||(a("cannot access response document"),t=k),t===D&&x)return x.abort("timeout"),void S.reject(x,"timeout");if(t==k&&x)return x.abort("server abort"),void S.reject(x,"error","server abort");if(M&&M.location.href!=m.iframeSrc||T){g.detachEvent?g.detachEvent("onload",s):g.removeEventListener("load",s,!1);var r,i="success";try{if(T)throw"timeout";var o="xml"==m.dataType||M.XMLDocument||e.isXMLDoc(M);if(a("isXml="+o),!o&&window.opera&&(null===M.body||!M.body.innerHTML)&&--O)return a("requeing onLoad callback, DOM not available"),void setTimeout(s,250);var u=M.body?M.body:M.documentElement;x.responseText=u?u.innerHTML:null,x.responseXML=M.XMLDocument?M.XMLDocument:M,o&&(m.dataType="xml"),x.getResponseHeader=function(e){var t={"content-type":m.dataType};return t[e.toLowerCase()]},u&&(x.status=Number(u.getAttribute("status"))||x.status,x.statusText=u.getAttribute("statusText")||x.statusText);var c=(m.dataType||"").toLowerCase(),l=/(json|script|text)/.test(c);if(l||m.textarea){var f=M.getElementsByTagName("textarea")[0];if(f)x.responseText=f.value,x.status=Number(f.getAttribute("status"))||x.status,x.statusText=f.getAttribute("statusText")||x.statusText;else if(l){var p=M.getElementsByTagName("pre")[0],h=M.getElementsByTagName("body")[0];p?x.responseText=p.textContent?p.textContent:p.innerText:h&&(x.responseText=h.textContent?h.textContent:h.innerText)}}else"xml"==c&&!x.responseXML&&x.responseText&&(x.responseXML=X(x.responseText));try{E=_(x,c,m)}catch(y){i="parsererror",x.error=r=y||i}}catch(y){a("error caught: ",y),i="error",x.error=r=y||i}x.aborted&&(a("upload aborted"),i=null),x.status&&(i=x.status>=200&&x.status<300||304===x.status?"success":"error"),"success"===i?(m.success&&m.success.call(m.context,E,"success",x),S.resolve(x.responseText,"success",x),d&&e.event.trigger("ajaxSuccess",[x,m])):i&&(void 0===r&&(r=x.statusText),m.error&&m.error.call(m.context,x,i,r),S.reject(x,"error",r),d&&e.event.trigger("ajaxError",[x,m,r])),d&&e.event.trigger("ajaxComplete",[x,m]),d&&!--e.active&&e.event.trigger("ajaxStop"),m.complete&&m.complete.call(m.context,x,i),F=!0,m.timeout&&clearTimeout(j),setTimeout(function(){m.iframeTarget?v.attr("src",m.iframeSrc):v.remove(),x.responseXML=null},100)}}}var c,l,m,d,p,v,g,x,y,b,T,j,w=f[0],S=e.Deferred();if(S.abort=function(e){x.abort(e)},r)for(l=0;l'),v.css({position:"absolute",top:"-1000px",left:"-1000px"})),g=v[0],x={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(t){var r="timeout"===t?"timeout":"aborted";a("aborting upload... "+r),this.aborted=1;try{g.contentWindow.document.execCommand&&g.contentWindow.document.execCommand("Stop")}catch(n){}v.attr("src",m.iframeSrc),x.error=r,m.error&&m.error.call(m.context,x,r,t),d&&e.event.trigger("ajaxError",[x,m,r]),m.complete&&m.complete.call(m.context,x,r)}},d=m.global,d&&0===e.active++&&e.event.trigger("ajaxStart"),d&&e.event.trigger("ajaxSend",[x,m]),m.beforeSend&&m.beforeSend.call(m.context,x,m)===!1)return m.global&&e.active--,S.reject(),S;if(x.aborted)return S.reject(),S;y=w.clk,y&&(b=y.name,b&&!y.disabled&&(m.extraData=m.extraData||{},m.extraData[b]=y.value,"image"==y.type&&(m.extraData[b+".x"]=w.clk_x,m.extraData[b+".y"]=w.clk_y)));var D=1,k=2,A=e("meta[name=csrf-token]").attr("content"),L=e("meta[name=csrf-param]").attr("content");L&&A&&(m.extraData=m.extraData||{},m.extraData[L]=A),m.forceSync?o():setTimeout(o,10);var E,M,F,O=50,X=e.parseXML||function(e,t){return window.ActiveXObject?(t=new ActiveXObject("Microsoft.XMLDOM"),t.async="false",t.loadXML(e)):t=(new DOMParser).parseFromString(e,"text/xml"),t&&t.documentElement&&"parsererror"!=t.documentElement.nodeName?t:null},C=e.parseJSON||function(e){return window.eval("("+e+")")},_=function(t,r,a){var n=t.getResponseHeader("content-type")||"",i="xml"===r||!r&&n.indexOf("xml")>=0,o=i?t.responseXML:t.responseText;return i&&"parsererror"===o.documentElement.nodeName&&e.error&&e.error("parsererror"),a&&a.dataFilter&&(o=a.dataFilter(o,r)),"string"==typeof o&&("json"===r||!r&&n.indexOf("json")>=0?o=C(o):("script"===r||!r&&n.indexOf("javascript")>=0)&&e.globalEval(o)),o};return S}if(!this.length)return a("ajaxSubmit: skipping submit process - no element selected"),this;var u,c,l,f=this;"function"==typeof t?t={success:t}:void 0===t&&(t={}),u=t.type||this.attr2("method"),c=t.url||this.attr2("action"),l="string"==typeof c?e.trim(c):"",l=l||window.location.href||"",l&&(l=(l.match(/^([^#]+)/)||[])[1]),t=e.extend(!0,{url:l,success:e.ajaxSettings.success,type:u||e.ajaxSettings.type,iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},t);var m={};if(this.trigger("form-pre-serialize",[this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-pre-serialize trigger"),this;if(t.beforeSerialize&&t.beforeSerialize(this,t)===!1)return a("ajaxSubmit: submit aborted via beforeSerialize callback"),this;var d=t.traditional;void 0===d&&(d=e.ajaxSettings.traditional);var p,h=[],v=this.formToArray(t.semantic,h);if(t.data&&(t.extraData=t.data,p=e.param(t.data,d)),t.beforeSubmit&&t.beforeSubmit(v,this,t)===!1)return a("ajaxSubmit: submit aborted via beforeSubmit callback"),this;if(this.trigger("form-submit-validate",[v,this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-submit-validate trigger"),this;var g=e.param(v,d);p&&(g=g?g+"&"+p:p),"GET"==t.type.toUpperCase()?(t.url+=(t.url.indexOf("?")>=0?"&":"?")+g,t.data=null):t.data=g;var x=[];if(t.resetForm&&x.push(function(){f.resetForm()}),t.clearForm&&x.push(function(){f.clearForm(t.includeHidden)}),!t.dataType&&t.target){var y=t.success||function(){};x.push(function(r){var a=t.replaceTarget?"replaceWith":"html";e(t.target)[a](r).each(y,arguments)})}else t.success&&x.push(t.success);if(t.success=function(e,r,a){for(var n=t.context||this,i=0,o=x.length;o>i;i++)x[i].apply(n,[e,r,a||f,f])},t.error){var b=t.error;t.error=function(e,r,a){var n=t.context||this;b.apply(n,[e,r,a,f])}}if(t.complete){var T=t.complete;t.complete=function(e,r){var a=t.context||this;T.apply(a,[e,r,f])}}var j=e("input[type=file]:enabled",this).filter(function(){return""!==e(this).val()}),w=j.length>0,S="multipart/form-data",D=f.attr("enctype")==S||f.attr("encoding")==S,k=n.fileapi&&n.formdata;a("fileAPI :"+k);var A,L=(w||D)&&!k;t.iframe!==!1&&(t.iframe||L)?t.closeKeepAlive?e.get(t.closeKeepAlive,function(){A=s(v)}):A=s(v):A=(w||D)&&k?o(v):e.ajax(t),f.removeData("jqxhr").data("jqxhr",A);for(var E=0;Ec;c++)if(d=u[c],f=d.name,f&&!d.disabled)if(t&&o.clk&&"image"==d.type)o.clk==d&&(a.push({name:f,value:e(d).val(),type:d.type}),a.push({name:f+".x",value:o.clk_x},{name:f+".y",value:o.clk_y}));else if(m=e.fieldValue(d,!0),m&&m.constructor==Array)for(r&&r.push(d),l=0,h=m.length;h>l;l++)a.push({name:f,value:m[l]});else if(n.fileapi&&"file"==d.type){r&&r.push(d);var v=d.files;if(v.length)for(l=0;li;i++)r.push({name:a,value:n[i]});else null!==n&&"undefined"!=typeof n&&r.push({name:this.name,value:n})}}),e.param(r)},e.fn.fieldValue=function(t){for(var r=[],a=0,n=this.length;n>a;a++){var i=this[a],o=e.fieldValue(i,t);null===o||"undefined"==typeof o||o.constructor==Array&&!o.length||(o.constructor==Array?e.merge(r,o):r.push(o))}return r},e.fieldValue=function(t,r){var a=t.name,n=t.type,i=t.tagName.toLowerCase();if(void 0===r&&(r=!0),r&&(!a||t.disabled||"reset"==n||"button"==n||("checkbox"==n||"radio"==n)&&!t.checked||("submit"==n||"image"==n)&&t.form&&t.form.clk!=t||"select"==i&&-1==t.selectedIndex))return null;if("select"==i){var o=t.selectedIndex;if(0>o)return null;for(var s=[],u=t.options,c="select-one"==n,l=c?o+1:u.length,f=c?o:0;l>f;f++){var m=u[f];if(m.selected){var d=m.value;if(d||(d=m.attributes&&m.attributes.value&&!m.attributes.value.specified?m.text:m.value),c)return d;s.push(d)}}return s}return e(t).val()},e.fn.clearForm=function(t){return this.each(function(){e("input,select,textarea",this).clearFields(t)})},e.fn.clearFields=e.fn.clearInputs=function(t){var r=/^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i;return this.each(function(){var a=this.type,n=this.tagName.toLowerCase();r.test(a)||"textarea"==n?this.value="":"checkbox"==a||"radio"==a?this.checked=!1:"select"==n?this.selectedIndex=-1:"file"==a?/MSIE/.test(navigator.userAgent)?e(this).replaceWith(e(this).clone(!0)):e(this).val(""):t&&(t===!0&&/hidden/.test(a)||"string"==typeof t&&e(this).is(t))&&(this.value="")})},e.fn.resetForm=function(){return this.each(function(){("function"==typeof this.reset||"object"==typeof this.reset&&!this.reset.nodeType)&&this.reset()})},e.fn.enable=function(e){return void 0===e&&(e=!0),this.each(function(){this.disabled=!e})},e.fn.selected=function(t){return void 0===t&&(t=!0),this.each(function(){var r=this.type;if("checkbox"==r||"radio"==r)this.checked=t;else if("option"==this.tagName.toLowerCase()){var a=e(this).parent("select");t&&a[0]&&"select-one"==a[0].type&&a.find("option").selected(!1),this.selected=t}})},e.fn.ajaxSubmit.debug=!1}); +!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof module&&module.exports?module.exports=function(t,r){return void 0===r&&(r="undefined"!=typeof window?require("jquery"):require("jquery")(t)),e(r),r}:e(jQuery)}(function(e){"use strict";function t(t){var r=t.data;t.isDefaultPrevented()||(t.preventDefault(),e(t.target).closest("form").ajaxSubmit(r))}function r(t){var r=t.target,a=e(r);if(!a.is("[type=submit],[type=image]")){var n=a.closest("[type=submit]");if(0===n.length)return;r=n[0]}var i=r.form;if(i.clk=r,"image"===r.type)if(void 0!==t.offsetX)i.clk_x=t.offsetX,i.clk_y=t.offsetY;else if("function"==typeof e.fn.offset){var o=a.offset();i.clk_x=t.pageX-o.left,i.clk_y=t.pageY-o.top}else i.clk_x=t.pageX-r.offsetLeft,i.clk_y=t.pageY-r.offsetTop;setTimeout(function(){i.clk=i.clk_x=i.clk_y=null},100)}function a(){if(e.fn.ajaxSubmit.debug){var t="[jquery.form] "+Array.prototype.join.call(arguments,"");window.console&&window.console.log?window.console.log(t):window.opera&&window.opera.postError&&window.opera.postError(t)}}var n=/\r?\n/g,i={};i.fileapi=void 0!==e('').get(0).files,i.formdata=void 0!==window.FormData;var o=!!e.fn.prop;e.fn.attr2=function(){if(!o)return this.attr.apply(this,arguments);var e=this.prop.apply(this,arguments);return e&&e.jquery||"string"==typeof e?e:this.attr.apply(this,arguments)},e.fn.ajaxSubmit=function(t,r,n,s){function u(r){var a,n,i=e.param(r,t.traditional).split("&"),o=i.length,s=[];for(a=0;a',k).val(f.extraData[c].value).appendTo(w)[0]):u.push(e('',k).val(f.extraData[c]).appendTo(w)[0]));f.iframeTarget||h.appendTo(D),v.attachEvent?v.attachEvent("onload",s):v.addEventListener("load",s,!1),setTimeout(t,15);try{w.submit()}catch(e){document.createElement("form").submit.apply(w)}}finally{w.setAttribute("action",i),w.setAttribute("enctype",o),r?w.setAttribute("target",r):p.removeAttr("target"),e(u).remove()}}function s(t){if(!x.aborted&&!X){if((O=n(v))||(a("cannot access response document"),t=L),t===A&&x)return x.abort("timeout"),void S.reject(x,"timeout");if(t===L&&x)return x.abort("server abort"),void S.reject(x,"error","server abort");if(O&&O.location.href!==f.iframeSrc||T){v.detachEvent?v.detachEvent("onload",s):v.removeEventListener("load",s,!1);var r,i="success";try{if(T)throw"timeout";var o="xml"===f.dataType||O.XMLDocument||e.isXMLDoc(O);if(a("isXml="+o),!o&&window.opera&&(null===O.body||!O.body.innerHTML)&&--C)return a("requeing onLoad callback, DOM not available"),void setTimeout(s,250);var u=O.body?O.body:O.documentElement;x.responseText=u?u.innerHTML:null,x.responseXML=O.XMLDocument?O.XMLDocument:O,o&&(f.dataType="xml"),x.getResponseHeader=function(e){return{"content-type":f.dataType}[e.toLowerCase()]},u&&(x.status=Number(u.getAttribute("status"))||x.status,x.statusText=u.getAttribute("statusText")||x.statusText);var c=(f.dataType||"").toLowerCase(),l=/(json|script|text)/.test(c);if(l||f.textarea){var p=O.getElementsByTagName("textarea")[0];if(p)x.responseText=p.value,x.status=Number(p.getAttribute("status"))||x.status,x.statusText=p.getAttribute("statusText")||x.statusText;else if(l){var m=O.getElementsByTagName("pre")[0],g=O.getElementsByTagName("body")[0];m?x.responseText=m.textContent?m.textContent:m.innerText:g&&(x.responseText=g.textContent?g.textContent:g.innerText)}}else"xml"===c&&!x.responseXML&&x.responseText&&(x.responseXML=q(x.responseText));try{M=N(x,c,f)}catch(e){i="parsererror",x.error=r=e||i}}catch(e){a("error caught: ",e),i="error",x.error=r=e||i}x.aborted&&(a("upload aborted"),i=null),x.status&&(i=x.status>=200&&x.status<300||304===x.status?"success":"error"),"success"===i?(f.success&&f.success.call(f.context,M,"success",x),S.resolve(x.responseText,"success",x),d&&e.event.trigger("ajaxSuccess",[x,f])):i&&(void 0===r&&(r=x.statusText),f.error&&f.error.call(f.context,x,i,r),S.reject(x,"error",r),d&&e.event.trigger("ajaxError",[x,f,r])),d&&e.event.trigger("ajaxComplete",[x,f]),d&&!--e.active&&e.event.trigger("ajaxStop"),f.complete&&f.complete.call(f.context,x,i),X=!0,f.timeout&&clearTimeout(j),setTimeout(function(){f.iframeTarget?h.attr("src",f.iframeSrc):h.remove(),x.responseXML=null},100)}}}var u,c,f,d,m,h,v,x,y,b,T,j,w=p[0],S=e.Deferred();if(S.abort=function(e){x.abort(e)},r)for(c=0;c',k)).css({position:"absolute",top:"-1000px",left:"-1000px"}),v=h[0],x={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(t){var r="timeout"===t?"timeout":"aborted";a("aborting upload... "+r),this.aborted=1;try{v.contentWindow.document.execCommand&&v.contentWindow.document.execCommand("Stop")}catch(e){}h.attr("src",f.iframeSrc),x.error=r,f.error&&f.error.call(f.context,x,r,t),d&&e.event.trigger("ajaxError",[x,f,r]),f.complete&&f.complete.call(f.context,x,r)}},(d=f.global)&&0==e.active++&&e.event.trigger("ajaxStart"),d&&e.event.trigger("ajaxSend",[x,f]),f.beforeSend&&!1===f.beforeSend.call(f.context,x,f))return f.global&&e.active--,S.reject(),S;if(x.aborted)return S.reject(),S;(y=w.clk)&&(b=y.name)&&!y.disabled&&(f.extraData=f.extraData||{},f.extraData[b]=y.value,"image"===y.type&&(f.extraData[b+".x"]=w.clk_x,f.extraData[b+".y"]=w.clk_y));var A=1,L=2,F=e("meta[name=csrf-token]").attr("content"),E=e("meta[name=csrf-param]").attr("content");E&&F&&(f.extraData=f.extraData||{},f.extraData[E]=F),f.forceSync?i():setTimeout(i,10);var M,O,X,C=50,q=e.parseXML||function(e,t){return window.ActiveXObject?((t=new ActiveXObject("Microsoft.XMLDOM")).async="false",t.loadXML(e)):t=(new DOMParser).parseFromString(e,"text/xml"),t&&t.documentElement&&"parsererror"!==t.documentElement.nodeName?t:null},_=e.parseJSON||function(e){return window.eval("("+e+")")},N=function(t,r,a){var n=t.getResponseHeader("content-type")||"",i=("xml"===r||!r)&&n.indexOf("xml")>=0,o=i?t.responseXML:t.responseText;return i&&"parsererror"===o.documentElement.nodeName&&e.error&&e.error("parsererror"),a&&a.dataFilter&&(o=a.dataFilter(o,r)),"string"==typeof o&&(("json"===r||!r)&&n.indexOf("json")>=0?o=_(o):("script"===r||!r)&&n.indexOf("javascript")>=0&&e.globalEval(o)),o};return S}if(!this.length)return a("ajaxSubmit: skipping submit process - no element selected"),this;var l,f,d,p=this;"function"==typeof t?t={success:t}:"string"==typeof t||!1===t&&arguments.length>0?(t={url:t,data:r,dataType:n},"function"==typeof s&&(t.success=s)):void 0===t&&(t={}),l=t.method||t.type||this.attr2("method"),(d=(d="string"==typeof(f=t.url||this.attr2("action"))?e.trim(f):"")||window.location.href||"")&&(d=(d.match(/^([^#]+)/)||[])[1]),t=e.extend(!0,{url:d,success:e.ajaxSettings.success,type:l||e.ajaxSettings.type,iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},t);var m={};if(this.trigger("form-pre-serialize",[this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-pre-serialize trigger"),this;if(t.beforeSerialize&&!1===t.beforeSerialize(this,t))return a("ajaxSubmit: submit aborted via beforeSerialize callback"),this;var h=t.traditional;void 0===h&&(h=e.ajaxSettings.traditional);var v,g=[],x=this.formToArray(t.semantic,g,t.filtering);if(t.data){var y=e.isFunction(t.data)?t.data(x):t.data;t.extraData=y,v=e.param(y,h)}if(t.beforeSubmit&&!1===t.beforeSubmit(x,this,t))return a("ajaxSubmit: submit aborted via beforeSubmit callback"),this;if(this.trigger("form-submit-validate",[x,this,t,m]),m.veto)return a("ajaxSubmit: submit vetoed via form-submit-validate trigger"),this;var b=e.param(x,h);v&&(b=b?b+"&"+v:v),"GET"===t.type.toUpperCase()?(t.url+=(t.url.indexOf("?")>=0?"&":"?")+b,t.data=null):t.data=b;var T=[];if(t.resetForm&&T.push(function(){p.resetForm()}),t.clearForm&&T.push(function(){p.clearForm(t.includeHidden)}),!t.dataType&&t.target){var j=t.success||function(){};T.push(function(r,a,n){var i=arguments,o=t.replaceTarget?"replaceWith":"html";e(t.target)[o](r).each(function(){j.apply(this,i)})})}else t.success&&(e.isArray(t.success)?e.merge(T,t.success):T.push(t.success));if(t.success=function(e,r,a){for(var n=t.context||this,i=0,o=T.length;i0,D="multipart/form-data",A=p.attr("enctype")===D||p.attr("encoding")===D,L=i.fileapi&&i.formdata;a("fileAPI :"+L);var F,E=(k||A)&&!L;!1!==t.iframe&&(t.iframe||E)?t.closeKeepAlive?e.get(t.closeKeepAlive,function(){F=c(x)}):F=c(x):F=(k||A)&&L?function(r){for(var a=new FormData,n=0;n0)&&(n={url:n,data:i,dataType:o},"function"==typeof s&&(n.success=s)),n=n||{},n.delegation=n.delegation&&e.isFunction(e.fn.on),!n.delegation&&0===this.length){var u={s:this.selector,c:this.context};return!e.isReady&&u.s?(a("DOM not ready, queuing ajaxForm"),e(function(){e(u.s,u.c).ajaxForm(n)}),this):(a("terminating; zero elements found by selector"+(e.isReady?"":" (DOM not ready)")),this)}return n.delegation?(e(document).off("submit.form-plugin",this.selector,t).off("click.form-plugin",this.selector,r).on("submit.form-plugin",this.selector,n,t).on("click.form-plugin",this.selector,n,r),this):this.ajaxFormUnbind().on("submit.form-plugin",n,t).on("click.form-plugin",n,r)},e.fn.ajaxFormUnbind=function(){return this.off("submit.form-plugin click.form-plugin")},e.fn.formToArray=function(t,r,a){var n=[];if(0===this.length)return n;var o,s=this[0],u=this.attr("id"),c=t||void 0===s.elements?s.getElementsByTagName("*"):s.elements;if(c&&(c=e.makeArray(c)),u&&(t||/(Edge|Trident)\//.test(navigator.userAgent))&&(o=e(':input[form="'+u+'"]').get()).length&&(c=(c||[]).concat(o)),!c||!c.length)return n;e.isFunction(a)&&(c=e.map(c,a));var l,f,d,p,m,h,v;for(l=0,h=c.length;l=5.5.9", - "doctrine/common": "2.5.*", + "doctrine/common": "^2.5", "doctrine/annotations": "1.2.*", "drupal/core-file-cache": "^8.2", "drupal/core-plugin": "^8.2", diff --git a/core/lib/Drupal/Component/ClassFinder/composer.json b/core/lib/Drupal/Component/ClassFinder/composer.json index bba8254..c5a97b0 100644 --- a/core/lib/Drupal/Component/ClassFinder/composer.json +++ b/core/lib/Drupal/Component/ClassFinder/composer.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=5.5.9", - "doctrine/common": "2.5.*" + "doctrine/common": "^2.5" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/DependencyInjection/composer.json b/core/lib/Drupal/Component/DependencyInjection/composer.json index 56864cb..846034a 100644 --- a/core/lib/Drupal/Component/DependencyInjection/composer.json +++ b/core/lib/Drupal/Component/DependencyInjection/composer.json @@ -12,7 +12,7 @@ }, "require": { "php": ">=5.5.9", - "symfony/dependency-injection": "^2.8" + "symfony/dependency-injection": ">=2.8 <4.0.0" }, "suggest": { "symfony/expression-language": "For using expressions in service container configuration" diff --git a/core/lib/Drupal/Component/EventDispatcher/composer.json b/core/lib/Drupal/Component/EventDispatcher/composer.json index 8039bb7..cfbcc72 100644 --- a/core/lib/Drupal/Component/EventDispatcher/composer.json +++ b/core/lib/Drupal/Component/EventDispatcher/composer.json @@ -6,8 +6,8 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=5.5.9", - "symfony/dependency-injection": "^2.8", - "symfony/event-dispatcher": "^2.7" + "symfony/dependency-injection": ">=2.8 <4.0.0", + "symfony/event-dispatcher": ">=2.7 <4.0.0" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/HttpFoundation/composer.json b/core/lib/Drupal/Component/HttpFoundation/composer.json index f82c252..910a648 100644 --- a/core/lib/Drupal/Component/HttpFoundation/composer.json +++ b/core/lib/Drupal/Component/HttpFoundation/composer.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=5.5.9", - "symfony/http-foundation": "^2.7" + "symfony/http-foundation": ">=2.7 <4.0.0" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/Plugin/composer.json b/core/lib/Drupal/Component/Plugin/composer.json index d9a5f02..77f02d6 100644 --- a/core/lib/Drupal/Component/Plugin/composer.json +++ b/core/lib/Drupal/Component/Plugin/composer.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=5.5.9", - "symfony/validator": "^2.7" + "symfony/validator": ">=2.7 <4.0.0" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/Render/FormattableMarkup.php b/core/lib/Drupal/Component/Render/FormattableMarkup.php index 6e98928..1b91b9d 100644 --- a/core/lib/Drupal/Component/Render/FormattableMarkup.php +++ b/core/lib/Drupal/Component/Render/FormattableMarkup.php @@ -62,6 +62,13 @@ class FormattableMarkup implements MarkupInterface, \Countable { /** + * The string containing placeholders. + * + * @var string + */ + protected $string; + + /** * The arguments to replace placeholders with. * * @var array diff --git a/core/lib/Drupal/Component/Serialization/composer.json b/core/lib/Drupal/Component/Serialization/composer.json index e618363..3439f58 100644 --- a/core/lib/Drupal/Component/Serialization/composer.json +++ b/core/lib/Drupal/Component/Serialization/composer.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "require": { "php": ">=5.5.9", - "symfony/yaml": "^2.7" + "symfony/yaml": ">=2.7 <4.0.0" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Component/Utility/Color.php b/core/lib/Drupal/Component/Utility/Color.php index aa296e9..e3cb56b 100644 --- a/core/lib/Drupal/Component/Utility/Color.php +++ b/core/lib/Drupal/Component/Utility/Color.php @@ -94,4 +94,28 @@ public static function rgbToHex($input) { return '#' . str_pad(dechex($out), 6, 0, STR_PAD_LEFT); } + /** + * Normalize the hex color length to 6 characters for comparison. + * + * @param string $hex + * The hex color to normalize. + * + * @return string + * The 6 character hex color. + */ + public static function normalizeHexLength($hex) { + // Ignore '#' prefixes. + $hex = ltrim($hex, '#'); + + if (strlen($hex) === 3) { + $hex[5] = $hex[2]; + $hex[4] = $hex[2]; + $hex[3] = $hex[1]; + $hex[2] = $hex[1]; + $hex[1] = $hex[0]; + } + + return '#' . $hex; + } + } diff --git a/core/lib/Drupal/Component/Utility/Unicode.php b/core/lib/Drupal/Component/Utility/Unicode.php index f8e026f..e136a89 100644 --- a/core/lib/Drupal/Component/Utility/Unicode.php +++ b/core/lib/Drupal/Component/Utility/Unicode.php @@ -548,7 +548,7 @@ public static function truncate($string, $max_length, $wordsafe = FALSE, $add_el // Find the last word boundary, if there is one within $min_wordsafe_length // to $max_length characters. preg_match() is always greedy, so it will // find the longest string possible. - $found = preg_match('/^(.{' . $min_wordsafe_length . ',' . $max_length . '})[' . Unicode::PREG_CLASS_WORD_BOUNDARY . ']/u', $string, $matches); + $found = preg_match('/^(.{' . $min_wordsafe_length . ',' . $max_length . '})[' . Unicode::PREG_CLASS_WORD_BOUNDARY . ']/us', $string, $matches); if ($found) { $string = $matches[1]; } diff --git a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php index 4589973..af1fb6e 100644 --- a/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php +++ b/core/lib/Drupal/Core/Block/Plugin/Block/PageTitleBlock.php @@ -11,6 +11,9 @@ * @Block( * id = "page_title_block", * admin_label = @Translation("Page title"), + * forms = { + * "settings_tray" = FALSE, + * }, * ) */ class PageTitleBlock extends BlockBase implements TitleBlockPluginInterface { diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php index 5f3a4a9..04f9564 100644 --- a/core/lib/Drupal/Core/Composer/Composer.php +++ b/core/lib/Drupal/Core/Composer/Composer.php @@ -162,7 +162,7 @@ public static function upgradePHPUnit(Event $event) { // If the PHP version is 7.2 or above and PHPUnit is less than version 6 // call the drupal-phpunit-upgrade script to upgrade PHPUnit. - if (version_compare(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, '7.2') >= 0 && version_compare($phpunit_package->getVersion(), '6.1') < 0) { + if (!static::upgradePHPUnitCheck($phpunit_package->getVersion())) { $event->getComposer() ->getEventDispatcher() ->dispatchScript('drupal-phpunit-upgrade'); @@ -170,6 +170,22 @@ public static function upgradePHPUnit(Event $event) { } /** + * Determines if PHPUnit needs to be upgraded. + * + * This method is located in this file because it is possible that it is + * called before the autoloader is available. + * + * @param string $phpunit_version + * The PHPUnit version string. + * + * @return bool + * TRUE if the PHPUnit needs to be upgraded, FALSE if not. + */ + public static function upgradePHPUnitCheck($phpunit_version) { + return !(version_compare(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, '7.2') >= 0 && version_compare($phpunit_version, '6.1') < 0); + } + + /** * Remove possibly problematic test files from vendored projects. * * @param \Composer\Installer\PackageEvent $event diff --git a/core/lib/Drupal/Core/Controller/ControllerBase.php b/core/lib/Drupal/Core/Controller/ControllerBase.php index c0d0cdd..2419367 100644 --- a/core/lib/Drupal/Core/Controller/ControllerBase.php +++ b/core/lib/Drupal/Core/Controller/ControllerBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Routing\UrlGeneratorTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Messenger\MessengerTrait; /** * Utility base class for thin controllers. @@ -35,6 +36,7 @@ use LinkGeneratorTrait; use LoggerChannelTrait; + use MessengerTrait; use RedirectDestinationTrait; use StringTranslationTrait; use UrlGeneratorTrait; diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php index 0b1d7dd..67f2526 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php @@ -108,6 +108,16 @@ protected function connect() { // Now, attempt the connection again; if it's successful, attempt to // create the database. Database::getConnection()->createDatabase($database); + Database::closeConnection(); + + // Now, restore the database config. + Database::removeConnection('default'); + $connection_info['default']['database'] = $database; + Database::addConnectionInfo('default', 'default', $connection_info['default']); + + // Check the database connection. + Database::getConnection(); + $this->pass('Drupal can CONNECT to the database ok.'); } catch (DatabaseNotFoundException $e) { // Still no dice; probably a permission issue. Raise the error to the diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php index 8777dda..e11c6da 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php @@ -413,7 +413,7 @@ protected function alterTable($table, $old_schema, $new_schema, array $mapping = // Now add the fields. foreach ($mapping as $field_alias => $field_source) { - // Just ignore this field (ie. use it's default value). + // Just ignore this field (ie. use its default value). if (!isset($field_source)) { continue; } diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php index 43188ec..6a4a5ca 100644 --- a/core/lib/Drupal/Core/Database/Query/Merge.php +++ b/core/lib/Drupal/Core/Database/Query/Merge.php @@ -174,7 +174,7 @@ public function updateFields(array $fields) { * Specifies fields to be updated as an expression. * * Expression fields are cases such as counter = counter + 1. This method - * takes precedence over MergeQuery::updateFields() and it's wrappers, + * takes precedence over MergeQuery::updateFields() and its wrappers, * MergeQuery::key() and MergeQuery::fields(). * * @param $field diff --git a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php index 00b6f76..9f51078 100644 --- a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php +++ b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php @@ -27,8 +27,8 @@ class ContainerBuilder extends SymfonyContainerBuilder { * {@inheritdoc} */ public function __construct(ParameterBagInterface $parameterBag = NULL) { - $this->setResourceTracking(FALSE); parent::__construct($parameterBag); + $this->setResourceTracking(FALSE); } /** @@ -88,10 +88,7 @@ public function register($id, $class = null) { if (strtolower($id) !== $id) { throw new \InvalidArgumentException("Service ID names must be lowercase: $id"); } - $definition = parent::register($id, $class); - // As of Symfony 3.4 all services are private by default. - $definition->setPublic(TRUE); - return $definition; + return parent::register($id, $class); } /** @@ -107,6 +104,22 @@ public function setAlias($alias, $id) { /** * {@inheritdoc} */ + public function setDefinition($id, Definition $definition) { + $definition = parent::setDefinition($id, $definition); + // As of Symfony 3.4 all definitions are private by default. + // \Symfony\Component\DependencyInjection\Compiler\ResolvePrivatesPassOnly + // removes services marked as private from the container even if they are + // also marked as public. Drupal requires services that are public to + // remain in the container and not be removed. + if ($definition->isPublic()) { + $definition->setPrivate(FALSE); + } + return $definition; + } + + /** + * {@inheritdoc} + */ public function setParameter($name, $value) { if (strtolower($name) !== $name) { throw new \InvalidArgumentException("Parameter names must be lowercase: $name"); diff --git a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php index 55687bb..27e9d78 100644 --- a/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php +++ b/core/lib/Drupal/Core/DependencyInjection/YamlFileLoader.php @@ -158,8 +158,6 @@ private function parseDefinition($id, $service, $file) $definition = new ChildDefinition($service['parent']); } else { $definition = new Definition(); - // As of Symfony 3.4 all services are private by default. - $definition->setPublic(TRUE); } if (isset($service['class'])) { diff --git a/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php b/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php index 96bc6b1..88f86db 100644 --- a/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php +++ b/core/lib/Drupal/Core/Entity/Annotation/EntityReferenceSelection.php @@ -63,7 +63,7 @@ class EntityReferenceSelection extends Plugin { public $entity_types = []; /** - * The weight of the plugin in it's group. + * The weight of the plugin in its group. * * @var int */ diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index ad6c62d..6b999c6 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -420,8 +420,8 @@ public function getRevisionId() { * {@inheritdoc} */ public function isTranslatable() { - // Check that the bundle is translatable, the entity has a language defined - // and if we have more than one language on the site. + // Check the bundle is translatable, the entity has a language defined, and + // the site has more than one language. $bundles = $this->entityManager()->getBundleInfo($this->entityTypeId); return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual(); } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php index 5be34c9..3871405 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityType.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php @@ -15,6 +15,18 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface protected $revision_metadata_keys = []; /** + * The required revision metadata keys. + * + * This property should only be filled in the constructor. This ensures that + * only new instances get newly added required revision metadata keys. + * Unserialized objects will only retrieve the keys that they already have + * been cached with. + * + * @var array + */ + protected $requiredRevisionMetadataKeys = []; + + /** * {@inheritdoc} */ public function __construct($definition) { @@ -25,9 +37,23 @@ public function __construct($definition) { 'view_builder' => 'Drupal\Core\Entity\EntityViewBuilder', ]; - $this->revision_metadata_keys += [ - 'revision_default' => 'revision_default', - ]; + // Only new instances should provide the required revision metadata keys. + // The cached instances should return only what already has been stored + // under the property $revision_metadata_keys. The BC layer in + // ::getRevisionMetadataKeys() has to detect if the revision metadata keys + // have been provided by the entity type annotation, therefore we add keys + // to the property $requiredRevisionMetadataKeys only if those keys aren't + // set in the entity type annotation. + if (!isset($this->revision_metadata_keys['revision_default'])) { + $this->requiredRevisionMetadataKeys['revision_default'] = 'revision_default'; + } + + // Add the required revision metadata fields here instead in the getter + // method, so that they are serialized as part of the object even if the + // getter method doesn't get called. This allows the list to be further + // extended. Only new instances of the class will contain the new list, + // while the cached instances contain the previous version of the list. + $this->revision_metadata_keys += $this->requiredRevisionMetadataKeys; } /** @@ -59,7 +85,7 @@ protected function checkStorageClass($class) { public function getRevisionMetadataKeys($include_backwards_compatibility_field_names = TRUE) { // Provide backwards compatibility in case the revision metadata keys are // not defined in the entity annotation. - if (!$this->revision_metadata_keys && $include_backwards_compatibility_field_names) { + if ((!$this->revision_metadata_keys || ($this->revision_metadata_keys == $this->requiredRevisionMetadataKeys)) && $include_backwards_compatibility_field_names) { $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id()); if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) { @trigger_error('The revision_user revision metadata key is not set.', E_USER_DEPRECATED); diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index e82ed81..cbe91d9 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -39,7 +39,7 @@ public function getInfo() { $info['#validate_reference'] = TRUE; // IMPORTANT! This should only be set to FALSE if the #default_value // property is processed at another level (e.g. by a Field API widget) and - // it's value is properly checked for access. + // its value is properly checked for access. $info['#process_default_value'] = TRUE; $info['#element_validate'] = [[$class, 'validateEntityAutocomplete']]; diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManager.php b/core/lib/Drupal/Core/Entity/EntityFieldManager.php index c199564..025d92c 100644 --- a/core/lib/Drupal/Core/Entity/EntityFieldManager.php +++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php @@ -224,7 +224,9 @@ protected function buildBaseFieldDefinitions($entity_type_id) { // Make sure that revisionable entity types are correctly defined. if ($entity_type->isRevisionable()) { - $field_name = $entity_type->getRevisionMetadataKey('revision_default'); + // Disable the BC layer to prevent a recursion, this only needs the + // revision_default key that is always set. + $field_name = $entity_type->getRevisionMetadataKeys(FALSE)['revision_default']; $base_field_definitions[$field_name] = BaseFieldDefinition::create('boolean') ->setLabel($this->t('Default revision')) ->setDescription($this->t('A flag indicating whether this was a default revision when it was saved.')) diff --git a/core/lib/Drupal/Core/Entity/EntityResolverManager.php b/core/lib/Drupal/Core/Entity/EntityResolverManager.php index 10ad93a..e9a87d6 100644 --- a/core/lib/Drupal/Core/Entity/EntityResolverManager.php +++ b/core/lib/Drupal/Core/Entity/EntityResolverManager.php @@ -197,30 +197,6 @@ protected function setParametersFromEntityInformation(Route $route) { } /** - * Ensure revisionable entities load the latest revision on entity forms. - * - * @param \Symfony\Component\Routing\Route $route - * The route object. - */ - protected function setLatestRevisionFlag(Route $route) { - if (!$entity_form = $route->getDefault('_entity_form')) { - return; - } - // Only set the flag on entity types which are revisionable. - list($entity_type) = explode('.', $entity_form, 2); - if (!isset($this->getEntityTypes()[$entity_type]) || !$this->getEntityTypes()[$entity_type]->isRevisionable()) { - return; - } - $parameters = $route->getOption('parameters') ?: []; - foreach ($parameters as &$parameter) { - if ($parameter['type'] === 'entity:' . $entity_type && !isset($parameter['load_latest_revision'])) { - $parameter['load_latest_revision'] = TRUE; - } - } - $route->setOption('parameters', $parameters); - } - - /** * Set the upcasting route objects. * * @param \Symfony\Component\Routing\Route $route @@ -236,7 +212,6 @@ public function setRouteOptions(Route $route) { // Try to use _entity_* information on the route. $this->setParametersFromEntityInformation($route); - $this->setLatestRevisionFlag($route); } /** diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index f5204f8..28f5b4e 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -155,6 +155,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface { protected $data_table = NULL; /** + * Indicates whether the entity data is internal. + * + * @var bool + */ + protected $internal = FALSE; + + /** * Indicates whether entities of this type have multilingual support. * * @var bool @@ -357,6 +364,13 @@ public function set($property, $value) { /** * {@inheritdoc} */ + public function isInternal() { + return $this->internal; + } + + /** + * {@inheritdoc} + */ public function isStaticallyCacheable() { return $this->static_cache; } diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index 6d398d7..7d994ca 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -563,6 +563,24 @@ public function getBundleLabel(); public function getBaseTable(); /** + * Indicates whether the entity data is internal. + * + * This can be used in a scenario when it is not desirable to expose data of + * this entity type to an external system. + * + * The implications of this method are left to the discretion of the caller. + * For example, a module providing an HTTP API may not expose entities of + * this type or a custom entity reference field settings form may deprioritize + * entities of this type in a select list. + * + * @return bool + * TRUE if the entity data is internal, FALSE otherwise. + * + * @see \Drupal\Core\TypedData\DataDefinitionInterface::isInternal() + */ + public function isInternal(); + + /** * Indicates whether entities of this type have multilingual support. * * At an entity level, this indicates language support and at a bundle level diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php index 98be20e..5114c40 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php +++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/Deriver/EntityDeriver.php @@ -89,6 +89,7 @@ public function getDerivativeDefinitions($base_plugin_definition) { $this->derivatives[$entity_type_id] = [ 'label' => $entity_type->getLabel(), 'constraints' => $entity_type->getConstraints(), + 'internal' => $entity_type->isInternal(), ] + $base_plugin_definition; // Incorporate the bundles as entity:$entity_type:$bundle, if any. diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php index db194b5..b0b94b8 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php +++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php @@ -167,7 +167,7 @@ public function applyDefaultValue($notify = TRUE) { * {@inheritdoc} */ public function getIterator() { - return isset($this->entity) ? $this->entity->getIterator() : new \ArrayIterator([]); + return $this->entity instanceof \IteratorAggregate ? $this->entity->getIterator() : new \ArrayIterator([]); } } diff --git a/core/lib/Drupal/Core/Entity/Query/QueryBase.php b/core/lib/Drupal/Core/Entity/Query/QueryBase.php index d84a266..06dfb8f 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryBase.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.php @@ -449,7 +449,7 @@ public function groupBy($field, $langcode = NULL) { } /** - * Generates an alias for a field and it's aggregated function. + * Generates an alias for a field and its aggregated function. * * @param string $field * The field name used in the alias. diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php index 11f048e..d7f9bde 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php @@ -118,7 +118,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen * {@inheritdoc} */ public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) { - return isset($element['target_id']) ? $element['target_id'] : FALSE; + return $element['target_id']; } /** diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index b1579f2..6367157 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -104,6 +104,19 @@ public function form(FieldItemListInterface $items, array &$form, FormStateInter $elements = $this->formMultipleElements($items, $form, $form_state); } + // Allow modules to alter the field multi-value widget form element. + // This hook can also be used for single-value fields. + $context = [ + 'form' => $form, + 'widget' => $this, + 'items' => $items, + 'default' => $this->isDefaultValueWidget($form_state), + ]; + \Drupal::moduleHandler()->alter([ + 'field_widget_multivalue_form', + 'field_widget_multivalue_' . $this->getPluginId() . '_form', + ], $elements, $form_state, $context); + // Populate the 'array_parents' information in $form_state->get('field') // after the form is built, so that we catch changes in the form structure // performed in alter() hooks. @@ -410,21 +423,26 @@ public function flagErrors(FieldItemListInterface $items, ConstraintViolationLis if (Element::isVisibleElement($element)) { $handles_multiple = $this->handlesMultipleValues(); - $violations_by_delta = []; + $violations_by_delta = $item_list_violations = []; foreach ($violations as $violation) { // Separate violations by delta. $property_path = explode('.', $violation->getPropertyPath()); $delta = array_shift($property_path); - $violations_by_delta[$delta][] = $violation; + if (is_numeric($delta)) { + $violations_by_delta[$delta][] = $violation; + } + // Violations at the ItemList level are not associated to any delta. + else { + $item_list_violations[] = $violation; + } $violation->arrayPropertyPath = $property_path; } /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $delta_violations */ foreach ($violations_by_delta as $delta => $delta_violations) { - // Pass violations to the main element: - // - if this is a multiple-value widget, - // - or if the violations are at the ItemList level. - if ($handles_multiple || !is_numeric($delta)) { + // Pass violations to the main element if this is a multiple-value + // widget. + if ($handles_multiple) { $delta_element = $element; } // Otherwise, pass errors by delta to the corresponding sub-element. @@ -440,6 +458,13 @@ public function flagErrors(FieldItemListInterface $items, ConstraintViolationLis } } } + + /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $item_list_violations */ + // Pass violations to the main element without going through + // errorElement() if the violations are at the ItemList level. + foreach ($item_list_violations as $violation) { + $form_state->setError($element, $violation->getMessage()); + } } } } diff --git a/core/lib/Drupal/Core/Form/FormBase.php b/core/lib/Drupal/Core/Form/FormBase.php index 02a1c39..165cb3a 100644 --- a/core/lib/Drupal/Core/Form/FormBase.php +++ b/core/lib/Drupal/Core/Form/FormBase.php @@ -12,6 +12,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Drupal\Core\Messenger\MessengerTrait; /** * Provides a base class for forms. @@ -45,6 +46,7 @@ use DependencySerializationTrait; use LinkGeneratorTrait; use LoggerChannelTrait; + use MessengerTrait; use RedirectDestinationTrait; use StringTranslationTrait; use UrlGeneratorTrait; diff --git a/core/lib/Drupal/Core/Form/FormErrorHandler.php b/core/lib/Drupal/Core/Form/FormErrorHandler.php index 9783cb5..02457ce 100644 --- a/core/lib/Drupal/Core/Form/FormErrorHandler.php +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php @@ -127,7 +127,7 @@ protected function setElementErrorsFromFormState(array &$form, FormStateInterfac } // Additionally store the errors of the direct child itself, keyed by - // it's parent elements structure. + // its parent elements structure. if (!empty($child['#errors'])) { $child_parents = implode('][', $child['#array_parents']); $children_errors[$child_parents] = $child['#errors']; diff --git a/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php b/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php index ea209ad..488242a 100644 --- a/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php +++ b/core/lib/Drupal/Core/Installer/Form/SelectProfileForm.php @@ -72,6 +72,11 @@ public function buildForm(array $form, FormStateInterface $form_state, $install_ ]; foreach (array_keys($names) as $profile_name) { $form['profile'][$profile_name]['#description'] = isset($profiles[$profile_name]['description']) ? $this->t($profiles[$profile_name]['description']) : ''; + // @todo Remove hardcoding of 'demo_umami' profile for a generic warning + // system in https://www.drupal.org/project/drupal/issues/2822414. + if ($profile_name === 'demo_umami') { + $this->addUmamiWarning($form); + } } $form['actions'] = ['#type' => 'actions']; $form['actions']['submit'] = [ @@ -90,4 +95,28 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $install_state['parameters']['profile'] = $form_state->getValue('profile'); } + /** + * Show profile warning if 'demo_umami' profile is selected. + */ + protected function addUmamiWarning(array &$form) { + // Warning to show when this profile is selected. + $description = $form['profile']['demo_umami']['#description']; + // Re-defines radio #description to show warning when selected. + $form['profile']['demo_umami']['#description'] = [ + 'warning' => [ + '#type' => 'item', + '#markup' => $this->t('This profile is intended for demonstration purposes only.'), + '#wrapper_attributes' => [ + 'class' => ['messages', 'messages--warning'], + ], + '#states' => [ + 'visible' => [ + ':input[name="profile"]' => ['value' => 'demo_umami'], + ], + ], + ], + 'description' => ['#markup' => $description], + ]; + } + } diff --git a/core/lib/Drupal/Core/Messenger/MessengerTrait.php b/core/lib/Drupal/Core/Messenger/MessengerTrait.php new file mode 100644 index 0000000..1454243 --- /dev/null +++ b/core/lib/Drupal/Core/Messenger/MessengerTrait.php @@ -0,0 +1,40 @@ +messenger = $messenger; + } + + /** + * Gets the messenger. + * + * @return \Drupal\Core\Messenger\MessengerInterface + * The messenger. + */ + public function messenger() { + if (!isset($this->messenger)) { + $this->messenger = \Drupal::messenger(); + } + return $this->messenger; + } + +} diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 0024694..d435798 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -218,7 +218,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Early-return nothing if user does not have access. if (isset($elements['#access'])) { - // If #access is an AccessResultInterface object, we must apply it's + // If #access is an AccessResultInterface object, we must apply its // cacheability metadata to the render array. if ($elements['#access'] instanceof AccessResultInterface) { $this->addCacheableDependency($elements, $elements['#access']); diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php index 6c20b1e..95cbd67 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -6,6 +6,8 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Drupal\Core\State\StateInterface; @@ -86,6 +88,13 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv protected $pathProcessor; /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** * Cache ID prefix used to load routes. */ const ROUTE_LOAD_CID_PREFIX = 'route_provider.route_load:'; @@ -107,8 +116,10 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv * The cache tag invalidator. * @param string $table * (Optional) The table in the database to use for matching. Defaults to 'router' + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * (Optional) The language manager. */ - public function __construct(Connection $connection, StateInterface $state, CurrentPathStack $current_path, CacheBackendInterface $cache_backend, InboundPathProcessorInterface $path_processor, CacheTagsInvalidatorInterface $cache_tag_invalidator, $table = 'router') { + public function __construct(Connection $connection, StateInterface $state, CurrentPathStack $current_path, CacheBackendInterface $cache_backend, InboundPathProcessorInterface $path_processor, CacheTagsInvalidatorInterface $cache_tag_invalidator, $table = 'router', LanguageManagerInterface $language_manager = NULL) { $this->connection = $connection; $this->state = $state; $this->currentPath = $current_path; @@ -116,6 +127,7 @@ public function __construct(Connection $connection, StateInterface $state, Curre $this->cacheTagInvalidator = $cache_tag_invalidator; $this->pathProcessor = $path_processor; $this->tableName = $table; + $this->languageManager = $language_manager ?: \Drupal::languageManager(); } /** @@ -147,7 +159,7 @@ public function __construct(Connection $connection, StateInterface $state, Curre public function getRouteCollectionForRequest(Request $request) { // Cache both the system path as well as route parameters and matching // routes. - $cid = 'route:' . $request->getPathInfo() . ':' . $request->getQueryString(); + $cid = $this->getRouteCollectionCacheId($request); if ($cached = $this->cache->get($cid)) { $this->currentPath->setPath($cached->data['path'], $request); $request->query->replace($cached->data['query']); @@ -431,4 +443,35 @@ public function getRoutesCount() { return $this->connection->query("SELECT COUNT(*) FROM {" . $this->connection->escapeTable($this->tableName) . "}")->fetchField(); } + /** + * Returns the cache ID for the route collection cache. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return string + * The cache ID. + */ + protected function getRouteCollectionCacheId(Request $request) { + // Include the current language code in the cache identifier as + // the language information can be elsewhere than in the path, for example + // based on the domain. + $language_part = $this->getCurrentLanguageCacheIdPart(); + return 'route:' . $language_part . ':' . $request->getPathInfo() . ':' . $request->getQueryString(); + } + + /** + * Returns the language identifier for the route collection cache. + * + * @return string + * The language identifier. + */ + protected function getCurrentLanguageCacheIdPart() { + // This must be in sync with the language logic in + // \Drupal\Core\PathProcessor\PathProcessorAlias::processInbound() and + // \Drupal\Core\Path\AliasManager::getPathByAlias(). + // @todo Update this if necessary in https://www.drupal.org/node/1125428. + return $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(); + } + } diff --git a/core/lib/Drupal/Core/StreamWrapper/PrivateStream.php b/core/lib/Drupal/Core/StreamWrapper/PrivateStream.php index e7bd97e..22fb90f 100644 --- a/core/lib/Drupal/Core/StreamWrapper/PrivateStream.php +++ b/core/lib/Drupal/Core/StreamWrapper/PrivateStream.php @@ -48,7 +48,7 @@ public function getDirectoryPath() { */ public function getExternalUrl() { $path = str_replace('\\', '/', $this->getTarget()); - return $this->url('system.private_file_download', ['filepath' => $path], ['absolute' => TRUE]); + return $this->url('system.private_file_download', ['filepath' => $path], ['absolute' => TRUE, 'path_processing' => FALSE]); } /** diff --git a/core/lib/Drupal/Core/StringTranslation/TranslatableMarkup.php b/core/lib/Drupal/Core/StringTranslation/TranslatableMarkup.php index 8016212..3e9461a 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslatableMarkup.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslatableMarkup.php @@ -24,13 +24,6 @@ class TranslatableMarkup extends FormattableMarkup { use ToStringTrait; /** - * The string to be translated. - * - * @var string - */ - protected $string; - - /** * The translated markup without placeholder replacements. * * @var string @@ -139,8 +132,7 @@ public function __construct($string, array $arguments = [], array $options = [], $message = $string instanceof TranslatableMarkup ? '$string ("' . $string->getUntranslatedString() . '") must be a string.' : '$string ("' . (string) $string . '") must be a string.'; throw new \InvalidArgumentException($message); } - $this->string = $string; - $this->arguments = $arguments; + parent::__construct($string, $arguments); $this->options = $options; $this->stringTranslation = $string_translation; } diff --git a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php index 5b57a87..76a9f95 100644 --- a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php +++ b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php @@ -71,7 +71,7 @@ public function getTranslation($langcode); public function getUntranslated(); /** - * Returns TRUE there is a translation for the given language code. + * Checks there is a translation for the given language code. * * @param string $langcode * The language code identifying the translation. diff --git a/core/misc/dialog/off-canvas.base.css b/core/misc/dialog/off-canvas.base.css index 470e330..138ea03 100644 --- a/core/misc/dialog/off-canvas.base.css +++ b/core/misc/dialog/off-canvas.base.css @@ -20,7 +20,7 @@ font-weight: normal; color: #85bef4; text-decoration: none; - transition: color .5s ease; + transition: color 0.5s ease; } #drupal-off-canvas a:focus, @@ -31,7 +31,7 @@ } #drupal-off-canvas hr { height: 1px; - background: #cccccc; + background: #ccc; } #drupal-off-canvas summary, #drupal-off-canvas .fieldgroup:not(.form-composite) > legend { diff --git a/core/misc/dialog/off-canvas.button.css b/core/misc/dialog/off-canvas.button.css index 96a6823..7345bd7 100644 --- a/core/misc/dialog/off-canvas.button.css +++ b/core/misc/dialog/off-canvas.button.css @@ -24,7 +24,7 @@ background: transparent; font-size: 14px; color: #85bef4; - transition: color .5s ease; + transition: color 0.5s ease; } #drupal-off-canvas button.link:hover, #drupal-off-canvas button.link:focus { @@ -45,7 +45,7 @@ color: #f5f5f5; text-align: center; cursor: pointer; - transition: background .5s ease; + transition: background 0.5s ease; } #drupal-off-canvas input[type="submit"].button:hover, #drupal-off-canvas input[type="submit"].button:focus, diff --git a/core/misc/dialog/off-canvas.details.css b/core/misc/dialog/off-canvas.details.css index dcaea5e..1bd5097 100644 --- a/core/misc/dialog/off-canvas.details.css +++ b/core/misc/dialog/off-canvas.details.css @@ -35,7 +35,7 @@ text-shadow: none; padding: 10px 20px; font-size: 14px; - transition: all .5s ease; + transition: all 0.5s ease; } #drupal-off-canvas summary:hover, #drupal-off-canvas summary:focus { diff --git a/core/misc/dialog/off-canvas.dropbutton.css b/core/misc/dialog/off-canvas.dropbutton.css index 8e698bc..aa36ffb 100644 --- a/core/misc/dialog/off-canvas.dropbutton.css +++ b/core/misc/dialog/off-canvas.dropbutton.css @@ -24,7 +24,7 @@ text-align: center; line-height: normal; cursor: pointer; - transition: background .5s ease; + transition: background 0.5s ease; } #drupal-off-canvas .dropbutton-widget:hover { background: #2b8bd8; @@ -224,7 +224,7 @@ background: transparent; } -/* Prevent list item from expanding it's container. */ +/* Prevent list item from expanding its container. */ #drupal-off-canvas td ul.dropbutton li.edit { width: 2em; height: 2em; diff --git a/core/misc/dialog/off-canvas.form.css b/core/misc/dialog/off-canvas.form.css index f1def25..a393f15 100644 --- a/core/misc/dialog/off-canvas.form.css +++ b/core/misc/dialog/off-canvas.form.css @@ -84,7 +84,7 @@ #drupal-off-canvas .form-textarea, #drupal-off-canvas .form-date, #drupal-off-canvas .form-time { - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .125); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.125); background-color: #eee; border-color: #333; color: #595959; @@ -101,7 +101,7 @@ #drupal-off-canvas .form-date:focus, #drupal-off-canvas .form-time:focus { border-color: #40b6ff; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .125), 0 0 8px #40b6ff; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.125), 0 0 8px #40b6ff; background-color: #fff; } #drupal-off-canvas td .form-item, diff --git a/core/misc/dialog/off-canvas.motion.css b/core/misc/dialog/off-canvas.motion.css index 2d5ea5f..b3158e9 100644 --- a/core/misc/dialog/off-canvas.motion.css +++ b/core/misc/dialog/off-canvas.motion.css @@ -7,5 +7,5 @@ */ .dialog-off-canvas-main-canvas { - transition: all .7s ease; + transition: all 0.7s ease; } diff --git a/core/misc/dialog/off-canvas.tabledrag.css b/core/misc/dialog/off-canvas.tabledrag.css index edeef83..ae8c02a 100644 --- a/core/misc/dialog/off-canvas.tabledrag.css +++ b/core/misc/dialog/off-canvas.tabledrag.css @@ -53,7 +53,7 @@ text-decoration: none; } #drupal-off-canvas tr td { - transition: background .3s ease; + transition: background 0.3s ease; } #drupal-off-canvas tr td abbr { diff --git a/core/misc/dialog/off-canvas.theme.css b/core/misc/dialog/off-canvas.theme.css index f3f6930..ddc165a 100644 --- a/core/misc/dialog/off-canvas.theme.css +++ b/core/misc/dialog/off-canvas.theme.css @@ -40,12 +40,12 @@ position: absolute; top: calc(50% - 6px); right: 1em; - transition: all .5s ease; + transition: all 0.5s ease; } .ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar-close:hover, .ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar-close:focus { background-image: url(../icons/ffffff/ex.svg); - border: 3px solid #ffffff; + border: 3px solid #fff; } [dir="rtl"] .ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar-close { left: 1em; diff --git a/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php b/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php index 7f7b7ef..938752a 100644 --- a/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php +++ b/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php @@ -15,7 +15,7 @@ class ActionTest extends MigrateSqlSourceTestBase { /** * {@inheritdoc} */ - public static $modules = ['action', 'migrate_drupal']; + public static $modules = ['action', 'migrate_drupal', 'system']; /** * {@inheritdoc} diff --git a/core/modules/aggregator/src/Controller/AggregatorController.php b/core/modules/aggregator/src/Controller/AggregatorController.php index b459b19..7fd0743 100644 --- a/core/modules/aggregator/src/Controller/AggregatorController.php +++ b/core/modules/aggregator/src/Controller/AggregatorController.php @@ -93,7 +93,7 @@ public function feedRefresh(FeedInterface $aggregator_feed) { $message = $aggregator_feed->refreshItems() ? $this->t('There is new syndicated content from %site.', ['%site' => $aggregator_feed->label()]) : $this->t('There is no new syndicated content from %site.', ['%site' => $aggregator_feed->label()]); - drupal_set_message($message); + $this->messenger()->addStatus($message); return $this->redirect('aggregator.admin_overview'); } diff --git a/core/modules/aggregator/src/Plugin/Block/AggregatorFeedBlock.php b/core/modules/aggregator/src/Plugin/Block/AggregatorFeedBlock.php index 90d8f66..f4c8e2a 100644 --- a/core/modules/aggregator/src/Plugin/Block/AggregatorFeedBlock.php +++ b/core/modules/aggregator/src/Plugin/Block/AggregatorFeedBlock.php @@ -167,8 +167,10 @@ public function build() { */ public function getCacheTags() { $cache_tags = parent::getCacheTags(); - $feed = $this->feedStorage->load($this->configuration['feed']); - return Cache::mergeTags($cache_tags, $feed->getCacheTags()); + if ($feed = $this->feedStorage->load($this->configuration['feed'])) { + $cache_tags = Cache::mergeTags($cache_tags, $feed->getCacheTags()); + } + return $cache_tags; } } diff --git a/core/modules/block_content/block_content.routing.yml b/core/modules/block_content/block_content.routing.yml index 37d9b71..35724a6 100644 --- a/core/modules/block_content/block_content.routing.yml +++ b/core/modules/block_content/block_content.routing.yml @@ -1,11 +1,3 @@ -entity.block_content_type.collection: - path: '/admin/structure/block/block-content/types' - defaults: - _entity_list: 'block_content_type' - _title: 'Custom block library' - requirements: - _permission: 'administer blocks' - block_content.add_page: path: '/block/add' defaults: @@ -26,16 +18,6 @@ block_content.add_form: requirements: _permission: 'administer blocks' -entity.block_content_type.delete_form: - path: '/admin/structure/block/block-content/manage/{block_content_type}/delete' - defaults: - _entity_form: 'block_content_type.delete' - _title: 'Delete' - requirements: - _entity_access: 'block_content_type.delete' - options: - _admin_route: TRUE - entity.block_content.canonical: path: '/block/{block_content}' defaults: @@ -75,14 +57,6 @@ block_content.type_add: requirements: _permission: 'administer blocks' -entity.block_content_type.edit_form: - path: '/admin/structure/block/block-content/manage/{block_content_type}' - defaults: - _entity_form: 'block_content_type.edit' - _title_callback: '\Drupal\Core\Entity\Controller\EntityController::title' - requirements: - _entity_access: 'block_content_type.update' - entity.block_content.collection: path: '/admin/structure/block/block-content' defaults: diff --git a/core/modules/block_content/src/Entity/BlockContentType.php b/core/modules/block_content/src/Entity/BlockContentType.php index 339b4e5..1b63eda 100644 --- a/core/modules/block_content/src/Entity/BlockContentType.php +++ b/core/modules/block_content/src/Entity/BlockContentType.php @@ -11,6 +11,7 @@ * @ConfigEntityType( * id = "block_content_type", * label = @Translation("Custom block type"), + * label_collection = @Translation("Custom block library"), * handlers = { * "form" = { * "default" = "Drupal\block_content\BlockContentTypeForm", @@ -18,6 +19,9 @@ * "edit" = "Drupal\block_content\BlockContentTypeForm", * "delete" = "Drupal\block_content\Form\BlockContentTypeDeleteForm" * }, + * "route_provider" = { + * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider" + * }, * "list_builder" = "Drupal\block_content\BlockContentTypeListBuilder" * }, * admin_permission = "administer blocks", diff --git a/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php b/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php index 6c76a90..8105d3f 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentTypeTest.php @@ -122,7 +122,7 @@ public function testBlockContentTypeEditing() { $front_page_path => 'Home', 'admin/structure/block' => 'Block layout', 'admin/structure/block/block-content' => 'Custom block library', - 'admin/structure/block/block-content/manage/basic' => 'Bar', + 'admin/structure/block/block-content/manage/basic' => 'Edit Bar', ]); \Drupal::entityManager()->clearCachedFieldDefinitions(); diff --git a/core/modules/block_place/css/block-place.css b/core/modules/block_place/css/block-place.css index d8f036b..9eba9b6 100644 --- a/core/modules/block_place/css/block-place.css +++ b/core/modules/block_place/css/block-place.css @@ -14,8 +14,8 @@ .block-place-region a.button { position: relative; - background: url(../../../misc/icons/bebebe/plus.svg) #ffffff center center / 16px 16px no-repeat; - border: 1px solid #cccccc; + background: url(../../../misc/icons/bebebe/plus.svg) #fff center center / 16px 16px no-repeat; + border: 1px solid #ccc; box-sizing: border-box; font-size: 1rem; padding: 0; diff --git a/core/modules/book/src/BookOutlineStorageInterface.php b/core/modules/book/src/BookOutlineStorageInterface.php index daaf92d..c09c52a 100644 --- a/core/modules/book/src/BookOutlineStorageInterface.php +++ b/core/modules/book/src/BookOutlineStorageInterface.php @@ -69,7 +69,7 @@ public function getChildRelativeDepth($book_link, $max_depth); public function delete($nid); /** - * Loads book's children using it's parent ID. + * Loads book's children using its parent ID. * * @param int $pid * The book's parent ID. diff --git a/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php b/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php index feece1f..25d0906 100644 --- a/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php +++ b/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php @@ -13,7 +13,7 @@ class BookTest extends MigrateSqlSourceTestBase { /** * {@inheritdoc} */ - public static $modules = ['book', 'migrate_drupal']; + public static $modules = ['book', 'migrate_drupal', 'node']; /** * {@inheritdoc} diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css index cc801e7..9a8a48b 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -180,7 +180,7 @@ padding: 4px 6px; position: relative; text-decoration: none; - text-shadow: 0 1px 0 rgba(255, 255, 255, .5); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); white-space: nowrap; } .ckeditor-toolbar-dividers { diff --git a/core/modules/ckeditor/css/ckeditor.css b/core/modules/ckeditor/css/ckeditor.css index b4b1e16..bc2c43a 100644 --- a/core/modules/ckeditor/css/ckeditor.css +++ b/core/modules/ckeditor/css/ckeditor.css @@ -7,7 +7,7 @@ .ckeditor-dialog-loading-link { border-radius: 0 0 5px 5px; - border: 1px solid #B6B6B6; + border: 1px solid #b6b6b6; border-top: none; background: white; padding: 3px 10px; diff --git a/core/modules/color/color.module b/core/modules/color/color.module index f4ee9fd..9ca7870 100644 --- a/core/modules/color/color.module +++ b/core/modules/color/color.module @@ -5,6 +5,7 @@ * Allows users to change the color scheme of themes. */ +use Drupal\Component\Utility\Color; use Drupal\Component\Utility\Unicode; use Drupal\Core\Asset\CssOptimizer; use Drupal\Component\Utility\Bytes; @@ -513,7 +514,8 @@ function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette, $style) { // Prepare color conversion table. $conversion = $palette; foreach ($conversion as $k => $v) { - $conversion[$k] = Unicode::strtolower($v); + $v = Unicode::strtolower($v); + $conversion[$k] = Color::normalizeHexLength($v); } $default = color_get_palette($theme, TRUE); @@ -533,6 +535,7 @@ function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette, $style) { foreach ($style as $chunk) { if ($is_color) { $chunk = Unicode::strtolower($chunk); + $chunk = Color::normalizeHexLength($chunk); // Check if this is one of the colors in the default palette. if ($key = array_search($chunk, $default)) { $chunk = $conversion[$key]; diff --git a/core/modules/color/tests/modules/color_test/themes/color_test_theme/css/colors.css b/core/modules/color/tests/modules/color_test/themes/color_test_theme/css/colors.css index 07a4193..fa6836b 100644 --- a/core/modules/color/tests/modules/color_test/themes/color_test_theme/css/colors.css +++ b/core/modules/color/tests/modules/color_test/themes/color_test_theme/css/colors.css @@ -1,7 +1,7 @@ html { - color: #0000ff; + color: #00f; } body { - color: #ff0000; + color: #f00; } diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module index 985f91f..4b07e45 100644 --- a/core/modules/content_moderation/content_moderation.module +++ b/core/modules/content_moderation/content_moderation.module @@ -128,6 +128,15 @@ function content_moderation_entity_translation_delete(EntityInterface $translati } /** + * Implements hook_entity_prepare_form(). + */ +function content_moderation_entity_prepare_form(EntityInterface $entity, $operation, FormStateInterface $form_state) { + \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityTypeInfo::class) + ->entityPrepareForm($entity, $operation, $form_state); +} + +/** * Implements hook_form_alter(). */ function content_moderation_form_alter(&$form, FormStateInterface $form_state, $form_id) { @@ -252,6 +261,7 @@ function content_moderation_action_info_alter(&$definitions) { * Implements hook_entity_bundle_info_alter(). */ function content_moderation_entity_bundle_info_alter(&$bundles) { + $translatable = FALSE; /** @var \Drupal\workflows\WorkflowInterface $workflow */ foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */ @@ -260,10 +270,18 @@ function content_moderation_entity_bundle_info_alter(&$bundles) { foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) { if (isset($bundles[$entity_type_id][$bundle_id])) { $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id(); + // If we have even one moderation-enabled translatable bundle, we need + // to make the moderation state bundle translatable as well, to enable + // the revision translation merge logic also for content moderation + // state revisions. + if (!empty($bundles[$entity_type_id][$bundle_id]['translatable'])) { + $translatable = TRUE; + } } } } } + $bundles['content_moderation_state']['content_moderation_state']['translatable'] = $translatable; } /** @@ -301,13 +319,3 @@ function content_moderation_workflow_update(WorkflowInterface $entity) { // Clear field cache so extra field is added or removed. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); } - -/** - * Implements hook_rest_resource_alter(). - */ -function content_moderation_rest_resource_alter(&$definitions) { - // ContentModerationState is an internal entity type. Therefore it should not - // be exposed via REST. - // @see \Drupal\content_moderation\ContentModerationStateAccessControlHandler - unset($definitions['entity:content_moderation_state']); -} diff --git a/core/modules/content_moderation/content_moderation.post_update.php b/core/modules/content_moderation/content_moderation.post_update.php new file mode 100644 index 0000000..e2e583e --- /dev/null +++ b/core/modules/content_moderation/content_moderation.post_update.php @@ -0,0 +1,95 @@ +getTypePlugin(); + foreach ($plugin->getEntityTypes() as $entity_type_id) { + $sandbox['entity_type_ids'][$entity_type_id] = $entity_type_id; + foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle) { + $sandbox['bundles'][$entity_type_id][$bundle] = $bundle; + } + } + } + $sandbox['offset'] = 0; + $sandbox['limit'] = Settings::get('entity_update_batch_size', 50); + $sandbox['total'] = count($sandbox['entity_type_ids']); + $entity_type_id = array_shift($sandbox['entity_type_ids']); + } + + // If there are no moderated bundles or we processed all of them, we are done. + $entity_type_manager = \Drupal::entityTypeManager(); + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $content_moderation_state_storage */ + $content_moderation_state_storage = $entity_type_manager->getStorage('content_moderation_state'); + if (!$entity_type_id) { + $content_moderation_state_storage->resetCache(); + $sandbox['#finished'] = 1; + return; + } + + // Retrieve a batch of moderated entities to be processed. + $storage = $entity_type_manager->getStorage($entity_type_id); + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + $query = $storage->getQuery() + ->accessCheck(FALSE) + ->sort($entity_type->getKey('id')) + ->range($sandbox['offset'], $sandbox['limit']); + $bundle_key = $entity_type->getKey('bundle'); + if ($bundle_key && !empty($sandbox['bundles'][$entity_type_id])) { + $bundles = array_keys($sandbox['bundles'][$entity_type_id]); + $query->condition($bundle_key, $bundles, 'IN'); + } + $entity_ids = $query->execute(); + + // Compute progress status and skip to the next entity type, if needed. + $sandbox['#finished'] = ($sandbox['total'] - count($sandbox['entity_type_ids']) - 1) / $sandbox['total']; + if (!$entity_ids) { + $sandbox['offset'] = 0; + $entity_type_id = array_shift($sandbox['entity_type_ids']) ?: FALSE; + return; + } + + // Load the "content_moderation_state" revisions corresponding to the + // moderated entity default revisions. + $result = $content_moderation_state_storage->getQuery() + ->allRevisions() + ->condition('content_entity_type_id', $entity_type_id) + ->condition('content_entity_revision_id', array_keys($entity_ids), 'IN') + ->execute(); + /** @var \Drupal\Core\Entity\ContentEntityInterface[] $revisions */ + $revisions = $content_moderation_state_storage->loadMultipleRevisions(array_keys($result)); + + // Update "content_moderation_state" data. + foreach ($revisions as $revision) { + if (!$revision->isDefaultRevision()) { + $revision->setNewRevision(FALSE); + $revision->isDefaultRevision(TRUE); + $content_moderation_state_storage->save($revision); + } + } + + // Clear static cache to avoid memory issues. + $storage->resetCache($entity_ids); + + $sandbox['offset'] += $sandbox['limit']; +} diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml index 3d6055a..225d44d 100644 --- a/core/modules/content_moderation/content_moderation.services.yml +++ b/core/modules/content_moderation/content_moderation.services.yml @@ -20,3 +20,8 @@ services: arguments: ['@config.manager', '@entity_type.manager'] tags: - { name: event_subscriber } + content_moderation.route_subscriber: + class: Drupal\content_moderation\Routing\ContentModerationRouteSubscriber + arguments: ['@entity_type.manager'] + tags: + - { name: event_subscriber } diff --git a/core/modules/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php index e54fbbe..abb92eb 100644 --- a/core/modules/content_moderation/src/Entity/ContentModerationState.php +++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php @@ -31,6 +31,7 @@ * data_table = "content_moderation_state_field_data", * revision_data_table = "content_moderation_state_field_revision", * translatable = TRUE, + * internal = TRUE, * entity_keys = { * "id" = "id", * "revision" = "revision_id", @@ -221,4 +222,16 @@ protected function realSave() { return parent::save(); } + /** + * {@inheritdoc} + */ + protected function getFieldsToSkipFromTranslationChangesCheck() { + $field_names = parent::getFieldsToSkipFromTranslationChangesCheck(); + // We need to skip the parent entity revision ID, since that will always + // change on every save, otherwise every translation would be marked as + // affected regardless of actual changes. + $field_names[] = 'content_entity_revision_id'; + return $field_names; + } + } diff --git a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php index f2c6917..c44ab09 100644 --- a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php +++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php @@ -35,11 +35,6 @@ public function onPresave(ContentEntityInterface $entity, $default_revision, $pu // This is probably not necessary if configuration is setup correctly. $entity->setNewRevision(TRUE); $entity->isDefaultRevision($default_revision); - if ($entity->hasField('revision_translation_affected')) { - // @todo remove this when revision and translation issues have been - // resolved. https://www.drupal.org/node/2860097 - $entity->set('revision_translation_affected', TRUE); - } // Update publishing status if it can be updated and if it needs updating. if (($entity instanceof EntityPublishedInterface) && $entity->isPublished() !== $published_state) { diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php index a7fcf7e..e268860 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -11,6 +11,8 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\content_moderation\Form\EntityModerationForm; +use Drupal\Core\Routing\RouteBuilderInterface; +use Drupal\workflows\Entity\Workflow; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -49,6 +51,13 @@ class EntityOperations implements ContainerInjectionInterface { protected $bundleInfo; /** + * The router builder service. + * + * @var \Drupal\Core\Routing\RouteBuilderInterface + */ + protected $routerBuilder; + + /** * Constructs a new EntityOperations object. * * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info @@ -59,12 +68,15 @@ class EntityOperations implements ContainerInjectionInterface { * The form builder. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info * The entity bundle information service. + * @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder + * The router builder service. */ - public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info) { + public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info, RouteBuilderInterface $router_builder) { $this->moderationInfo = $moderation_info; $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; $this->bundleInfo = $bundle_info; + $this->routerBuilder = $router_builder; } /** @@ -75,7 +87,8 @@ public static function create(ContainerInterface $container) { $container->get('content_moderation.moderation_information'), $container->get('entity_type.manager'), $container->get('form_builder'), - $container->get('entity_type.bundle.info') + $container->get('entity_type.bundle.info'), + $container->get('router.builder') ); } @@ -98,10 +111,9 @@ public function entityPresave(EntityInterface $entity) { $current_state = $workflow->getTypePlugin() ->getState($entity->moderation_state->value); - // This entity is default if it is new, a new translation, the default - // revision, or the default revision is not published. + // This entity is default if it is new, the default revision, or the + // default revision is not published. $update_default_revision = $entity->isNew() - || $entity->isNewTranslation() || $current_state->isDefaultRevisionState() || !$this->moderationInfo->isDefaultRevisionPublished($entity); @@ -134,6 +146,12 @@ public function entityUpdate(EntityInterface $entity) { if ($this->moderationInfo->isModeratedEntity($entity)) { $this->updateOrCreateFromEntity($entity); } + // When updating workflow settings for Content Moderation, we need to + // rebuild routes as we may be enabling new entity types and the related + // entity forms. + elseif ($entity instanceof Workflow && $entity->getTypePlugin()->getPluginId() == 'content_moderation') { + $this->routerBuilder->setRebuildNeeded(); + } } /** @@ -147,9 +165,10 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { $entity_revision_id = $entity->getRevisionId(); $workflow = $this->moderationInfo->getWorkflowForEntity($entity); $content_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($entity); + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage('content_moderation_state'); if (!($content_moderation_state instanceof ContentModerationStateInterface)) { - $storage = $this->entityTypeManager->getStorage('content_moderation_state'); $content_moderation_state = $storage->create([ 'content_entity_type_id' => $entity->getEntityTypeId(), 'content_entity_id' => $entity->id(), @@ -159,11 +178,6 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { ]); $content_moderation_state->workflow->target_id = $workflow->id(); } - elseif ($content_moderation_state->content_entity_revision_id->value != $entity_revision_id) { - // If a new revision of the content has been created, add a new content - // moderation state revision. - $content_moderation_state->setNewRevision(TRUE); - } // Sync translations. if ($entity->getEntityType()->hasKey('langcode')) { @@ -176,6 +190,12 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { } } + // If a new revision of the content has been created, add a new content + // moderation state revision. + if (!$content_moderation_state->isNew() && $content_moderation_state->content_entity_revision_id->value != $entity_revision_id) { + $content_moderation_state = $storage->createRevision($content_moderation_state, $entity->isDefaultRevision()); + } + // Create the ContentModerationState entity for the inserted entity. $moderation_state = $entity->moderation_state->value; /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ @@ -245,27 +265,31 @@ public function entityTranslationDelete(EntityInterface $translation) { * @see EntityFieldManagerInterface::getExtraFields() */ public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ if (!$this->moderationInfo->isModeratedEntity($entity)) { return; } - if (!$this->moderationInfo->isLatestRevision($entity)) { + if (isset($entity->in_preview) && $entity->in_preview) { return; } - if ($this->moderationInfo->isLiveRevision($entity)) { + // If the component is not defined for this display, we have nothing to do. + if (!$display->getComponent('content_moderation_control')) { return; } - // Don't display the moderation form when when: - // - The revision is not translation affected. - // - There are more than one translation languages. - // - The entity has pending revisions. - if (!$this->moderationInfo->isPendingRevisionAllowed($entity)) { + // The moderation form should be displayed only when viewing the latest + // (translation-affecting) revision, unless it was created as published + // default revision. + if (!$entity->isLatestRevision() && !$entity->isLatestTranslationAffectedRevision()) { return; } - - $component = $display->getComponent('content_moderation_control'); - if ($component) { - $build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity); + if (($entity->isDefaultRevision() || $entity->wasDefaultRevision()) && ($moderation_state = $entity->get('moderation_state')->value)) { + $workflow = $this->moderationInfo->getWorkflowForEntity($entity); + if ($workflow->getTypePlugin()->getState($moderation_state)->isPublishedState()) { + return; + } } + + $build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity); } } diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index 666ee26..c03cf49 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -7,10 +7,12 @@ use Drupal\Core\Entity\ContentEntityFormInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -269,6 +271,40 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { } /** + * Replaces the entity form entity object with a proper revision object. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being edited. + * @param string $operation + * The entity form operation. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @see hook_entity_prepare_form() + */ + public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ + $form_object = $form_state->getFormObject(); + + if ($this->isModeratedEntityEditForm($form_object) && !$entity->isNew()) { + // Generate a proper revision object for the current entity. This allows + // to correctly handle translatable entities having pending revisions. + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + /** @var \Drupal\Core\Entity\ContentEntityInterface $new_revision */ + $new_revision = $storage->createRevision($entity, FALSE); + + // Restore the revision ID as other modules may expect to find it still + // populated. This will reset the "new revision" flag, however the entity + // object will be marked as a new revision again on submit. + // @see \Drupal\Core\Entity\ContentEntityForm::buildEntity() + $revision_key = $new_revision->getEntityType()->getKey('revision'); + $new_revision->set($revision_key, $new_revision->getLoadedRevisionId()); + $form_object->setEntity($new_revision); + } + } + + /** * Alters bundle forms to enforce revision handling. * * @param array $form @@ -291,57 +327,15 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id $this->entityTypeManager->getHandler($config_entity_type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id); } } - elseif ($form_object instanceof ContentEntityFormInterface && in_array($form_object->getOperation(), ['edit', 'default'])) { + elseif ($this->isModeratedEntityEditForm($form_object)) { + /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */ + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $form_object->getEntity(); if ($this->moderationInfo->isModeratedEntity($entity)) { $this->entityTypeManager ->getHandler($entity->getEntityTypeId(), 'moderation') ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id); - if (!$this->moderationInfo->isPendingRevisionAllowed($entity)) { - $latest_revision = $this->moderationInfo->getLatestRevision($entity->getEntityTypeId(), $entity->id()); - if ($entity->bundle()) { - $bundle_type_id = $entity->getEntityType()->getBundleEntityType(); - $bundle = $this->entityTypeManager->getStorage($bundle_type_id)->load($entity->bundle()); - $type_label = $bundle->label(); - } - else { - $type_label = $entity->getEntityType()->getLabel(); - } - - $translation = $this->moderationInfo->getAffectedRevisionTranslation($latest_revision); - $args = [ - '@type_label' => $type_label, - '@latest_revision_edit_url' => $translation->toUrl('edit-form', ['language' => $translation->language()])->toString(), - '@latest_revision_delete_url' => $translation->toUrl('delete-form', ['language' => $translation->language()])->toString(), - ]; - $label = $this->t('Unable to save this @type_label.', $args); - $message = $this->t('Publish or delete the latest revision to allow all workflow transitions.', $args); - $full_message = $this->t('Unable to save this @type_label. Publish or delete the latest revision to allow all workflow transitions.', $args); - drupal_set_message($full_message, 'error'); - - $form['moderation_state']['#access'] = FALSE; - $form['actions']['#access'] = FALSE; - $form['invalid_transitions'] = [ - 'label' => [ - '#type' => 'item', - '#prefix' => '', - '#markup' => $label, - '#suffix' => '', - ], - 'message' => [ - '#type' => 'item', - '#markup' => $message, - ], - '#weight' => 999, - '#no_valid_transitions' => TRUE, - ]; - - if ($form['footer']) { - $form['invalid_transitions']['#group'] = 'footer'; - } - } - // Submit handler to redirect to the latest version, if available. $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect']; @@ -351,16 +345,31 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id $form['moderation_state']['#group'] = 'footer'; } - // Duplicate the label of the current moderation state to the meta - // region, if available. + // If the publishing status exists in the meta region, replace it with + // the current state instead. if (isset($form['meta']['published'])) { - $form['meta']['published']['#markup'] = $form['moderation_state']['widget'][0]['current']['#markup']; + $form['meta']['published']['#markup'] = $this->moderationInfo->getWorkflowForEntity($entity)->getTypePlugin()->getState($entity->moderation_state->value)->label(); } } } } /** + * Checks whether the specified form allows to edit a moderated entity. + * + * @param \Drupal\Core\Form\FormInterface $form_object + * The form object. + * + * @return bool + * TRUE if the form should get form moderation, FALSE otherwise. + */ + protected function isModeratedEntityEditForm(FormInterface $form_object) { + return $form_object instanceof ContentEntityFormInterface && + in_array($form_object->getOperation(), ['edit', 'default'], TRUE) && + $this->moderationInfo->isModeratedEntity($form_object->getEntity()); + } + + /** * Redirect content entity edit forms on save, if there is a pending revision. * * When saving their changes, editors should see those changes displayed on diff --git a/core/modules/content_moderation/src/Form/EntityModerationForm.php b/core/modules/content_moderation/src/Form/EntityModerationForm.php index 507ef54..98f6fdf 100644 --- a/core/modules/content_moderation/src/Form/EntityModerationForm.php +++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php @@ -138,6 +138,9 @@ public function buildForm(array $form, FormStateInterface $form_state, ContentEn public function submitForm(array &$form, FormStateInterface $form_state) { /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $form_state->get('entity'); + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); + $entity = $storage->createRevision($entity, $entity->isDefaultRevision()); $new_state = $form_state->getValue('new_state'); diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php index 7e3e513..f42ec33 100644 --- a/core/modules/content_moderation/src/ModerationInformation.php +++ b/core/modules/content_moderation/src/ModerationInformation.php @@ -130,13 +130,6 @@ public function getAffectedRevisionTranslation(ContentEntityInterface $entity) { /** * {@inheritdoc} */ - public function isPendingRevisionAllowed(ContentEntityInterface $entity) { - return !(!$entity->isRevisionTranslationAffected() && count($entity->getTranslationLanguages()) > 1 && $this->hasPendingRevision($entity)); - } - - /** - * {@inheritdoc} - */ public function isLatestRevision(ContentEntityInterface $entity) { return $entity->getRevisionId() == $this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id()); } @@ -145,8 +138,20 @@ public function isLatestRevision(ContentEntityInterface $entity) { * {@inheritdoc} */ public function hasPendingRevision(ContentEntityInterface $entity) { - return $this->isModeratedEntity($entity) - && !($this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id()) == $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id())); + $result = FALSE; + if ($this->isModeratedEntity($entity)) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId()); + $default_revision_id = $entity->isDefaultRevision() && !$entity->isNewRevision() && ($revision_id = $entity->getRevisionId()) ? + $revision_id : $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id()); + if ($latest_revision_id != $default_revision_id) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */ + $latest_revision = $storage->loadRevision($latest_revision_id); + $result = !$latest_revision->wasDefaultRevision(); + } + } + return $result; } /** @@ -172,9 +177,15 @@ public function isDefaultRevisionPublished(ContentEntityInterface $entity) { // Loop through each language that has a translation. foreach ($default_revision->getTranslationLanguages() as $language) { // Load the translated revision. - $language_revision = $default_revision->getTranslation($language->getId()); + $translation = $default_revision->getTranslation($language->getId()); + // If the moderation state is empty, it was not stored yet so no point + // in doing further work. + $moderation_state = $translation->moderation_state->value; + if (!$moderation_state) { + continue; + } // Return TRUE if a translation with a published state is found. - if ($workflow->getTypePlugin()->getState($language_revision->moderation_state->value)->isPublishedState()) { + if ($workflow->getTypePlugin()->getState($moderation_state)->isPublishedState()) { return TRUE; } } diff --git a/core/modules/content_moderation/src/ModerationInformationInterface.php b/core/modules/content_moderation/src/ModerationInformationInterface.php index 1dafb3f..739c16b 100644 --- a/core/modules/content_moderation/src/ModerationInformationInterface.php +++ b/core/modules/content_moderation/src/ModerationInformationInterface.php @@ -101,19 +101,6 @@ public function getDefaultRevisionId($entity_type_id, $entity_id); public function getAffectedRevisionTranslation(ContentEntityInterface $entity); /** - * Determines if pending revisions are allowed. - * - * @internal - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The content entity. - * - * @return bool - * If pending revisions are allowed. - */ - public function isPendingRevisionAllowed(ContentEntityInterface $entity); - - /** * Determines if an entity is a latest revision. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php index b526363..1256bf2 100644 --- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Field\FieldItemList; +use Drupal\Core\TypedData\ComputedItemListTrait; /** * A computed field that provides a content entity's moderation state. @@ -14,6 +15,38 @@ */ class ModerationStateFieldItemList extends FieldItemList { + use ComputedItemListTrait { + ensureComputedValue as traitEnsureComputedValue; + get as traitGet; + } + + /** + * {@inheritdoc} + */ + protected function computeValue() { + $moderation_state = $this->getModerationStateId(); + // Do not store NULL values, in the case where an entity does not have a + // moderation workflow associated with it, we do not create list items for + // the computed field. + if ($moderation_state) { + // An entity can only have a single moderation state. + $this->list[0] = $this->createItem(0, $moderation_state); + } + } + + /** + * {@inheritdoc} + */ + protected function ensureComputedValue() { + // If the moderation state field is set to an empty value, always recompute + // the state. Empty is not a valid moderation state value, when none is + // present the default state is used. + if (!isset($this->list[0]) || $this->list[0]->isEmpty()) { + $this->valueComputed = FALSE; + } + $this->traitEnsureComputedValue(); + } + /** * Gets the moderation state ID linked to a content entity revision. * @@ -90,32 +123,7 @@ public function get($index) { if ($index !== 0) { throw new \InvalidArgumentException('An entity can not have multiple moderation states at the same time.'); } - $this->computeModerationFieldItemList(); - return isset($this->list[$index]) ? $this->list[$index] : NULL; - } - - /** - * {@inheritdoc} - */ - public function getIterator() { - $this->computeModerationFieldItemList(); - return parent::getIterator(); - } - - /** - * Recalculate the moderation field item list. - */ - protected function computeModerationFieldItemList() { - // Compute the value of the moderation state. - $index = 0; - if (!isset($this->list[$index]) || $this->list[$index]->isEmpty()) { - - $moderation_state = $this->getModerationStateId(); - // Do not store NULL values in the static cache. - if ($moderation_state) { - $this->list[$index] = $this->createItem($index, $moderation_state); - } - } + return $this->traitGet($index); } /** @@ -134,6 +142,7 @@ public function setValue($values, $notify = TRUE) { parent::setValue($values, $notify); if (isset($this->list[0])) { + $this->valueComputed = TRUE; $this->updateModeratedEntity($this->list[0]->value); } } @@ -157,10 +166,9 @@ protected function updateModeratedEntity($moderation_state_id) { /** @var \Drupal\content_moderation\ContentModerationState $current_state */ $current_state = $workflow->getTypePlugin()->getState($moderation_state_id); - // This entity is default if it is new, a new translation, the default - // revision state, or the default revision is not published. + // This entity is default if it is new, the default revision state, or the + // default revision is not published. $update_default_revision = $entity->isNew() - || $entity->isNewTranslation() || $current_state->isDefaultRevisionState() || !$content_moderation_info->isDefaultRevisionPublished($entity); diff --git a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php index 035ad3e..c658ef8 100644 --- a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php @@ -176,8 +176,8 @@ protected function opSimple() { $entity_base_table_alias = $this->table; // The bundle field of an entity type is not revisionable so we need to - // join the data table. - $entity_base_table = $entity_type->isTranslatable() ? $entity_type->getDataTable() : $entity_type->getBaseTable(); + // join the base table. + $entity_base_table = $entity_type->getBaseTable(); $entity_revision_base_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable(); if ($this->table === $entity_revision_base_table) { $configuration = [ @@ -187,12 +187,6 @@ protected function opSimple() { 'left_field' => $entity_type->getKey('id'), 'type' => 'INNER', ]; - if ($entity_type->isTranslatable()) { - $configuration['extra'][] = [ - 'field' => $entity_type->getKey('langcode'), - 'left_field' => $entity_type->getKey('langcode'), - ]; - } $join = Views::pluginManager('join')->createInstance('standard', $configuration); $entity_base_table_alias = $this->query->addRelationship($entity_base_table, $join, $entity_revision_base_table); diff --git a/core/modules/content_moderation/src/Routing/ContentModerationRouteSubscriber.php b/core/modules/content_moderation/src/Routing/ContentModerationRouteSubscriber.php new file mode 100644 index 0000000..4cfdbbe --- /dev/null +++ b/core/modules/content_moderation/src/Routing/ContentModerationRouteSubscriber.php @@ -0,0 +1,113 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + protected function alterRoutes(RouteCollection $collection) { + foreach ($collection as $route) { + $this->setLatestRevisionFlag($route); + } + } + + /** + * Ensure revisionable entities load the latest revision on entity forms. + * + * @param \Symfony\Component\Routing\Route $route + * The route object. + */ + protected function setLatestRevisionFlag(Route $route) { + if (!$entity_form = $route->getDefault('_entity_form')) { + return; + } + // Only set the flag on entity types which are revisionable. + list($entity_type) = explode('.', $entity_form, 2); + if (!isset($this->getModeratedEntityTypes()[$entity_type]) || !$this->getModeratedEntityTypes()[$entity_type]->isRevisionable()) { + return; + } + $parameters = $route->getOption('parameters') ?: []; + foreach ($parameters as &$parameter) { + if ($parameter['type'] === 'entity:' . $entity_type && !isset($parameter['load_latest_revision'])) { + $parameter['load_latest_revision'] = TRUE; + } + } + $route->setOption('parameters', $parameters); + } + + /** + * Returns the moderated entity types. + * + * @return \Drupal\Core\Entity\ContentEntityTypeInterface[] + * An associative array of moderated entity types keyed by ID. + */ + protected function getModeratedEntityTypes() { + if (!isset($this->moderatedEntityTypes)) { + $entity_types = $this->entityTypeManager->getDefinitions(); + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { + /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */ + $plugin = $workflow->getTypePlugin(); + foreach ($plugin->getEntityTypes() as $entity_type_id) { + $this->moderatedEntityTypes[$entity_type_id] = $entity_types[$entity_type_id]; + } + } + } + return $this->moderatedEntityTypes; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = parent::getSubscribedEvents(); + // This needs to run after that EntityResolverManager has set the route + // entity type. + $events[RoutingEvents::ALTER] = ['onAlterRoutes', -200]; + return $events; + } + +} diff --git a/core/modules/content_moderation/tests/fixtures/update/drupal-8.4.0-content_moderation_installed.php b/core/modules/content_moderation/tests/fixtures/update/drupal-8.4.0-content_moderation_installed.php new file mode 100644 index 0000000..0784013 --- /dev/null +++ b/core/modules/content_moderation/tests/fixtures/update/drupal-8.4.0-content_moderation_installed.php @@ -0,0 +1,665 @@ +delete('config') + ->condition('name', ['core.extension'], 'IN') + ->execute(); + +$connection->insert('config') + ->fields(array( + 'collection', + 'name', + 'data', + )) + ->values(array( + 'collection' => '', + 'name' => 'core.extension', + 'data' => 'a:4:{s:6:"module";a:44:{s:14:"automated_cron";i:0;s:5:"block";i:0;s:13:"block_content";i:0;s:10:"breakpoint";i:0;s:8:"ckeditor";i:0;s:5:"color";i:0;s:7:"comment";i:0;s:6:"config";i:0;s:7:"contact";i:0;s:18:"content_moderation";i:0;s:10:"contextual";i:0;s:8:"datetime";i:0;s:5:"dblog";i:0;s:18:"dynamic_page_cache";i:0;s:6:"editor";i:0;s:5:"field";i:0;s:8:"field_ui";i:0;s:4:"file";i:0;s:6:"filter";i:0;s:4:"help";i:0;s:7:"history";i:0;s:5:"image";i:0;s:4:"link";i:0;s:7:"menu_ui";i:0;s:4:"node";i:0;s:7:"options";i:0;s:10:"page_cache";i:0;s:4:"path";i:0;s:9:"quickedit";i:0;s:3:"rdf";i:0;s:6:"search";i:0;s:8:"shortcut";i:0;s:6:"system";i:0;s:8:"taxonomy";i:0;s:4:"text";i:0;s:7:"toolbar";i:0;s:4:"tour";i:0;s:6:"update";i:0;s:4:"user";i:0;s:8:"views_ui";i:0;s:9:"workflows";i:0;s:17:"menu_link_content";i:1;s:5:"views";i:10;s:8:"standard";i:1000;}s:5:"theme";a:4:{s:6:"stable";i:0;s:6:"classy";i:0;s:6:"bartik";i:0;s:5:"seven";i:0;}s:7:"profile";s:8:"standard";s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"R4IF-ClDHXxblLcG0L7MgsLvfBIMAvi_skumNFQwkDc";}}', + )) + ->values(array( + 'collection' => '', + 'name' => 'workflows.workflow.editorial', + 'data' => 'a:9:{s:4:"uuid";s:36:"08b548c7-ff59-468b-9347-7d697680d035";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"module";a:1:{i:0;s:18:"content_moderation";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"T_JxNjYlfoRBi7Bj1zs5Xv9xv1btuBkKp5C1tNrjMhI";}s:2:"id";s:9:"editorial";s:5:"label";s:9:"Editorial";s:4:"type";s:18:"content_moderation";s:13:"type_settings";a:3:{s:6:"states";a:3:{s:8:"archived";a:4:{s:5:"label";s:8:"Archived";s:6:"weight";i:5;s:9:"published";b:0;s:16:"default_revision";b:1;}s:5:"draft";a:4:{s:5:"label";s:5:"Draft";s:9:"published";b:0;s:16:"default_revision";b:0;s:6:"weight";i:-5;}s:9:"published";a:4:{s:5:"label";s:9:"Published";s:9:"published";b:1;s:16:"default_revision";b:1;s:6:"weight";i:0;}}s:11:"transitions";a:5:{s:7:"archive";a:4:{s:5:"label";s:7:"Archive";s:4:"from";a:1:{i:0;s:9:"published";}s:2:"to";s:8:"archived";s:6:"weight";i:2;}s:14:"archived_draft";a:4:{s:5:"label";s:16:"Restore to Draft";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:5:"draft";s:6:"weight";i:3;}s:18:"archived_published";a:4:{s:5:"label";s:7:"Restore";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:9:"published";s:6:"weight";i:4;}s:16:"create_new_draft";a:4:{s:5:"label";s:16:"Create New Draft";s:2:"to";s:5:"draft";s:6:"weight";i:0;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}s:7:"publish";a:4:{s:5:"label";s:7:"Publish";s:2:"to";s:9:"published";s:6:"weight";i:1;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}}s:12:"entity_types";a:0:{}}}', + )) + ->execute(); + +$connection->schema()->createTable('content_moderation_state', array( + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'uuid' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '128', + ), + 'langcode' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '12', + ), + ), + 'primary key' => array( + 'id', + ), + 'unique keys' => array( + 'content_moderation_state_field__uuid__value' => array( + 'uuid', + ), + 'content_moderation_state__revision_id' => array( + 'revision_id', + ), + ), + 'mysql_character_set' => 'utf8mb4', +)); + +$connection->schema()->createTable('content_moderation_state_field_data', array( + 'fields' => array( + 'id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'langcode' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '12', + ), + 'uid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'workflow' => array( + 'type' => 'varchar_ascii', + 'not null' => FALSE, + 'length' => '255', + ), + 'moderation_state' => array( + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => '255', + ), + 'content_entity_type_id' => array( + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => '32', + ), + 'content_entity_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + ), + 'content_entity_revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + ), + 'default_langcode' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + ), + 'revision_translation_affected' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'tiny', + ), + ), + 'primary key' => array( + 'id', + 'langcode', + ), + 'unique keys' => array( + 'content_moderation_state__lookup' => array( + 'content_entity_type_id', + 'content_entity_id', + 'content_entity_revision_id', + 'workflow', + 'langcode', + ), + ), + 'indexes' => array( + 'content_moderation_state__id__default_langcode__langcode' => array( + 'id', + 'default_langcode', + 'langcode', + ), + 'content_moderation_state__revision_id' => array( + 'revision_id', + ), + 'content_moderation_state_field__uid__target_id' => array( + 'uid', + ), + 'content_moderation_state__09628d8dbc' => array( + 'workflow', + ), + ), + 'mysql_character_set' => 'utf8mb4', +)); + +$connection->schema()->createTable('content_moderation_state_field_revision', array( + 'fields' => array( + 'id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'langcode' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '12', + ), + 'uid' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'workflow' => array( + 'type' => 'varchar_ascii', + 'not null' => FALSE, + 'length' => '255', + ), + 'moderation_state' => array( + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => '255', + ), + 'content_entity_type_id' => array( + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => '32', + ), + 'content_entity_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + ), + 'content_entity_revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + ), + 'default_langcode' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + ), + 'revision_translation_affected' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'tiny', + ), + ), + 'primary key' => array( + 'revision_id', + 'langcode', + ), + 'unique keys' => array( + 'content_moderation_state__lookup' => array( + 'content_entity_type_id', + 'content_entity_id', + 'content_entity_revision_id', + 'workflow', + 'langcode', + ), + ), + 'indexes' => array( + 'content_moderation_state__id__default_langcode__langcode' => array( + 'id', + 'default_langcode', + 'langcode', + ), + 'content_moderation_state_field__uid__target_id' => array( + 'uid', + ), + 'content_moderation_state__09628d8dbc' => array( + 'workflow', + ), + ), + 'mysql_character_set' => 'utf8mb4', +)); + +$connection->schema()->createTable('content_moderation_state_revision', array( + 'fields' => array( + 'id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'langcode' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '12', + ), + ), + 'primary key' => array( + 'revision_id', + ), + 'indexes' => array( + 'content_moderation_state__id' => array( + 'id', + ), + ), + 'mysql_character_set' => 'utf8mb4', +)); + +$connection->delete('key_value') + ->condition('name', [ + 'routing.non_admin_routes', + 'system.js_cache_files', + 'system.theme.files', + ], 'IN') + ->execute(); + +$connection->insert('key_value') + ->fields(array( + 'collection', + 'name', + 'value', + )) + ->values(array( + 'collection' => 'config.entity.key_store.workflow', + 'name' => 'uuid:08b548c7-ff59-468b-9347-7d697680d035', + 'value' => 'a:1:{i:0;s:28:"workflows.workflow.editorial";}', + )) + ->values(array( + 'collection' => 'entity.definitions.installed', + 'name' => 'content_moderation_state.entity_type', + 'value' => 'O:36:"Drupal\Core\Entity\ContentEntityType":38:{s:25:"*revision_metadata_keys";a:0:{}s:15:"*static_cache";b:1;s:15:"*render_cache";b:1;s:19:"*persistent_cache";b:1;s:14:"*entity_keys";a:8:{s:2:"id";s:2:"id";s:8:"revision";s:11:"revision_id";s:4:"uuid";s:4:"uuid";s:3:"uid";s:3:"uid";s:8:"langcode";s:8:"langcode";s:6:"bundle";s:0:"";s:16:"default_langcode";s:16:"default_langcode";s:29:"revision_translation_affected";s:29:"revision_translation_affected";}s:5:"*id";s:24:"content_moderation_state";s:16:"*originalClass";s:55:"Drupal\content_moderation\Entity\ContentModerationState";s:11:"*handlers";a:5:{s:14:"storage_schema";s:61:"Drupal\content_moderation\ContentModerationStateStorageSchema";s:10:"views_data";s:29:"\Drupal\views\EntityViewsData";s:6:"access";s:68:"Drupal\content_moderation\ContentModerationStateAccessControlHandler";s:7:"storage";s:46:"Drupal\Core\Entity\Sql\SqlContentEntityStorage";s:12:"view_builder";s:36:"Drupal\Core\Entity\EntityViewBuilder";}s:19:"*admin_permission";N;s:25:"*permission_granularity";s:11:"entity_type";s:8:"*links";a:0:{}s:17:"*label_callback";N;s:21:"*bundle_entity_type";N;s:12:"*bundle_of";N;s:15:"*bundle_label";N;s:13:"*base_table";s:24:"content_moderation_state";s:22:"*revision_data_table";s:39:"content_moderation_state_field_revision";s:17:"*revision_table";s:33:"content_moderation_state_revision";s:13:"*data_table";s:35:"content_moderation_state_field_data";s:15:"*translatable";b:1;s:19:"*show_revision_ui";b:0;s:8:"*label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:24:"Content moderation state";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:19:"*label_collection";s:0:"";s:17:"*label_singular";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:24:"content moderation state";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:15:"*label_plural";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:25:"content moderation states";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:14:"*label_count";a:3:{s:8:"singular";s:31:"@count content moderation state";s:6:"plural";s:32:"@count content moderation states";s:7:"context";N;}s:15:"*uri_callback";N;s:8:"*group";s:7:"content";s:14:"*group_label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:7:"Content";s:12:"*arguments";a:0:{}s:10:"*options";a:1:{s:7:"context";s:17:"Entity type group";}}s:22:"*field_ui_base_route";N;s:26:"*common_reference_target";b:0;s:22:"*list_cache_contexts";a:0:{}s:18:"*list_cache_tags";a:1:{i:0;s:29:"content_moderation_state_list";}s:14:"*constraints";a:0:{}s:13:"*additional";a:0:{}s:8:"*class";s:55:"Drupal\content_moderation\Entity\ContentModerationState";s:11:"*provider";s:18:"content_moderation";s:20:"*stringTranslation";N;}', + )) + ->values(array( + 'collection' => 'entity.definitions.installed', + 'name' => 'content_moderation_state.field_storage_definitions', + 'value' => 'a:12:{s:2:"id";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:7:"integer";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:3:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:2;s:13:"*definition";a:2:{s:4:"type";s:18:"field_item:integer";s:8:"settings";a:6:{s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:3:"min";s:0:"";s:3:"max";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";}}}s:13:"*definition";a:6:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:2:"ID";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:9:"read-only";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:2:"id";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:4:"uuid";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:4:"uuid";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:3:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:6:"binary";b:0;}}s:11:"unique keys";a:1:{s:5:"value";a:1:{i:0;s:5:"value";}}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:35;s:13:"*definition";a:2:{s:4:"type";s:15:"field_item:uuid";s:8:"settings";a:3:{s:10:"max_length";i:128;s:8:"is_ascii";b:1;s:14:"case_sensitive";b:0;}}}s:13:"*definition";a:6:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:4:"UUID";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:9:"read-only";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:4:"uuid";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:11:"revision_id";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:7:"integer";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:3:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:67;s:13:"*definition";a:2:{s:4:"type";s:18:"field_item:integer";s:8:"settings";a:6:{s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:3:"min";s:0:"";s:3:"max";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";}}}s:13:"*definition";a:6:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:11:"Revision ID";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:9:"read-only";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:11:"revision_id";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:8:"langcode";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:8:"language";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:2:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:12;}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:100;s:13:"*definition";a:2:{s:4:"type";s:19:"field_item:language";s:8:"settings";a:0:{}}}s:13:"*definition";a:8:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:8:"Language";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:7:"display";a:2:{s:4:"view";a:1:{s:7:"options";a:1:{s:6:"region";s:6:"hidden";}}s:4:"form";a:1:{s:7:"options";a:2:{s:4:"type";s:15:"language_select";s:6:"weight";i:2;}}}s:12:"revisionable";b:1;s:12:"translatable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:8:"langcode";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:3:"uid";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:16:"entity_reference";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:9:"target_id";a:3:{s:11:"description";s:28:"The ID of the target entity.";s:4:"type";s:3:"int";s:8:"unsigned";b:1;}}s:7:"indexes";a:1:{s:9:"target_id";a:1:{i:0;s:9:"target_id";}}s:11:"unique keys";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:135;s:13:"*definition";a:2:{s:4:"type";s:27:"field_item:entity_reference";s:8:"settings";a:3:{s:11:"target_type";s:4:"user";s:7:"handler";s:7:"default";s:16:"handler_settings";a:0:{}}}}s:13:"*definition";a:9:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:4:"User";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:35:"The username of the entity creator.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:22:"default_value_callback";s:73:"Drupal\content_moderation\Entity\ContentModerationState::getCurrentUserId";s:12:"translatable";b:1;s:12:"revisionable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:3:"uid";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:8:"workflow";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:16:"entity_reference";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:9:"target_id";a:3:{s:11:"description";s:28:"The ID of the target entity.";s:4:"type";s:13:"varchar_ascii";s:6:"length";i:255;}}s:7:"indexes";a:1:{s:9:"target_id";a:1:{i:0;s:9:"target_id";}}s:11:"unique keys";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:173;s:13:"*definition";a:2:{s:4:"type";s:27:"field_item:entity_reference";s:8:"settings";a:3:{s:11:"target_type";s:8:"workflow";s:7:"handler";s:7:"default";s:16:"handler_settings";a:0:{}}}}s:13:"*definition";a:8:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:8:"Workflow";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:40:"The workflow the moderation state is in.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:8:"required";b:1;s:12:"revisionable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:8:"workflow";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:16:"moderation_state";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:6:"string";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:3:{s:4:"type";s:7:"varchar";s:6:"length";i:255;s:6:"binary";b:0;}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:210;s:13:"*definition";a:2:{s:4:"type";s:17:"field_item:string";s:8:"settings";a:3:{s:10:"max_length";i:255;s:8:"is_ascii";b:0;s:14:"case_sensitive";b:0;}}}s:13:"*definition";a:9:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:16:"Moderation state";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:47:"The moderation state of the referenced content.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:8:"required";b:1;s:12:"translatable";b:1;s:12:"revisionable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:16:"moderation_state";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:22:"content_entity_type_id";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:6:"string";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:3:{s:4:"type";s:7:"varchar";s:6:"length";i:32;s:6:"binary";b:0;}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:246;s:13:"*definition";a:2:{s:4:"type";s:17:"field_item:string";s:8:"settings";a:3:{s:10:"max_length";i:32;s:8:"is_ascii";b:0;s:14:"case_sensitive";b:0;}}}s:13:"*definition";a:8:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:22:"Content entity type ID";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:63:"The ID of the content entity type this moderation state is for.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:8:"required";b:1;s:12:"revisionable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:22:"content_entity_type_id";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:17:"content_entity_id";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:7:"integer";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:3:{s:4:"type";s:3:"int";s:8:"unsigned";b:0;s:4:"size";s:6:"normal";}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:281;s:13:"*definition";a:2:{s:4:"type";s:18:"field_item:integer";s:8:"settings";a:6:{s:8:"unsigned";b:0;s:4:"size";s:6:"normal";s:3:"min";s:0:"";s:3:"max";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";}}}s:13:"*definition";a:8:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:17:"Content entity ID";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:58:"The ID of the content entity this moderation state is for.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:8:"required";b:1;s:12:"revisionable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:17:"content_entity_id";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:26:"content_entity_revision_id";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:7:"integer";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:3:{s:4:"type";s:3:"int";s:8:"unsigned";b:0;s:4:"size";s:6:"normal";}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:319;s:13:"*definition";a:2:{s:4:"type";s:18:"field_item:integer";s:8:"settings";a:6:{s:8:"unsigned";b:0;s:4:"size";s:6:"normal";s:3:"min";s:0:"";s:3:"max";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";}}}s:13:"*definition";a:8:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:26:"Content entity revision ID";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:67:"The revision ID of the content entity this moderation state is for.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:8:"required";b:1;s:12:"revisionable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:26:"content_entity_revision_id";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:16:"default_langcode";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:7:"boolean";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:2:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:357;s:13:"*definition";a:2:{s:4:"type";s:18:"field_item:boolean";s:8:"settings";a:2:{s:8:"on_label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:2:"On";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:9:"off_label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:3:"Off";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}}}}s:13:"*definition";a:9:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:19:"Default translation";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:58:"A flag indicating whether this is the default translation.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:12:"translatable";b:1;s:12:"revisionable";b:1;s:13:"default_value";a:1:{i:0;a:1:{s:5:"value";b:1;}}s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:16:"default_langcode";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}s:29:"revision_translation_affected";O:37:"Drupal\Core\Field\BaseFieldDefinition":5:{s:7:"*type";s:7:"boolean";s:9:"*schema";a:4:{s:7:"columns";a:1:{s:5:"value";a:2:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";}}s:11:"unique keys";a:0:{}s:7:"indexes";a:0:{}s:12:"foreign keys";a:0:{}}s:10:"*indexes";a:0:{}s:17:"*itemDefinition";O:51:"Drupal\Core\Field\TypedData\FieldItemDataDefinition":2:{s:18:"*fieldDefinition";r:399;s:13:"*definition";a:2:{s:4:"type";s:18:"field_item:boolean";s:8:"settings";a:2:{s:8:"on_label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:2:"On";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:9:"off_label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:3:"Off";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}}}}s:13:"*definition";a:9:{s:5:"label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:29:"Revision translation affected";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:11:"description";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:72:"Indicates if the last edit of a translation belongs to current revision.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:9:"read-only";b:1;s:12:"revisionable";b:1;s:12:"translatable";b:1;s:8:"provider";s:18:"content_moderation";s:10:"field_name";s:29:"revision_translation_affected";s:11:"entity_type";s:24:"content_moderation_state";s:6:"bundle";N;}}}', + )) + ->values(array( + 'collection' => 'entity.definitions.installed', + 'name' => 'workflow.entity_type', + 'value' => 'O:42:"Drupal\Core\Config\Entity\ConfigEntityType":41:{s:16:"*config_prefix";s:8:"workflow";s:15:"*static_cache";b:0;s:14:"*lookup_keys";a:1:{i:0;s:4:"uuid";}s:16:"*config_export";a:4:{i:0;s:2:"id";i:1;s:5:"label";i:2;s:4:"type";i:3;s:13:"type_settings";}s:21:"*mergedConfigExport";a:0:{}s:15:"*render_cache";b:1;s:19:"*persistent_cache";b:1;s:14:"*entity_keys";a:8:{s:2:"id";s:2:"id";s:5:"label";s:5:"label";s:4:"uuid";s:4:"uuid";s:8:"revision";s:0:"";s:6:"bundle";s:0:"";s:8:"langcode";s:8:"langcode";s:16:"default_langcode";s:16:"default_langcode";s:29:"revision_translation_affected";s:29:"revision_translation_affected";}s:5:"*id";s:8:"workflow";s:16:"*originalClass";s:32:"Drupal\workflows\Entity\Workflow";s:11:"*handlers";a:5:{s:6:"access";s:45:"Drupal\workflows\WorkflowAccessControlHandler";s:12:"list_builder";s:36:"Drupal\workflows\WorkflowListBuilder";s:4:"form";a:9:{s:3:"add";s:37:"Drupal\workflows\Form\WorkflowAddForm";s:4:"edit";s:38:"Drupal\workflows\Form\WorkflowEditForm";s:6:"delete";s:40:"Drupal\workflows\Form\WorkflowDeleteForm";s:9:"add-state";s:42:"Drupal\workflows\Form\WorkflowStateAddForm";s:10:"edit-state";s:43:"Drupal\workflows\Form\WorkflowStateEditForm";s:12:"delete-state";s:45:"Drupal\workflows\Form\WorkflowStateDeleteForm";s:14:"add-transition";s:47:"Drupal\workflows\Form\WorkflowTransitionAddForm";s:15:"edit-transition";s:48:"Drupal\workflows\Form\WorkflowTransitionEditForm";s:17:"delete-transition";s:50:"Drupal\workflows\Form\WorkflowTransitionDeleteForm";}s:14:"route_provider";a:1:{s:4:"html";s:49:"Drupal\Core\Entity\Routing\AdminHtmlRouteProvider";}s:7:"storage";s:45:"Drupal\Core\Config\Entity\ConfigEntityStorage";}s:19:"*admin_permission";s:20:"administer workflows";s:25:"*permission_granularity";s:11:"entity_type";s:8:"*links";a:6:{s:8:"add-form";s:36:"/admin/config/workflow/workflows/add";s:9:"edit-form";s:50:"/admin/config/workflow/workflows/manage/{workflow}";s:11:"delete-form";s:57:"/admin/config/workflow/workflows/manage/{workflow}/delete";s:14:"add-state-form";s:60:"/admin/config/workflow/workflows/manage/{workflow}/add_state";s:19:"add-transition-form";s:65:"/admin/config/workflow/workflows/manage/{workflow}/add_transition";s:10:"collection";s:32:"/admin/config/workflow/workflows";}s:17:"*label_callback";N;s:21:"*bundle_entity_type";N;s:12:"*bundle_of";N;s:15:"*bundle_label";N;s:13:"*base_table";N;s:22:"*revision_data_table";N;s:17:"*revision_table";N;s:13:"*data_table";N;s:15:"*translatable";b:0;s:19:"*show_revision_ui";b:0;s:8:"*label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:8:"Workflow";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:19:"*label_collection";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:9:"Workflows";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}s:17:"*label_singular";s:0:"";s:15:"*label_plural";s:0:"";s:14:"*label_count";a:0:{}s:15:"*uri_callback";N;s:8:"*group";s:13:"configuration";s:14:"*group_label";O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:13:"Configuration";s:12:"*arguments";a:0:{}s:10:"*options";a:1:{s:7:"context";s:17:"Entity type group";}}s:22:"*field_ui_base_route";N;s:26:"*common_reference_target";b:0;s:22:"*list_cache_contexts";a:0:{}s:18:"*list_cache_tags";a:1:{i:0;s:20:"config:workflow_list";}s:14:"*constraints";a:0:{}s:13:"*additional";a:0:{}s:8:"*class";s:32:"Drupal\workflows\Entity\Workflow";s:11:"*provider";s:9:"workflows";s:20:"*stringTranslation";N;}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.entity_schema_data', + 'value' => 'a:4:{s:24:"content_moderation_state";a:2:{s:11:"primary key";a:1:{i:0;s:2:"id";}s:11:"unique keys";a:1:{s:37:"content_moderation_state__revision_id";a:1:{i:0;s:11:"revision_id";}}}s:33:"content_moderation_state_revision";a:2:{s:11:"primary key";a:1:{i:0;s:11:"revision_id";}s:7:"indexes";a:1:{s:28:"content_moderation_state__id";a:1:{i:0;s:2:"id";}}}s:35:"content_moderation_state_field_data";a:3:{s:11:"primary key";a:2:{i:0;s:2:"id";i:1;s:8:"langcode";}s:7:"indexes";a:2:{s:56:"content_moderation_state__id__default_langcode__langcode";a:3:{i:0;s:2:"id";i:1;s:16:"default_langcode";i:2;s:8:"langcode";}s:37:"content_moderation_state__revision_id";a:1:{i:0;s:11:"revision_id";}}s:11:"unique keys";a:1:{s:32:"content_moderation_state__lookup";a:5:{i:0;s:22:"content_entity_type_id";i:1;s:17:"content_entity_id";i:2;s:26:"content_entity_revision_id";i:3;s:8:"workflow";i:4;s:8:"langcode";}}}s:39:"content_moderation_state_field_revision";a:3:{s:11:"primary key";a:2:{i:0;s:11:"revision_id";i:1;s:8:"langcode";}s:7:"indexes";a:1:{s:56:"content_moderation_state__id__default_langcode__langcode";a:3:{i:0;s:2:"id";i:1;s:16:"default_langcode";i:2;s:8:"langcode";}}s:11:"unique keys";a:1:{s:32:"content_moderation_state__lookup";a:5:{i:0;s:22:"content_entity_type_id";i:1;s:17:"content_entity_id";i:2;s:26:"content_entity_revision_id";i:3;s:8:"workflow";i:4;s:8:"langcode";}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.content_entity_id', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:17:"content_entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:0;s:4:"size";s:6:"normal";s:8:"not null";b:0;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:17:"content_entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:0;s:4:"size";s:6:"normal";s:8:"not null";b:0;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.content_entity_revision_id', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:26:"content_entity_revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:0;s:4:"size";s:6:"normal";s:8:"not null";b:0;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:26:"content_entity_revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:0;s:4:"size";s:6:"normal";s:8:"not null";b:0;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.content_entity_type_id', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:22:"content_entity_type_id";a:4:{s:4:"type";s:7:"varchar";s:6:"length";i:32;s:6:"binary";b:0;s:8:"not null";b:0;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:22:"content_entity_type_id";a:4:{s:4:"type";s:7:"varchar";s:6:"length";i:32;s:6:"binary";b:0;s:8:"not null";b:0;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.default_langcode', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:16:"default_langcode";a:3:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:16:"default_langcode";a:3:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.id', + 'value' => 'a:4:{s:24:"content_moderation_state";a:1:{s:6:"fields";a:1:{s:2:"id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:1;}}}s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:2:"id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:1;}}}s:33:"content_moderation_state_revision";a:1:{s:6:"fields";a:1:{s:2:"id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:1;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:2:"id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:1;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.langcode', + 'value' => 'a:4:{s:24:"content_moderation_state";a:1:{s:6:"fields";a:1:{s:8:"langcode";a:3:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:12;s:8:"not null";b:1;}}}s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:8:"langcode";a:3:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:12;s:8:"not null";b:1;}}}s:33:"content_moderation_state_revision";a:1:{s:6:"fields";a:1:{s:8:"langcode";a:3:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:12;s:8:"not null";b:1;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:8:"langcode";a:3:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:12;s:8:"not null";b:1;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.moderation_state', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:16:"moderation_state";a:4:{s:4:"type";s:7:"varchar";s:6:"length";i:255;s:6:"binary";b:0;s:8:"not null";b:0;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:16:"moderation_state";a:4:{s:4:"type";s:7:"varchar";s:6:"length";i:255;s:6:"binary";b:0;s:8:"not null";b:0;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.revision_id', + 'value' => 'a:4:{s:24:"content_moderation_state";a:1:{s:6:"fields";a:1:{s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:0;}}}s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:1;}}}s:33:"content_moderation_state_revision";a:1:{s:6:"fields";a:1:{s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:1;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:4:"size";s:6:"normal";s:8:"not null";b:1;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.revision_translation_affected', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:1:{s:6:"fields";a:1:{s:29:"revision_translation_affected";a:3:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:0;}}}s:39:"content_moderation_state_field_revision";a:1:{s:6:"fields";a:1:{s:29:"revision_translation_affected";a:3:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:0;}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.uid', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:2:{s:6:"fields";a:1:{s:3:"uid";a:4:{s:11:"description";s:28:"The ID of the target entity.";s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;}}s:7:"indexes";a:1:{s:46:"content_moderation_state_field__uid__target_id";a:1:{i:0;s:3:"uid";}}}s:39:"content_moderation_state_field_revision";a:2:{s:6:"fields";a:1:{s:3:"uid";a:4:{s:11:"description";s:28:"The ID of the target entity.";s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;}}s:7:"indexes";a:1:{s:46:"content_moderation_state_field__uid__target_id";a:1:{i:0;s:3:"uid";}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.uuid', + 'value' => 'a:1:{s:24:"content_moderation_state";a:2:{s:6:"fields";a:1:{s:4:"uuid";a:4:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:6:"binary";b:0;s:8:"not null";b:1;}}s:11:"unique keys";a:1:{s:43:"content_moderation_state_field__uuid__value";a:1:{i:0;s:4:"uuid";}}}}', + )) + ->values(array( + 'collection' => 'entity.storage_schema.sql', + 'name' => 'content_moderation_state.field_schema_data.workflow', + 'value' => 'a:2:{s:35:"content_moderation_state_field_data";a:2:{s:6:"fields";a:1:{s:8:"workflow";a:4:{s:11:"description";s:28:"The ID of the target entity.";s:4:"type";s:13:"varchar_ascii";s:6:"length";i:255;s:8:"not null";b:0;}}s:7:"indexes";a:1:{s:36:"content_moderation_state__09628d8dbc";a:1:{i:0;s:8:"workflow";}}}s:39:"content_moderation_state_field_revision";a:2:{s:6:"fields";a:1:{s:8:"workflow";a:4:{s:11:"description";s:28:"The ID of the target entity.";s:4:"type";s:13:"varchar_ascii";s:6:"length";i:255;s:8:"not null";b:0;}}s:7:"indexes";a:1:{s:36:"content_moderation_state__09628d8dbc";a:1:{i:0;s:8:"workflow";}}}}', + )) + ->values(array( + 'collection' => 'state', + 'name' => 'routing.non_admin_routes', + 'value' => 'a:97:{i:0;s:27:"block.category_autocomplete";i:1;s:22:"block_content.add_page";i:2;s:22:"block_content.add_form";i:3;s:30:"entity.block_content.canonical";i:4;s:30:"entity.block_content.edit_form";i:5;s:32:"entity.block_content.delete_form";i:6;s:24:"entity.comment.edit_form";i:7;s:15:"comment.approve";i:8;s:24:"entity.comment.canonical";i:9;s:26:"entity.comment.delete_form";i:10;s:13:"comment.reply";i:11;s:31:"comment.new_comments_node_links";i:12;s:21:"comment.node_redirect";i:13;s:17:"contact.site_page";i:14;s:29:"entity.contact_form.canonical";i:15;s:24:"entity.user.contact_form";i:16;s:17:"contextual.render";i:17;s:17:"editor.filter_xss";i:18;s:31:"editor.field_untransformed_text";i:19;s:19:"editor.image_dialog";i:20;s:18:"editor.link_dialog";i:21;s:18:"file.ajax_progress";i:22;s:15:"filter.tips_all";i:23;s:11:"filter.tips";i:24;s:26:"history.get_last_node_view";i:25;s:17:"history.read_node";i:26;s:18:"image.style_public";i:27;s:19:"image.style_private";i:28;s:12:"image.upload";i:29;s:10:"image.info";i:30;s:13:"node.add_page";i:31;s:8:"node.add";i:32;s:19:"entity.node.preview";i:33;s:27:"entity.node.version_history";i:34;s:20:"entity.node.revision";i:35;s:28:"node.revision_revert_confirm";i:36;s:40:"node.revision_revert_translation_confirm";i:37;s:28:"node.revision_delete_confirm";i:38;s:18:"quickedit.metadata";i:39;s:21:"quickedit.attachments";i:40;s:20:"quickedit.field_form";i:41;s:21:"quickedit.entity_save";i:42;s:11:"search.view";i:43;s:23:"search.view_node_search";i:44;s:23:"search.help_node_search";i:45;s:23:"search.view_user_search";i:46;s:23:"search.help_user_search";i:47;s:19:"shortcut.set_switch";i:48;s:10:"system.401";i:49;s:10:"system.403";i:50;s:10:"system.404";i:51;s:10:"system.4xx";i:52;s:11:"system.cron";i:53;s:33:"system.machine_name_transliterate";i:54;s:12:"system.files";i:55;s:28:"system.private_file_download";i:56;s:16:"system.temporary";i:57;s:7:"";i:58;s:6:"";i:59;s:8:"";i:60;s:9:"";i:61;s:15:"system.timezone";i:62;s:22:"system.batch_page.html";i:63;s:22:"system.batch_page.json";i:64;s:16:"system.db_update";i:65;s:26:"system.entity_autocomplete";i:66;s:16:"system.csrftoken";i:67;s:30:"entity.taxonomy_term.edit_form";i:68;s:32:"entity.taxonomy_term.delete_form";i:69;s:16:"toolbar.subtrees";i:70;s:13:"user.register";i:71;s:11:"user.logout";i:72;s:9:"user.pass";i:73;s:14:"user.pass.http";i:74;s:9:"user.page";i:75;s:10:"user.login";i:76;s:15:"user.login.http";i:77;s:22:"user.login_status.http";i:78;s:16:"user.logout.http";i:79;s:19:"user.cancel_confirm";i:80;s:16:"user.reset.login";i:81;s:10:"user.reset";i:82;s:15:"user.reset.form";i:83;s:21:"view.frontpage.feed_1";i:84;s:21:"view.frontpage.page_1";i:85;s:25:"view.taxonomy_term.feed_1";i:86;s:25:"view.taxonomy_term.page_1";i:87;s:10:"views.ajax";i:88;s:35:"entity.block_content.latest_version";i:89;s:21:"entity.node.canonical";i:90;s:23:"entity.node.delete_form";i:91;s:21:"entity.node.edit_form";i:92;s:26:"entity.node.latest_version";i:93;s:21:"entity.user.canonical";i:94;s:21:"entity.user.edit_form";i:95;s:23:"entity.user.cancel_form";i:96;s:30:"entity.taxonomy_term.canonical";}', + )) + ->values(array( + 'collection' => 'state', + 'name' => 'system.js_cache_files', + 'value' => 'a:10:{s:64:"ef5219d33ebedcd4b9b0ccc64f741d50bebb463122945dd3b12519b97e268ab4";s:61:"public://js/js_VtafjXmRvoUgAzqzYTA3Wrjkx9wcWhjP0G4ZnnqRamA.js";s:64:"22b57c12b5f7dfa20d16a8fb27842e2c48a55df949019086a2e14bfa9b53ed21";s:61:"public://js/js_BKcMdIbOMdbTdLn9dkUq3KCJfIKKo2SvKoQ1AnB8D-g.js";s:64:"c839df7c4fcaff2cb7890a0c2e9316f456b4c990c363fb4eb87a2a601c594055";s:61:"public://js/js_VhqXmo4azheUjYC30rijnR_Dddo0WjWkF27k5gTL8S4.js";s:64:"4290e1da549b525e5a284c0b6932deb2925f10d69b9e9df47ab9cf9be6f908c3";s:61:"public://js/js_bXOpMT4zIssDSNf-hJCfDU-GMYjogKxosCScYEEjggE.js";s:64:"ffc78e60c19e191320a1b742a777ad5b93976fce4b274faf2332dea1c3cf2393";s:61:"public://js/js_lZ_KgpFfmlx3GgVnM7BsJsa7fCjkkusU9keGexj0zRU.js";s:64:"a3979c3d25cb559722f7d2706c5d35e45bee24623da43b716fd806beea460ea4";s:61:"public://js/js_jeYE5w7CHcwrxNQJfqi7dVmAaL_TOwRxNmRmq7vLsUQ.js";s:64:"79ab52de68ab5af51160a0ef90f0c3b81977061cd1b4ec411ace995fb97ed34f";s:61:"public://js/js_PSJbtOVCvisdPwajJGvk9V8i7H6XPQfSy9LE1sAkneE.js";s:64:"07fd78d9ba4d77f63cb7a40bfaf66bb5d6232e46a5822207e8dd0d9252810971";s:61:"public://js/js_yFV18P6CACJDKa_0KFPQJwI-GGWxK6FqfSt1jdGZzDo.js";s:64:"f7a654d4d83e97e639b9855ec7593433aa08380ffd163ea2860c4d17f53f0f1b";s:61:"public://js/js_a-XEqg_PQIgAR7_4F2EScN6QKaClD_F43n2X6kQJwu4.js";s:64:"ddea937b5008530524945e74d82ce7ad1660346c4d44396941f743f3a0440973";s:61:"public://js/js_8BEUTcp1kBATjLlIGkgkfV9MI1FiKvn5V0c3C89wHSI.js";}', + )) + ->values(array( + 'collection' => 'state', + 'name' => 'system.theme.files', + 'value' => 'a:47:{s:19:"test_invalid_engine";s:81:"core/modules/system/tests/themes/test_invalid_engine/test_invalid_engine.info.yml";s:34:"test_ckeditor_stylesheets_external";s:111:"core/modules/system/tests/themes/test_ckeditor_stylesheets_external/test_ckeditor_stylesheets_external.info.yml";s:43:"test_ckeditor_stylesheets_protocol_relative";s:129:"core/modules/system/tests/themes/test_ckeditor_stylesheets_protocol_relative/test_ckeditor_stylesheets_protocol_relative.info.yml";s:34:"test_ckeditor_stylesheets_relative";s:111:"core/modules/system/tests/themes/test_ckeditor_stylesheets_relative/test_ckeditor_stylesheets_relative.info.yml";s:26:"test_theme_nyan_cat_engine";s:95:"core/modules/system/tests/themes/test_theme_nyan_cat_engine/test_theme_nyan_cat_engine.info.yml";s:19:"test_theme_settings";s:81:"core/modules/system/tests/themes/test_theme_settings/test_theme_settings.info.yml";s:16:"test_theme_theme";s:75:"core/modules/system/tests/themes/test_theme_theme/test_theme_theme.info.yml";s:14:"test_wild_west";s:71:"core/modules/system/tests/themes/test_wild_west/test_wild_west.info.yml";s:5:"stark";s:32:"core/themes/stark/stark.info.yml";s:19:"big_pipe_test_theme";s:83:"core/modules/big_pipe/tests/themes/big_pipe_test_theme/big_pipe_test_theme.info.yml";s:29:"block_test_specialchars_theme";s:119:"core/modules/block/tests/modules/block_test/themes/block_test_specialchars_theme/block_test_specialchars_theme.info.yml";s:16:"block_test_theme";s:93:"core/modules/block/tests/modules/block_test/themes/block_test_theme/block_test_theme.info.yml";s:21:"breakpoint_theme_test";s:89:"core/modules/breakpoint/tests/themes/breakpoint_theme_test/breakpoint_theme_test.info.yml";s:16:"color_test_theme";s:93:"core/modules/color/tests/modules/color_test/themes/color_test_theme/color_test_theme.info.yml";s:23:"config_clash_test_theme";s:82:"core/modules/config/tests/config_clash_test_theme/config_clash_test_theme.info.yml";s:29:"config_translation_test_theme";s:113:"core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml";s:24:"statistics_test_attached";s:95:"core/modules/statistics/tests/themes/statistics_test_attached/statistics_test_attached.info.yml";s:14:"test_basetheme";s:71:"core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml";s:22:"test_invalid_basetheme";s:87:"core/modules/system/tests/themes/test_invalid_basetheme/test_invalid_basetheme.info.yml";s:26:"test_invalid_basetheme_sub";s:95:"core/modules/system/tests/themes/test_invalid_basetheme_sub/test_invalid_basetheme_sub.info.yml";s:17:"test_invalid_core";s:77:"core/modules/system/tests/themes/test_invalid_core/test_invalid_core.info.yml";s:19:"test_invalid_region";s:81:"core/modules/system/tests/themes/test_invalid_region/test_invalid_region.info.yml";s:11:"test_stable";s:65:"core/modules/system/tests/themes/test_stable/test_stable.info.yml";s:16:"test_subsubtheme";s:75:"core/modules/system/tests/themes/test_subsubtheme/test_subsubtheme.info.yml";s:10:"test_theme";s:63:"core/modules/system/tests/themes/test_theme/test_theme.info.yml";s:51:"test_theme_having_veery_long_name_which_is_too_long";s:145:"core/modules/system/tests/themes/test_theme_having_veery_long_name_which_is_too_long/test_theme_having_veery_long_name_which_is_too_long.info.yml";s:26:"test_theme_libraries_empty";s:95:"core/modules/system/tests/themes/test_theme_libraries_empty/test_theme_libraries_empty.info.yml";s:27:"test_theme_libraries_extend";s:97:"core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.info.yml";s:50:"test_theme_libraries_override_with_drupal_settings";s:143:"core/modules/system/tests/themes/test_theme_libraries_override_with_drupal_settings/test_theme_libraries_override_with_drupal_settings.info.yml";s:48:"test_theme_libraries_override_with_invalid_asset";s:139:"core/modules/system/tests/themes/test_theme_libraries_override_with_invalid_asset/test_theme_libraries_override_with_invalid_asset.info.yml";s:40:"test_theme_twig_registry_loader_subtheme";s:123:"core/modules/system/tests/themes/test_theme_twig_registry_loader_subtheme/test_theme_twig_registry_loader_subtheme.info.yml";s:20:"update_test_subtheme";s:83:"core/modules/update/tests/themes/update_test_subtheme/update_test_subtheme.info.yml";s:15:"user_test_theme";s:71:"core/modules/user/tests/themes/user_test_theme/user_test_theme.info.yml";s:27:"views_test_checkboxes_theme";s:96:"core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.info.yml";s:16:"views_test_theme";s:74:"core/modules/views/tests/themes/views_test_theme/views_test_theme.info.yml";s:13:"test_subtheme";s:69:"core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml";s:31:"test_theme_twig_registry_loader";s:105:"core/modules/system/tests/themes/test_theme_twig_registry_loader/test_theme_twig_registry_loader.info.yml";s:37:"test_theme_twig_registry_loader_theme";s:117:"core/modules/system/tests/themes/test_theme_twig_registry_loader_theme/test_theme_twig_registry_loader_theme.info.yml";s:21:"update_test_basetheme";s:85:"core/modules/update/tests/themes/update_test_basetheme/update_test_basetheme.info.yml";s:6:"stable";s:34:"core/themes/stable/stable.info.yml";s:5:"seven";s:32:"core/themes/seven/seven.info.yml";s:6:"bartik";s:34:"core/themes/bartik/bartik.info.yml";s:6:"classy";s:34:"core/themes/classy/classy.info.yml";s:23:"entity_print_test_theme";s:90:"modules/entity_print/tests/themes/entity_print_test_theme/entity_print_test_theme.info.yml";s:28:"webform_bootstrap_test_theme";s:121:"modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/webform_bootstrap_test_theme.info.yml";s:19:"webform_test_bartik";s:77:"modules/webform/tests/themes/webform_test_bartik/webform_test_bartik.info.yml";s:4:"mayo";s:25:"themes/mayo/mayo.info.yml";}', + )) + ->values(array( + 'collection' => 'system.schema', + 'name' => 'content_moderation', + 'value' => 's:4:"8401";', + )) + ->values(array( + 'collection' => 'system.schema', + 'name' => 'workflows', + 'value' => 'i:8000;', + )) + ->execute(); + +$connection->delete('menu_tree') + ->condition('mlid', [ + '24', + ], 'IN') + ->execute(); + +$connection->insert('menu_tree') + ->fields(array( + 'menu_name', + 'mlid', + 'id', + 'parent', + 'route_name', + 'route_param_key', + 'route_parameters', + 'url', + 'title', + 'description', + 'class', + 'options', + 'provider', + 'enabled', + 'discovered', + 'expanded', + 'weight', + 'metadata', + 'has_children', + 'depth', + 'p1', + 'p2', + 'p3', + 'p4', + 'p5', + 'p6', + 'p7', + 'p8', + 'p9', + 'form_class', + )) + ->values(array( + 'menu_name' => 'admin', + 'mlid' => '24', + 'id' => 'system.admin_config_workflow', + 'parent' => 'system.admin_config', + 'route_name' => 'system.admin_config_workflow', + 'route_param_key' => '', + 'route_parameters' => 'a:0:{}', + 'url' => '', + 'title' => 'O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:8:"Workflow";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}', + 'description' => 'O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:28:"Manage the content workflow.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}', + 'class' => 'Drupal\Core\Menu\MenuLinkDefault', + 'options' => 'a:0:{}', + 'provider' => 'system', + 'enabled' => '1', + 'discovered' => '1', + 'expanded' => '0', + 'weight' => '5', + 'metadata' => 'a:0:{}', + 'has_children' => '1', + 'depth' => '3', + 'p1' => '1', + 'p2' => '6', + 'p3' => '24', + 'p4' => '0', + 'p5' => '0', + 'p6' => '0', + 'p7' => '0', + 'p8' => '0', + 'p9' => '0', + 'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm', + )) + ->values(array( + 'menu_name' => 'admin', + 'mlid' => '63', + 'id' => 'entity.workflow.collection', + 'parent' => 'system.admin_config_workflow', + 'route_name' => 'entity.workflow.collection', + 'route_param_key' => '', + 'route_parameters' => 'a:0:{}', + 'url' => '', + 'title' => 'O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:9:"Workflows";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}', + 'description' => 'O:48:"Drupal\Core\StringTranslation\TranslatableMarkup":3:{s:9:"*string";s:20:"Configure workflows.";s:12:"*arguments";a:0:{}s:10:"*options";a:0:{}}', + 'class' => 'Drupal\Core\Menu\MenuLinkDefault', + 'options' => 'a:0:{}', + 'provider' => 'workflows', + 'enabled' => '1', + 'discovered' => '1', + 'expanded' => '0', + 'weight' => '0', + 'metadata' => 'a:0:{}', + 'has_children' => '0', + 'depth' => '4', + 'p1' => '1', + 'p2' => '6', + 'p3' => '24', + 'p4' => '63', + 'p5' => '0', + 'p6' => '0', + 'p7' => '0', + 'p8' => '0', + 'p9' => '0', + 'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm', + )) + ->execute(); + +$connection->delete('router') + ->condition('name', [ + 'entity.block_content.canonical', + 'entity.block_content.edit_form', + 'entity.node.edit_form', + ], 'IN') + ->execute(); + +$connection->insert('router') + ->fields(array( + 'name', + 'path', + 'pattern_outline', + 'fit', + 'route', + 'number_parts', + )) + ->values(array( + 'name' => 'content_moderation.workflow_type_edit_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/type/{entity_type_id}', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/type/%', + 'fit' => '250', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1871:{a:9:{s:4:"path";s:72:"/admin/config/workflow/workflows/manage/{workflow}/type/{entity_type_id}";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:5:"_form";s:73:"\Drupal\content_moderation\Form\ContentModerationConfigureEntityTypesForm";s:15:"_title_callback";s:83:"\Drupal\content_moderation\Form\ContentModerationConfigureEntityTypesForm::getTitle";}s:12:"requirements";a:1:{s:11:"_permission";s:20:"administer workflows";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:19:"route_enhancer.form";}s:14:"_access_checks";a:1:{i:0;s:23:"access_check.permission";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":768:{a:11:{s:4:"vars";a:2:{i:0;s:8:"workflow";i:1;s:14:"entity_type_id";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:97:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/type/(?P[^/]++)$#s";s:11:"path_tokens";a:4:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:14:"entity_type_id";}i:1;a:2:{i:0;s:4:"text";i:1;s:5:"/type";}i:2;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:3;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:2:{i:0;s:8:"workflow";i:1;s:14:"entity_type_id";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:250;s:14:"patternOutline";s:48:"/admin/config/workflow/workflows/manage/%/type/%";s:8:"numParts";i:8;}}}}', + 'number_parts' => '8', + )) + ->values(array( + 'name' => 'entity.block_content.canonical', + 'path' => '/block/{block_content}', + 'pattern_outline' => '/block/%', + 'fit' => '2', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1368:{a:9:{s:4:"path";s:22:"/block/{block_content}";s:4:"host";s:0:"";s:8:"defaults";a:1:{s:12:"_entity_form";s:18:"block_content.edit";}s:12:"requirements";a:2:{s:14:"_entity_access";s:20:"block_content.update";s:13:"block_content";s:3:"\d+";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:12:"_admin_route";b:1;s:10:"parameters";a:1:{s:13:"block_content";a:2:{s:4:"type";s:20:"entity:block_content";s:9:"converter";s:30:"paramconverter.latest_revision";}}s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":466:{a:11:{s:4:"vars";a:1:{i:0;s:13:"block_content";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:34:"#^/block/(?P\d+)$#s";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:13:"block_content";}i:1;a:2:{i:0;s:4:"text";i:1;s:6:"/block";}}s:9:"path_vars";a:1:{i:0;s:13:"block_content";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:2;s:14:"patternOutline";s:8:"/block/%";s:8:"numParts";i:2;}}}}', + 'number_parts' => '2', + )) + ->values(array( + 'name' => 'entity.block_content.edit_form', + 'path' => '/block/{block_content}', + 'pattern_outline' => '/block/%', + 'fit' => '2', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1368:{a:9:{s:4:"path";s:22:"/block/{block_content}";s:4:"host";s:0:"";s:8:"defaults";a:1:{s:12:"_entity_form";s:18:"block_content.edit";}s:12:"requirements";a:2:{s:14:"_entity_access";s:20:"block_content.update";s:13:"block_content";s:3:"\d+";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:12:"_admin_route";b:1;s:10:"parameters";a:1:{s:13:"block_content";a:2:{s:4:"type";s:20:"entity:block_content";s:9:"converter";s:30:"paramconverter.latest_revision";}}s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":466:{a:11:{s:4:"vars";a:1:{i:0;s:13:"block_content";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:34:"#^/block/(?P\d+)$#s";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:13:"block_content";}i:1;a:2:{i:0;s:4:"text";i:1;s:6:"/block";}}s:9:"path_vars";a:1:{i:0;s:13:"block_content";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:2;s:14:"patternOutline";s:8:"/block/%";s:8:"numParts";i:2;}}}}', + 'number_parts' => '2', + )) + ->values(array( + 'name' => 'entity.block_content.latest_version', + 'path' => '/block/{block_content}/latest', + 'pattern_outline' => '/block/%/latest', + 'fit' => '5', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1678:{a:9:{s:4:"path";s:29:"/block/{block_content}/latest";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_view";s:18:"block_content.full";s:15:"_title_callback";s:54:"\Drupal\Core\Entity\Controller\EntityController::title";}s:12:"requirements";a:3:{s:14:"_entity_access";s:18:"block_content.view";s:34:"_content_moderation_latest_version";s:4:"TRUE";s:13:"block_content";s:3:"\d+";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:31:"_content_moderation_entity_type";s:13:"block_content";s:10:"parameters";a:1:{s:13:"block_content";a:3:{s:4:"type";s:20:"entity:block_content";s:21:"load_pending_revision";i:1;s:9:"converter";s:30:"paramconverter.latest_revision";}}s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:2:{i:0;s:19:"access_check.entity";i:1;s:28:"access_check.latest_revision";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":524:{a:11:{s:4:"vars";a:1:{i:0;s:13:"block_content";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:41:"#^/block/(?P\d+)/latest$#s";s:11:"path_tokens";a:3:{i:0;a:2:{i:0;s:4:"text";i:1;s:7:"/latest";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:13:"block_content";}i:2;a:2:{i:0;s:4:"text";i:1;s:6:"/block";}}s:9:"path_vars";a:1:{i:0;s:13:"block_content";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:5;s:14:"patternOutline";s:15:"/block/%/latest";s:8:"numParts";i:3;}}}}', + 'number_parts' => '3', + )) + ->values(array( + 'name' => 'entity.node.edit_form', + 'path' => '/node/{node}/edit', + 'pattern_outline' => '/node/%/edit', + 'fit' => '5', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1358:{a:9:{s:4:"path";s:17:"/node/{node}/edit";s:4:"host";s:0:"";s:8:"defaults";a:1:{s:12:"_entity_form";s:9:"node.edit";}s:12:"requirements";a:2:{s:14:"_entity_access";s:11:"node.update";s:4:"node";s:3:"\d+";}s:7:"options";a:7:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:21:"_node_operation_route";b:1;s:12:"_admin_route";b:1;s:10:"parameters";a:1:{s:4:"node";a:2:{s:4:"type";s:11:"entity:node";s:9:"converter";s:30:"paramconverter.latest_revision";}}s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":476:{a:11:{s:4:"vars";a:1:{i:0;s:4:"node";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:29:"#^/node/(?P\d+)/edit$#s";s:11:"path_tokens";a:3:{i:0;a:2:{i:0;s:4:"text";i:1;s:5:"/edit";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:4:"node";}i:2;a:2:{i:0;s:4:"text";i:1;s:5:"/node";}}s:9:"path_vars";a:1:{i:0;s:4:"node";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:5;s:14:"patternOutline";s:12:"/node/%/edit";s:8:"numParts";i:3;}}}}', + 'number_parts' => '3', + )) + ->values(array( + 'name' => 'entity.node.latest_version', + 'path' => '/node/{node}/latest', + 'pattern_outline' => '/node/%/latest', + 'fit' => '5', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1567:{a:9:{s:4:"path";s:19:"/node/{node}/latest";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_view";s:9:"node.full";s:15:"_title_callback";s:54:"\Drupal\Core\Entity\Controller\EntityController::title";}s:12:"requirements";a:3:{s:14:"_entity_access";s:9:"node.view";s:34:"_content_moderation_latest_version";s:4:"TRUE";s:4:"node";s:3:"\d+";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:31:"_content_moderation_entity_type";s:4:"node";s:10:"parameters";a:1:{s:4:"node";a:3:{s:4:"type";s:11:"entity:node";s:21:"load_pending_revision";i:1;s:9:"converter";s:30:"paramconverter.latest_revision";}}s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:2:{i:0;s:19:"access_check.entity";i:1;s:28:"access_check.latest_revision";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":482:{a:11:{s:4:"vars";a:1:{i:0;s:4:"node";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:31:"#^/node/(?P\d+)/latest$#s";s:11:"path_tokens";a:3:{i:0;a:2:{i:0;s:4:"text";i:1;s:7:"/latest";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:4:"node";}i:2;a:2:{i:0;s:4:"text";i:1;s:5:"/node";}}s:9:"path_vars";a:1:{i:0;s:4:"node";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:5;s:14:"patternOutline";s:14:"/node/%/latest";s:8:"numParts";i:3;}}}}', + 'number_parts' => '3', + )) + ->values(array( + 'name' => 'entity.workflow.add_form', + 'path' => '/admin/config/workflow/workflows/add', + 'pattern_outline' => '/admin/config/workflow/workflows/add', + 'fit' => '31', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1264:{a:9:{s:4:"path";s:36:"/admin/config/workflow/workflows/add";s:4:"host";s:0:"";s:8:"defaults";a:3:{s:12:"_entity_form";s:12:"workflow.add";s:14:"entity_type_id";s:8:"workflow";s:15:"_title_callback";s:56:"Drupal\Core\Entity\Controller\EntityController::addTitle";}s:12:"requirements";a:1:{s:21:"_entity_create_access";s:8:"workflow";}s:7:"options";a:5:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:26:"access_check.entity_create";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":404:{a:11:{s:4:"vars";a:0:{}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:41:"#^/admin/config/workflow/workflows/add$#s";s:11:"path_tokens";a:1:{i:0;a:2:{i:0;s:4:"text";i:1;s:36:"/admin/config/workflow/workflows/add";}}s:9:"path_vars";a:0:{}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:31;s:14:"patternOutline";s:36:"/admin/config/workflow/workflows/add";s:8:"numParts";i:5;}}}}', + 'number_parts' => '5', + )) + ->values(array( + 'name' => 'entity.workflow.add_state_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/add_state', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/add_state', + 'fit' => '125', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1572:{a:9:{s:4:"path";s:60:"/admin/config/workflow/workflows/manage/{workflow}/add_state";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_form";s:18:"workflow.add-state";s:6:"_title";s:9:"Add state";}s:12:"requirements";a:1:{s:14:"_entity_access";s:13:"workflow.edit";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":619:{a:11:{s:4:"vars";a:1:{i:0;s:8:"workflow";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:75:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/add_state$#s";s:11:"path_tokens";a:3:{i:0;a:2:{i:0;s:4:"text";i:1;s:10:"/add_state";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:2;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:1:{i:0;s:8:"workflow";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:125;s:14:"patternOutline";s:51:"/admin/config/workflow/workflows/manage/%/add_state";s:8:"numParts";i:7;}}}}', + 'number_parts' => '7', + )) + ->values(array( + 'name' => 'entity.workflow.add_transition_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/add_transition', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/add_transition', + 'fit' => '125', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1603:{a:9:{s:4:"path";s:65:"/admin/config/workflow/workflows/manage/{workflow}/add_transition";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_form";s:23:"workflow.add-transition";s:6:"_title";s:14:"Add transition";}s:12:"requirements";a:1:{s:14:"_entity_access";s:13:"workflow.edit";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":634:{a:11:{s:4:"vars";a:1:{i:0;s:8:"workflow";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:80:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/add_transition$#s";s:11:"path_tokens";a:3:{i:0;a:2:{i:0;s:4:"text";i:1;s:15:"/add_transition";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:2;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:1:{i:0;s:8:"workflow";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:125;s:14:"patternOutline";s:56:"/admin/config/workflow/workflows/manage/%/add_transition";s:8:"numParts";i:7;}}}}', + 'number_parts' => '7', + )) + ->values(array( + 'name' => 'entity.workflow.collection', + 'path' => '/admin/config/workflow/workflows', + 'pattern_outline' => '/admin/config/workflow/workflows', + 'fit' => '15', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1207:{a:9:{s:4:"path";s:32:"/admin/config/workflow/workflows";s:4:"host";s:0:"";s:8:"defaults";a:4:{s:12:"_entity_list";s:8:"workflow";s:6:"_title";s:9:"Workflows";s:16:"_title_arguments";a:0:{}s:14:"_title_context";s:0:"";}s:12:"requirements";a:1:{s:11:"_permission";s:20:"administer workflows";}s:7:"options";a:5:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:23:"access_check.permission";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":392:{a:11:{s:4:"vars";a:0:{}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:37:"#^/admin/config/workflow/workflows$#s";s:11:"path_tokens";a:1:{i:0;a:2:{i:0;s:4:"text";i:1;s:32:"/admin/config/workflow/workflows";}}s:9:"path_vars";a:0:{}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:15;s:14:"patternOutline";s:32:"/admin/config/workflow/workflows";s:8:"numParts";i:4;}}}}', + 'number_parts' => '4', + )) + ->values(array( + 'name' => 'entity.workflow.delete_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/delete', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/delete', + 'fit' => '125', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1620:{a:9:{s:4:"path";s:57:"/admin/config/workflow/workflows/manage/{workflow}/delete";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_form";s:15:"workflow.delete";s:15:"_title_callback";s:60:"\Drupal\Core\Entity\Controller\EntityController::deleteTitle";}s:12:"requirements";a:1:{s:14:"_entity_access";s:15:"workflow.delete";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":609:{a:11:{s:4:"vars";a:1:{i:0;s:8:"workflow";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:72:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/delete$#s";s:11:"path_tokens";a:3:{i:0;a:2:{i:0;s:4:"text";i:1;s:7:"/delete";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:2;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:1:{i:0;s:8:"workflow";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:125;s:14:"patternOutline";s:48:"/admin/config/workflow/workflows/manage/%/delete";s:8:"numParts";i:7;}}}}', + 'number_parts' => '7', + )) + ->values(array( + 'name' => 'entity.workflow.delete_state_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}/delete', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/state/%/delete', + 'fit' => '501', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1845:{a:9:{s:4:"path";s:80:"/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}/delete";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:5:"_form";s:46:"\Drupal\workflows\Form\WorkflowStateDeleteForm";s:6:"_title";s:12:"Delete state";}s:12:"requirements";a:1:{s:29:"_workflow_state_delete_access";s:4:"true";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:19:"route_enhancer.form";}s:14:"_access_checks";a:1:{i:0;s:35:"workflows.access_check.delete_state";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":829:{a:11:{s:4:"vars";a:2:{i:0;s:8:"workflow";i:1;s:14:"workflow_state";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:105:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/state/(?P[^/]++)/delete$#s";s:11:"path_tokens";a:5:{i:0;a:2:{i:0;s:4:"text";i:1;s:7:"/delete";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:14:"workflow_state";}i:2;a:2:{i:0;s:4:"text";i:1;s:6:"/state";}i:3;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:4;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:2:{i:0;s:8:"workflow";i:1;s:14:"workflow_state";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:501;s:14:"patternOutline";s:56:"/admin/config/workflow/workflows/manage/%/state/%/delete";s:8:"numParts";i:9;}}}}', + 'number_parts' => '9', + )) + ->values(array( + 'name' => 'entity.workflow.delete_transition_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}/delete', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/transition/%/delete', + 'fit' => '501', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1880:{a:9:{s:4:"path";s:90:"/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}/delete";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:5:"_form";s:51:"\Drupal\workflows\Form\WorkflowTransitionDeleteForm";s:6:"_title";s:17:"Delete transition";}s:12:"requirements";a:1:{s:14:"_entity_access";s:13:"workflow.edit";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:19:"route_enhancer.form";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":865:{a:11:{s:4:"vars";a:2:{i:0;s:8:"workflow";i:1;s:19:"workflow_transition";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:115:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/transition/(?P[^/]++)/delete$#s";s:11:"path_tokens";a:5:{i:0;a:2:{i:0;s:4:"text";i:1;s:7:"/delete";}i:1;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:19:"workflow_transition";}i:2;a:2:{i:0;s:4:"text";i:1;s:11:"/transition";}i:3;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:4;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:2:{i:0;s:8:"workflow";i:1;s:19:"workflow_transition";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:501;s:14:"patternOutline";s:61:"/admin/config/workflow/workflows/manage/%/transition/%/delete";s:8:"numParts";i:9;}}}}', + 'number_parts' => '9', + )) + ->values(array( + 'name' => 'entity.workflow.edit_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%', + 'fit' => '62', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1551:{a:9:{s:4:"path";s:50:"/admin/config/workflow/workflows/manage/{workflow}";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_form";s:13:"workflow.edit";s:15:"_title_callback";s:58:"\Drupal\Core\Entity\Controller\EntityController::editTitle";}s:12:"requirements";a:1:{s:14:"_entity_access";s:15:"workflow.update";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":551:{a:11:{s:4:"vars";a:1:{i:0;s:8:"workflow";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:65:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)$#s";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:1;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:1:{i:0;s:8:"workflow";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:62;s:14:"patternOutline";s:41:"/admin/config/workflow/workflows/manage/%";s:8:"numParts";i:6;}}}}', + 'number_parts' => '6', + )) + ->values(array( + 'name' => 'entity.workflow.edit_state_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/state/%', + 'fit' => '250', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1740:{a:9:{s:4:"path";s:73:"/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_form";s:19:"workflow.edit-state";s:6:"_title";s:10:"Edit state";}s:12:"requirements";a:1:{s:14:"_entity_access";s:13:"workflow.edit";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":771:{a:11:{s:4:"vars";a:2:{i:0;s:8:"workflow";i:1;s:14:"workflow_state";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:98:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/state/(?P[^/]++)$#s";s:11:"path_tokens";a:4:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:14:"workflow_state";}i:1;a:2:{i:0;s:4:"text";i:1;s:6:"/state";}i:2;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:3;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:2:{i:0;s:8:"workflow";i:1;s:14:"workflow_state";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:250;s:14:"patternOutline";s:49:"/admin/config/workflow/workflows/manage/%/state/%";s:8:"numParts";i:8;}}}}', + 'number_parts' => '8', + )) + ->values(array( + 'name' => 'entity.workflow.edit_transition_form', + 'path' => '/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}', + 'pattern_outline' => '/admin/config/workflow/workflows/manage/%/transition/%', + 'fit' => '250', + 'route' => 'C:31:"Symfony\Component\Routing\Route":1797:{a:9:{s:4:"path";s:83:"/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}";s:4:"host";s:0:"";s:8:"defaults";a:2:{s:12:"_entity_form";s:24:"workflow.edit-transition";s:6:"_title";s:15:"Edit transition";}s:12:"requirements";a:1:{s:14:"_entity_access";s:13:"workflow.edit";}s:7:"options";a:6:{s:14:"compiler_class";s:34:"\Drupal\Core\Routing\RouteCompiler";s:10:"parameters";a:1:{s:8:"workflow";a:2:{s:4:"type";s:15:"entity:workflow";s:9:"converter";s:63:"drupal.proxy_original_service.paramconverter.configentity_admin";}}s:12:"_admin_route";b:1;s:14:"_route_filters";a:2:{i:0;s:13:"method_filter";i:1;s:27:"content_type_header_matcher";}s:16:"_route_enhancers";a:2:{i:0;s:31:"route_enhancer.param_conversion";i:1;s:21:"route_enhancer.entity";}s:14:"_access_checks";a:1:{i:0;s:19:"access_check.entity";}}s:7:"schemes";a:0:{}s:7:"methods";a:2:{i:0;s:3:"GET";i:1;s:4:"POST";}s:9:"condition";s:0:"";s:8:"compiled";C:33:"Drupal\Core\Routing\CompiledRoute":808:{a:11:{s:4:"vars";a:2:{i:0;s:8:"workflow";i:1;s:19:"workflow_transition";}s:11:"path_prefix";s:0:"";s:10:"path_regex";s:108:"#^/admin/config/workflow/workflows/manage/(?P[^/]++)/transition/(?P[^/]++)$#s";s:11:"path_tokens";a:4:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:19:"workflow_transition";}i:1;a:2:{i:0;s:4:"text";i:1;s:11:"/transition";}i:2;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:6:"[^/]++";i:3;s:8:"workflow";}i:3;a:2:{i:0;s:4:"text";i:1;s:39:"/admin/config/workflow/workflows/manage";}}s:9:"path_vars";a:2:{i:0;s:8:"workflow";i:1;s:19:"workflow_transition";}s:10:"host_regex";N;s:11:"host_tokens";a:0:{}s:9:"host_vars";a:0:{}s:3:"fit";i:250;s:14:"patternOutline";s:54:"/admin/config/workflow/workflows/manage/%/transition/%";s:8:"numParts";i:8;}}}}', + 'number_parts' => '8', + )) + ->execute(); diff --git a/core/modules/content_moderation/tests/fixtures/update/drupal-8.default-cms-entity-id-2941736.php b/core/modules/content_moderation/tests/fixtures/update/drupal-8.default-cms-entity-id-2941736.php new file mode 100644 index 0000000..5e9d236 --- /dev/null +++ b/core/modules/content_moderation/tests/fixtures/update/drupal-8.default-cms-entity-id-2941736.php @@ -0,0 +1,804 @@ +insert('block_content') + ->fields(array( + 'id', + 'revision_id', + 'type', + 'uuid', + 'langcode', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '1', + 'type' => 'test_block_content', + 'uuid' => '811fac6c-8184-4de5-99eb-9e70d28709f4', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'type' => 'test_block_content', + 'uuid' => 'b89f025c-0538-4075-bd8e-96acf74211c9', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'type' => 'test_block_content', + 'uuid' => '62e428e1-88a6-478c-a8c6-a554ca2332ae', + 'langcode' => 'en', + )) + ->execute(); + +$connection->insert('block_content_field_data') + ->fields(array( + 'id', + 'revision_id', + 'type', + 'langcode', + 'info', + 'changed', + 'default_langcode', + 'revision_translation_affected', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '1', + 'type' => 'test_block_content', + 'langcode' => 'en', + 'info' => 'draft pending revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'type' => 'test_block_content', + 'langcode' => 'en', + 'info' => 'published default revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'type' => 'test_block_content', + 'langcode' => 'en', + 'info' => 'archived default revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->execute(); + +$connection->insert('block_content_field_revision') + ->fields(array( + 'id', + 'revision_id', + 'langcode', + 'info', + 'changed', + 'default_langcode', + 'revision_translation_affected', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + 'info' => 'draft pending revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '2', + 'langcode' => 'en', + 'info' => 'draft pending revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'langcode' => 'en', + 'info' => 'published default revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '4', + 'langcode' => 'en', + 'info' => 'archived default revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'langcode' => 'en', + 'info' => 'archived default revision', + 'changed' => '1517725800', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->execute(); + +$connection->insert('block_content_revision') + ->fields(array( + 'id', + 'revision_id', + 'langcode', + 'revision_user', + 'revision_created', + 'revision_log', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + 'revision_user' => NULL, + 'revision_created' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'id' => '1', + 'revision_id' => '2', + 'langcode' => 'en', + 'revision_user' => NULL, + 'revision_created' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'langcode' => 'en', + 'revision_user' => NULL, + 'revision_created' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'id' => '3', + 'revision_id' => '4', + 'langcode' => 'en', + 'revision_user' => NULL, + 'revision_created' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'langcode' => 'en', + 'revision_user' => NULL, + 'revision_created' => '1517725800', + 'revision_log' => NULL, + )) + ->execute(); + +$connection->delete('config') + ->condition('name', ['workflows.workflow.editorial'], 'IN') + ->execute(); + +$connection->insert('config') + ->fields(array( + 'collection', + 'name', + 'data', + )) + ->values(array( + 'collection' => '', + 'name' => 'block_content.type.test_block_content', + 'data' => 'a:8:{s:4:"uuid";s:36:"966baba6-525e-48fe-b8c5-a5f131b1857f";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:0:{}s:2:"id";s:18:"test_block_content";s:5:"label";s:18:"Test Block Content";s:8:"revision";N;s:11:"description";N;}', + )) + ->values(array( + 'collection' => '', + 'name' => 'workflows.workflow.editorial', + 'data' => 'a:9:{s:4:"uuid";s:36:"08b548c7-ff59-468b-9347-7d697680d035";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:37:"block_content.type.test_block_content";i:1;s:17:"node.type.article";}s:6:"module";a:1:{i:0;s:18:"content_moderation";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"T_JxNjYlfoRBi7Bj1zs5Xv9xv1btuBkKp5C1tNrjMhI";}s:2:"id";s:9:"editorial";s:5:"label";s:9:"Editorial";s:4:"type";s:18:"content_moderation";s:13:"type_settings";a:3:{s:6:"states";a:3:{s:8:"archived";a:4:{s:5:"label";s:8:"Archived";s:6:"weight";i:5;s:9:"published";b:0;s:16:"default_revision";b:1;}s:5:"draft";a:4:{s:5:"label";s:5:"Draft";s:9:"published";b:0;s:16:"default_revision";b:0;s:6:"weight";i:-5;}s:9:"published";a:4:{s:5:"label";s:9:"Published";s:9:"published";b:1;s:16:"default_revision";b:1;s:6:"weight";i:0;}}s:11:"transitions";a:5:{s:7:"archive";a:4:{s:5:"label";s:7:"Archive";s:4:"from";a:1:{i:0;s:9:"published";}s:2:"to";s:8:"archived";s:6:"weight";i:2;}s:14:"archived_draft";a:4:{s:5:"label";s:16:"Restore to Draft";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:5:"draft";s:6:"weight";i:3;}s:18:"archived_published";a:4:{s:5:"label";s:7:"Restore";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:9:"published";s:6:"weight";i:4;}s:16:"create_new_draft";a:4:{s:5:"label";s:16:"Create New Draft";s:2:"to";s:5:"draft";s:6:"weight";i:0;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}s:7:"publish";a:4:{s:5:"label";s:7:"Publish";s:2:"to";s:9:"published";s:6:"weight";i:1;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}}s:12:"entity_types";a:2:{s:13:"block_content";a:1:{i:0;s:18:"test_block_content";}s:4:"node";a:1:{i:0;s:7:"article";}}}}', + )) + ->execute(); + +$connection->insert('content_moderation_state') + ->fields(array( + 'id', + 'revision_id', + 'uuid', + 'langcode', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '2', + 'uuid' => '3ce04732-f65f-4937-aa49-821f5842ae06', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'uuid' => 'a6507b55-3001-4748-8d32-f4fa47319754', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'uuid' => '112d2bd2-552b-4e2f-9a6d-526740ba1b38', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '4', + 'revision_id' => '7', + 'uuid' => 'a85d0d06-e046-4509-b9b4-75d78dcdd91e', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '5', + 'revision_id' => '8', + 'uuid' => '3797f5de-116b-4d75-b7e3-5206e6f97c41', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '6', + 'revision_id' => '10', + 'uuid' => '8d9b11c1-8ddf-4c61-bb8d-9ac724e28d9e', + 'langcode' => 'en', + )) + ->execute(); + +$connection->insert('content_moderation_state_field_data') + ->fields(array( + 'id', + 'revision_id', + 'langcode', + 'uid', + 'workflow', + 'moderation_state', + 'content_entity_type_id', + 'content_entity_id', + 'content_entity_revision_id', + 'default_langcode', + 'revision_translation_affected', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '2', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'draft', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '1', + 'content_entity_revision_id' => '2', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '2', + 'content_entity_revision_id' => '3', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'archived', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '3', + 'content_entity_revision_id' => '5', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '4', + 'revision_id' => '7', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'draft', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '1', + 'content_entity_revision_id' => '2', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '5', + 'revision_id' => '8', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '2', + 'content_entity_revision_id' => '3', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '6', + 'revision_id' => '10', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'archived', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '3', + 'content_entity_revision_id' => '5', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->execute(); + +$connection->insert('content_moderation_state_field_revision') + ->fields(array( + 'id', + 'revision_id', + 'langcode', + 'uid', + 'workflow', + 'moderation_state', + 'content_entity_type_id', + 'content_entity_id', + 'content_entity_revision_id', + 'default_langcode', + 'revision_translation_affected', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '1', + 'content_entity_revision_id' => '1', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '2', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'draft', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '1', + 'content_entity_revision_id' => '2', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '2', + 'content_entity_revision_id' => '3', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '4', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '3', + 'content_entity_revision_id' => '4', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'archived', + 'content_entity_type_id' => 'node', + 'content_entity_id' => '3', + 'content_entity_revision_id' => '5', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '4', + 'revision_id' => '6', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '1', + 'content_entity_revision_id' => '1', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '4', + 'revision_id' => '7', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'draft', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '1', + 'content_entity_revision_id' => '2', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '5', + 'revision_id' => '8', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '2', + 'content_entity_revision_id' => '3', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '6', + 'revision_id' => '9', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'published', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '3', + 'content_entity_revision_id' => '4', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'id' => '6', + 'revision_id' => '10', + 'langcode' => 'en', + 'uid' => '0', + 'workflow' => 'editorial', + 'moderation_state' => 'archived', + 'content_entity_type_id' => 'block_content', + 'content_entity_id' => '3', + 'content_entity_revision_id' => '5', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->execute(); + +$connection->insert('content_moderation_state_revision') + ->fields(array( + 'id', + 'revision_id', + 'langcode', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '1', + 'revision_id' => '2', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '2', + 'revision_id' => '3', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '4', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '3', + 'revision_id' => '5', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '4', + 'revision_id' => '6', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '4', + 'revision_id' => '7', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '5', + 'revision_id' => '8', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '6', + 'revision_id' => '9', + 'langcode' => 'en', + )) + ->values(array( + 'id' => '6', + 'revision_id' => '10', + 'langcode' => 'en', + )) + ->execute(); + +$connection->insert('key_value') + ->fields(array( + 'collection', + 'name', + 'value', + )) + ->values(array( + 'collection' => 'config.entity.key_store.block_content_type', + 'name' => 'uuid:966baba6-525e-48fe-b8c5-a5f131b1857f', + 'value' => 'a:1:{i:0;s:37:"block_content.type.test_block_content";}', + )) + ->execute(); + +$connection->insert('node') + ->fields(array( + 'nid', + 'vid', + 'type', + 'uuid', + 'langcode', + )) + ->values(array( + 'nid' => '1', + 'vid' => '1', + 'type' => 'article', + 'uuid' => '11143847-fe18-4808-a797-8b15966adf4c', + 'langcode' => 'en', + )) + ->values(array( + 'nid' => '2', + 'vid' => '3', + 'type' => 'article', + 'uuid' => '336e6941-9340-419e-a763-65d4c11ea031', + 'langcode' => 'en', + )) + ->values(array( + 'nid' => '3', + 'vid' => '5', + 'type' => 'article', + 'uuid' => '3eebe337-f977-4a32-94d2-4095947f125d', + 'langcode' => 'en', + )) + ->execute(); + +$connection->insert('node_field_data') + ->fields(array( + 'nid', + 'vid', + 'type', + 'langcode', + 'status', + 'title', + 'uid', + 'created', + 'changed', + 'promote', + 'sticky', + 'default_langcode', + 'revision_translation_affected', + )) + ->values(array( + 'nid' => '1', + 'vid' => '1', + 'type' => 'article', + 'langcode' => 'en', + 'status' => '1', + 'title' => 'draft pending revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'nid' => '2', + 'vid' => '3', + 'type' => 'article', + 'langcode' => 'en', + 'status' => '1', + 'title' => 'published default revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'nid' => '3', + 'vid' => '5', + 'type' => 'article', + 'langcode' => 'en', + 'status' => '0', + 'title' => 'archived default revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->execute(); + +$connection->insert('node_field_revision') + ->fields(array( + 'nid', + 'vid', + 'langcode', + 'status', + 'title', + 'uid', + 'created', + 'changed', + 'promote', + 'sticky', + 'default_langcode', + 'revision_translation_affected', + )) + ->values(array( + 'nid' => '1', + 'vid' => '1', + 'langcode' => 'en', + 'status' => '1', + 'title' => 'draft pending revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'nid' => '1', + 'vid' => '2', + 'langcode' => 'en', + 'status' => '0', + 'title' => 'draft pending revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'nid' => '2', + 'vid' => '3', + 'langcode' => 'en', + 'status' => '1', + 'title' => 'published default revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'nid' => '3', + 'vid' => '4', + 'langcode' => 'en', + 'status' => '1', + 'title' => 'archived default revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->values(array( + 'nid' => '3', + 'vid' => '5', + 'langcode' => 'en', + 'status' => '0', + 'title' => 'archived default revision', + 'uid' => '0', + 'created' => '1517725800', + 'changed' => '1517725800', + 'promote' => '1', + 'sticky' => '0', + 'default_langcode' => '1', + 'revision_translation_affected' => '1', + )) + ->execute(); + +$connection->insert('node_revision') + ->fields(array( + 'nid', + 'vid', + 'langcode', + 'revision_uid', + 'revision_timestamp', + 'revision_log', + )) + ->values(array( + 'nid' => '1', + 'vid' => '1', + 'langcode' => 'en', + 'revision_uid' => '0', + 'revision_timestamp' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'nid' => '1', + 'vid' => '2', + 'langcode' => 'en', + 'revision_uid' => '0', + 'revision_timestamp' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'nid' => '2', + 'vid' => '3', + 'langcode' => 'en', + 'revision_uid' => '0', + 'revision_timestamp' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'nid' => '3', + 'vid' => '4', + 'langcode' => 'en', + 'revision_uid' => '0', + 'revision_timestamp' => '1517725800', + 'revision_log' => NULL, + )) + ->values(array( + 'nid' => '3', + 'vid' => '5', + 'langcode' => 'en', + 'revision_uid' => '0', + 'revision_timestamp' => '1517725800', + 'revision_log' => NULL, + )) + ->execute(); diff --git a/core/modules/content_moderation/tests/src/Functional/DefaultContentModerationStateRevisionUpdateTest.php b/core/modules/content_moderation/tests/src/Functional/DefaultContentModerationStateRevisionUpdateTest.php new file mode 100644 index 0000000..6ec6b05 --- /dev/null +++ b/core/modules/content_moderation/tests/src/Functional/DefaultContentModerationStateRevisionUpdateTest.php @@ -0,0 +1,101 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz', + __DIR__ . '/../../fixtures/update/drupal-8.4.0-content_moderation_installed.php', + __DIR__ . '/../../fixtures/update/drupal-8.default-cms-entity-id-2941736.php', + ]; + } + + /** + * Test updating the default revision. + */ + public function testUpdateDefaultRevision() { + $this->runUpdates(); + + foreach (['node', 'block_content'] as $entity_type_id) { + $draft_pending_revision = $this->getEntityByLabel($entity_type_id, 'draft pending revision'); + $this->assertFalse($draft_pending_revision->isLatestRevision()); + $this->assertCompositeEntityMatchesDefaultRevisionId($draft_pending_revision); + + $published_default_revision = $this->getEntityByLabel($entity_type_id, 'published default revision'); + $this->assertTrue($published_default_revision->isLatestRevision()); + $this->assertCompositeEntityMatchesDefaultRevisionId($published_default_revision); + + $archived_default_revision = $this->getEntityByLabel($entity_type_id, 'archived default revision'); + $this->assertTrue($archived_default_revision->isLatestRevision()); + $this->assertCompositeEntityMatchesDefaultRevisionId($archived_default_revision); + } + } + + /** + * Assert for the given entity, the default revision ID matches. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity to use for the assertion. + */ + protected function assertCompositeEntityMatchesDefaultRevisionId(ContentEntityInterface $entity) { + $entity_type_manager = $this->container->get('entity_type.manager'); + $entity_list = $entity_type_manager->getStorage('content_moderation_state') + ->loadByProperties([ + 'content_entity_type_id' => $entity->getEntityTypeId(), + 'content_entity_id' => $entity->id(), + ]); + $content_moderation_state_entity = array_shift($entity_list); + $this->assertEquals($entity->getLoadedRevisionId(), $content_moderation_state_entity->content_entity_revision_id->value); + + // Check that the data table records were updated correctly. + /** @var \Drupal\Core\Database\Connection $database */ + $database = $this->container->get('database'); + $query = 'SELECT * FROM {content_moderation_state_field_data} WHERE id = :id'; + $records = $database->query($query, [':id' => $content_moderation_state_entity->id()]) + ->fetchAllAssoc('langcode'); + foreach ($records as $langcode => $record) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $translation */ + $translation = $content_moderation_state_entity->getTranslation($langcode); + foreach ((array) $record as $field_name => $value) { + if ($translation->hasField($field_name)) { + $items = $translation->get($field_name)->getValue(); + $this->assertEquals(current($items[0]), $value); + } + } + } + } + + /** + * Load an entity by label. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $label + * The label of the entity to load. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * The loaded entity. + */ + protected function getEntityByLabel($entity_type_id, $label) { + $entity_type_manager = $this->container->get('entity_type.manager'); + $label_field = $entity_type_manager->getDefinition($entity_type_id)->getKey('label'); + $entity_list = $entity_type_manager->getStorage($entity_type_id) + ->loadByProperties([$label_field => $label]); + return array_shift($entity_list); + } + +} diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php index 13fd0c0..bb9e92d 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationFormTest.php @@ -2,7 +2,9 @@ namespace Drupal\Tests\content_moderation\Functional; +use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\workflows\Entity\Workflow; +use Drupal\Core\Url; /** * Tests the moderation form, specifically on nodes. @@ -79,6 +81,21 @@ public function testModerationForm() { $this->assertResponse(200); $this->assertField('edit-new-state', 'The node view page has a moderation form.'); + // Preview the draft. + $this->drupalPostForm($edit_path, [ + 'body[0][value]' => 'Second version of the content.', + 'moderation_state[0][state]' => 'draft', + ], t('Preview')); + + // The preview view should not have a moderation form. + $preview_url = Url::fromRoute('entity.node.preview', [ + 'node_preview' => $node->uuid(), + 'view_mode_id' => 'full', + ]); + $this->assertResponse(200); + $this->assertUrl($preview_url); + $this->assertNoField('edit-new-state', 'The node preview page has no moderation form.'); + // The latest version page should not show, because there is still no // pending revision. $this->drupalGet($latest_version_path); @@ -295,14 +312,6 @@ public function testContentTranslationNodeForm() { $this->drupalGet($latest_version_path, ['language' => $french]); $this->assertTrue($this->xpath('//ul[@class="entity-moderation-form"]')); - // It should not be possible to add a new english revision. - $this->drupalGet($edit_path); - $this->assertSession()->fieldNotExists('moderation_state[0][state]'); - $this->assertSession()->pageTextContains('Unable to save this Moderated content.'); - - $this->clickLink('Publish'); - $this->assertSession()->fieldValueEquals('body[0][value]', 'Third version of the content.'); - $this->drupalGet($edit_path); $this->clickLink('Delete'); $this->assertSession()->buttonExists('Delete'); @@ -323,7 +332,7 @@ public function testContentTranslationNodeForm() { $this->drupalGet($latest_version_path, ['language' => $french]); $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]')); - // Now we can publish the english (revision 5). + // Publish the English pending revision (revision 5). $this->drupalGet($edit_path); $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); $this->assertSession()->optionExists('moderation_state[0][state]', 'published'); @@ -336,13 +345,13 @@ public function testContentTranslationNodeForm() { $this->drupalGet($latest_version_path); $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]')); - // Make sure we're allowed to create a pending french revision. + // Make sure we are allowed to create a pending French revision. $this->drupalGet($edit_path, ['language' => $french]); $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); $this->assertSession()->optionExists('moderation_state[0][state]', 'published'); $this->assertSession()->optionExists('moderation_state[0][state]', 'archived'); - // Add a english pending revision (revision 6). + // Add an English pending revision (revision 6). $this->drupalGet($edit_path); $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); $this->assertSession()->optionExists('moderation_state[0][state]', 'published'); @@ -354,16 +363,10 @@ public function testContentTranslationNodeForm() { $this->drupalGet($latest_version_path); $this->assertTrue($this->xpath('//ul[@class="entity-moderation-form"]')); - - // Make sure we're not allowed to create a pending french revision. - $this->drupalGet($edit_path, ['language' => $french]); - $this->assertSession()->fieldNotExists('moderation_state[0][state]'); - $this->assertSession()->pageTextContains('Unable to save this Moderated content.'); - $this->drupalGet($latest_version_path, ['language' => $french]); $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]')); - // We should be able to publish the english pending revision (revision 7) + // Publish the English pending revision (revision 7) $this->drupalGet($edit_path); $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); $this->assertSession()->optionExists('moderation_state[0][state]', 'published'); @@ -376,44 +379,17 @@ public function testContentTranslationNodeForm() { $this->drupalGet($latest_version_path); $this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]')); - // Make sure we're allowed to create a pending french revision. + // Make sure we are allowed to create a pending French revision. $this->drupalGet($edit_path, ['language' => $french]); $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); $this->assertSession()->optionExists('moderation_state[0][state]', 'published'); $this->assertSession()->optionExists('moderation_state[0][state]', 'archived'); - // Make sure we're allowed to create a pending english revision. - $this->drupalGet($edit_path); - $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); - $this->assertSession()->optionExists('moderation_state[0][state]', 'published'); - $this->assertSession()->optionExists('moderation_state[0][state]', 'archived'); - - // Create new moderated content. (revision 1). - $this->drupalPostForm('node/add/moderated_content', [ - 'title[0][value]' => 'Second moderated content', - 'body[0][value]' => 'First version of the content.', - 'moderation_state[0][state]' => 'published', - ], t('Save')); - - $node = $this->drupalGetNodeByTitle('Second moderated content'); - $this->assertTrue($node->language(), 'en'); - $edit_path = sprintf('node/%d/edit', $node->id()); - $translate_path = sprintf('node/%d/translations/add/en/fr', $node->id()); - - // Add a pending revision (revision 2). + // Make sure we are allowed to create a pending English revision. $this->drupalGet($edit_path); $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); $this->assertSession()->optionExists('moderation_state[0][state]', 'published'); $this->assertSession()->optionExists('moderation_state[0][state]', 'archived'); - $this->drupalPostForm(NULL, [ - 'body[0][value]' => 'Second version of the content.', - 'moderation_state[0][state]' => 'draft', - ], t('Save')); - - // It shouldn't be possible to translate as we have a pending revision. - $this->drupalGet($translate_path); - $this->assertSession()->fieldNotExists('moderation_state[0][state]'); - $this->assertSession()->pageTextContains('Unable to save this Moderated content.'); // Create new moderated content (revision 1). $this->drupalPostForm('node/add/moderated_content', [ @@ -444,11 +420,6 @@ public function testContentTranslationNodeForm() { 'moderation_state[0][state]' => 'draft', ], t('Save (this translation)')); - // Editing the original translation should not be possible. - $this->drupalGet($edit_path); - $this->assertSession()->fieldNotExists('moderation_state[0][state]'); - $this->assertSession()->pageTextContains('Unable to save this Moderated content.'); - // Updating and publishing the french translation is still possible. $this->drupalGet($edit_path, ['language' => $french]); $this->assertSession()->optionExists('moderation_state[0][state]', 'draft'); @@ -469,6 +440,23 @@ public function testContentTranslationNodeForm() { } /** + * Test the moderation_state field when an alternative widget is set. + */ + public function testAlternativeModerationStateWidget() { + $entity_form_display = EntityFormDisplay::load('node.moderated_content.default'); + $entity_form_display->setComponent('moderation_state', [ + 'type' => 'string_textfield', + 'region' => 'content', + ]); + $entity_form_display->save(); + $this->drupalPostForm('node/add/moderated_content', [ + 'title[0][value]' => 'Test content', + 'moderation_state[0][value]' => 'published', + ], 'Save'); + $this->assertSession()->pageTextContains('Moderated content Test content has been created.'); + } + + /** * Tests that workflows and states can not be deleted if they are in use. * * @covers \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowHasData diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php index 3104450..28de50c 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationLocaleTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\content_moderation\Functional; +use Drupal\node\NodeInterface; + /** * Test content_moderation functionality with localization and translation. * @@ -22,19 +24,23 @@ class ModerationLocaleTest extends ModerationStateTestBase { ]; /** - * Tests article translations can be moderated separately. + * {@inheritdoc} */ - public function testTranslateModeratedContent() { + protected function setUp() { + parent::setUp(); + $this->drupalLogin($this->rootUser); // Enable moderation on Article node type. $this->createContentTypeFromUi('Article', 'article', TRUE); - // Add French language. - $edit = [ - 'predefined_langcode' => 'fr', - ]; - $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + // Add French and Italian languages. + foreach (['fr', 'it'] as $langcode) { + $edit = [ + 'predefined_langcode' => $langcode, + ]; + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + } // Enable content translation on articles. $this->drupalGet('admin/config/regional/content-language'); @@ -48,7 +54,12 @@ public function testTranslateModeratedContent() { // Adding languages requires a container rebuild in the test running // environment so that multilingual services are used. $this->rebuildContainer(); + } + /** + * Tests article translations can be moderated separately. + */ + public function testTranslateModeratedContent() { // Create a published article in English. $edit = [ 'title[0][value]' => 'Published English node', @@ -191,4 +202,358 @@ public function testTranslateModeratedContent() { $this->assertFalse($french_node->isPublished()); } + /** + * Tests that individual translations can be moderated independently. + */ + public function testLanguageIndependentContentModeration() { + // Create a published article in English (revision 1). + $this->drupalGet('node/add/article'); + $node = $this->submitNodeForm('Test 1.1 EN', 'published'); + $this->assertNotLatestVersionPage($node); + + $edit_path = $node->toUrl('edit-form'); + $translate_path = $node->toUrl('drupal:content-translation-overview'); + + // Create a new English draft (revision 2). + $this->drupalGet($edit_path); + $this->submitNodeForm('Test 1.2 EN', 'draft', TRUE); + $this->assertLatestVersionPage($node); + + // Add a French translation draft (revision 3). + $this->drupalGet($translate_path); + $this->clickLink(t('Add')); + $this->submitNodeForm('Test 1.3 FR', 'draft'); + $fr_node = $this->loadTranslation($node, 'fr'); + $this->assertLatestVersionPage($fr_node); + $this->assertModerationForm($node); + + // Add an Italian translation draft (revision 4). + $this->drupalGet($translate_path); + $this->clickLink(t('Add')); + $this->submitNodeForm('Test 1.4 IT', 'draft'); + $it_node = $this->loadTranslation($node, 'it'); + $this->assertLatestVersionPage($it_node); + $this->assertModerationForm($node); + $this->assertModerationForm($fr_node); + + // Publish the English draft (revision 5). + $this->drupalGet($edit_path); + $this->submitNodeForm('Test 1.5 EN', 'published', TRUE); + $this->assertNotLatestVersionPage($node); + $this->assertModerationForm($fr_node); + $this->assertModerationForm($it_node); + + // Publish the Italian draft (revision 6). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 2); + $this->submitNodeForm('Test 1.6 IT', 'published'); + $this->assertNotLatestVersionPage($it_node); + $this->assertNoModerationForm($node); + $this->assertModerationForm($fr_node); + + // Publish the French draft (revision 7). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 1); + $this->submitNodeForm('Test 1.7 FR', 'published'); + $this->assertNotLatestVersionPage($fr_node); + $this->assertNoModerationForm($node); + $this->assertNoModerationForm($it_node); + + // Create an Italian draft (revision 8). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 2); + $this->submitNodeForm('Test 1.8 IT', 'draft'); + $this->assertLatestVersionPage($it_node); + $this->assertNoModerationForm($node); + $this->assertNoModerationForm($fr_node); + + // Create a French draft (revision 9). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 1); + $this->submitNodeForm('Test 1.9 FR', 'draft'); + $this->assertLatestVersionPage($fr_node); + $this->assertNoModerationForm($node); + $this->assertModerationForm($it_node); + + // Create an English draft (revision 10). + $this->drupalGet($edit_path); + $this->submitNodeForm('Test 1.10 EN', 'draft'); + $this->assertLatestVersionPage($node); + $this->assertModerationForm($fr_node); + $this->assertModerationForm($it_node); + + // Now start from a draft article in English (revision 1). + $this->drupalGet('node/add/article'); + $node2 = $this->submitNodeForm('Test 2.1 EN', 'draft', TRUE); + $this->assertNotLatestVersionPage($node2, TRUE); + + $edit_path = $node2->toUrl('edit-form'); + $translate_path = $node2->toUrl('drupal:content-translation-overview'); + + // Add a French translation (revision 2). + $this->drupalGet($translate_path); + $this->clickLink(t('Add')); + $this->submitNodeForm('Test 2.2 FR', 'draft'); + $fr_node2 = $this->loadTranslation($node2, 'fr'); + $this->assertNotLatestVersionPage($fr_node2, TRUE); + $this->assertModerationForm($node2, FALSE); + + // Add an Italian translation (revision 3). + $this->drupalGet($translate_path); + $this->clickLink(t('Add')); + $this->submitNodeForm('Test 2.3 IT', 'draft'); + $it_node2 = $this->loadTranslation($node2, 'it'); + $this->assertNotLatestVersionPage($it_node2, TRUE); + $this->assertModerationForm($node2, FALSE); + $this->assertModerationForm($fr_node2, FALSE); + + // Publish the English draft (revision 4). + $this->drupalGet($edit_path); + $this->submitNodeForm('Test 2.4 EN', 'published', TRUE); + $this->assertNotLatestVersionPage($node2); + $this->assertModerationForm($fr_node2, FALSE); + $this->assertModerationForm($it_node2, FALSE); + + // Publish the Italian draft (revision 5). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 2); + $this->submitNodeForm('Test 2.5 IT', 'published'); + $this->assertNotLatestVersionPage($it_node2); + $this->assertNoModerationForm($node2); + $this->assertModerationForm($fr_node2, FALSE); + + // Publish the French draft (revision 6). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 1); + $this->submitNodeForm('Test 2.6 FR', 'published'); + $this->assertNotLatestVersionPage($fr_node2); + $this->assertNoModerationForm($node2); + $this->assertNoModerationForm($it_node2); + + // Now that all revision translations are published, verify that the + // moderation form is never displayed on revision pages. + /** @var \Drupal\node\NodeStorageInterface $storage */ + $storage = $this->container->get('entity_type.manager')->getStorage('node'); + foreach (range(11, 16) as $revision_id) { + /** @var \Drupal\node\NodeInterface $revision */ + $revision = $storage->loadRevision($revision_id); + foreach ($revision->getTranslationLanguages() as $langcode => $language) { + if ($revision->isRevisionTranslationAffected()) { + $this->drupalGet($revision->toUrl('revision')); + $this->assertFalse($this->hasModerationForm(), 'Moderation form is not displayed correctly for revision ' . $revision_id); + break; + } + } + } + + // Create an Italian draft (revision 7). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 2); + $this->submitNodeForm('Test 2.7 IT', 'draft'); + $this->assertLatestVersionPage($it_node2); + $this->assertNoModerationForm($node2); + $this->assertNoModerationForm($fr_node2); + + // Create a French draft (revision 8). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 1); + $this->submitNodeForm('Test 2.8 FR', 'draft'); + $this->assertLatestVersionPage($fr_node2); + $this->assertNoModerationForm($node2); + $this->assertModerationForm($it_node2); + + // Create an English draft (revision 9). + $this->drupalGet($edit_path); + $this->submitNodeForm('Test 2.9 EN', 'draft', TRUE); + $this->assertLatestVersionPage($node2); + $this->assertModerationForm($fr_node2); + $this->assertModerationForm($it_node2); + + // Now publish a draft in another language first and verify that the + // moderation form is not displayed on the English node view page. + $this->drupalGet('node/add/article'); + $node3 = $this->submitNodeForm('Test 3.1 EN', 'published'); + $this->assertNotLatestVersionPage($node3); + + $edit_path = $node3->toUrl('edit-form'); + $translate_path = $node3->toUrl('drupal:content-translation-overview'); + + // Create an English draft (revision 2). + $this->drupalGet($edit_path); + $this->submitNodeForm('Test 3.2 EN', 'draft', TRUE); + $this->assertLatestVersionPage($node3); + + // Add a French translation (revision 3). + $this->drupalGet($translate_path); + $this->clickLink(t('Add')); + $this->submitNodeForm('Test 3.3 FR', 'draft'); + $fr_node3 = $this->loadTranslation($node3, 'fr'); + $this->assertLatestVersionPage($fr_node3); + $this->assertModerationForm($node3); + + // Publish the French draft (revision 4). + $this->drupalGet($translate_path); + $this->clickLink(t('Edit'), 1); + $this->submitNodeForm('Test 3.4 FR', 'published'); + $this->assertNotLatestVersionPage($fr_node3); + $this->assertModerationForm($node3); + } + + /** + * Checks that new translation values are populated properly. + */ + public function testNewTranslationSourceValues() { + // Create a published article in Italian (revision 1). + $this->drupalGet('node/add/article'); + $node = $this->submitNodeForm('Test 1.1 IT', 'published', TRUE, 'it'); + $this->assertNotLatestVersionPage($node); + + // Create a new draft (revision 2). + $this->drupalGet($node->toUrl('edit-form')); + $this->submitNodeForm('Test 1.2 IT', 'draft', TRUE); + $this->assertLatestVersionPage($node); + + // Create an English draft (revision 3) and verify that the Italian draft + // values are used as source values. + $url = $node->toUrl('drupal:content-translation-add'); + $url->setRouteParameter('source', 'it'); + $url->setRouteParameter('target', 'en'); + $this->drupalGet($url); + $this->assertSession()->pageTextContains('Test 1.2 IT'); + $this->submitNodeForm('Test 1.3 EN', 'draft'); + $this->assertLatestVersionPage($node); + + // Create a French draft (without saving) and verify that the Italian draft + // values are used as source values. + $url->setRouteParameter('target', 'fr'); + $this->drupalGet($url); + $this->assertSession()->pageTextContains('Test 1.2 IT'); + + // Now switch source language and verify that the English draft values are + // used as source values. + $url->setRouteParameter('source', 'en'); + $this->drupalGet($url); + $this->assertSession()->pageTextContains('Test 1.3 EN'); + } + + /** + * Submits the node form at the current URL with the specified values. + * + * @param string $title + * The node title. + * @param string $moderation_state + * The moderation state. + * @param bool $default_translation + * (optional) Whether we are editing the default translation. + * @param string|null $langcode + * (optional) The node language. Defaults to English. + * + * @return \Drupal\node\NodeInterface|null + * A node object if a new one is being created, NULL otherwise. + */ + protected function submitNodeForm($title, $moderation_state, $default_translation = FALSE, $langcode = 'en') { + $is_new = strpos($this->getSession()->getCurrentUrl(), '/node/add/') !== FALSE; + $edit = [ + 'title[0][value]' => $title, + 'moderation_state[0][state]' => $moderation_state, + ]; + if ($is_new) { + $default_translation = TRUE; + $edit['langcode[0][value]'] = $langcode; + } + $submit = $default_translation ? t('Save') : t('Save (this translation)'); + $this->drupalPostForm(NULL, $edit, $submit); + $message = $is_new ? "Article $title has been created." : "Article $title has been updated."; + $this->assertSession()->pageTextContains($message); + return $is_new ? $this->drupalGetNodeByTitle($title) : NULL; + } + + /** + * Loads the node translation for the specified language. + * + * @param \Drupal\node\NodeInterface $node + * A node object. + * @param string $langcode + * The translation language code. + * + * @return \Drupal\node\NodeInterface + * The node translation object. + */ + protected function loadTranslation(NodeInterface $node, $langcode) { + /** @var \Drupal\node\NodeStorageInterface $storage */ + $storage = $this->container->get('entity_type.manager')->getStorage('node'); + /** @var \Drupal\node\NodeInterface $node */ + $node = $storage->loadRevision($storage->getLatestRevisionId($node->id())); + return $node->getTranslation($langcode); + } + + /** + * Asserts that this is the "latest version" page for the specified node. + * + * @param \Drupal\node\NodeInterface $node + * A node object. + */ + public function assertLatestVersionPage(NodeInterface $node) { + $this->assertEquals($node->toUrl('latest-version')->setAbsolute()->toString(), $this->getSession()->getCurrentUrl()); + $this->assertModerationForm($node); + } + + /** + * Asserts that this is not the "latest version" page for the specified node. + * + * @param \Drupal\node\NodeInterface $node + * A node object. + * @param bool $moderation_form + * (optional) Whether the page should contain the moderation form. Defaults + * to FALSE. + */ + public function assertNotLatestVersionPage(NodeInterface $node, $moderation_form = FALSE) { + $this->assertNotEquals($node->toUrl('latest-version')->setAbsolute()->toString(), $this->getSession()->getCurrentUrl()); + if ($moderation_form) { + $this->assertModerationForm($node, FALSE); + } + else { + $this->assertNoModerationForm($node); + } + } + + /** + * Asserts that the moderation form is displayed for the specified node. + * + * @param \Drupal\node\NodeInterface $node + * A node object. + * @param bool $latest_tab + * (optional) Whether the node form is expected to be displayed on the + * latest version page or on the node view page. Defaults to the former. + */ + public function assertModerationForm(NodeInterface $node, $latest_tab = TRUE) { + $this->drupalGet($node->toUrl()); + $this->assertEquals(!$latest_tab, $this->hasModerationForm()); + $this->drupalGet($node->toUrl('latest-version')); + $this->assertEquals($latest_tab, $this->hasModerationForm()); + } + + /** + * Asserts that the moderation form is not displayed for the specified node. + * + * @param \Drupal\node\NodeInterface $node + * A node object. + */ + public function assertNoModerationForm(NodeInterface $node) { + $this->drupalGet($node->toUrl()); + $this->assertFalse($this->hasModerationForm()); + $this->drupalGet($node->toUrl('latest-version')); + $this->assertEquals(403, $this->getSession()->getStatusCode()); + } + + /** + * Checks whether the page contains the moderation form. + * + * @return bool + * TRUE if the moderation form could be find in the page, FALSE otherwise. + */ + public function hasModerationForm() { + return (bool) $this->xpath('//ul[@class="entity-moderation-form"]'); + } + } diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationRevisionRevertTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationRevisionRevertTest.php index 703c10f..479e9df 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationRevisionRevertTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationRevisionRevertTest.php @@ -38,6 +38,10 @@ public function setUp() { $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated_bundle'); $workflow->save(); + /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */ + $router_builder = $this->container->get('router.builder'); + $router_builder->rebuildIfNeeded(); + $admin = $this->drupalCreateUser([ 'access content overview', 'administer nodes', diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php index 5ee72ee..712732f 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateTestBase.php @@ -132,6 +132,9 @@ public function enableModerationThroughUi($content_type_id, $workflow_id = 'edit // @see content_moderation_workflow_insert() \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */ + $router_builder = $this->container->get('router.builder'); + $router_builder->rebuildIfNeeded(); } /** diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateResourceTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateResourceTest.php index cc09e59..2ea1d8f 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateResourceTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateResourceTest.php @@ -18,7 +18,7 @@ class ContentModerationStateResourceTest extends KernelTestBase { public static $modules = ['serialization', 'rest', 'content_moderation']; /** - * @see content_moderation_rest_resource_alter() + * @see \Drupal\content_moderation\Entity\ContentModerationState */ public function testCreateContentModerationStateResource() { $this->setExpectedException(PluginNotFoundException::class, 'The "entity:content_moderation_state" plugin does not exist.'); diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php index 00e3644..ef63472 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php @@ -174,12 +174,11 @@ public function basicModerationTestCases() { } /** - * Tests removal of content moderation state entity field data. + * Tests removal of content moderation state entity. * * @dataProvider basicModerationTestCases */ public function testContentModerationStateDataRemoval($entity_type_id) { - // Test content moderation state deletion. /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $this->createEntity($entity_type_id); $entity->save(); @@ -187,44 +186,80 @@ public function testContentModerationStateDataRemoval($entity_type_id) { $entity->delete(); $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); $this->assertFalse($content_moderation_state); + } - // Test content moderation state revision deletion. - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity2 */ - $entity2 = $this->createEntity($entity_type_id); - $entity2->save(); - $revision = clone $entity2; + /** + * Tests removal of content moderation state entity revisions. + * + * @dataProvider basicModerationTestCases + */ + public function testContentModerationStateRevisionDataRemoval($entity_type_id) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $this->createEntity($entity_type_id); + $entity->save(); + $revision = clone $entity; $revision->isDefaultRevision(FALSE); $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision); $this->assertTrue($content_moderation_state); - $entity2 = $this->reloadEntity($entity2); - $entity2->setNewRevision(TRUE); - $entity2->save(); + $entity = $this->reloadEntity($entity); + $entity->setNewRevision(TRUE); + $entity->save(); $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); $entity_storage->deleteRevision($revision->getRevisionId()); $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision); $this->assertFalse($content_moderation_state); - $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity2); + $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); $this->assertTrue($content_moderation_state); + } + /** + * Tests removal of content moderation state pending entity revisions. + * + * @dataProvider basicModerationTestCases + */ + public function testContentModerationStatePendingRevisionDataRemoval($entity_type_id) { + $entity = $this->createEntity($entity_type_id); + $entity->moderation_state = 'published'; + $entity->save(); + $entity->setNewRevision(TRUE); + $entity->moderation_state = 'draft'; + $entity->save(); + + $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); + $this->assertTrue($content_moderation_state); + + $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); + $entity_storage->deleteRevision($entity->getRevisionId()); + + $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); + $this->assertFalse($content_moderation_state); + } + + /** + * Tests removal of content moderation state translations. + * + * @dataProvider basicModerationTestCases + */ + public function testContentModerationStateTranslationDataRemoval($entity_type_id) { // Test content moderation state translation deletion. if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) { - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity3 */ - $entity3 = $this->createEntity($entity_type_id); + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $this->createEntity($entity_type_id); $langcode = 'it'; ConfigurableLanguage::createFromLangcode($langcode) ->save(); - $entity3->save(); - $translation = $entity3->addTranslation($langcode, ['title' => 'Titolo test']); + $entity->save(); + $translation = $entity->addTranslation($langcode, ['title' => 'Titolo test']); // Make sure we add values for all of the required fields. if ($entity_type_id == 'block_content') { $translation->info = $this->randomString(); } $translation->save(); - $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3); + $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); $this->assertTrue($content_moderation_state->hasTranslation($langcode)); - $entity3->removeTranslation($langcode); - $entity3->save(); - $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3); + $entity->removeTranslation($langcode); + $entity->save(); + $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity); $this->assertFalse($content_moderation_state->hasTranslation($langcode)); } } @@ -537,6 +572,48 @@ public function testWorkflowNonConfigBundleDependencies() { } /** + * Test the revision default state of the moderation state entity revisions. + * + * @param string $entity_type_id + * The ID of entity type to be tested. + * + * @dataProvider basicModerationTestCases + */ + public function testRevisionDefaultState($entity_type_id) { + // Check that the revision default state of the moderated entity and the + // content moderation state entity always match. + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity_type_id); + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $cms_storage */ + $cms_storage = $this->entityTypeManager->getStorage('content_moderation_state'); + + $entity = $this->createEntity($entity_type_id); + $entity->get('moderation_state')->value = 'published'; + $storage->save($entity); + /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */ + $cms_entity = $cms_storage->loadUnchanged(1); + $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value); + + $entity->get('moderation_state')->value = 'published'; + $storage->save($entity); + /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */ + $cms_entity = $cms_storage->loadUnchanged(1); + $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value); + + $entity->get('moderation_state')->value = 'draft'; + $storage->save($entity); + /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */ + $cms_entity = $cms_storage->loadUnchanged(1); + $this->assertEquals($entity->getLoadedRevisionId() - 1, $cms_entity->get('content_entity_revision_id')->value); + + $entity->get('moderation_state')->value = 'published'; + $storage->save($entity); + /** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */ + $cms_entity = $cms_storage->loadUnchanged(1); + $this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value); + } + + /** * Creates an entity. * * The entity will have required fields populated and the corresponding bundle diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php index d72000b..aeed848 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php @@ -181,7 +181,7 @@ public function testInvalidStateMultilingual() { ]); $node->save(); - $node_fr = $node->addTranslation('fr'); + $node_fr = $node->addTranslation('fr', $node->toArray()); $node_fr->setTitle('French Published Node'); $node_fr->save(); $this->assertEquals('published', $node_fr->moderation_state->value); @@ -207,7 +207,7 @@ public function testInvalidStateMultilingual() { $this->assertCount(0, $violations); // From the latest french revision, there should also be no violation. - $node_fr = $node->getTranslation('fr'); + $node_fr = Node::load($node->id())->getTranslation('fr'); $this->assertEquals('published', $node_fr->moderation_state->value); $node_fr->moderation_state = 'archived'; $violations = $node_fr->validate(); diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php index 6281ab8..7d95054 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php @@ -43,6 +43,10 @@ protected function setUp() { $this->installEntitySchema('content_moderation_state'); $this->installConfig('content_moderation'); + NodeType::create([ + 'type' => 'unmoderated', + ])->save(); + $node_type = NodeType::create([ 'type' => 'example', ]); @@ -80,6 +84,51 @@ public function testArrayIteration() { } /** + * @covers ::getValue + */ + public function testGetValue() { + $this->assertEquals([['value' => 'draft']], $this->testNode->moderation_state->getValue()); + } + + /** + * @covers ::get + */ + public function testGet() { + $this->assertEquals('draft', $this->testNode->moderation_state->get(0)->value); + $this->setExpectedException(\InvalidArgumentException::class); + $this->testNode->moderation_state->get(2); + } + + /** + * Tests the computed field when it is unset or set to an empty value. + */ + public function testSetEmptyState() { + $this->testNode->moderation_state->value = ''; + $this->assertEquals('draft', $this->testNode->moderation_state->value); + + $this->testNode->moderation_state = ''; + $this->assertEquals('draft', $this->testNode->moderation_state->value); + + unset($this->testNode->moderation_state); + $this->assertEquals('draft', $this->testNode->moderation_state->value); + } + + /** + * Test the list class with a non moderated entity. + */ + public function testNonModeratedEntity() { + $unmoderated_node = Node::create([ + 'type' => 'unmoderated', + 'title' => 'Test title', + ]); + $unmoderated_node->save(); + $this->assertEquals(0, $unmoderated_node->moderation_state->count()); + + $unmoderated_node->moderation_state = NULL; + $this->assertEquals(0, $unmoderated_node->moderation_state->count()); + } + + /** * Tests that moderation state changes also change the related entity state. */ public function testModerationStateChanges() { diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php index a1b396f..00e1072 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php @@ -108,14 +108,12 @@ public function testContentModerationStateBaseJoin() { $expected_result = [ [ 'nid' => $node->id(), - // @todo I would have expected that the content_moderation_state default - // revision is the same one as in the node, but it isn't. // Joins from the base table to the default revision of the // content_moderation. - 'moderation_state' => 'draft', + 'moderation_state' => 'published', // Joins from the revision table to the default revision of the // content_moderation. - 'moderation_state_1' => 'draft', + 'moderation_state_1' => 'published', // Joins from the revision table to the revision of the // content_moderation. 'moderation_state_2' => 'published', diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php index 637e70a..da7ac92 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php @@ -121,10 +121,10 @@ public function testStateFilterViewsRelationship() { $translated_forward_revision->moderation_state = 'translated_draft'; $translated_forward_revision->save(); - // Four revisions for the nodes when no filter. - $this->assertNodesWithFilters([$node, $second_node, $third_node, $third_node], []); + // The three default revisions are listed when no filter is specified. + $this->assertNodesWithFilters([$node, $second_node, $third_node], []); - // The default revision of node one and three is published. + // The default revision of node one and three are published. $this->assertNodesWithFilters([$node, $third_node], [ 'default_revision_state' => 'editorial-published', ]); diff --git a/core/modules/content_moderation/tests/src/Unit/ContentModerationRouteSubscriberTest.php b/core/modules/content_moderation/tests/src/Unit/ContentModerationRouteSubscriberTest.php new file mode 100644 index 0000000..e043656 --- /dev/null +++ b/core/modules/content_moderation/tests/src/Unit/ContentModerationRouteSubscriberTest.php @@ -0,0 +1,227 @@ +createMock(EntityTypeManagerInterface::class); + $this->routeSubscriber = new ContentModerationRouteSubscriber($entity_type_manager); + $this->setupEntityTypes(); + } + + /** + * Creates the entity manager mock returning entity type objects. + */ + protected function setupEntityTypes() { + $definition = $this->createMock(EntityTypeInterface::class); + $definition->expects($this->any()) + ->method('getClass') + ->will($this->returnValue(SimpleTestEntity::class)); + $definition->expects($this->any()) + ->method('isRevisionable') + ->willReturn(FALSE); + $revisionable_definition = $this->createMock(EntityTypeInterface::class); + $revisionable_definition->expects($this->any()) + ->method('getClass') + ->will($this->returnValue(SimpleTestEntity::class)); + $revisionable_definition->expects($this->any()) + ->method('isRevisionable') + ->willReturn(TRUE); + $entity_types = [ + 'entity_test' => $definition, + 'entity_test_rev' => $revisionable_definition, + ]; + + $reflector = new \ReflectionProperty($this->routeSubscriber, 'moderatedEntityTypes'); + $reflector->setAccessible(TRUE); + $reflector->setValue($this->routeSubscriber, $entity_types); + } + + /** + * Data provider for ::testSetLatestRevisionFlag. + */ + public function setLatestRevisionFlagTestCases() { + return [ + 'Entity parameter not on an entity form' => [ + [], + [ + 'entity_test' => [ + 'type' => 'entity:entity_test_rev', + ], + ], + ], + 'Entity parameter on an entity form' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + ], + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => TRUE, + ], + ], + ], + 'Entity form with no operation' => [ + [ + '_entity_form' => 'entity_test_rev' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + ], + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => TRUE, + ], + ], + ], + 'Non-moderated entity form' => [ + [ + '_entity_form' => 'entity_test_mulrev' + ], + [ + 'entity_test_mulrev' => [ + 'type' => 'entity:entity_test_mulrev', + ], + ], + ], + 'Multiple entity parameters on an entity form' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + ], + 'node' => [ + 'type' => 'entity:node', + ], + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => TRUE, + ], + 'node' => [ + 'type' => 'entity:node', + ], + ], + ], + 'Overridden load_latest_revision flag does not change' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => FALSE, + ], + ], + ], + 'Non-revisionable entity type will not change' => [ + [ + '_entity_form' => 'entity_test.edit' + ], + [ + 'entity_test' => [ + 'type' => 'entity:entity_test', + ], + ], + FALSE, + FALSE, + ], + 'Overridden load_latest_revision flag does not change with multiple parameters' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + ], + 'node' => [ + 'type' => 'entity:node', + 'load_latest_revision' => FALSE, + ], + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => TRUE, + ], + 'node' => [ + 'type' => 'entity:node', + 'load_latest_revision' => FALSE, + ], + ], + ], + ]; + } + + /** + * Tests that the "load_latest_revision" flag is handled correctly. + * + * @param array $defaults + * The route defaults. + * @param array $parameters + * The route parameters. + * @param array|bool $expected_parameters + * (optional) The expected route parameters. Defaults to FALSE. + * + * @covers ::setLatestRevisionFlag + * + * @dataProvider setLatestRevisionFlagTestCases + */ + public function testSetLatestRevisionFlag($defaults, $parameters, $expected_parameters = FALSE) { + $route = new Route('/foo/{entity_test}', $defaults, [], [ + 'parameters' => $parameters, + ]); + + $route_collection = new RouteCollection(); + $route_collection->add('test', $route); + $event = new RouteBuildEvent($route_collection); + $this->routeSubscriber->onAlterRoutes($event); + + // If expected parameters have not been provided, assert they are unchanged. + $this->assertEquals($expected_parameters ?: $parameters, $route->getOption('parameters')); + } + +} + +/** + * A concrete entity. + */ +class SimpleTestEntity extends Entity { +} diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 48feccc..1b09b39 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -60,6 +60,14 @@ function content_translation_module_implements_alter(&$implementations, $hook) { unset($implementations['content_translation']); $implementations['content_translation'] = $group; break; + + // Move our hook_entity_bundle_info_alter() implementation to the top of the + // list, so that any other hook implementation can rely on bundles being + // correctly marked as translatable. + case 'entity_bundle_info_alter': + $group = $implementations['content_translation']; + $implementations = ['content_translation' => $group] + $implementations; + break; } } @@ -155,6 +163,8 @@ function content_translation_entity_type_alter(array &$entity_types) { } $entity_type->set('translation', $translation); } + + $entity_type->addConstraint('ContentTranslationSynchronizedFields'); } } @@ -422,6 +432,11 @@ function content_translation_form_field_config_edit_form_alter(array &$form, For */ function content_translation_entity_presave(EntityInterface $entity) { if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew()) { + /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */ + $manager = \Drupal::service('content_translation.manager'); + if (!$manager->isEnabled($entity->getEntityTypeId(), $entity->bundle())) { + return; + } // If we are creating a new translation we need to use the source language // as original language, since source values are the only ones available to // compare against. @@ -430,8 +445,6 @@ function content_translation_entity_presave(EntityInterface $entity) { ->getStorage($entity->entityType())->loadUnchanged($entity->id()); } $langcode = $entity->language()->getId(); - /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */ - $manager = \Drupal::service('content_translation.manager'); $source_langcode = !$entity->original->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL; \Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode); } diff --git a/core/modules/content_translation/content_translation.services.yml b/core/modules/content_translation/content_translation.services.yml index f7cc11f..066142f 100644 --- a/core/modules/content_translation/content_translation.services.yml +++ b/core/modules/content_translation/content_translation.services.yml @@ -1,7 +1,7 @@ services: content_translation.synchronizer: class: Drupal\content_translation\FieldTranslationSynchronizer - arguments: ['@entity.manager'] + arguments: ['@entity.manager', '@plugin.manager.field.field_type'] content_translation.subscriber: class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index cb6776f..2ba76e7 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -688,7 +688,7 @@ public function entityFormSubmit($form, FormStateInterface $form_state) { // after the entity has been validated, so that it does not break the // EntityChanged constraint validator. The content translation metadata // field for the changed timestamp does not have such a constraint defined - // at the moment, but it is correct to update it's value in a submission + // at the moment, but it is correct to update its value in a submission // handler as well and have the same logic like in the Form API. if ($entity->hasField('content_translation_changed')) { $metadata = $this->manager->getTranslationMetadata($entity); diff --git a/core/modules/content_translation/src/ContentTranslationManager.php b/core/modules/content_translation/src/ContentTranslationManager.php index 8b3831a..3a21f94 100644 --- a/core/modules/content_translation/src/ContentTranslationManager.php +++ b/core/modules/content_translation/src/ContentTranslationManager.php @@ -145,4 +145,21 @@ protected function loadContentLanguageSettings($entity_type_id, $bundle) { return $config; } + /** + * Checks whether support for pending revisions should be enabled. + * + * @return bool + * TRUE if pending revisions should be enabled, FALSE otherwise. + * + * @internal + * There is ongoing discussion about how pending revisions should behave. + * The logic enabling pending revision support is likely to change once a + * decision is made. + * + * @see https://www.drupal.org/node/2940575 + */ + public static function isPendingRevisionSupportEnabled() { + return \Drupal::moduleHandler()->moduleExists('content_moderation'); + } + } diff --git a/core/modules/content_translation/src/Controller/ContentTranslationController.php b/core/modules/content_translation/src/Controller/ContentTranslationController.php index 190778d..e556154 100644 --- a/core/modules/content_translation/src/Controller/ContentTranslationController.php +++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php @@ -2,6 +2,7 @@ namespace Drupal\content_translation\Controller; +use Drupal\content_translation\ContentTranslationManager; use Drupal\content_translation\ContentTranslationManagerInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Controller\ControllerBase; @@ -87,6 +88,7 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL $handler = $this->entityManager()->getHandler($entity_type_id, 'translation'); $manager = $this->manager; $entity_type = $entity->getEntityType(); + $use_latest_revisions = $entity_type->isRevisionable() && ContentTranslationManager::isPendingRevisionSupportEnabled(); // Start collecting the cacheability metadata, starting with the entity and // later merge in the access result cacheability metadata. @@ -99,6 +101,9 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL $rows = []; $show_source_column = FALSE; + $default_revision = $entity; + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager()->getStorage($entity_type_id); if ($this->languageManager()->isMultilingual()) { // Determine whether the current entity is translatable. @@ -121,6 +126,16 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL $language_name = $language->getName(); $langcode = $language->getId(); + // If the entity type is revisionable, we may have pending revisions + // with translations not available yet in the default revision. Thus we + // need to load the latest translation-affecting revision for each + // language to be sure we are listing all available translations. + if ($use_latest_revisions) { + $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode); + $entity = $latest_revision_id ? $storage->loadRevision($latest_revision_id) : $default_revision; + $translations = $entity->getTranslationLanguages(); + } + $add_url = new Url( "entity.$entity_type_id.content_translation_add", [ @@ -330,8 +345,21 @@ public function overview(RouteMatchInterface $route_match, $entity_type_id = NUL * A processed form array ready to be rendered. */ public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $route_match->getParameter($entity_type_id); + // In case of a pending revision, make sure we load the latest + // translation-affecting revision for the source language, otherwise the + // initial form values may not be up-to-date. + if (!$entity->isDefaultRevision() && ContentTranslationManager::isPendingRevisionSupportEnabled()) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId()); + $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $source->getId()); + if ($revision_id != $entity->getRevisionId()) { + $entity = $storage->loadRevision($revision_id); + } + } + // @todo Exploit the upcoming hook_entity_prepare() when available. // See https://www.drupal.org/node/1810394. $this->prepareTranslation($entity, $source, $target); diff --git a/core/modules/content_translation/src/FieldTranslationSynchronizer.php b/core/modules/content_translation/src/FieldTranslationSynchronizer.php index 13b805d..3f3595e 100644 --- a/core/modules/content_translation/src/FieldTranslationSynchronizer.php +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php @@ -5,6 +5,8 @@ use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; /** * Provides field translation synchronization capabilities. @@ -19,13 +21,56 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf protected $entityManager; /** + * The field type plugin manager. + * + * @var \Drupal\Core\Field\FieldTypePluginManagerInterface + */ + protected $fieldTypeManager; + + /** * Constructs a FieldTranslationSynchronizer object. * * @param \Drupal\Core\Entity\EntityManagerInterface $entityManager * The entity manager. + * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager + * The field type plugin manager. */ - public function __construct(EntityManagerInterface $entityManager) { + public function __construct(EntityManagerInterface $entityManager, FieldTypePluginManagerInterface $field_type_manager) { $this->entityManager = $entityManager; + $this->fieldTypeManager = $field_type_manager; + } + + /** + * {@inheritdoc} + */ + public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition) { + $properties = []; + $settings = $this->getFieldSynchronizationSettings($field_definition); + foreach ($settings as $group => $translatable) { + if (!$translatable) { + $field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType()); + if (!empty($field_type_definition['column_groups'][$group]['columns'])) { + $properties = array_merge($properties, $field_type_definition['column_groups'][$group]['columns']); + } + } + } + return $properties; + } + + /** + * Returns the synchronization settings for the specified field. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * A field definition. + * + * @return string[] + * An array of synchronized field property names. + */ + protected function getFieldSynchronizationSettings(FieldDefinitionInterface $field_definition) { + if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable()) { + return $field_definition->getThirdPartySetting('content_translation', 'translation_sync', []); + } + return []; } /** @@ -33,7 +78,6 @@ public function __construct(EntityManagerInterface $entityManager) { */ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) { $translations = $entity->getTranslationLanguages(); - $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); // If we have no information about what to sync to, if we are creating a new // entity, if we have no translations for the current entity and we are not @@ -43,21 +87,55 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode } // If the entity language is being changed there is nothing to synchronize. - $entity_type = $entity->getEntityTypeId(); - $entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id()); + $entity_unchanged = $this->getOriginalEntity($entity); if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) { return; } + if ($entity->isNewRevision()) { + if ($entity->isDefaultTranslationAffectedOnly()) { + // If changes to untranslatable fields are configured to affect only the + // default translation, we need to skip synchronization in pending + // revisions, otherwise multiple translations would be affected. + if (!$entity->isDefaultRevision()) { + return; + } + // When this mode is enabled, changes to synchronized properties are + // allowed only in the default translation, thus we need to make sure this + // is always used as source for the synchronization process. + else { + $sync_langcode = $entity->getUntranslated()->language()->getId(); + } + } + elseif ($entity->isDefaultRevision()) { + // If a new default revision is being saved, but a newer default + // revision was created meanwhile, use any other translation as source + // for synchronization, since that will have been merged from the + // default revision. In this case the actual language does not matter as + // synchronized properties are the same for all the translations in the + // default revision. + /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */ + $default_revision = $this->entityManager + ->getStorage($entity->getEntityTypeId()) + ->load($entity->id()); + if ($default_revision->getLoadedRevisionId() !== $entity->getLoadedRevisionId()) { + $other_langcodes = array_diff_key($default_revision->getTranslationLanguages(), [$sync_langcode => FALSE]); + if ($other_langcodes) { + $sync_langcode = key($other_langcodes); + } + } + } + } + /** @var \Drupal\Core\Field\FieldItemListInterface $items */ foreach ($entity as $field_name => $items) { $field_definition = $items->getFieldDefinition(); - $field_type_definition = $field_type_manager->getDefinition($field_definition->getType()); + $field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType()); $column_groups = $field_type_definition['column_groups']; // Sync if the field is translatable, not empty, and the synchronization // setting is enabled. - if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) { + if (($translation_sync = $this->getFieldSynchronizationSettings($field_definition)) && !$items->isEmpty()) { // Retrieve all the untranslatable column groups and merge them into // single list. $groups = array_keys(array_diff($translation_sync, array_filter($translation_sync))); @@ -102,6 +180,26 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode } /** + * Returns the original unchanged entity to be used to detect changes. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being changed. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * The unchanged entity. + */ + protected function getOriginalEntity(ContentEntityInterface $entity) { + if (!isset($entity->original)) { + $storage = $this->entityManager->getStorage($entity->getEntityTypeId()); + $original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId()); + } + else { + $original = $entity->original; + } + return $original; + } + + /** * {@inheritdoc} */ public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $columns) { @@ -174,9 +272,7 @@ public function synchronizeItems(array &$values, array $unchanged_items, $sync_l // items and the other columns from the existing values. This only // works if the delta exists in the language. elseif ($created && !empty($original_field_values[$langcode][$delta])) { - $item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($columns)); - $item_columns_to_keep = array_diff_key($original_field_values[$langcode][$delta], array_flip($columns)); - $values[$langcode][$delta] = $item_columns_to_sync + $item_columns_to_keep; + $values[$langcode][$delta] = $this->createMergedItem($source_items[$delta], $original_field_values[$langcode][$delta], $columns); } // If the delta doesn't exist, copy from the source language. elseif ($created) { @@ -190,7 +286,11 @@ public function synchronizeItems(array &$values, array $unchanged_items, $sync_l // If the value has only been reordered we just move the old one in // the new position. $item = isset($original_field_values[$langcode][$old_delta]) ? $original_field_values[$langcode][$old_delta] : $source_items[$new_delta]; - $values[$langcode][$new_delta] = $item; + // When saving a default revision starting from a pending revision, + // we may have desynchronized field values, so we make sure that + // untranslatable properties are synchronized, even if in any other + // situation this would not be necessary. + $values[$langcode][$new_delta] = $this->createMergedItem($source_items[$new_delta], $item, $columns); } } } @@ -198,6 +298,26 @@ public function synchronizeItems(array &$values, array $unchanged_items, $sync_l } /** + * Creates a merged item. + * + * @param array $source_item + * An item containing the untranslatable properties to be synchronized. + * @param array $target_item + * An item containing the translatable properties to be kept. + * @param string[] $properties + * An array of properties to be synchronized. + * + * @return array + * A merged item array. + */ + protected function createMergedItem(array $source_item, array $target_item, array $properties) { + $property_keys = array_flip($properties); + $item_properties_to_sync = array_intersect_key($source_item, $property_keys); + $item_properties_to_keep = array_diff_key($target_item, $property_keys); + return $item_properties_to_sync + $item_properties_to_keep; + } + + /** * Computes a hash code for the specified item. * * @param array $items diff --git a/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php b/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php index 88cb921..a07ac59 100644 --- a/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php +++ b/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php @@ -3,6 +3,7 @@ namespace Drupal\content_translation; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; /** * Provides field translation synchronization capabilities. @@ -54,4 +55,15 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode */ public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $columns); + /** + * Returns the synchronized properties for the specified field definition. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * A field definition. + * + * @return string[] + * An array of synchronized field property names. + */ + public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition); + } diff --git a/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraint.php b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraint.php new file mode 100644 index 0000000..0d2bc52 --- /dev/null +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraint.php @@ -0,0 +1,25 @@ +entityTypeManager = $entity_type_manager; + $this->contentTranslationManager = $content_translation_manager; + $this->synchronizer = $synchronizer; + } + + /** + * [@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('content_translation.manager'), + $container->get('content_translation.synchronizer') + ); + } + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + /** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */ + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $value; + if ($entity->isNew() || !$entity->getEntityType()->isRevisionable()) { + return; + } + // When changes to untranslatable fields are configured to affect all + // revision translations, we always allow changes in default revisions. + if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) { + return; + } + $entity_type_id = $entity->getEntityTypeId(); + if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) { + return; + } + $synchronized_properties = $this->getSynchronizedPropertiesByField($entity->getFieldDefinitions()); + if (!$synchronized_properties) { + return; + } + + /** @var \Drupal\Core\Entity\ContentEntityInterface $original */ + $original = $this->getOriginalEntity($entity); + $original_translation = $this->getOriginalTranslation($entity, $original); + if ($this->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) { + if ($entity->isDefaultTranslationAffectedOnly()) { + foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) { + if ($entity->getTranslation($langcode)->hasTranslationChanges()) { + $this->context->addViolation($constraint->defaultTranslationMessage); + break; + } + } + } + else { + $this->context->addViolation($constraint->defaultRevisionMessage); + } + } + } + + /** + * Checks whether any synchronized property has changes. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being validated. + * @param \Drupal\Core\Entity\ContentEntityInterface $original + * The original unchanged entity. + * @param string[][] $synchronized_properties + * An associative array of arrays of synchronized field properties keyed by + * field name. + * + * @return bool + * TRUE if changes in synchronized properties were detected, FALSE + * otherwise. + */ + protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) { + foreach ($synchronized_properties as $field_name => $properties) { + foreach ($properties as $property) { + $items = $entity->get($field_name)->getValue(); + $original_items = $original->get($field_name)->getValue(); + if (count($items) !== count($original_items)) { + return TRUE; + } + foreach ($items as $delta => $item) { + // @todo This loose comparison is not fully reliable. Revisit this + // after https://www.drupal.org/project/drupal/issues/2941092. + if ($items[$delta][$property] != $original_items[$delta][$property]) { + return TRUE; + } + } + } + } + return FALSE; + } + + /** + * Returns the original unchanged entity to be used to detect changes. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being changed. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * The unchanged entity. + */ + protected function getOriginalEntity(ContentEntityInterface $entity) { + if (!isset($entity->original)) { + $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + $original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId()); + } + else { + $original = $entity->original; + } + return $original; + } + + /** + * Returns the original translation. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being validated. + * @param \Drupal\Core\Entity\ContentEntityInterface $original + * The original entity. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * The original entity translation object. + */ + protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) { + $langcode = $entity->language()->getId(); + if ($original->hasTranslation($langcode)) { + $original_langcode = $langcode; + } + else { + $metadata = $this->contentTranslationManager->getTranslationMetadata($entity); + $original_langcode = $metadata->getSource(); + } + return $original->getTranslation($original_langcode); + } + + /** + * Returns the synchronized properties for every specified field. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions + * An array of field definitions. + * + * @return string[][] + * An associative array of arrays of field property names keyed by field + * name. + */ + public function getSynchronizedPropertiesByField(array $field_definitions) { + $synchronizer = $this->synchronizer; + $synchronized_properties = array_filter(array_map( + function (FieldDefinitionInterface $field_definition) use ($synchronizer) { + return $synchronizer->getFieldSynchronizedProperties($field_definition); + }, + $field_definitions + )); + return $synchronized_properties; + } + +} diff --git a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php index 6aae426..ac979d3 100644 --- a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php +++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\content_translation\Routing; +use Drupal\content_translation\ContentTranslationManager; use Drupal\content_translation\ContentTranslationManagerInterface; use Drupal\Core\Routing\RouteSubscriberBase; use Drupal\Core\Routing\RoutingEvents; @@ -55,6 +56,7 @@ protected function alterRoutes(RouteCollection $collection) { } $path = $base_path . '/translations'; + $load_latest_revision = ContentTranslationManager::isPendingRevisionSupportEnabled(); $route = new Route( $path, @@ -70,6 +72,7 @@ protected function alterRoutes(RouteCollection $collection) { 'parameters' => [ $entity_type_id => [ 'type' => 'entity:' . $entity_type_id, + 'load_latest_revision' => $load_latest_revision, ], ], '_admin_route' => $is_admin, @@ -102,6 +105,7 @@ protected function alterRoutes(RouteCollection $collection) { ], $entity_type_id => [ 'type' => 'entity:' . $entity_type_id, + 'load_latest_revision' => $load_latest_revision, ], ], '_admin_route' => $is_admin, @@ -127,6 +131,7 @@ protected function alterRoutes(RouteCollection $collection) { ], $entity_type_id => [ 'type' => 'entity:' . $entity_type_id, + 'load_latest_revision' => $load_latest_revision, ], ], '_admin_route' => $is_admin, @@ -152,6 +157,7 @@ protected function alterRoutes(RouteCollection $collection) { ], $entity_type_id => [ 'type' => 'entity:' . $entity_type_id, + 'load_latest_revision' => $load_latest_revision, ], ], '_admin_route' => $is_admin, diff --git a/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module b/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module index e231807..50495a8 100644 --- a/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module +++ b/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module @@ -5,7 +5,39 @@ * Helper module for the Content Translation tests. */ +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; + +/** + * Implements hook_entity_bundle_info_alter(). + */ +function content_translation_test_entity_bundle_info_alter(&$bundles) { + // Store the initial status of the "translatable" property for the + // "entity_test_mul" bundle. + $translatable = !empty($bundles['entity_test_mul']['entity_test_mul']['translatable']); + \Drupal::state()->set('content_translation_test.translatable', $translatable); + // Make it translatable if Content Translation did not. This will make the + // entity object translatable even if it is disabled in Content Translation + // settings. + if (!$translatable) { + $bundles['entity_test_mul']['entity_test_mul']['translatable'] = TRUE; + } +} + +/** + * Implements hook_entity_access(). + */ +function content_translation_test_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + $access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId()); + if (!empty($access[$operation])) { + return AccessResult::allowed(); + } + else { + return AccessResult::neutral(); + } +} /** * Implements hook_form_BASE_FORM_ID_alter(). diff --git a/core/modules/content_translation/tests/src/Kernel/ContentTranslationEntityBundleInfoTest.php b/core/modules/content_translation/tests/src/Kernel/ContentTranslationEntityBundleInfoTest.php new file mode 100644 index 0000000..dcd596ec --- /dev/null +++ b/core/modules/content_translation/tests/src/Kernel/ContentTranslationEntityBundleInfoTest.php @@ -0,0 +1,90 @@ +contentTranslationManager = $this->container->get('content_translation.manager'); + $this->bundleInfo = $this->container->get('entity_type.bundle.info'); + + $this->installEntitySchema('entity_test_mul'); + + ConfigurableLanguage::createFromLangcode('it')->save(); + } + + /** + * Tests that modules can know whether bundles are translatable. + */ + public function testHookInvocationOrder() { + $this->contentTranslationManager->setEnabled('entity_test_mul', 'entity_test_mul', TRUE); + $this->bundleInfo->clearCachedBundles(); + $this->bundleInfo->getAllBundleInfo(); + + // Verify that the test module comes first in the module list, which would + // normally make its hook implementation to be invoked first. + /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */ + $module_handler = $this->container->get('module_handler'); + $module_list = $module_handler->getModuleList(); + $expected_modules = [ + 'content_translation_test', + 'content_translation', + ]; + $actual_modules = array_keys(array_intersect_key($module_list, array_flip($expected_modules))); + $this->assertEquals($expected_modules, $actual_modules); + + // Check that the "content_translation_test" hook implementation has access + // to the "translatable" bundle info property. + /** @var \Drupal\Core\State\StateInterface $state */ + $state = $this->container->get('state'); + $this->assertTrue($state->get('content_translation_test.translatable')); + } + + /** + * Tests that field synchronization is skipped for disabled bundles. + */ + public function testFieldSynchronizationWithDisabledBundle() { + $entity = EntityTestMul::create(); + $entity->save(); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $translation */ + $translation = $entity->addTranslation('it'); + $translation->save(); + + $this->assertTrue($entity->isTranslatable()); + } + +} diff --git a/core/modules/content_translation/tests/src/Kernel/ContentTranslationFieldSyncRevisionTest.php b/core/modules/content_translation/tests/src/Kernel/ContentTranslationFieldSyncRevisionTest.php new file mode 100644 index 0000000..8185182 --- /dev/null +++ b/core/modules/content_translation/tests/src/Kernel/ContentTranslationFieldSyncRevisionTest.php @@ -0,0 +1,482 @@ +installEntitySchema($entity_type_id); + $this->installEntitySchema('file'); + $this->installSchema('file', ['file_usage']); + + ConfigurableLanguage::createFromLangcode('it')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + /** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */ + $field_storage_config = FieldStorageConfig::create([ + 'field_name' => $this->fieldName, + 'type' => 'image', + 'entity_type' => $entity_type_id, + 'cardinality' => 1, + 'translatable' => 1, + ]); + $field_storage_config->save(); + + $field_config = FieldConfig::create([ + 'entity_type' => $entity_type_id, + 'field_name' => $this->fieldName, + 'bundle' => $entity_type_id, + 'label' => 'Synchronized field', + 'translatable' => 1, + ]); + $field_config->save(); + + $property_settings = [ + 'alt' => 'alt', + 'title' => 'title', + 'file' => 0, + ]; + $field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings); + $field_config->save(); + + $this->entityManager->clearCachedDefinitions(); + + $this->contentTranslationManager = $this->container->get('content_translation.manager'); + $this->contentTranslationManager->setEnabled($entity_type_id, $entity_type_id, TRUE); + + $this->storage = $this->entityManager->getStorage($entity_type_id); + + foreach ($this->getTestFiles('image') as $file) { + $entity = File::create((array) $file + ['status' => 1]); + $entity->save(); + } + + $this->state->set('content_translation.entity_access.file', ['view' => TRUE]); + + $account = User::create([ + 'name' => $this->randomMachineName(), + 'status' => 1, + ]); + $account->save(); + } + + /** + * Checks that field synchronization works as expected with revisions. + * + * @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::create + * @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::validate + * @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::hasSynchronizedPropertyChanges + * @covers \Drupal\content_translation\FieldTranslationSynchronizer::getFieldSynchronizedProperties + * @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeFields + * @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeItems + */ + public function testFieldSynchronizationAndValidation() { + // Test that when untranslatable field widgets are displayed, synchronized + // field properties can be changed only in default revisions. + $this->setUntranslatableFieldWidgetsDisplay(TRUE); + $entity = $this->saveNewEntity(); + $entity_id = $entity->id(); + $this->assertLatestRevisionFieldValues($entity_id, [1, 1, 1, 'Alt 1 EN']); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */ + $en_revision = $this->createRevision($entity, FALSE); + $en_revision->get($this->fieldName)->target_id = 2; + $violations = $en_revision->validate(); + $this->assertViolations($violations); + + $it_translation = $entity->addTranslation('it', $entity->toArray()); + /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */ + $it_revision = $this->createRevision($it_translation, FALSE); + $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision); + $metadata->setSource('en'); + $it_revision->get($this->fieldName)->target_id = 2; + $it_revision->get($this->fieldName)->alt = 'Alt 2 IT'; + $violations = $it_revision->validate(); + $this->assertViolations($violations); + $it_revision->isDefaultRevision(TRUE); + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [2, 2, 2, 'Alt 1 EN', 'Alt 2 IT']); + + $en_revision = $this->createRevision($en_revision, FALSE); + $en_revision->get($this->fieldName)->alt = 'Alt 3 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [3, 2, 2, 'Alt 3 EN', 'Alt 2 IT']); + + $it_revision = $this->createRevision($it_revision, FALSE); + $it_revision->get($this->fieldName)->alt = 'Alt 4 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [4, 2, 2, 'Alt 1 EN', 'Alt 4 IT']); + + $en_revision = $this->createRevision($en_revision); + $en_revision->get($this->fieldName)->alt = 'Alt 5 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [5, 2, 2, 'Alt 5 EN', 'Alt 2 IT']); + + $en_revision = $this->createRevision($en_revision); + $en_revision->get($this->fieldName)->target_id = 6; + $en_revision->get($this->fieldName)->alt = 'Alt 6 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [6, 6, 6, 'Alt 6 EN', 'Alt 2 IT']); + + $it_revision = $this->createRevision($it_revision); + $it_revision->get($this->fieldName)->alt = 'Alt 7 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [7, 6, 6, 'Alt 6 EN', 'Alt 7 IT']); + + // Test that when untranslatable field widgets are hidden, synchronized + // field properties can be changed only when editing the default + // translation. This may lead to temporarily desynchronized values, when + // saving a pending revision for the default translation that changes a + // synchronized property (see revision 11). + $this->setUntranslatableFieldWidgetsDisplay(FALSE); + $entity = $this->saveNewEntity(); + $entity_id = $entity->id(); + $this->assertLatestRevisionFieldValues($entity_id, [8, 1, 1, 'Alt 1 EN']); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */ + $en_revision = $this->createRevision($entity, FALSE); + $en_revision->get($this->fieldName)->target_id = 2; + $en_revision->get($this->fieldName)->alt = 'Alt 2 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [9, 2, 2, 'Alt 2 EN']); + + $it_translation = $entity->addTranslation('it', $entity->toArray()); + /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */ + $it_revision = $this->createRevision($it_translation, FALSE); + $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision); + $metadata->setSource('en'); + $it_revision->get($this->fieldName)->target_id = 3; + $violations = $it_revision->validate(); + $this->assertViolations($violations); + $it_revision->isDefaultRevision(TRUE); + $violations = $it_revision->validate(); + $this->assertViolations($violations); + + $it_revision = $this->createRevision($it_translation); + $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision); + $metadata->setSource('en'); + $it_revision->get($this->fieldName)->alt = 'Alt 3 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [10, 1, 1, 'Alt 1 EN', 'Alt 3 IT']); + + $en_revision = $this->createRevision($en_revision, FALSE); + $en_revision->get($this->fieldName)->alt = 'Alt 4 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [11, 2, 1, 'Alt 4 EN', 'Alt 3 IT']); + + $it_revision = $this->createRevision($it_revision, FALSE); + $it_revision->get($this->fieldName)->alt = 'Alt 5 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [12, 1, 1, 'Alt 1 EN', 'Alt 5 IT']); + + $en_revision = $this->createRevision($en_revision); + $en_revision->get($this->fieldName)->target_id = 6; + $en_revision->get($this->fieldName)->alt = 'Alt 6 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [13, 6, 6, 'Alt 6 EN', 'Alt 3 IT']); + + $it_revision = $this->createRevision($it_revision); + $it_revision->get($this->fieldName)->target_id = 7; + $violations = $it_revision->validate(); + $this->assertViolations($violations); + + $it_revision = $this->createRevision($it_revision); + $it_revision->get($this->fieldName)->alt = 'Alt 7 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [14, 6, 6, 'Alt 6 EN', 'Alt 7 IT']); + + // Test that creating a default revision starting from a pending revision + // having changes to synchronized properties, without introducing new + // changes works properly. + $this->setUntranslatableFieldWidgetsDisplay(FALSE); + $entity = $this->saveNewEntity(); + $entity_id = $entity->id(); + $this->assertLatestRevisionFieldValues($entity_id, [15, 1, 1, 'Alt 1 EN']); + + $it_translation = $entity->addTranslation('it', $entity->toArray()); + /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */ + $it_revision = $this->createRevision($it_translation); + $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision); + $metadata->setSource('en'); + $it_revision->get($this->fieldName)->alt = 'Alt 2 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [16, 1, 1, 'Alt 1 EN', 'Alt 2 IT']); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */ + $en_revision = $this->createRevision($entity); + $en_revision->get($this->fieldName)->target_id = 3; + $en_revision->get($this->fieldName)->alt = 'Alt 3 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [17, 3, 3, 'Alt 3 EN', 'Alt 2 IT']); + + $en_revision = $this->createRevision($entity, FALSE); + $en_revision->get($this->fieldName)->target_id = 4; + $en_revision->get($this->fieldName)->alt = 'Alt 4 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [18, 4, 3, 'Alt 4 EN', 'Alt 2 IT']); + + $en_revision = $this->createRevision($entity); + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [19, 4, 4, 'Alt 4 EN', 'Alt 2 IT']); + + $it_revision = $this->createRevision($it_revision); + $it_revision->get($this->fieldName)->alt = 'Alt 6 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [20, 4, 4, 'Alt 4 EN', 'Alt 6 IT']); + + // Check that we are not allowed to perform changes to multiple translations + // in pending revisions when synchronized properties are involved. + $this->setUntranslatableFieldWidgetsDisplay(FALSE); + $entity = $this->saveNewEntity(); + $entity_id = $entity->id(); + $this->assertLatestRevisionFieldValues($entity_id, [21, 1, 1, 'Alt 1 EN']); + + $it_translation = $entity->addTranslation('it', $entity->toArray()); + /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */ + $it_revision = $this->createRevision($it_translation); + $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision); + $metadata->setSource('en'); + $it_revision->get($this->fieldName)->alt = 'Alt 2 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [22, 1, 1, 'Alt 1 EN', 'Alt 2 IT']); + + $en_revision = $this->createRevision($entity, FALSE); + $en_revision->get($this->fieldName)->target_id = 2; + $en_revision->getTranslation('it')->get($this->fieldName)->alt = 'Alt 3 IT'; + $violations = $en_revision->validate(); + $this->assertViolations($violations); + + // Test that when saving a new default revision starting from a pending + // revision, outdated synchronized properties do not override more recent + // ones. + $this->setUntranslatableFieldWidgetsDisplay(TRUE); + $entity = $this->saveNewEntity(); + $entity_id = $entity->id(); + $this->assertLatestRevisionFieldValues($entity_id, [23, 1, 1, 'Alt 1 EN']); + + $it_translation = $entity->addTranslation('it', $entity->toArray()); + /** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */ + $it_revision = $this->createRevision($it_translation, FALSE); + $metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision); + $metadata->setSource('en'); + $it_revision->get($this->fieldName)->alt = 'Alt 2 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [24, 1, 1, 'Alt 1 EN', 'Alt 2 IT']); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */ + $en_revision = $this->createRevision($entity); + $en_revision->get($this->fieldName)->target_id = 3; + $en_revision->get($this->fieldName)->alt = 'Alt 3 EN'; + $violations = $en_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($en_revision); + $this->assertLatestRevisionFieldValues($entity_id, [25, 3, 3, 'Alt 3 EN', 'Alt 2 IT']); + + $it_revision = $this->createRevision($it_revision); + $it_revision->get($this->fieldName)->alt = 'Alt 4 IT'; + $violations = $it_revision->validate(); + $this->assertEmpty($violations); + $this->storage->save($it_revision); + $this->assertLatestRevisionFieldValues($entity_id, [26, 3, 3, 'Alt 3 EN', 'Alt 4 IT']); + } + + /** + * Sets untranslatable field widgets' display status. + * + * @param bool $display + * Whether untranslatable field widgets should be displayed. + */ + protected function setUntranslatableFieldWidgetsDisplay($display) { + $entity_type_id = $this->storage->getEntityTypeId(); + $settings = ['untranslatable_fields_hide' => !$display]; + $this->contentTranslationManager->setBundleTranslationSettings($entity_type_id, $entity_type_id, $settings); + /** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */ + $bundle_info = $this->container->get('entity_type.bundle.info'); + $bundle_info->clearCachedBundles(); + } + + /** + * @return \Drupal\Core\Entity\ContentEntityInterface + */ + protected function saveNewEntity() { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = EntityTestMulRev::create([ + 'uid' => 1, + 'langcode' => 'en', + $this->fieldName => [ + 'target_id' => 1, + 'alt' => 'Alt 1 EN', + ], + ]); + $metadata = $this->contentTranslationManager->getTranslationMetadata($entity); + $metadata->setSource(LanguageInterface::LANGCODE_NOT_SPECIFIED); + $violations = $entity->validate(); + $this->assertEmpty($violations); + $this->storage->save($entity); + return $entity; + } + + /** + * Creates a new revision starting from the latest translation-affecting one. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $translation + * The translation to be revisioned. + * @param bool $default + * (optional) Whether the new revision should be marked as default. Defaults + * to TRUE. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * An entity revision object. + */ + protected function createRevision(ContentEntityInterface $translation, $default = TRUE) { + if (!$translation->isNewTranslation()) { + $langcode = $translation->language()->getId(); + $revision_id = $this->storage->getLatestTranslationAffectedRevisionId($translation->id(), $langcode); + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + $revision = $this->storage->loadRevision($revision_id); + $translation = $revision->getTranslation($langcode); + } + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + $revision = $this->storage->createRevision($translation, $default); + return $revision; + } + + /** + * Asserts that the expected violations were found. + * + * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations + * A list of violations. + */ + protected function assertViolations(EntityConstraintViolationListInterface $violations) { + $entity_type_id = $this->storage->getEntityTypeId(); + $settings = $this->contentTranslationManager->getBundleTranslationSettings($entity_type_id, $entity_type_id); + $message = !empty($settings['untranslatable_fields_hide']) ? + 'Non-translatable field elements can only be changed when updating the original language.' : + 'Non-translatable field elements can only be changed when updating the current revision.'; + + $list = []; + foreach ($violations as $violation) { + if ((string) $violation->getMessage() === $message) { + $list[] = $violation; + } + } + $this->assertCount(1, $list); + } + + /** + * Asserts that the latest revision has the expected field values. + * + * @param $entity_id + * The entity ID. + * @param array $expected_values + * An array of expected values in the following order: + * - revision ID + * - target ID (en) + * - target ID (it) + * - alt (en) + * - alt (it) + */ + protected function assertLatestRevisionFieldValues($entity_id, array $expected_values) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $this->storage->loadRevision($this->storage->getLatestRevisionId($entity_id)); + @list($revision_id, $target_id_en, $target_id_it, $alt_en, $alt_it) = $expected_values; + $this->assertEquals($revision_id, $entity->getRevisionId()); + $this->assertEquals($target_id_en, $entity->get($this->fieldName)->target_id); + $this->assertEquals($alt_en, $entity->get($this->fieldName)->alt); + if ($entity->hasTranslation('it')) { + $it_translation = $entity->getTranslation('it'); + $this->assertEquals($target_id_it, $it_translation->get($this->fieldName)->target_id); + $this->assertEquals($alt_it, $it_translation->get($this->fieldName)->alt); + } + } + +} diff --git a/core/modules/content_translation/tests/src/Kernel/ContentTranslationSyncUnitTest.php b/core/modules/content_translation/tests/src/Kernel/ContentTranslationSyncUnitTest.php index fa5fd1c..5c4784a 100644 --- a/core/modules/content_translation/tests/src/Kernel/ContentTranslationSyncUnitTest.php +++ b/core/modules/content_translation/tests/src/Kernel/ContentTranslationSyncUnitTest.php @@ -59,7 +59,7 @@ class ContentTranslationSyncUnitTest extends KernelTestBase { protected function setUp() { parent::setUp(); - $this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager')); + $this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'), $this->container->get('plugin.manager.field.field_type')); $this->synchronized = ['sync1', 'sync2']; $this->columns = array_merge($this->synchronized, ['var1', 'var2']); $this->langcodes = ['en', 'it', 'fr', 'de', 'es']; diff --git a/core/modules/dblog/css/dblog.module.css b/core/modules/dblog/css/dblog.module.css index e844e14..1346206 100644 --- a/core/modules/dblog/css/dblog.module.css +++ b/core/modules/dblog/css/dblog.module.css @@ -5,12 +5,12 @@ .dblog-filter-form .form-item-type, .dblog-filter-form .form-item-severity { display: inline-block; - margin: .1em .9em .1em .1em; /* LTR */ + margin: 0.1em 0.9em 0.1em 0.1em; /* LTR */ max-width: 30%; } [dir="rtl"] .dblog-filter-form .form-item-type, [dir="rtl"] .dblog-filter-form .form-item-severity { - margin: .1em .1em .1em .9em; + margin: 0.1em 0.1em 0.1em 0.9em; } .dblog-filter-form .form-actions { display: inline-block; diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 130f923..40b42e1 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -164,6 +164,11 @@ function hook_field_widget_info_alter(array &$info) { /** * Alter forms for field widgets provided by other modules. * + * This hook can only modify individual elements within a field widget and + * cannot alter the top level (parent element) for multi-value fields. In most + * cases, you should use hook_field_widget_multivalue_form_alter() instead and + * loop over the elements. + * * @param $element * The field widget form element as constructed by * \Drupal\Core\Field\WidgetBaseInterface::form(). @@ -183,6 +188,7 @@ function hook_field_widget_info_alter(array &$info) { * @see \Drupal\Core\Field\WidgetBaseInterface::form() * @see \Drupal\Core\Field\WidgetBase::formSingleElement() * @see hook_field_widget_WIDGET_TYPE_form_alter() + * @see hook_field_widget_multivalue_form_alter() */ function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) { // Add a css class to widget form elements for all fields of type mytype. @@ -200,6 +206,11 @@ function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInte * specific widget form, rather than using hook_field_widget_form_alter() and * checking the widget type. * + * This hook can only modify individual elements within a field widget and + * cannot alter the top level (parent element) for multi-value fields. In most + * cases, you should use hook_field_widget_multivalue_WIDGET_TYPE_form_alter() + * instead and loop over the elements. + * * @param $element * The field widget form element as constructed by * \Drupal\Core\Field\WidgetBaseInterface::form(). @@ -212,6 +223,7 @@ function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInte * @see \Drupal\Core\Field\WidgetBaseInterface::form() * @see \Drupal\Core\Field\WidgetBase::formSingleElement() * @see hook_field_widget_form_alter() + * @see hook_field_widget_multivalue_WIDGET_TYPE_form_alter() */ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) { // Code here will only act on widgets of type WIDGET_TYPE. For example, @@ -221,6 +233,74 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, \Drupal\Core\Form\F } /** + * Alter forms for multi-value field widgets provided by other modules. + * + * To alter the individual elements within the widget, loop over + * \Drupal\Core\Render\Element::children($elements). + * + * @param array $elements + * The field widget form elements as constructed by + * \Drupal\Core\Field\WidgetBase::formMultipleElements(). + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $context + * An associative array containing the following key-value pairs: + * - form: The form structure to which widgets are being attached. This may be + * a full form structure, or a sub-element of a larger form. + * - widget: The widget plugin instance. + * - items: The field values, as a + * \Drupal\Core\Field\FieldItemListInterface object. + * - default: A boolean indicating whether the form is being shown as a dummy + * form to set default values. + * + * @see \Drupal\Core\Field\WidgetBaseInterface::form() + * @see \Drupal\Core\Field\WidgetBase::formMultipleElements() + * @see hook_field_widget_multivalue_WIDGET_TYPE_form_alter() + */ +function hook_field_widget_multivalue_form_alter(array &$elements, \Drupal\Core\Form\FormStateInterface $form_state, array $context) { + // Add a css class to widget form elements for all fields of type mytype. + $field_definition = $context['items']->getFieldDefinition(); + if ($field_definition->getType() == 'mytype') { + // Be sure not to overwrite existing attributes. + $elements['#attributes']['class'][] = 'myclass'; + } +} + +/** + * Alter multi-value widget forms for a widget provided by another module. + * + * Modules can implement hook_field_widget_multivalue_WIDGET_TYPE_form_alter() to + * modify a specific widget form, rather than using + * hook_field_widget_form_alter() and checking the widget type. + * + * To alter the individual elements within the widget, loop over + * \Drupal\Core\Render\Element::children($elements). + * + * @param array $elements + * The field widget form elements as constructed by + * \Drupal\Core\Field\WidgetBase::formMultipleElements(). + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $context + * An associative array. See hook_field_widget_multivalue_form_alter() for + * the structure and content of the array. + * + * @see \Drupal\Core\Field\WidgetBaseInterface::form() + * @see \Drupal\Core\Field\WidgetBase::formMultipleElements() + * @see hook_field_widget_multivalue_form_alter() + */ +function hook_field_widget_multivalue_WIDGET_TYPE_form_alter(array &$elements, \Drupal\Core\Form\FormStateInterface $form_state, array $context) { + // Code here will only act on widgets of type WIDGET_TYPE. For example, + // hook_field_widget_multivalue_mymodule_autocomplete_form_alter() will only + // act on widgets of type 'mymodule_autocomplete'. + // Change the autcomplete route for each autocomplete element within the + // multivalue widget. + foreach (Element::children($elements) as $delta => $element) { + $elements[$delta]['#autocomplete_route_name'] = 'mymodule.autocomplete_route'; + } +} + +/** * @} End of "defgroup field_widget". */ diff --git a/core/modules/field/field.install b/core/modules/field/field.install index ecd2b31..80639a0 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -6,6 +6,8 @@ */ use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; /** * Removes the stale 'target_bundle' storage setting on entity_reference fields. @@ -104,3 +106,30 @@ function field_update_8003() { } } } + +/** + * Update the definition of deleted fields. + */ +function field_update_8500() { + $state = \Drupal::state(); + + // Convert the old deleted field definitions from an array to a FieldConfig + // object. + $deleted_field_definitions = $state->get('field.field.deleted', []); + foreach ($deleted_field_definitions as $key => $deleted_field_definition) { + if (is_array($deleted_field_definition)) { + $deleted_field_definitions[$key] = new FieldConfig($deleted_field_definition); + } + } + $state->set('field.field.deleted', $deleted_field_definitions); + + // Convert the old deleted field storage definitions from an array to a + // FieldStorageConfig object. + $deleted_field_storage_definitions = $state->get('field.storage.deleted', []); + foreach ($deleted_field_storage_definitions as $key => $deleted_field_storage_definition) { + if (is_array($deleted_field_storage_definition)) { + $deleted_field_storage_definitions[$key] = new FieldStorageConfig($deleted_field_storage_definition); + } + } + $state->set('field.storage.deleted', $deleted_field_storage_definitions); +} diff --git a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php b/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php deleted file mode 100644 index 7e4d2ff..0000000 --- a/core/modules/field/src/Tests/Boolean/BooleanFieldTest.php +++ /dev/null @@ -1,249 +0,0 @@ -drupalLogin($this->drupalCreateUser([ - 'view test entity', - 'administer entity_test content', - 'administer entity_test form display', - 'administer entity_test fields', - ])); - } - - /** - * Tests boolean field. - */ - public function testBooleanField() { - $on = $this->randomMachineName(); - $off = $this->randomMachineName(); - $label = $this->randomMachineName(); - - // Create a field with settings to validate. - $field_name = Unicode::strtolower($this->randomMachineName()); - $this->fieldStorage = FieldStorageConfig::create([ - 'field_name' => $field_name, - 'entity_type' => 'entity_test', - 'type' => 'boolean', - ]); - $this->fieldStorage->save(); - $this->field = FieldConfig::create([ - 'field_name' => $field_name, - 'entity_type' => 'entity_test', - 'bundle' => 'entity_test', - 'label' => $label, - 'required' => TRUE, - 'settings' => [ - 'on_label' => $on, - 'off_label' => $off, - ], - ]); - $this->field->save(); - - // Create a form display for the default form mode. - entity_get_form_display('entity_test', 'entity_test', 'default') - ->setComponent($field_name, [ - 'type' => 'boolean_checkbox', - ]) - ->save(); - // Create a display for the full view mode. - entity_get_display('entity_test', 'entity_test', 'full') - ->setComponent($field_name, [ - 'type' => 'boolean', - ]) - ->save(); - - // Display creation form. - $this->drupalGet('entity_test/add'); - $this->assertFieldByName("{$field_name}[value]", '', 'Widget found.'); - $this->assertText($this->field->label(), 'Uses field label by default.'); - $this->assertNoRaw($on, 'Does not use the "On" label.'); - - // Submit and ensure it is accepted. - $edit = [ - "{$field_name}[value]" => 1, - ]; - $this->drupalPostForm(NULL, $edit, t('Save')); - preg_match('|entity_test/manage/(\d+)|', $this->url, $match); - $id = $match[1]; - $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); - - // Verify that boolean value is displayed. - $entity = EntityTest::load($id); - $display = entity_get_display($entity->getEntityTypeId(), $entity->bundle(), 'full'); - $content = $display->build($entity); - $this->setRawContent(\Drupal::service('renderer')->renderRoot($content)); - $this->assertRaw('
' . $on . '
'); - - // Test with "On" label option. - entity_get_form_display('entity_test', 'entity_test', 'default') - ->setComponent($field_name, [ - 'type' => 'boolean_checkbox', - 'settings' => [ - 'display_label' => FALSE, - ] - ]) - ->save(); - - $this->drupalGet('entity_test/add'); - $this->assertFieldByName("{$field_name}[value]", '', 'Widget found.'); - $this->assertRaw($on); - $this->assertNoText($this->field->label()); - - // Test if we can change the on label. - $on = $this->randomMachineName(); - $edit = [ - 'settings[on_label]' => $on, - ]; - $this->drupalPostForm('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name, $edit, t('Save settings')); - // Check if we see the updated labels in the creation form. - $this->drupalGet('entity_test/add'); - $this->assertRaw($on); - - // Go to the form display page and check if the default settings works as - // expected. - $fieldEditUrl = 'entity_test/structure/entity_test/form-display'; - $this->drupalGet($fieldEditUrl); - - // Click on the widget settings button to open the widget settings form. - $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit"); - - $this->assertText( - 'Use field label instead of the "On" label as the label.', - t('Display setting checkbox available.') - ); - - // Enable setting. - $edit = ['fields[' . $field_name . '][settings_edit_form][settings][display_label]' => 1]; - $this->drupalPostAjaxForm(NULL, $edit, $field_name . "_plugin_settings_update"); - $this->drupalPostForm(NULL, NULL, 'Save'); - - // Go again to the form display page and check if the setting - // is stored and has the expected effect. - $this->drupalGet($fieldEditUrl); - $this->assertText('Use field label: Yes', 'Checking the display settings checkbox updated the value.'); - - $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit"); - $this->assertText( - 'Use field label instead of the "On" label as the label.', - t('Display setting checkbox is available') - ); - $this->assertFieldByXPath( - '*//input[starts-with(@id, "edit-fields-' . $field_name . '-settings-edit-form-settings-display-label") and @value="1"]', - TRUE, - t('Display label changes label of the checkbox') - ); - - // Test the boolean field settings. - $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name); - $this->assertFieldById('edit-settings-on-label', $on); - $this->assertFieldById('edit-settings-off-label', $off); - } - - /** - * Test field access. - */ - public function testFormAccess() { - $on = 'boolean_on'; - $off = 'boolean_off'; - $label = 'boolean_label'; - $field_name = 'boolean_name'; - $this->fieldStorage = FieldStorageConfig::create([ - 'field_name' => $field_name, - 'entity_type' => 'entity_test', - 'type' => 'boolean', - ]); - $this->fieldStorage->save(); - $this->field = FieldConfig::create([ - 'field_name' => $field_name, - 'entity_type' => 'entity_test', - 'bundle' => 'entity_test', - 'label' => $label, - 'settings' => [ - 'on_label' => $on, - 'off_label' => $off, - ], - ]); - $this->field->save(); - - // Create a form display for the default form mode. - entity_get_form_display('entity_test', 'entity_test', 'default') - ->setComponent($field_name, [ - 'type' => 'boolean_checkbox', - ]) - ->save(); - - // Create a display for the full view mode. - entity_get_display('entity_test', 'entity_test', 'full') - ->setComponent($field_name, [ - 'type' => 'boolean', - ]) - ->save(); - - // Display creation form. - $this->drupalGet('entity_test/add'); - $this->assertFieldByName("{$field_name}[value]"); - - // Should be posted OK. - $this->drupalPostForm(NULL, [], t('Save')); - preg_match('|entity_test/manage/(\d+)|', $this->url, $match); - $id = $match[1]; - $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); - - // Tell the test module to disable access to the field. - \Drupal::state()->set('field.test_boolean_field_access_field', $field_name); - $this->drupalGet('entity_test/add'); - // Field should not be there anymore. - $this->assertNoFieldByName("{$field_name}[value]"); - // Should still be able to post the form. - $this->drupalPostForm(NULL, [], t('Save')); - preg_match('|entity_test/manage/(\d+)|', $this->url, $match); - $id = $match[1]; - $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); - } - -} diff --git a/core/modules/field/src/Tests/FormTest.php b/core/modules/field/src/Tests/FormTest.php index 786e367..326cfe2 100644 --- a/core/modules/field/src/Tests/FormTest.php +++ b/core/modules/field/src/Tests/FormTest.php @@ -113,6 +113,9 @@ public function testFieldFormSingle() { // Check that hook_field_widget_form_alter() does not believe this is the // default value form. $this->assertNoText('From hook_field_widget_form_alter(): Default form is true.', 'Not default value form in hook_field_widget_form_alter().'); + // Check that hook_field_widget_form_alter() does not believe this is the + // default value form. + $this->assertNoText('From hook_field_widget_multivalue_form_alter(): Default form is true.', 'Not default value form in hook_field_widget_form_alter().'); // Submit with invalid value (field-level validation). $edit = [ @@ -634,4 +637,64 @@ public function testLabelOnMultiValueFields() { $this->assertEscaped(""); } + /** + * Tests hook_field_widget_multivalue_form_alter(). + */ + public function testFieldFormMultipleWidgetAlter() { + $this->widgetAlterTest('hook_field_widget_multivalue_form_alter', 'test_field_widget_multiple'); + } + + /** + * Tests hook_field_widget_multivalue_form_alter() with single value elements. + */ + public function testFieldFormMultipleWidgetAlterSingleValues() { + $this->widgetAlterTest('hook_field_widget_multivalue_form_alter', 'test_field_widget_multiple_single_value'); + } + + /** + * Tests hook_field_widget_multivalue_WIDGET_TYPE_form_alter(). + */ + public function testFieldFormMultipleWidgetTypeAlter() { + $this->widgetAlterTest('hook_field_widget_multivalue_WIDGET_TYPE_form_alter', 'test_field_widget_multiple'); + } + + /** + * Tests hook_field_widget_multivalue_WIDGET_TYPE_form_alter() with single value elements. + */ + public function testFieldFormMultipleWidgetTypeAlterSingleValues() { + $this->widgetAlterTest('hook_field_widget_multivalue_WIDGET_TYPE_form_alter', 'test_field_widget_multiple_single_value'); + } + + /** + * Tests widget alter hooks for a given hook name. + */ + protected function widgetAlterTest($hook, $widget) { + // Create a field with fixed cardinality, configure the form to use a + // "multiple" widget. + $field_storage = $this->fieldStorageMultiple; + $field_name = $field_storage['field_name']; + $this->field['field_name'] = $field_name; + FieldStorageConfig::create($field_storage)->save(); + FieldConfig::create($this->field)->save(); + + // Set a flag in state so that the hook implementations will run. + \Drupal::state()->set("field_test.widget_alter_test", [ + 'hook' => $hook, + 'field_name' => $field_name, + 'widget' => $widget, + ]); + entity_get_form_display($this->field['entity_type'], $this->field['bundle'], 'default') + ->setComponent($field_name, [ + 'type' => $widget, + ]) + ->save(); + + $this->drupalGet('entity_test/add'); + $this->assertUniqueText("From $hook(): prefix on $field_name parent element."); + if ($widget === 'test_field_widget_multiple_single_value') { + $suffix_text = "From $hook(): suffix on $field_name child element."; + $this->assertEqual($field_storage['cardinality'], substr_count($this->getTextContent(), $suffix_text), "'$suffix_text' was found {$field_storage['cardinality']} times using widget $widget"); + } + } + } diff --git a/core/modules/field/src/Tests/reEnableModuleFieldTest.php b/core/modules/field/src/Tests/reEnableModuleFieldTest.php index 24c0eff..ce98c7f 100644 --- a/core/modules/field/src/Tests/reEnableModuleFieldTest.php +++ b/core/modules/field/src/Tests/reEnableModuleFieldTest.php @@ -86,7 +86,7 @@ public function testReEnabledField() { $this->assertRaw(''); // Test that the module can't be uninstalled from the UI while there is data - // for it's fields. + // for its fields. $admin_user = $this->drupalCreateUser(['access administration pages', 'administer modules']); $this->drupalLogin($admin_user); $this->drupalGet('admin/modules/uninstall'); diff --git a/core/modules/field/tests/fixtures/update/drupal-8.update_deleted_field_definitions-2931436.php b/core/modules/field/tests/fixtures/update/drupal-8.update_deleted_field_definitions-2931436.php new file mode 100644 index 0000000..9a210e0 --- /dev/null +++ b/core/modules/field/tests/fixtures/update/drupal-8.update_deleted_field_definitions-2931436.php @@ -0,0 +1,203 @@ +insert('key_value') + ->fields([ + 'collection', + 'name', + 'value', + ]) + ->values([ + 'collection' => 'entity.storage_schema.sql', + 'name' => 'node.field_schema_data.field_test', + 'value' => 'a:2:{s:16:"node__field_test";a:4:{s:11:"description";s:39:"Data storage for node field field_test.";s:6:"fields";a:7:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:16:"field_test_value";a:3:{s:4:"type";s:7:"varchar";s:6:"length";i:254;s:8:"not null";b:1;}}s:11:"primary key";a:4:{i:0;s:9:"entity_id";i:1;s:7:"deleted";i:2;s:5:"delta";i:3;s:8:"langcode";}s:7:"indexes";a:2:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}}}s:25:"node_revision__field_test";a:4:{s:11:"description";s:51:"Revision archive storage for node field field_test.";s:6:"fields";a:7:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:16:"field_test_value";a:3:{s:4:"type";s:7:"varchar";s:6:"length";i:254;s:8:"not null";b:1;}}s:11:"primary key";a:5:{i:0;s:9:"entity_id";i:1;s:11:"revision_id";i:2;s:7:"deleted";i:3;s:5:"delta";i:4;s:8:"langcode";}s:7:"indexes";a:2:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}}}}', + ]) + ->values([ + 'collection' => 'state', + 'name' => 'field.field.deleted', + 'value' => 'a:1:{s:36:"5d0d9870-560b-46c4-b838-0dcded0502dd";a:18:{s:4:"uuid";s:36:"5d0d9870-560b-46c4-b838-0dcded0502dd";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"config";a:2:{i:0;s:29:"field.storage.node.field_test";i:1;s:17:"node.type.article";}}s:2:"id";s:23:"node.article.field_test";s:10:"field_name";s:10:"field_test";s:11:"entity_type";s:4:"node";s:6:"bundle";s:7:"article";s:5:"label";s:4:"Test";s:11:"description";s:0:"";s:8:"required";b:0;s:12:"translatable";b:0;s:13:"default_value";a:0:{}s:22:"default_value_callback";s:0:"";s:8:"settings";a:0:{}s:10:"field_type";s:5:"email";s:7:"deleted";b:1;s:18:"field_storage_uuid";s:36:"ce93d7c2-1da7-4a2c-9e6d-b4925e3b129f";}}', + ]) + ->values([ + 'collection' => 'state', + 'name' => 'field.storage.deleted', + 'value' => 'a:1:{s:36:"ce93d7c2-1da7-4a2c-9e6d-b4925e3b129f";a:18:{s:4:"uuid";s:36:"ce93d7c2-1da7-4a2c-9e6d-b4925e3b129f";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"module";a:1:{i:0;s:4:"node";}}s:2:"id";s:15:"node.field_test";s:10:"field_name";s:10:"field_test";s:11:"entity_type";s:4:"node";s:4:"type";s:5:"email";s:8:"settings";a:0:{}s:6:"module";s:4:"core";s:6:"locked";b:0;s:11:"cardinality";i:1;s:12:"translatable";b:1;s:7:"indexes";a:0:{}s:22:"persist_with_no_fields";b:0;s:14:"custom_storage";b:0;s:7:"deleted";b:1;s:7:"bundles";a:0:{}}}', + ]) + ->execute(); + +// Create and populate the deleted field tables. +// @see \Drupal\Core\Entity\Sql\DefaultTableMapping::getDedicatedDataTableName() +$deleted_field_data_table_name = "field_deleted_data_" . substr(hash('sha256', 'ce93d7c2-1da7-4a2c-9e6d-b4925e3b129f'), 0, 10); +$connection->schema()->createTable($deleted_field_data_table_name, array( + 'fields' => array( + 'bundle' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'langcode' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_test_value' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '254', + ), + ), + 'primary key' => array( + 'entity_id', + 'deleted', + 'delta', + 'langcode', + ), + 'indexes' => array( + 'bundle' => array( + 'bundle', + ), + 'revision_id' => array( + 'revision_id', + ), + ), +)); + +$connection->insert($deleted_field_data_table_name) +->fields(array( + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'langcode', + 'delta', + 'field_test_value', +)) +->values(array( + 'bundle' => 'article', + 'deleted' => '1', + 'entity_id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + 'delta' => '0', + 'field_test_value' => 'test@test.com', +)) +->execute(); + +// @see \Drupal\Core\Entity\Sql\DefaultTableMapping::getDedicatedDataTableName() +$deleted_field_revision_table_name = "field_deleted_revision_" . substr(hash('sha256', 'ce93d7c2-1da7-4a2c-9e6d-b4925e3b129f'), 0, 10); +$connection->schema()->createTable($deleted_field_revision_table_name, array( + 'fields' => array( + 'bundle' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'langcode' => array( + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_test_value' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '254', + ), + ), + 'primary key' => array( + 'entity_id', + 'revision_id', + 'deleted', + 'delta', + 'langcode', + ), + 'indexes' => array( + 'bundle' => array( + 'bundle', + ), + 'revision_id' => array( + 'revision_id', + ), + ), +)); + +$connection->insert($deleted_field_revision_table_name) +->fields(array( + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'langcode', + 'delta', + 'field_test_value', +)) +->values(array( + 'bundle' => 'article', + 'deleted' => '1', + 'entity_id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + 'delta' => '0', + 'field_test_value' => 'test@test.com', +)) +->execute(); diff --git a/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml b/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml index 3712b47..45f5685 100644 --- a/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml +++ b/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml @@ -55,6 +55,14 @@ field.widget.settings.test_field_widget_multiple: type: string label: 'Test setting' +field.widget.settings.test_field_widget_multiple_single_value: + type: mapping + label: 'Test multiple field widget settings: single values' + mapping: + test_widget_setting_multiple: + type: string + label: 'Test setting' + field.widget.third_party.color: type: mapping label: 'Field test entity display color module third party settings' diff --git a/core/modules/field/tests/modules/field_test/field_test.field.inc b/core/modules/field/tests/modules/field_test/field_test.field.inc index 5edd25d..df5fc26 100644 --- a/core/modules/field/tests/modules/field_test/field_test.field.inc +++ b/core/modules/field/tests/modules/field_test/field_test.field.inc @@ -19,6 +19,13 @@ function field_test_field_widget_info_alter(&$info) { $info['test_field_widget_multiple']['field_types'][] = 'test_field'; $info['test_field_widget_multiple']['field_types'][] = 'test_field_with_preconfigured_options'; + // Add extra widget when needed for tests. + // @see \Drupal\field\Tests\FormTest::widgetAlterTest(). + if ($alter_info = \Drupal::state()->get("field_test.widget_alter_test")) { + if ($alter_info['widget'] === 'test_field_widget_multiple_single_value') { + $info['test_field_widget_multiple_single_value']['field_types'][] = 'test_field'; + } + } } /** diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module index 9121e94..1d7e2d6 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -15,6 +15,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\field\FieldStorageConfigInterface; require_once __DIR__ . '/field_test.entity.inc'; @@ -100,16 +101,6 @@ function field_test_entity_display_build_alter(&$output, $context) { * Implements hook_field_widget_form_alter(). */ function field_test_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) { - $field_definition = $context['items']->getFieldDefinition(); - switch ($field_definition->getName()) { - case 'alter_test_text': - drupal_set_message('Field size: ' . $context['widget']->getSetting('size')); - break; - - case 'alter_test_options': - drupal_set_message('Widget type: ' . $context['widget']->getPluginId()); - break; - } // Set a message if this is for the form displayed to set default value for // the field. if ($context['default']) { @@ -118,6 +109,50 @@ function field_test_field_widget_form_alter(&$element, FormStateInterface $form_ } /** + * Implements hook_field_widget_multivalue_form_alter(). + */ +function field_test_field_widget_multivalue_form_alter(array &$elements, FormStateInterface $form_state, array $context) { + _field_test_alter_widget("hook_field_widget_multivalue_form_alter", $elements, $form_state, $context); +} + +/** + * Implements hook_field_widget_multivalue_WIDGET_TYPE_form_alter(). + */ +function field_test_field_widget_multivalue_test_field_widget_multiple_form_alter(array &$elements, FormStateInterface $form_state, array $context) { + _field_test_alter_widget("hook_field_widget_multivalue_WIDGET_TYPE_form_alter", $elements, $form_state, $context); +} + +/** + * Implements hook_field_widget_multivalue_WIDGET_TYPE_form_alter(). + */ +function field_test_field_widget_multivalue_test_field_widget_multiple_single_value_form_alter(array &$elements, FormStateInterface $form_state, array $context) { + _field_test_alter_widget("hook_field_widget_multivalue_WIDGET_TYPE_form_alter", $elements, $form_state, $context); +} + + +/** + * Sets up alterations for widget alter tests. + * + * @see \Drupal\field\Tests\FormTest::widgetAlterTest() + */ +function _field_test_alter_widget($hook, array &$elements, FormStateInterface $form_state, array $context) { + + // Set a message if this is for the form displayed to set default value for + // the field. + if ($context['default']) { + drupal_set_message("From $hook(): Default form is true."); + } + $alter_info = \Drupal::state()->get("field_test.widget_alter_test"); + $name = $context['items']->getFieldDefinition()->getName(); + if (!empty($alter_info) && $hook === $alter_info['hook'] && $name === $alter_info['field_name']) { + $elements['#prefix'] = "From $hook(): prefix on $name parent element."; + foreach (Element::children($elements) as $delta => $element) { + $elements[$delta]['#suffix'] = "From $hook(): suffix on $name child element."; + } + } +} + +/** * Implements hook_query_TAG_alter() for tag 'efq_table_prefixing_test'. * * @see \Drupal\system\Tests\Entity\EntityFieldQueryTest::testTablePrefixing() diff --git a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidgetMultipleSingleValues.php b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidgetMultipleSingleValues.php new file mode 100644 index 0000000..48b0ed0 --- /dev/null +++ b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidgetMultipleSingleValues.php @@ -0,0 +1,22 @@ +drupalLogin($this->drupalCreateUser([ + 'view test entity', + 'administer entity_test content', + 'administer entity_test form display', + 'administer entity_test fields', + ])); + } + + /** + * Tests boolean field. + */ + public function testBooleanField() { + $on = $this->randomMachineName(); + $off = $this->randomMachineName(); + $label = $this->randomMachineName(); + + // Create a field with settings to validate. + $field_name = Unicode::strtolower($this->randomMachineName()); + $this->fieldStorage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'boolean', + ]); + $this->fieldStorage->save(); + $this->field = FieldConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + 'label' => $label, + 'required' => TRUE, + 'settings' => [ + 'on_label' => $on, + 'off_label' => $off, + ], + ]); + $this->field->save(); + + // Create a form display for the default form mode. + entity_get_form_display('entity_test', 'entity_test', 'default') + ->setComponent($field_name, [ + 'type' => 'boolean_checkbox', + ]) + ->save(); + // Create a display for the full view mode. + entity_get_display('entity_test', 'entity_test', 'full') + ->setComponent($field_name, [ + 'type' => 'boolean', + ]) + ->save(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[value]", '', 'Widget found.'); + $this->assertText($this->field->label(), 'Uses field label by default.'); + $this->assertNoRaw($on, 'Does not use the "On" label.'); + + // Submit and ensure it is accepted. + $edit = [ + "{$field_name}[value]" => 1, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + + // Verify that boolean value is displayed. + $entity = EntityTest::load($id); + $this->drupalGet($entity->toUrl()); + $this->assertRaw('
' . $on . '
'); + + // Test with "On" label option. + entity_get_form_display('entity_test', 'entity_test', 'default') + ->setComponent($field_name, [ + 'type' => 'boolean_checkbox', + 'settings' => [ + 'display_label' => FALSE, + ] + ]) + ->save(); + + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[value]", '', 'Widget found.'); + $this->assertRaw($on); + $this->assertNoText($this->field->label()); + + // Test if we can change the on label. + $on = $this->randomMachineName(); + $edit = [ + 'settings[on_label]' => $on, + ]; + $this->drupalPostForm('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name, $edit, t('Save settings')); + // Check if we see the updated labels in the creation form. + $this->drupalGet('entity_test/add'); + $this->assertRaw($on); + + // Go to the form display page and check if the default settings works as + // expected. + $fieldEditUrl = 'entity_test/structure/entity_test/form-display'; + $this->drupalGet($fieldEditUrl); + + // Click on the widget settings button to open the widget settings form. + $this->drupalPostForm(NULL, [], $field_name . "_settings_edit"); + + $this->assertText( + 'Use field label instead of the "On" label as the label.', + t('Display setting checkbox available.') + ); + + // Enable setting. + $edit = ['fields[' . $field_name . '][settings_edit_form][settings][display_label]' => 1]; + $this->drupalPostForm(NULL, $edit, $field_name . "_plugin_settings_update"); + $this->drupalPostForm(NULL, NULL, 'Save'); + + // Go again to the form display page and check if the setting + // is stored and has the expected effect. + $this->drupalGet($fieldEditUrl); + $this->assertText('Use field label: Yes', 'Checking the display settings checkbox updated the value.'); + + $this->drupalPostForm(NULL, [], $field_name . "_settings_edit"); + $this->assertText( + 'Use field label instead of the "On" label as the label.', + t('Display setting checkbox is available') + ); + $this->getSession()->getPage()->hasCheckedField('fields[' . $field_name . '][settings_edit_form][settings][display_label]'); + + // Test the boolean field settings. + $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.' . $field_name); + $this->assertFieldById('edit-settings-on-label', $on); + $this->assertFieldById('edit-settings-off-label', $off); + } + + /** + * Test field access. + */ + public function testFormAccess() { + $on = 'boolean_on'; + $off = 'boolean_off'; + $label = 'boolean_label'; + $field_name = 'boolean_name'; + $this->fieldStorage = FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'boolean', + ]); + $this->fieldStorage->save(); + $this->field = FieldConfig::create([ + 'field_name' => $field_name, + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + 'label' => $label, + 'settings' => [ + 'on_label' => $on, + 'off_label' => $off, + ], + ]); + $this->field->save(); + + // Create a form display for the default form mode. + entity_get_form_display('entity_test', 'entity_test', 'default') + ->setComponent($field_name, [ + 'type' => 'boolean_checkbox', + ]) + ->save(); + + // Create a display for the full view mode. + entity_get_display('entity_test', 'entity_test', 'full') + ->setComponent($field_name, [ + 'type' => 'boolean', + ]) + ->save(); + + // Display creation form. + $this->drupalGet('entity_test/add'); + $this->assertFieldByName("{$field_name}[value]"); + + // Should be posted OK. + $this->drupalPostForm(NULL, [], t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + + // Tell the test module to disable access to the field. + \Drupal::state()->set('field.test_boolean_field_access_field', $field_name); + $this->drupalGet('entity_test/add'); + // Field should not be there anymore. + $this->assertNoFieldByName("{$field_name}[value]"); + // Should still be able to post the form. + $this->drupalPostForm(NULL, [], t('Save')); + preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match); + $id = $match[1]; + $this->assertText(t('entity_test @id has been created.', ['@id' => $id])); + } + +} diff --git a/core/modules/field/tests/src/Functional/Update/FieldUpdateTest.php b/core/modules/field/tests/src/Functional/Update/FieldUpdateTest.php index b9175b1..2791ef0 100644 --- a/core/modules/field/tests/src/Functional/Update/FieldUpdateTest.php +++ b/core/modules/field/tests/src/Functional/Update/FieldUpdateTest.php @@ -3,9 +3,12 @@ namespace Drupal\Tests\field\Functional\Update; use Drupal\Core\Config\Config; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\field\Entity\FieldConfig; use Drupal\FunctionalTests\Update\UpdatePathTestBase; use Drupal\node\Entity\Node; +use Drupal\Tests\Traits\Core\CronRunTrait; /** * Tests that field settings are properly updated during database updates. @@ -14,6 +17,8 @@ */ class FieldUpdateTest extends UpdatePathTestBase { + use CronRunTrait; + /** * The config factory service. * @@ -22,11 +27,44 @@ class FieldUpdateTest extends UpdatePathTestBase { protected $configFactory; /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The key-value collection for tracking installed storage schema. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $installedStorageSchema; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * The deleted fields repository. + * + * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface + */ + protected $deletedFieldsRepository; + + /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); + $this->configFactory = $this->container->get('config.factory'); + $this->database = $this->container->get('database'); + $this->installedStorageSchema = $this->container->get('keyvalue')->get('entity.storage_schema.sql'); + $this->state = $this->container->get('state'); + $this->deletedFieldsRepository = $this->container->get('entity_field.deleted_fields_repository'); } /** @@ -37,6 +75,7 @@ protected function setDatabaseDumpFiles() { __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', __DIR__ . '/../../../fixtures/update/drupal-8.views_entity_reference_plugins-2429191.php', __DIR__ . '/../../../fixtures/update/drupal-8.remove_handler_submit_setting-2715589.php', + __DIR__ . '/../../../fixtures/update/drupal-8.update_deleted_field_definitions-2931436.php', ]; } @@ -128,6 +167,76 @@ public function testFieldUpdate8003() { } /** + * Tests field_update_8500(). + * + * @see field_update_8500() + */ + public function testFieldUpdate8500() { + $field_name = 'field_test'; + $field_uuid = '5d0d9870-560b-46c4-b838-0dcded0502dd'; + $field_storage_uuid = 'ce93d7c2-1da7-4a2c-9e6d-b4925e3b129f'; + + // Check that we have pre-existing entries for 'field.field.deleted' and + // 'field.storage.deleted'. + $deleted_fields = $this->state->get('field.field.deleted'); + $this->assertCount(1, $deleted_fields); + $this->assertArrayHasKey($field_uuid, $deleted_fields); + + $deleted_field_storages = $this->state->get('field.storage.deleted'); + $this->assertCount(1, $deleted_field_storages); + $this->assertArrayHasKey($field_storage_uuid, $deleted_field_storages); + + // Ensure that cron does not run automatically after running the updates. + $this->state->set('system.cron_last', REQUEST_TIME + 100); + + // Run updates. + $this->runUpdates(); + + // Now that we can use the API, check that the "delete fields" state entries + // have been converted to proper field definition objects. + $deleted_fields = $this->deletedFieldsRepository->getFieldDefinitions(); + + $this->assertCount(1, $deleted_fields); + $this->assertArrayHasKey($field_uuid, $deleted_fields); + $this->assertTrue($deleted_fields[$field_uuid] instanceof FieldDefinitionInterface); + $this->assertEquals($field_name, $deleted_fields[$field_uuid]->getName()); + + $deleted_field_storages = $this->deletedFieldsRepository->getFieldStorageDefinitions(); + $this->assertCount(1, $deleted_field_storages); + $this->assertArrayHasKey($field_storage_uuid, $deleted_field_storages); + $this->assertTrue($deleted_field_storages[$field_storage_uuid] instanceof FieldStorageDefinitionInterface); + $this->assertEquals($field_name, $deleted_field_storages[$field_storage_uuid]->getName()); + + // Check that the installed storage schema still exists. + $this->assertNotNull($this->installedStorageSchema->get("node.field_schema_data.$field_name")); + + // Check that the deleted field tables exist. + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = \Drupal::entityTypeManager()->getStorage('node')->getTableMapping(); + + $deleted_field_data_table_name = $table_mapping->getDedicatedDataTableName($deleted_field_storages[$field_storage_uuid], TRUE); + $this->assertTrue($this->database->schema()->tableExists($deleted_field_data_table_name)); + $deleted_field_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_field_storages[$field_storage_uuid], TRUE); + $this->assertTrue($this->database->schema()->tableExists($deleted_field_revision_table_name)); + + // Run cron and repeat the checks above. + $this->cronRun(); + + $deleted_fields = $this->deletedFieldsRepository->getFieldDefinitions(); + $this->assertCount(0, $deleted_fields); + + $deleted_field_storages = $this->deletedFieldsRepository->getFieldStorageDefinitions(); + $this->assertCount(0, $deleted_field_storages); + + // Check that the installed storage schema has been deleted. + $this->assertNull($this->installedStorageSchema->get("node.field_schema_data.$field_name")); + + // Check that the deleted field tables have been deleted. + $this->assertFalse($this->database->schema()->tableExists($deleted_field_data_table_name)); + $this->assertFalse($this->database->schema()->tableExists($deleted_field_revision_table_name)); + } + + /** * Asserts that a config depends on 'entity_reference' or not * * @param \Drupal\Core\Config\Config $config diff --git a/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php b/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php index 5f4c8ee..095e4bd 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/Views/EntityReferenceRelationshipTest.php @@ -67,7 +67,7 @@ protected function setUp($import_test_views = TRUE) { // Create reference from entity_test_mul to entity_test. $this->createEntityReferenceField('entity_test_mul', 'entity_test_mul', 'field_data_test', 'field_data_test', 'entity_test'); - // Create another field for testing with a long name. So it's storage name + // Create another field for testing with a long name. So its storage name // will become hashed. Use entity_test_mul_changed, so the resulting field // tables created will be greater than 48 chars long. // @see \Drupal\Core\Entity\Sql\DefaultTableMapping::generateFieldTableName() diff --git a/core/modules/field_ui/css/field_ui.admin.css b/core/modules/field_ui/css/field_ui.admin.css index 08f6c2d..f171649 100644 --- a/core/modules/field_ui/css/field_ui.admin.css +++ b/core/modules/field_ui/css/field_ui.admin.css @@ -17,7 +17,7 @@ } .field-ui-overview .field-plugin-summary { float: left; /* LTR */ - font-size: .9em; + font-size: 0.9em; } [dir="rtl"] .field-ui-overview .field-plugin-summary { float: right; @@ -25,7 +25,7 @@ .field-ui-overview .field-plugin-summary-cell .warning { display: block; float: left; /* LTR */ - margin-right: .5em; + margin-right: 0.5em; } [dir="rtl"] .field-ui-overview .field-plugin-summary-cell .warning { float: right; diff --git a/core/modules/file/migrations/d6_upload.yml b/core/modules/file/migrations/d6_upload.yml index a497099..8b7e7cb 100644 --- a/core/modules/file/migrations/d6_upload.yml +++ b/core/modules/file/migrations/d6_upload.yml @@ -7,6 +7,10 @@ source: process: nid: nid vid: vid + langcode: + plugin: user_langcode + source: language + fallback_to_site_default: true type: type upload: plugin: sub_process diff --git a/core/modules/file/src/Plugin/migrate/source/d6/Upload.php b/core/modules/file/src/Plugin/migrate/source/d6/Upload.php index 475ce61..4d03e5f 100644 --- a/core/modules/file/src/Plugin/migrate/source/d6/Upload.php +++ b/core/modules/file/src/Plugin/migrate/source/d6/Upload.php @@ -29,6 +29,7 @@ public function query() { ->fields('u', ['nid', 'vid']); $query->innerJoin('node', 'n', static::JOIN); $query->addField('n', 'type'); + $query->addField('n', 'language'); return $query; } @@ -54,6 +55,7 @@ public function fields() { 'nid' => $this->t('The node Id.'), 'vid' => $this->t('The version Id.'), 'type' => $this->t('The node type'), + 'language' => $this->t('The node language.'), 'description' => $this->t('The file description.'), 'list' => $this->t('Whether the list should be visible on the node page.'), 'weight' => $this->t('The file weight.'), diff --git a/core/modules/file/src/Tests/SaveUploadFormTest.php b/core/modules/file/src/Tests/SaveUploadFormTest.php index e5680706..07ebd1d 100644 --- a/core/modules/file/src/Tests/SaveUploadFormTest.php +++ b/core/modules/file/src/Tests/SaveUploadFormTest.php @@ -419,7 +419,7 @@ public function testErrorMessagesAreNotChanged() { // after calling _file_save_upload_from_form() are correct. $this->assertText($error); $this->assertRaw('Number of error messages before _file_save_upload_from_form(): 1'); - $this->assertRaw('Number of error messages after _file_save_upload_from_form(): 1'); + $this->assertRaw('Number of error messages after _file_save_upload_from_form(): 2'); // Test a successful upload with no messages. $edit = [ diff --git a/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php b/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php index 97cd950..2dce2fa 100644 --- a/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php +++ b/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php @@ -4,6 +4,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\State\StateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,13 +21,23 @@ class FileTestSaveUploadFromForm extends FormBase { protected $state; /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** * Constructs a FileTestSaveUploadFromForm object. * * @param \Drupal\Core\State\StateInterface $state * The state key value store. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. */ - public function __construct(StateInterface $state) { + public function __construct(StateInterface $state, MessengerInterface $messenger) { $this->state = $state; + $this->messenger = $messenger; } /** @@ -34,7 +45,8 @@ public function __construct(StateInterface $state) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('state') + $container->get('state'), + $container->get('messenger') ); } @@ -117,7 +129,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { // Preset custom error message if requested. if ($form_state->getValue('error_message')) { - drupal_set_message($form_state->getValue('error_message'), 'error'); + $this->messenger->addError($form_state->getValue('error_message')); } // Setup validators. @@ -143,19 +155,19 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $form['file_test_upload']['#upload_validators'] = $validators; $form['file_test_upload']['#upload_location'] = $destination; - drupal_set_message($this->t('Number of error messages before _file_save_upload_from_form(): @count.', ['@count' => count(drupal_get_messages('error', FALSE))])); + $this->messenger->addStatus($this->t('Number of error messages before _file_save_upload_from_form(): @count.', ['@count' => count($this->messenger->messagesByType(MessengerInterface::TYPE_ERROR))])); $file = _file_save_upload_from_form($form['file_test_upload'], $form_state, 0, $form_state->getValue('file_test_replace')); - drupal_set_message($this->t('Number of error messages after _file_save_upload_from_form(): @count.', ['@count' => count(drupal_get_messages('error', FALSE))])); + $this->messenger->addStatus($this->t('Number of error messages after _file_save_upload_from_form(): @count.', ['@count' => count($this->messenger->messagesByType(MessengerInterface::TYPE_ERROR))])); if ($file) { $form_state->setValue('file_test_upload', $file); - drupal_set_message($this->t('File @filepath was uploaded.', ['@filepath' => $file->getFileUri()])); - drupal_set_message($this->t('File name is @filename.', ['@filename' => $file->getFilename()])); - drupal_set_message($this->t('File MIME type is @mimetype.', ['@mimetype' => $file->getMimeType()])); - drupal_set_message($this->t('You WIN!')); + $this->messenger->addStatus($this->t('File @filepath was uploaded.', ['@filepath' => $file->getFileUri()])); + $this->messenger->addStatus($this->t('File name is @filename.', ['@filename' => $file->getFilename()])); + $this->messenger->addStatus($this->t('File MIME type is @mimetype.', ['@mimetype' => $file->getMimeType()])); + $this->messenger->addStatus($this->t('You WIN!')); } elseif ($file === FALSE) { - drupal_set_message($this->t('Epic upload FAIL!'), 'error'); + $this->messenger->addError($this->t('Epic upload FAIL!')); } } diff --git a/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php b/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php index fc379e5..b807503 100644 --- a/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php +++ b/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php @@ -16,7 +16,7 @@ class MigrateUploadTest extends MigrateDrupal6TestBase { /** * {@inheritdoc} */ - public static $modules = ['menu_ui']; + public static $modules = ['language', 'menu_ui']; /** * {@inheritdoc} @@ -51,7 +51,7 @@ protected function setUp() { } $this->prepareMigrations($id_mappings); - $this->migrateContent(); + $this->migrateContent(['translations']); // Since we are only testing a subset of the file migration, do not check // that the full file migration has been run. $migration = $this->getMigration('d6_upload'); @@ -65,16 +65,18 @@ protected function setUp() { public function testUpload() { $this->container->get('entity.manager') ->getStorage('node') - ->resetCache([1, 2]); + ->resetCache([1, 2, 12]); - $nodes = Node::loadMultiple([1, 2]); + $nodes = Node::loadMultiple([1, 2, 12]); $node = $nodes[1]; + $this->assertEquals('en', $node->langcode->value); $this->assertIdentical(1, count($node->upload)); $this->assertIdentical('1', $node->upload[0]->target_id); $this->assertIdentical('file 1-1-1', $node->upload[0]->description); $this->assertIdentical(FALSE, $node->upload[0]->isDisplayed()); $node = $nodes[2]; + $this->assertEquals('en', $node->langcode->value); $this->assertIdentical(2, count($node->upload)); $this->assertIdentical('3', $node->upload[0]->target_id); $this->assertIdentical('file 2-3-3', $node->upload[0]->description); @@ -82,6 +84,13 @@ public function testUpload() { $this->assertIdentical('2', $node->upload[1]->target_id); $this->assertIdentical(TRUE, $node->upload[1]->isDisplayed()); $this->assertIdentical('file 2-3-2', $node->upload[1]->description); + + $node = $nodes[12]; + $this->assertEquals('zu', $node->langcode->value); + $this->assertEquals(1, count($node->upload)); + $this->assertEquals('3', $node->upload[0]->target_id); + $this->assertEquals('file 12-15-3', $node->upload[0]->description); + $this->assertEquals(FALSE, $node->upload[0]->isDisplayed()); } } diff --git a/core/modules/file/tests/src/Kernel/Plugin/migrate/source/d6/UploadTest.php b/core/modules/file/tests/src/Kernel/Plugin/migrate/source/d6/UploadTest.php index 96389d5..1f950b4 100644 --- a/core/modules/file/tests/src/Kernel/Plugin/migrate/source/d6/UploadTest.php +++ b/core/modules/file/tests/src/Kernel/Plugin/migrate/source/d6/UploadTest.php @@ -34,6 +34,14 @@ public function providerSource() { 'list' => '0', 'weight' => '-1', ], + [ + 'fid' => '3', + 'nid' => '12', + 'vid' => '15', + 'description' => 'file 12-15-3', + 'list' => '0', + 'weight' => '0', + ], ]; $tests[0]['source_data']['node'] = [ @@ -54,6 +62,23 @@ public function providerSource() { 'tnid' => '0', 'translate' => '0', ], + [ + 'nid' => '12', + 'vid' => '15', + 'type' => 'page', + 'language' => 'zu', + 'title' => 'Abantu zulu', + 'uid' => '1', + 'status' => '1', + 'created' => '1444238800', + 'changed' => '1444238808', + 'comment' => '0', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '12', + 'translate' => '0', + ], ]; // The expected results. @@ -66,10 +91,24 @@ public function providerSource() { 'list' => '0', ], ], + 'language' => '', 'nid' => '1', 'vid' => '1', 'type' => 'story', ], + [ + 'upload' => [ + [ + 'fid' => '3', + 'description' => 'file 12-15-3', + 'list' => '0', + ], + ], + 'language' => 'zu', + 'nid' => '12', + 'vid' => '15', + 'type' => 'page', + ], ]; return $tests; diff --git a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php index 06f72bf..eff0b97 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonAnonTest.php @@ -5,6 +5,7 @@ use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase; +use Drupal\Tests\rest\Functional\EntityResource\FormatSpecificGetBcRouteTestTrait; use Drupal\user\Entity\User; /** @@ -14,6 +15,7 @@ class EntityTestHalJsonAnonTest extends EntityTestResourceTestBase { use HalEntityNormalizationTrait; use AnonResourceTestTrait; + use FormatSpecificGetBcRouteTestTrait; /** * {@inheritdoc} diff --git a/core/modules/help/src/Plugin/Block/HelpBlock.php b/core/modules/help/src/Plugin/Block/HelpBlock.php index 17dd88e..04f8bcb 100644 --- a/core/modules/help/src/Plugin/Block/HelpBlock.php +++ b/core/modules/help/src/Plugin/Block/HelpBlock.php @@ -15,7 +15,10 @@ * * @Block( * id = "help_block", - * admin_label = @Translation("Help") + * admin_label = @Translation("Help"), + * forms = { + * "settings_tray" = FALSE, + * }, * ) */ class HelpBlock extends BlockBase implements ContainerFactoryPluginInterface { diff --git a/core/modules/image/css/editors/image.theme.css b/core/modules/image/css/editors/image.theme.css index 5946458..2bd1435 100644 --- a/core/modules/image/css/editors/image.theme.css +++ b/core/modules/image/css/editors/image.theme.css @@ -5,12 +5,12 @@ .quickedit-image-dropzone { background: rgba(116, 183, 255, 0.8); - transition: background .2s; + transition: background 0.2s; } .quickedit-image-icon { margin: 0 0 10px 0; - transition: margin .5s; + transition: margin 0.5s; } .quickedit-image-dropzone.hover { diff --git a/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php b/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php index 5fdbd2f..f40d083 100644 --- a/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php +++ b/core/modules/image/src/PathProcessor/PathProcessorImageStyles.php @@ -48,8 +48,11 @@ public function processInbound($path, Request $request) { if (strpos($path, '/' . $directory_path . '/styles/') === 0) { $path_prefix = '/' . $directory_path . '/styles/'; } - elseif (strpos($path, '/system/files/styles/') === 0) { + // Check if the string '/system/files/styles/' exists inside the path, + // that means we have a case of private file's image style. + elseif (strpos($path, '/system/files/styles/') !== FALSE) { $path_prefix = '/system/files/styles/'; + $path = substr($path, strpos($path, $path_prefix), strlen($path)); } else { return $path; diff --git a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php index 7b7ce1a..16160b2 100644 --- a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php +++ b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php @@ -295,7 +295,8 @@ public static function process($element, FormStateInterface $form_state, $form) public static function validateRequiredFields($element, FormStateInterface $form_state) { // Only do validation if the function is triggered from other places than // the image process form. - if (!in_array('file_managed_file_submit', $form_state->getTriggeringElement()['#submit'])) { + $triggering_element = $form_state->getTriggeringElement(); + if (empty($triggering_element['#submit']) || !in_array('file_managed_file_submit', $triggering_element['#submit'])) { // If the image is not there, we do not check for empty values. $parents = $element['#parents']; $field = array_pop($parents); diff --git a/core/modules/image/src/Tests/ImageFieldValidateTest.php b/core/modules/image/src/Tests/ImageFieldValidateTest.php index ba1a766..87c042b 100644 --- a/core/modules/image/src/Tests/ImageFieldValidateTest.php +++ b/core/modules/image/src/Tests/ImageFieldValidateTest.php @@ -2,6 +2,9 @@ namespace Drupal\image\Tests; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\field\Entity\FieldConfig; + /** * Tests validation functions such as min/max resolution. * @@ -238,4 +241,48 @@ public function testAJAXValidationMessage() { $this->assertEqual(count($elements), 1, 'Ajax validation messages are displayed once.'); } + /** + * Tests that image field validation works with other form submit handlers. + */ + public function testFriendlyAjaxValidation() { + // Add a custom field to the Article content type that contains an AJAX + // handler on a select field. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_dummy_select', + 'type' => 'image_module_test_dummy_ajax', + 'entity_type' => 'node', + 'cardinality' => 1, + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'entity_type' => 'node', + 'bundle' => 'article', + 'field_name' => 'field_dummy_select', + 'label' => t('Dummy select'), + ])->save(); + + \Drupal::entityTypeManager() + ->getStorage('entity_form_display') + ->load('node.article.default') + ->setComponent( + 'field_dummy_select', + [ + 'type' => 'image_module_test_dummy_ajax_widget', + 'weight' => 1, + ]) + ->save(); + + // Then, add an image field. + $this->createImageField('field_dummy_image', 'article'); + + // Open an article and trigger the AJAX handler. + $this->drupalGet('node/add/article'); + $edit = [ + 'field_dummy_select[select_widget]' => 'bam', + ]; + $this->drupalPostAjaxForm(NULL, $edit, 'field_dummy_select[select_widget]'); + } + } diff --git a/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php b/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php index baa4e7f..eef166c 100644 --- a/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php +++ b/core/modules/image/src/Tests/ImageStylesPathAndUrlTest.php @@ -3,6 +3,7 @@ namespace Drupal\image\Tests; use Drupal\image\Entity\ImageStyle; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; /** @@ -17,18 +18,29 @@ class ImageStylesPathAndUrlTest extends WebTestBase { * * @var array */ - public static $modules = ['image', 'image_module_test']; + public static $modules = ['image', 'image_module_test', 'language']; /** + * The image style. + * * @var \Drupal\image\ImageStyleInterface */ protected $style; + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); - $this->style = ImageStyle::create(['name' => 'style_foo', 'label' => $this->randomString()]); + $this->style = ImageStyle::create([ + 'name' => 'style_foo', + 'label' => $this->randomString(), + ]); $this->style->save(); + + // Create a new language. + ConfigurableLanguage::createFromLangcode('fr')->save(); } /** @@ -74,6 +86,20 @@ public function testImageStyleUrlAndPathPrivateUnclean() { } /** + * Tests an image style URL with the "public://" schema and language prefix. + */ + public function testImageStyleUrlAndPathPublicLanguage() { + $this->doImageStyleUrlAndPathTests('public', TRUE, TRUE, 'fr'); + } + + /** + * Tests an image style URL with the "private://" schema and language prefix. + */ + public function testImageStyleUrlAndPathPrivateLanguage() { + $this->doImageStyleUrlAndPathTests('private', TRUE, TRUE, 'fr'); + } + + /** * Tests an image style URL with a file URL that has an extra slash in it. */ public function testImageStyleUrlExtraSlash() { @@ -93,7 +119,7 @@ public function testImageStyleUrlForMissingSourceImage() { /** * Tests building an image style URL. */ - public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_slash = FALSE) { + public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_slash = FALSE, $langcode = FALSE) { $this->prepareRequestForGenerator($clean_url); // Make the default scheme neither "public" nor "private" to verify the @@ -105,6 +131,13 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s $status = file_prepare_directory($directory, FILE_CREATE_DIRECTORY); $this->assertNotIdentical(FALSE, $status, 'Created the directory for the generated images for the test style.'); + // Override the language to build the URL for the correct language. + if ($langcode) { + $language_manager = \Drupal::service('language_manager'); + $language = $language_manager->getLanguage($langcode); + $language_manager->setConfigOverrideLanguage($language); + } + // Create a working copy of the file. $files = $this->drupalGetTestFiles('image'); $file = array_shift($files); @@ -119,6 +152,11 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); $generate_url = $this->style->buildUrl($original_uri, $clean_url); + // Make sure that language prefix is never added to the image style URL. + if ($langcode) { + $this->assertTrue(strpos($generate_url, "/$langcode/") === FALSE, 'Langcode was not found in the image style URL.'); + } + // Ensure that the tests still pass when the file is generated by accessing // a poorly constructed (but still valid) file URL that has an extra slash // in it. @@ -157,7 +195,8 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s $this->assertEqual($this->drupalGetHeader('Content-Length'), $image->getFileSize(), 'Expected Content-Length was reported.'); // Check that we did not download the original file. - $original_image = $this->container->get('image.factory')->get($original_uri); + $original_image = $this->container->get('image.factory') + ->get($original_uri); $this->assertNotEqual($this->drupalGetHeader('Content-Length'), $original_image->getFileSize()); if ($scheme == 'private') { @@ -192,13 +231,15 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s $this->drupalGet($generate_url_noaccess); $this->assertResponse(403, 'Confirmed that access is denied for the private image style.'); - // Verify that images are not appended to the response. Currently this test only uses PNG images. + // Verify that images are not appended to the response. + // Currently this test only uses PNG images. if (strpos($generate_url, '.png') === FALSE) { $this->fail('Confirming that private image styles are not appended require PNG file.'); } else { - // Check for PNG-Signature (cf. http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.2) in the - // response body. + // Check for PNG-Signature + // (cf. http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.2) + // in the response body. $this->assertNoRaw(chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10), 'No PNG signature found in the response body.'); } } @@ -215,7 +256,9 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s // Allow insecure image derivatives to be created for the remainder of this // test. - $this->config('image.settings')->set('allow_insecure_derivatives', TRUE)->save(); + $this->config('image.settings') + ->set('allow_insecure_derivatives', TRUE) + ->save(); // Create another working copy of the file. $files = $this->drupalGetTestFiles('image'); @@ -239,7 +282,8 @@ public function doImageStyleUrlAndPathTests($scheme, $clean_url = TRUE, $extra_s // Stop supressing the security token in the URL. $this->config('image.settings')->set('suppress_itok_output', FALSE)->save(); // Ensure allow_insecure_derivatives is enabled. - $this->assertEqual($this->config('image.settings')->get('allow_insecure_derivatives'), TRUE); + $this->assertEqual($this->config('image.settings') + ->get('allow_insecure_derivatives'), TRUE); // Check that a security token is still required when generating a second // image derivative using the first one as a source. $nested_url = $this->style->buildUrl($generated_uri, $clean_url); diff --git a/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldFormatter/DummyAjaxFormatter.php b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldFormatter/DummyAjaxFormatter.php new file mode 100644 index 0000000..cebc54b --- /dev/null +++ b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldFormatter/DummyAjaxFormatter.php @@ -0,0 +1,39 @@ + [ + 'value' => [ + 'type' => 'varchar', + 'length' => 255, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return empty($this->get('value')->getValue()); + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties['value'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('Dummy string value')); + + return $properties; + } + +} diff --git a/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldWidget/DummyAjaxWidget.php b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldWidget/DummyAjaxWidget.php new file mode 100644 index 0000000..4da5911 --- /dev/null +++ b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldWidget/DummyAjaxWidget.php @@ -0,0 +1,57 @@ + 'select', + '#title' => $this->t('Dummy select'), + '#options' => ['pow' => 'Pow!', 'bam' => 'Bam!'], + '#required' => TRUE, + '#ajax' => [ + 'callback' => get_called_class() . '::dummyAjaxCallback', + 'effect' => 'fade', + ], + ]; + + return $element; + } + + /** + * Ajax callback for Dummy AJAX test. + * + * @param array $form + * The build form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * Ajax response. + */ + public static function dummyAjaxCallback(array &$form, FormStateInterface $form_state) { + return new AjaxResponse(); + } + +} diff --git a/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateDefaultLanguageTest.php b/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateDefaultLanguageTest.php index 290160c..e76b455 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateDefaultLanguageTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateDefaultLanguageTest.php @@ -45,10 +45,10 @@ public function testMigrationWithNonExistentLanguage() { $count = 0; foreach ($messages as $message) { $count++; - $this->assertSame($message->message, "The language 'tv' does not exist on this site."); - $this->assertSame((int) $message->level, MigrationInterface::MESSAGE_ERROR); + $this->assertSame("The language 'tv' does not exist on this site.", $message->message); + $this->assertSame(MigrationInterface::MESSAGE_ERROR, (int) $message->level); } - $this->assertSame($count, 1); + $this->assertSame(1, $count); } /** diff --git a/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageNegotiationSettingsTest.php b/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageNegotiationSettingsTest.php index 4a88427..96bc4a0 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageNegotiationSettingsTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d6/MigrateLanguageNegotiationSettingsTest.php @@ -29,26 +29,26 @@ public function testLanguageNegotiationWithDefaultPathPrefix() { ]); $config = $this->config('language.negotiation'); - $this->assertSame($config->get('session.parameter'), 'language'); - $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); - $this->assertSame($config->get('selected_langcode'), 'site_default'); + $this->assertSame('language', $config->get('session.parameter')); + $this->assertSame(LanguageNegotiationUrl::CONFIG_PATH_PREFIX, $config->get('url.source')); + $this->assertSame('site_default', $config->get('selected_langcode')); $expected_prefixes = [ 'en' => '', 'fr' => 'fr', 'zu' => 'zu', ]; - $this->assertSame($config->get('url.prefixes'), $expected_prefixes); + $this->assertSame($expected_prefixes, $config->get('url.prefixes')); $config = $this->config('language.types'); - $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); - $this->assertSame($config->get('configurable'), ['language_interface']); - $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); - $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $this->assertSame(['language_interface', 'language_content', 'language_url'], $config->get('all')); + $this->assertSame(['language_interface'], $config->get('configurable')); + $this->assertSame(['language-interface' => 0], $config->get('negotiation.language_content.enabled')); + $this->assertSame(['language-url' => 0, 'language-url-fallback' => 1], $config->get('negotiation.language_url.enabled')); $expected_language_interface = [ 'language-url' => 0, 'language-selected' => 1, ]; - $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + $this->assertSame($expected_language_interface, $config->get('negotiation.language_interface.enabled')); } /** @@ -68,19 +68,19 @@ public function testLanguageNegotiationWithNoNegotiation() { ]); $config = $this->config('language.negotiation'); - $this->assertSame($config->get('session.parameter'), 'language'); - $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); - $this->assertSame($config->get('selected_langcode'), 'site_default'); + $this->assertSame('language', $config->get('session.parameter')); + $this->assertSame(LanguageNegotiationUrl::CONFIG_PATH_PREFIX, $config->get('url.source')); + $this->assertSame('site_default', $config->get('selected_langcode')); $config = $this->config('language.types'); - $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); - $this->assertSame($config->get('configurable'), ['language_interface']); - $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); - $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $this->assertSame(['language_interface', 'language_content', 'language_url'], $config->get('all')); + $this->assertSame(['language_interface'], $config->get('configurable')); + $this->assertSame(['language-interface' => 0], $config->get('negotiation.language_content.enabled')); + $this->assertSame(['language-url' => 0, 'language-url-fallback' => 1], $config->get('negotiation.language_url.enabled')); $expected_language_interface = [ 'language-selected' => 0, ]; - $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + $this->assertSame($expected_language_interface, $config->get('negotiation.language_interface.enabled')); } /** @@ -100,28 +100,28 @@ public function testLanguageNegotiationWithPathPrefix() { ]); $config = $this->config('language.negotiation'); - $this->assertSame($config->get('session.parameter'), 'language'); - $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); - $this->assertSame($config->get('selected_langcode'), 'site_default'); + $this->assertSame('language', $config->get('session.parameter')); + $this->assertSame(LanguageNegotiationUrl::CONFIG_PATH_PREFIX, $config->get('url.source')); + $this->assertSame('site_default', $config->get('selected_langcode')); $expected_prefixes = [ 'en' => '', 'fr' => 'fr', 'zu' => 'zu', ]; - $this->assertSame($config->get('url.prefixes'), $expected_prefixes); + $this->assertSame($expected_prefixes, $config->get('url.prefixes')); $config = $this->config('language.types'); - $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); - $this->assertSame($config->get('configurable'), ['language_interface']); - $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); - $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $this->assertSame(['language_interface', 'language_content', 'language_url'], $config->get('all')); + $this->assertSame(['language_interface'], $config->get('configurable')); + $this->assertSame(['language-interface' => 0], $config->get('negotiation.language_content.enabled')); + $this->assertSame(['language-url' => 0, 'language-url-fallback' => 1], $config->get('negotiation.language_url.enabled')); $expected_language_interface = [ 'language-url' => 0, 'language-user' => 1, 'language-browser' => 2, 'language-selected' => 3, ]; - $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + $this->assertSame($expected_language_interface, $config->get('negotiation.language_interface.enabled')); } /** @@ -142,26 +142,26 @@ public function testLanguageNegotiationWithDomain() { global $base_url; $config = $this->config('language.negotiation'); - $this->assertSame($config->get('session.parameter'), 'language'); - $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_DOMAIN); - $this->assertSame($config->get('selected_langcode'), 'site_default'); + $this->assertSame('language', $config->get('session.parameter')); + $this->assertSame(LanguageNegotiationUrl::CONFIG_DOMAIN, $config->get('url.source')); + $this->assertSame('site_default', $config->get('selected_langcode')); $expected_domains = [ 'en' => parse_url($base_url, PHP_URL_HOST), 'fr' => 'fr.drupal.org', 'zu' => 'zu.drupal.org', ]; - $this->assertSame($config->get('url.domains'), $expected_domains); + $this->assertSame($expected_domains, $config->get('url.domains')); $config = $this->config('language.types'); - $this->assertSame($config->get('all'), ['language_interface', 'language_content', 'language_url']); - $this->assertSame($config->get('configurable'), ['language_interface']); - $this->assertSame($config->get('negotiation.language_content.enabled'), ['language-interface' => 0]); - $this->assertSame($config->get('negotiation.language_url.enabled'), ['language-url' => 0, 'language-url-fallback' => 1]); + $this->assertSame(['language_interface', 'language_content', 'language_url'], $config->get('all')); + $this->assertSame(['language_interface'], $config->get('configurable')); + $this->assertSame(['language-interface' => 0], $config->get('negotiation.language_content.enabled')); + $this->assertSame(['language-url' => 0, 'language-url-fallback' => 1], $config->get('negotiation.language_url.enabled')); $expected_language_interface = [ 'language-url' => 0, 'language-selected' => 1, ]; - $this->assertSame($config->get('negotiation.language_interface.enabled'), $expected_language_interface); + $this->assertSame($expected_language_interface, $config->get('negotiation.language_interface.enabled')); } } diff --git a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateDefaultLanguageTest.php b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateDefaultLanguageTest.php index ac7ca2e..2a11b25 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateDefaultLanguageTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateDefaultLanguageTest.php @@ -45,10 +45,10 @@ public function testMigrationWithNonExistentLanguage() { $count = 0; foreach ($messages as $message) { $count++; - $this->assertSame($message->message, "The language 'tv' does not exist on this site."); - $this->assertSame((int) $message->level, MigrationInterface::MESSAGE_ERROR); + $this->assertSame("The language 'tv' does not exist on this site.", $message->message); + $this->assertSame(MigrationInterface::MESSAGE_ERROR, (int) $message->level); } - $this->assertSame($count, 1); + $this->assertSame(1, $count); } /** diff --git a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php index 09681de..fe6296c 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php @@ -28,10 +28,10 @@ public function testLanguageTypes() { ]); $config = $this->config('language.types'); - $this->assertSame($config->get('all'), ['language_content', 'language_url', 'language_interface']); - $this->assertSame($config->get('configurable'), ['language_interface']); - $this->assertSame($config->get('negotiation.language_content'), ['enabled' => ['language-interface' => 0]]); - $this->assertSame($config->get('negotiation.language_url'), ['enabled' => ['language-url' => 0, 'language-url-fallback' => 1]]); + $this->assertSame(['language_content', 'language_url', 'language_interface'], $config->get('all')); + $this->assertSame(['language_interface'], $config->get('configurable')); + $this->assertSame(['enabled' => ['language-interface' => 0]], $config->get('negotiation.language_content')); + $this->assertSame(['enabled' => ['language-url' => 0, 'language-url-fallback' => 1]], $config->get('negotiation.language_url')); $expected_language_interface = [ 'enabled' => [ 'language-url' => -9, @@ -46,7 +46,7 @@ public function testLanguageTypes() { 'language-selected' => -6, ], ]; - $this->assertSame($config->get('negotiation.language_interface'), $expected_language_interface); + $this->assertSame($expected_language_interface, $config->get('negotiation.language_interface')); } /** @@ -60,14 +60,14 @@ public function testLanguageNegotiationWithPrefix() { ]); $config = $this->config('language.negotiation'); - $this->assertSame($config->get('session.parameter'), 'language'); - $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); - $this->assertSame($config->get('selected_langcode'), 'site_default'); + $this->assertSame('language', $config->get('session.parameter')); + $this->assertSame(LanguageNegotiationUrl::CONFIG_PATH_PREFIX, $config->get('url.source')); + $this->assertSame('site_default', $config->get('selected_langcode')); $expected_prefixes = [ 'en' => '', 'is' => 'is', ]; - $this->assertSame($config->get('url.prefixes'), $expected_prefixes); + $this->assertSame($expected_prefixes, $config->get('url.prefixes')); } /** @@ -87,14 +87,14 @@ public function testLanguageNegotiationWithDomain() { global $base_url; $config = $this->config('language.negotiation'); - $this->assertSame($config->get('session.parameter'), 'language'); - $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_DOMAIN); - $this->assertSame($config->get('selected_langcode'), 'site_default'); + $this->assertSame('language', $config->get('session.parameter')); + $this->assertSame(LanguageNegotiationUrl::CONFIG_DOMAIN, $config->get('url.source')); + $this->assertSame('site_default', $config->get('selected_langcode')); $expected_domains = [ 'en' => parse_url($base_url, PHP_URL_HOST), 'is' => 'is.drupal.org', ]; - $this->assertSame($config->get('url.domains'), $expected_domains); + $this->assertSame($expected_domains, $config->get('url.domains')); } /** @@ -112,14 +112,14 @@ public function testLanguageNegotiationWithNonExistentVariables() { ]); $config = $this->config('language.negotiation'); - $this->assertSame($config->get('session.parameter'), 'language'); - $this->assertSame($config->get('url.source'), LanguageNegotiationUrl::CONFIG_PATH_PREFIX); - $this->assertSame($config->get('selected_langcode'), 'site_default'); + $this->assertSame('language', $config->get('session.parameter')); + $this->assertSame(LanguageNegotiationUrl::CONFIG_PATH_PREFIX, $config->get('url.source')); + $this->assertSame('site_default', $config->get('selected_langcode')); $expected_prefixes = [ 'en' => '', 'is' => 'is', ]; - $this->assertSame($config->get('url.prefixes'), $expected_prefixes); + $this->assertSame($expected_prefixes, $config->get('url.prefixes')); } } diff --git a/core/modules/layout_builder/config/schema/layout_builder.schema.yml b/core/modules/layout_builder/config/schema/layout_builder.schema.yml index b870007..682caa7 100644 --- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml @@ -5,3 +5,42 @@ core.entity_view_display.*.*.*.third_party.layout_builder: allow_custom: type: boolean label: 'Allow a customized layout' + sections: + type: sequence + sequence: + type: layout_builder.section + +layout_builder.section: + type: mapping + label: 'Layout section' + mapping: + layout_id: + type: string + label: 'Layout ID' + layout_settings: + type: layout_plugin.settings.[%parent.layout_id] + label: 'Layout settings' + components: + type: sequence + label: 'Components' + sequence: + type: layout_builder.component + +layout_builder.component: + type: mapping + label: 'Component' + mapping: + configuration: + type: block.settings.[id] + region: + type: string + label: 'Region' + uuid: + type: uuid + label: 'UUID' + weight: + type: integer + label: 'Weight' + additional: + type: ignore + label: 'Additional data' diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css index 11d8c55..9d4bc90 100644 --- a/core/modules/layout_builder/css/layout-builder.css +++ b/core/modules/layout_builder/css/layout-builder.css @@ -22,8 +22,8 @@ .layout-section .remove-section { position: relative; - background: url(../../../misc/icons/bebebe/ex.svg) #ffffff center center / 16px 16px no-repeat; - border: 1px solid #cccccc; + background: url(../../../misc/icons/bebebe/ex.svg) #fff center center / 16px 16px no-repeat; + border: 1px solid #ccc; box-sizing: border-box; font-size: 1rem; padding: 0; diff --git a/core/modules/layout_builder/layout_builder.info.yml b/core/modules/layout_builder/layout_builder.info.yml index e985911..d4ce72e 100644 --- a/core/modules/layout_builder/layout_builder.info.yml +++ b/core/modules/layout_builder/layout_builder.info.yml @@ -7,3 +7,5 @@ core: 8.x dependencies: - layout_discovery - contextual + # @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999. + - field_ui diff --git a/core/modules/layout_builder/layout_builder.install b/core/modules/layout_builder/layout_builder.install new file mode 100644 index 0000000..acb1e4f --- /dev/null +++ b/core/modules/layout_builder/layout_builder.install @@ -0,0 +1,40 @@ +getThirdPartySettings('field_layout'); + if (isset($field_layout['id'])) { + $field_layout += ['settings' => []]; + $display->appendSection(new Section($field_layout['id'], $field_layout['settings'])); + } + + // Sort the components by weight. + $components = $display->get('content'); + uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); + foreach ($components as $name => $component) { + $display->setComponent($name, $component); + } + $display->save(); + } + + // Clear the rendered cache to ensure the new layout builder flow is used. + // While in many cases the above change will not affect the rendered output, + // the cacheability metadata will have changed and should be processed to + // prepare for future changes. + Cache::invalidateTags(['rendered']); +} diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 895145d..b1ecb5d 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -5,17 +5,14 @@ * Provides hook implementations for Layout Builder. */ -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\Context\Context; -use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; +use Drupal\field\FieldConfigInterface; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage; +use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm; /** * Implements hook_help(). @@ -52,17 +49,18 @@ function layout_builder_entity_type_alter(array &$entity_types) { $entity_type->setLinkTemplate('layout-builder', $entity_type->getLinkTemplate('canonical') . '/layout'); } } + $entity_types['entity_view_display'] + ->setClass(LayoutBuilderEntityViewDisplay::class) + ->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class) + ->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class); } /** - * Removes the Layout Builder field both visually and from the #fields handling. - * - * This prevents any interaction with this field. It is rendered directly - * in layout_builder_entity_view_alter(). - * - * @internal + * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm. */ -function _layout_builder_hide_layout_field(array &$form) { +function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) { + // Hides the Layout Builder field. It is rendered directly in + // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple(). unset($form['fields']['layout_builder__layout']); $key = array_search('layout_builder__layout', $form['#fields']); if ($key !== FALSE) { @@ -71,140 +69,21 @@ function _layout_builder_hide_layout_field(array &$form) { } /** - * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm. - */ -function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) { - _layout_builder_hide_layout_field($form); -} - -/** - * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityViewDisplayEditForm. - */ -function layout_builder_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) { - /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */ - $display = $form_state->getFormObject()->getEntity(); - $entity_type = \Drupal::entityTypeManager()->getDefinition($display->getTargetEntityTypeId()); - - _layout_builder_hide_layout_field($form); - - // @todo Expand to work for all view modes in - // https://www.drupal.org/node/2907413. - if (!in_array($display->getMode(), ['full', 'default'], TRUE)) { - return; - } - - $form['layout'] = [ - '#type' => 'details', - '#open' => TRUE, - '#title' => t('Layout options'), - '#tree' => TRUE, - ]; - // @todo Unchecking this box is a destructive action, this should be made - // clear to the user in https://www.drupal.org/node/2914484. - $form['layout']['allow_custom'] = [ - '#type' => 'checkbox', - '#title' => t('Allow each @entity to have its layout customized.', [ - '@entity' => $entity_type->getSingularLabel(), - ]), - '#default_value' => $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE), - ]; - - $form['#entity_builders'][] = 'layout_builder_form_entity_view_display_edit_entity_builder'; -} - -/** - * Entity builder for layout options on the entity view display form. - * - * @see layout_builder_form_entity_view_display_edit_form_alter() - */ -function layout_builder_form_entity_view_display_edit_entity_builder($entity_type_id, EntityViewDisplayInterface $display, &$form, FormStateInterface &$form_state) { - $new_value = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE); - $display->setThirdPartySetting('layout_builder', 'allow_custom', $new_value); -} - -/** - * Implements hook_ENTITY_TYPE_presave(). - */ -function layout_builder_entity_view_display_presave(EntityViewDisplayInterface $display) { - $original_value = isset($display->original) ? $display->original->getThirdPartySetting('layout_builder', 'allow_custom', FALSE) : FALSE; - $new_value = $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE); - if ($original_value !== $new_value) { - $entity_type_id = $display->getTargetEntityTypeId(); - $bundle = $display->getTargetBundle(); - - if ($new_value) { - layout_builder_add_layout_section_field($entity_type_id, $bundle); - } - elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) { - $field->delete(); - } - } -} - -/** - * Adds a layout section field to a given bundle. - * - * @param string $entity_type_id - * The entity type ID. - * @param string $bundle - * The bundle. - * @param string $field_name - * (optional) The name for the layout section field. Defaults to - * 'layout_builder__layout'. - * - * @return \Drupal\field\FieldConfigInterface - * A layout section field. + * Implements hook_field_config_insert(). */ -function layout_builder_add_layout_section_field($entity_type_id, $bundle, $field_name = 'layout_builder__layout') { - $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name); - if (!$field) { - $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name); - if (!$field_storage) { - $field_storage = FieldStorageConfig::create([ - 'entity_type' => $entity_type_id, - 'field_name' => $field_name, - 'type' => 'layout_section', - ]); - $field_storage->save(); - } - - $field = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => $bundle, - 'label' => t('Layout'), - ]); - $field->save(); - } - return $field; +function layout_builder_field_config_insert(FieldConfigInterface $field_config) { + // Clear the sample entity for this entity type and bundle. + $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator'); + $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle()); + \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); } /** - * Implements hook_entity_view_alter(). + * Implements hook_field_config_delete(). */ -function layout_builder_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { - if ($display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE) && !$entity->layout_builder__layout->isEmpty()) { - $contexts = \Drupal::service('context.repository')->getAvailableContexts(); - // @todo Use EntityContextDefinition after resolving - // https://www.drupal.org/node/2932462. - $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity); - $sections = $entity->layout_builder__layout->getSections(); - foreach ($sections as $delta => $section) { - $build['_layout_builder'][$delta] = $section->toRenderArray($contexts); - } - - // If field layout is active, that is all that needs to be removed. - if (\Drupal::moduleHandler()->moduleExists('field_layout') && isset($build['_field_layout'])) { - unset($build['_field_layout']); - return; - } - - /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */ - $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle()); - // Remove all display-configurable fields. - foreach (array_keys($display->getComponents()) as $name) { - if ($name !== 'layout_builder__layout' && isset($field_definitions[$name]) && $field_definitions[$name]->isDisplayConfigurable('view')) { - unset($build[$name]); - } - } - } +function layout_builder_field_config_delete(FieldConfigInterface $field_config) { + // Clear the sample entity for this entity type and bundle. + $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator'); + $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle()); + \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); } diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index 9846553..322e230 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -115,6 +115,3 @@ layout_builder.move_block: parameters: section_storage: layout_builder_tempstore: TRUE - -route_callbacks: - - 'layout_builder.routes:getRoutes' diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index db6a1c1..778a6d5 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -6,23 +6,28 @@ services: class: Drupal\layout_builder\Access\LayoutSectionAccessCheck tags: - { name: access_check, applies_to: _has_layout_section } + plugin.manager.layout_builder.section_storage: + class: Drupal\layout_builder\SectionStorage\SectionStorageManager + parent: default_plugin_manager layout_builder.routes: class: Drupal\layout_builder\Routing\LayoutBuilderRoutes - arguments: ['@entity_type.manager', '@entity_field.manager'] + arguments: ['@plugin.manager.layout_builder.section_storage'] + tags: + - { name: event_subscriber } layout_builder.route_enhancer: class: Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer tags: - { name: route_enhancer } layout_builder.param_converter: class: Drupal\layout_builder\Routing\LayoutTempstoreParamConverter - arguments: ['@layout_builder.tempstore_repository', '@class_resolver'] + arguments: ['@layout_builder.tempstore_repository', '@plugin.manager.layout_builder.section_storage'] tags: - { name: paramconverter, priority: 10 } - layout_builder.section_storage_param_converter.overrides: - class: Drupal\layout_builder\Routing\SectionStorageOverridesParamConverter - arguments: ['@entity.manager'] cache_context.layout_builder_is_active: class: Drupal\layout_builder\Cache\LayoutBuilderIsActiveCacheContext arguments: ['@current_route_match'] tags: - { name: cache.context} + layout_builder.sample_entity_generator: + class: Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator + arguments: ['@tempstore.shared', '@entity_type.manager'] diff --git a/core/modules/layout_builder/src/Annotation/SectionStorage.php b/core/modules/layout_builder/src/Annotation/SectionStorage.php new file mode 100644 index 0000000..42f4a47 --- /dev/null +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php @@ -0,0 +1,32 @@ +definition); + } + +} diff --git a/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php b/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php index c632f4b..3c3bc25 100644 --- a/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php +++ b/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php @@ -4,8 +4,8 @@ use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\Context\CalculatedCacheContextInterface; -use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\layout_builder\OverridesSectionStorageInterface; /** * Determines whether Layout Builder is active for a given entity type or not. @@ -49,7 +49,7 @@ public function getContext($entity_type_id = NULL) { } $display = $this->getDisplay($entity_type_id); - return ($display && $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE)) ? '1' : '0'; + return ($display && $display->isOverridable()) ? '1' : '0'; } /** @@ -72,15 +72,15 @@ public function getCacheableMetadata($entity_type_id = NULL) { * * @param string $entity_type_id * The entity type ID. - * @param string $view_mode - * (optional) The view mode that should be used to render the entity. * - * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface|null + * @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface|null * The entity view display, if it exists. */ - protected function getDisplay($entity_type_id, $view_mode = 'full') { + protected function getDisplay($entity_type_id) { if ($entity = $this->routeMatch->getParameter($entity_type_id)) { - return EntityViewDisplay::collectRenderDisplay($entity, $view_mode); + if ($entity instanceof OverridesSectionStorageInterface) { + return $entity->getDefaultSectionStorage(); + } } } diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php index 5ccddfa..8f7666f 100644 --- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php @@ -3,11 +3,13 @@ namespace Drupal\layout_builder\Controller; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\layout_builder\Context\LayoutBuilderContextTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\OverridesSectionStorageInterface; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,13 +33,23 @@ class LayoutBuilderController implements ContainerInjectionInterface { protected $layoutTempstoreRepository; /** + * The messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** * LayoutBuilderController constructor. * * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository * The layout tempstore repository. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. */ - public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) { + public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) { $this->layoutTempstoreRepository = $layout_tempstore_repository; + $this->messenger = $messenger; } /** @@ -45,7 +57,8 @@ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore */ public static function create(ContainerInterface $container) { return new static( - $container->get('layout_builder.tempstore_repository') + $container->get('layout_builder.tempstore_repository'), + $container->get('messenger') ); } @@ -101,9 +114,16 @@ public function layout(SectionStorageInterface $section_storage, $is_rebuilding * Indicates if the layout is rebuilding. */ protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) { - // For a new layout, begin with a single section of one column. + // Only add sections if the layout is new and empty. if (!$is_rebuilding && $section_storage->count() === 0) { $sections = []; + // If this is an empty override, copy the sections from the corresponding + // default. + if ($section_storage instanceof OverridesSectionStorageInterface) { + $sections = $section_storage->getDefaultSectionStorage()->getSections(); + } + + // For an empty layout, begin with a single section of one column. if (!$sections) { $sections[] = new Section('layout_onecol'); } @@ -172,7 +192,7 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $section = $section_storage->getSection($delta); $layout = $section->getLayout(); - $build = $section->toRenderArray($this->getAvailableContexts($section_storage)); + $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE); $layout_definition = $layout->getPluginDefinition(); foreach ($layout_definition->getRegions() as $region => $info) { @@ -277,7 +297,15 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s public function saveLayout(SectionStorageInterface $section_storage) { $section_storage->save(); $this->layoutTempstoreRepository->delete($section_storage); - return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString()); + + if ($section_storage instanceof OverridesSectionStorageInterface) { + $this->messenger->addMessage($this->t('The layout override has been saved.')); + } + else { + $this->messenger->addMessage($this->t('The layout has been saved.')); + } + + return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString()); } /** @@ -291,7 +319,10 @@ public function saveLayout(SectionStorageInterface $section_storage) { */ public function cancelLayout(SectionStorageInterface $section_storage) { $this->layoutTempstoreRepository->delete($section_storage); - return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString()); + + $this->messenger->addMessage($this->t('The changes to the layout have been discarded.')); + + return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString()); } } diff --git a/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php b/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php new file mode 100644 index 0000000..9397c6c --- /dev/null +++ b/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php @@ -0,0 +1,33 @@ +getThirdPartySetting('layout_builder', 'allow_custom', FALSE); + } + + /** + * {@inheritdoc} + */ + public function setOverridable($overridable = TRUE) { + $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSections() { + return $this->getThirdPartySetting('layout_builder', 'sections', []); + } + + /** + * {@inheritdoc} + */ + protected function setSections(array $sections) { + $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections)); + return $this; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + $original_value = isset($this->original) ? $this->original->isOverridable() : FALSE; + $new_value = $this->isOverridable(); + if ($original_value !== $new_value) { + $entity_type_id = $this->getTargetEntityTypeId(); + $bundle = $this->getTargetBundle(); + + if ($new_value) { + $this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout'); + } + elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) { + $field->delete(); + } + } + } + + /** + * Adds a layout section field to a given bundle. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle + * The bundle. + * @param string $field_name + * The name for the layout section field. + */ + protected function addSectionField($entity_type_id, $bundle, $field_name) { + $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name); + if (!$field) { + $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name); + if (!$field_storage) { + $field_storage = FieldStorageConfig::create([ + 'entity_type' => $entity_type_id, + 'field_name' => $field_name, + 'type' => 'layout_section', + 'locked' => TRUE, + ]); + $field_storage->save(); + } + + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => $bundle, + 'label' => t('Layout'), + ]); + $field->save(); + } + } + + /** + * {@inheritdoc} + */ + protected function getDefaultRegion() { + if ($this->hasSection(0)) { + return $this->getSection(0)->getDefaultRegion(); + } + + return parent::getDefaultRegion(); + } + + /** + * Wraps the context repository service. + * + * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface + * The context repository service. + */ + protected function contextRepository() { + return \Drupal::service('context.repository'); + } + + /** + * {@inheritdoc} + */ + public function buildMultiple(array $entities) { + $build_list = parent::buildMultiple($entities); + + foreach ($entities as $id => $entity) { + $sections = $this->getRuntimeSections($entity); + if ($sections) { + foreach ($build_list[$id] as $name => $build_part) { + $field_definition = $this->getFieldDefinition($name); + if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) { + unset($build_list[$id][$name]); + } + } + + // Bypass ::getContexts() in order to use the runtime entity, not a + // sample entity. + $contexts = $this->contextRepository()->getAvailableContexts(); + // @todo Use EntityContextDefinition after resolving + // https://www.drupal.org/node/2932462. + $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity); + foreach ($sections as $delta => $section) { + $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts); + } + } + } + + return $build_list; + } + + /** + * Gets the runtime sections for a given entity. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity. + * + * @return \Drupal\layout_builder\Section[] + * The sections. + */ + protected function getRuntimeSections(FieldableEntityInterface $entity) { + if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) { + return $entity->get('layout_builder__layout')->getSections(); + } + + return $this->getSections(); + } + + /** + * {@inheritdoc} + * + * @todo Move this upstream in https://www.drupal.org/node/2939931. + */ + public function label() { + $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId()); + $bundle_label = $bundle_info[$this->getTargetBundle()]['label']; + $target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId()); + return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + foreach ($this->getSections() as $delta => $section) { + foreach ($section->getComponents() as $uuid => $component) { + $this->calculatePluginDependencies($component->getPlugin()); + } + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + + // Loop through all components and determine if the removed dependencies are + // used by their plugins. + foreach ($this->getSections() as $delta => $section) { + foreach ($section->getComponents() as $uuid => $component) { + $plugin_dependencies = $this->getPluginDependencies($component->getPlugin()); + $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies); + if ($component_removed_dependencies) { + // @todo Allow the plugins to react to their dependency removal in + // https://www.drupal.org/project/drupal/issues/2579743. + $section->removeComponent($uuid); + $changed = TRUE; + } + } + } + return $changed; + } + + /** + * Calculates and returns dependencies of a specific plugin instance. + * + * @param \Drupal\Component\Plugin\PluginInspectionInterface $instance + * The plugin instance. + * + * @return array + * An array of dependencies keyed by the type of dependency. + * + * @todo Replace this in https://www.drupal.org/project/drupal/issues/2939925. + */ + protected function getPluginDependencies(PluginInspectionInterface $instance) { + $definition = $instance->getPluginDefinition(); + $dependencies['module'][] = $definition['provider']; + // Plugins can declare additional dependencies in their definition. + if (isset($definition['config_dependencies'])) { + $dependencies = NestedArray::mergeDeep($dependencies, $definition['config_dependencies']); + } + + // If a plugin is dependent, calculate its dependencies. + if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) { + $dependencies = NestedArray::mergeDeep($dependencies, $plugin_dependencies); + } + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function setComponent($name, array $options = []) { + parent::setComponent($name, $options); + + // @todo Remove workaround for EntityViewBuilder::getSingleFieldDisplay() in + // https://www.drupal.org/project/drupal/issues/2936464. + if ($this->getMode() === static::CUSTOM_MODE) { + return $this; + } + + // Retrieve the updated options after the parent:: call. + $options = $this->content[$name]; + // Provide backwards compatibility by converting to a section component. + $field_definition = $this->getFieldDefinition($name); + if ($field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type'])) { + $configuration = []; + $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name; + $configuration['label_display'] = FALSE; + $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']); + $configuration['formatter'] = array_intersect_key($options, $keys); + $configuration['context_mapping']['entity'] = 'layout_builder.entity'; + + $section = $this->getDefaultSection(); + $region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion(); + $new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration)); + $section->appendComponent($new_component); + } + return $this; + } + + /** + * Gets a default section. + * + * @return \Drupal\layout_builder\Section + * The default section. + */ + protected function getDefaultSection() { + // If no section exists, append a new one. + if (!$this->hasSection(0)) { + $this->appendSection(new Section('layout_onecol')); + } + + // Return the first section. + return $this->getSection(0); + } + +} diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php new file mode 100644 index 0000000..e86df9d --- /dev/null +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php @@ -0,0 +1,60 @@ +toArray(); + }, $record['third_party_settings']['layout_builder']['sections']); + } + return $record; + } + + /** + * {@inheritdoc} + */ + protected function mapFromStorageRecords(array $records) { + foreach ($records as $id => &$record) { + if (!empty($record['third_party_settings']['layout_builder']['sections'])) { + $sections = &$record['third_party_settings']['layout_builder']['sections']; + foreach ($sections as $section_delta => $section) { + $sections[$section_delta] = new Section( + $section['layout_id'], + $section['layout_settings'], + array_map(function (array $component) { + return (new SectionComponent( + $component['uuid'], + $component['region'], + $component['configuration'], + $component['additional'] + ))->setWeight($component['weight']); + }, $section['components']) + ); + } + } + } + return parent::mapFromStorageRecords($records); + } + +} diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderSampleEntityGenerator.php b/core/modules/layout_builder/src/Entity/LayoutBuilderSampleEntityGenerator.php new file mode 100644 index 0000000..9f60607 --- /dev/null +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderSampleEntityGenerator.php @@ -0,0 +1,91 @@ +tempStoreFactory = $temp_store_factory; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * Gets a sample entity for a given entity type and bundle. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle_id + * The bundle ID. + * + * @return \Drupal\Core\Entity\EntityInterface + * An entity. + */ + public function get($entity_type_id, $bundle_id) { + $tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity'); + if ($entity = $tempstore->get("$entity_type_id.$bundle_id")) { + return $entity; + } + + $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); + if (!$entity_storage instanceof ContentEntityStorageInterface) { + throw new \InvalidArgumentException(sprintf('The "%s" entity storage is not supported', $entity_type_id)); + } + + $entity = $entity_storage->createWithSampleValues($bundle_id); + // Mark the sample entity as being a preview. + $entity->in_preview = TRUE; + $tempstore->set("$entity_type_id.$bundle_id", $entity); + return $entity; + } + + /** + * Deletes a sample entity for a given entity type and bundle. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle_id + * The bundle ID. + * + * @return $this + */ + public function delete($entity_type_id, $bundle_id) { + $tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity'); + $tempstore->delete("$entity_type_id.$bundle_id"); + return $this; + } + +} diff --git a/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php new file mode 100644 index 0000000..b8fd21d --- /dev/null +++ b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php @@ -0,0 +1,36 @@ +get($delta)) { - /** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */ - $item = $this->createItem($delta); - $item->section = $section; - - $start = array_slice($this->list, 0, $delta); - $end = array_slice($this->list, $delta); - $this->list = array_merge($start, [$item], $end); - } - else { - $this->appendSection($section); - } - return $this; - } - - /** - * {@inheritdoc} - */ - public function appendSection(Section $section) { - $this->appendItem()->section = $section; - return $this; - } + use SectionStorageTrait; /** * {@inheritdoc} @@ -60,80 +32,21 @@ public function getSections() { /** * {@inheritdoc} */ - public function getSection($delta) { + protected function setSections(array $sections) { + $this->list = []; + $sections = array_values($sections); /** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */ - if (!$item = $this->get($delta)) { - throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" entity', $delta, $this->getEntity()->label())); + foreach ($sections as $section) { + $item = $this->appendItem(); + $item->section = $section; } - return $item->section; - } - - /** - * {@inheritdoc} - */ - public function removeSection($delta) { - $this->removeItem($delta); return $this; } /** * {@inheritdoc} */ - public function getContexts() { - $entity = $this->getEntity(); - // @todo Use EntityContextDefinition after resolving - // https://www.drupal.org/node/2932462. - $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity); - return $contexts; - } - - /** - * {@inheritdoc} - */ - public function getStorageType() { - return 'overrides'; - } - - /** - * {@inheritdoc} - */ - public function getStorageId() { - $entity = $this->getEntity(); - return $entity->getEntityTypeId() . ':' . $entity->id(); - } - - /** - * {@inheritdoc} - */ - public function label() { - return $this->getEntity()->label(); - } - - /** - * {@inheritdoc} - */ - public function save() { - return $this->getEntity()->save(); - } - - /** - * {@inheritdoc} - */ - public function getCanonicalUrl() { - return $this->getEntity()->toUrl('canonical'); - } - - /** - * {@inheritdoc} - */ - public function getLayoutBuilderUrl() { - return $this->getEntity()->toUrl('layout-builder'); - } - - /** - * {@inheritdoc} - */ public function __wakeup() { // Ensure the entity is updated with the latest value. $this->getEntity()->set($this->getName(), $this->getValue()); diff --git a/core/modules/layout_builder/src/Form/AddBlockForm.php b/core/modules/layout_builder/src/Form/AddBlockForm.php index 83effd6..704d136 100644 --- a/core/modules/layout_builder/src/Form/AddBlockForm.php +++ b/core/modules/layout_builder/src/Form/AddBlockForm.php @@ -2,8 +2,9 @@ namespace Drupal\layout_builder\Form; -use Drupal\layout_builder\Section; +use Drupal\Core\Form\FormStateInterface; use Drupal\layout_builder\SectionComponent; +use Drupal\layout_builder\SectionStorageInterface; /** * Provides a form to add a block. @@ -27,10 +28,32 @@ protected function submitLabel() { } /** - * {@inheritdoc} + * Builds the form for the block. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param \Drupal\layout_builder\SectionStorageInterface $section_storage + * The section storage being configured. + * @param int $delta + * The delta of the section. + * @param string $region + * The region of the block. + * @param string|null $plugin_id + * The plugin ID of the block to add. + * + * @return array + * The form array. */ - protected function submitBlock(Section $section, $region, $uuid, array $configuration) { - $section->appendComponent(new SectionComponent($uuid, $region, $configuration)); + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL) { + // Only generate a new component once per form submission. + if (!$component = $form_state->getTemporaryValue('layout_builder__component')) { + $component = new SectionComponent($this->uuidGenerator->generate(), $region, ['id' => $plugin_id]); + $section_storage->getSection($delta)->appendComponent($component); + $form_state->setTemporaryValue('layout_builder__component', $component); + } + return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component); } } diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php index b75e88f..e1103e9 100644 --- a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php +++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php @@ -17,7 +17,7 @@ use Drupal\layout_builder\Context\LayoutBuilderContextTrait; use Drupal\layout_builder\Controller\LayoutRebuildTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; -use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionComponent; use Drupal\layout_builder\SectionStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -59,7 +59,7 @@ * * @var \Drupal\Component\Uuid\UuidInterface */ - protected $uuid; + protected $uuidGenerator; /** * The plugin form manager. @@ -83,6 +83,13 @@ protected $region; /** + * The UUID of the component. + * + * @var string + */ + protected $uuid; + + /** * The section storage. * * @var \Drupal\layout_builder\SectionStorageInterface @@ -109,7 +116,7 @@ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore $this->layoutTempstoreRepository = $layout_tempstore_repository; $this->contextRepository = $context_repository; $this->blockManager = $block_manager; - $this->uuid = $uuid; + $this->uuidGenerator = $uuid; $this->classResolver = $class_resolver; $this->pluginFormFactory = $plugin_form_manager; } @@ -129,25 +136,6 @@ public static function create(ContainerInterface $container) { } /** - * Prepares the block plugin based on the block ID. - * - * @param string $block_id - * Either a block ID, or the plugin ID used to create a new block. - * @param array $configuration - * The block configuration. - * - * @return \Drupal\Core\Block\BlockPluginInterface - * The block plugin. - */ - protected function prepareBlock($block_id, array $configuration) { - if (!isset($configuration['uuid'])) { - $configuration['uuid'] = $this->uuid->generate(); - } - - return $this->blockManager->createInstance($block_id, $configuration); - } - - /** * Builds the form for the block. * * @param array $form @@ -158,21 +146,17 @@ protected function prepareBlock($block_id, array $configuration) { * The section storage being configured. * @param int $delta * The delta of the section. - * @param string $region - * The region of the block. - * @param string|null $plugin_id - * The plugin ID of the block to add. - * @param array $configuration - * (optional) The array of configuration for the block. + * @param \Drupal\layout_builder\SectionComponent $component + * The section component containing the block. * * @return array * The form array. */ - public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL, array $configuration = []) { + public function doBuildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, SectionComponent $component = NULL) { $this->sectionStorage = $section_storage; $this->delta = $delta; - $this->region = $region; - $this->block = $this->prepareBlock($plugin_id, $configuration); + $this->uuid = $component->getUuid(); + $this->block = $component->getPlugin(); $form_state->setTemporaryValue('gathered_contexts', $this->getAvailableContexts($section_storage)); @@ -205,20 +189,6 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt abstract protected function submitLabel(); /** - * Handles the submission of a block. - * - * @param \Drupal\layout_builder\Section $section - * The layout section. - * @param string $region - * The region name. - * @param string $uuid - * The UUID of the block. - * @param array $configuration - * The block configuration. - */ - abstract protected function submitBlock(Section $section, $region, $uuid, array $configuration); - - /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { @@ -242,7 +212,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $configuration = $this->block->getConfiguration(); $section = $this->sectionStorage->getSection($this->delta); - $this->submitBlock($section, $this->region, $configuration['uuid'], $configuration); + $section->getComponent($this->uuid)->setConfiguration($configuration); $this->layoutTempstoreRepository->set($this->sectionStorage); $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl()); diff --git a/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php new file mode 100644 index 0000000..63ec398 --- /dev/null +++ b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php @@ -0,0 +1,137 @@ +sectionStorage = $section_storage; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + // Hide the table of fields. + $form['fields']['#access'] = FALSE; + $form['#fields'] = []; + $form['#extra'] = []; + + $form['manage_layout'] = [ + '#type' => 'link', + '#title' => $this->t('Manage layout'), + '#weight' => -10, + '#attributes' => ['class' => ['button']], + '#url' => $this->sectionStorage->getLayoutBuilderUrl(), + ]; + + // @todo Expand to work for all view modes in + // https://www.drupal.org/node/2907413. + if ($this->entity->getMode() === 'default') { + $form['layout'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Layout options'), + '#tree' => TRUE, + ]; + + $entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId()); + $form['layout']['allow_custom'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow each @entity to have its layout customized.', [ + '@entity' => $entity_type->getSingularLabel(), + ]), + '#default_value' => $this->entity->isOverridable(), + ]; + // Prevent turning off overrides while any exist. + if ($this->hasOverrides($this->entity)) { + $form['layout']['allow_custom']['#disabled'] = TRUE; + $form['layout']['allow_custom']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.'); + } + else { + $form['#entity_builders'][] = '::entityFormEntityBuild'; + } + } + return $form; + } + + /** + * Determines if the defaults have any overrides. + * + * @param \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display + * The entity display. + * + * @return bool + * TRUE if there are any overrides of this default, FALSE otherwise. + */ + protected function hasOverrides(LayoutEntityDisplayInterface $display) { + if (!$display->isOverridable()) { + return FALSE; + } + + $entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId()); + $query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery() + ->exists('layout_builder__layout'); + if ($bundle_key = $entity_type->getKey('bundle')) { + $query->condition($bundle_key, $display->getTargetBundle()); + } + return (bool) $query->count()->execute(); + } + + /** + * Entity builder for layout options on the entity view display form. + */ + public function entityFormEntityBuild($entity_type_id, LayoutEntityDisplayInterface $display, &$form, FormStateInterface &$form_state) { + $new_value = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE); + $display->setOverridable($new_value); + } + + /** + * {@inheritdoc} + */ + protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + protected function buildExtraFieldRow($field_id, $extra_field) { + // Intentionally empty. + } + +} diff --git a/core/modules/layout_builder/src/Form/RevertOverridesForm.php b/core/modules/layout_builder/src/Form/RevertOverridesForm.php new file mode 100644 index 0000000..b6d07d9 --- /dev/null +++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php @@ -0,0 +1,117 @@ +layoutTempstoreRepository = $layout_tempstore_repository; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('layout_builder.tempstore_repository'), + $container->get('messenger') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'layout_builder_revert_overrides'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to revert this to defaults?'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Revert'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->sectionStorage->getLayoutBuilderUrl(); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) { + if (!$section_storage instanceof OverridesSectionStorageInterface) { + throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide overrides', $section_storage->getStorageType(), $section_storage->getStorageId())); + } + + $this->sectionStorage = $section_storage; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Remove all sections. + while ($this->sectionStorage->count()) { + $this->sectionStorage->removeSection(0); + } + $this->sectionStorage->save(); + $this->layoutTempstoreRepository->delete($this->sectionStorage); + + $this->messenger->addMessage($this->t('The layout has been reverted back to defaults.')); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/core/modules/layout_builder/src/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php index afca0d2..c00b406 100644 --- a/core/modules/layout_builder/src/Form/UpdateBlockForm.php +++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php @@ -2,9 +2,7 @@ namespace Drupal\layout_builder\Form; -use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionStorageInterface; /** @@ -36,19 +34,13 @@ public function getFormId() { * The region of the block. * @param string $uuid * The UUID of the block being updated. - * @param array $configuration - * (optional) The array of configuration for the block. * * @return array * The form array. */ - public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL, array $configuration = []) { - $plugin = $section_storage->getSection($delta)->getComponent($uuid)->getPlugin(); - if ($plugin instanceof ConfigurablePluginInterface) { - $configuration = $plugin->getConfiguration(); - } - - return parent::buildForm($form, $form_state, $section_storage, $delta, $region, $plugin->getPluginId(), $configuration); + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) { + $component = $section_storage->getSection($delta)->getComponent($uuid); + return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component); } /** @@ -58,11 +50,4 @@ protected function submitLabel() { return $this->t('Update'); } - /** - * {@inheritdoc} - */ - protected function submitBlock(Section $section, $region, $uuid, array $configuration) { - $section->getComponent($uuid)->setConfiguration($configuration); - } - } diff --git a/core/modules/layout_builder/src/LayoutTempstoreRepository.php b/core/modules/layout_builder/src/LayoutTempstoreRepository.php index 0fa9d73..39725af 100644 --- a/core/modules/layout_builder/src/LayoutTempstoreRepository.php +++ b/core/modules/layout_builder/src/LayoutTempstoreRepository.php @@ -71,7 +71,7 @@ public function delete(SectionStorageInterface $section_storage) { * The tempstore. */ protected function getTempstore(SectionStorageInterface $section_storage) { - $collection = 'layout_builder.' . $section_storage->getStorageType(); + $collection = 'layout_builder.section_storage.' . $section_storage->getStorageType(); return $this->tempStoreFactory->get($collection); } diff --git a/core/modules/layout_builder/src/OverridesSectionStorageInterface.php b/core/modules/layout_builder/src/OverridesSectionStorageInterface.php new file mode 100644 index 0000000..76e15b5 --- /dev/null +++ b/core/modules/layout_builder/src/OverridesSectionStorageInterface.php @@ -0,0 +1,26 @@ +moduleHandler = $module_handler; // Get the entity type and field name from the plugin ID. - list (, $entity_type_id, $field_name) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 3); + list (, $entity_type_id, $bundle, $field_name) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4); $this->entityTypeId = $entity_type_id; + $this->bundle = $bundle; $this->fieldName = $field_name; parent::__construct($configuration, $plugin_id, $plugin_definition); @@ -130,7 +140,11 @@ protected function getEntity() { */ public function build() { $display_settings = $this->getConfiguration()['formatter']; - $build = $this->getEntity()->get($this->fieldName)->view($display_settings); + $entity = $this->getEntity(); + $build = $entity->get($this->fieldName)->view($display_settings); + if (!empty($entity->in_preview) && !Element::getVisibleChildren($build)) { + $build['content']['#markup'] = new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]); + } CacheableMetadata::createFromObject($this)->applyTo($build); return $build; } @@ -171,6 +185,7 @@ protected function blockAccess(AccountInterface $account) { */ public function defaultConfiguration() { return [ + 'label_display' => FALSE, 'formatter' => [ 'label' => 'above', 'type' => $this->pluginDefinition['default_formatter'], @@ -299,8 +314,7 @@ public function blockSubmit($form, FormStateInterface $form_state) { */ protected function getFieldDefinition() { if (empty($this->fieldDefinition)) { - $bundle = reset($this->getPluginDefinition()['bundles']); - $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $bundle); + $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $this->bundle); $this->fieldDefinition = $field_definitions[$this->fieldName]; } return $this->fieldDefinition; diff --git a/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php index 6a39f4a..71b9c1e 100644 --- a/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php +++ b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php @@ -6,6 +6,7 @@ use Drupal\Component\Plugin\PluginBase; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeRepositoryInterface; +use Drupal\Core\Field\FieldConfigInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Field\FormatterPluginManager; use Drupal\Core\Plugin\Context\ContextDefinition; @@ -87,80 +88,47 @@ public static function create(ContainerInterface $container, $base_plugin_id) { public function getDerivativeDefinitions($base_plugin_definition) { $entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels(); foreach ($this->entityFieldManager->getFieldMap() as $entity_type_id => $entity_field_map) { - foreach ($this->entityFieldManager->getFieldStorageDefinitions($entity_type_id) as $field_storage_definition) { - $derivative = $base_plugin_definition; - $field_name = $field_storage_definition->getName(); - - // The blocks are based on fields. However, we are looping through field - // storages for which no fields may exist. If that is the case, skip - // this field storage. - if (!isset($entity_field_map[$field_name])) { - continue; - } - $field_info = $entity_field_map[$field_name]; - + foreach ($entity_field_map as $field_name => $field_info) { // Skip fields without any formatters. - $options = $this->formatterManager->getOptions($field_storage_definition->getType()); + $options = $this->formatterManager->getOptions($field_info['type']); if (empty($options)) { continue; } - // Store the default formatter on the definition. - $derivative['default_formatter'] = ''; - $field_type_definition = $this->fieldTypeManager->getDefinition($field_storage_definition->getType()); - if (isset($field_type_definition['default_formatter'])) { - $derivative['default_formatter'] = $field_type_definition['default_formatter']; - } + foreach ($field_info['bundles'] as $bundle) { + $derivative = $base_plugin_definition; + $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name]; - // Get the admin label for both base and configurable fields. - if ($field_storage_definition->isBaseField()) { - $admin_label = $field_storage_definition->getLabel(); - } - else { - // We take the field label used on the first bundle. - $first_bundle = reset($field_info['bundles']); - $bundle_field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $first_bundle); - - // The field storage config may exist, but it's possible that no - // fields are actually using it. If that's the case, skip to the next - // field. - if (empty($bundle_field_definitions[$field_name])) { - continue; + // Store the default formatter on the definition. + $derivative['default_formatter'] = ''; + $field_type_definition = $this->fieldTypeManager->getDefinition($field_info['type']); + if (isset($field_type_definition['default_formatter'])) { + $derivative['default_formatter'] = $field_type_definition['default_formatter']; } - $admin_label = $bundle_field_definitions[$field_name]->getLabel(); - } - // Set plugin definition for derivative. - $derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]); - $derivative['admin_label'] = $admin_label; - $bundles = array_keys($field_info['bundles']); + $derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]); - // For any field that is not display configurable, mark it as - // unavailable to place in the block UI. - $block_ui_hidden = TRUE; - foreach ($bundles as $bundle) { - $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name]; - if ($field_definition->isDisplayConfigurable('view')) { - $block_ui_hidden = FALSE; - break; + $derivative['admin_label'] = $field_definition->getLabel(); + + // Add a dependency on the field if it is configurable. + if ($field_definition instanceof FieldConfigInterface) { + $derivative['config_dependencies'][$field_definition->getConfigDependencyKey()][] = $field_definition->getConfigDependencyName(); } + // For any field that is not display configurable, mark it as + // unavailable to place in the block UI. + $derivative['_block_ui_hidden'] = !$field_definition->isDisplayConfigurable('view'); + + // @todo Use EntityContextDefinition after resolving + // https://www.drupal.org/node/2932462. + $context_definition = new ContextDefinition('entity:' . $entity_type_id, $entity_type_labels[$entity_type_id], TRUE); + $context_definition->addConstraint('Bundle', [$bundle]); + $derivative['context'] = [ + 'entity' => $context_definition, + ]; + + $derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle . PluginBase::DERIVATIVE_SEPARATOR . $field_name; + $this->derivatives[$derivative_id] = $derivative; } - $derivative['_block_ui_hidden'] = $block_ui_hidden; - $derivative['bundles'] = $bundles; - $context_definition = new ContextDefinition('entity:' . $entity_type_id, $entity_type_labels[$entity_type_id], TRUE); - // Limit available blocks by bundles to which the field is attached. - // @todo To workaround https://www.drupal.org/node/2671964 this only - // adds a bundle constraint if the entity type has bundles. When an - // entity type has no bundles, the entity type ID itself is used. - if (count($bundles) > 1 || !isset($field_info['bundles'][$entity_type_id])) { - $context_definition->addConstraint('Bundle', $bundles); - } - $derivative['context'] = [ - 'entity' => $context_definition, - ]; - - $derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $field_name; - $this->derivatives[$derivative_id] = $derivative; } } return $this->derivatives; diff --git a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php index 0aacc0d..c2046c0 100644 --- a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php +++ b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php @@ -12,6 +12,8 @@ /** * Provides local task definitions for the layout builder user interface. * + * @todo Remove this in https://www.drupal.org/project/drupal/issues/2936655. + * * @internal */ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeriverInterface { @@ -48,30 +50,55 @@ public static function create(ContainerInterface $container, $base_plugin_id) { * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition) { - foreach (array_keys($this->getEntityTypes()) as $entity_type_id) { - $this->derivatives["entity.$entity_type_id.layout_builder"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout_builder", + foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { + // Overrides. + $this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.view", 'weight' => 15, 'title' => $this->t('Layout'), 'base_route' => "entity.$entity_type_id.canonical", - 'entity_type_id' => $entity_type_id, 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; - $this->derivatives["entity.$entity_type_id.layout_builder_save"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout_builder_save", + $this->derivatives["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.save", 'title' => $this->t('Save Layout'), - 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view", 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; - $this->derivatives["entity.$entity_type_id.layout_builder_cancel"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout_builder_cancel", + $this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.cancel", 'title' => $this->t('Cancel Layout'), - 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view", 'weight' => 5, 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; + // @todo This link should be conditionally displayed, see + // https://www.drupal.org/node/2917777. + $this->derivatives["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.revert", + 'title' => $this->t('Revert to defaults'), + 'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view", + 'weight' => 10, + 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], + ]; + + // Defaults. + $this->derivatives["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.defaults.$entity_type_id.view", + 'title' => $this->t('Manage layout'), + 'base_route' => "layout_builder.defaults.$entity_type_id.view", + ]; + $this->derivatives["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.defaults.$entity_type_id.save", + 'title' => $this->t('Save Layout'), + 'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view", + ]; + $this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.defaults.$entity_type_id.cancel", + 'title' => $this->t('Cancel Layout'), + 'weight' => 5, + 'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view", + ]; } return $this->derivatives; diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php index a8d5c70..0865f0c 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php @@ -67,7 +67,6 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) 'section' => [ 'type' => 'blob', 'size' => 'normal', - // @todo Address in https://www.drupal.org/node/2914503. 'serialize' => TRUE, ], ], diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php new file mode 100644 index 0000000..a83ca04 --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php @@ -0,0 +1,295 @@ +entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->sampleEntityGenerator = $sample_entity_generator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info'), + $container->get('layout_builder.sample_entity_generator') + ); + } + + /** + * {@inheritdoc} + */ + public function setSectionList(SectionListInterface $section_list) { + if (!$section_list instanceof LayoutEntityDisplayInterface) { + throw new \InvalidArgumentException('Defaults expect a display-based section list'); + } + + return parent::setSectionList($section_list); + } + + /** + * Gets the entity storing the overrides. + * + * @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface + * The entity storing the defaults. + */ + protected function getDisplay() { + return $this->getSectionList(); + } + + /** + * {@inheritdoc} + */ + public function getStorageId() { + return $this->getDisplay()->id(); + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrl() { + return Url::fromRoute("entity.entity_view_display.{$this->getDisplay()->getTargetEntityTypeId()}.view_mode", $this->getRouteParameters()); + } + + /** + * {@inheritdoc} + */ + public function getLayoutBuilderUrl() { + return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getDisplay()->getTargetEntityTypeId()}.view", $this->getRouteParameters()); + } + + /** + * Provides the route parameters needed to generate a URL for this object. + * + * @return mixed[] + * An associative array of parameter names and values. + */ + protected function getRouteParameters() { + $display = $this->getDisplay(); + $entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId()); + $route_parameters = FieldUI::getRouteBundleParameter($entity_type, $display->getTargetBundle()); + $route_parameters['view_mode_name'] = $display->getMode(); + return $route_parameters; + } + + /** + * {@inheritdoc} + */ + public function buildRoutes(RouteCollection $collection) { + foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { + // Try to get the route from the current collection. + if (!$entity_route = $collection->get($entity_type->get('field_ui_base_route'))) { + continue; + } + + $path = $entity_route->getPath() . '/display-layout/{view_mode_name}'; + + $defaults = []; + $defaults['entity_type_id'] = $entity_type_id; + // If the entity type has no bundles and it doesn't use {bundle} in its + // admin path, use the entity type. + if (strpos($path, '{bundle}') === FALSE) { + if (!$entity_type->hasKey('bundle')) { + $defaults['bundle'] = $entity_type_id; + } + else { + $defaults['bundle_key'] = $entity_type->getBundleEntityType(); + } + } + + $requirements = []; + $requirements['_field_ui_view_mode_access'] = 'administer ' . $entity_type_id . ' display'; + + $options = $entity_route->getOptions(); + $options['_admin_route'] = FALSE; + + $this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $path, $defaults, $requirements, $options, $entity_type_id); + + $route_names = [ + "entity.entity_view_display.{$entity_type_id}.default", + "entity.entity_view_display.{$entity_type_id}.view_mode", + ]; + foreach ($route_names as $route_name) { + if (!$route = $collection->get($route_name)) { + continue; + } + + $route->addDefaults([ + 'section_storage_type' => $this->getStorageType(), + 'section_storage' => '', + ] + $defaults); + $parameters['section_storage']['layout_builder_tempstore'] = TRUE; + $parameters = NestedArray::mergeDeep($parameters, $route->getOption('parameters') ?: []); + $route->setOption('parameters', $parameters); + } + } + } + + /** + * Returns an array of relevant entity types. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of entity types. + */ + protected function getEntityTypes() { + return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { + return $entity_type->hasLinkTemplate('layout-builder') && $entity_type->get('field_ui_base_route'); + }); + } + + /** + * {@inheritdoc} + */ + public function extractIdFromRoute($value, $definition, $name, array $defaults) { + if (is_string($value) && strpos($value, '.') !== FALSE) { + return $value; + } + + // If a bundle is not provided but a value corresponding to the bundle key + // is, use that for the bundle value. + if (empty($defaults['bundle']) && isset($defaults['bundle_key']) && !empty($defaults[$defaults['bundle_key']])) { + $defaults['bundle'] = $defaults[$defaults['bundle_key']]; + } + + if (!empty($defaults['entity_type_id']) && !empty($defaults['bundle']) && !empty($defaults['view_mode_name'])) { + return $defaults['entity_type_id'] . '.' . $defaults['bundle'] . '.' . $defaults['view_mode_name']; + } + } + + /** + * {@inheritdoc} + */ + public function getSectionListFromId($id) { + if (strpos($id, '.') === FALSE) { + throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType())); + } + + $storage = $this->entityTypeManager->getStorage('entity_view_display'); + // If the display does not exist, create a new one. + if (!$display = $storage->load($id)) { + list($entity_type_id, $bundle, $view_mode) = explode('.', $id, 3); + $display = $storage->create([ + 'targetEntityType' => $entity_type_id, + 'bundle' => $bundle, + 'mode' => $view_mode, + 'status' => TRUE, + ]); + } + return $display; + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + $display = $this->getDisplay(); + $entity = $this->sampleEntityGenerator->get($display->getTargetEntityTypeId(), $display->getTargetBundle()); + $context_label = new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()]); + + // @todo Use EntityContextDefinition after resolving + // https://www.drupal.org/node/2932462. + $contexts = []; + $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", $context_label), $entity); + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->getDisplay()->label(); + } + + /** + * {@inheritdoc} + */ + public function save() { + return $this->getDisplay()->save(); + } + + /** + * {@inheritdoc} + */ + public function isOverridable() { + return $this->getDisplay()->isOverridable(); + } + + /** + * {@inheritdoc} + */ + public function setOverridable($overridable = TRUE) { + $this->getDisplay()->setOverridable($overridable); + return $this; + } + +} diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php new file mode 100644 index 0000000..58adb55 --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -0,0 +1,234 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function setSectionList(SectionListInterface $section_list) { + if (!$section_list instanceof FieldItemListInterface) { + throw new \InvalidArgumentException('Overrides expect a field-based section list'); + } + + return parent::setSectionList($section_list); + } + + /** + * Gets the entity storing the overrides. + * + * @return \Drupal\Core\Entity\FieldableEntityInterface + * The entity storing the overrides. + */ + protected function getEntity() { + return $this->getSectionList()->getEntity(); + } + + /** + * {@inheritdoc} + */ + public function getStorageId() { + $entity = $this->getEntity(); + return $entity->getEntityTypeId() . '.' . $entity->id(); + } + + /** + * {@inheritdoc} + */ + public function extractIdFromRoute($value, $definition, $name, array $defaults) { + if (strpos($value, '.') !== FALSE) { + return $value; + } + + if (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) { + $entity_type_id = $defaults['entity_type_id']; + $entity_id = $defaults[$entity_type_id]; + return $entity_type_id . '.' . $entity_id; + } + } + + /** + * {@inheritdoc} + */ + public function getSectionListFromId($id) { + if (strpos($id, '.') !== FALSE) { + list($entity_type_id, $entity_id) = explode('.', $id, 2); + $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id); + if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) { + return $entity->get('layout_builder__layout'); + } + } + throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType())); + } + + /** + * {@inheritdoc} + */ + public function buildRoutes(RouteCollection $collection) { + foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { + $defaults = []; + $defaults['entity_type_id'] = $entity_type_id; + + $requirements = []; + if ($this->hasIntegerId($entity_type)) { + $requirements[$entity_type_id] = '\d+'; + } + + $options = []; + // Ensure that upcasting is run in the correct order. + $options['parameters']['section_storage'] = []; + $options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id; + + $template = $entity_type->getLinkTemplate('layout-builder'); + $this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id); + } + } + + /** + * Determines if this entity type's ID is stored as an integer. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * An entity type. + * + * @return bool + * TRUE if this entity type's ID key is always an integer, FALSE otherwise. + */ + protected function hasIntegerId(EntityTypeInterface $entity_type) { + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); + return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer'; + } + + /** + * Returns an array of relevant entity types. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of entity types. + */ + protected function getEntityTypes() { + return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { + return $entity_type->hasLinkTemplate('layout-builder'); + }); + } + + /** + * {@inheritdoc} + */ + public function getDefaultSectionStorage() { + return LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), 'default'); + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrl() { + return $this->getEntity()->toUrl('canonical'); + } + + /** + * {@inheritdoc} + */ + public function getLayoutBuilderUrl() { + $entity = $this->getEntity(); + $route_parameters[$entity->getEntityTypeId()] = $entity->id(); + return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.view", $route_parameters); + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + $entity = $this->getEntity(); + // @todo Use EntityContextDefinition after resolving + // https://www.drupal.org/node/2932462. + $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity); + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->getEntity()->label(); + } + + /** + * {@inheritdoc} + */ + public function save() { + return $this->getEntity()->save(); + } + +} diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php new file mode 100644 index 0000000..c419060 --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php @@ -0,0 +1,106 @@ +sectionList = $section_list; + return $this; + } + + /** + * Gets the section list. + * + * @return \Drupal\layout_builder\SectionListInterface + * The section list. + * + * @throws \RuntimeException + * Thrown if ::setSectionList() is not called first. + */ + protected function getSectionList() { + if (!$this->sectionList) { + throw new \RuntimeException(sprintf('%s::setSectionList() must be called first', static::class)); + } + return $this->sectionList; + } + + /** + * {@inheritdoc} + */ + public function getStorageType() { + return $this->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function count() { + return $this->getSectionList()->count(); + } + + /** + * {@inheritdoc} + */ + public function getSections() { + return $this->getSectionList()->getSections(); + } + + /** + * {@inheritdoc} + */ + public function getSection($delta) { + return $this->getSectionList()->getSection($delta); + } + + /** + * {@inheritdoc} + */ + public function appendSection(Section $section) { + $this->getSectionList()->appendSection($section); + return $this; + } + + /** + * {@inheritdoc} + */ + public function insertSection($delta, Section $section) { + $this->getSectionList()->insertSection($delta, $section); + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeSection($delta) { + $this->getSectionList()->removeSection($delta); + return $this; + } + +} diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php index 563021e..1046579 100644 --- a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php +++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php @@ -2,156 +2,55 @@ namespace Drupal\layout_builder\Routing; -use Drupal\Core\Entity\EntityFieldManagerInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Symfony\Component\Routing\Route; +use Drupal\Core\Routing\RouteBuildEvent; +use Drupal\Core\Routing\RoutingEvents; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Provides routes for the Layout Builder UI. * * @internal */ -class LayoutBuilderRoutes { +class LayoutBuilderRoutes implements EventSubscriberInterface { /** - * The entity type manager. + * The section storage manager. * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface */ - protected $entityTypeManager; - - /** - * The entity field manager. - * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface - */ - protected $entityFieldManager; + protected $sectionStorageManager; /** * Constructs a new LayoutBuilderRoutes. * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager - * The entity field manager. + * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager + * The section storage manager. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) { - $this->entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_manager; + public function __construct(SectionStorageManagerInterface $section_storage_manager) { + $this->sectionStorageManager = $section_storage_manager; } /** - * Generates layout builder routes. + * Alters existing routes for a specific collection. * - * @return \Symfony\Component\Routing\Route[] - * An array of route objects. + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. */ - public function getRoutes() { - $routes = []; - - foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { - $defaults = []; - $defaults['entity_type_id'] = $entity_type_id; - - $requirements = []; - if ($this->hasIntegerId($entity_type)) { - $requirements[$entity_type_id] = '\d+'; - } - - $options = []; - $options['parameters']['section_storage']['layout_builder_tempstore'] = TRUE; - $options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id; - - $template = $entity_type->getLinkTemplate('layout-builder'); - $routes += $this->buildRoute('overrides', 'entity.' . $entity_type_id, $template, $defaults, $requirements, $options); + public function onAlterRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) { + $this->sectionStorageManager->loadEmpty($plugin_id)->buildRoutes($collection); } - return $routes; - } - - /** - * Builds the layout routes for the given values. - * - * @param string $type - * The section storage type. - * @param string $route_name_prefix - * The prefix to use for the route name. - * @param string $path - * The path patten for the routes. - * @param array $defaults - * An array of default parameter values. - * @param array $requirements - * An array of requirements for parameters. - * @param array $options - * An array of options. - * - * @return \Symfony\Component\Routing\Route[] - * An array of route objects. - */ - protected function buildRoute($type, $route_name_prefix, $path, array $defaults, array $requirements, array $options) { - $routes = []; - - $defaults['section_storage_type'] = $type; - // Provide an empty value to allow the section storage to be upcast. - $defaults['section_storage'] = ''; - // Trigger the layout builder access check. - $requirements['_has_layout_section'] = 'true'; - // Trigger the layout builder RouteEnhancer. - $options['_layout_builder'] = TRUE; - - $main_defaults = $defaults; - $main_defaults['is_rebuilding'] = FALSE; - $main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout'; - $main_defaults['_title_callback'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::title'; - $route = (new Route($path)) - ->setDefaults($main_defaults) - ->setRequirements($requirements) - ->setOptions($options); - $routes["{$route_name_prefix}.layout_builder"] = $route; - - $save_defaults = $defaults; - $save_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout'; - $route = (new Route("$path/save")) - ->setDefaults($save_defaults) - ->setRequirements($requirements) - ->setOptions($options); - $routes["{$route_name_prefix}.layout_builder_save"] = $route; - - $cancel_defaults = $defaults; - $cancel_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout'; - $route = (new Route("$path/cancel")) - ->setDefaults($cancel_defaults) - ->setRequirements($requirements) - ->setOptions($options); - $routes["{$route_name_prefix}.layout_builder_cancel"] = $route; - - return $routes; } /** - * Determines if this entity type's ID is stored as an integer. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * An entity type. - * - * @return bool - * TRUE if this entity type's ID key is always an integer, FALSE otherwise. - */ - protected function hasIntegerId(EntityTypeInterface $entity_type) { - $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); - return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer'; - } - - /** - * Returns an array of relevant entity types. - * - * @return \Drupal\Core\Entity\EntityTypeInterface[] - * An array of entity types. + * {@inheritdoc} */ - protected function getEntityTypes() { - return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { - return $entity_type->hasLinkTemplate('layout-builder'); - }); + public static function getSubscribedEvents() { + // Run after \Drupal\field_ui\Routing\RouteSubscriber. + $events[RoutingEvents::ALTER] = ['onAlterRoutes', -110]; + return $events; } } diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php new file mode 100644 index 0000000..febedbe --- /dev/null +++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php @@ -0,0 +1,97 @@ +id(); + $defaults['section_storage_type'] = $type; + // Provide an empty value to allow the section storage to be upcast. + $defaults['section_storage'] = ''; + // Trigger the layout builder access check. + $requirements['_has_layout_section'] = 'true'; + // Trigger the layout builder RouteEnhancer. + $options['_layout_builder'] = TRUE; + // Trigger the layout builder param converter. + $parameters['section_storage']['layout_builder_tempstore'] = TRUE; + // Merge the passed in options in after Layout Builder's parameters. + $options = NestedArray::mergeDeep(['parameters' => $parameters], $options); + + if ($route_name_prefix) { + $route_name_prefix = "layout_builder.$type.$route_name_prefix"; + } + else { + $route_name_prefix = "layout_builder.$type"; + } + + $main_defaults = $defaults; + $main_defaults['is_rebuilding'] = FALSE; + $main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout'; + $main_defaults['_title_callback'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::title'; + $route = (new Route($path)) + ->setDefaults($main_defaults) + ->setRequirements($requirements) + ->setOptions($options); + $collection->add("$route_name_prefix.view", $route); + + $save_defaults = $defaults; + $save_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout'; + $route = (new Route("$path/save")) + ->setDefaults($save_defaults) + ->setRequirements($requirements) + ->setOptions($options); + $collection->add("$route_name_prefix.save", $route); + + $cancel_defaults = $defaults; + $cancel_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout'; + $route = (new Route("$path/cancel")) + ->setDefaults($cancel_defaults) + ->setRequirements($requirements) + ->setOptions($options); + $collection->add("$route_name_prefix.cancel", $route); + + if (is_subclass_of($definition->getClass(), OverridesSectionStorageInterface::class)) { + $revert_defaults = $defaults; + $revert_defaults['_form'] = '\Drupal\layout_builder\Form\RevertOverridesForm'; + $route = (new Route("$path/revert")) + ->setDefaults($revert_defaults) + ->setRequirements($requirements) + ->setOptions($options); + $collection->add("$route_name_prefix.revert", $route); + } + } + +} diff --git a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php index 228a786..263b767 100644 --- a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php +++ b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php @@ -2,9 +2,9 @@ namespace Drupal\layout_builder\Routing; -use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\ParamConverter\ParamConverterInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Symfony\Component\Routing\Route; /** @@ -22,59 +22,33 @@ class LayoutTempstoreParamConverter implements ParamConverterInterface { protected $layoutTempstoreRepository; /** - * The class resolver. + * The section storage manager. * - * @var \Drupal\Core\DependencyInjection\ClassResolverInterface + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface */ - protected $classResolver; + protected $sectionStorageManager; /** * Constructs a new LayoutTempstoreParamConverter. * * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository * The layout tempstore repository. - * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver - * The class resolver. + * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager + * The section storage manager. */ - public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) { + public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, SectionStorageManagerInterface $section_storage_manager) { $this->layoutTempstoreRepository = $layout_tempstore_repository; - $this->classResolver = $class_resolver; + $this->sectionStorageManager = $section_storage_manager; } /** * {@inheritdoc} */ public function convert($value, $definition, $name, array $defaults) { - if ($converter = $this->getParamConverterFromDefaults($defaults)) { - if ($object = $converter->convert($value, $definition, $name, $defaults)) { - // Pass the result of the storage param converter through the - // tempstore repository. - return $this->layoutTempstoreRepository->get($object); - } - } - } - - /** - * Gets a param converter based on the provided defaults. - * - * @param array $defaults - * The route defaults array. - * - * @return \Drupal\layout_builder\Routing\SectionStorageParamConverterInterface|null - * A section storage param converter if found, NULL otherwise. - */ - protected function getParamConverterFromDefaults(array $defaults) { - // If a storage type was specified, get the corresponding param converter. - if (isset($defaults['section_storage_type'])) { - try { - $converter = $this->classResolver->getInstanceFromDefinition('layout_builder.section_storage_param_converter.' . $defaults['section_storage_type']); - } - catch (\InvalidArgumentException $e) { - $converter = NULL; - } - - if ($converter instanceof SectionStorageParamConverterInterface) { - return $converter; + if (isset($defaults['section_storage_type']) && $this->sectionStorageManager->hasDefinition($defaults['section_storage_type'])) { + if ($section_storage = $this->sectionStorageManager->loadFromRoute($defaults['section_storage_type'], $value, $definition, $name, $defaults)) { + // Pass the plugin through the tempstore repository. + return $this->layoutTempstoreRepository->get($section_storage); } } } diff --git a/core/modules/layout_builder/src/Routing/SectionStorageOverridesParamConverter.php b/core/modules/layout_builder/src/Routing/SectionStorageOverridesParamConverter.php deleted file mode 100644 index 8d8ae58..0000000 --- a/core/modules/layout_builder/src/Routing/SectionStorageOverridesParamConverter.php +++ /dev/null @@ -1,70 +0,0 @@ -getEntityIdFromDefaults($value, $defaults); - $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults); - if (!$entity_id || !$entity_type_id) { - return NULL; - } - - $entity = parent::convert($entity_id, $definition, $name, $defaults); - if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) { - return $entity->get('layout_builder__layout'); - } - } - - /** - * Determines the entity ID given a parameter value and route defaults. - * - * @param string $value - * The parameter value. - * @param array $defaults - * The route defaults array. - * - * @return string|null - * The entity ID if it exists, NULL otherwise. - */ - protected function getEntityIdFromDefaults($value, array $defaults) { - $entity_id = NULL; - // Layout Builder routes will have this parameter in the form of - // 'entity_type_id:entity_id'. - if (strpos($value, ':') !== FALSE) { - list(, $entity_id) = explode(':', $value); - } - // Overridden routes have the entity ID available in the defaults. - elseif (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) { - $entity_id = $defaults[$defaults['entity_type_id']]; - } - return $entity_id; - } - - /** - * {@inheritdoc} - */ - protected function getEntityTypeFromDefaults($definition, $name, array $defaults) { - // Layout Builder routes will have this parameter in the form of - // 'entity_type_id:entity_id'. - if (isset($defaults[$name]) && strpos($defaults[$name], ':') !== FALSE) { - list($entity_type_id) = explode(':', $defaults[$name], 2); - return $entity_type_id; - } - // Overridden routes have the entity type ID available in the defaults. - elseif (isset($defaults['entity_type_id'])) { - return $defaults['entity_type_id']; - } - } - -} diff --git a/core/modules/layout_builder/src/Routing/SectionStorageParamConverterInterface.php b/core/modules/layout_builder/src/Routing/SectionStorageParamConverterInterface.php deleted file mode 100644 index 955b673..0000000 --- a/core/modules/layout_builder/src/Routing/SectionStorageParamConverterInterface.php +++ /dev/null @@ -1,34 +0,0 @@ -getComponents() as $component) { - if ($output = $component->toRenderArray($contexts)) { + if ($output = $component->toRenderArray($contexts, $in_preview)) { $regions[$component->getRegion()][$component->getUuid()] = $output; } } @@ -133,6 +135,16 @@ public function setLayoutSettings(array $layout_settings) { } /** + * Gets the default region. + * + * @return string + * The machine-readable name of the default region. + */ + public function getDefaultRegion() { + return $this->layoutPluginManager()->getDefinition($this->getLayoutId())->getDefaultRegion(); + } + + /** * Returns the components of the section. * * @return \Drupal\layout_builder\SectionComponent[] @@ -307,4 +319,23 @@ protected function layoutPluginManager() { return \Drupal::service('plugin.manager.core.layout'); } + /** + * Returns an array representation of the section. + * + * @internal + * This is intended for use by a storage mechanism for sections. + * + * @return array + * An array representation of the section component. + */ + public function toArray() { + return [ + 'layout_id' => $this->getLayoutId(), + 'layout_settings' => $this->getLayoutSettings(), + 'components' => array_map(function (SectionComponent $component) { + return $component->toArray(); + }, $this->getComponents()), + ]; + } + } diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php index 6a0c238..6c08fb3 100644 --- a/core/modules/layout_builder/src/SectionComponent.php +++ b/core/modules/layout_builder/src/SectionComponent.php @@ -3,6 +3,7 @@ namespace Drupal\layout_builder; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Plugin\ContextAwarePluginInterface; @@ -90,22 +91,31 @@ public function __construct($uuid, $region, array $configuration = [], array $ad * * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts * An array of available contexts. + * @param bool $in_preview + * TRUE if the component is being previewed, FALSE otherwise. * * @return array * A renderable array representing the content of the component. */ - public function toRenderArray(array $contexts = []) { + public function toRenderArray(array $contexts = [], $in_preview = FALSE) { $output = []; $plugin = $this->getPlugin($contexts); // @todo Figure out the best way to unify fields and blocks and components // in https://www.drupal.org/node/1875974. if ($plugin instanceof BlockPluginInterface) { - $access = $plugin->access($this->currentUser(), TRUE); - $cacheability = CacheableMetadata::createFromObject($access); + $cacheability = CacheableMetadata::createFromObject($plugin); + // Only check access if the component is not being previewed. + if ($in_preview) { + $access = AccessResult::allowed()->setCacheMaxAge(0); + } + else { + $access = $plugin->access($this->currentUser(), TRUE); + } + + $cacheability->addCacheableDependency($access); if ($access->isAllowed()) { - $cacheability->addCacheableDependency($plugin); // @todo Move this to BlockBase in https://www.drupal.org/node/2931040. $output = [ '#theme' => 'block', @@ -242,7 +252,7 @@ public function setConfiguration(array $configuration) { * @throws \Drupal\Component\Plugin\Exception\PluginException * Thrown if the plugin ID cannot be found. */ - protected function getPluginId() { + public function getPluginId() { if (empty($this->configuration['id'])) { throw new PluginException(sprintf('No plugin ID specified for component with "%s" UUID', $this->uuid)); } @@ -306,4 +316,23 @@ protected function currentUser() { return \Drupal::currentUser(); } + /** + * Returns an array representation of the section component. + * + * @internal + * This is intended for use by a storage mechanism for section components. + * + * @return array + * An array representation of the section component. + */ + public function toArray() { + return [ + 'uuid' => $this->getUuid(), + 'region' => $this->getRegion(), + 'configuration' => $this->getConfiguration(), + 'additional' => $this->additional, + 'weight' => $this->getWeight(), + ]; + } + } diff --git a/core/modules/layout_builder/src/SectionListInterface.php b/core/modules/layout_builder/src/SectionListInterface.php new file mode 100644 index 0000000..8df586a --- /dev/null +++ b/core/modules/layout_builder/src/SectionListInterface.php @@ -0,0 +1,74 @@ + $value) { + $this->set($property, $value); + } + } + + /** + * Gets any arbitrary property. + * + * @param string $property + * The property to retrieve. + * + * @return mixed + * The value for that property, or NULL if the property does not exist. + */ + public function get($property) { + if (property_exists($this, $property)) { + $value = isset($this->{$property}) ? $this->{$property} : NULL; + } + else { + $value = isset($this->additional[$property]) ? $this->additional[$property] : NULL; + } + return $value; + } + + /** + * Sets a value to an arbitrary property. + * + * @param string $property + * The property to use for the value. + * @param mixed $value + * The value to set. + * + * @return $this + */ + public function set($property, $value) { + if (property_exists($this, $property)) { + $this->{$property} = $value; + } + else { + $this->additional[$property] = $value; + } + return $this; + } + +} diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php new file mode 100644 index 0000000..18147cd --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php @@ -0,0 +1,71 @@ +alterInfo('layout_builder_section_storage'); + $this->setCacheBackend($cache_backend, 'layout_builder_section_storage_plugins'); + } + + /** + * {@inheritdoc} + */ + public function loadEmpty($id) { + return $this->createInstance($id); + } + + /** + * {@inheritdoc} + */ + public function loadFromStorageId($type, $id) { + /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ + $plugin = $this->createInstance($type); + return $plugin->setSectionList($plugin->getSectionListFromId($id)); + } + + /** + * {@inheritdoc} + */ + public function loadFromRoute($type, $value, $definition, $name, array $defaults) { + /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ + $plugin = $this->createInstance($type); + if ($id = $plugin->extractIdFromRoute($value, $definition, $name, $defaults)) { + try { + return $plugin->setSectionList($plugin->getSectionListFromId($id)); + } + catch (\InvalidArgumentException $e) { + // Intentionally empty. + } + } + } + +} diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php new file mode 100644 index 0000000..3b269fc --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php @@ -0,0 +1,65 @@ +getSections()); + } + + /** + * {@inheritdoc} + */ + public function getSection($delta) { + if (!$this->hasSection($delta)) { + throw new \OutOfBoundsException(sprintf('Invalid delta "%s"', $delta)); + } + + return $this->getSections()[$delta]; + } + + /** + * Sets the section for the given delta on the display. + * + * @param int $delta + * The delta of the section. + * @param \Drupal\layout_builder\Section $section + * The layout section. + * + * @return $this + */ + protected function setSection($delta, Section $section) { + $sections = $this->getSections(); + $sections[$delta] = $section; + $this->setSections($sections); + return $this; + } + + /** + * {@inheritdoc} + */ + public function appendSection(Section $section) { + $delta = $this->count(); + + $this->setSection($delta, $section); + return $this; + } + + /** + * {@inheritdoc} + */ + public function insertSection($delta, Section $section) { + if ($this->hasSection($delta)) { + // @todo Use https://www.drupal.org/node/66183 once resolved. + $start = array_slice($this->getSections(), 0, $delta); + $end = array_slice($this->getSections(), $delta); + $this->setSections(array_merge($start, [$section], $end)); + } + else { + $this->appendSection($section); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeSection($delta) { + $sections = $this->getSections(); + unset($sections[$delta]); + $this->setSections($sections); + return $this; + } + + /** + * Indicates if there is a section at the specified delta. + * + * @param int $delta + * The delta of the section. + * + * @return bool + * TRUE if there is a section for this delta, FALSE otherwise. + */ + protected function hasSection($delta) { + return isset($this->getSections()[$delta]); + } + +} diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index 13217d8..e7fbb08 100644 --- a/core/modules/layout_builder/src/SectionStorageInterface.php +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -2,97 +2,124 @@ namespace Drupal\layout_builder; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Symfony\Component\Routing\RouteCollection; + /** - * Defines the interface for an object that stores layout sections. + * Defines an interface for Section Storage type plugins. * * @internal * Layout Builder is currently experimental and should only be leveraged by * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. - * - * @see \Drupal\layout_builder\Section */ -interface SectionStorageInterface extends \Countable { +interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface { /** - * Gets the layout sections. + * Returns an identifier for this storage. * - * @return \Drupal\layout_builder\Section[] - * An array of sections. + * @return string + * The unique identifier for this storage. */ - public function getSections(); + public function getStorageId(); /** - * Gets a domain object for the layout section. + * Returns the type of this storage. * - * @param int $delta - * The delta of the section. + * Used in conjunction with the storage ID. * - * @return \Drupal\layout_builder\Section - * The layout section. + * @return string + * The type of storage. */ - public function getSection($delta); + public function getStorageType(); /** - * Appends a new section to the end of the list. + * Sets the section list on the storage. * - * @param \Drupal\layout_builder\Section $section - * The section to append. + * @param \Drupal\layout_builder\SectionListInterface $section_list + * The section list. * * @return $this + * + * @internal + * This should only be called during section storage instantiation. */ - public function appendSection(Section $section); + public function setSectionList(SectionListInterface $section_list); /** - * Inserts a new section at a given delta. + * Derives the section list from the storage ID. * - * If a section exists at the given index, the section at that position and - * others after it are shifted backward. + * @param string $id + * The storage ID, see ::getStorageId(). * - * @param int $delta - * The delta of the section. - * @param \Drupal\layout_builder\Section $section - * The section to insert. + * @return \Drupal\layout_builder\SectionListInterface + * The section list. * - * @return $this + * @throws \InvalidArgumentException + * Thrown if the ID is invalid. + * + * @internal + * This should only be called during section storage instantiation. */ - public function insertSection($delta, Section $section); + public function getSectionListFromId($id); /** - * Removes the section at the given delta. + * Provides the routes needed for Layout Builder UI. * - * @param int $delta - * The delta of the section. + * Allows the plugin to add or alter routes during the route building process. + * \Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait is provided for the + * typical use case of building a standard Layout Builder UI. * - * @return $this + * @param \Symfony\Component\Routing\RouteCollection $collection + * The route collection. + * + * @see \Drupal\Core\Routing\RoutingEvents::ALTER */ - public function removeSection($delta); + public function buildRoutes(RouteCollection $collection); /** - * Provides any available contexts for the object using the sections. + * Gets the URL used when redirecting away from the Layout Builder UI. * - * @return \Drupal\Core\Plugin\Context\ContextInterface[] - * The array of context objects. + * @return \Drupal\Core\Url + * The URL object. */ - public function getContexts(); + public function getRedirectUrl(); /** - * Returns an identifier for this storage. + * Gets the URL used to display the Layout Builder UI. * - * @return string - * The unique identifier for this storage. + * @return \Drupal\Core\Url + * The URL object. */ - public function getStorageId(); + public function getLayoutBuilderUrl(); /** - * Returns the type of this storage. - * - * Used in conjunction with the storage ID. + * Configures the plugin based on route values. + * + * @param mixed $value + * The raw value. + * @param mixed $definition + * The parameter definition provided in the route options. + * @param string $name + * The name of the parameter. + * @param array $defaults + * The route defaults array. + * + * @return string|null + * The section storage ID if it could be extracted, NULL otherwise. + * + * @internal + * This should only be called during section storage instantiation. + */ + public function extractIdFromRoute($value, $definition, $name, array $defaults); + + /** + * Provides any available contexts for the object using the sections. * - * @return string - * The type of storage. + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + * The array of context objects. */ - public function getStorageType(); + public function getContexts(); /** * Gets the label for the object using the sections. @@ -111,20 +138,4 @@ public function label(); */ public function save(); - /** - * Returns a URL for viewing the object using the sections. - * - * @return \Drupal\Core\Url - * The URL object. - */ - public function getCanonicalUrl(); - - /** - * Returns a URL to edit the sections in the Layout Builder UI. - * - * @return \Drupal\Core\Url - * The URL object. - */ - public function getLayoutBuilderUrl(); - } diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php new file mode 100644 index 0000000..d14007a --- /dev/null +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -0,0 +1,241 @@ +drupalPlaceBlock('local_tasks_block'); + + // Create two nodes. + $this->createContentType(['type' => 'bundle_with_section_field']); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The first node title', + 'body' => [ + [ + 'value' => 'The first node body', + ], + ], + ]); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The second node title', + 'body' => [ + [ + 'value' => 'The second node body', + ], + ], + ]); + } + + /** + * {@inheritdoc} + */ + public function testLayoutBuilderUi() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer node fields', + ])); + + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The first node body'); + $assert_session->pageTextNotContains('Powered by Drupal'); + $assert_session->linkNotExists('Layout'); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + + // From the manage display page, go to manage the layout. + $this->drupalGet("$field_ui_prefix/display/default"); + $assert_session->linkExists('Manage layout'); + $this->clickLink('Manage layout'); + $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); + // The body field is present. + $assert_session->elementExists('css', '.field--name-body'); + + // Add a new block. + $assert_session->linkExists('Add Block'); + $this->clickLink('Add Block'); + $assert_session->linkExists('Powered by Drupal'); + $this->clickLink('Powered by Drupal'); + $page->fillField('settings[label]', 'This is the label'); + $page->checkField('settings[label_display]'); + $page->pressButton('Add Block'); + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->pageTextContains('This is the label'); + $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); + + // Save the defaults. + $assert_session->linkExists('Save Layout'); + $this->clickLink('Save Layout'); + $assert_session->addressEquals("$field_ui_prefix/display/default"); + + // The node uses the defaults, no overrides available. + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The first node body'); + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->linkNotExists('Layout'); + + // Enable overrides. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + $this->drupalGet('node/1'); + + // Remove the section from the defaults. + $assert_session->linkExists('Layout'); + $this->clickLink('Layout'); + $assert_session->linkExists('Remove section'); + $this->clickLink('Remove section'); + $page->pressButton('Remove'); + + // Add a new section. + $this->clickLink('Add Section'); + $assert_session->linkExists('Two column'); + $this->clickLink('Two column'); + $assert_session->linkExists('Save Layout'); + $this->clickLink('Save Layout'); + $assert_session->pageTextNotContains('The first node body'); + $assert_session->pageTextNotContains('Powered by Drupal'); + + // Assert that overrides cannot be turned off while overrides exist. + $this->drupalGet("$field_ui_prefix/display/default"); + $assert_session->fieldDisabled('layout[allow_custom]'); + + // Alter the defaults. + $this->drupalGet("$field_ui_prefix/display-layout/default"); + $assert_session->linkExists('Add Block'); + $this->clickLink('Add Block'); + $assert_session->linkExists('Title'); + $this->clickLink('Title'); + $page->pressButton('Add Block'); + // The title field is present. + $assert_session->elementExists('css', '.field--name-title'); + $this->clickLink('Save Layout'); + + // View the other node, which is still using the defaults. + $this->drupalGet('node/2'); + $assert_session->pageTextContains('The second node title'); + $assert_session->pageTextContains('The second node body'); + $assert_session->pageTextContains('Powered by Drupal'); + + // The overridden node does not pick up the changes to defaults. + $this->drupalGet('node/1'); + $assert_session->elementNotExists('css', '.field--name-title'); + $assert_session->pageTextNotContains('The first node body'); + $assert_session->pageTextNotContains('Powered by Drupal'); + $assert_session->linkExists('Layout'); + + // Reverting the override returns it to the defaults. + $this->clickLink('Layout'); + $assert_session->linkExists('Revert to defaults'); + $this->clickLink('Revert to defaults'); + $page->pressButton('Revert'); + $assert_session->pageTextContains('The layout has been reverted back to defaults.'); + $assert_session->elementExists('css', '.field--name-title'); + $assert_session->pageTextContains('The first node body'); + $assert_session->pageTextContains('Powered by Drupal'); + + // Assert that overrides can be turned off now that all overrides are gone. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => FALSE], 'Save'); + $this->drupalGet('node/1'); + $assert_session->linkNotExists('Layout'); + + // Add a new field. + $edit = [ + 'new_storage_type' => 'string', + 'label' => 'My text field', + 'field_name' => 'my_text', + ]; + $this->drupalPostForm("$field_ui_prefix/fields/add-field", $edit, 'Save and continue'); + $page->pressButton('Save field settings'); + $page->pressButton('Save settings'); + $this->drupalGet("$field_ui_prefix/display-layout/default"); + $assert_session->pageTextContains('My text field'); + $assert_session->elementExists('css', '.field--name-field-my-text'); + + // Delete the field. + $this->drupalPostForm("$field_ui_prefix/fields/node.bundle_with_section_field.field_my_text/delete", [], 'Delete'); + $this->drupalGet("$field_ui_prefix/display-layout/default"); + $assert_session->pageTextNotContains('My text field'); + $assert_session->elementNotExists('css', '.field--name-field-my-text'); + } + + /** + * Tests that component's dependencies are respected during removal. + */ + public function testPluginDependencies() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->container->get('module_installer')->install(['menu_ui']); + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer menu', + ])); + + // Create a new menu. + $this->drupalGet('admin/structure/menu/add'); + $page->fillField('label', 'My Menu'); + $page->fillField('id', 'mymenu'); + $page->pressButton('Save'); + + // Add a menu block. + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->linkExists('Add Block'); + $this->clickLink('Add Block'); + $assert_session->linkExists('My Menu'); + $this->clickLink('My Menu'); + $page->pressButton('Add Block'); + + // Add another block alongside the menu. + $assert_session->linkExists('Add Block'); + $this->clickLink('Add Block'); + $assert_session->linkExists('Powered by Drupal'); + $this->clickLink('Powered by Drupal'); + $page->pressButton('Add Block'); + + // Assert that the blocks are visible, and save the layout. + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->pageTextContains('My Menu'); + $assert_session->elementExists('css', '.block.menu--mymenu'); + $assert_session->linkExists('Save Layout'); + $this->clickLink('Save Layout'); + + // Delete the menu. + $this->drupalPostForm('admin/structure/menu/manage/mymenu/delete', [], 'Delete'); + + // Ensure that the menu block is gone, but that the other block remains. + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->pageTextNotContains('My Menu'); + $assert_session->elementNotExists('css', '.block.menu--mymenu'); + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index b2b5f9c..d5e0c75 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -2,8 +2,8 @@ namespace Drupal\Tests\layout_builder\Functional; -use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; use Drupal\Tests\BrowserTestBase; @@ -18,7 +18,7 @@ class LayoutSectionTest extends BrowserTestBase { /** * {@inheritdoc} */ - public static $modules = ['layout_builder', 'node', 'block_test']; + public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test']; /** * The name of the layout section field. @@ -34,19 +34,21 @@ protected function setUp() { parent::setUp(); $this->createContentType([ - 'type' => 'bundle_with_section_field', + 'type' => 'bundle_without_section_field', ]); $this->createContentType([ - 'type' => 'bundle_without_section_field', + 'type' => 'bundle_with_section_field', ]); - layout_builder_add_layout_section_field('node', 'bundle_with_section_field'); - $display = EntityViewDisplay::load('node.bundle_with_section_field.default'); - $display->setThirdPartySetting('layout_builder', 'allow_custom', TRUE); - $display->save(); + LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default') + ->setOverridable() + ->save(); $this->drupalLogin($this->drupalCreateUser([ 'configure any layout', + 'administer node display', + 'administer node fields', + 'administer content types', ], 'foobar')); } @@ -74,7 +76,6 @@ public function providerTestLayoutSectionFormatter() { ], [ 'foobar', - 'User context found', ], 'user', 'user:2', @@ -85,7 +86,7 @@ public function providerTestLayoutSectionFormatter() { [ 'section' => new Section('layout_onecol', [], [ 'baz' => new SectionComponent('baz', 'content', [ - 'id' => 'field_block:node:body', + 'id' => 'field_block:node:bundle_with_section_field:body', 'context_mapping' => [ 'entity' => 'layout_builder.entity', ], @@ -167,10 +168,11 @@ public function providerTestLayoutSectionFormatter() { public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) { $node = $this->createSectionNode($layout_data); - $this->drupalGet($node->toUrl('canonical')); + $canonical_url = $node->toUrl('canonical'); + $this->drupalGet($canonical_url); $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache); - $this->drupalGet($node->toUrl('layout-builder')); + $this->drupalGet($canonical_url->toString() . '/layout'); $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE'); } @@ -253,7 +255,7 @@ public function testLayoutPageTitle() { $this->drupalPlaceBlock('page_title_block'); $node = $this->createSectionNode([]); - $this->drupalGet($node->toUrl('layout-builder')); + $this->drupalGet($node->toUrl('canonical')->toString() . '/layout'); $this->assertSession()->titleEquals('Edit layout for The node title | Drupal'); $this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText()); } @@ -272,11 +274,52 @@ public function testLayoutUrlNoSectionField() { ], ]); $node->save(); - $this->drupalGet($node->toUrl('layout-builder')); + + $this->drupalGet($node->toUrl('canonical')->toString() . '/layout'); $this->assertSession()->statusCodeEquals(404); } /** + * Tests that deleting a field removes it from the layout. + */ + public function testLayoutDeletingField() { + $assert_session = $this->assertSession(); + + $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->statusCodeEquals(200); + $assert_session->elementExists('css', '.field--name-body'); + + // Delete the field from both bundles. + $this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete'); + $this->submitForm([], 'Delete'); + $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->statusCodeEquals(200); + $assert_session->elementExists('css', '.field--name-body'); + + $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete'); + $this->submitForm([], 'Delete'); + $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->statusCodeEquals(200); + $assert_session->elementNotExists('css', '.field--name-body'); + } + + /** + * Tests that deleting a bundle removes the layout. + */ + public function testLayoutDeletingBundle() { + $assert_session = $this->assertSession(); + + $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default'); + $this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display); + + $this->drupalPostForm('/admin/structure/types/manage/bundle_with_section_field/delete', [], 'Delete'); + $assert_session->statusCodeEquals(200); + + $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default'); + $this->assertNull($display); + } + + /** * Asserts the output of a layout section. * * @param string|array $expected_selector diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php index e7ae850..4f086d6 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php @@ -67,7 +67,7 @@ public function testFieldBlock() { $assert_session->pageTextNotContains('Initial email'); $assert_session->pageTextContains('Date field'); - $block_url = 'admin/structure/block/add/field_block%3Auser%3Afield_date/classy'; + $block_url = 'admin/structure/block/add/field_block%3Auser%3Auser%3Afield_date/classy'; $assert_session->linkByHrefExists($block_url); $this->drupalGet($block_url); @@ -112,6 +112,7 @@ public function testFieldBlock() { ]; $config = $this->container->get('config.factory')->get('block.block.datefield'); $this->assertEquals($expected, $config->get('settings.formatter')); + $this->assertEquals(['field.field.user.user.field_date'], $config->get('dependencies.config')); // Assert that the block is displaying the user field. $this->drupalGet('admin'); diff --git a/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php index a118cf5..8c529cc 100644 --- a/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php @@ -186,7 +186,7 @@ protected function getTestBlock(ProphecyInterface $entity_prophecy, array $confi $block = new FieldBlock( $configuration, - 'field_block:entity_test:the_field_name', + 'field_block:entity_test:entity_test:the_field_name', $plugin_definition, $entity_field_manager->reveal(), $formatter_manager->reveal(), diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php index 6c376fa..d5a4cd0 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php @@ -42,6 +42,7 @@ protected function setUp() { $this->installEntitySchema('entity_test_base_field_display'); $this->installConfig(['filter']); + $this->installSchema('system', ['key_value_expire']); // Set up a non-admin user that is allowed to view test entities. \Drupal::currentUser()->setAccount($this->createUser(['uid' => 2], ['view test entity'])); @@ -68,7 +69,7 @@ protected function setUp() { 'status' => TRUE, ]); $this->display - ->setComponent('test_field_display_configurable', ['region' => 'content', 'weight' => 5]) + ->setComponent('test_field_display_configurable', ['weight' => 5]) ->save(); // Create an entity with fields that are configurable and non-configurable. @@ -92,7 +93,7 @@ protected function installLayoutBuilder() { $this->refreshServices(); $this->display = $this->reloadEntity($this->display); - $this->display->setThirdPartySetting('layout_builder', 'allow_custom', TRUE)->save(); + $this->display->setOverridable()->save(); $this->entity = $this->reloadEntity($this->entity); } diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php new file mode 100644 index 0000000..a554fb5 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php @@ -0,0 +1,43 @@ + 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + 'third_party_settings' => [ + 'layout_builder' => [ + 'sections' => $section_data, + ], + ], + ]); + $display->save(); + return $display; + } + + /** + * Tests that configuration schema enforces valid values. + */ + public function testInvalidConfiguration() { + $this->setExpectedException(SchemaIncompleteException::class); + $this->sectionStorage->getSection(0)->getComponent('first-uuid')->setConfiguration(['id' => 'foo', 'bar' => 'baz']); + $this->sectionStorage->save(); + } + +} diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php index 3873af8..fa646df 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\layout_builder\Kernel; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\layout_builder\Section; /** @@ -49,6 +51,40 @@ public function testCompatibility() { $this->entity->get('layout_builder__layout')->removeSection(0); $this->entity->save(); $this->assertFieldAttributes($this->entity, $expected_fields); + + // Test that adding a new field after Layout Builder has been installed will + // add the new field to the default region of the first section. + $field_storage = FieldStorageConfig::create([ + 'entity_type' => 'entity_test_base_field_display', + 'field_name' => 'test_field_display_post_install', + 'type' => 'text', + ]); + $field_storage->save(); + FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'entity_test_base_field_display', + 'label' => 'FieldConfig with configurable display', + ])->save(); + + $this->entity = $this->reloadEntity($this->entity); + $this->entity->test_field_display_post_install = 'Test string'; + $this->entity->save(); + + $this->display = $this->reloadEntity($this->display); + $this->display + ->setComponent('test_field_display_post_install', ['weight' => 50]) + ->save(); + $new_expected_fields = [ + 'field field--name-name field--type-string field--label-hidden field__item', + 'field field--name-test-field-display-configurable field--type-boolean field--label-above', + 'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above', + 'clearfix text-formatted field field--name-test-field-display-post-install field--type-text field--label-above', + 'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above', + 'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above', + ]; + $this->assertFieldAttributes($this->entity, $new_expected_fields); + $this->assertNotEmpty($this->cssSelect('.layout--onecol')); + $this->assertText('Test string'); } } diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php index f3a6e6f..af8395f 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\layout_builder\Kernel; use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; /** * Tests the field type for Layout Sections. @@ -26,7 +27,12 @@ class LayoutSectionItemListTest extends SectionStorageTestBase { */ protected function getSectionStorage(array $section_data) { $this->installEntitySchema('entity_test_base_field_display'); - layout_builder_add_layout_section_field('entity_test_base_field_display', 'entity_test_base_field_display'); + LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test_base_field_display', + 'bundle' => 'entity_test_base_field_display', + 'mode' => 'default', + 'status' => TRUE, + ])->setOverridable()->save(); array_map(function ($row) { return ['section' => $row]; diff --git a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php index 97c1fba..1879977 100644 --- a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php +++ b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php @@ -2,14 +2,14 @@ namespace Drupal\Tests\layout_builder\Kernel; -use Drupal\KernelTests\KernelTestBase; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; /** * Provides a base class for testing implementations of section storage. */ -abstract class SectionStorageTestBase extends KernelTestBase { +abstract class SectionStorageTestBase extends EntityKernelTestBase { /** * {@inheritdoc} @@ -18,8 +18,6 @@ 'layout_builder', 'layout_discovery', 'layout_test', - 'user', - 'entity_test', ]; /** @@ -35,12 +33,14 @@ protected function setUp() { parent::setUp(); + $this->installSchema('system', ['key_value_expire']); + $section_data = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ]; $this->sectionStorage = $this->getSectionStorage($section_data); @@ -63,10 +63,10 @@ protected function setUp() { public function testGetSections() { $expected = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ]; $this->assertSections($expected); @@ -83,7 +83,7 @@ public function testGetSection() { * @covers ::getSection */ public function testGetSectionInvalidDelta() { - $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2" for the "The test entity"'); + $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2"'); $this->sectionStorage->getSection(2); } @@ -93,11 +93,11 @@ public function testGetSectionInvalidDelta() { public function testInsertSection() { $expected = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('setting_1'), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ]; @@ -111,10 +111,10 @@ public function testInsertSection() { public function testAppendSection() { $expected = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), new Section('foo'), ]; @@ -129,7 +129,7 @@ public function testAppendSection() { public function testRemoveSection() { $expected = [ new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ]; diff --git a/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php new file mode 100644 index 0000000..1aa1c16 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php @@ -0,0 +1,339 @@ +entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class); + $sample_entity_generator = $this->prophesize(LayoutBuilderSampleEntityGenerator::class); + + $definition = new SectionStorageDefinition([ + 'id' => 'defaults', + 'class' => DefaultsSectionStorage::class, + ]); + $this->plugin = new DefaultsSectionStorage([], '', $definition, $this->entityTypeManager->reveal(), $entity_type_bundle_info->reveal(), $sample_entity_generator->reveal()); + } + + /** + * @covers ::extractIdFromRoute + * + * @dataProvider providerTestExtractIdFromRoute + */ + public function testExtractIdFromRoute($expected, $value, array $defaults) { + $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); + $this->assertSame($expected, $result); + } + + /** + * Provides data for ::testExtractIdFromRoute(). + */ + public function providerTestExtractIdFromRoute() { + $data = []; + $data['with value'] = [ + 'foo.bar.baz', + 'foo.bar.baz', + [], + ]; + $data['empty value, without bundle'] = [ + 'my_entity_type.bundle_name.default', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'view_mode_name' => 'default', + 'bundle_key' => 'my_bundle', + 'my_bundle' => 'bundle_name', + ], + ]; + $data['empty value, with bundle'] = [ + 'my_entity_type.bundle_name.default', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'view_mode_name' => 'default', + 'bundle' => 'bundle_name', + ], + ]; + $data['without value, empty defaults'] = [ + NULL, + '', + [], + ]; + return $data; + } + + /** + * @covers ::getSectionListFromId + * + * @dataProvider providerTestGetSectionListFromId + */ + public function testGetSectionListFromId($success, $expected_entity_id, $value) { + if ($expected_entity_id) { + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage->load($expected_entity_id)->willReturn('the_return_value'); + + $this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); + $this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); + } + else { + $this->entityTypeManager->getDefinition('entity_view_display')->shouldNotBeCalled(); + $this->entityTypeManager->getStorage('entity_view_display')->shouldNotBeCalled(); + } + + if (!$success) { + $this->setExpectedException(\InvalidArgumentException::class); + } + + $result = $this->plugin->getSectionListFromId($value); + if ($success) { + $this->assertEquals('the_return_value', $result); + } + } + + /** + * Provides data for ::testGetSectionListFromId(). + */ + public function providerTestGetSectionListFromId() { + $data = []; + $data['with value'] = [ + TRUE, + 'foo.bar.baz', + 'foo.bar.baz', + ]; + $data['without value, empty defaults'] = [ + FALSE, + NULL, + '', + ]; + return $data; + } + + /** + * @covers ::getSectionListFromId + */ + public function testGetSectionListFromIdCreate() { + $expected = 'the_return_value'; + $value = 'foo.bar.baz'; + $expected_create_values = [ + 'targetEntityType' => 'foo', + 'bundle' => 'bar', + 'mode' => 'baz', + 'status' => TRUE, + ]; + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage->load($value)->willReturn(NULL); + $entity_storage->create($expected_create_values)->willReturn($expected); + + $this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); + $this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); + + $result = $this->plugin->getSectionListFromId($value); + $this->assertSame($expected, $result); + } + + /** + * @covers ::buildRoutes + * @covers ::getEntityTypes + */ + public function testBuildRoutes() { + $entity_types = []; + $entity_types['no_link_template'] = new EntityType(['id' => 'no_link_template']); + $entity_types['unknown_field_ui_route'] = new EntityType([ + 'id' => 'unknown_field_ui_route', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + 'field_ui_base_route' => 'unknown', + ]); + $entity_types['with_bundle_key'] = new EntityType([ + 'id' => 'with_bundle_key', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id', 'bundle' => 'bundle'], + 'bundle_entity_type' => 'my_bundle_type', + 'field_ui_base_route' => 'known', + ]); + $entity_types['with_bundle_parameter'] = new EntityType([ + 'id' => 'with_bundle_parameter', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + 'field_ui_base_route' => 'with_bundle', + ]); + $this->entityTypeManager->getDefinitions()->willReturn($entity_types); + + $expected = [ + 'known' => new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE]), + 'with_bundle' => new Route('/admin/entity/{bundle}'), + 'layout_builder.defaults.with_bundle_key.view' => new Route( + '/admin/entity/whatever/display-layout/{view_mode_name}', + [ + 'entity_type_id' => 'with_bundle_key', + 'bundle_key' => 'my_bundle_type', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_key display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_key.save' => new Route( + '/admin/entity/whatever/display-layout/{view_mode_name}/save', + [ + 'entity_type_id' => 'with_bundle_key', + 'bundle_key' => 'my_bundle_type', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_key display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_key.cancel' => new Route( + '/admin/entity/whatever/display-layout/{view_mode_name}/cancel', + [ + 'entity_type_id' => 'with_bundle_key', + 'bundle_key' => 'my_bundle_type', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_key display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_parameter.view' => new Route( + '/admin/entity/{bundle}/display-layout/{view_mode_name}', + [ + 'entity_type_id' => 'with_bundle_parameter', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_parameter.save' => new Route( + '/admin/entity/{bundle}/display-layout/{view_mode_name}/save', + [ + 'entity_type_id' => 'with_bundle_parameter', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_parameter.cancel' => new Route( + '/admin/entity/{bundle}/display-layout/{view_mode_name}/cancel', + [ + 'entity_type_id' => 'with_bundle_parameter', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + ]; + + $collection = new RouteCollection(); + $collection->add('known', new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE])); + $collection->add('with_bundle', new Route('/admin/entity/{bundle}')); + + $this->plugin->buildRoutes($collection); + $this->assertEquals($expected, $collection->all()); + $this->assertSame(array_keys($expected), array_keys($collection->all())); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php index e209f6a..e4b20bd 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php @@ -2,13 +2,15 @@ namespace Drupal\Tests\layout_builder\Unit; -use Drupal\Core\Entity\EntityFieldManagerInterface; -use Drupal\Core\Entity\EntityType; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Routing\RouteBuildEvent; use Drupal\layout_builder\Routing\LayoutBuilderRoutes; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; +use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; /** * @coversDefaultClass \Drupal\layout_builder\Routing\LayoutBuilderRoutes @@ -20,6 +22,13 @@ class LayoutBuilderRoutesTest extends UnitTestCase { /** * The Layout Builder route builder. * + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface + */ + protected $sectionStorageManager; + + /** + * The Layout Builder route builder. + * * @var \Drupal\layout_builder\Routing\LayoutBuilderRoutes */ protected $routeBuilder; @@ -30,373 +39,46 @@ class LayoutBuilderRoutesTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $entity_types = []; - $entity_types['no_link_template'] = new EntityType(['id' => 'no_link_template']); - $entity_types['with_link_template'] = new EntityType([ - 'id' => 'with_link_template', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - 'field_ui_base_route' => 'unknown', - ]); - $entity_types['with_integer_id'] = new EntityType([ - 'id' => 'with_integer_id', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - ]); - $entity_types['with_field_ui_route'] = new EntityType([ - 'id' => 'with_field_ui_route', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - 'field_ui_base_route' => 'known', - ]); - $entity_types['with_bundle_key'] = new EntityType([ - 'id' => 'with_field_ui_route', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id', 'bundle' => 'bundle'], - 'bundle_entity_type' => 'my_bundle_type', - 'field_ui_base_route' => 'known', - ]); - $entity_types['with_bundle_parameter'] = new EntityType([ - 'id' => 'with_bundle_parameter', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - 'field_ui_base_route' => 'with_bundle', - ]); - $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $entity_type_manager->getDefinitions()->willReturn($entity_types); - - $string_id = $this->prophesize(FieldStorageDefinitionInterface::class); - $string_id->getType()->willReturn('string'); - $integer_id = $this->prophesize(FieldStorageDefinitionInterface::class); - $integer_id->getType()->willReturn('integer'); - $entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class); - $entity_field_manager->getFieldStorageDefinitions('no_link_template')->shouldNotBeCalled(); - $entity_field_manager->getFieldStorageDefinitions('with_link_template')->willReturn(['id' => $string_id->reveal()]); - $entity_field_manager->getFieldStorageDefinitions('with_integer_id')->willReturn(['id' => $integer_id->reveal()]); - $entity_field_manager->getFieldStorageDefinitions('with_field_ui_route')->willReturn(['id' => $integer_id->reveal()]); - $entity_field_manager->getFieldStorageDefinitions('with_bundle_parameter')->willReturn(['id' => $integer_id->reveal()]); - - $this->routeBuilder = new LayoutBuilderRoutes($entity_type_manager->reveal(), $entity_field_manager->reveal()); + $this->sectionStorageManager = $this->prophesize(SectionStorageManagerInterface::class); + $this->routeBuilder = new LayoutBuilderRoutes($this->sectionStorageManager->reveal()); } /** - * @covers ::getRoutes - * @covers ::buildRoute - * @covers ::hasIntegerId - * @covers ::getEntityTypes + * @covers ::onAlterRoutes */ - public function testGetRoutes() { + public function testOnAlterRoutes() { $expected = [ - 'entity.with_link_template.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_link_template', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_link_template' => ['type' => 'entity:with_link_template'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_link_template.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_link_template', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_link_template' => ['type' => 'entity:with_link_template'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_link_template.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_link_template', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_link_template' => ['type' => 'entity:with_link_template'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_integer_id.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_integer_id', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_integer_id' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_integer_id' => ['type' => 'entity:with_integer_id'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_integer_id.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_integer_id', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_integer_id' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_integer_id' => ['type' => 'entity:with_integer_id'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_integer_id.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_integer_id', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_integer_id' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_integer_id' => ['type' => 'entity:with_integer_id'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_field_ui_route.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_field_ui_route', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_field_ui_route' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_field_ui_route.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_field_ui_route', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_field_ui_route' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_field_ui_route.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_field_ui_route', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_field_ui_route' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_key.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_bundle_key', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_key' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_key' => ['type' => 'entity:with_bundle_key'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_key.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_bundle_key', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_key' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_key' => ['type' => 'entity:with_bundle_key'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_key.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_bundle_key', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_key' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_key' => ['type' => 'entity:with_bundle_key'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_parameter.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_parameter' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_parameter.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_parameter' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_parameter.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_parameter' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'], - ], - '_layout_builder' => TRUE, - ] - ), + 'test_route1' => new Route('/test/path1'), + 'test_route_shared' => new Route('/test/path/shared2'), + 'test_route2' => new Route('/test/path2'), ]; - $this->assertEquals($expected, $this->routeBuilder->getRoutes()); + $section_storage_first = $this->prophesize(SectionStorageInterface::class); + $section_storage_first->buildRoutes(Argument::type(RouteCollection::class))->shouldBeCalled()->will(function ($args) { + /** @var \Symfony\Component\Routing\RouteCollection $collection */ + $collection = $args[0]; + $collection->add('test_route_shared', new Route('/test/path/shared1')); + $collection->add('test_route1', new Route('/test/path1')); + }); + $section_storage_second = $this->prophesize(SectionStorageInterface::class); + $section_storage_second->buildRoutes(Argument::type(RouteCollection::class))->shouldBeCalled()->will(function ($args) { + /** @var \Symfony\Component\Routing\RouteCollection $collection */ + $collection = $args[0]; + $collection->add('test_route_shared', new Route('/test/path/shared2')); + $collection->add('test_route2', new Route('/test/path2')); + }); + + $this->sectionStorageManager->loadEmpty('first')->willReturn($section_storage_first->reveal()); + $this->sectionStorageManager->loadEmpty('second')->willReturn($section_storage_second->reveal()); + $definitions['first'] = new SectionStorageDefinition(); + $definitions['second'] = new SectionStorageDefinition(); + $this->sectionStorageManager->getDefinitions()->willReturn($definitions); + + $collection = new RouteCollection(); + $event = new RouteBuildEvent($collection); + $this->routeBuilder->onAlterRoutes($event); + $this->assertEquals($expected, $collection->all()); + $this->assertSame(array_keys($expected), array_keys($collection->all())); } } diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php index 3c3c9f4..5f8dc05 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php @@ -2,10 +2,9 @@ namespace Drupal\Tests\layout_builder\Unit; -use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Drupal\layout_builder\Routing\LayoutTempstoreParamConverter; -use Drupal\layout_builder\Routing\SectionStorageParamConverterInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; @@ -18,23 +17,23 @@ class LayoutTempstoreParamConverterTest extends UnitTestCase { /** * @covers ::convert - * @covers ::getParamConverterFromDefaults */ public function testConvert() { $layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class); - $class_resolver = $this->prophesize(ClassResolverInterface::class); - $param_converter = $this->prophesize(SectionStorageParamConverterInterface::class); - $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $class_resolver->reveal()); + $section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class); + $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal()); + + $section_storage = $this->prophesize(SectionStorageInterface::class); $value = 'some_value'; $definition = ['layout_builder_tempstore' => TRUE]; $name = 'the_parameter_name'; $defaults = ['section_storage_type' => 'my_type']; - $section_storage = $this->prophesize(SectionStorageInterface::class); $expected = 'the_return_value'; - $class_resolver->getInstanceFromDefinition('layout_builder.section_storage_param_converter.my_type')->willReturn($param_converter->reveal()); - $param_converter->convert($value, $definition, $name, $defaults)->willReturn($section_storage->reveal()); + $section_storage_manager->hasDefinition('my_type')->willReturn(TRUE); + $section_storage_manager->loadFromRoute('my_type', $value, $definition, $name, $defaults)->willReturn($section_storage); + $layout_tempstore_repository->get($section_storage->reveal())->willReturn($expected); $result = $converter->convert($value, $definition, $name, $defaults); @@ -43,19 +42,19 @@ public function testConvert() { /** * @covers ::convert - * @covers ::getParamConverterFromDefaults */ public function testConvertNoType() { $layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class); - $class_resolver = $this->prophesize(ClassResolverInterface::class); - $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $class_resolver->reveal()); + $section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class); + $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal()); $value = 'some_value'; $definition = ['layout_builder_tempstore' => TRUE]; $name = 'the_parameter_name'; $defaults = ['section_storage_type' => NULL]; - $class_resolver->getInstanceFromDefinition()->shouldNotBeCalled(); + $section_storage_manager->hasDefinition()->shouldNotBeCalled(); + $section_storage_manager->loadFromRoute()->shouldNotBeCalled(); $layout_tempstore_repository->get()->shouldNotBeCalled(); $result = $converter->convert($value, $definition, $name, $defaults); @@ -64,19 +63,19 @@ public function testConvertNoType() { /** * @covers ::convert - * @covers ::getParamConverterFromDefaults */ public function testConvertInvalidConverter() { $layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class); - $class_resolver = $this->prophesize(ClassResolverInterface::class); - $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $class_resolver->reveal()); + $section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class); + $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal()); $value = 'some_value'; $definition = ['layout_builder_tempstore' => TRUE]; $name = 'the_parameter_name'; $defaults = ['section_storage_type' => 'invalid']; - $class_resolver->getInstanceFromDefinition('layout_builder.section_storage_param_converter.invalid')->willThrow(\InvalidArgumentException::class); + $section_storage_manager->hasDefinition('invalid')->willReturn(FALSE); + $section_storage_manager->loadFromRoute()->shouldNotBeCalled(); $layout_tempstore_repository->get()->shouldNotBeCalled(); $result = $converter->convert($value, $definition, $name, $defaults); diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php index b7702c1..0c74d01 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php @@ -2,11 +2,11 @@ namespace Drupal\Tests\layout_builder\Unit; +use Drupal\Core\TempStore\SharedTempStore; +use Drupal\Core\TempStore\SharedTempStoreFactory; use Drupal\layout_builder\LayoutTempstoreRepository; use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; -use Drupal\Core\TempStore\SharedTempStore; -use Drupal\Core\TempStore\SharedTempStoreFactory; /** * @coversDefaultClass \Drupal\layout_builder\LayoutTempstoreRepository @@ -26,7 +26,7 @@ public function testGetEmptyTempstore() { $tempstore->get('my_storage_id')->shouldBeCalled(); $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class); - $tempstore_factory->get('layout_builder.my_storage_type')->willReturn($tempstore->reveal()); + $tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal()); $repository = new LayoutTempstoreRepository($tempstore_factory->reveal()); @@ -46,7 +46,7 @@ public function testGetLoadedTempstore() { $tempstore = $this->prophesize(SharedTempStore::class); $tempstore->get('my_storage_id')->willReturn(['section_storage' => $tempstore_section_storage->reveal()]); $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class); - $tempstore_factory->get('layout_builder.my_storage_type')->willReturn($tempstore->reveal()); + $tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal()); $repository = new LayoutTempstoreRepository($tempstore_factory->reveal()); @@ -67,7 +67,7 @@ public function testGetInvalidEntry() { $tempstore->get('my_storage_id')->willReturn(['section_storage' => 'this_is_not_an_entity']); $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class); - $tempstore_factory->get('layout_builder.my_storage_type')->willReturn($tempstore->reveal()); + $tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal()); $repository = new LayoutTempstoreRepository($tempstore_factory->reveal()); diff --git a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php new file mode 100644 index 0000000..711ce5d --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php @@ -0,0 +1,363 @@ +entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class); + + $definition = new SectionStorageDefinition([ + 'id' => 'overrides', + 'class' => OverridesSectionStorage::class, + ]); + $this->plugin = new OverridesSectionStorage([], 'overrides', $definition, $this->entityTypeManager->reveal(), $this->entityFieldManager->reveal()); + } + + /** + * @covers ::extractIdFromRoute + * + * @dataProvider providerTestExtractIdFromRoute + */ + public function testExtractIdFromRoute($expected, $value, array $defaults) { + $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); + $this->assertSame($expected, $result); + } + + /** + * Provides data for ::testExtractIdFromRoute(). + */ + public function providerTestExtractIdFromRoute() { + $data = []; + $data['with value, with layout'] = [ + 'my_entity_type.entity_with_layout', + 'my_entity_type.entity_with_layout', + [], + ]; + $data['with value, without layout'] = [ + NULL, + 'my_entity_type', + [], + ]; + $data['empty value, populated defaults'] = [ + 'my_entity_type.entity_with_layout', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'my_entity_type' => 'entity_with_layout', + ], + ]; + $data['empty value, empty defaults'] = [ + NULL, + '', + [], + ]; + return $data; + } + + /** + * @covers ::getSectionListFromId + * + * @dataProvider providerTestGetSectionListFromId + */ + public function testGetSectionListFromId($success, $expected_entity_type_id, $id) { + $defaults['the_parameter_name'] = $id; + + if ($expected_entity_type_id) { + $entity_storage = $this->prophesize(EntityStorageInterface::class); + + $entity_without_layout = $this->prophesize(FieldableEntityInterface::class); + $entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE); + $entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled(); + $entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal()); + + $entity_with_layout = $this->prophesize(FieldableEntityInterface::class); + $entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE); + $entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value'); + $entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal()); + + $this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal()); + } + else { + $this->entityTypeManager->getStorage(Argument::any())->shouldNotBeCalled(); + } + + if (!$success) { + $this->setExpectedException(\InvalidArgumentException::class); + } + + $result = $this->plugin->getSectionListFromId($id); + if ($success) { + $this->assertEquals('the_return_value', $result); + } + } + + /** + * Provides data for ::testGetSectionListFromId(). + */ + public function providerTestGetSectionListFromId() { + $data = []; + $data['with value, with layout'] = [ + TRUE, + 'my_entity_type', + 'my_entity_type.entity_with_layout', + ]; + $data['with value, without layout'] = [ + FALSE, + 'my_entity_type', + 'my_entity_type.entity_without_layout', + ]; + $data['empty value, empty defaults'] = [ + FALSE, + NULL, + '', + ]; + return $data; + } + + /** + * @covers ::buildRoutes + * @covers ::hasIntegerId + * @covers ::getEntityTypes + */ + public function testBuildRoutes() { + $entity_types = []; + + $entity_types['no_link_template'] = new EntityType(['id' => 'no_link_template']); + $this->entityFieldManager->getFieldStorageDefinitions('no_link_template')->shouldNotBeCalled(); + + $entity_types['with_string_id'] = new EntityType([ + 'id' => 'with_string_id', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + ]); + $string_id = $this->prophesize(FieldStorageDefinitionInterface::class); + $string_id->getType()->willReturn('string'); + $this->entityFieldManager->getFieldStorageDefinitions('with_string_id')->willReturn(['id' => $string_id->reveal()]); + + $entity_types['with_integer_id'] = new EntityType([ + 'id' => 'with_integer_id', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + ]); + $integer_id = $this->prophesize(FieldStorageDefinitionInterface::class); + $integer_id->getType()->willReturn('integer'); + $this->entityFieldManager->getFieldStorageDefinitions('with_integer_id')->willReturn(['id' => $integer_id->reveal()]); + + $this->entityTypeManager->getDefinitions()->willReturn($entity_types); + + $expected = [ + 'layout_builder.overrides.with_string_id.view' => new Route( + '/entity/{entity}/layout', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_string_id.save' => new Route( + '/entity/{entity}/layout/save', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_string_id.cancel' => new Route( + '/entity/{entity}/layout/cancel', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_string_id.revert' => new Route( + '/entity/{entity}/layout/revert', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.view' => new Route( + '/entity/{entity}/layout', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.save' => new Route( + '/entity/{entity}/layout/save', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.cancel' => new Route( + '/entity/{entity}/layout/cancel', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.revert' => new Route( + '/entity/{entity}/layout/revert', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + ]; + + $collection = new RouteCollection(); + $this->plugin->buildRoutes($collection); + $this->assertEquals($expected, $collection->all()); + $this->assertSame(array_keys($expected), array_keys($collection->all())); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php index 6dade75..ed7dfd2 100644 --- a/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php @@ -136,6 +136,9 @@ public function testToRenderArrayAccessDenied() { $access_result = AccessResult::forbidden(); $block->access($this->account->reveal(), TRUE)->willReturn($access_result); $block->build()->shouldNotBeCalled(); + $block->getCacheContexts()->willReturn([]); + $block->getCacheTags()->willReturn([]); + $block->getCacheMaxAge()->willReturn(Cache::PERMANENT); $section = [ new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), @@ -158,6 +161,50 @@ public function testToRenderArrayAccessDenied() { /** * @covers ::toRenderArray */ + public function testToRenderArrayPreview() { + $block_content = ['#markup' => 'The block content.']; + $render_array = [ + '#theme' => 'block', + '#weight' => 0, + '#configuration' => [], + '#plugin_id' => 'block_plugin_id', + '#base_plugin_id' => 'block_plugin_id', + '#derivative_plugin_id' => NULL, + 'content' => $block_content, + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => 0, + ], + ]; + $block = $this->prophesize(BlockPluginInterface::class); + $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); + + $block->access($this->account->reveal(), TRUE)->shouldNotBeCalled(); + $block->build()->willReturn($block_content); + $block->getCacheContexts()->willReturn([]); + $block->getCacheTags()->willReturn([]); + $block->getCacheMaxAge()->willReturn(Cache::PERMANENT); + $block->getConfiguration()->willReturn([]); + $block->getPluginId()->willReturn('block_plugin_id'); + $block->getBaseId()->willReturn('block_plugin_id'); + $block->getDerivativeId()->willReturn(NULL); + + $section = [ + new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), + ]; + $expected = [ + 'content' => [ + 'some_uuid' => $render_array, + ], + ]; + $result = (new Section('layout_onecol', [], $section))->toRenderArray([], TRUE); + $this->assertEquals($expected, $result); + } + + /** + * @covers ::toRenderArray + */ public function testToRenderArrayEmpty() { $section = []; $expected = []; diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php new file mode 100644 index 0000000..392cd89 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php @@ -0,0 +1,100 @@ +prophesize(CacheBackendInterface::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $this->manager = new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal()); + + $this->plugin = $this->prophesize(SectionStorageInterface::class); + + $factory = $this->prophesize(FactoryInterface::class); + $factory->createInstance('the_plugin_id', [])->willReturn($this->plugin->reveal()); + $reflection_property = new \ReflectionProperty($this->manager, 'factory'); + $reflection_property->setAccessible(TRUE); + $reflection_property->setValue($this->manager, $factory->reveal()); + } + + /** + * @covers ::loadEmpty + */ + public function testLoadEmpty() { + $result = $this->manager->loadEmpty('the_plugin_id'); + $this->assertInstanceOf(SectionStorageInterface::class, $result); + } + + /** + * @covers ::loadFromStorageId + */ + public function testLoadFromStorageId() { + $section_list = $this->prophesize(SectionListInterface::class); + $this->plugin->setSectionList($section_list->reveal())->will(function () { + return $this; + }); + $this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal()); + + $result = $this->manager->loadFromStorageId('the_plugin_id', 'the_storage_id'); + $this->assertInstanceOf(SectionStorageInterface::class, $result); + } + + /** + * @covers ::loadFromRoute + */ + public function testLoadFromRoute() { + $section_list = $this->prophesize(SectionListInterface::class); + $this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', [])->willReturn('the_storage_id'); + $this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal()); + $this->plugin->setSectionList($section_list->reveal())->will(function () { + return $this; + }); + + $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', []); + $this->assertInstanceOf(SectionStorageInterface::class, $result); + } + + /** + * @covers ::loadFromRoute + */ + public function testLoadFromRouteNull() { + $this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn(NULL); + + $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', ['_route' => 'the_route_name']); + $this->assertNull($result); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageOverridesParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageOverridesParamConverterTest.php deleted file mode 100644 index f0c0146..0000000 --- a/core/modules/layout_builder/tests/src/Unit/SectionStorageOverridesParamConverterTest.php +++ /dev/null @@ -1,119 +0,0 @@ -entityManager = $this->prophesize(EntityManagerInterface::class); - $this->converter = new SectionStorageOverridesParamConverter($this->entityManager->reveal()); - } - - /** - * @covers ::convert - * @covers ::getEntityTypeFromDefaults - * @covers ::getEntityIdFromDefaults - * - * @dataProvider providerTestConvert - */ - public function testConvert($success, $expected_entity_type_id, $value, array $defaults) { - $defaults['the_parameter_name'] = $value; - - if ($expected_entity_type_id) { - $entity_storage = $this->prophesize(EntityStorageInterface::class); - - $entity_without_layout = $this->prophesize(FieldableEntityInterface::class); - $entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE); - $entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled(); - $entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal()); - - $entity_with_layout = $this->prophesize(FieldableEntityInterface::class); - $entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE); - $entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value'); - $entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal()); - - $this->entityManager->getDefinition($expected_entity_type_id)->willReturn(new EntityType(['id' => 'entity_view_display'])); - $this->entityManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal()); - } - else { - $this->entityManager->getDefinition(Argument::any())->shouldNotBeCalled(); - $this->entityManager->getStorage(Argument::any())->shouldNotBeCalled(); - } - - $result = $this->converter->convert($value, [], 'the_parameter_name', $defaults); - if ($success) { - $this->assertEquals('the_return_value', $result); - } - else { - $this->assertNull($result); - } - } - - /** - * Provides data for ::testConvert(). - */ - public function providerTestConvert() { - $data = []; - $data['with value, with layout'] = [ - TRUE, - 'my_entity_type', - 'my_entity_type:entity_with_layout', - [], - ]; - $data['with value, without layout'] = [ - FALSE, - 'my_entity_type', - 'my_entity_type:entity_without_layout', - [], - ]; - $data['empty value, populated defaults'] = [ - TRUE, - 'my_entity_type', - '', - [ - 'entity_type_id' => 'my_entity_type', - 'my_entity_type' => 'entity_with_layout', - ], - ]; - $data['empty value, empty defaults'] = [ - FALSE, - NULL, - '', - [], - ]; - return $data; - } - -} diff --git a/core/modules/locale/css/locale.admin.css b/core/modules/locale/css/locale.admin.css index 76bb2e4..739f895 100644 --- a/core/modules/locale/css/locale.admin.css +++ b/core/modules/locale/css/locale.admin.css @@ -72,12 +72,12 @@ vertical-align: top; } .locale-translation-update__wrapper { - background: transparent url(../../../misc/menu-collapsed.png) left .6em no-repeat; + background: transparent url(../../../misc/menu-collapsed.png) left 0.6em no-repeat; margin-left: -12px; padding-left: 12px; } .expanded .locale-translation-update__wrapper { - background: transparent url(../../../misc/menu-expanded.png) left .6em no-repeat; + background: transparent url(../../../misc/menu-expanded.png) left 0.6em no-repeat; } #locale-translation-status-form .description { cursor: pointer; diff --git a/core/modules/media/config/optional/system.action.media_delete_action.yml b/core/modules/media/config/optional/system.action.media_delete_action.yml new file mode 100644 index 0000000..7311917 --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_delete_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_delete_action +label: 'Delete media' +type: media +plugin: entity:delete_action:media +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_publish_action.yml b/core/modules/media/config/optional/system.action.media_publish_action.yml new file mode 100644 index 0000000..0e5cf4f --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_publish_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_publish_action +label: 'Publish media' +type: media +plugin: entity:publish_action:media +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_save_action.yml b/core/modules/media/config/optional/system.action.media_save_action.yml new file mode 100644 index 0000000..ead7574 --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_save_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_save_action +label: 'Save media' +type: media +plugin: entity:save_action:media +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_unpublish_action.yml b/core/modules/media/config/optional/system.action.media_unpublish_action.yml new file mode 100644 index 0000000..2d24d73 --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_unpublish_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_unpublish_action +label: 'Unpublish media' +type: media +plugin: entity:unpublish_action:media +configuration: { } diff --git a/core/modules/media/config/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml index 88b7674..84587b5 100644 --- a/core/modules/media/config/optional/views.view.media.yml +++ b/core/modules/media/config/optional/views.view.media.yml @@ -132,6 +132,59 @@ display: row: type: fields fields: + media_bulk_form: + id: media_bulk_form + table: media + field: media_bulk_form + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + action_title: Action + include_exclude: exclude + selected_actions: { } + entity_type: media + plugin_id: bulk_form thumbnail__target_id: id: thumbnail__target_id table: media_field_data diff --git a/core/modules/media/media.info.yml b/core/modules/media/media.info.yml index e69f14a..cb3ec1a 100644 --- a/core/modules/media/media.info.yml +++ b/core/modules/media/media.info.yml @@ -4,7 +4,6 @@ type: module package: Core version: VERSION core: 8.x -hidden: true dependencies: - file - image diff --git a/core/modules/media/media.module b/core/modules/media/media.module index 2969459..5079f0f 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -9,8 +9,10 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Render\Element\RenderElement; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Template\Attribute; use Drupal\Core\Url; use Drupal\field\FieldConfigInterface; @@ -66,6 +68,10 @@ function media_theme() { 'media' => [ 'render element' => 'elements', ], + 'media_reference_help' => [ + 'render element' => 'element', + 'base hook' => 'field_multiple_value_form', + ], ]; } @@ -172,3 +178,128 @@ function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInter $form['add']['new_storage_type']['#weight'] = 0; $form['add']['description_wrapper']['#weight'] = 1; } + +/** + * Implements hook_field_widget_multivalue_form_alter(). + */ +function media_field_widget_multivalue_form_alter(array &$elements, FormStateInterface $form_state, array $context) { + // Do not alter the default settings form. + if ($context['default']) { + return; + } + + // Only act on entity reference fields that reference media. + $field_type = $context['items']->getFieldDefinition()->getType(); + $target_type = $context['items']->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type'); + if ($field_type !== 'entity_reference' || $target_type !== 'media') { + return; + } + + // Autocomplete widgets need different help text than options widgets. + $widget_plugin_id = $context['widget']->getPluginId(); + if (in_array($widget_plugin_id, ['entity_reference_autocomplete', 'entity_reference_autocomplete_tags'])) { + $is_autocomplete = TRUE; + } + else { + // @todo We can't yet properly alter non-autocomplete fields. Resolve this + // in https://www.drupal.org/node/2943020 and remove this condition. + return; + } + $elements['#media_help'] = []; + + // Retrieve the media bundle list and add information for the user based on + // which bundles are available to be created or referenced. + $settings = $context['items']->getFieldDefinition()->getSetting('handler_settings'); + $allowed_bundles = isset($settings['target_bundles']) ? $settings['target_bundles'] : []; + $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media'); + $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media'); + $bundle_labels = []; + $create_bundles = []; + foreach ($allowed_bundles as $bundle) { + $bundle_labels[] = $all_bundles[$bundle]['label']; + if ($access_handler->createAccess($bundle)) { + $create_bundles[] = $bundle; + if (count($create_bundles) > 1) { + // If the user has access to create more than 1 bundle then the + // individual media type form can not be used. + break; + } + } + } + + // Add a section about how to create media if the user has access to do so. + if (!empty($create_bundles)) { + if (count($create_bundles) === 1) { + $add_url = Url::fromRoute('entity.media.add_form', ['media_type' => $create_bundles[0]])->toString(); + } + elseif (count($create_bundles) > 1) { + $add_url = Url::fromRoute('entity.media.add_page')->toString(); + } + $elements['#media_help']['#media_add_help'] = t('Create your media on the
media add page (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]); + } + + $elements['#theme'] = 'media_reference_help'; + // @todo template_preprocess_field_multiple_value_form() assumes this key + // exists, but it does not exist in the case of a single widget that + // accepts multiple values. This is for some reason necessary to use + // our template for the entity_autocomplete_tags widget. + // Research and resolve this in https://www.drupal.org/node/2943020. + if (empty($elements['#cardinality_multiple'])) { + $elements['#cardinality_multiple'] = NULL; + } + + // Use the title set on the element if it exists, otherwise fall back to the + // field label. + $elements['#media_help']['#original_label'] = isset($elements['#title']) ? $elements['#title'] : $context['items']->getFieldDefinition()->getLabel(); + + // Customize the label for the field widget. + // @todo Research a better approach https://www.drupal.org/node/2943024. + $use_existing_label = t('Use existing media'); + if (!empty($elements[0]['target_id']['#title'])) { + $elements[0]['target_id']['#title'] = $use_existing_label; + } + if (!empty($elements['#title'])) { + $elements['#title'] = $use_existing_label; + } + if (!empty($elements['target_id']['#title'])) { + $elements['target_id']['#title'] = $use_existing_label; + } + + // This help text is only relevant for autocomplete widgets. When the user + // is presented with options, they don't need to type anything or know what + // types of media are allowed. + if ($is_autocomplete) { + $elements['#media_help']['#media_list_help'] = t('Type part of the media name.'); + + $overview_url = Url::fromRoute('entity.media.collection'); + if ($overview_url->access()) { + $elements['#media_help']['#media_list_link'] = t('See the media list (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]); + } + $elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]); + } +} + +/** + * Implements hook_preprocess_HOOK() for media reference widgets. + */ +function media_preprocess_media_reference_help(&$variables) { + // Most of these attribute checks are copied from + // template_preprocess_fieldset(). Our template extends + // field-multiple-value-form.html.twig to provide our help text, but also + // groups the information within a semantic fieldset with a legend. So, we + // incorporate parity for both. + $element = $variables['element']; + Element::setAttributes($element, ['id']); + RenderElement::setAttributes($element); + $variables['attributes'] = isset($element['#attributes']) ? $element['#attributes'] : []; + $variables['legend_attributes'] = new Attribute(); + $variables['header_attributes'] = new Attribute(); + $variables['description']['attributes'] = new Attribute(); + $variables['legend_span_attributes'] = new Attribute(); + + if (!empty($element['#media_help'])) { + foreach ($element['#media_help'] as $key => $text) { + $variables[substr($key, 1)] = $text; + } + } +} diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml index 9fbadef..ea0858d 100644 --- a/core/modules/media/media.routing.yml +++ b/core/modules/media/media.routing.yml @@ -1,3 +1,8 @@ +# @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0. +# This route is not used in Drupal core. As an internal API, it may also be +# removed in a minor release. If you are using it, copy the class +# and the related "entity.media.multiple_delete_confirm" route to your +# module. entity.media.multiple_delete_confirm: path: '/admin/content/media/delete' defaults: diff --git a/core/modules/media/src/Annotation/MediaSource.php b/core/modules/media/src/Annotation/MediaSource.php index fe1735d..2f7bdad 100644 --- a/core/modules/media/src/Annotation/MediaSource.php +++ b/core/modules/media/src/Annotation/MediaSource.php @@ -91,7 +91,7 @@ class MediaSource extends Plugin { /** * (optional) The metadata attribute name to provide the thumbnail title. * - * The name of the media entity will be used if the attribute name is not + * The name of the media item will be used if the attribute name is not * provided. * * @var string|null diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php index 6e58771..cca42de 100644 --- a/core/modules/media/src/Entity/Media.php +++ b/core/modules/media/src/Entity/Media.php @@ -38,6 +38,7 @@ * "add" = "Drupal\media\MediaForm", * "edit" = "Drupal\media\MediaForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", + * "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm", * }, * "translation" = "Drupal\content_translation\ContentTranslationHandler", * "views_data" = "Drupal\media\MediaViewsData", @@ -76,6 +77,7 @@ * "canonical" = "/media/{media}", * "collection" = "/admin/content/media", * "delete-form" = "/media/{media}/delete", + * "delete-multiple-form" = "/media/delete", * "edit-form" = "/media/{media}/edit", * "revision" = "/media/{media}/revisions/{media_revision}/view", * } diff --git a/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php b/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php index 6659887..2d6850b 100644 --- a/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php +++ b/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php @@ -13,6 +13,12 @@ /** * Provides a confirmation form to delete multiple media items at once. * + * @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0. + * This route is not used in Drupal core. As an internal API, it may also be + * removed in a minor release. If you are using it, copy the class + * and the related "entity.media.multiple_delete_confirm" route to your + * module. + * * @internal */ class MediaDeleteMultipleConfirmForm extends ConfirmFormBase { @@ -47,6 +53,7 @@ class MediaDeleteMultipleConfirmForm extends ConfirmFormBase { * The entity type manager. */ public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $manager) { + @trigger_error(__CLASS__ . ' is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. It is not used in Drupal core. As an internal API, it may also be removed in a minor release. If you are using it, copy the class and the related "entity.media.multiple_delete_confirm" route to your module.', E_USER_DEPRECATED); $this->tempStoreFactory = $temp_store_factory; $this->storage = $manager->getStorage('media'); } diff --git a/core/modules/media/src/MediaAccessControlHandler.php b/core/modules/media/src/MediaAccessControlHandler.php index b631da8..d420372 100644 --- a/core/modules/media/src/MediaAccessControlHandler.php +++ b/core/modules/media/src/MediaAccessControlHandler.php @@ -8,7 +8,7 @@ use Drupal\Core\Session\AccountInterface; /** - * Defines an access control handler for the media entity. + * Defines an access control handler for media items. */ class MediaAccessControlHandler extends EntityAccessControlHandler { diff --git a/core/modules/media/src/MediaSourceInterface.php b/core/modules/media/src/MediaSourceInterface.php index 3daa4f6..6d08c20 100644 --- a/core/modules/media/src/MediaSourceInterface.php +++ b/core/modules/media/src/MediaSourceInterface.php @@ -25,7 +25,7 @@ * - Image: handles local images, * - oEmbed: handles resources that are exposed through the oEmbed standard, * - YouTube: handles YouTube videos, - * - SoundCould: handles SoundCloud audio, + * - SoundCloud: handles SoundCloud audio, * - Instagram: handles Instagram posts, * - Twitter: handles tweets, * - ... diff --git a/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php index 8ef7792..dbceaf8 100644 --- a/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php +++ b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php @@ -100,7 +100,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { $link_types = [ 'content' => $this->t('Content'), - 'media' => $this->t('Media entity'), + 'media' => $this->t('Media item'), ]; $element['image_link']['#options'] = $link_types; diff --git a/core/modules/media/templates/media-reference-help.html.twig b/core/modules/media/templates/media-reference-help.html.twig new file mode 100644 index 0000000..9243c5d --- /dev/null +++ b/core/modules/media/templates/media-reference-help.html.twig @@ -0,0 +1,66 @@ +{# +/** + * @file + * Theme override for media reference fields. + * + * @see template_preprocess_field_multiple_value_form() + * @see core/themes/classy/templates/form/fieldset.html.twig + */ +#} +{% + set classes = [ + 'js-form-item', + 'form-item', + 'js-form-wrapper', + 'form-wrapper', + ] +%} + + {% + set legend_span_classes = [ + 'fieldset-legend', + required ? 'js-form-required', + required ? 'form-required', + ] + %} + {# Always wrap fieldset legends in a for CSS positioning. #} + + {{ original_label }} + + +
+ {% if media_add_help %} + + {% trans %} + Create new media + {% endtrans %} +
+
+ {{ media_add_help }} +
+ {% endif %} + + {% if multiple %} + {{ table }} + {% else %} + {% for element in elements %} + {{ element }} + {% endfor %} + {% endif %} + + + {% if multiple and description.content %} +
    +
  • {{ media_list_help }} {{ media_list_link }} {{ allowed_types_help }}
  • +
  • {{ description.content }}
  • +
+ {% else %} + {{ media_list_help }} {{ media_list_link }} {{ allowed_types_help }} + {% endif %} + {% if multiple and button %} +
{{ button }}
+ {% endif %} +
+ + + diff --git a/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml new file mode 100644 index 0000000..2c212b8 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml @@ -0,0 +1,154 @@ +langcode: en +status: true +dependencies: + module: + - media + - user +id: test_media_bulk_form +label: '' +module: views +description: '' +tag: '' +base_table: media_field_data +base_field: mid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + style: + type: table + row: + type: fields + fields: + media_bulk_form: + id: media_bulk_form + table: media + field: media_bulk_form + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + action_title: 'With selection' + include_exclude: exclude + selected_actions: { } + entity_type: media + plugin_id: bulk_form + name: + id: name + table: media_field_data + field: name + entity_type: media + entity_field: media + hide_empty: false + empty_zero: false + settings: + link_to_entity: false + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: 'Media name' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + status: + id: status + table: media_field_data + field: status + relationship: none + group_type: group + admin_label: '' + label: Status + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: boolean + settings: + format: custom + format_custom_true: Published + format_custom_false: Unpublished + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: media + entity_field: status + plugin_id: field + sorts: + mid: + id: mid + table: media_field_data + field: mid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: media + entity_field: mid + plugin_id: standard + title: 'Entity bulk form test view' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + path: test-media-bulk-form diff --git a/core/modules/media/tests/src/Functional/MediaBulkFormTest.php b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php new file mode 100644 index 0000000..44ec6b8 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php @@ -0,0 +1,113 @@ +testMediaType = $this->createMediaType(); + + // Create some test media items. + $this->mediaItems = []; + for ($i = 1; $i <= 5; $i++) { + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + ]); + $media->save(); + $this->mediaItems[] = $media; + } + } + + /** + * Tests the media bulk form. + */ + public function testBulkForm() { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + // Check that all created items are present in the test view. + $view = Views::getView('test_media_bulk_form'); + $view->execute(); + $this->assertEquals($view->total_rows, 5); + + // Check the operations are accessible to the logged in user. + $this->drupalGet('test-media-bulk-form'); + // Current available actions: Delete, Save, Publish, Unpublish. + $available_actions = [ + 'media_delete_action', + 'media_publish_action', + 'media_save_action', + 'media_unpublish_action', + ]; + foreach ($available_actions as $action_name) { + $assert_session->optionExists('action', $action_name); + } + + // Test unpublishing in bulk. + $page->checkField('media_bulk_form[0]'); + $page->checkField('media_bulk_form[1]'); + $page->checkField('media_bulk_form[2]'); + $page->selectFieldOption('action', 'media_unpublish_action'); + $page->pressButton('Apply to selected items'); + $assert_session->pageTextContains('Unpublish media was applied to 3 items'); + $this->assertFalse($this->storage->loadUnchanged(1)->isPublished(), 'The unpublish action failed in some of the media items.'); + $this->assertFalse($this->storage->loadUnchanged(2)->isPublished(), 'The unpublish action failed in some of the media items.'); + $this->assertFalse($this->storage->loadUnchanged(3)->isPublished(), 'The unpublish action failed in some of the media items.'); + + // Test publishing in bulk. + $page->checkField('media_bulk_form[0]'); + $page->checkField('media_bulk_form[1]'); + $page->selectFieldOption('action', 'media_publish_action'); + $page->pressButton('Apply to selected items'); + $assert_session->pageTextContains('Publish media was applied to 2 items'); + $this->assertTrue($this->storage->loadUnchanged(1)->isPublished(), 'The publish action failed in some of the media items.'); + $this->assertTrue($this->storage->loadUnchanged(2)->isPublished(), 'The publish action failed in some of the media items.'); + + // Test deletion in bulk. + $page->checkField('media_bulk_form[0]'); + $page->checkField('media_bulk_form[1]'); + $page->selectFieldOption('action', 'media_delete_action'); + $page->pressButton('Apply to selected items'); + $assert_session->pageTextContains('Are you sure you want to delete these media items?'); + $page->pressButton('Delete'); + $assert_session->pageTextContains('Deleted 2 items.'); + $this->assertNull($this->storage->loadUnchanged(1), 'Could not delete some of the media items.'); + $this->assertNull($this->storage->loadUnchanged(2), 'Could not delete some of the media items.'); + } + +} diff --git a/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php b/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php index 8da814b..e1b6cf7 100644 --- a/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php +++ b/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php @@ -7,7 +7,7 @@ use Drupal\system\Tests\Entity\EntityWithUriCacheTagsTestBase; /** - * Tests the media entity's cache tags. + * Tests the media items cache tags. * * @group media */ diff --git a/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php index 9ce3934..379ba74 100644 --- a/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php @@ -13,7 +13,8 @@ * @var array */ protected static $adminUserPermissions = [ - // Media entity permissions. + // Media module permissions. + 'access media overview', 'administer media', 'administer media fields', 'administer media form display', diff --git a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php index 8d876b7..c6c1c95 100644 --- a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php +++ b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php @@ -2,7 +2,12 @@ namespace Drupal\Tests\media\Functional; +use Behat\Mink\Element\NodeElement; use Drupal\media\Entity\Media; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Url; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; /** * Ensures that media UI works correctly. @@ -193,6 +198,308 @@ public function testRenderedEntityReferencedMedia() { } /** + * Data provider for testMediaReferenceWidget(). + * + * @return array[] + * Test data. See testMediaReferenceWidget() for the child array structure. + */ + public function providerTestMediaReferenceWidget() { + return [ + // Single-value fields with a single media type and the default widget: + // - The user can create and list the media. + 'single_value:single_type:create_list' => [1, [TRUE], TRUE], + // - The user can list but not create the media. + 'single_value:single_type:list' => [1, [FALSE], TRUE], + // - The user can create but not list the media. + 'single_value:single_type:create' => [1, [TRUE], FALSE], + // - The user can neither create nor list the media. + 'single_value:single_type' => [1, [FALSE], FALSE], + + // Single-value fields with the tags-style widget: + // - The user can create and list the media. + 'single_value:single_type:create_list:tags' => [1, [TRUE], TRUE, 'entity_reference_autocomplete_tags'], + // - The user can list but not create the media. + 'single_value:single_type:list:tags' => [1, [FALSE], TRUE, 'entity_reference_autocomplete_tags'], + // - The user can create but not list the media. + 'single_value:single_type:create:tags' => [1, [TRUE], FALSE, 'entity_reference_autocomplete_tags'], + // - The user can neither create nor list the media. + 'single_value:single_type:tags' => [1, [FALSE], FALSE, 'entity_reference_autocomplete_tags'], + + // Single-value fields with two media types: + // - The user can create both types. + 'single_value:two_type:create2_list' => [1, [TRUE, TRUE], TRUE], + // - The user can create only one type. + 'single_value:two_type:create1_list' => [1, [TRUE, FALSE], TRUE], + // - The user cannot create either type. + 'single_value:two_type:list' => [1, [FALSE, FALSE], TRUE], + + // Multiple-value field with a cardinality of 3, with media the user can + // create and list. + 'multi_value:single_type:create_list' => [3, [TRUE], TRUE], + // The same, with the tags field. + 'multi-value:single_type:create_list:tags' => [3, [TRUE], TRUE, 'entity_reference_autocomplete_tags'], + + // Unlimited value field. + 'unlimited_value:single_type:create_list' => [FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, [TRUE], TRUE], + // Unlimited value field with the tags widget. + 'unlimited_value:single_type:create_list' => [FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, [TRUE], TRUE, 'entity_reference_autocomplete_tags'], + ]; + } + + /** + * Tests the default autocomplete widgets for media reference fields. + * + * @param int $cardinality + * The field cardinality. + * @param bool[] $media_type_create_access + * An array of booleans indicating whether to grant the test user create + * access for each media type. A media type is created automatically for + * each; for example, an array [TRUE, FALSE] would create two media types, + * one that allows the user to create media and a second that does not. + * @param bool $list_access + * Whether to grant the test user access to list media. + * + * @see media_field_widget_entity_reference_autocomplete_form_alter() + * @see media_field_widget_multiple_entity_reference_autocomplete_form_alter() + * + * @dataProvider providerTestMediaReferenceWidget + */ + public function testMediaReferenceWidget($cardinality, array $media_type_create_access, $list_access, $widget_id = 'entity_reference_autocomplete') { + $assert_session = $this->assertSession(); + + // Create two content types. + $non_media_content_type = $this->createContentType(); + $content_type = $this->createContentType(); + + // Create some media types. + $media_types = []; + $permissions = []; + $create_media_types = []; + foreach ($media_type_create_access as $id => $access) { + if ($access) { + $create_media_types[] = "media_type_$id"; + $permissions[] = "create media_type_$id media"; + } + $this->createMediaType(['bundle' => "media_type_$id"]); + $media_types["media_type_$id"] = "media_type_$id"; + } + + // Create a user that can create content of the type, with other + // permissions as given by the data provider. + $permissions[] = "create {$content_type->id()} content"; + if ($list_access) { + $permissions[] = "access media overview"; + } + $test_user = $this->drupalCreateUser($permissions); + + // Create a non-media entity reference. + $non_media_storage = FieldStorageConfig::create([ + 'field_name' => 'field_not_a_media_field', + 'entity_type' => 'node', + 'type' => 'entity_reference', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'target_type' => 'node', + ], + ]); + $non_media_storage->save(); + $non_media_field = FieldConfig::create([ + 'label' => 'No media here!', + 'field_storage' => $non_media_storage, + 'entity_type' => 'node', + 'bundle' => $non_media_content_type->id(), + 'settings' => [ + 'handler' => 'default', + 'handler_settings' => [ + 'target_bundles' => [ + $non_media_content_type->id() => $non_media_content_type->id(), + ], + ], + ], + ]); + $non_media_field->save(); + \Drupal::entityTypeManager() + ->getStorage('entity_form_display') + ->load('node.' . $non_media_content_type->id() . '.default') + ->setComponent('field_not_a_media_field', [ + 'type' => $widget_id, + ]) + ->save(); + + // Create a media field through the user interface to ensure that the + // help text handling does not break the default value entry on the field + // settings form. + // Using drupalPostForm() to avoid dealing with JavaScript on the previous + // page in the field creation. + $edit = [ + 'new_storage_type' => 'field_ui:entity_reference:media', + 'label' => "Media (cardinality $cardinality)", + 'field_name' => 'media_reference', + ]; + $this->drupalPostForm("admin/structure/types/manage/{$content_type->id()}/fields/add-field", $edit, 'Save and continue'); + $edit = []; + foreach ($media_types as $type) { + $edit["settings[handler_settings][target_bundles][$type]"] = TRUE; + } + $this->drupalPostForm("admin/structure/types/manage/{$content_type->id()}/fields/node.{$content_type->id()}.field_media_reference", $edit, "Save settings"); + \Drupal::entityTypeManager() + ->getStorage('entity_form_display') + ->load('node.' . $content_type->id() . '.default') + ->setComponent('field_media_reference', [ + 'type' => $widget_id, + ]) + ->save(); + + // Some of the expected texts. + $create_help = 'Create your media on the media add page (opens a new window), then add it by name to the field below.'; + $list_text = 'See the media list (opens a new window) to help locate media.'; + $use_help = 'Type part of the media name.'; + $create_header = "Create new media"; + $use_header = "Use existing media"; + + // First check that none of the help texts are on the non-media content. + $this->drupalGet("/node/add/{$non_media_content_type->id()}"); + $this->assertNoHelpTexts([ + $create_header, + $create_help, + $use_header, + $use_help, + $list_text, + 'Allowed media types:', + ]); + + // Now, check that the widget displays the expected help text under the + // given conditions for the test user. + $this->drupalLogin($test_user); + $this->drupalGet("/node/add/{$content_type->id()}"); + + // Specific expected help texts for the media field. + $create_header = "Create new media"; + $use_header = "Use existing media"; + $type_list = 'Allowed media types: ' . implode(", ", array_keys($media_types)); + + $fieldset_selector = '#edit-field-media-reference-wrapper fieldset'; + $fieldset = $assert_session->elementExists('css', $fieldset_selector); + + $this->assertSame("Media (cardinality $cardinality)", $assert_session->elementExists('css', 'legend', $fieldset)->getText()); + + // Assert text that should be displayed regardless of other access. + $this->assertHelpTexts([$use_header, $use_help, $type_list], $fieldset_selector); + + // The entire section for creating new media should only be displayed if + // the user can create at least one media of the type. + if ($create_media_types) { + if (count($create_media_types) === 1) { + $url = Url::fromRoute('entity.media.add_form')->setRouteParameter('media_type', $create_media_types[0]); + } + else { + $url = Url::fromRoute('entity.media.add_page'); + } + $this->assertHelpTexts([$create_header, $create_help], $fieldset_selector); + $this->assertHelpLink( + $fieldset, + 'media add page', + [ + 'target' => '_blank', + 'href' => $url->toString(), + ] + ); + } + else { + $this->assertNoHelpTexts([$create_header, $create_help]); + $this->assertNoHelpLink($fieldset, 'media add page'); + } + + if ($list_access) { + $this->assertHelpTexts([$list_text], $fieldset_selector); + $this->assertHelpLink( + $fieldset, + 'media list', + [ + 'target' => '_blank', + 'href' => Url::fromRoute('entity.media.collection')->toString(), + ] + ); + } + else { + $this->assertNoHelpTexts([$list_text]); + $this->assertNoHelpLink($fieldset, 'media list'); + } + } + + /** + * Asserts that the given texts are present exactly once. + * + * @param string[] $texts + * A list of the help texts to check. + * @param string $selector + * (optional) The selector to search. + */ + public function assertHelpTexts(array $texts, $selector = '') { + $assert_session = $this->assertSession(); + foreach ($texts as $text) { + // We only want to escape single quotes, so use str_replace() rather than + // addslashes(). + $text = str_replace("'", "\'", $text); + if ($selector) { + $assert_session->elementsCount('css', $selector . ":contains('$text')", 1); + } + else { + $assert_session->pageTextContains($text); + } + } + } + + /** + * Asserts that none of the given texts are present. + * + * @param string[] $texts + * A list of the help texts to check. + */ + public function assertNoHelpTexts(array $texts) { + $assert_session = $this->assertSession(); + foreach ($texts as $text) { + $assert_session->pageTextNotContains($text); + } + } + + /** + * Asserts whether a given link is present. + * + * @param \Behat\Mink\Element\NodeElement $element + * The element to search. + * @param string $text + * The link text. + * @param string[] $attributes + * An associative array of any expected attributes, keyed by the + * attribute name. + */ + protected function assertHelpLink(NodeElement $element, $text, array $attributes = []) { + // Find all the links inside the element. + $link = $element->findLink($text); + + $this->assertNotEmpty($link); + foreach ($attributes as $attribute => $value) { + $this->assertEquals($link->getAttribute($attribute), $value); + } + } + + /** + * Asserts that a given link is not present. + * + * @param \Behat\Mink\Element\NodeElement $element + * The element to search. + * @param string $text + * The link text. + */ + protected function assertNoHelpLink(NodeElement $element, $text) { + $assert_session = $this->assertSession(); + // Assert that the link and its text are not present anywhere on the page. + $assert_session->elementNotExists('named', ['link', $text], $element); + $assert_session->pageTextNotContains($text); + } + + /** * Test the media collection route. */ public function testMediaCollectionRoute() { diff --git a/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php b/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php index fa39962..c00dc3d 100644 --- a/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php +++ b/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php @@ -109,7 +109,7 @@ protected function createMediaType($media_source_name) { } /** - * Helper to generate media entity. + * Helper to generate a media item. * * @param string $filename * String filename with extension. @@ -117,7 +117,7 @@ protected function createMediaType($media_source_name) { * The the media type. * * @return \Drupal\media\Entity\Media - * A media entity. + * A media item. */ protected function generateMedia($filename, MediaTypeInterface $media_type) { vfsStream::setup('drupal_root'); diff --git a/core/modules/media/tests/src/Kernel/MediaLinkRelationsTest.php b/core/modules/media/tests/src/Kernel/MediaLinkRelationsTest.php index 164df60..c3b508c 100644 --- a/core/modules/media/tests/src/Kernel/MediaLinkRelationsTest.php +++ b/core/modules/media/tests/src/Kernel/MediaLinkRelationsTest.php @@ -5,7 +5,7 @@ use Drupal\media\Entity\Media; /** - * Tests link relationships for Media entity. + * Tests link relationships for media items. * * @group media */ @@ -20,7 +20,7 @@ public function testExistLinkRelationships() { $media = Media::create(['bundle' => $this->testMediaType->id()]); $media->save(); foreach ($media->uriRelationships() as $relation_name) { - $this->assertTrue($link_relation_type_manager->hasDefinition($relation_name), "Link relationship '{$relation_name}' for Media entity"); + $this->assertTrue($link_relation_type_manager->hasDefinition($relation_name), "Link relationship '{$relation_name}' for a media item"); } } diff --git a/core/modules/media/tests/src/Kernel/MediaSourceTest.php b/core/modules/media/tests/src/Kernel/MediaSourceTest.php index ffde236..37434e5 100644 --- a/core/modules/media/tests/src/Kernel/MediaSourceTest.php +++ b/core/modules/media/tests/src/Kernel/MediaSourceTest.php @@ -161,7 +161,7 @@ public function testThumbnail() { file_put_contents('public://thumbnail1.jpg', ''); file_put_contents('public://thumbnail2.jpg', ''); - // Save a media entity and make sure thumbnail was added. + // Save a media item and make sure thumbnail was added. \Drupal::state()->set('media_source_test_attributes', [ 'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'], ]); @@ -174,7 +174,7 @@ public function testThumbnail() { $media_source = $media->getSource(); $this->assertEquals('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); $media->save(); - $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not added to the media entity.'); + $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not added to the media item.'); $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); @@ -193,7 +193,7 @@ public function testThumbnail() { $media->thumbnail->target_id = NULL; $this->assertEquals('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); $media->save(); - $this->assertEquals('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media entity.'); + $this->assertEquals('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.'); $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); @@ -205,7 +205,7 @@ public function testThumbnail() { $media->field_media_test->value = 'some_new_value'; $this->assertEquals('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); $media->save(); - $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media entity.'); + $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media item.'); $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); @@ -282,13 +282,13 @@ public function testThumbnail() { 'field_media_test' => 'some_value', ]); $media->save(); - $this->assertEquals('Boxer', $media->getName(), 'Correct name was not set on the media entity.'); + $this->assertEquals('Boxer', $media->getName(), 'Correct name was not set on the media item.'); $this->assertEquals('This will be title.', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); $this->assertEquals('This will be alt.', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); } /** - * Tests the media entity constraints functionality. + * Tests the media item constraints functionality. */ public function testConstraints() { // Test entity constraints. diff --git a/core/modules/media/tests/src/Kernel/MediaTest.php b/core/modules/media/tests/src/Kernel/MediaTest.php index df48a9b..ea76a2e 100644 --- a/core/modules/media/tests/src/Kernel/MediaTest.php +++ b/core/modules/media/tests/src/Kernel/MediaTest.php @@ -12,7 +12,7 @@ class MediaTest extends MediaKernelTestBase { /** - * Tests various aspects of a Media entity. + * Tests various aspects of a media item. */ public function testEntity() { $media = Media::create(['bundle' => $this->testMediaType->id()]); diff --git a/core/modules/menu_link_content/menu_link_content.module b/core/modules/menu_link_content/menu_link_content.module index f94a139..a5541cd 100644 --- a/core/modules/menu_link_content/menu_link_content.module +++ b/core/modules/menu_link_content/menu_link_content.module @@ -89,10 +89,17 @@ function menu_link_content_path_delete($path) { function menu_link_content_entity_predelete(EntityInterface $entity) { /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $entity_type_id = $entity->getEntityTypeId(); foreach ($entity->uriRelationships() as $rel) { $url = $entity->toUrl($rel); + $route_parameters = $url->getRouteParameters(); + if (!isset($route_parameters[$entity_type_id])) { + // Do not delete links which do not relate to this exact entity. For + // example, "collection", "add-form", etc. + continue; + } // Delete all MenuLinkContent links that point to this entity route. - $result = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters()); + $result = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $route_parameters); if ($result) { foreach ($result as $id => $instance) { diff --git a/core/modules/menu_link_content/tests/src/Functional/LinksTest.php b/core/modules/menu_link_content/tests/src/Functional/LinksTest.php index 298af6f..7cce477 100644 --- a/core/modules/menu_link_content/tests/src/Functional/LinksTest.php +++ b/core/modules/menu_link_content/tests/src/Functional/LinksTest.php @@ -147,8 +147,12 @@ public function testCreateLink() { * Tests that menu link pointing to entities get removed on entity remove. */ public function testMenuLinkOnEntityDelete() { + + // Create user. $user = User::create(['name' => 'username']); $user->save(); + + // Create "canonical" menu link pointing to the user. $menu_link_content = MenuLinkContent::create([ 'title' => 'username profile', 'menu_name' => 'menu_test', @@ -156,11 +160,30 @@ public function testMenuLinkOnEntityDelete() { 'bundle' => 'menu_test', ]); $menu_link_content->save(); + + // Create "collection" menu link pointing to the user listing page. + $menu_link_content_collection = MenuLinkContent::create([ + 'title' => 'users listing', + 'menu_name' => 'menu_test', + 'link' => [['uri' => 'internal:/' . $user->toUrl('collection')->getInternalPath()]], + 'bundle' => 'menu_test', + ]); + $menu_link_content_collection->save(); + + // Check is menu links present in the menu. $menu_tree_condition = (new MenuTreeParameters())->addCondition('route_name', 'entity.user.canonical'); $this->assertCount(1, \Drupal::menuTree()->load('menu_test', $menu_tree_condition)); + $menu_tree_condition_collection = (new MenuTreeParameters())->addCondition('route_name', 'entity.user.collection'); + $this->assertCount(1, \Drupal::menuTree()->load('menu_test', $menu_tree_condition_collection)); + // Delete the user. $user->delete(); + + // The "canonical" menu item has to be deleted. $this->assertCount(0, \Drupal::menuTree()->load('menu_test', $menu_tree_condition)); + + // The "collection" menu item should still present in the menu. + $this->assertCount(1, \Drupal::menuTree()->load('menu_test', $menu_tree_condition_collection)); } /** diff --git a/core/modules/migrate/src/Exception/RequirementsException.php b/core/modules/migrate/src/Exception/RequirementsException.php index 93da7ee..5ca5fd9 100644 --- a/core/modules/migrate/src/Exception/RequirementsException.php +++ b/core/modules/migrate/src/Exception/RequirementsException.php @@ -5,7 +5,7 @@ use Exception; /** - * Defines an + * Defines an exception thrown when a migration does not meet the requirements. * * @see \Drupal\migrate\Plugin\RequirementsInterface */ diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php index dffde5d..8d15cf4 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php @@ -275,7 +275,7 @@ protected function processStubRow(Row $row) { if ($field_definition->isRequired() && is_null($row->getDestinationProperty($field_name))) { // Use the configured default value for this specific field, if any. if ($default_value = $field_definition->getDefaultValueLiteral()) { - $values[] = $default_value; + $values = $default_value; } else { // Otherwise, ask the field type to generate a sample value. diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php index 8bc17f1..a69e31c 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -3,6 +3,7 @@ namespace Drupal\migrate\Plugin\migrate\id_map; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Database\DatabaseException; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; @@ -161,6 +162,18 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition $this->migration = $migration; $this->eventDispatcher = $event_dispatcher; $this->message = new MigrateMessage(); + + if (!isset($this->database)) { + $this->database = \Drupal::database(); + } + + // Default generated table names, limited to 63 characters. + $machine_name = str_replace(':', '__', $this->migration->id()); + $prefix_length = strlen($this->database->tablePrefix()); + $this->mapTableName = 'migrate_map_' . Unicode::strtolower($machine_name); + $this->mapTableName = Unicode::substr($this->mapTableName, 0, 63 - $prefix_length); + $this->messageTableName = 'migrate_message_' . Unicode::strtolower($machine_name); + $this->messageTableName = Unicode::substr($this->messageTableName, 0, 63 - $prefix_length); } /** @@ -246,7 +259,6 @@ protected function destinationIdFields() { * The map table name. */ public function mapTableName() { - $this->init(); return $this->mapTableName; } @@ -257,7 +269,6 @@ public function mapTableName() { * The message table name. */ public function messageTableName() { - $this->init(); return $this->messageTableName; } @@ -278,9 +289,6 @@ public function getQualifiedMapTableName() { * The database connection object. */ public function getDatabase() { - if (!isset($this->database)) { - $this->database = \Drupal::database(); - } $this->init(); return $this->database; } @@ -291,13 +299,6 @@ public function getDatabase() { protected function init() { if (!$this->initialized) { $this->initialized = TRUE; - // Default generated table names, limited to 63 characters. - $machine_name = str_replace(':', '__', $this->migration->id()); - $prefix_length = strlen($this->getDatabase()->tablePrefix()); - $this->mapTableName = 'migrate_map_' . Unicode::strtolower($machine_name); - $this->mapTableName = Unicode::substr($this->mapTableName, 0, 63 - $prefix_length); - $this->messageTableName = 'migrate_message_' . Unicode::strtolower($machine_name); - $this->messageTableName = Unicode::substr($this->messageTableName, 0, 63 - $prefix_length); $this->ensureTables(); } } @@ -696,21 +697,17 @@ public function prepareUpdate() { * {@inheritdoc} */ public function processedCount() { - return $this->getDatabase()->select($this->mapTableName()) - ->countQuery() - ->execute() - ->fetchField(); + return $this->countHelper(NULL, $this->mapTableName()); } /** * {@inheritdoc} */ public function importedCount() { - return $this->getDatabase()->select($this->mapTableName()) - ->condition('source_row_status', [MigrateIdMapInterface::STATUS_IMPORTED, MigrateIdMapInterface::STATUS_NEEDS_UPDATE], 'IN') - ->countQuery() - ->execute() - ->fetchField(); + return $this->countHelper([ + MigrateIdMapInterface::STATUS_IMPORTED, + MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + ]); } /** @@ -737,20 +734,28 @@ public function messageCount() { /** * Counts records in a table. * - * @param int $status - * An integer for the source_row_status column. + * @param int|array $status + * (optional) Status code(s) to filter the source_row_status column. * @param string $table * (optional) The table to work. Defaults to NULL. * * @return int * The number of records. */ - protected function countHelper($status, $table = NULL) { - $query = $this->getDatabase()->select($table ?: $this->mapTableName()); + protected function countHelper($status = NULL, $table = NULL) { + // Use database directly to avoid creating tables. + $query = $this->database->select($table ?: $this->mapTableName()); if (isset($status)) { - $query->condition('source_row_status', $status); + $query->condition('source_row_status', $status, is_array($status) ? 'IN' : '='); + } + try { + $count = (int) $query->countQuery()->execute()->fetchField(); + } + catch (DatabaseException $e) { + // The table does not exist, therefore there are no records. + $count = 0; } - return $query->countQuery()->execute()->fetchField(); + return $count; } /** @@ -976,8 +981,9 @@ function (array $id) { // Get the highest id from the list of map tables. $ids = [0]; foreach ($map_tables as $map_table) { + // If the map_table does not exist then continue on to the next map_table. if (!$this->getDatabase()->schema()->tableExists($map_table)) { - break; + continue; } $query = $this->getDatabase()->select($map_table, 'map') diff --git a/core/modules/migrate/src/Plugin/migrate/process/Get.php b/core/modules/migrate/src/Plugin/migrate/process/Get.php index 2322edb..26799cb 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Get.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Get.php @@ -33,7 +33,7 @@ * bar: foo * @endcode * - * get also supports a list of source properties. + * Get also supports a list of source properties. * * Example: * @@ -53,7 +53,7 @@ * value will be used. This makes it impossible to reach a source property with * an empty string as its name. * - * get also supports copying destination values. These are indicated by a + * Get also supports copying destination values. These are indicated by a * starting @ sign. Values using @ must be wrapped in quotes. * * @code @@ -69,20 +69,18 @@ * This will simply copy the destination value of foo to the destination * property bar. foo configuration is included for illustration purposes. * - * Because of this, if your source or destination property actually starts with - * a @ you need to double those starting characters up. This means that if a - * destination property happens to start with a @ and you want to refer it, - * you'll need to start with three @ characters -- one to indicate the - * destination and two for escaping the real @. + * Because of this, if the source or destination property actually starts with a + * @, that character must be escaped with @@. + * The referenced property becomes, for example, @@@foo. * * @code * process: - * @foo: - * plugin: machine_name - * source: baz - * bar: - * plugin: get - * source: '@@@foo' + * '@foo': + * plugin: machine_name + * source: baz + * bar: + * plugin: get + * source: '@@@foo' * @endcode * * This should occur extremely rarely. diff --git a/core/modules/migrate/src/Plugin/migrate/process/Substr.php b/core/modules/migrate/src/Plugin/migrate/process/Substr.php index f36da0a..4d49a38 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Substr.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Substr.php @@ -33,8 +33,8 @@ * new_text_field: * plugin: substr * source: some_text_field - * start: 6 - * length: 10 + * start: 6 + * length: 10 * @endcode * If some_text_field was 'Marie Skłodowska Curie' then * $destination['new_text_field'] would be 'Skłodowska'. diff --git a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php index 3ae7fac..3dd7144 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php +++ b/core/modules/migrate/src/Plugin/migrate/source/SourcePluginBase.php @@ -217,7 +217,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition * Initializes the iterator with the source data. * * @return \Iterator - * An array of the data for this source. + * Returns an iteratable object of data for this source. */ abstract protected function initializeIterator(); diff --git a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php index 18ef4d9..91bbcb4 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php @@ -381,7 +381,7 @@ protected function fetchNextBatch() { * {@inheritdoc} */ public function count($refresh = FALSE) { - return $this->query()->countQuery()->execute()->fetchField(); + return (int) $this->query()->countQuery()->execute()->fetchField(); } /** diff --git a/core/modules/migrate/src/Row.php b/core/modules/migrate/src/Row.php index 83e97b1..1b28b2d 100644 --- a/core/modules/migrate/src/Row.php +++ b/core/modules/migrate/src/Row.php @@ -104,7 +104,7 @@ public function __construct(array $values = [], array $source_ids = [], $is_stub $this->isStub = $is_stub; foreach (array_keys($source_ids) as $id) { if (!$this->hasSourceProperty($id)) { - throw new \InvalidArgumentException("$id has no value"); + throw new \InvalidArgumentException("$id is defined as a source ID but has no value."); } } } diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/external_translated_test_node.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/external_translated_test_node.yml new file mode 100644 index 0000000..f643b60 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/external_translated_test_node.yml @@ -0,0 +1,19 @@ +id: external_translated_test_node +label: External translated content +source: + plugin: migrate_external_translated_test + default_lang: true + constants: + type: external_test +process: + type: constants/type + title: title + langcode: + plugin: static_map + source: lang + map: + English: en + French: fr + Spanish: es +destination: + plugin: entity:node diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/external_translated_test_node_translation.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/external_translated_test_node_translation.yml new file mode 100644 index 0000000..0363aa2 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/external_translated_test_node_translation.yml @@ -0,0 +1,27 @@ +id: external_translated_test_node_translation +label: External translated content translations +source: + plugin: migrate_external_translated_test + default_lang: false + constants: + type: external_test +process: + nid: + plugin: migration_lookup + source: name + migration: external_translated_test_node + type: constants/type + title: title + langcode: + plugin: static_map + source: lang + map: + English: en + French: fr + Spanish: es +destination: + plugin: entity:node + translations: true +migration_dependencies: + required: + - external_translated_test_node diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml deleted file mode 100644 index f643b60..0000000 --- a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml +++ /dev/null @@ -1,19 +0,0 @@ -id: external_translated_test_node -label: External translated content -source: - plugin: migrate_external_translated_test - default_lang: true - constants: - type: external_test -process: - type: constants/type - title: title - langcode: - plugin: static_map - source: lang - map: - English: en - French: fr - Spanish: es -destination: - plugin: entity:node diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml deleted file mode 100644 index 0363aa2..0000000 --- a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml +++ /dev/null @@ -1,27 +0,0 @@ -id: external_translated_test_node_translation -label: External translated content translations -source: - plugin: migrate_external_translated_test - default_lang: false - constants: - type: external_test -process: - nid: - plugin: migration_lookup - source: name - migration: external_translated_test_node - type: constants/type - title: title - langcode: - plugin: static_map - source: lang - map: - English: en - French: fr - Spanish: es -destination: - plugin: entity:node - translations: true -migration_dependencies: - required: - - external_translated_test_node diff --git a/core/modules/migrate/tests/modules/migrate_high_water_test/migrations/high_water_test.yml b/core/modules/migrate/tests/modules/migrate_high_water_test/migrations/high_water_test.yml new file mode 100644 index 0000000..6a86577 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_high_water_test/migrations/high_water_test.yml @@ -0,0 +1,16 @@ +id: high_water_test +label: High water test. +source: + plugin: high_water_test + high_water_property: + name: changed +destination: + plugin: entity:node +migration_tags: + test: test +process: + changed: changed + title: title + type: + plugin: default_value + default_value: high_water_import_node diff --git a/core/modules/migrate/tests/modules/migrate_high_water_test/migrations/migrate.migration.high_water_test.yml b/core/modules/migrate/tests/modules/migrate_high_water_test/migrations/migrate.migration.high_water_test.yml deleted file mode 100644 index 6a86577..0000000 --- a/core/modules/migrate/tests/modules/migrate_high_water_test/migrations/migrate.migration.high_water_test.yml +++ /dev/null @@ -1,16 +0,0 @@ -id: high_water_test -label: High water test. -source: - plugin: high_water_test - high_water_property: - name: changed -destination: - plugin: entity:node -migration_tags: - test: test -process: - changed: changed - title: title - type: - plugin: default_value - default_value: high_water_import_node diff --git a/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php b/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php index e2f02ae..13803bd 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\migrate\Kernel; +use Drupal\entity_test\Entity\EntityTestMul; use Drupal\KernelTests\KernelTestBase; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\migrate\MigrateExecutable; @@ -44,6 +45,11 @@ class MigrateEntityContentBaseTest extends KernelTestBase { */ protected function setUp() { parent::setUp(); + + // Enable two required fields with default values: a single-value field and + // a multi-value field. + \Drupal::state()->set('entity_test.required_default_field', TRUE); + \Drupal::state()->set('entity_test.required_multi_default_field', TRUE); $this->installEntitySchema('entity_test_mul'); ConfigurableLanguage::createFromLangcode('en')->save(); @@ -265,4 +271,34 @@ public function testEmptyDestinations() { $this->assertNull($entity->version->value); } + /** + * Tests stub rows. + */ + public function testStubRows() { + // Create a destination. + $this->createDestination([]); + + // Import a stub row. + $row = new Row([], [], TRUE); + $row->setDestinationProperty('type', 'test'); + $ids = $this->destination->import($row); + $this->assertCount(1, $ids); + + // Make sure the entity was saved. + $entity = EntityTestMul::load(reset($ids)); + $this->assertInstanceOf(EntityTestMul::class, $entity); + // Make sure the default value was applied to the required fields. + $single_field_name = 'required_default_field'; + $single_default_value = $entity->getFieldDefinition($single_field_name)->getDefaultValueLiteral(); + $this->assertSame($single_default_value, $entity->get($single_field_name)->getValue()); + + $multi_field_name = 'required_multi_default_field'; + $multi_default_value = $entity->getFieldDefinition($multi_field_name)->getDefaultValueLiteral(); + $count = 3; + $this->assertCount($count, $multi_default_value); + for ($i = 0; $i < $count; ++$i) { + $this->assertSame($multi_default_value[$i], $entity->get($multi_field_name)->get($i)->getValue()); + } + } + } diff --git a/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php b/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php index 321ca95..8837d5f 100644 --- a/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php +++ b/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php @@ -47,8 +47,8 @@ public function testConnectionTypes() { // Verify that falling back to the default 'migrate' connection (defined in // the base class) works. - $this->assertSame($sql_base->getDatabase()->getTarget(), 'default'); - $this->assertSame($sql_base->getDatabase()->getKey(), 'migrate'); + $this->assertSame('default', $sql_base->getDatabase()->getTarget()); + $this->assertSame('migrate', $sql_base->getDatabase()->getKey()); // Verify the fallback state key overrides the 'migrate' connection. $target = 'test_fallback_target'; diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php index 2ad2b3d..bc84991 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php @@ -211,10 +211,10 @@ public function testClearMessages() { } // Truncate and check that 4 messages were deleted. - $this->assertEquals($id_map->messageCount(), 4); + $this->assertSame($id_map->messageCount(), 4); $id_map->clearMessages(); $count = $id_map->messageCount(); - $this->assertEquals($count, 0); + $this->assertSame($count, 0); } /** @@ -284,7 +284,7 @@ public function testMessageCount() { // Test count message multiple times starting from 0. foreach ($expected_results as $key => $expected_result) { $count = $id_map->messageCount(); - $this->assertEquals($expected_result, $count); + $this->assertSame($expected_result, $count); $id_map->saveMessage(['source_id_property' => $key], $message); } } @@ -684,21 +684,21 @@ public function testImportedCount() { $row = new Row($source, ['source_id_property' => []]); $destination = ['destination_id_property' => 'destination_value_failed']; $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_FAILED); - $this->assertSame(0, (int) $id_map->importedCount()); + $this->assertSame(0, $id_map->importedCount()); // Add an imported row and assert single count. $source = ['source_id_property' => 'source_value_imported']; $row = new Row($source, ['source_id_property' => []]); $destination = ['destination_id_property' => 'destination_value_imported']; $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_IMPORTED); - $this->assertSame(1, (int) $id_map->importedCount()); + $this->assertSame(1, $id_map->importedCount()); // Add a row needing update and assert multiple imported rows. $source = ['source_id_property' => 'source_value_update']; $row = new Row($source, ['source_id_property' => []]); $destination = ['destination_id_property' => 'destination_value_update']; $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE); - $this->assertSame(2, (int) $id_map->importedCount()); + $this->assertSame(2, $id_map->importedCount()); } /** @@ -712,7 +712,7 @@ public function testImportedCount() { public function testProcessedCount() { $id_map = $this->getIdMap(); // Assert zero rows have been processed before adding rows. - $this->assertSame(0, (int) $id_map->processedCount()); + $this->assertSame(0, $id_map->processedCount()); $row_statuses = [ MigrateIdMapInterface::STATUS_IMPORTED, MigrateIdMapInterface::STATUS_NEEDS_UPDATE, @@ -727,11 +727,11 @@ public function testProcessedCount() { $id_map->saveIdMapping($row, $destination, $status); if ($status == MigrateIdMapInterface::STATUS_IMPORTED) { // Assert a single row has been processed. - $this->assertSame(1, (int) $id_map->processedCount()); + $this->assertSame(1, $id_map->processedCount()); } } // Assert multiple rows have been processed. - $this->assertSame(count($row_statuses), (int) $id_map->processedCount()); + $this->assertSame(count($row_statuses), $id_map->processedCount()); } /** @@ -779,7 +779,7 @@ public function testUpdateCount($num_update_rows) { $this->saveMap($row); } $id_map = $this->getIdMap(); - $this->assertSame($num_update_rows, (int) $id_map->updateCount()); + $this->assertSame($num_update_rows, $id_map->updateCount()); } /** @@ -827,7 +827,7 @@ public function testErrorCount($num_error_rows) { $this->saveMap($row); } - $this->assertSame($num_error_rows, (int) $this->getIdMap()->errorCount()); + $this->assertSame($num_error_rows, $this->getIdMap()->errorCount()); } /** @@ -1010,4 +1010,27 @@ private function getIdMapContents() { return $contents; } + /** + * Tests the delayed creation of the "map" and "message" migrate tables. + */ + public function testMapTableCreation() { + $id_map = $this->getIdMap(); + $map_table_name = $id_map->mapTableName(); + $message_table_name = $id_map->messageTableName(); + + // Check that tables names do exist. + $this->assertEquals('migrate_map_sql_idmap_test', $map_table_name); + $this->assertEquals('migrate_message_sql_idmap_test', $message_table_name); + + // Check that tables don't exist. + $this->assertFalse($this->database->schema()->tableExists($map_table_name)); + $this->assertFalse($this->database->schema()->tableExists($message_table_name)); + + $id_map->getDatabase(); + + // Check that tables do exist. + $this->assertTrue($this->database->schema()->tableExists($map_table_name)); + $this->assertTrue($this->database->schema()->tableExists($message_table_name)); + } + } diff --git a/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityDisplayTest.php b/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityDisplayTest.php index 6deaec8..f450984 100644 --- a/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityDisplayTest.php +++ b/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityDisplayTest.php @@ -44,8 +44,8 @@ public function testImport() { ->method('save') ->with(); $plugin = new TestPerComponentEntityDisplay($entity); - $this->assertSame($plugin->import($row), ['entity_type_test', 'bundle_test', 'view_mode_test', 'field_name_test']); - $this->assertSame($plugin->getTestValues(), ['entity_type_test', 'bundle_test', 'view_mode_test']); + $this->assertSame(['entity_type_test', 'bundle_test', 'view_mode_test', 'field_name_test'], $plugin->import($row)); + $this->assertSame(['entity_type_test', 'bundle_test', 'view_mode_test'], $plugin->getTestValues()); } } diff --git a/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityFormDisplayTest.php b/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityFormDisplayTest.php index 0424ceb..157d45e 100644 --- a/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityFormDisplayTest.php +++ b/core/modules/migrate/tests/src/Unit/destination/PerComponentEntityFormDisplayTest.php @@ -44,8 +44,8 @@ public function testImport() { ->method('save') ->with(); $plugin = new TestPerComponentEntityFormDisplay($entity); - $this->assertSame($plugin->import($row), ['entity_type_test', 'bundle_test', 'form_mode_test', 'field_name_test']); - $this->assertSame($plugin->getTestValues(), ['entity_type_test', 'bundle_test', 'form_mode_test']); + $this->assertSame(['entity_type_test', 'bundle_test', 'form_mode_test', 'field_name_test'], $plugin->import($row)); + $this->assertSame(['entity_type_test', 'bundle_test', 'form_mode_test'], $plugin->getTestValues()); } } diff --git a/core/modules/migrate/tests/src/Unit/process/CallbackTest.php b/core/modules/migrate/tests/src/Unit/process/CallbackTest.php index e3b8f27..91eb2e7 100644 --- a/core/modules/migrate/tests/src/Unit/process/CallbackTest.php +++ b/core/modules/migrate/tests/src/Unit/process/CallbackTest.php @@ -30,7 +30,7 @@ protected function setUp() { public function testCallbackWithFunction() { $this->plugin->setCallable('strtolower'); $value = $this->plugin->transform('FooBar', $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'foobar'); + $this->assertSame('foobar', $value); } /** @@ -39,7 +39,7 @@ public function testCallbackWithFunction() { public function testCallbackWithClassMethod() { $this->plugin->setCallable(['\Drupal\Component\Utility\Unicode', 'strtolower']); $value = $this->plugin->transform('FooBar', $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'foobar'); + $this->assertSame('foobar', $value); } } diff --git a/core/modules/migrate/tests/src/Unit/process/ConcatTest.php b/core/modules/migrate/tests/src/Unit/process/ConcatTest.php index ea93e46..e7873fe 100644 --- a/core/modules/migrate/tests/src/Unit/process/ConcatTest.php +++ b/core/modules/migrate/tests/src/Unit/process/ConcatTest.php @@ -30,7 +30,7 @@ protected function setUp() { */ public function testConcatWithoutDelimiter() { $value = $this->plugin->transform(['foo', 'bar'], $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'foobar'); + $this->assertSame('foobar', $value); } /** @@ -47,7 +47,7 @@ public function testConcatWithNonArray() { public function testConcatWithDelimiter() { $this->plugin->setDelimiter('_'); $value = $this->plugin->transform(['foo', 'bar'], $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'foo_bar'); + $this->assertSame('foo_bar', $value); } } diff --git a/core/modules/migrate/tests/src/Unit/process/ExplodeTest.php b/core/modules/migrate/tests/src/Unit/process/ExplodeTest.php index 20bf3a0..466d002 100644 --- a/core/modules/migrate/tests/src/Unit/process/ExplodeTest.php +++ b/core/modules/migrate/tests/src/Unit/process/ExplodeTest.php @@ -29,7 +29,7 @@ protected function setUp() { */ public function testTransform() { $value = $this->plugin->transform('foo,bar,tik', $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, ['foo', 'bar', 'tik']); + $this->assertSame(['foo', 'bar', 'tik'], $value); } /** @@ -38,7 +38,7 @@ public function testTransform() { public function testTransformLimit() { $plugin = new Explode(['delimiter' => '_', 'limit' => 2], 'map', []); $value = $plugin->transform('foo_bar_tik', $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, ['foo', 'bar_tik']); + $this->assertSame(['foo', 'bar_tik'], $value); } /** @@ -49,7 +49,7 @@ public function testChainedTransform() { $concat = new Concat([], 'map', []); $concatenated = $concat->transform($exploded, $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($concatenated, 'foobartik'); + $this->assertSame('foobartik', $concatenated); } /** diff --git a/core/modules/migrate/tests/src/Unit/process/ExtractTest.php b/core/modules/migrate/tests/src/Unit/process/ExtractTest.php index 14f3f9a..ad0a45a 100644 --- a/core/modules/migrate/tests/src/Unit/process/ExtractTest.php +++ b/core/modules/migrate/tests/src/Unit/process/ExtractTest.php @@ -25,7 +25,7 @@ protected function setUp() { */ public function testExtract() { $value = $this->plugin->transform(['foo' => 'bar'], $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'bar'); + $this->assertSame('bar', $value); } /** @@ -50,7 +50,7 @@ public function testExtractFail() { public function testExtractFailDefault() { $plugin = new Extract(['index' => ['foo'], 'default' => 'test'], 'map', []); $value = $plugin->transform(['bar' => 'foo'], $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'test', ''); + $this->assertSame('test', $value, ''); } } diff --git a/core/modules/migrate/tests/src/Unit/process/FlattenTest.php b/core/modules/migrate/tests/src/Unit/process/FlattenTest.php index 73b20c2..496cf92 100644 --- a/core/modules/migrate/tests/src/Unit/process/FlattenTest.php +++ b/core/modules/migrate/tests/src/Unit/process/FlattenTest.php @@ -17,7 +17,7 @@ class FlattenTest extends MigrateProcessTestCase { public function testFlatten() { $plugin = new Flatten([], 'flatten', []); $flattened = $plugin->transform([1, 2, [3, 4, [5]], [], [7, 8]], $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($flattened, [1, 2, 3, 4, 5, 7, 8]); + $this->assertSame([1, 2, 3, 4, 5, 7, 8], $flattened); } } diff --git a/core/modules/migrate/tests/src/Unit/process/GetTest.php b/core/modules/migrate/tests/src/Unit/process/GetTest.php index 59ce8b6..5bc6247 100644 --- a/core/modules/migrate/tests/src/Unit/process/GetTest.php +++ b/core/modules/migrate/tests/src/Unit/process/GetTest.php @@ -34,7 +34,7 @@ public function testTransformSourceString() { ->will($this->returnValue('source_value')); $this->plugin->setSource('test'); $value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'source_value'); + $this->assertSame('source_value', $value); } /** @@ -52,7 +52,7 @@ public function testTransformSourceArray() { return $map[$argument]; })); $value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, ['source_value1', 'source_value2']); + $this->assertSame(['source_value1', 'source_value2'], $value); } /** @@ -65,7 +65,7 @@ public function testTransformSourceStringAt() { ->will($this->returnValue('source_value')); $this->plugin->setSource('@@test'); $value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'source_value'); + $this->assertSame('source_value', $value); } /** @@ -85,7 +85,7 @@ public function testTransformSourceArrayAt() { return $map[$argument]; })); $value = $this->plugin->transform(NULL, $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, ['source_value1', 'source_value2', 'source_value3', 'source_value4']); + $this->assertSame(['source_value1', 'source_value2', 'source_value3', 'source_value4'], $value); } /** diff --git a/core/modules/migrate/tests/src/Unit/process/IteratorTest.php b/core/modules/migrate/tests/src/Unit/process/IteratorTest.php index 46daade..dbf0c6b 100644 --- a/core/modules/migrate/tests/src/Unit/process/IteratorTest.php +++ b/core/modules/migrate/tests/src/Unit/process/IteratorTest.php @@ -75,10 +75,10 @@ public function testIterator() { // values ended up in the proper destinations, and that the value of the // key (@id) is the same as the destination ID (42). $new_value = $plugin->transform($current_value, $migrate_executable, $row, 'test'); - $this->assertSame(count($new_value), 1); - $this->assertSame(count($new_value[42]), 2); - $this->assertSame($new_value[42]['foo'], 'test'); - $this->assertSame($new_value[42]['id'], 42); + $this->assertSame(1, count($new_value)); + $this->assertSame(2, count($new_value[42])); + $this->assertSame('test', $new_value[42]['foo']); + $this->assertSame(42, $new_value[42]['id']); } } diff --git a/core/modules/migrate/tests/src/Unit/process/SkipOnEmptyTest.php b/core/modules/migrate/tests/src/Unit/process/SkipOnEmptyTest.php index 08fbef2..d409a88 100644 --- a/core/modules/migrate/tests/src/Unit/process/SkipOnEmptyTest.php +++ b/core/modules/migrate/tests/src/Unit/process/SkipOnEmptyTest.php @@ -31,7 +31,7 @@ public function testProcessBypassesOnNonEmpty() { $configuration['method'] = 'process'; $value = (new SkipOnEmpty($configuration, 'skip_on_empty', [])) ->transform(' ', $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, ' '); + $this->assertSame(' ', $value); } /** @@ -51,7 +51,7 @@ public function testRowBypassesOnNonEmpty() { $configuration['method'] = 'row'; $value = (new SkipOnEmpty($configuration, 'skip_on_empty', [])) ->transform(' ', $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, ' '); + $this->assertSame(' ', $value); } /** diff --git a/core/modules/migrate/tests/src/Unit/process/StaticMapTest.php b/core/modules/migrate/tests/src/Unit/process/StaticMapTest.php index 714d5f7..91a0d2c 100644 --- a/core/modules/migrate/tests/src/Unit/process/StaticMapTest.php +++ b/core/modules/migrate/tests/src/Unit/process/StaticMapTest.php @@ -27,7 +27,7 @@ protected function setUp() { */ public function testMapWithSourceString() { $value = $this->plugin->transform('foo', $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, ['bar' => 'baz']); + $this->assertSame(['bar' => 'baz'], $value); } /** @@ -35,7 +35,7 @@ public function testMapWithSourceString() { */ public function testMapWithSourceList() { $value = $this->plugin->transform(['foo', 'bar'], $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'baz'); + $this->assertSame('baz', $value); } /** @@ -62,7 +62,7 @@ public function testMapWithInvalidSourceWithADefaultValue() { $configuration['default_value'] = 'test'; $this->plugin = new StaticMap($configuration, 'map', []); $value = $this->plugin->transform(['bar'], $this->migrateExecutable, $this->row, 'destinationproperty'); - $this->assertSame($value, 'test'); + $this->assertSame('test', $value); } /** diff --git a/core/modules/migrate/tests/src/Unit/process/SubProcessTest.php b/core/modules/migrate/tests/src/Unit/process/SubProcessTest.php index dfcdec3..1d9061b 100644 --- a/core/modules/migrate/tests/src/Unit/process/SubProcessTest.php +++ b/core/modules/migrate/tests/src/Unit/process/SubProcessTest.php @@ -73,10 +73,10 @@ public function testSubProcess() { // values ended up in the proper destinations, and that the value of the // key (@id) is the same as the destination ID (42). $new_value = $plugin->transform($current_value, $migrate_executable, $row, 'test'); - $this->assertSame(count($new_value), 1); - $this->assertSame(count($new_value[42]), 2); - $this->assertSame($new_value[42]['foo'], 'test'); - $this->assertSame($new_value[42]['id'], 42); + $this->assertSame(1, count($new_value)); + $this->assertSame(2, count($new_value[42])); + $this->assertSame('test', $new_value[42]['foo']); + $this->assertSame(42, $new_value[42]['id']); } } diff --git a/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php b/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php index 6838232..b566c82 100644 --- a/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php +++ b/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php @@ -125,8 +125,8 @@ protected function getMigrations($database_state_key, $drupal_version) { * @param \Drupal\Core\Database\Connection $connection * The database connection object. * - * @return int|false - * An integer representing the major branch of Drupal core (e.g. '6' for + * @return string|false + * A string representing the major branch of Drupal core (e.g. '6' for * Drupal 6.x), or FALSE if no valid version is matched. */ protected function getLegacyDrupalVersion(Connection $connection) { diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntity.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntity.php new file mode 100644 index 0000000..1219510 --- /dev/null +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntity.php @@ -0,0 +1,294 @@ + NULL, + 'include_translations' => TRUE, + ]; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) { + if (empty($plugin_definition['entity_type'])) { + throw new InvalidPluginDefinitionException($plugin_id, 'Missing required "entity_type" definition.'); + } + $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->entityType = $this->entityTypeManager->getDefinition($plugin_definition['entity_type']); + if (!$this->entityType instanceof ContentEntityTypeInterface) { + throw new InvalidPluginDefinitionException($plugin_id, sprintf('The entity type (%s) is not supported. The "content_entity" source plugin only supports content entities.', $plugin_definition['entity_type'])); + } + if (!empty($configuration['bundle'])) { + if (!$this->entityType->hasKey('bundle')) { + throw new \InvalidArgumentException(sprintf('A bundle was provided but the entity type (%s) is not bundleable.', $plugin_definition['entity_type'])); + } + $bundle_info = array_keys($this->entityTypeBundleInfo->getBundleInfo($this->entityType->id())); + if (!in_array($configuration['bundle'], $bundle_info, TRUE)) { + throw new \InvalidArgumentException(sprintf('The provided bundle (%s) is not valid for the (%s) entity type.', $configuration['bundle'], $plugin_definition['entity_type'])); + } + } + parent::__construct($configuration + $this->defaultConfiguration, $plugin_id, $plugin_definition, $migration); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return (string) $this->entityType->getPluralLabel(); + } + + /** + * Initializes the iterator with the source data. + * + * @return \Generator + * A data generator for this source. + */ + protected function initializeIterator() { + $ids = $this->query()->execute(); + return $this->yieldEntities($ids); + } + + /** + * Loads and yields entities, one at a time. + * + * @param array $ids + * The entity IDs. + * + * @return \Generator + * An iterable of the loaded entities. + */ + protected function yieldEntities(array $ids) { + $storage = $this->entityTypeManager + ->getStorage($this->entityType->id()); + foreach ($ids as $id) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $storage->load($id); + yield $this->toArray($entity); + if ($this->configuration['include_translations']) { + foreach ($entity->getTranslationLanguages(FALSE) as $language) { + yield $this->toArray($entity->getTranslation($language->getId())); + } + } + } + } + + /** + * Converts an entity to an array. + * + * Makes all IDs into flat values. All other values are returned as per + * $entity->toArray(), which is a nested array. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity to convert. + * + * @return array + * The entity, represented as an array. + */ + protected function toArray(ContentEntityInterface $entity) { + $return = $entity->toArray(); + // This is necessary because the IDs must be flat. They cannot be nested for + // the ID map. + foreach (array_keys($this->getIds()) as $id) { + /** @var \Drupal\Core\TypedData\Plugin\DataType\ItemList $value */ + $value = $entity->get($id); + // Force the IDs on top of the previous values. + $return[$id] = $value->first()->getString(); + } + return $return; + } + + /** + * Query to retrieve the entities. + * + * @return \Drupal\Core\Entity\Query\QueryInterface + * The query. + */ + public function query() { + $query = $this->entityTypeManager + ->getStorage($this->entityType->id()) + ->getQuery() + ->accessCheck(FALSE); + if (!empty($this->configuration['bundle'])) { + $query->condition($this->entityType->getKey('bundle'), $this->configuration['bundle']); + } + return $query; + } + + /** + * {@inheritdoc} + */ + public function count($refresh = FALSE) { + // If no translations are included, then a simple query is possible. + if (!$this->configuration['include_translations']) { + return parent::count($refresh); + } + // @TODO: Determine a better way to retrieve a valid count for translations. + // https://www.drupal.org/project/drupal/issues/2937166 + return -1; + } + + /** + * {@inheritdoc} + */ + protected function doCount() { + return $this->query()->count()->execute(); + } + + /** + * {@inheritdoc} + */ + public function fields() { + // Retrieving fields from a non-fieldable content entity will throw a + // LogicException. Return an empty list of fields instead. + if (!$this->entityType->entityClassImplements('Drupal\Core\Entity\FieldableEntityInterface')) { + return []; + } + $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id()); + if (!empty($this->configuration['bundle'])) { + $field_definitions += $this->entityFieldManager->getFieldDefinitions($this->entityType->id(), $this->configuration['bundle']); + } + $fields = array_map(function ($definition) { + return (string) $definition->getLabel(); + }, $field_definitions); + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $id_key = $this->entityType->getKey('id'); + $ids[$id_key] = $this->getDefinitionFromEntity($id_key); + if ($this->entityType->isTranslatable()) { + $langcode_key = $this->entityType->getKey('langcode'); + $ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key); + } + return $ids; + } + + /** + * Gets the field definition from a specific entity base field. + * + * @param string $key + * The field ID key. + * + * @return array + * An associative array with a structure that contains the field type, keyed + * as 'type', together with field storage settings as they are returned by + * FieldStorageDefinitionInterface::getSettings(). + * + * @see \Drupal\migrate\Plugin\migrate\destination\EntityContentBase::getDefinitionFromEntity() + */ + protected function getDefinitionFromEntity($key) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ + $field_definition = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id())[$key]; + return [ + 'type' => $field_definition->getType(), + ] + $field_definition->getSettings(); + } + +} diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntityDeriver.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntityDeriver.php new file mode 100644 index 0000000..2740441 --- /dev/null +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntityDeriver.php @@ -0,0 +1,60 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $this->derivatives = []; + foreach ($this->entityTypeManager->getDefinitions() as $id => $definition) { + if ($definition instanceof ContentEntityTypeInterface) { + $this->derivatives[$id] = $base_plugin_definition; + // Provide entity_type so the source can be used apart from a deriver. + $this->derivatives[$id]['entity_type'] = $id; + } + } + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php index 9334ac0..1c8a875 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php @@ -46499,6 +46499,14 @@ 'list' => '0', 'weight' => '1', )) +->values(array( + 'fid' => '3', + 'nid' => '12', + 'vid' => '15', + 'description' => 'file 12-15-3', + 'list' => '0', + 'weight' => '0', +)) ->execute(); $connection->schema()->createTable('url_alias', array( @@ -46583,6 +46591,12 @@ 'dst' => 'the-zulu-people', 'language' => 'en', )) +->values(array( + 'pid' => '8', + 'src' => 'admin', + 'dst' => 'source-noslash', + 'language' => '', +)) ->execute(); $connection->schema()->createTable('users', array( diff --git a/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php new file mode 100644 index 0000000..72372af --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php @@ -0,0 +1,444 @@ +installEntitySchema('node'); + $this->installEntitySchema('file'); + $this->installEntitySchema('media'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('taxonomy_vocabulary'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['sequences']); + $this->installSchema('user', 'users_data'); + $this->installSchema('file', 'file_usage'); + $this->installSchema('node', ['node_access']); + $this->installConfig($this->modules); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Create article content type. + $node_type = NodeType::create(['type' => $this->bundle, 'name' => 'Article']); + $node_type->save(); + + // Create a vocabulary. + $vocabulary = Vocabulary::create([ + 'name' => $this->vocabulary, + 'description' => $this->vocabulary, + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + $vocabulary->save(); + + // Create a term reference field on node. + $this->createEntityReferenceField( + 'node', + $this->bundle, + $this->fieldName, + 'Term reference', + 'taxonomy_term', + 'default', + ['target_bundles' => [$this->vocabulary]], + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + // Create a term reference field on user. + $this->createEntityReferenceField( + 'user', + 'user', + $this->fieldName, + 'Term reference', + 'taxonomy_term', + 'default', + ['target_bundles' => [$this->vocabulary]], + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + + // Create some data. + $this->user = User::create([ + 'name' => 'user123', + 'uid' => 1, + 'mail' => 'example@example.com', + ]); + $this->user->save(); + + $term = Term::create([ + 'vid' => $this->vocabulary, + 'name' => 'Apples', + 'uid' => $this->user->id(), + ]); + $term->save(); + $this->user->set($this->fieldName, $term->id()); + $this->user->save(); + $node = Node::create([ + 'type' => $this->bundle, + 'title' => 'Apples', + $this->fieldName => $term->id(), + 'uid' => $this->user->id(), + ]); + $node->save(); + $node->addTranslation('fr', [ + 'title' => 'Pommes', + $this->fieldName => $term->id(), + ])->save(); + + $this->sourcePluginManager = $this->container->get('plugin.manager.migrate.source'); + $this->migrationPluginManager = $this->container->get('plugin.manager.migration'); + } + + /** + * Tests the constructor for missing entity_type. + */ + public function testConstructorEntityTypeMissing() { + $migration = $this->prophesize(MigrationInterface::class)->reveal(); + $configuration = []; + $plugin_definition = [ + 'entity_type' => '', + ]; + $this->setExpectedException(InvalidPluginDefinitionException::class, 'Missing required "entity_type" definition.'); + ContentEntity::create($this->container, $configuration, 'content_entity', $plugin_definition, $migration); + } + + /** + * Tests the constructor for non content entity. + */ + public function testConstructorNonContentEntity() { + $migration = $this->prophesize(MigrationInterface::class)->reveal(); + $configuration = []; + $plugin_definition = [ + 'entity_type' => 'node_type', + ]; + $this->setExpectedException(InvalidPluginDefinitionException::class, 'The entity type (node_type) is not supported. The "content_entity" source plugin only supports content entities.'); + ContentEntity::create($this->container, $configuration, 'content_entity:node_type', $plugin_definition, $migration); + } + + /** + * Tests the constructor for not bundleable entity. + */ + public function testConstructorNotBundable() { + $migration = $this->prophesize(MigrationInterface::class)->reveal(); + $configuration = [ + 'bundle' => 'foo', + ]; + $plugin_definition = [ + 'entity_type' => 'user', + ]; + $this->setExpectedException(\InvalidArgumentException::class, 'A bundle was provided but the entity type (user) is not bundleable'); + ContentEntity::create($this->container, $configuration, 'content_entity:user', $plugin_definition, $migration); + } + + /** + * Tests the constructor for invalid entity bundle. + */ + public function testConstructorInvalidBundle() { + $migration = $this->prophesize(MigrationInterface::class)->reveal(); + $configuration = [ + 'bundle' => 'foo', + ]; + $plugin_definition = [ + 'entity_type' => 'node', + ]; + $this->setExpectedException(\InvalidArgumentException::class, 'The provided bundle (foo) is not valid for the (node) entity type.'); + ContentEntity::create($this->container, $configuration, 'content_entity:node', $plugin_definition, $migration); + } + + /** + * Tests user source plugin. + */ + public function testUserSource() { + $configuration = [ + 'include_translations' => FALSE, + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:user')); + $user_source = $this->sourcePluginManager->createInstance('content_entity:user', $configuration, $migration); + $this->assertSame('user entities', $user_source->__toString()); + $this->assertEquals(1, $user_source->count()); + $ids = $user_source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('uid', $ids); + $fields = $user_source->fields(); + $this->assertArrayHasKey('name', $fields); + $this->assertArrayHasKey('pass', $fields); + $this->assertArrayHasKey('mail', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('roles', $fields); + $user_source->rewind(); + $values = $user_source->current()->getSource(); + $this->assertEquals('example@example.com', $values['mail'][0]['value']); + $this->assertEquals('user123', $values['name'][0]['value']); + $this->assertEquals(1, $values['uid']); + $this->assertEquals(1, $values['field_entity_reference'][0]['target_id']); + } + + /** + * Tests file source plugin. + */ + public function testFileSource() { + $file = File::create([ + 'filename' => 'foo.txt', + 'uid' => $this->user->id(), + 'uri' => 'public://foo.txt', + ]); + $file->save(); + + $configuration = [ + 'include_translations' => FALSE, + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:file')); + $file_source = $this->sourcePluginManager->createInstance('content_entity:file', $configuration, $migration); + $this->assertSame('file entities', $file_source->__toString()); + $this->assertEquals(1, $file_source->count()); + $ids = $file_source->getIds(); + $this->assertArrayHasKey('fid', $ids); + $fields = $file_source->fields(); + $this->assertArrayHasKey('fid', $fields); + $this->assertArrayHasKey('filemime', $fields); + $this->assertArrayHasKey('filename', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('uri', $fields); + $file_source->rewind(); + $values = $file_source->current()->getSource(); + $this->assertEquals('text/plain', $values['filemime'][0]['value']); + $this->assertEquals('public://foo.txt', $values['uri'][0]['value']); + $this->assertEquals('foo.txt', $values['filename'][0]['value']); + $this->assertEquals(1, $values['fid']); + } + + /** + * Tests node source plugin. + */ + public function testNodeSource() { + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:node')); + $node_source = $this->sourcePluginManager->createInstance('content_entity:node', ['bundle' => $this->bundle], $migration); + $this->assertSame('content items', $node_source->__toString()); + $ids = $node_source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('nid', $ids); + $fields = $node_source->fields(); + $this->assertArrayHasKey('nid', $fields); + $this->assertArrayHasKey('vid', $fields); + $this->assertArrayHasKey('title', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('sticky', $fields); + $node_source->rewind(); + $values = $node_source->current()->getSource(); + $this->assertEquals($this->bundle, $values['type'][0]['target_id']); + $this->assertEquals(1, $values['nid']); + $this->assertEquals('en', $values['langcode']); + $this->assertEquals(1, $values['status'][0]['value']); + $this->assertEquals('Apples', $values['title'][0]['value']); + $this->assertEquals(1, $values['default_langcode'][0]['value']); + $this->assertEquals(1, $values['field_entity_reference'][0]['target_id']); + $node_source->next(); + $values = $node_source->current()->getSource(); + $this->assertEquals($this->bundle, $values['type'][0]['target_id']); + $this->assertEquals(1, $values['nid']); + $this->assertEquals('fr', $values['langcode']); + $this->assertEquals(1, $values['status'][0]['value']); + $this->assertEquals('Pommes', $values['title'][0]['value']); + $this->assertEquals(0, $values['default_langcode'][0]['value']); + $this->assertEquals(1, $values['field_entity_reference'][0]['target_id']); + } + + /** + * Tests media source plugin. + */ + public function testMediaSource() { + $values = [ + 'id' => 'image', + 'bundle' => 'image', + 'label' => 'Image', + 'source' => 'test', + 'new_revision' => FALSE, + ]; + $media_type = $this->createMediaType($values); + $media = Media::create([ + 'name' => 'Foo media', + 'uid' => $this->user->id(), + 'bundle' => $media_type->id(), + ]); + $media->save(); + + $configuration = [ + 'include_translations' => FALSE, + 'bundle' => 'image', + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:media')); + $media_source = $this->sourcePluginManager->createInstance('content_entity:media', $configuration, $migration); + $this->assertSame('media items', $media_source->__toString()); + $this->assertEquals(1, $media_source->count()); + $ids = $media_source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('mid', $ids); + $fields = $media_source->fields(); + $this->assertArrayHasKey('bundle', $fields); + $this->assertArrayHasKey('mid', $fields); + $this->assertArrayHasKey('name', $fields); + $this->assertArrayHasKey('status', $fields); + $media_source->rewind(); + $values = $media_source->current()->getSource(); + $this->assertEquals(1, $values['mid']); + $this->assertEquals('Foo media', $values['name'][0]['value']); + $this->assertEquals('Foo media', $values['thumbnail'][0]['title']); + $this->assertEquals(1, $values['uid'][0]['target_id']); + $this->assertEquals('image', $values['bundle'][0]['target_id']); + } + + /** + * Tests term source plugin. + */ + public function testTermSource() { + $term2 = Term::create([ + 'vid' => $this->vocabulary, + 'name' => 'Granny Smith', + 'uid' => $this->user->id(), + 'parent' => 1, + ]); + $term2->save(); + + $configuration = [ + 'include_translations' => FALSE, + 'bundle' => $this->vocabulary, + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:taxonomy_term')); + $term_source = $this->sourcePluginManager->createInstance('content_entity:taxonomy_term', $configuration, $migration); + $this->assertSame('taxonomy term entities', $term_source->__toString()); + $this->assertEquals(2, $term_source->count()); + $ids = $term_source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('tid', $ids); + $fields = $term_source->fields(); + $this->assertArrayHasKey('vid', $fields); + $this->assertArrayHasKey('tid', $fields); + $this->assertArrayHasKey('name', $fields); + $term_source->rewind(); + $values = $term_source->current()->getSource(); + $this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']); + $this->assertEquals(1, $values['tid']); + // @TODO: Add test coverage for parent in + // https://www.drupal.org/project/drupal/issues/2940198 + $this->assertEquals('Apples', $values['name'][0]['value']); + $term_source->next(); + $values = $term_source->current()->getSource(); + $this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']); + $this->assertEquals(2, $values['tid']); + // @TODO: Add test coverage for parent in + // https://www.drupal.org/project/drupal/issues/2940198 + $this->assertEquals('Granny Smith', $values['name'][0]['value']); + } + + /** + * Get a migration definition. + * + * @param string $plugin_id + * The plugin id. + * + * @return array + * The definition. + */ + protected function migrationDefinition($plugin_id) { + return [ + 'source' => [ + 'plugin' => $plugin_id, + ], + 'process' => [], + 'destination' => [ + 'plugin' => 'null', + ], + ]; + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php index a0bb591..bee030e 100644 --- a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php @@ -37,6 +37,7 @@ protected function setUp() { $this->installSchema('forum', ['forum_index']); $this->installSchema('node', ['node_access']); $this->installSchema('search', ['search_dataset']); + $this->installSchema('system', ['sequences']); $this->installSchema('tracker', ['tracker_node', 'tracker_user']); // Enable content moderation for nodes of type page. @@ -59,8 +60,9 @@ public function testMultipleMigrationWithoutIdConflicts() { // Insert data in the d6_node:page migration mappping table to simulate a // previously migrated node. - $table_name = $this->getMigration('d6_node:page')->getIdMap()->mapTableName(); - $this->container->get('database')->insert($table_name) + $id_map = $this->getMigration('d6_node:page')->getIdMap(); + $table_name = $id_map->mapTableName(); + $id_map->getDatabase()->insert($table_name) ->fields([ 'source_ids_hash' => 1, 'sourceid1' => 1, @@ -156,8 +158,9 @@ public function testDraftRevisionIdConflicts() { // Insert data in the d6_node_revision:page migration mappping table to // simulate a previously migrated node revison. - $table_name = $this->getMigration('d6_node_revision:page')->getIdMap()->mapTableName(); - $this->container->get('database')->insert($table_name) + $id_map = $this->getMigration('d6_node_revision:page')->getIdMap(); + $table_name = $id_map->mapTableName(); + $id_map->getDatabase()->insert($table_name) ->fields([ 'source_ids_hash' => 1, 'sourceid1' => 1, diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php index 1d74423..d12445b 100644 --- a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php @@ -37,6 +37,7 @@ protected function setUp() { $this->installSchema('forum', ['forum_index']); $this->installSchema('node', ['node_access']); $this->installSchema('search', ['search_dataset']); + $this->installSchema('system', ['sequences']); $this->installSchema('tracker', ['tracker_node', 'tracker_user']); // Enable content moderation for nodes of type page. @@ -59,8 +60,9 @@ public function testMultipleMigrationWithoutIdConflicts() { // Insert data in the d7_node:page migration mappping table to simulate a // previously migrated node. - $table_name = $this->getMigration('d7_node:page')->getIdMap()->mapTableName(); - $this->container->get('database')->insert($table_name) + $id_map = $this->getMigration('d7_node:page')->getIdMap(); + $table_name = $id_map->mapTableName(); + $id_map->getDatabase()->insert($table_name) ->fields([ 'source_ids_hash' => 1, 'sourceid1' => 1, @@ -155,8 +157,9 @@ public function testDraftRevisionIdConflicts() { // Insert data in the d7_node_revision:page migration mappping table to // simulate a previously migrated node revison. - $table_name = $this->getMigration('d7_node_revision:page')->getIdMap()->mapTableName(); - $this->container->get('database')->insert($table_name) + $id_map = $this->getMigration('d7_node_revision:page')->getIdMap(); + $table_name = $id_map->mapTableName(); + $id_map->getDatabase()->insert($table_name) ->fields([ 'source_ids_hash' => 1, 'sourceid1' => 1, diff --git a/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php index db720e8..8896222 100644 --- a/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php +++ b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php @@ -2,16 +2,6 @@ namespace Drupal\Tests\migrate_drupal\Traits; -use Drupal\aggregator\Entity\Feed; -use Drupal\aggregator\Entity\Item; -use Drupal\block_content\Entity\BlockContent; -use Drupal\comment\Entity\Comment; -use Drupal\file\Entity\File; -use Drupal\menu_link_content\Entity\MenuLinkContent; -use Drupal\node\Entity\Node; -use Drupal\taxonomy\Entity\Term; -use Drupal\user\Entity\User; - /** * Provides helper methods for creating test content. */ @@ -60,72 +50,161 @@ protected function installEntitySchemas() { * Create several pieces of generic content. */ protected function createContent() { + $entity_type_manager = \Drupal::entityTypeManager(); + // Create an aggregator feed. - $feed = Feed::create([ - 'title' => 'feed', - 'url' => 'http://www.example.com', - ]); - $feed->save(); - - // Create an aggregator feed item. - $item = Item::create([ - 'title' => 'feed item', - 'fid' => $feed->id(), - 'link' => 'http://www.example.com', - ]); - $item->save(); + if ($entity_type_manager->hasDefinition('aggregator_feed')) { + $feed = $entity_type_manager->getStorage('aggregator_feed')->create([ + 'title' => 'feed', + 'url' => 'http://www.example.com', + ]); + $feed->save(); + + // Create an aggregator feed item. + $item = $entity_type_manager->getStorage('aggregator_item')->create([ + 'title' => 'feed item', + 'fid' => $feed->id(), + 'link' => 'http://www.example.com', + ]); + $item->save(); + } + + // Create a block content. + if ($entity_type_manager->hasDefinition('block_content')) { + $block = $entity_type_manager->getStorage('block_content')->create([ + 'info' => 'block', + 'type' => 'block', + ]); + $block->save(); + } + + // Create a node. + if ($entity_type_manager->hasDefinition('node')) { + $node = $entity_type_manager->getStorage('node')->create([ + 'type' => 'page', + 'title' => 'page', + ]); + $node->save(); + + // Create a comment. + if ($entity_type_manager->hasDefinition('comment')) { + $comment = $entity_type_manager->getStorage('comment')->create([ + 'comment_type' => 'comment', + 'field_name' => 'comment', + 'entity_type' => 'node', + 'entity_id' => $node->id(), + ]); + $comment->save(); + } + } + + // Create a file. + if ($entity_type_manager->hasDefinition('file')) { + $file = $entity_type_manager->getStorage('file')->create([ + 'uri' => 'public://example.txt', + ]); + $file->save(); + } + + // Create a menu link. + if ($entity_type_manager->hasDefinition('menu_link_content')) { + $menu_link = $entity_type_manager->getStorage('menu_link_content')->create([ + 'title' => 'menu link', + 'link' => ['uri' => 'http://www.example.com'], + 'menu_name' => 'tools', + ]); + $menu_link->save(); + } + + // Create a taxonomy term. + if ($entity_type_manager->hasDefinition('taxonomy_term')) { + $term = $entity_type_manager->getStorage('taxonomy_term')->create([ + 'name' => 'term', + 'vid' => 'term', + ]); + $term->save(); + } + + // Create a user. + if ($entity_type_manager->hasDefinition('user')) { + $user = $entity_type_manager->getStorage('user')->create([ + 'name' => 'user', + 'mail' => 'user@example.com', + ]); + $user->save(); + } + } + + /** + * Create several pieces of generic content. + */ + protected function createContentPostUpgrade() { + $entity_type_manager = \Drupal::entityTypeManager(); // Create a block content. - $block = BlockContent::create([ - 'info' => 'block', - 'type' => 'block', - ]); - $block->save(); + if ($entity_type_manager->hasDefinition('block_content')) { + $block = $entity_type_manager->getStorage('block_content')->create([ + 'info' => 'Post upgrade block', + 'type' => 'block', + ]); + $block->save(); + } // Create a node. - $node = Node::create([ - 'type' => 'page', - 'title' => 'page', - ]); - $node->save(); - - // Create a comment. - $comment = Comment::create([ - 'comment_type' => 'comment', - 'field_name' => 'comment', - 'entity_type' => 'node', - 'entity_id' => $node->id(), - ]); - $comment->save(); + if ($entity_type_manager->hasDefinition('node')) { + $node = $entity_type_manager->getStorage('node')->create([ + 'type' => 'page', + 'title' => 'Post upgrade page', + ]); + $node->save(); + + // Create a comment. + if ($entity_type_manager->hasDefinition('comment')) { + $comment = $entity_type_manager->getStorage('comment')->create([ + 'comment_type' => 'comment', + 'field_name' => 'comment', + 'entity_type' => 'node', + 'entity_id' => $node->id(), + ]); + $comment->save(); + } + } // Create a file. - $file = File::create([ - 'uri' => 'public://example.txt', - ]); - $file->save(); + if ($entity_type_manager->hasDefinition('file')) { + $file = $entity_type_manager->getStorage('file')->create([ + 'uri' => 'public://post_upgrade_example.txt', + ]); + $file->save(); + } // Create a menu link. - $menu_link = MenuLinkContent::create([ - 'title' => 'menu link', - 'link' => ['uri' => 'http://www.example.com'], - 'menu_name' => 'tools', - ]); - $menu_link->save(); + if ($entity_type_manager->hasDefinition('menu_link_content')) { + $menu_link = $entity_type_manager->getStorage('menu_link_content')->create([ + 'title' => 'post upgrade menu link', + 'link' => ['uri' => 'http://www.drupal.org'], + 'menu_name' => 'tools', + ]); + $menu_link->save(); + } // Create a taxonomy term. - $term = Term::create([ - 'name' => 'term', - 'vid' => 'term', - ]); - $term->save(); + if ($entity_type_manager->hasDefinition('taxonomy_term')) { + $term = $entity_type_manager->getStorage('taxonomy_term')->create([ + 'name' => 'post upgrade term', + 'vid' => 'term', + ]); + $term->save(); + } // Create a user. - $user = User::create([ - 'uid' => 2, - 'name' => 'user', - 'mail' => 'user@example.com', - ]); - $user->save(); + if ($entity_type_manager->hasDefinition('user')) { + $user = $entity_type_manager->getStorage('user')->create([ + 'name' => 'universe', + 'mail' => 'universe@example.com', + ]); + $user->save(); + } } } diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.module b/core/modules/migrate_drupal_ui/migrate_drupal_ui.module index 0a8471a..aef9f91 100644 --- a/core/modules/migrate_drupal_ui/migrate_drupal_ui.module +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.module @@ -31,8 +31,6 @@ function migrate_drupal_ui_help($route_name, RouteMatchInterface $route_match) { $output .= '
' . t('Reviewing the upgrade log') . '
'; $output .= '
' . t('You can review a log of upgrade messages by clicking the link in the message provided after the upgrade or by filtering the messages for the type migrate_drupal_ui on the Recent log messages page.', [':log' => \Drupal::url('migrate_drupal_ui.log'), ':messages' => \Drupal::url('dblog.overview')]) . '
'; - $output .= '
' . t('Incremental upgrades') . '
'; - $output .= '
' . t('Incremental upgrades are not yet supported through the user interface.') . '
'; $output .= '
' . t('Rolling back an upgrade') . '
'; $output .= '
' . t('Rolling back an upgrade is not yet supported through the user interface.') . '
'; $output .= ''; diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index c080a39..6ff17f2 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -2,6 +2,7 @@ namespace Drupal\migrate_drupal_ui\Form; +use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\ConfirmFormBase; @@ -274,14 +275,20 @@ public function buildOverviewForm(array $form, FormStateInterface $form_state) { $form['#title'] = $this->t('Upgrade'); if ($date_performed = $this->state->get('migrate_drupal_ui.performed')) { - // @todo Add back support for rollbacks and incremental migrations. - // https://www.drupal.org/node/2687843 + // @todo Add back support for rollbacks. // https://www.drupal.org/node/2687849 $form['upgrade_option_item'] = [ '#type' => 'item', - '#prefix' => $this->t('An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal 8. Rollbacks and incremental migrations are not yet supported through the user interface. For more information, see the upgrading handbook.', [':url' => 'https://www.drupal.org/upgrade/migrate']), + '#prefix' => $this->t('An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal 8. Rollbacks are not yet supported through the user interface. For more information, see the upgrading handbook.', [':url' => 'https://www.drupal.org/upgrade/migrate']), '#description' => $this->t('Last upgrade: @date', ['@date' => $this->dateFormatter->format($date_performed)]), ]; + $form['actions']['incremental'] = [ + '#type' => 'submit', + '#value' => $this->t('Import new configuration and content from old site'), + '#button_type' => 'primary', + '#validate' => ['::validateIncrementalForm'], + '#submit' => ['::submitIncrementalForm'], + ]; return $form; } else { @@ -344,8 +351,50 @@ public function buildOverviewForm(array $form, FormStateInterface $form_state) { * The current state of the form. */ public function submitOverviewForm(array &$form, FormStateInterface $form_state) { - $form_state->set('step', 'credentials'); - $form_state->setRebuild(); + $form_state->set('step', 'credentials')->setRebuild(); + } + + /** + * Validation handler for the incremental overview form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function validateIncrementalForm(array &$form, FormStateInterface $form_state) { + // Retrieve the database driver from state. + $database_state_key = $this->state->get('migrate.fallback_state_key', ''); + if ($database_state_key) { + try { + $database = $this->state->get($database_state_key, [])['database']; + if ($connection = $this->getConnection($database)) { + if ($version = $this->getLegacyDrupalVersion($connection)) { + $this->setupMigrations($database, $form_state); + $valid_legacy_database = TRUE; + } + } + } + catch (DatabaseExceptionWrapper $exception) { + // Hide DB exceptions and forward to the DB credentials form. In that + // form we can more properly display errors and accept new credentials. + } + } + if (empty($valid_legacy_database)) { + $form_state->setValue('step', 'credentials')->setRebuild(); + } + } + + /** + * Form submission handler for the incremental overview form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitIncrementalForm(array &$form, FormStateInterface $form_state) { + $form_state->set('step', 'confirm_id_conflicts')->setRebuild(); } /** @@ -528,31 +577,7 @@ public function validateCredentialForm(array &$form, FormStateInterface $form_st ])); } else { - $this->createDatabaseStateSettings($database, $version); - $migrations = $this->getMigrations('migrate_drupal_' . $version, $version); - - // Get the system data from source database. - $system_data = $this->getSystemData($connection); - - // Convert the migration object into array - // so that it can be stored in form storage. - $migration_array = []; - foreach ($migrations as $migration) { - $migration_array[$migration->id()] = $migration->label(); - } - - // Store the retrieved migration IDs in form storage. - $form_state->set('version', $version); - $form_state->set('migrations', $migration_array); - if ($version === '6') { - $form_state->set('source_base_path', $form_state->getValue('d6_source_base_path')); - } - else { - $form_state->set('source_base_path', $form_state->getValue('source_base_path')); - } - $form_state->set('source_private_file_path', $form_state->getValue('source_private_file_path')); - // Store the retrived system data in form storage. - $form_state->set('system_data', $system_data); + $this->setupMigrations($database, $form_state); } } catch (\Exception $e) { @@ -975,6 +1000,48 @@ protected function getDatabaseTypes() { } /** + * Puts migrations information in form state. + * + * Gets all the migrations, converts each to an array and stores it in the + * form state. The source base path for public and private files is also + * put into form state. + * + * @param array $database + * Database array representing the source Drupal database. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function setupMigrations(array $database, FormStateInterface $form_state) { + $connection = $this->getConnection($database); + $version = $this->getLegacyDrupalVersion($connection); + $this->createDatabaseStateSettings($database, $version); + $migrations = $this->getMigrations('migrate_drupal_' . $version, $version); + + // Get the system data from source database. + $system_data = $this->getSystemData($connection); + + // Convert the migration object into array + // so that it can be stored in form storage. + $migration_array = []; + foreach ($migrations as $migration) { + $migration_array[$migration->id()] = $migration->label(); + } + + // Store the retrieved migration IDs in form storage. + $form_state->set('version', $version); + $form_state->set('migrations', $migration_array); + if ($version == 6) { + $form_state->set('source_base_path', $form_state->getValue('d6_source_base_path')); + } + else { + $form_state->set('source_base_path', $form_state->getValue('source_base_path')); + } + $form_state->set('source_private_file_path', $form_state->getValue('source_private_file_path')); + // Store the retrieved system data in form storage. + $form_state->set('system_data', $system_data); + } + + /** * {@inheritdoc} */ public function getQuestion() { diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php index 4758a5e..a9d7de1 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php @@ -2,7 +2,6 @@ namespace Drupal\Tests\migrate_drupal_ui\Functional; -use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate_drupal\MigrationConfigurationTrait; use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait; @@ -26,6 +25,12 @@ protected function setUp() { /** * Executes all steps of migrations upgrade. + * + * The upgrade is started three times. The first time is to test that + * providing incorrect database credentials fails as expected. The second + * time is to run the migration and assert the results. The third time is + * to test an incremental migration, by installing the aggregator module, + * and assert the results. */ public function testMigrateUpgradeExecute() { $connection_options = $this->sourceDatabase->getConnectionOptions(); @@ -84,22 +89,11 @@ public function testMigrateUpgradeExecute() { $session->fieldExists('mysql[host]'); $this->drupalPostForm(NULL, $edits, t('Review upgrade')); - $session->pageTextContains('WARNING: Content may be overwritten on your new site.'); - $session->pageTextContains('There is conflicting content of these types:'); - $session->pageTextContains('aggregator feed entities'); - $session->pageTextContains('aggregator feed item entities'); - $session->pageTextContains('custom block entities'); - $session->pageTextContains('custom menu link entities'); - $session->pageTextContains('file entities'); - $session->pageTextContains('taxonomy term entities'); - $session->pageTextContains('user entities'); - $session->pageTextContains('comments'); - $session->pageTextContains('content item revisions'); - $session->pageTextContains('content items'); - $session->pageTextContains('There is translated content of these types:'); + $this->assertIdConflict($session); + $this->drupalPostForm(NULL, [], t('I acknowledge I may lose data. Continue anyway.')); $session->statusCodeEquals(200); - $session->pageTextContains('What will be upgraded?'); + // Ensure there are no errors about missing modules from the test module. $session->pageTextNotContains(t('Source module not found for migration_provider_no_annotation.')); $session->pageTextNotContains(t('Source module not found for migration_provider_test.')); @@ -109,49 +103,40 @@ public function testMigrateUpgradeExecute() { // Test the upgrade paths. $available_paths = $this->getAvailablePaths(); $missing_paths = $this->getMissingPaths(); - $this->assertUpgradePaths($session, $available_paths, $missing_paths); + $this->assertReviewPage($session, $available_paths, $missing_paths); $this->drupalPostForm(NULL, [], t('Perform upgrade')); $this->assertText(t('Congratulations, you upgraded Drupal!')); + $this->assertMigrationResults($this->getEntityCounts(), $version); - // Have to reset all the statics after migration to ensure entities are - // loadable. - $this->resetAll(); - - $expected_counts = $this->getEntityCounts(); - foreach (array_keys(\Drupal::entityTypeManager() - ->getDefinitions()) as $entity_type) { - $real_count = \Drupal::entityQuery($entity_type)->count()->execute(); - $expected_count = isset($expected_counts[$entity_type]) ? $expected_counts[$entity_type] : 0; - $this->assertEqual($expected_count, $real_count, "Found $real_count $entity_type entities, expected $expected_count."); - } - - $plugin_manager = \Drupal::service('plugin.manager.migration'); - /** @var \Drupal\migrate\Plugin\Migration[] $all_migrations */ - $all_migrations = $plugin_manager->createInstancesByTag('Drupal ' . $version); - foreach ($all_migrations as $migration) { - $id_map = $migration->getIdMap(); - foreach ($id_map as $source_id => $map) { - // Convert $source_id into a keyless array so that - // \Drupal\migrate\Plugin\migrate\id_map\Sql::getSourceHash() works as - // expected. - $source_id_values = array_values(unserialize($source_id)); - $row = $id_map->getRowBySource($source_id_values); - $destination = serialize($id_map->currentDestination()); - $message = "Migration of $source_id to $destination as part of the {$migration->id()} migration. The source row status is " . $row['source_row_status']; - // A completed migration should have maps with - // MigrateIdMapInterface::STATUS_IGNORED or - // MigrateIdMapInterface::STATUS_IMPORTED. - if ($row['source_row_status'] == MigrateIdMapInterface::STATUS_FAILED || $row['source_row_status'] == MigrateIdMapInterface::STATUS_NEEDS_UPDATE) { - $this->fail($message); - } - else { - $this->pass($message); - } - } - } \Drupal::service('module_installer')->install(['forum']); \Drupal::service('module_installer')->install(['book']); + + // Test incremental migration. + $this->createContentPostUpgrade(); + + $this->drupalGet('/upgrade'); + $session->pageTextContains('An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal 8. Rollbacks are not yet supported through the user interface.'); + $this->drupalPostForm(NULL, [], t('Import new configuration and content from old site')); + $session->pageTextContains('WARNING: Content may be overwritten on your new site.'); + $session->pageTextContains('There is conflicting content of these types:'); + $session->pageTextContains('file entities'); + $session->pageTextContains('content item revisions'); + $session->pageTextContains('There is translated content of these types:'); + $session->pageTextContains('content items'); + + $this->drupalPostForm(NULL, [], t('I acknowledge I may lose data. Continue anyway.')); + $session->statusCodeEquals(200); + + // Need to update available and missing path lists. + $all_available = $this->getAvailablePaths(); + $all_available[] = 'aggregator'; + $all_missing = $this->getMissingPaths(); + $all_missing = array_diff($all_missing, ['aggregator']); + $this->assertReviewPage($session, $all_available, $all_missing); + $this->drupalPostForm(NULL, [], t('Perform upgrade')); + $session->pageTextContains(t('Congratulations, you upgraded Drupal!')); + $this->assertMigrationResults($this->getEntityCountsIncremental(), $version); } } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php index eb72bdd..4138bea 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php @@ -121,4 +121,11 @@ protected function getEntityCounts() { return []; } + /** + * {@inheritdoc} + */ + protected function getEntityCountsIncremental() { + return []; + } + } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php index 624a052..23d5c29 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\migrate_drupal_ui\Functional; use Drupal\Core\Database\Database; +use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate_drupal\MigrationConfigurationTrait; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait; @@ -178,4 +179,107 @@ protected function assertUpgradePaths(WebAssert $session, array $available_paths */ abstract protected function getMissingPaths(); + /** + * Gets expected number of entities per entity after incremental migration. + * + * @return int[] + * An array of expected counts keyed by entity type ID. + */ + abstract protected function getEntityCountsIncremental(); + + /** + * Helper method to assert the text on the 'Upgrade analysis report' page. + * + * @param \Drupal\Tests\WebAssert $session + * The current session. + * @param array $all_available + * Array of modules that will be upgraded. + * @param array $all_missing + * Array of modules that will not be upgraded. + */ + protected function assertReviewPage(WebAssert $session, array $all_available, array $all_missing) { + $this->assertText('What will be upgraded?'); + + // Ensure there are no errors about the missing modules from the test module. + $session->pageTextNotContains(t('Source module not found for migration_provider_no_annotation.')); + $session->pageTextNotContains(t('Source module not found for migration_provider_test.')); + $session->pageTextNotContains(t('Destination module not found for migration_provider_test')); + // Ensure there are no errors about any other missing migration providers. + $session->pageTextNotContains(t('module not found')); + + // Test the available migration paths. + foreach ($all_available as $available) { + $session->elementExists('xpath', "//span[contains(@class, 'checked') and text() = '$available']"); + $session->elementNotExists('xpath', "//span[contains(@class, 'error') and text() = '$available']"); + } + + // Test the missing migration paths. + foreach ($all_missing as $missing) { + $session->elementExists('xpath', "//span[contains(@class, 'error') and text() = '$missing']"); + $session->elementNotExists('xpath', "//span[contains(@class, 'checked') and text() = '$missing']"); + } + } + + /** + * Helper method that asserts text on the ID conflict form. + * + * @param \Drupal\Tests\WebAssert $session + * The current session. + * @param $session + * The current session. + */ + protected function assertIdConflict(WebAssert $session) { + $session->pageTextContains('WARNING: Content may be overwritten on your new site.'); + $session->pageTextContains('There is conflicting content of these types:'); + $session->pageTextContains('custom block entities'); + $session->pageTextContains('custom menu link entities'); + $session->pageTextContains('file entities'); + $session->pageTextContains('taxonomy term entities'); + $session->pageTextContains('user entities'); + $session->pageTextContains('comments'); + $session->pageTextContains('content item revisions'); + $session->pageTextContains('content items'); + $session->pageTextContains('There is translated content of these types:'); + } + + /** + * Checks that migrations have been performed successfully. + * + * @param array $expected_counts + * The expected counts of each entity type. + * @param int $version + * The Drupal version. + */ + protected function assertMigrationResults(array $expected_counts, $version) { + // Have to reset all the statics after migration to ensure entities are + // loadable. + $this->resetAll(); + foreach (array_keys(\Drupal::entityTypeManager()->getDefinitions()) as $entity_type) { + $real_count = (int) \Drupal::entityQuery($entity_type)->count()->execute(); + $expected_count = isset($expected_counts[$entity_type]) ? $expected_counts[$entity_type] : 0; + $this->assertSame($expected_count, $real_count, "Found $real_count $entity_type entities, expected $expected_count."); + } + + $plugin_manager = \Drupal::service('plugin.manager.migration'); + /** @var \Drupal\migrate\Plugin\Migration[] $all_migrations */ + $all_migrations = $plugin_manager->createInstancesByTag('Drupal ' . $version); + foreach ($all_migrations as $migration) { + $id_map = $migration->getIdMap(); + foreach ($id_map as $source_id => $map) { + // Convert $source_id into a keyless array so that + // \Drupal\migrate\Plugin\migrate\id_map\Sql::getSourceHash() works as + // expected. + $source_id_values = array_values(unserialize($source_id)); + $row = $id_map->getRowBySource($source_id_values); + $destination = serialize($id_map->currentDestination()); + $message = "Migration of $source_id to $destination as part of the {$migration->id()} migration. The source row status is " . $row['source_row_status']; + // A completed migration should have maps with + // MigrateIdMapInterface::STATUS_IGNORED or + // MigrateIdMapInterface::STATUS_IMPORTED. + $this->assertNotSame(MigrateIdMapInterface::STATUS_FAILED, $row['source_row_status'], $message); + $this->assertNotSame(MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $row['source_row_status'], $message); + } + } + } + } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php index 9af8ec7..2b9382e 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php @@ -99,6 +99,24 @@ protected function getEntityCounts() { /** * {@inheritdoc} */ + protected function getEntityCountsIncremental() { + $counts = $this->getEntityCounts(); + $counts['block_content'] = 3; + $counts['comment'] = 7; + $counts['entity_view_display'] = 53; + $counts['entity_view_mode'] = 14; + $counts['file'] = 9; + $counts['menu_link_content'] = 6; + $counts['node'] = 18; + $counts['taxonomy_term'] = 9; + $counts['user'] = 8; + $counts['view'] = 16; + return $counts; + } + + /** + * {@inheritdoc} + */ protected function getAvailablePaths() { return [ 'aggregator', diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php index 6156c5e..839b27c 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php @@ -99,6 +99,21 @@ protected function getEntityCounts() { /** * {@inheritdoc} */ + protected function getEntityCountsIncremental() { + $counts = $this->getEntityCounts(); + $counts['block_content'] = 2; + $counts['comment'] = 2; + $counts['file'] = 4; + $counts['menu_link_content'] = 9; + $counts['node'] = 6; + $counts['taxonomy_term'] = 19; + $counts['user'] = 5; + return $counts; + } + + /** + * {@inheritdoc} + */ protected function getAvailablePaths() { return [ 'aggregator', diff --git a/core/modules/node/node.libraries.yml b/core/modules/node/node.libraries.yml index 59947a2..a420112 100644 --- a/core/modules/node/node.libraries.yml +++ b/core/modules/node/node.libraries.yml @@ -20,6 +20,7 @@ drupal.node.preview: - core/jquery - core/jquery.once - core/drupal + - core/drupal.dialog - core/drupal.form drupal.content_types: diff --git a/core/modules/node/node.links.menu.yml b/core/modules/node/node.links.menu.yml index f83d760..db34628 100644 --- a/core/modules/node/node.links.menu.yml +++ b/core/modules/node/node.links.menu.yml @@ -9,4 +9,3 @@ node.add_page: node.add_menu: class: \Drupal\node\Plugin\Menu\NodeMenuLink deriver: \Drupal\node\Plugin\Derivative\NodeMenuLinkDeriver - diff --git a/core/modules/node/node.preview.es6.js b/core/modules/node/node.preview.es6.js index abd670a..ef67b68 100644 --- a/core/modules/node/node.preview.es6.js +++ b/core/modules/node/node.preview.es6.js @@ -45,9 +45,9 @@ } } - const $preview = $(context).find('.content').once('node-preview'); + const $preview = $(context).once('node-preview'); if ($(context).find('.node-preview-container').length) { - $preview.on('click.preview', 'a:not([href^=#], #edit-backlink, #toolbar-administration a)', clickPreviewModal); + $preview.on('click.preview', 'a:not([href^="#"], .node-preview-container a)', clickPreviewModal); } }, detach(context, settings, trigger) { diff --git a/core/modules/node/node.preview.js b/core/modules/node/node.preview.js index f18af28..60c8dfd 100644 --- a/core/modules/node/node.preview.js +++ b/core/modules/node/node.preview.js @@ -29,9 +29,9 @@ } } - var $preview = $(context).find('.content').once('node-preview'); + var $preview = $(context).once('node-preview'); if ($(context).find('.node-preview-container').length) { - $preview.on('click.preview', 'a:not([href^=#], #edit-backlink, #toolbar-administration a)', clickPreviewModal); + $preview.on('click.preview', 'a:not([href^="#"], .node-preview-container a)', clickPreviewModal); } }, detach: function detach(context, settings, trigger) { diff --git a/core/modules/node/src/Plugin/Derivative/NodeMenuLinkDeriver.php b/core/modules/node/src/Plugin/Derivative/NodeMenuLinkDeriver.php index 27df952..3dc3ddb 100644 --- a/core/modules/node/src/Plugin/Derivative/NodeMenuLinkDeriver.php +++ b/core/modules/node/src/Plugin/Derivative/NodeMenuLinkDeriver.php @@ -7,11 +7,6 @@ use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -/** - * Class NodeMenuDeriver. Provides menu links to add nodes of existing bundles. - * - * @package Drupal\node\Plugin\Derivative - */ class NodeMenuLinkDeriver extends DeriverBase implements ContainerDeriverInterface { /** diff --git a/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php b/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php new file mode 100644 index 0000000..8eb7a26 --- /dev/null +++ b/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php @@ -0,0 +1,60 @@ + 'filtered_html', + 'name' => 'Filtered HTML', + ]); + $filtered_html_format->save(); + + $this->drupalCreateContentType(['type' => 'test']); + + $user = $this->drupalCreateUser([ + 'access content', + 'edit own test content', + 'create test content', + $filtered_html_format->getPermissionName(), + ]); + $this->drupalLogin($user); + } + + /** + * Test the behavior of clicking preview links. + */ + public function testPreviewLinks() { + $assertSession = $this->assertSession(); + $this->drupalPostForm('node/add/test', [ + 'title[0][value]' => 'Test node', + 'body[0][value]' => 'Anchor linkNormal link', + ], t('Preview')); + $this->clickLink('Anchor link'); + $assertSession->pageTextNotContains('Leave preview?'); + $this->clickLink('Normal link'); + $assertSession->pageTextContains('Leave preview?'); + $this->click('button:contains("Leave preview")'); + $this->assertStringEndsWith('/foo', $this->getUrl()); + } + +} diff --git a/core/modules/path/migrations/d6_url_alias.yml b/core/modules/path/migrations/d6_url_alias.yml index fcf2036..68c4d96 100644 --- a/core/modules/path/migrations/d6_url_alias.yml +++ b/core/modules/path/migrations/d6_url_alias.yml @@ -26,7 +26,9 @@ process: source: src delimiter: / - + # If the source path has no slashes return a dummy default value. plugin: extract + default: 'INVALID_NID' index: - 1 - diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index 65cb14b..cd62bea 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -89,7 +89,7 @@ public function postSave($update) { */ public static function generateSampleValue(FieldDefinitionInterface $field_definition) { $random = new Random(); - $values['alias'] = str_replace(' ', '-', strtolower($random->sentences(3))); + $values['alias'] = '/' . str_replace(' ', '-', strtolower($random->sentences(3))); return $values; } diff --git a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php index 092d5ac..0b22cc3 100644 --- a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php +++ b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php @@ -103,6 +103,14 @@ public function testUrlAlias() { ]; $path = \Drupal::service('path.alias_storage')->load($conditions); $this->assertPath('3', $conditions, $path); + + $path = \Drupal::service('path.alias_storage')->load(['alias' => '/source-noslash']); + $conditions = [ + 'source' => '/admin', + 'alias' => '/source-noslash', + 'langcode' => 'und', + ]; + $this->assertPath('2', $conditions, $path); } /** diff --git a/core/modules/path/tests/src/Kernel/PathItemTest.php b/core/modules/path/tests/src/Kernel/PathItemTest.php index d7627c3..224a842 100644 --- a/core/modules/path/tests/src/Kernel/PathItemTest.php +++ b/core/modules/path/tests/src/Kernel/PathItemTest.php @@ -189,6 +189,18 @@ public function testPathItem() { // Change the alias for the second node to a different one and try again. $second_node->get('path')->alias = '/foobar'; $this->assertFalse($node->get('path')->equals($second_node->get('path'))); + + // Test the generateSampleValue() method. + $node = Node::create([ + 'title' => $this->randomString(), + 'type' => 'foo', + 'path' => ['alias' => '/foo'], + ]); + $node->save(); + $path_field = $node->get('path'); + $path_field->generateSampleItems(); + $node->save(); + $this->assertStringStartsWith('/', $node->get('path')->alias); } } diff --git a/core/modules/quickedit/css/quickedit.icons.theme.css b/core/modules/quickedit/css/quickedit.icons.theme.css index 7845416..fc5b31f 100644 --- a/core/modules/quickedit/css/quickedit.icons.theme.css +++ b/core/modules/quickedit/css/quickedit.icons.theme.css @@ -48,7 +48,7 @@ font-size: 1em; } .quickedit .icon-pencil { - margin-left: .5em; + margin-left: 0.5em; padding-left: 1.5em; } diff --git a/core/modules/quickedit/css/quickedit.theme.css b/core/modules/quickedit/css/quickedit.theme.css index 69fb7f3..cc65e8f 100644 --- a/core/modules/quickedit/css/quickedit.theme.css +++ b/core/modules/quickedit/css/quickedit.theme.css @@ -45,7 +45,7 @@ margin: 0; } .quickedit-form .form-wrapper { - margin: .5em; + margin: 0.5em; } /** @@ -55,35 +55,35 @@ opacity: 0; } .quickedit-animate-default { - -webkit-transition: all .4s ease; - transition: all .4s ease; + -webkit-transition: all 0.4s ease; + transition: all 0.4s ease; } .quickedit-animate-slow { - -webkit-transition: all .6s ease; - transition: all .6s ease; + -webkit-transition: all 0.6s ease; + transition: all 0.6s ease; } .quickedit-animate-delay-veryfast { - -webkit-transition-delay: .05s; - transition-delay: .05s; + -webkit-transition-delay: 0.05s; + transition-delay: 0.05s; } .quickedit-animate-delay-fast { - -webkit-transition-delay: .2s; - transition-delay: .2s; + -webkit-transition-delay: 0.2s; + transition-delay: 0.2s; } .quickedit-animate-disable-width { -webkit-transition: width 0s; transition: width 0s; } .quickedit-animate-only-visibility { - -webkit-transition: opacity .2s ease; - transition: opacity .2s ease; + -webkit-transition: opacity 0.2s ease; + transition: opacity 0.2s ease; } /** * In-place editors that don't use a popup. */ .quickedit-validation-errors .messages.error { - box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5); + box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, 0.5); background-color: white; } @@ -208,8 +208,8 @@ margin: 0; opacity: 1; padding: 0.345em; - -webkit-transition: opacity .1s ease; - transition: opacity .1s ease; + -webkit-transition: opacity 0.1s ease; + transition: opacity 0.1s ease; } .quickedit-button[aria-hidden="true"] { visibility: hidden; diff --git a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php index a74e8b2..e631f5d 100644 --- a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php +++ b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php @@ -65,6 +65,10 @@ public function getDerivativeDefinitions($base_plugin_definition) { if (!isset($this->derivatives)) { // Add in the default plugin configuration and the resource type. foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->isInternal()) { + continue; + } + $this->derivatives[$entity_type_id] = [ 'id' => 'entity:' . $entity_type_id, 'entity_type' => $entity_type_id, diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php index 012942c..92dd505 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -7,7 +7,6 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\rest\Plugin\ResourceInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -26,13 +25,6 @@ class RequestHandler implements ContainerInjectionInterface { /** - * The resource configuration storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $resourceStorage; - - /** * The config factory. * * @var \Drupal\Core\Config\ConfigFactoryInterface @@ -49,15 +41,12 @@ class RequestHandler implements ContainerInjectionInterface { /** * Creates a new RequestHandler instance. * - * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage - * The resource configuration storage. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. * @param \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface $serializer * The serializer. */ - public function __construct(EntityStorageInterface $entity_storage, ConfigFactoryInterface $config_factory, SerializerInterface $serializer) { - $this->resourceStorage = $entity_storage; + public function __construct(ConfigFactoryInterface $config_factory, SerializerInterface $serializer) { $this->configFactory = $config_factory; $this->serializer = $serializer; } @@ -67,7 +56,6 @@ public function __construct(EntityStorageInterface $entity_storage, ConfigFactor */ public static function create(ContainerInterface $container) { return new static( - $container->get('entity_type.manager')->getStorage('rest_resource_config'), $container->get('config.factory'), $container->get('serializer') ); @@ -80,19 +68,17 @@ public static function create(ContainerInterface $container) { * The route match. * @param \Symfony\Component\HttpFoundation\Request $request * The HTTP request object. + * @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config + * REST resource config entity ID. * - * @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface + * @return \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response * The REST resource response. */ - public function handle(RouteMatchInterface $route_match, Request $request) { - $resource_config_id = $route_match->getRouteObject()->getDefault('_rest_resource_config'); - /** @var \Drupal\rest\RestResourceConfigInterface $resource_config */ - $resource_config = $this->resourceStorage->load($resource_config_id); - - $response = $this->delegateToRestResourcePlugin($route_match, $request, $resource_config->getResourcePlugin()); + public function handle(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) { + $response = $this->delegateToRestResourcePlugin($route_match, $request, $_rest_resource_config->getResourcePlugin()); if ($response instanceof CacheableResponseInterface) { - $response->addCacheableDependency($resource_config); + $response->addCacheableDependency($_rest_resource_config); // Add global rest settings config's cache tag, for BC flags. // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions() // @see \Drupal\rest\EventSubscriber\RestConfigSubscriber diff --git a/core/modules/rest/src/Routing/ResourceRoutes.php b/core/modules/rest/src/Routing/ResourceRoutes.php index a8449f9..81bf789 100644 --- a/core/modules/rest/src/Routing/ResourceRoutes.php +++ b/core/modules/rest/src/Routing/ResourceRoutes.php @@ -133,6 +133,12 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_ } $route->setOption('_auth', $rest_resource_config->getAuthenticationProviders($method)); $route->setDefault('_rest_resource_config', $rest_resource_config->id()); + $parameters = $route->getOption('parameters') ?: []; + $route->setOption('parameters', $parameters + [ + '_rest_resource_config' => [ + 'type' => 'entity:' . $rest_resource_config->getEntityTypeId(), + ], + ]); $collection->add("rest.$name", $route); } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceRestTestCoverageTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceRestTestCoverageTest.php index d10d55c..6421472 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceRestTestCoverageTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceRestTestCoverageTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Tests\BrowserTestBase; /** @@ -45,15 +46,11 @@ protected function setUp() { $this->definitions = $this->container->get('entity_type.manager')->getDefinitions(); - // Remove definitions for which the REST resource plugin definition was - // removed via hook_rest_resource_alter(). Entity types which are never - // exposed via REST also don't need test coverage. - $resource_plugin_ids = array_keys($this->container->get('plugin.manager.rest')->getDefinitions()); - foreach (array_keys($this->definitions) as $entity_type_id) { - if (!in_array("entity:$entity_type_id", $resource_plugin_ids, TRUE)) { - unset($this->definitions[$entity_type_id]); - } - } + // Entity types marked as "internal" are not exposed by the entity REST + // resource plugin and hence also don't need test coverage. + $this->definitions = array_filter($this->definitions, function (EntityTypeInterface $entity_type) { + return !$entity_type->isInternal(); + }); } /** diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index d1a5ac0..669382d 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -21,7 +21,6 @@ use Drupal\Tests\rest\Functional\ResourceTestBase; use GuzzleHttp\RequestOptions; use Psr\Http\Message\ResponseInterface; -use Symfony\Component\Routing\Exception\RouteNotFoundException; /** * Even though there is the generic EntityResource, it's necessary for every @@ -286,6 +285,23 @@ protected function getNormalizedPatchEntity() { } /** + * Gets the second normalized POST entity. + * + * Entity types can have non-sequential IDs, and in that case the second + * entity created for POST testing needs to be able to specify a different ID. + * + * @see ::testPost + * @see ::getNormalizedPostEntity + * + * @return array + * An array structure as returned by ::getNormalizedPostEntity(). + */ + protected function getSecondNormalizedPostEntity() { + // Return the values of the "parent" method by default. + return $this->getNormalizedPostEntity(); + } + + /** * Gets the normalized POST entity with random values for its unique fields. * * @see ::testPost @@ -703,18 +719,6 @@ public function testGet() { $path = str_replace('987654321', '{' . static::$entityTypeId . '}', $url->setAbsolute()->setOptions(['base_url' => '', 'query' => []])->toString()); $message = 'The "' . static::$entityTypeId . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . static::$entityTypeId . '.GET")'; $this->assertResourceErrorResponse(404, $message, $response); - - // BC: Format-specific GET routes are deprecated. They are available on both - // new and old sites, but trigger deprecation notices. - $bc_route = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format, $url->getRouteParameters(), $url->getOptions()); - $bc_route->setUrlGenerator($this->container->get('url_generator')); - $this->assertSame($url->toString(TRUE)->getGeneratedUrl(), $bc_route->toString(TRUE)->getGeneratedUrl()); - // Verify no format-specific GET BC routes are created for other formats. - $other_format = static::$format === 'json' ? 'xml' : 'json'; - $bc_route_other_format = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . $other_format, $url->getRouteParameters(), $url->getOptions()); - $bc_route_other_format->setUrlGenerator($this->container->get('url_generator')); - $this->setExpectedException(RouteNotFoundException::class); - $bc_route_other_format->toString(TRUE); } /** @@ -778,7 +782,7 @@ public function testPost() { // Try with all of the following request bodies. $unparseable_request_body = '!{>}<'; $parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); - $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); + $parseable_valid_request_body_2 = $this->serializer->encode($this->getSecondNormalizedPostEntity(), static::$format); $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity(), 'label'), static::$format); $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format); $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format); @@ -902,11 +906,7 @@ public function testPost() { // contains the serialized created entity. $created_entity = $this->entityStorage->loadUnchanged(static::$firstCreatedEntityId); $created_entity_normalization = $this->serializer->normalize($created_entity, static::$format, ['account' => $this->account]); - // @todo Remove this if-test in https://www.drupal.org/node/2543726: execute - // its body unconditionally. - if (static::$entityTypeId !== 'taxonomy_term') { - $this->assertSame($created_entity_normalization, $this->serializer->decode((string) $response->getBody(), static::$format)); - } + $this->assertSame($created_entity_normalization, $this->serializer->decode((string) $response->getBody(), static::$format)); // Assert that the entity was indeed created using the POSTed values. foreach ($this->getNormalizedPostEntity() as $field_name => $field_normalization) { // Some top-level keys in the normalization may not be fields on the @@ -1397,9 +1397,7 @@ protected static function getModifiedEntityForPatchTesting(EntityInterface $enti // PathItem::generateSampleValue() doesn't set a PID, which causes // PathItem::postSave() to fail. Keep the PID (and other properties), // just modify the alias. - $value = $field->getValue(); - $value['alias'] = str_replace(' ', '-', strtolower((new Random())->sentences(3))); - $field->setValue($value); + $field->alias = str_replace(' ', '-', strtolower((new Random())->sentences(3))); break; default: $original_field = clone $field; diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonAnonTest.php index ff047d4..1deeec0 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonAnonTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonAnonTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; +use Drupal\Tests\rest\Functional\EntityResource\FormatSpecificGetBcRouteTestTrait; /** * @group rest @@ -10,6 +11,7 @@ class EntityTestJsonAnonTest extends EntityTestResourceTestBase { use AnonResourceTestTrait; + use FormatSpecificGetBcRouteTestTrait; /** * {@inheritdoc} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php index d14ec38..0c82ea9 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php @@ -5,11 +5,13 @@ use Drupal\entity_test\Entity\EntityTest; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; +use Drupal\Tests\Traits\ExpectDeprecationTrait; use Drupal\user\Entity\User; abstract class EntityTestResourceTestBase extends EntityResourceTestBase { use BcTimestampNormalizerUnixTestTrait; + use ExpectDeprecationTrait; /** * {@inheritdoc} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestXmlAnonTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestXmlAnonTest.php index e278083..52dcad4 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestXmlAnonTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestXmlAnonTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; +use Drupal\Tests\rest\Functional\EntityResource\FormatSpecificGetBcRouteTestTrait; use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait; /** @@ -11,6 +12,7 @@ class EntityTestXmlAnonTest extends EntityTestResourceTestBase { use AnonResourceTestTrait; + use FormatSpecificGetBcRouteTestTrait; use XmlEntityNormalizationQuirksTrait; /** diff --git a/core/modules/rest/tests/src/Functional/EntityResource/FormatSpecificGetBcRouteTestTrait.php b/core/modules/rest/tests/src/Functional/EntityResource/FormatSpecificGetBcRouteTestTrait.php new file mode 100644 index 0000000..54bf816 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/FormatSpecificGetBcRouteTestTrait.php @@ -0,0 +1,50 @@ +provisionEntityResource(); + $url = $this->getEntityResourceUrl(); + + // BC: Format-specific GET routes are deprecated. They are available on both + // new and old sites, but trigger deprecation notices. + $bc_route = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format, $url->getRouteParameters(), $url->getOptions()); + $bc_route->setUrlGenerator($this->container->get('url_generator')); + $this->expectDeprecation(sprintf("The 'rest.entity.entity_test.GET.%s' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead.", static::$format)); + $this->assertSame($url->toString(TRUE)->getGeneratedUrl(), $bc_route->toString(TRUE)->getGeneratedUrl()); + } + + /** + * @group legacy + * + * @see \Drupal\rest\Plugin\ResourceBase::routes + */ + public function testNoFormatSpecificGetBcRouteForOtherFormats() { + $this->setExpectedException(RouteNotFoundException::class); + + $this->provisionEntityResource(); + $url = $this->getEntityResourceUrl(); + + // Verify no format-specific GET BC routes are created for other formats. + $other_format = static::$format === 'json' ? 'xml' : 'json'; + $bc_route_other_format = Url::fromRoute('rest.entity.entity_test.GET.' . $other_format, $url->getRouteParameters(), $url->getOptions()); + $bc_route_other_format->setUrlGenerator($this->container->get('url_generator')); + $bc_route_other_format->toString(TRUE); + } + +} diff --git a/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php b/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php index 3ad421c..de75f3e 100644 --- a/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php +++ b/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php @@ -4,7 +4,6 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ImmutableConfig; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Routing\RouteMatch; use Drupal\KernelTests\KernelTestBase; use Drupal\rest\Plugin\ResourceBase; @@ -42,12 +41,11 @@ class RequestHandlerTest extends KernelTestBase { */ public function setUp() { parent::setUp(); - $this->entityStorage = $this->prophesize(EntityStorageInterface::class); $config_factory = $this->prophesize(ConfigFactoryInterface::class); $config_factory->get('rest.settings') ->willReturn($this->prophesize(ImmutableConfig::class)->reveal()); $serializer = $this->prophesize(SerializerInterface::class); - $this->requestHandler = new RequestHandler($this->entityStorage->reveal(), $config_factory->reveal(), $serializer->reveal()); + $this->requestHandler = new RequestHandler($config_factory->reveal(), $serializer->reveal()); } /** @@ -67,18 +65,17 @@ public function testHandle() { $config->getCacheContexts()->willReturn([]); $config->getCacheTags()->willReturn([]); $config->getCacheMaxAge()->willReturn(12); - $this->entityStorage->load('restplugin')->willReturn($config->reveal()); // Response returns NULL this time because response from plugin is not // a ResourceResponse so it is passed through directly. - $response = $this->requestHandler->handle($route_match, $request); + $response = $this->requestHandler->handle($route_match, $request, $config->reveal()); $this->assertEquals(NULL, $response); // Response will return a ResourceResponse this time. $response = new ResourceResponse([]); $resource->get(NULL, $request) ->willReturn($response); - $handler_response = $this->requestHandler->handle($route_match, $request); + $handler_response = $this->requestHandler->handle($route_match, $request, $config->reveal()); $this->assertEquals($response, $handler_response); // We will call the patch method this time. @@ -88,7 +85,7 @@ public function testHandle() { $resource->patch(NULL, $request) ->shouldBeCalledTimes(1) ->willReturn($response); - $handler_response = $this->requestHandler->handle($route_match, $request); + $handler_response = $this->requestHandler->handle($route_match, $request, $config->reveal()); $this->assertEquals($response, $handler_response); } diff --git a/core/modules/settings_tray/css/settings_tray.motion.css b/core/modules/settings_tray/css/settings_tray.motion.css index aab7c1d..820f708 100644 --- a/core/modules/settings_tray/css/settings_tray.motion.css +++ b/core/modules/settings_tray/css/settings_tray.motion.css @@ -8,12 +8,12 @@ /* Transition the edit icon in the toolbar. */ #toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before { - transition: all .7s ease; + transition: all 0.7s ease; } /* Transition the editables on the page, their contextual links and their hover states. */ .dialog-off-canvas-main-canvas .contextual, .dialog-off-canvas-main-canvas .js-settings-tray-edit-mode .settings-tray-editable, .dialog-off-canvas-main-canvas.js-off-canvas-dialog-open .js-settings-tray-edit-mode .settings-tray-editable { - transition: all .7s ease; + transition: all 0.7s ease; } diff --git a/core/modules/settings_tray/css/settings_tray.theme.css b/core/modules/settings_tray/css/settings_tray.theme.css index 7a0e308..d906bda 100644 --- a/core/modules/settings_tray/css/settings_tray.theme.css +++ b/core/modules/settings_tray/css/settings_tray.theme.css @@ -9,7 +9,7 @@ /* Style the edit mode toolbar and tabs. */ #toolbar-bar.js-settings-tray-edit-mode { - background-image: linear-gradient(to bottom, #0A7BC1, #0a6eb4); + background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4); } .js-settings-tray-edit-mode .toolbar-item:not(.toolbar-icon-edit) { color: #999; @@ -22,8 +22,8 @@ .toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item, .toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active, .toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:focus { - background-color: #0066A1; - background-image: linear-gradient(to bottom, #0066A1, #005b98); + background-color: #0066a1; + background-image: linear-gradient(to bottom, #0066a1, #005b98); color: #eee; text-shadow: none; font-weight: bold; @@ -37,8 +37,8 @@ } /* Make the hover of the active state the same as the inactive state. */ .toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { - background-color: #0066A1; - background-image: linear-gradient(to bottom, #0066A1, #005b98); + background-color: #0066a1; + background-image: linear-gradient(to bottom, #0066a1, #005b98); color: #fff; } /* Make the inactive icon grey. */ diff --git a/core/modules/settings_tray/css/settings_tray.toolbar.css b/core/modules/settings_tray/css/settings_tray.toolbar.css index 7fe5eef..4d4880e 100644 --- a/core/modules/settings_tray/css/settings_tray.toolbar.css +++ b/core/modules/settings_tray/css/settings_tray.toolbar.css @@ -20,8 +20,8 @@ /* Style both the edit and editing states of the contextual links toggle tab. */ .toolbar-icon-edit.toolbar-item { - background-color: #0066A1; - background-image: linear-gradient(to bottom, #0066A1, #005b98); + background-color: #0066a1; + background-image: linear-gradient(to bottom, #0066a1, #005b98); color: #eee; text-shadow: 0 1px hsla(0, 0%, 0%, 0.5); font-weight: 700; diff --git a/core/modules/settings_tray/settings_tray.info.yml b/core/modules/settings_tray/settings_tray.info.yml index bb2e36c..c3adfd2 100644 --- a/core/modules/settings_tray/settings_tray.info.yml +++ b/core/modules/settings_tray/settings_tray.info.yml @@ -1,7 +1,7 @@ name: 'Settings Tray' type: module description: 'Provides a sidebar to configure blocks on the page.' -package: Core (Experimental) +package: Core version: VERSION core: 8.x dependencies: diff --git a/core/modules/settings_tray/settings_tray.links.contextual.yml b/core/modules/settings_tray/settings_tray.links.contextual.yml index 5534ab2..c62fa98 100644 --- a/core/modules/settings_tray/settings_tray.links.contextual.yml +++ b/core/modules/settings_tray/settings_tray.links.contextual.yml @@ -1,6 +1,6 @@ settings_tray.block_configure: title: 'Quick edit' - route_name: 'entity.block.off_canvas_form' + route_name: 'entity.block.settings_tray_form' group: 'block' options: attributes: diff --git a/core/modules/settings_tray/settings_tray.module b/core/modules/settings_tray/settings_tray.module index 208e1ef..c9c5dd9 100644 --- a/core/modules/settings_tray/settings_tray.module +++ b/core/modules/settings_tray/settings_tray.module @@ -7,12 +7,9 @@ use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Url; -use Drupal\settings_tray\Block\BlockEntityOffCanvasForm; -use Drupal\settings_tray\Form\SystemBrandingOffCanvasForm; -use Drupal\settings_tray\Form\SystemMenuOffCanvasForm; use Drupal\block\entity\Block; use Drupal\block\BlockInterface; +use Drupal\settings_tray\Block\BlockEntitySettingTrayForm; /** * Implements hook_help(). @@ -98,8 +95,8 @@ function settings_tray_block_view_alter(array &$build) { function settings_tray_entity_type_build(array &$entity_types) { /* @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ $entity_types['block'] - ->setFormClass('off_canvas', BlockEntityOffCanvasForm::class) - ->setLinkTemplate('off_canvas-form', '/admin/structure/block/manage/{block}/off-canvas'); + ->setFormClass('settings_tray', BlockEntitySettingTrayForm::class) + ->setLinkTemplate('settings_tray-form', '/admin/structure/block/manage/{block}/settings-tray'); } /** @@ -168,47 +165,10 @@ function settings_tray_toolbar_alter(&$items) { */ function settings_tray_block_alter(&$definitions) { foreach ($definitions as &$definition) { - // If a block plugin already defines its own 'settings_tray' form, use that - // form instead of specifying one here. - if (isset($definition['forms']['settings_tray'])) { - continue; - } - - switch ($definition['id']) { - // Use specialized forms for certain blocks that do not yet provide the - // form with their own annotation. - // @todo Move these into the corresponding block plugin annotations in - // https://www.drupal.org/node/2896356. - case 'system_menu_block': - $definition['forms']['settings_tray'] = SystemMenuOffCanvasForm::class; - break; - - case 'system_branding_block': - $definition['forms']['settings_tray'] = SystemBrandingOffCanvasForm::class; - break; - - // No off-canvas form for the page title block, despite it having - // contextual links: it's too confusing that you're editing configuration, - // not content, so the title itself cannot actually be changed. - // @todo Move these into the corresponding block plugin annotations in - // https://www.drupal.org/node/2896356. - case 'page_title_block': - $definition['forms']['settings_tray'] = FALSE; - break; - - case 'system_main_block': - $definition['forms']['settings_tray'] = FALSE; - break; - - case 'help_block': - $definition['forms']['settings_tray'] = FALSE; - break; - - // Otherwise, use the block plugin's normal form rather than - // a custom form for Settings Tray. - default: - $definition['forms']['settings_tray'] = $definition['class']; - break; + // If a block plugin does not define its own 'settings_tray' form, use the + // plugin class itself. + if (!isset($definition['forms']['settings_tray'])) { + $definition['forms']['settings_tray'] = $definition['class']; } } } diff --git a/core/modules/settings_tray/settings_tray.routing.yml b/core/modules/settings_tray/settings_tray.routing.yml index f8e2bfe..370fc7f 100644 --- a/core/modules/settings_tray/settings_tray.routing.yml +++ b/core/modules/settings_tray/settings_tray.routing.yml @@ -1,9 +1,16 @@ -entity.block.off_canvas_form: - path: '/admin/structure/block/manage/{block}/off-canvas' +entity.block.settings_tray_form: + path: '/admin/structure/block/manage/{block}/settings-tray' defaults: - _entity_form: 'block.off_canvas' - _title_callback: '\Drupal\settings_tray\Block\BlockEntityOffCanvasForm::title' + _entity_form: 'block.settings_tray' + _title_callback: '\Drupal\settings_tray\Block\BlockEntitySettingTrayForm::title' requirements: _permission: 'administer blocks' _access_block_plugin_has_settings_tray_form: 'TRUE' _access_block_has_overrides_settings_tray_form: 'TRUE' + +# Deprecated. +# @see entity.block.settings_tray_form +# @see \Drupal\settings_tray\RouteProcessor\BlockEntityOffCanvasFormRouteProcessorBC +# @todo Remove in Drupal 9.0.0. +entity.block.off_canvas_form: + path: '' diff --git a/core/modules/settings_tray/settings_tray.services.yml b/core/modules/settings_tray/settings_tray.services.yml index 7c57e95..9f61546 100644 --- a/core/modules/settings_tray/settings_tray.services.yml +++ b/core/modules/settings_tray/settings_tray.services.yml @@ -7,3 +7,12 @@ services: class: Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck tags: - { name: access_check, applies_to: _access_block_plugin_has_settings_tray_form } + + # BC layers. + # @todo Remove in Drupal 9.0.0. + settings_tray.route_processor_off_canvas_form_bc: + class: \Drupal\settings_tray\RouteProcessor\BlockEntityOffCanvasFormRouteProcessorBC + arguments: ['@router.route_provider'] + public: false + tags: + - { name: route_processor_outbound } diff --git a/core/modules/settings_tray/src/Block/BlockEntityOffCanvasForm.php b/core/modules/settings_tray/src/Block/BlockEntityOffCanvasForm.php deleted file mode 100644 index 2c6f80d..0000000 --- a/core/modules/settings_tray/src/Block/BlockEntityOffCanvasForm.php +++ /dev/null @@ -1,194 +0,0 @@ - once - // https://www.drupal.org/node/2359901 is fixed. - return $this->t('Configure @block', ['@block' => $block->getPlugin()->getPluginDefinition()['admin_label']]); - } - - /** - * {@inheritdoc} - */ - public function form(array $form, FormStateInterface $form_state) { - $form = parent::form($form, $form_state); - - // Create link to full block form. - $query = []; - if ($destination = $this->getRequest()->query->get('destination')) { - $query['destination'] = $destination; - } - $form['advanced_link'] = [ - '#type' => 'link', - '#title' => $this->t('Advanced block options'), - '#url' => $this->entity->toUrl('edit-form', ['query' => $query]), - '#weight' => 1000, - ]; - - // Remove the ID and region elements. - unset($form['id'], $form['region'], $form['settings']['admin_label']); - - if (isset($form['settings']['label_display']) && isset($form['settings']['label'])) { - // Only show the label input if the label will be shown on the page. - $form['settings']['label_display']['#weight'] = -100; - $form['settings']['label']['#states']['visible'] = [ - ':input[name="settings[label_display]"]' => ['checked' => TRUE], - ]; - - // Relabel to "Block title" because on the front-end this may be confused - // with page title. - $form['settings']['label']['#title'] = $this->t("Block title"); - $form['settings']['label_display']['#title'] = $this->t("Display block title"); - } - return $form; - } - - /** - * {@inheritdoc} - */ - protected function actions(array $form, FormStateInterface $form_state) { - $actions = parent::actions($form, $form_state); - $actions['submit']['#value'] = $this->t('Save @block', ['@block' => $this->entity->getPlugin()->getPluginDefinition()['admin_label']]); - $actions['delete']['#access'] = FALSE; - return $actions; - } - - /** - * {@inheritdoc} - */ - protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) { - // Do not display the visibility. - return []; - } - - /** - * {@inheritdoc} - */ - protected function validateVisibility(array $form, FormStateInterface $form_state) { - // Intentionally empty. - } - - /** - * {@inheritdoc} - */ - protected function submitVisibility(array $form, FormStateInterface $form_state) { - // Intentionally empty. - } - - /** - * {@inheritdoc} - */ - protected function getPluginForm(BlockPluginInterface $block) { - if ($block instanceof PluginWithFormsInterface) { - return $this->pluginFormFactory->createInstance($block, 'settings_tray', 'configure'); - } - return $block; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $form = parent::buildForm($form, $form_state); - $form['actions']['submit']['#ajax'] = [ - 'callback' => '::submitFormDialog', - ]; - $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; - - // static::submitFormDialog() requires data-drupal-selector to be the same - // between the various Ajax requests. A bug in - // \Drupal\Core\Form\FormBuilder prevents that from happening unless - // $form['#id'] is also the same. Normally, #id is set to a unique HTML ID - // via Html::getUniqueId(), but here we bypass that in order to work around - // the data-drupal-selector bug. This is okay so long as we assume that this - // form only ever occurs once on a page. - // @todo Remove this workaround once https://www.drupal.org/node/2897377 is - // fixed. - $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); - - return $form; - } - - /** - * Submit form dialog #ajax callback. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return \Drupal\Core\Ajax\AjaxResponse - * An AJAX response that display validation error messages or redirects - * to a URL - * - * @todo Repalce this callback with generic trait in - * https://www.drupal.org/node/2896535. - */ - public function submitFormDialog(array &$form, FormStateInterface $form_state) { - $response = new AjaxResponse(); - if ($form_state->hasAnyErrors()) { - $form['status_messages'] = [ - '#type' => 'status_messages', - '#weight' => -1000, - ]; - $command = new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form); - } - else { - if ($redirect_url = $this->getRedirectUrl()) { - $command = new RedirectCommand($redirect_url->setAbsolute()->toString()); - } - else { - // Settings Tray always provides a destination. - throw new \Exception("No destination provided by Settings Tray form"); - } - } - return $response->addCommand($command); - } - - /** - * Gets the form's redirect URL from 'destination' provide in the request. - * - * @return \Drupal\Core\Url|null - * The redirect URL or NULL if dialog should just be closed. - */ - protected function getRedirectUrl() { - // \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly - // because it will use if 'destination' is not in the query - // string. - if ($this->getRequest()->query->has('destination') && $destination = $this->getRedirectDestination()->get()) { - return Url::fromUserInput('/' . $destination); - } - } - -} diff --git a/core/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php b/core/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php new file mode 100644 index 0000000..fd44b0c --- /dev/null +++ b/core/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php @@ -0,0 +1,194 @@ + once + // https://www.drupal.org/node/2359901 is fixed. + return $this->t('Configure @block', ['@block' => $block->getPlugin()->getPluginDefinition()['admin_label']]); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + // Create link to full block form. + $query = []; + if ($destination = $this->getRequest()->query->get('destination')) { + $query['destination'] = $destination; + } + $form['advanced_link'] = [ + '#type' => 'link', + '#title' => $this->t('Advanced block options'), + '#url' => $this->entity->toUrl('edit-form', ['query' => $query]), + '#weight' => 1000, + ]; + + // Remove the ID and region elements. + unset($form['id'], $form['region'], $form['settings']['admin_label']); + + if (isset($form['settings']['label_display']) && isset($form['settings']['label'])) { + // Only show the label input if the label will be shown on the page. + $form['settings']['label_display']['#weight'] = -100; + $form['settings']['label']['#states']['visible'] = [ + ':input[name="settings[label_display]"]' => ['checked' => TRUE], + ]; + + // Relabel to "Block title" because on the front-end this may be confused + // with page title. + $form['settings']['label']['#title'] = $this->t("Block title"); + $form['settings']['label_display']['#title'] = $this->t("Display block title"); + } + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Save @block', ['@block' => $this->entity->getPlugin()->getPluginDefinition()['admin_label']]); + $actions['delete']['#access'] = FALSE; + return $actions; + } + + /** + * {@inheritdoc} + */ + protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) { + // Do not display the visibility. + return []; + } + + /** + * {@inheritdoc} + */ + protected function validateVisibility(array $form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + protected function submitVisibility(array $form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + protected function getPluginForm(BlockPluginInterface $block) { + if ($block instanceof PluginWithFormsInterface) { + return $this->pluginFormFactory->createInstance($block, 'settings_tray', 'configure'); + } + return $block; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $form['actions']['submit']['#ajax'] = [ + 'callback' => '::submitFormDialog', + ]; + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + + // static::submitFormDialog() requires data-drupal-selector to be the same + // between the various Ajax requests. A bug in + // \Drupal\Core\Form\FormBuilder prevents that from happening unless + // $form['#id'] is also the same. Normally, #id is set to a unique HTML ID + // via Html::getUniqueId(), but here we bypass that in order to work around + // the data-drupal-selector bug. This is okay so long as we assume that this + // form only ever occurs once on a page. + // @todo Remove this workaround once https://www.drupal.org/node/2897377 is + // fixed. + $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); + + return $form; + } + + /** + * Submit form dialog #ajax callback. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * An AJAX response that display validation error messages or redirects + * to a URL + * + * @todo Repalce this callback with generic trait in + * https://www.drupal.org/node/2896535. + */ + public function submitFormDialog(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + if ($form_state->hasAnyErrors()) { + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -1000, + ]; + $command = new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form); + } + else { + if ($redirect_url = $this->getRedirectUrl()) { + $command = new RedirectCommand($redirect_url->setAbsolute()->toString()); + } + else { + // Settings Tray always provides a destination. + throw new \Exception("No destination provided by Settings Tray form"); + } + } + return $response->addCommand($command); + } + + /** + * Gets the form's redirect URL from 'destination' provide in the request. + * + * @return \Drupal\Core\Url|null + * The redirect URL or NULL if dialog should just be closed. + */ + protected function getRedirectUrl() { + // \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly + // because it will use if 'destination' is not in the query + // string. + if ($this->getRequest()->query->has('destination') && $destination = $this->getRedirectDestination()->get()) { + return Url::fromUserInput('/' . $destination); + } + } + +} diff --git a/core/modules/settings_tray/src/Form/SystemBrandingOffCanvasForm.php b/core/modules/settings_tray/src/Form/SystemBrandingOffCanvasForm.php deleted file mode 100644 index 94c8052..0000000 --- a/core/modules/settings_tray/src/Form/SystemBrandingOffCanvasForm.php +++ /dev/null @@ -1,114 +0,0 @@ -configFactory = $config_factory; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('config.factory') - ); - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form = $this->plugin->buildConfigurationForm($form, $form_state); - - $form['block_branding']['#type'] = 'details'; - $form['block_branding']['#weight'] = 10; - - // Unset links to Site Information form, we can make these changes here. - unset($form['block_branding']['use_site_name']['#description'], $form['block_branding']['use_site_slogan']['#description']); - - $site_config = $this->configFactory->getEditable('system.site'); - // Load the immutable config to load the overrides. - $site_config_immutable = $this->configFactory->get('system.site'); - $form['site_information'] = [ - '#type' => 'details', - '#title' => t('Site details'), - '#open' => TRUE, - '#access' => AccessResult::allowedIf(!$site_config_immutable->hasOverrides('name') && !$site_config_immutable->hasOverrides('slogan')), - ]; - $form['site_information']['site_name'] = [ - '#type' => 'textfield', - '#title' => t('Site name'), - '#default_value' => $site_config->get('name'), - '#required' => TRUE, - ]; - $form['site_information']['site_slogan'] = [ - '#type' => 'textfield', - '#title' => t('Slogan'), - '#default_value' => $site_config->get('slogan'), - '#description' => t("How this is used depends on your site's theme."), - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { - $this->plugin->validateConfigurationForm($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - $site_config = $this->configFactory->get('system.site'); - if (AccessResult::allowedIf(!$site_config->hasOverrides('name') && !$site_config->hasOverrides('slogan'))->isAllowed()) { - $site_info = $form_state->getValue('site_information'); - $this->configFactory->getEditable('system.site') - ->set('name', $site_info['site_name']) - ->set('slogan', $site_info['site_slogan']) - ->save(); - } - - $this->plugin->submitConfigurationForm($form, $form_state); - } - -} diff --git a/core/modules/settings_tray/src/Form/SystemMenuOffCanvasForm.php b/core/modules/settings_tray/src/Form/SystemMenuOffCanvasForm.php deleted file mode 100644 index 1f8114f..0000000 --- a/core/modules/settings_tray/src/Form/SystemMenuOffCanvasForm.php +++ /dev/null @@ -1,184 +0,0 @@ -menuStorage = $menu_storage; - $this->entityTypeManager = $entity_type_manager; - $this->stringTranslation = $string_translation; - $this->configFactory = $config_factory; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager')->getStorage('menu'), - $container->get('entity_type.manager'), - $container->get('string_translation'), - $container->get('config.factory') - ); - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form = $this->plugin->buildConfigurationForm([], $form_state); - // Move the menu levels section to the bottom. - $form['menu_levels']['#weight'] = 100; - - $form['entity_form'] = [ - '#type' => 'details', - '#title' => $this->t('Edit menu %label', ['%label' => $this->menu->label()]), - '#open' => TRUE, - '#access' => AccessResult::allowedIf(!$this->hasMenuOverrides()), - ]; - $form['entity_form'] += $this->getEntityForm($this->menu)->buildForm([], $form_state); - - // Print the menu link titles as text instead of a link. - if (!empty($form['entity_form']['links']['links'])) { - foreach (Element::children($form['entity_form']['links']['links']) as $child) { - $title = $form['entity_form']['links']['links'][$child]['title'][1]['#title']; - $form['entity_form']['links']['links'][$child]['title'][1] = ['#markup' => $title]; - } - } - // Change the header text. - $form['entity_form']['links']['links']['#header'][0] = $this->t('Link'); - $form['entity_form']['links']['links']['#header'][1]['data'] = $this->t('On'); - - // Remove the label, ID, description, and buttons from the entity form. - unset($form['entity_form']['label'], $form['entity_form']['id'], $form['entity_form']['description'], $form['entity_form']['actions']); - // Since the overview form is further nested than expected, update the - // #parents. See \Drupal\menu_ui\MenuForm::form(). - $form_state->set('menu_overview_form_parents', ['settings', 'entity_form', 'links']); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { - $this->plugin->validateConfigurationForm($form, $form_state); - if (!$this->hasMenuOverrides()) { - $this->getEntityForm($this->menu)->validateForm($form, $form_state); - } - } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - $this->plugin->submitConfigurationForm($form, $form_state); - if (!$this->hasMenuOverrides()) { - $this->getEntityForm($this->menu)->submitForm($form, $form_state); - $this->menu->save(); - } - } - - /** - * Gets the entity form for this menu. - * - * @param \Drupal\system\MenuInterface $menu - * The menu entity. - * - * @return \Drupal\Core\Entity\EntityFormInterface - * The entity form. - */ - protected function getEntityForm(MenuInterface $menu) { - $entity_form = $this->entityTypeManager->getFormObject('menu', 'edit'); - $entity_form->setEntity($menu); - return $entity_form; - } - - /** - * {@inheritdoc} - */ - public function setPlugin(PluginInspectionInterface $plugin) { - $this->plugin = $plugin; - $this->menu = $this->menuStorage->loadOverrideFree($this->plugin->getDerivativeId()); - } - - /** - * Determines if the menu has configuration overrides. - * - * @return bool - * TRUE if the menu has configuration overrides, otherwise FALSE. - */ - protected function hasMenuOverrides() { - // @todo Replace the following with $this->menu->hasOverrides() in https://www.drupal.org/project/drupal/issues/2910353 - // and remove this function. - return $this->configFactory->get($this->menu->getEntityType() - ->getConfigPrefix() . '.' . $this->menu->id())->hasOverrides(); - } - -} diff --git a/core/modules/settings_tray/src/RouteProcessor/BlockEntityOffCanvasFormRouteProcessorBC.php b/core/modules/settings_tray/src/RouteProcessor/BlockEntityOffCanvasFormRouteProcessorBC.php new file mode 100644 index 0000000..48935ae --- /dev/null +++ b/core/modules/settings_tray/src/RouteProcessor/BlockEntityOffCanvasFormRouteProcessorBC.php @@ -0,0 +1,65 @@ +routeProvider = $route_provider; + } + + /** + * {@inheritdoc} + */ + public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { + if ($route_name === 'entity.block.off_canvas_form') { + $redirected_route_name = 'entity.block.settings_tray_form'; + @trigger_error(sprintf("The '%s' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the '%s' route instead.", $route_name, $redirected_route_name), E_USER_DEPRECATED); + static::overwriteRoute($route, $this->routeProvider->getRouteByName($redirected_route_name)); + } + } + + /** + * Overwrites one route's metadata with the other's. + * + * @param \Symfony\Component\Routing\Route $target_route + * The route whose metadata to overwrite. + * @param \Symfony\Component\Routing\Route $source_route + * The route whose metadata to read from. + * + * @see \Symfony\Component\Routing\Route + */ + protected static function overwriteRoute(Route $target_route, Route $source_route) { + $target_route->setPath($source_route->getPath()); + $target_route->setDefaults($source_route->getDefaults()); + $target_route->setRequirements($source_route->getRequirements()); + $target_route->setOptions($source_route->getOptions()); + $target_route->setHost($source_route->getHost()); + $target_route->setSchemes($source_route->getSchemes()); + $target_route->setMethods($source_route->getMethods()); + } + +} diff --git a/core/modules/settings_tray/tests/src/Functional/BcRoutesTest.php b/core/modules/settings_tray/tests/src/Functional/BcRoutesTest.php new file mode 100644 index 0000000..6750b1c --- /dev/null +++ b/core/modules/settings_tray/tests/src/Functional/BcRoutesTest.php @@ -0,0 +1,33 @@ +placeBlock('system_powered_by_block'); + $url_for_current_route = Url::fromRoute('entity.block.settings_tray_form', ['block' => $block->id()])->toString(TRUE)->getGeneratedUrl(); + $url_for_bc_route = Url::fromRoute('entity.block.off_canvas_form', ['block' => $block->id()])->toString(TRUE)->getGeneratedUrl(); + $this->assertSame($url_for_current_route, $url_for_bc_route); + } + +} diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php index c677d08..7475dab 100644 --- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php @@ -84,7 +84,7 @@ public function testBlocks($theme, $block_plugin, $new_page_text, $element_selec $link = $page->find('css', "$block_selector .contextual-links li a"); $this->assertEquals('Quick edit', $link->getText(), "'Quick edit' is the first contextual link for the block."); - $this->assertContains("/admin/structure/block/manage/$block_id/off-canvas?destination=user/2", $link->getAttribute('href')); + $this->assertContains("/admin/structure/block/manage/$block_id/settings-tray?destination=user/2", $link->getAttribute('href')); if (isset($toolbar_item)) { // Check that you can open a toolbar tray and it will be closed after @@ -525,7 +525,7 @@ public function testCustomBlockLinks() { $href = array_search('Quick edit', $link_labels); $this->assertEquals('', $href); $href = array_search('Quick edit settings', $link_labels); - $this->assertTrue(strstr($href, '/admin/structure/block/manage/custom/off-canvas?destination=user/2') !== FALSE); + $this->assertTrue(strstr($href, '/admin/structure/block/manage/custom/settings-tray?destination=user/2') !== FALSE); } /** diff --git a/core/modules/shortcut/css/shortcut.theme.css b/core/modules/shortcut/css/shortcut.theme.css index 73d58c6..3fe631c 100644 --- a/core/modules/shortcut/css/shortcut.theme.css +++ b/core/modules/shortcut/css/shortcut.theme.css @@ -32,11 +32,11 @@ margin-right: 0.3em; } .shortcut-action__message { - background: #000000; + background: #000; background: rgba(0, 0, 0, 0.5); border-radius: 5px; padding: 0 5px; - color: #ffffff; + color: #fff; display: inline-block; margin-left: 0.3em; /* LTR */ opacity: 0; diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 6670e5a..0e0ea2a 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -661,6 +661,10 @@ function simpletest_clean_environment() { else { drupal_set_message(t('Clear results is disabled and the test results table will not be cleared.'), 'warning'); } + + // Detect test classes that have been added, renamed or deleted. + \Drupal::cache()->delete('simpletest'); + \Drupal::cache()->delete('simpletest_phpunit'); } /** diff --git a/core/modules/simpletest/simpletest.services.yml b/core/modules/simpletest/simpletest.services.yml index 326cf38..8b645de 100644 --- a/core/modules/simpletest/simpletest.services.yml +++ b/core/modules/simpletest/simpletest.services.yml @@ -1,9 +1,4 @@ services: test_discovery: class: Drupal\simpletest\TestDiscovery - arguments: ['@app.root', '@class_loader', '@module_handler'] - cache_context.test_discovery: - class: Drupal\simpletest\Cache\Context\TestDiscoveryCacheContext - arguments: ['@test_discovery', '@private_key'] - tags: - - { name: cache.context} + arguments: ['@app.root', '@class_loader', '@module_handler', '@?cache.discovery'] diff --git a/core/modules/simpletest/src/Cache/Context/TestDiscoveryCacheContext.php b/core/modules/simpletest/src/Cache/Context/TestDiscoveryCacheContext.php deleted file mode 100644 index e3d5cf3..0000000 --- a/core/modules/simpletest/src/Cache/Context/TestDiscoveryCacheContext.php +++ /dev/null @@ -1,94 +0,0 @@ -testDiscovery = $test_discovery; - $this->privateKey = $private_key; - } - - /** - * {@inheritdoc} - */ - public static function getLabel() { - return t('Test discovery'); - } - - /** - * {@inheritdoc} - */ - public function getContext() { - if (empty($this->hash)) { - $tests = $this->testDiscovery->getTestClasses(); - $this->hash = $this->hash(serialize($tests)); - } - return $this->hash; - } - - /** - * {@inheritdoc} - */ - public function getCacheableMetadata() { - return new CacheableMetadata(); - } - - /** - * Hashes the given string. - * - * @param string $identifier - * The string to be hashed. - * - * @return string - * The hash. - */ - protected function hash($identifier) { - return hash('sha256', $this->privateKey->get() . Settings::getHashSalt() . $identifier); - } - -} diff --git a/core/modules/simpletest/src/Form/SimpletestTestForm.php b/core/modules/simpletest/src/Form/SimpletestTestForm.php index 69f31e4..0b704f9 100644 --- a/core/modules/simpletest/src/Form/SimpletestTestForm.php +++ b/core/modules/simpletest/src/Form/SimpletestTestForm.php @@ -110,10 +110,6 @@ public function buildForm(array $form, FormStateInterface $form_state) { ]; $form['tests'] = [ - '#cache' => [ - 'keys' => ['simpletest_ui_table'], - 'contexts' => ['test_discovery'], - ], '#type' => 'table', '#id' => 'simpletest-form-table', '#tableselect' => TRUE, diff --git a/core/modules/simpletest/src/InstallerTestBase.php b/core/modules/simpletest/src/InstallerTestBase.php index 96d98e0..ae7c734 100644 --- a/core/modules/simpletest/src/InstallerTestBase.php +++ b/core/modules/simpletest/src/InstallerTestBase.php @@ -210,7 +210,7 @@ protected function setUpRequirementsProblem() { // By default, skip the "recommended PHP version" warning on older test // environments. This allows the installer to be tested consistently on // both recommended PHP versions and older (but still supported) versions. - if (version_compare(phpversion(), DRUPAL_RECOMMENDED_PHP) < 0) { + if (version_compare(phpversion(), '7.0') < 0) { $this->continueOnExpectedWarnings(['PHP']); } } diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php index da3d3af..9e6c32c 100644 --- a/core/modules/simpletest/src/TestDiscovery.php +++ b/core/modules/simpletest/src/TestDiscovery.php @@ -6,6 +6,7 @@ use Doctrine\Common\Reflection\StaticReflectionParser; use Drupal\Component\Annotation\Reflection\MockFileFinder; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\simpletest\Exception\MissingGroupException; @@ -24,11 +25,11 @@ class TestDiscovery { protected $classLoader; /** - * Statically cached list of test classes. + * Backend for caching discovery results. * - * @var array + * @var \Drupal\Core\Cache\CacheBackendInterface */ - protected $testClasses; + protected $cacheBackend; /** * Cached map of all test namespaces to respective directories. @@ -69,11 +70,14 @@ class TestDiscovery { * \Symfony\Component\ClassLoader\ApcClassLoader. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * (optional) Backend for caching discovery results. */ - public function __construct($root, $class_loader, ModuleHandlerInterface $module_handler) { + public function __construct($root, $class_loader, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend = NULL) { $this->root = $root; $this->classLoader = $class_loader; $this->moduleHandler = $module_handler; + $this->cacheBackend = $cache_backend; } /** @@ -155,9 +159,9 @@ public function getTestClasses($extension = NULL, array $types = []) { $reader = new SimpleAnnotationReader(); $reader->addNamespace('Drupal\\simpletest\\Annotation'); - if (!isset($extension) && empty($types)) { - if (!empty($this->testClasses)) { - return $this->testClasses; + if (!isset($extension)) { + if ($this->cacheBackend && $cache = $this->cacheBackend->get('simpletest:discovery:classes')) { + return $cache->data; } } $list = []; @@ -211,8 +215,10 @@ public function getTestClasses($extension = NULL, array $types = []) { // Allow modules extending core tests to disable originals. $this->moduleHandler->alter('simpletest', $list); - if (!isset($extension) && empty($types)) { - $this->testClasses = $list; + if (!isset($extension)) { + if ($this->cacheBackend) { + $this->cacheBackend->set('simpletest:discovery:classes', $list); + } } if ($types) { diff --git a/core/modules/simpletest/tests/src/Kernel/Cache/Context/TestDiscoveryCacheContextTest.php b/core/modules/simpletest/tests/src/Kernel/Cache/Context/TestDiscoveryCacheContextTest.php deleted file mode 100644 index 25b2af9..0000000 --- a/core/modules/simpletest/tests/src/Kernel/Cache/Context/TestDiscoveryCacheContextTest.php +++ /dev/null @@ -1,53 +0,0 @@ -getMockBuilder(TestDiscovery::class) - ->setMethods(['getTestClasses']) - ->disableOriginalConstructor() - ->getMock(); - // Set getTestClasses() to return different results on subsequent calls. - // This emulates changed tests in the filesystem. - $discovery->expects($this->any()) - ->method('getTestClasses') - ->willReturnOnConsecutiveCalls( - ['group1' => ['Test']], - ['group2' => ['Test2']] - ); - - // Make our cache context object. - $cache_context = new TestDiscoveryCacheContext($discovery, $this->container->get('private_key')); - - // Generate a context hash. - $context_hash = $cache_context->getContext(); - - // Since the context stores the hash, we have to reset it. - $hash_ref = new \ReflectionProperty($cache_context, 'hash'); - $hash_ref->setAccessible(TRUE); - $hash_ref->setValue($cache_context, NULL); - - // And then assert that we did not generate the same hash for different - // content. - $this->assertNotSame($context_hash, $cache_context->getContext()); - } - -} diff --git a/core/modules/system/css/components/progress.module.css b/core/modules/system/css/components/progress.module.css index 574c693..566fa86 100644 --- a/core/modules/system/css/components/progress.module.css +++ b/core/modules/system/css/components/progress.module.css @@ -27,7 +27,7 @@ .progress__percentage { color: #555; overflow: hidden; - font-size: .875em; + font-size: 0.875em; margin-top: 0.2em; } .progress__description { diff --git a/core/modules/system/css/components/system-status-report-counters.css b/core/modules/system/css/components/system-status-report-counters.css index 1a4e240..2a2ef6b 100644 --- a/core/modules/system/css/components/system-status-report-counters.css +++ b/core/modules/system/css/components/system-status-report-counters.css @@ -5,11 +5,11 @@ .system-status-report-counters__item { width: 100%; - padding: .5em 0; + padding: 0.5em 0; text-align: center; white-space: nowrap; background-color: rgba(0, 0, 0, 0.063); - margin-bottom: .5em; + margin-bottom: 0.5em; } @media screen and (min-width: 60em) { diff --git a/core/modules/system/css/system.admin.css b/core/modules/system/css/system.admin.css index cd5807b..a6b9e56 100644 --- a/core/modules/system/css/system.admin.css +++ b/core/modules/system/css/system.admin.css @@ -117,7 +117,7 @@ small .admin-link:after { text-transform: none; } .system-modules td details a { - color: #5C5C5B; + color: #5c5c5b; border: 0; } .system-modules td details { @@ -234,7 +234,7 @@ small .admin-link:after { background-image: url(../../../misc/icons/e29700/warning.svg); } .system-status-report__entry__value { - padding: 1em .5em; + padding: 1em 0.5em; } /** diff --git a/core/modules/system/js/system.es6.js b/core/modules/system/js/system.es6.js index 5100766..3f365d1 100644 --- a/core/modules/system/js/system.es6.js +++ b/core/modules/system/js/system.es6.js @@ -23,7 +23,10 @@ attach(context) { // List of fields IDs on which to bind the event listener. // Create an array of IDs to use with jQuery. - Object.keys(drupalSettings.copyFieldValue || {}).forEach(ids.push); + Object.keys(drupalSettings.copyFieldValue || {}).forEach((element) => { + ids.push(element); + }); + if (ids.length) { // Listen to value:copy events on all dependent fields. // We have to use body and not document because of the way jQuery events diff --git a/core/modules/system/js/system.js b/core/modules/system/js/system.js index 4bcfe34..f23a69a 100644 --- a/core/modules/system/js/system.js +++ b/core/modules/system/js/system.js @@ -10,7 +10,10 @@ Drupal.behaviors.copyFieldValue = { attach: function attach(context) { - Object.keys(drupalSettings.copyFieldValue || {}).forEach(ids.push); + Object.keys(drupalSettings.copyFieldValue || {}).forEach(function (element) { + ids.push(element); + }); + if (ids.length) { $('body').once('copy-field-values').on('value:copy', this.valueTargetCopyHandler); diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index 27e148d..84a1f78 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -173,7 +173,7 @@ public function systemAdminMenuBlockPage() { } /** - * Provides a single block from the add content menu as a page + * Provides a single block from the add content menu as a page. */ public function systemAddMenuBlockPage() { return $this->systemManager->getBlockContents(); diff --git a/core/modules/system/src/Form/PrepareModulesEntityUninstallForm.php b/core/modules/system/src/Form/PrepareModulesEntityUninstallForm.php index fb73b99..079e574 100644 --- a/core/modules/system/src/Form/PrepareModulesEntityUninstallForm.php +++ b/core/modules/system/src/Form/PrepareModulesEntityUninstallForm.php @@ -227,7 +227,7 @@ public static function deleteContentEntities($entity_type_id, &$context) { $storage->delete($entities); } // Sometimes deletes cause secondary deletes. For example, deleting a - // taxonomy term can cause it's children to be be deleted too. + // taxonomy term can cause its children to be be deleted too. $context['sandbox']['progress'] = $context['sandbox']['max'] - $storage->getQuery()->count()->execute(); // Inform the batch engine that we are not finished and provide an diff --git a/core/modules/system/src/Form/SystemBrandingOffCanvasForm.php b/core/modules/system/src/Form/SystemBrandingOffCanvasForm.php new file mode 100644 index 0000000..35e68308 --- /dev/null +++ b/core/modules/system/src/Form/SystemBrandingOffCanvasForm.php @@ -0,0 +1,112 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = $this->plugin->buildConfigurationForm($form, $form_state); + + $form['block_branding']['#type'] = 'details'; + $form['block_branding']['#weight'] = 10; + + // Unset links to Site Information form, we can make these changes here. + unset($form['block_branding']['use_site_name']['#description'], $form['block_branding']['use_site_slogan']['#description']); + + $site_config = $this->configFactory->getEditable('system.site'); + // Load the immutable config to load the overrides. + $site_config_immutable = $this->configFactory->get('system.site'); + $form['site_information'] = [ + '#type' => 'details', + '#title' => t('Site details'), + '#open' => TRUE, + '#access' => AccessResult::allowedIf(!$site_config_immutable->hasOverrides('name') && !$site_config_immutable->hasOverrides('slogan')), + ]; + $form['site_information']['site_name'] = [ + '#type' => 'textfield', + '#title' => t('Site name'), + '#default_value' => $site_config->get('name'), + '#required' => TRUE, + ]; + $form['site_information']['site_slogan'] = [ + '#type' => 'textfield', + '#title' => t('Slogan'), + '#default_value' => $site_config->get('slogan'), + '#description' => t("How this is used depends on your site's theme."), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->plugin->validateConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $site_config = $this->configFactory->get('system.site'); + if (AccessResult::allowedIf(!$site_config->hasOverrides('name') && !$site_config->hasOverrides('slogan'))->isAllowed()) { + $site_info = $form_state->getValue('site_information'); + $this->configFactory->getEditable('system.site') + ->set('name', $site_info['site_name']) + ->set('slogan', $site_info['site_slogan']) + ->save(); + } + + $this->plugin->submitConfigurationForm($form, $form_state); + } + +} diff --git a/core/modules/system/src/Form/SystemMenuOffCanvasForm.php b/core/modules/system/src/Form/SystemMenuOffCanvasForm.php new file mode 100644 index 0000000..82390bc --- /dev/null +++ b/core/modules/system/src/Form/SystemMenuOffCanvasForm.php @@ -0,0 +1,182 @@ +menuStorage = $menu_storage; + $this->entityTypeManager = $entity_type_manager; + $this->stringTranslation = $string_translation; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager')->getStorage('menu'), + $container->get('entity_type.manager'), + $container->get('string_translation'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = $this->plugin->buildConfigurationForm([], $form_state); + // Move the menu levels section to the bottom. + $form['menu_levels']['#weight'] = 100; + + $form['entity_form'] = [ + '#type' => 'details', + '#title' => $this->t('Edit menu %label', ['%label' => $this->menu->label()]), + '#open' => TRUE, + '#access' => AccessResult::allowedIf(!$this->hasMenuOverrides()), + ]; + $form['entity_form'] += $this->getEntityForm($this->menu)->buildForm([], $form_state); + + // Print the menu link titles as text instead of a link. + if (!empty($form['entity_form']['links']['links'])) { + foreach (Element::children($form['entity_form']['links']['links']) as $child) { + $title = $form['entity_form']['links']['links'][$child]['title'][1]['#title']; + $form['entity_form']['links']['links'][$child]['title'][1] = ['#markup' => $title]; + } + } + // Change the header text. + $form['entity_form']['links']['links']['#header'][0] = $this->t('Link'); + $form['entity_form']['links']['links']['#header'][1]['data'] = $this->t('On'); + + // Remove the label, ID, description, and buttons from the entity form. + unset($form['entity_form']['label'], $form['entity_form']['id'], $form['entity_form']['description'], $form['entity_form']['actions']); + // Since the overview form is further nested than expected, update the + // #parents. See \Drupal\menu_ui\MenuForm::form(). + $form_state->set('menu_overview_form_parents', ['settings', 'entity_form', 'links']); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->plugin->validateConfigurationForm($form, $form_state); + if (!$this->hasMenuOverrides()) { + $this->getEntityForm($this->menu)->validateForm($form, $form_state); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->plugin->submitConfigurationForm($form, $form_state); + if (!$this->hasMenuOverrides()) { + $this->getEntityForm($this->menu)->submitForm($form, $form_state); + $this->menu->save(); + } + } + + /** + * Gets the entity form for this menu. + * + * @param \Drupal\system\MenuInterface $menu + * The menu entity. + * + * @return \Drupal\Core\Entity\EntityFormInterface + * The entity form. + */ + protected function getEntityForm(MenuInterface $menu) { + $entity_form = $this->entityTypeManager->getFormObject('menu', 'edit'); + $entity_form->setEntity($menu); + return $entity_form; + } + + /** + * {@inheritdoc} + */ + public function setPlugin(PluginInspectionInterface $plugin) { + $this->plugin = $plugin; + $this->menu = $this->menuStorage->loadOverrideFree($this->plugin->getDerivativeId()); + } + + /** + * Determines if the menu has configuration overrides. + * + * @return bool + * TRUE if the menu has configuration overrides, otherwise FALSE. + */ + protected function hasMenuOverrides() { + // @todo Replace the following with $this->menu->hasOverrides() in https://www.drupal.org/project/drupal/issues/2910353 + // and remove this function. + return $this->configFactory->get($this->menu->getEntityType() + ->getConfigPrefix() . '.' . $this->menu->id())->hasOverrides(); + } + +} diff --git a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php index 3305285..fdb869a 100644 --- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php @@ -15,7 +15,10 @@ * * @Block( * id = "system_branding_block", - * admin_label = @Translation("Site branding") + * admin_label = @Translation("Site branding"), + * forms = { + * "settings_tray" = "Drupal\system\Form\SystemBrandingOffCanvasForm", + * }, * ) */ class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInterface { diff --git a/core/modules/system/src/Plugin/Block/SystemMainBlock.php b/core/modules/system/src/Plugin/Block/SystemMainBlock.php index beba38d..92f4430 100644 --- a/core/modules/system/src/Plugin/Block/SystemMainBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMainBlock.php @@ -10,7 +10,10 @@ * * @Block( * id = "system_main_block", - * admin_label = @Translation("Main page content") + * admin_label = @Translation("Main page content"), + * forms = { + * "settings_tray" = FALSE, + * }, * ) */ class SystemMainBlock extends BlockBase implements MainContentBlockPluginInterface { diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index 4489eb3..a551ca6 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -16,7 +16,10 @@ * id = "system_menu_block", * admin_label = @Translation("Menu"), * category = @Translation("Menus"), - * deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock" + * deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock", + * forms = { + * "settings_tray" = "\Drupal\system\Form\SystemMenuOffCanvasForm", + * }, * ) */ class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface { diff --git a/core/modules/system/src/Tests/System/ThemeTest.php b/core/modules/system/src/Tests/System/ThemeTest.php index a02680e..78fac8e 100644 --- a/core/modules/system/src/Tests/System/ThemeTest.php +++ b/core/modules/system/src/Tests/System/ThemeTest.php @@ -390,7 +390,7 @@ public function testUninstallingThemes() { // Check that bartik can be uninstalled now. $this->assertRaw('Uninstall Bartik theme', 'A link to uninstall the Bartik theme does appear on the theme settings page.'); - // Check that the classy theme still can't be uninstalled as neither of it's + // Check that the classy theme still can't be uninstalled as neither of its // base themes have been. $this->assertNoRaw('Uninstall Classy theme', 'A link to uninstall the Classy theme does not appear on the theme settings page.'); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 0fd4b26..1838326 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -189,12 +189,19 @@ function system_requirements($phase) { // If PHP is old, it's not safe to continue with the requirements check. return $requirements; } - // @todo Warn about specific end dates for our PHP 5.5, 5.6, and 7.0 support - // once each is set. - // @see https://www.drupal.org/project/drupal/issues/2927344 if ((version_compare($phpversion, DRUPAL_RECOMMENDED_PHP) < 0) && ($phase === 'install' || $phase === 'runtime')) { - $requirements['php']['description'] = t('Your PHP installation is running version %version. Support for this version will be dropped in a future Drupal release. Upgrade to PHP version %recommended or higher to ensure your site continues to receive Drupal updates and remains secure. See PHP\'s version support documentation and the Drupal 8 PHP requirements handbook page for more information.', ['%version' => $phpversion, '%recommended' => DRUPAL_RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/8/system-requirements/php']); - $requirements['php']['severity'] = REQUIREMENT_WARNING; + // Warn if still on PHP 5. If at least PHP 7.0, relax from "warning" to + // "info", and show it at runtime only, to not scare users while installing. + if (version_compare($phpversion, '7.0') < 0) { + $requirements['php']['description'] = t('Drupal will drop support for this version on March 6, 2019. Upgrade to PHP version %recommended or higher to ensure your site can receive updates and remain secure. See PHP\'s version support documentation and the Drupal 8 PHP requirements handbook page for more information.', ['%recommended' => DRUPAL_RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/8/system-requirements/php']); + $requirements['php']['severity'] = REQUIREMENT_WARNING; + } + else { + if ($phase === 'runtime') { + $requirements['php']['description'] = t('It is recommended to upgrade to PHP version %recommended or higher for the best ongoing support. See PHP\'s version support documentation and the Drupal 8 PHP requirements handbook page for more information.', ['%recommended' => DRUPAL_RECOMMENDED_PHP, ':php_requirements' => 'https://www.drupal.org/docs/8/system-requirements/php']); + $requirements['php']['severity'] = REQUIREMENT_INFO; + } + } } // Suggest to update to at least 5.5.21 or 5.6.5 for disabling multiple @@ -2085,9 +2092,23 @@ function system_update_8501() { // also to code using the latest installed definition. $installed_entity_type = $definition_update_manager->getEntityType($entity_type_id); $revision_metadata_keys = $installed_entity_type->get('revision_metadata_keys'); - $revision_metadata_keys['revision_default'] = $field_name; - $installed_entity_type->set('revision_metadata_keys', $revision_metadata_keys); - $definition_update_manager->updateEntityType($installed_entity_type); + + if (!isset($revision_metadata_keys['revision_default'])) { + // Update the property holding the required revision metadata keys, + // which is used by the BC layer for retrieving the revision metadata + // keys. + // @see \Drupal\Core\Entity\ContentEntityType::getRevisionMetadataKeys(). + $required_revision_metadata_keys = $installed_entity_type->get('requiredRevisionMetadataKeys'); + $required_revision_metadata_keys['revision_default'] = $field_name; + $installed_entity_type->set('requiredRevisionMetadataKeys', $required_revision_metadata_keys); + + // Update the revision metadata keys to add the new required revision + // metadata key "revision_default". + $revision_metadata_keys['revision_default'] = $required_revision_metadata_keys['revision_default']; + $installed_entity_type->set('revision_metadata_keys', $revision_metadata_keys); + + $definition_update_manager->updateEntityType($installed_entity_type); + } $storage_definition = BaseFieldDefinition::create('boolean') ->setLabel(t('Default revision')) @@ -2103,5 +2124,14 @@ function system_update_8501() { $definition_update_manager ->installFieldStorageDefinition($field_name, $entity_type_id, $entity_type_id, $storage_definition); } + else { + $variables = ['@entity_type_label' => $entity_type->getLabel()]; + if ($field_name === 'revision_default') { + \Drupal::logger('system')->error('An existing "Default revision" field was found for the @entity_type_label entity type, but no "revision_default" revision metadata key was found in its definition.', $variables); + } + else { + \Drupal::logger('system')->error('An existing "Default revision" field was found for the @entity_type_label entity type.', $variables); + } + } } } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 964d7fe..bb82d4b 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -963,7 +963,12 @@ function system_get_info($type, $name = NULL) { /** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */ $module_list = \Drupal::service('extension.list.module'); if (isset($name)) { - return $module_list->getExtensionInfo($name); + try { + return $module_list->getExtensionInfo($name); + } + catch (\InvalidArgumentException $e) { + return []; + } } else { return $module_list->getAllInstalledInfo(); diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index b6c91a4..abe1a2e 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -39,12 +39,12 @@ system.admin: _permission: 'access administration pages' system.add: - path: '/add' + path: '/content/add' defaults: _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' - _title: 'Add any content' + _title: 'Add' requirements: - _permission: 'access administration pages' + _node_add_access: 'node' system.admin_structure: path: '/admin/structure' diff --git a/core/modules/system/templates/status-report-grouped.html.twig b/core/modules/system/templates/status-report-grouped.html.twig index bd34e5a..8f5481c 100644 --- a/core/modules/system/templates/status-report-grouped.html.twig +++ b/core/modules/system/templates/status-report-grouped.html.twig @@ -23,7 +23,7 @@

{{ group.title }}

{% for requirement in group.items %} -
+
{% set summary_classes = [ 'system-status-report__status-title', diff --git a/core/modules/system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz b/core/modules/system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz new file mode 100644 index 0000000..b45aeca --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz @@ -0,0 +1,542 @@ +UvZdrupal-8.4.0.bare.standard.phpr'y +bGG94c0ۀ`P]qfwq:޶\/ZrQo?m֡{hZAd3: +=)7>2f|97K͞e23eF.Q2w2VFNetcyq2#|c,DX?4Ƕ曎Ə]w'zZ ?SsXoh?# t]?2 Y["lꛗE1B<{bYfuo;;367Wo.NWL2~ tdԮL@O3Chb!—Y&w|Vs4#mgAuC׆mC.2HՃOyn[HפZ $YQe +Iڐ`+~Kih'!. ]{d|[ +|*C2O|C~_'8ԉ[Wi 81nNGѩ0/v4OmfWAH‡J J;Ah ͏jv74MOOmlT5?&qsJ7aM"6uэc4q.Qw?p:)b"_`w6?x 4pi\wgAjf cRl B۳ QLVыxM#rݚ$f&Y}^6ƏȬ_1Î_FGFr^/}~J-G?&P>ΖmZNN`r$xvΚ 7x+Z5Um6VFh'G+SǯSu<18!v֕`BӎUa[1Q=[K|d6htgOEZu4z` :6;xp62]  ~U +s?P P ~U"{Q_Q_b0 0 ~UrWJ}J(0}"D3)=(_ !֕x |oRQo2\FsьYtR`0`&6ׅe6|?pZ}އ7CR +Aנwd=m.6y{`=eјCn8U:t('LIdgwǃwȇ'Ǖ@:'#±>z\B&jkufnO MChZWi7s(X/Nsh{_ +EUaqa/c7㶦 GfraˣK#Ɗ/fC6{u̪ZecJE{F]ͤ$֚=*6|d]Huvv}kkTېW +-?c(QyC!W .XV^7sp7> X^1&I3Ŵ=K?ϕlj7$ ,~-xmEK5qw ^c?!>fi)~?attѷ?!%] ݞOĒ +1r#۟5G>P%KǪ@H",<YAZX3L#r~lYp\񍊿(o,M~/~|#5iቐ/F mr*q* ӱ +q4PYI:h<*D*) +Q-̠nyn(W;מK1텴R!b 6tK)%9Euc4Le#;Y͉,!yW(%PMOSD 0C T5@%{Rٻ:n5}ݺ1OPI4鉔Г%]70W//lBn]c3XsFȗ&FP%@p^ր̋:` j1[<8P|n +0biM]  E5A{WLrTm[m߽z J3Ԡ=FZ5&hv3^a˴N}kk0?&*e""S4NqQ}on#^&9_:oMB1AUt]"%\ %tCGH I_ݺf/SJN%ۙ˯}T+HYuqqq)+]0p_f5w&XIp9_93FPh 4/bAusϰ41CqjWǯ&iw|tߑJOqS*lB*8cdo>j'j_Qǯ񤽻 ɨL#HׁBp<'XiѐK5 3Gk:wm!{5!u]hu7s +7d{r]S\w:YE g<^㬨PjaXgkZkVfl<ԀhS@>7.;nR7*|"[Oei^@Հip6eʫ GFwͺ7ߒk_q{Mkt+wsY6ٷ}%|b<2xi<deĩfū7WN#Hwl%j+S~CnaH^:Ѹ5z(Ͳmf W5yr$)KffeEpMc4/kEq]>ߩޔ$WKZ/:D +dܼ4ʱ0]?\ut94I`$(P*9asKU1JL(;+v[Eq۝{~ҧ[Smigkw +=r,>DaGĭ^ڌ[}ߐ ~BeUIzVSK>;PM +Oa?ꔢɣj5y>.]ˈ}Uīы$ `nv(e 3TeHcN> G1ekh 7*K%ĝ+܃vPbD(>HqL  ˫؋c@CZE(|v-y]rj{ޟݔzj `bq?FZ*m]@p?5p kg}PxaUQ(Re@2 $YYґv~Z"^s(oZOi%^9ײ/v۳G _ƈSiwe1rr^dZΑhxv*Eد;F]^t̚t)M!HG"tBU2h]VE d'ow37rbC㮦|ԦE_\lt[& F&)Bq]XS:4yt6^ д u;-wdfFUB6.8K 3cQ gjP4FWMƦZ}M]GGS.0fۺXo* <4qkZ.乎<5JyK4b*lFe"ec}x!sBǣV$r}"3f0RϭY@8f#Tc8-A.d2>4>Wa_xgvQu)Ái4jYH.۾N<ؒ:~ZEL(UF ,qgU`"%SY"'*B`H)fغ\3^.x,.Yjhxo=ml/֯UeԥO,܍D#?H;;.֋LdʌsJET]h{fxɎ+– MZ{I^ܪI_$.Vj|?Ղ~c9;U3e'rLD]8"T,(: 0Ry$dIG J5} "_ng80R)Ͷ $b.{BLHh(Xa#QcL]V+ɀlCO48ċq HY(҈x]i|=Y~qՄjw܋X(s'&լ' vIM{C{Y:,"R&Yta[HF!qRHTEOyԟykZumZqohyzvj1dW8\Ve|"2.6&֬^I'eAXVye dksP6<3Ph=ci,<+bwLeneF] Z\ kgG"B],jG~ۗg*ci,$MG1cdId`ub%~i,|uQ~6 sn=_ȨӛÐ TŢNty :챐ai/R)Nc4@˚:Kg<{x853zTP1z,7ΟY|39@J0jIIJ<3q.mZNZa5Ufh O*q/˟/+y^nr>Tfl1s7sšA3 G,"tPۺE>{K5D9.oH S2AT^0$))_/Դ¤"k v`cnvØ Yn &>q0ˮ{==pU 4OT2-N05 HSD2d8aY -~V*۹\oV,E+-u*F 0W%y]:U`){|vhN=A`8]9pH, 4hCD"\*4ġYR`sZ{/2p8H{3mdxϮY +9p/~ZiOo UڣIH yaQ %Bz(W#bx.tL|>N< ğI!?23Maf" m:3.G}^` Pϱp9( #=Օt Љ&6!9KjۓA=0d%Аb3u(ٹ,;#N=9t瀈 {<9%.]4䠖y k1jp:jl6yrLak>MJάGGv޸ SLm޹o>ZB6S!6س5S;bց-O}I$}SB;E- I>I'cPz2Կ+Ѓf#Ae"҈Q@dqX]rEV",AE^$̞HĤ/kφ0X '?yv&30I ߛ URwmb"$bYbCh9,TDc۝E_*qԗmúZEܠ{,!l[=/S~-(WEzH<jI&N8 lj0Q~ĿD1]~B3-+Y8d3HD-`! RfG<qny0AZOqFM@aVmfFK;&<tV4 C8! K: hc"#]kOq$Ԅ sIx1Jkz*5Z4zlKs/B)> ct= K4y*] +ee35̉/s=q%{S gOh G&r@asB4JĩLq;oFO\{Be;͋d+/K'vsi,#>RoM'zF^ot,&ͮ".Hi_/=#$U_? {MP`ͱw|`/F RuPD痉llx 샇fim6RX\OSí'n +$l il KNQ%8%gTMǿ,O2uyrލn[VV^C/wo^J/mQVON\L❶Sb4Z\&ow>\Ϻ<)y9E1,8IA@(l: 0hbу:x,75ihTh}}4J LEr4xf/@6z{[c4ߠ <#ӱ;6UTV]x J(K>qRXc`YzJ$B &B)sDyD"D,}a$uIg:/9y +rsϰƓ8byȖY ie'me3lul rq;TNOP&u˰^Z +WܠRt*i]IQnwv.ӀnTeo?&cd`k xEq?|*\dM9djA=o^ZYȴ9 i s9cyqt lܓR)sfw!/`ܲ+OTIۍݭ(@Px^׷Y>n\t>#GkLqA/)d{$zitb("``r P FPIB4d&.Up,kmxSQF-g-#6as՝jLݩ:0X<5FyiDݛyX1.A +Du1`xkBชXͶi|N XN>;`-!x o9yqC~5ՠ9pP@ e͕sb>{mK7XcbrAbQLe^fut;e8جNyk;R/|GUU*v9cۍW@fq2tԧ8yD̹wՑLuS*j[t9ؙsS`^*Δ2'݃yԦTӍtmc ǘHFEaW=$[ jΉ* @`$;`TZ( '5D6,"Z@{l[hɗU)M=ko{F;:|\V)1LIW6U4$E`̪k&N PeRHPi@#]Ch(\Ә%N>c~Эo0''l܌~Xm,:хKdouLNOJ9"Td8Gϴj@"Q,~DaJI:Ve2}jVISE fX1jM4< SL#/yHu1|T +RǙs[nHXD +RvT +YCl2+dlsU-%VoZFѳ,qcAhEaa3|N`rp8HW:,,ܦKIA%Z# :Tr@x: + ]zn0r=/_u莯6tMWb96ּ +K:aR+;?KȩJ"x:G$@4u}Yԩys;b Z'b@h!wk$ V7C r$+ohxf5nGRaD3qߜŠzl@ SP.)&e*9,^(!Zf1W-T]8H0CSd/wfwB hx̪s"[ۗj=h%At+WSSa4O{%Nʊ):iH#J(ie,)9a$\9h|*Q,0#F] |p4;bE-fmƶlԩ @4]u|jv*g<@=\g,$ $%YiZF}A|vb`B:$,UEY5p&I "˨5NʿGǡFgڔweF ӻ2W=a•O己nkR{tnM D7洍Υ/%ylK+ĿBF(}7NQMهaܱ@>J.4{xe3+iIi'Mxȓ&_'MMT01䈅58rj80:H!̈́ +1mbq"I %vH1DWKv,&VʂhH0Ewv,ѥS;ưlI0S/m$[VՐR|)Ô8kP([c2Bnx{H&Wյ`+яo`q!Kb¿sG"qFryz kiYШ/$R)%%PLu]Eh<9VO@2 Ik4램tHH}J=uEfp+FRtuL_F=M+inecCubZfJO}K®WQwk@t Dz2X,)"+lR4h L7lϯ.Da#E,BuRʩ؁LV%R)+'? XUsW;^h<+O4 ! kKlod3vvT:D2P?N7UH4(<2@uTM ]HtC<YW5UERm< +]*s6zjsUH u>y.w[9Otve8~9l% LZuEF,Eg@e9:/2'\bRec9VaM;Pիh{sQ +WmMfo^N7rb%Ӌf}*H9Ļ$ AT\.DY#ΐ?C[8"zYxb𪂥-a)[%>;";X"7Ac[^A۱Cg\OŚ^EEA9MC A$"@Bu,!WEJE4j)woؘm⬈xq]*7ҫM}60bm(Ff /7.=ȊVaqC`/⣗y&B\#F?nlޓ yeGpEcM6h4 ԌU 8c K[:1jJECUxSϵhS5JA"_nLq!p;g-nꑚ05Z deD#L4RE`=z WR@/*PfޓŽԘ6QV6H;wPs7eSA/>/36J\γh&x}c$#.F%yf$AsHi:9%<$-N .*,pb( 8'@^/)ԑLm9{Rt>/t)wԦQ-cG-jcyaDd 'GzDhĉY&c4VR Y%қjR0cA1}Uz7j8{zku[JN鍚7^I?B[Oj{U#6ՏPbt.I{PC<}FKG9$!>n'cU +'#&fu}/d[Iȟk f)fUA=,LJI:~c1J}oU@giD:`e )]YH3/wj.sQgېG5݉v^.Ti +\t>Q@gǴc/Ai ?tuӆN2u) &d7dxa*J1 `b!i>cX9r$[4J@ OC|~ oYgE]zo<+-'y}IKݡ>2vLUHlO;Q˷)m ._ޔgߴHZ/lR N],f꣤US^UFEjʒ.Z(gY(j +&7Ż1x)?޿vr۹aTꫤ6f|u/♐d}5Hzb@Kߖ]ED(pY3Jn]΍zM:bl7մU@]xQ k7@A4-/7ήnՓrralOWBC|0F#TFK%M%=U'LR0}԰cQ泲o&c)k|I9N l~m9Eq'Ŭ3TZګ:Dĩ>*Ywbrj$E;x0Ռ$tX1TSS%d<[vř&o:#n|c /o1i2[tx H{~RڎHȓJCiCHN0_Y鎱֠m,}Opzw]lwU,pY)n "@ndI<AW:,BHzɛŦJi}n.2LF4h%rZJcsmu'DֻF7C| 3&g2ũY$ !F" +?K:ruh>*h,լpg{wg6`z?lʖC"M :N2 4cnP:1d,-" R$w/(ix,^-Ox=L2,bie"$Jt^ M`O6-o0ΰwq6WCMΈs,~%l'&Zf4E'r12C0sHiqӹ,:Lsۿ +^4 +5N8jXLoh/CS.`׸1+ض +j*oƯo%JJHɐaRPU04`%ʆd4TB84Ȣ6ݸ)tU|RmZؕN^'T:X): ‰ +ib#Cj8l$&s|k2(c{&03.V!l %'JdF{ߣ΄j"I'GQ(Kv+/ZBW+7i"e2 Eǝ3QUj7!Vk~v *~G {빒{k$ kAOkvTBEDZPH[UM 83Bl7?O/?{T =,/dz>~Fa%G"Et0Dֱ ccCGHE?T_yG71%?j+cQ+^TN eb&)B s$ %K7F#s$I12QA잾%;bE&/h0.Z`Й?Q\gǎv J"TQvܶz6z +>@&͂w})f_s1IaO1|v7xPXNb1|x b]U O$wWM7wH oLj\Ӵk,2d2ؼu^V!%GN;}"ayݶ6o1oZ2݇- +q)U7;~h{g6Rc 5\p Aڞ;6Ft(V< @@dkp3݅goux}2[4|vko27BSɳgT!icXgu㏤jnsvak'BrwZD2dh d +%Mlfר54N,5ᨓ_}S)f,5uF|%4{,ۊPTL>" 8C% ψ CH /&<oO†(o0@xMiQ,<ۖOR Ja kP RvzaNh:l6rpnU m[s3}RkD8QL\7j( NL^D"MGIiQ:C!4Q`Ȩ<: X؂/¤7@ZFz̎jio&,ݖ6%o[ಾ% O2~"8hN| +ItR%؉̐ H[~-LP d rshp2Tf/Kgb@ I +e ߆-I_lW| ⢿xCX9l @ݠ`_xH0$vNDtrZ*Do<K5?c)<̧ଜMGLl{&fB &v||mXԡcVÊgt5j{Daà)!IxIYdOi -*uaP8&)! RD2"˴.SJjx%V"Rv"5fI5\q??yGz k8 6k?uαs.#] tVA 7D $ ++pKxTz!~T紙>(~>\5c$ɛ(bf[{ Xʆd c- 85MbL"QPRPGU={g_󏥗Ro4f!eW;%pV24v71pTSuP΀ *bАd0˿ђo0fuXZ +ТRmnߖUN)S$Z% AN)#MEU~])ˍmLj݁2wj]T7+,}-ihKATr} >9! %n.Gz4O_BXO ޥ2͍fc4hk:AB8U9]y +#I1M 3SAV_'X岢˵^ (`;e?S2~Ix>BV71ֹs("HNd"9"PuDg C:9):]lMKx139/3=MS'(HK$D:8Z,U<'o{R{{tMXԵehK{foA]7ȸePE^ϱV{`sރ. & "#Q񖁵%P@G%FE*q&3y4UY-=d!O1t_P'D\Td֎Rj3G^Ե_a z|EDRRnͱђoپ?^'& +. q~(L~ٌj,&& SzlR|}_ ~KL^ڛv4w{HشXxC +qxki-@ !ʴ")ߗ*&r/]ʪ +;("A Ut2~U<'-3G4-H +G@'n*C8h:%n%:F_el<}x=P^z +K;50hN>q۷YЧkF}7=? w}qG'Da 5wwf){:p^uSwoZ}䊼taEĮQ̂5}XGW"b,)yW1(hsoM&sW80%_Ԁ1׽2 +D`n닓#4 k8Y{zkIPmXoͅKbealM`_飌e0‹"3FI|{ ʞ32d+p&Ƞ3تF9}I~tb_y1 (lwNݔf _t\Ah7喇ٓ~ sD3."@yCEFg( (D;^azfTRz:U${a^/= 5؞mdE9l= (dH qXW`8#(:*k$aT@E8Wet4-#UDY=?~'=81NSgfҟBCY#hB=KoG̗Nm3d$$ 2dCw5mYNc.Y Ȝ ~ joePnU?~oT[j5bd$Q\HcY9O8ׯ+2c)!ᙤN FIC UdQR'پᔟ6Vs.:ۑ9ő")œ7pjv)qy!-b~lK4A`&kA}dـwY'0}JK8[WR/ew\[T؊֌0irc`C)ul"q ΍J9‹ i +dE,byJRq["h:DfFgpۀpܴKJ$,' +\7&?{8$]⿛C;ݦs_I . "7ZZ} feǒUT2mzDTnyx 4hf |dGLpG1& 3[߇(=|56:!>_._^>7#JN' .>{qo{Jރ>?C6s8}ABʜ 3VwO23bsdײ"67cbifɻǟO/6NgMk88*•+l`+w}ZU%pl1 M)1o?g~\Xʼnu`|?{{=rяƯnu=,vhF񠸄N^dq4TCԚ2ֆYX T{UJ&g6r^5qX y\هvA` @Z-дO(Vu#t2z4 _Ma`aO:viɁ@E1I\,!}gml?q|_?ϗ'ٓG?ۍhNhl?u %r܁` :Xѫto4H{$X49JGtpf M +O-.A2y9rA>H@uŨJT V&8)|7;˳E=#Yzۧa2HjOÛÛYs +d%Up ˿Q \vD%JI(E68Xo)XvOtN~~'?&ݫW{23rmrrz3_dǭzh>HBFىaA'sU> ^{黧Ύ^䯌_ßG~94?d4V?p09E0uu!÷X|['' +~7Qn.VON&S'FhUϦs)?7X|i gTfYv/~r_0890:N>r>YLPAeJɽԉ2, 7<#MbIع抎0cg" /1BgX\l9 ĵwEۢ{tQ=Z m/GE9^e>8kcc3gËTҔ]`iƷ1]?;x@Fɔ8ZΕu+x ZGφWZ>\ůfD=ƗIc%BqFhARJ}n[ZN @>Ʉi?Ft|tpYo8i6ЋUx#lqFT;Ƕ 2OqArL!t& 8 8]>aP~..U, ,84K _24h#)'=2N{J1LpH'Lm"OIzv5':aMZ9EĪyB0;~s\k'}@6sp:,r:Id[Ilc ֎☔nTBF`?axmo1^^`-#^Tn bThmNCyjӣ3VaC@|"ftYi0[[c +Ln6:,ìf֋ t4_Y<3t涑Ai(c(FHβ^j_ăP>|QӼ蠝7e IY/Ѹ 8=i'[{bem7+_i^kR6&% g_D% +%Oj;vf|wۅE)㉅$زo8c4x]=WKqʨK+iNz}LwZN\ΒW._oaPa3_yuSv$+xtbϭ}^)#\g:ai3)ٳCB qF8(XHmtr2pmµL@h8K +}8<9-uk͝sτX~Zl ~STIOQL UA Wԭu #giWMh?}/7?گ./ndmp2D&ܹ}ĺy q(\A3l~{.nV_70/OK(Q<:G#É<|t4!h=U< 2w/~`gп}{<> ?=XZ(E$lyn=#PƓ c1 *~3 H|=_Rds2DMj_He6&֦_7xJn Y@0>Wl Dp:7E_Zxƒv@`U`89?䴋Х~0zT<ׂe/n)}+f*q̼ʬlFaټ.\serB}-:0$X%V)o9+:UGbNj, xg-&$}n@Ց^ïdq6{bHrJD͵iYB3ztJ:DcX2NPMyPDT,b@mLX!65% ?XrLjldt5G{Mᶅ]+rg~1/_gG<}h}zpB ~7AVK8mqnzFhŖeYzJb?y"^>}?ӳ+WoS8@ U%rR/gsLDYg`m7͑o<_&Uc6,"A`!0kX)) ˎ3q$Ze~>={"3~(zo|o?O?M*iXEZLCm0U5U,_40{43:De[;QlA@ E @,"OK#ʟ]{hGOgM9JٙƬ \h˴)8Ǿ G7w?[{t_Ξiŋi)sQ#0;Ȭ,Y~Idᬨ3 +|\ikL@Ě\Rэ UҍlljVPxD${|!hR%`s%_6V@!;e*=JREʁXdDڍ@rjI''薥@D)'hF?|Ftv@Owh֗{cW<bgm$1k]xn>ѧuI,S'x?v_EH/2fb̽T/Mbмb Gr>`ډbK[ F"jd|ٗˢN)^~V]U9$^t=}pzgG<<eq?z =?OZ|H|J3YO=z/` 'kq+IFb%bCGj?Go^8^sVхqc{+{tu=~\ gA:{ŗ釛x3.H3P4]HkF:qjCiq: h +;u ~7N;3b+xv@$^ӏ3_>g珿 /Qۧzg>UFyGsmrPȆ28tS윜{] 3&R.yCV[EzP'E5JhEI%Zp--Os"d +ȟ(trـO)Ջx:]GWǝK%TQaQ Os}2*CxJ + J's tQ_KSL}-p7CIL1wu@}RB{ҷi/N|?QcXG1,Bv~>eQ[qh^`4N* /iLlU2KYkuqcЪftLc)DA8D",(ct!b d)4cHEдP&iw)2ŀr{,۞ zh:>!d]q?O)j*,v/Â]II|B.nsO`_/ (eHq2Pm +-n2{\z־̩3cjXt $1E}f̋fMM/tb>9,Tt(g <+œ)cf=eGl+-rBS2%vW;AW,ZLOa e?տȴO`Z5=5HF&*3f\)LI{)BC=z٫wwuE%?.SG>.K0L2e1VJ5&hVR녷L|Un23P:s ʞV A^18y"KB6PZRKws5%&&Q0G hoe.`Pd2z{Rj+OFT}_l?H <θXf\+uEn%z׳Ĉ M%۔/dby.(LH4Sud).$Tf4-"qsBsi"JKI}t^%rAhv+֛I`ftSh ͌Qmςl +QM/YKb U~wقˁ3e + + u"0R@hк? +yMʯSyYG?N-Xs=fU<Haѐ9Qaϥbf|m+]qWnז(a`[lZ6ɰsm?Ϻ> 9925ŸcM|TG|45|4 @tϿgtMrDӇ-\߳ +/#f3vσRb + ZTgXRDrKpR(2/B%ʕBu#J9@{5#H̐6Tch9tK!U vQ^RrlUzwz0ʄnIӫ +loUK\u89J[RfFdF٤"}bK%?&^E) + Aw^ʹ\9i0>@c|n<%Wd-R^(Ev-""~BfYup1Q)Zqt!H`&ي=>=Gl׮Av<J4r!Fh S8pp>$ ei貸دz Sv΅riae)釬/J',AipW\E[Nyv =JOԟ KQQV6$Q}ڊ- ЗXL3%10)N>m Q&X3+WV9. E %#I;,c;fp9~v eM 礄*#V*32^g(LwXqteV,n 59ȋ*z(LU#nU~$5BbEHS;USn*~3%*ΦX&d4ʚVϗj_`>o8v4;Rj\Fuçm +U4ߝ=ӝ=wS1z6穌4 h*t.Mm|UXZofjNZ4R0w7 юwwzUs-R-(SL ^/&JP="wֲrdp`6dlh05vdqʀf=h^ g8drXLƓi l MQNFLFW:*2MyBL`RcZ6 ں{l<,|tÌAz>TT:.$]3sb4#T3}\Rm>(9|UݞHNβSA;g+o(Z2.OԠڲ^O+ ZL+vEkVZ/F!O/bYWn/R:gw՞VM5^2$o`'zBU]E4zi&'9+ I>zSa5$ +D>/jTv,x|y!@K> mP|G״r: (jt3Y/<}|$";c#p1FqVgV](1 $$f]N4y,$}}zn{ k dR|. +JRӤ𯭋EGHvKYyC,R +w,]:Yp4EJg ttq¥LAf"\ Bndb;-YΊ)/gߴܠmҊ)_-&&smچTMl3?ۛE#P%ۚ>s`ƐWF钓4Ia+ߨ5ry*/yn?<p.'b'QF(6PCq?}ͣ _V}N(\űJƒ󬀟/"f3HfNW&IUyTS17d* >RDv_N0ߚ 2C'aT%n:bGK|5ԉ+gMiX)\.._h<ƣ]xQLܭ,BK VyH/tY!"^S`3>c>Q$n`-pi2dN:B<8zRUNu/ +!M1#Scc׼8P4߬ o;ggDMua@-A\:\ꎩ.xVD&$T[ȥ{Gήm<sfWܾ|Wv LU£n(G9z!z5gC b剿"'%ݻGy}7hRܿ֨=Kje/|]ilF kFlT aӠ74NxxB{ij ɾƌF\ uM}K|x:LHK +-y@mNth/(Bb~h-{UI ńX$ Сgl3|\+s}BCB-U6!Ac-T&EXΝQ9=>:W u}_KhbHeA) {]]P*^[p{bgN +: Wu!Ҏ0x>^ҎH99mX؟a֝IrJc+U5g>l&Mxƙfu;+WY!ۙ tp>QHM=~F5GXeQ +iϕG>XfDWfw]#>h%4bBBtZXx xJ\Hof :ϧDpĕcC#;Y#=e +q邼!L r҉t1)zO'9qloʤpZ䨖:Y*Wqk ZPԙXDwvԢeO}BS` K.:9$XAx9;~9;a wDjXA=,A6 Yp2v˽KuY2]ϒ-XR!t;$~eIogbޥwWy|9}xwɽS`3K=c:$}j*{wtGLwtjLtH{wtOwtH_ρ!wH1Sw;Wv:Uek"C$4h'WW|/:Noz7 \Ubi~wO@j%/{g=Ip@3pm];7r ":91Eb;b+ʉ;z¡ʸ4;Ěq2|%9be= bQ +htӬQNdF ٣W?^??|}0|8}p)[@F25 ~w4ijů}bo. _pZ<+Ǧ$= np1(bQn[oEQ)|0x`3,r eϲu0$YKITO]b3BrjY~IAap)#BP"Goh0dGiբy{v?L2˧@_ Zm'bSzISO +Z~JKc + S`JCW,ZЍIJΞ6Jf',= +U_k9L* 0\3%@%ohvƷۣS>N?jӛf9hĨo%_Sم;d {%X9(p$oc +X,h׀C#.i5TEסlj@n' %@i!7Sh7r-J +"%ӌB7{ߍRu$n\+%&7HMоJoLECVh[gв:߭:JwS#XtG+UAA/E96B޳T1PzWyWM]g#dz)zl0;?^Y&SI`S,Xa[p$0Bϲ((v6M +$k^Q2>I^?9poN)}>>y>q?WV*Dw,]T6~2=?"cXoc]W^7S5P+FtFzTD?"l5Ox2T!K2fͻHLGp3NZB,DJ-}2K`,E#͖W}gb8Q̫xEx40^Jpq+ <}3q{"~;ggͣ%ڜuxeEf-Ƽ"G%iUUcn(fJedRQ60T/_n|R)`UpV۱T}YykeT{ZpRw>#fF 6c5c +:NtNY;fUhZHH,s6s_Z搏MSU=޸k'<ɕ4PS\uX7]nj{[jll w^9`cҕgo,M]򬹦fici 3zK N95b:$&yПw^v Qo .m9,Umh+Fk>XMGg0O%|MTyGF( 1vezli\w*HG +%q?lv`kO*tX&ikrvr5`s)@IY-S,ͮxv%Sʕ2)q[OO<[ F-諎]N%q8#*7krN'}l&o CpVJK\PrdyA 7)fORA؋ )_I(,&Umen;KTE݁zF](aEcTiZ +t!J$M̪5j:!1ZHϏ¸Z2(˩;.°0# ??TVFڮVӍj(3-4:‰>æU|,]]x;:hھ:0敃O/yfGfǖUԷxSP׺Żm K'Q nVb IˈN򥕣7V0܊q7(JHHeB7[P>}]k_pD=bqu@ JLKqKX`ϹC-KV\zp][)+[kiuK`oKX^V ; VDAB5"+ +=;O(rXɰF髳W'^`3_aD ãz5ĀZ+ĢX_7f7R)]+8;6nat[pػhG,ʼn 0E>G~jjP}9җfH5{]p4tYwJ0S׃{=F1=zo{mφ=ԆG wa|J{ϸw,gW) h@!׀Ax i$B]?G3~%꿚OE [żWz[+|{ҕ}Dv_tfx }-I `%C˯g;{u[x/3ńQ*._4FR̗ܺϏ vMUݷZ2Ss4˒:(>wH.V6..2fqÈe*=*6^WlOw^[5  O?*zCMl ׍Iz6(`݈Gvw +X0?U^ t :(1uV2.j.Gw@)w88nͲzբҹVἪ|֫3A{>B;qI|B_d}ƳVj2hmCv +v&kmmo *ʑNW,99bo5ˊ*aY,V{-r)\-:^їj=bc f`6(\"B* 85M)+|XnY:-pcg]r9cξq/Ft`D,VG%b~C%:gy"|YTg$tܱ,&I +&5E Hno,md՘F~ puk(~ GpVfKUk:6zxȮP۝a⤐_pYtcXjŇGi*4mӬ_ֹdU3Hǥ뫡[ -Xi`/nYdJ>"RZ֓ +: PXQ,.yʑQXl)J2(EJ{Rewʵ-k`[Vv%лSSb~b7ߜm*1 )aTXIa֕Sx6hzټ~G] Q,-DR eqڛmQAAp?y:M䓢:Tۻ ~+ta+)Zh5k^iToIQ`d :wV^1y9^8@xu "{^xǪ:Vձ/UN[O^i|,8Q!БX!3drvמND%2t2̳7YIC:-iˬa|x P*p+"JE^S0pXbu{q{$w85)*z*~<4zٱO)tH˰t}[g᫫Wn6cNs[+˕ޥ1e,zk?gd2ʐ7onp#~>,gm$(ӃLCk^׆5s4ԡC$%V~Ͳɲ&yɮnfeɓ]ώkJP^S"36zj~fRV3٪I}Y6=KǭTfҞ ++H%zBI0Uq47*Ƨeh ۟1Y +_~vjܣaǖ5fߔnpBHDZɺs=lS狡jsHž0džLZMf"pHНcl.w4f VJ +p>hyRZjpe!Qy >z|tip+u Q 8纱_cp.Cܢ`L i"vbH< %so5acgQyU(7 +xZ/UtBWYE[1qslM-XޖNz:lFBb0 |PAY]Uyxazt!ݠ9V|y>^sd|'~/Zgn;^|fs͝ȡcheA8Y.լ"D6t6=tzn.2+!|L[3sC\" uy3}Yǧ92GþE`+3౑_mJץu)xw&omĞ*u΋;գY cS_b~e{EPk#;߈}cyXޝeÿgyC~\e%Kq r}C7[E?Yj;V۱;jyҏUӧmz8Y.㩎:pedt-ntI=֥ `4KfxiQ2&x>%vV,_E%%Ğ4+"4Cl` n-]ްݻd.ߑ +cyYDTcԷyQTP[R?UIR-WeRܺY)@^Q|"Aj0 ;R#\FJ {sL3@{!lq aVCUҕJU5 K^]C1ty E®[5BanV'0T,CZ0ŃSɠ5#%oQbC|z) |$yI}1~o1rW +_'IzD^nE*.L j +`|M\ݙw eKcuQsԹ&9*AnYЫkcdCvujjClU7VPH; +Pڜ|'# ]Aڑ:kᚁcDaalyv`iE5!М`n3Y |/Y?9?9zǏG<'O'qᣛݖSGU[z%[VItA >;`GzI6þ&(Q]Bᮅ|Vo PYYaҡ3cJj]er\LO1W+a|)՛F5N٥ZwFaXh3JeyWf!*fU &_ %Bx+,5&$~&|r\:` +71[ܥqnOӾ +8>yJs5@ڪ\P윹3sv\߬vZn]բl&Ww z\wUo[[e;ϵe>%N)fɾTN_Q5y mo/ZGYMQPV䢯SX떈H9qjǔK_kBsEHz0_|+;miuKji׃F[v"6&yhqË~ws_kcM|5kܪjZ;4m Φ6YhvY +1b#|Gf(pγ Ȓ+F dI6ɍ|Í07l#1 J%yϿxOgdg/QErrɳ] 槽bCu ZobgжVUg"\`?zt2^q~HXgJߖާu2E,}/~AK*xvKgg* %jn"J6cQMO Ś3^ 8ijSVۭ5%=_iP $$#T~#{L}U\)IC/Y[޶"ޢuR[[+٭ LRiYntYe[֫,Ѧz$B!h-zf[ cҟA=+U3m`ChSBPdCPn3U-`JIZ`wFvb1uLN3 +iHx25WC` +oaWQ^:1/J=P$)AYB\c9D9u%Thf>O RCsOUx1EgEa7H^u?+XN"Ү%wOü9ʏ8;Y5 +HLp9 5l&:񶎷AFeD>94`/rX!|#J8F=jImm9Sf5_ꯡ/IC`V;۱ގ~uwJX{8)4 +'y;a{1LWXc^{kG" юw|w| ` +<9d6;dL|TdlgG-f;-oze~7]gz)WfUNv66%$-'CGp!D֡LE Kf1R8 ?*̪4g#Bz46GŨo%"[oT&3?S*vtdA, ZqnaIG7 meB''bAZ Ϋ_#r~6 +~^||0 g({e> 0:1oUMn,ݒFJ2}%@n=aof WǖҧKÆ}cpQQ"ro3dYhIl*dflamU}]QM.ƺoz #PA5Ū6wJlҠcH)UOe#TwN[~NƐ8.siO,,FHg VhNuBPw]w Cv ]. v.ۺ0* +abRƦ(%EPR{ܪ"KSu)rȡlĩKIhir$ &Q*ld2K۟4V$Bc??}TgH]nǐvːĽ`3!*$ꔰJ"cT#ŎEu,cQwEHryLn=d*ToU ̳soJݩWjHU5VJU+2kPbm8Pԓ ;Wht.fߨ޼殱kgvE~n MóT爐<>{ +x~{G|n{:w궰HϿeKT끶{9uk\\ݚ) ;Pe &TUkjTH%N:Ơrme+`cң 'Ѝ5Vzba̳CҺtt_#-t1 -|c@vf?WQnWoNܚns{0hJ SFV Zne&u=ԛm22M- L&sR) bjˁIB'Uq|(:ҵaЕ]B"#}_nbtҟEc^rLAVNWN%inV`Qn{eqsrZ9foN=`+RTr,sbg31cq3M c +I?̏~ƗώaFOߟpWO9=>b=fxq7DEDz>}<:\aC$_{ԛ=V 9fBH18Z5HU7 g7½elOK-r_Jt\aAڎK+P#}2bKA-Kp.U)4ȠXQ`-&wm/ :X™DƔ}54T]S嘁4|Wn:qBH/ǥu,^%XE{9fJėcA|ihth$_U! +ڞ R"A!` u)a`At},)(gl~+p0a" D^gQBا** +Ei7h’{hg +>2cux /`+,ko<ւqHwYVၞhFr Ƒq7i(}\t.R?.]N."mt:[S(~cɨq,J:Fcܯ~4+5rAw)DD/M`%a;fe_M59m;TgSy(=)6d$kLի #J(C+d椏`GkogQp$,OЋzBQx݊@1-_}SP>]OK3J=X6+$xhՇV/MTS%\taR% Ea$.9+=lV1ԗlzJ2]=eJ4כe!ler?R{|JmbS^PoKc6,) u>$Q͘P +irqkp4wvѩp&벦WvY!sQс$FZ-ײۗH FTH7|0DTlN,lH`ufMl0//IҚ(S,48y4?svYEGKF^o6'G9 zK;f۔z=B} +/PD:6;S7;?N@d3>+jg|~pϖXmV}q |Y\WJb}i)uÒMvQtjW3wx; +;6Φ z2Y+g Q1 fk3{蔨;ޘ>fP@HE n9*NSzf|P-aW.j&Xt +1mb{}sqw2v]6WYiڿ Vbߙt'm-m%a{ j5wOy\+?,qC:/9Hh\kXs}RG{չVsSQ}Oy=./6Zf\k=3Q6Z}^.>/FS2Pprr`%ǯJU1K@ӟЧ1[@GO6Ƽo4tY o{ϋ㪸H/kps0dH=_x2 =w߼~3faw<Ο?ߠ묵49%Stm-;FmO۶fe#"jh 1E\2z"x8p\jj]uFGIb%6ddVkvaHy6ih嵛Zі9f,rx~:9>~XOA!PWΰْgP_D*_ۈ[-ZUsIh~yŝT.7$hJes‹yb& +sD.0yhrNg;<|[Es[֋mJ +}0 +3KMTؖ( +jԼE5)7_ٽb/7mi [#L071mrVN\s|ǔ+C?cIZscj?nYZWJ܍&ɭG<b)ЯHmȳsCA4FXV4UiY3a&=R68fvdFڹ 1c0,L8$fJ7Wcͳoeɐ4N>XZ]:CB+HC(6\ˌؙa~$Vf6$,@ij Yl1ypy 7]dan{ej)j( ~N4< qbˈl ~ 8nw4L  {~##C.X4eb2tueE~a҇ɐOLxqLxH$]@A2Cy4 C>2м!Q"`XOOhA$xǰy6iFd{QsPr,s9^gYb/&q9d2mH{H,L/gS + i 6ԾPjҭ{TPh25fOIh6{BfÉPH-3S] Sa 7 AN@]<;H Gy1%l`]wl˲,54=AbyR9Iڪl/٫ 'X.k1FfYcVkחvġ : dUĻE$8QC][j4M<; O\#ј&[m'Ӟ{6qA.Í3 s,4hG0M'\4$0 +|d<G8wi*Wk[Hz DRo.`o:n-JVٮ{мz.^Ṯg;n.,_mf>c8P ܁LU3V{Nl+H +A%C('/(p85=6Z1(i~ +\tp 'ejjpsX>GЋEofh,PC3+r"* ~MBŖfr~D# + SǰR׶A xmegDe9chF@:ia)zFdYp^z" +nx0laFK>RsJ=#V7avwÖ \& QyYBeD:$27돬K8I5 +#æ">@L܈=/4y$iBlt@)|80Z |Pb⸠xR;Urtlj;Elpr9![hc$9yl,v:(ώ@a!(d!QIt󚃢 " Mvge'A]}ψ,:nX$sUEFv60rX\hrBQװ];myNԪ5$vJt1x4&H Ybz~yV+ʍD7'' +hJ + '2sc (ɬ(#'h9Pv;4T<Ht*1F:a zUP pbw p};0"OyI;s֡lN&g}1Kh kK} ( f~k(i#)I'N p4͞d Ĉ q1U"ⷳQxp@Omɘ1s#5OX< [wr0yUـ(]+Ase^l8aG.J ПbҡDop(@a0 sxQ=-mAan:vpn:@h iv"o2 A-HAXCE֪,K` &e@gAG{KI 777-۠^icM`rmPjA7x 4801.AF '`D:U,0D{ ņ^ H{̂]Gez@^6]G>gV8$H .V8 + M7 7Q2cFhLLJ +m@Zdp6ΛW + +@eb1 H 3P+Ϭ04ecA N@1A + ,l$hZ-BV&,,]6fZ$BYOW[3;Y>7 l#S  1f l$kec#hD ׳؇ߵЈ@I `a5d;Fb+F +H,I ggi2:!^cM2>-oAHӍ aabFn%fҀtmco~c2/|b4F`m>=Ҫ6Zͅs/yF6Fx hG;g!(o&kq+0IHAQv wc/" ט,#+Od1f{٣":Zi̒򔛅3lDߟg2rԉ2,13BCf{]fU& Jh; Q`M0&ƕc%buV`1,&Z#)v b;|omnOa;Yĝw֑3OXIPB̛ Ҳ>}o z'It/~LѾt;9xA7`l;;"ǩ:Ŏ}  ^2[Lsyݱ5Oa8I_,m%יZCе):NOG5v<2z}4!6YröE /1ƾ;l`+Vn{p2|pxkQik{.!~3[F}9yy>!>F=a^OsWitƹ*htֳgp=tוhzƋ`|hj2!gMGԘ:}w-@ĚNbrpz]߃AM u$p^w5v$INIm7}ornw=t2Bx /Pp |әwC p #7Pbr&Fo=OT}F1hN\]8A<5V#j&8YHL|p\'td t|+69{=ӟyMM;;d_MKz'5{88CMM*X>swlj";hSlE ޲FmV@b2v{m1!|8Ӷ4"8Xv1ቫ*r[]BP3l_3(mϨ3wvmsLCK#A1\ x#AҺf<0}EJ5BK[,w633c>ɮ91[\~W:1$*75ш:`1.׏ś[ѡzA%;KUk¢ahowh㣧&|DtōԱ7Kzg+?߃q!|=7⿉~ `?\!A +=w/Ihc|bįOv5D[jmh<V8tFmUh',_ +z3OzZͶ8ta%ߣ6Y\M?C.Ԓkg&DQ2> Z z_~Z ~ IfB˚H_@Y{..aĦ SMD4Π@G>K˝N=p9#߂`!9eޮU@aR /oSy9U=5]id@r#*.˿ q~"5&J0#?vy752Cq L"O:\|< 㵿FXvvidT%q$]7[ +&Kd|E|/dec^ H# \3W[H"v zR^1~hx^%Z`\B2dv@;0T\ g؆C,F3ۭs$ZU!D[Ɇ e5rK{gS;d/ 4 i*65Amc`Q`~"z5P@g 'Lap]Y6H@Is#F$ A CMhWdIn7GlCУPa ďQ < <AXRJܑ5e6_/$[?oR]iMT^Pȡ*Yr0,E[f2* @yAWf}4ȑ8ypg ZrKSwLiAP( › shŬJ($(3)5d6J|x4eԒ 嫛L('#zZp4`5_3چ3d ~AA; BOI]p rX*^`lAQx<?DJ^)@ܘ|JG^^  拹@'yI4WtvE.<~V%5g$9-[nr%zPOelQ4)ayS>QWy1;\|]2#rT CciX>at{nx}ɿ-oXF&oDDh'DVBPc4ąSXAIU3D_r7[qqظRЊ%xTvՕ(+J'b=2P/*-"q~ gg1#عmfIGL#]KJ xQu1NS.wRRxZ\&BЎdQRHPj1pLҔ?tU;@( dvGxl[),\7nͥi?%ɔs\m͉Ltb *ijg:hucTvCFJ +ŒM`>k)G"I!;=FxA7C<|$;aߤ= mSSbm86דaB,b 8|#ёOYj@ %TCwuFvh%;q4E Д}ư(϶sIX_tplQxۊ(m 6<~v "[8 1niEHM0ĴC)Ohb-1]+P?]CH?h{QR.G#wA3#q/op{mwv*w/jW<* ڜ%y|};vҶ| :cOJv@wK;,$ k5(EIP@뿂8 ig2h{C3]+F"GA"6e1"*.ƒa|!c&Ehj{Ց8|xJPbjzP/ȺpP]䥩9-{ُyGe (n֢~4(W lK5e&vr𘇉/xWFN-;Ԭk@8y^E(ŕLpN&U;Mqc#xh5hФP*]*:ЄFxƮ zzҁ_/|m8zV~^ Xm=L崳Ab$2?Yԥm??dIcIp@R7SGlzŭ,wIHN! XZsx8;%ssgd_8ld8yd:=X9Wc*($(iLKi 1senWˀ=a0 +xEdO*ekYܔy _;Ois,o$#^/H%+'K"M >,VWg2-1y1}p5M R%xHX\İm +:6!INԠN B[Dp>WpUp9'.|6Gq'E '."{yT%0h@Xay]P̈ 8?L򺹙ɗF퐈6YU # ?̏60 +&VזJ0@]j +~-21Epk +3!ËPaFl t$%@kUiH:p=Pv#Wi[fncTRu5"$.M8;BYk)w 8O>D' Y-_T$impA411AAr.ޔ8AtZ]1dUvbN̓3 $|m tMVO'u%>ZZ~x K^ivCn5%ld kJgи-\v6ʨ2mO<>! b1rI 34 dtgcCMOH\Ww%`Kfc + +/% ra[0YAy+zNp`PEQٍZIT,7<< ibI#m뮇< 5H%* L[厩` Dawj;,ñz=tMܞ:}{:oh!Hc1!V Wlr5$I5m_k֏n4:h5֏n7}kݛ_/tӛ5lm" Di&Do4wY 'fHzز8LD_' +7[G^,;8P ̀uH; 1̀2 P X wBqxlbQ̀X 8l+^E<[l fL4؀[ dl73`3ҷH2@ƺ.陦|f@gRXФ ̀/p~v@ܤ4d,7>v + :1Dyd/oP\%'% aObE f + +$T *|TJwm d4dx0A@[J@(UTdR[% 1Ch8$ dWKmXۜxx'7v,@f2x<ļeBtQʋ[;N)Pzxֳ:bB=KprcaY,ZnN髰LSz$'[]:*|%"1Աݯ?xL.M4%5rp{8W^IrX {=5e-BM⊋ˆr?\$; 2QPyDL5S(?Ӄ_ð +h^2K^WSB.qyr az$yz)X5ૃOL]*NQ`Pt QȘNrLSN7o!0ô~ ߼e7 7.&{ $W4k}l,i>s6xA~;GּV m nD;/u~ϐ$*hYtah|xM\;j{' >02s^$rQpnSq!QHШߡRۘ=TZwM .iY-W*܏$F 2XXWFHx׽؞Z )(}ג!HܺYV1J[ZV AˮYvDX1yJ{$=l :*Edc?{1Ƨb-Dgf`O& :qC̷y"afn +ҿ>Ndw /5n +*Ew1AB;Ћ&P BŢhPNdEHpm:).n:LN4㢏&UJA'# [̙4f+C#S0WR#'F=Ap_{aR1ZJ[-B梅fmD 9 sC=|6_>[^g2}j1 +7Ⱥf@n֟pqސ|Q;:u [r;eM)tF;C;ɉ E,ۜ+U4嚏~ݿ12uX>O=y0P ,L7k+筭ä@xASbQs*yTa,JAs_4pdv&i[;Ǿq~}(ȰEI,n8dz &,dvxG7nmn5.>^}dOj-ɗ2=)rd^V"jg2ROLP5?HY>d(EgI~ m JJN6&Bg}KJ!)nvh]z|w\m\I&|VMy9_%ߛ1IrqFߌZfVr71JlSox9dBf X^eYnhKJ,]gV  *ɼ e9TM5"{uH*6\?.Vsi+[ٰ݀͜q2W3(MjVV~uiؖz#Hr*$끜cr5I*p%$*t!bqӬc\DN >CڱGen6Bb +egU3;Q*w!5}vq&v[߁A:)7@:x,qɨL1\WNsy_P0#l{|:y"~םįZ-n6$:Ǧ G)PіioA[9*F̓Q=eY!eeӥ$GDұ]RE,2eOJJpE_KsQdMtQКr*݂wދ"0OɕJ;zROALbZ eGs2I9q$D)TRHI#P^g1Bo M<2uGaV龯{k^Ml9U"TIj#U`gVl~cτ˞c耱 ++  th,@yAhfgA5"cf` 1 \H0ECvŎ zI Xx@YdpyDDhvR" Xwr.R.݊RzI- Ytj쥪.Vx!)}kX3ԭY%F^%9J4DM+G]>:fb7u."i*ʧz"s62ki&%䏖X2TBcWF.ӷ1{ HY6M?0jXD%>t,zvaW ?3惺/->i5]_ wLN#4W?[NHjD娺ßŕ%1+ 78&F)v5~ *Lf1?Olo6 U$Me3f6|09/D-c\`X26xpdUsQ,ydH8N(*;6N fx\$ ?h=0%¬=땼Dv7;p5` +GBкj>[ $y"2{)Hׄ?&H\?Louε\3 Afr93j'bx<v+U:5y#h̚I2Mԃ9JDj>-k)Z>#]ZZRkGh M{8Q(|B +ڠwĸZMQ0Y+ŃRBW(TU.^Ըp-9:=r-sG$$ +d$%jTnF Xƒ"O9lKew`^4%;]] `Տ]b?Ca$ףo r"x걿w"4|Om4 Gûps;\, +ߢ#il&3_> +ZfrS^&=e eDYXJ]9ZmɊZ094vGF񖼵 +f1e$ 6~zrLX0 6уogasBlNҐ6s0-IgYu <*JSOIR֝BC +7H##yv{FID4ƣms\ aC OFQH!sGvҧgK! +0^lOȅ@eIm$b&D07|F384z( 2WQ-˂P4%'N=>WF\k W*Yj +9eg3M =HbdVlFXnX7u=fZ2bU;pE$cxRC| Eu+Y~CiƞFi*R:Df*-i2[dXiU/ ڃձ۰0oG}Ξt:2g:NC"xbxUd'#^aZ*|!ckQ[em_g|x9_-#2y:D PPJsV͗>1q:q|㻗'1SB]bn!AΖcY;` npߤ+OD\[ϒ4^4iMʧzl4eR? [*'Mb?wĐ4LR'm?A'g`a{,Sv11n x [VW9K_"dQU^r=+dR ~*\|DHճA*y';v`,Y8Pr:=h+aLy.,l7j]vyII;HtW5:u٫?/&0ALJwk]Ԙ* MTtV- Gׁ *6F?̽Z]^Il6،Al+/FRQy͜.qHx-H\<pYGCw*NCw)QNW{9n߶^۱2kavDp4#Mo:~j/:q +"0؆7E8&;6$*f.`{vwb|/67+_F='&,4+%ċx*mGPȡerS^a@0mcנ.z"T73@{sVo 8,IJZc-1f- .tK6]@?IG59 +\aH  :e+uH]dL u@sDo:kOإ tPzyXwH?pt>`FRLP$V9%/oQG{3ٿ'B}}/BB! +jF*0}XƬ #wgq*Kvwy =SҦV#$Y$wzk'I4 &AIٗ'R,4{rBHa<%Q`Z~2pcznkGi)-k_|m $ByWρ}/)abwjz%5R!><]P]g1нaөؐ,f:Wi@9,)|XDfZ"Lj!*~LI_ RK5 ͂N Ze2]~=d|نkq;_5^kZܾ5nI{|E'2 q췘YKe5wKI P|2êF\#Y`"$ ֍Q$iܧ0},7Eůбs'<7 5rU6I ]5"ւlAo3\x[tnEv5:/UeI0-sZ΃Tʃeb>}8}|u(PƜ|k**@_@E`bmvʋl{/t+?Z}EfXl5Gj9e ֆ:\dMvo~bȔ{z:&]TXI +BD ê$7@(X-@YV]P 쐇tj%CldoKJ%fW6{ѨU] +3 pڙYfYK3iuc꫾?<{S ƎKJ_]aרbl u34hN(ߙn_0wm~De>fuyq?G?vXO§BE=y +RvaoB[T>-<7 Asb^JYXRvv60re+7{—xOȫCjpV̯!}Xk8{Zۈm#ij?UN(;žeAͥ!KC]е-ueMMLICNBBQ=Nx;ȹ" +:k.*PHfʇQ;zc̔qlg5 *~`;j9"+M66;41nrՙ_6 ְ,UCB똅K!\&I\ĽhD6H+2 +ib!G(4]kc@k7lWR ApɪHN]}%IFdTE;U/s0!-uy>L @ZTGsEXڈ)I8wuFOcJ̖7Q$Eؑ#C닾n`Q ,x392:iK_qߨhFAIp!t_DPtxTP)o+;ñB".% *iiiQ}.-sP=H8?`v AVs>JY&ӫ'cxCf> 4[^6񤑺eRI^Jus%}~#X|<RxnJxooZNEh*1-v6Q Z$flG<J!! %{%/|f<*x'֖7?#KH۞H BJ zOިx|DbM4*D; kjD% +i2Mե<ШlK) ,v  %?щhy,5EL\KTzz@r' ٖ;+!p4%+z +-t2"/}]>ʓ6r=i@-o-@'"YLXh4Ym + @Ӣ'LC42d a!Pd)fGZLwnM]gnY'vQBف +_KPRnѠitfޢq-W2Wz89& "k=G4!Cxq&)&',+i"%P\ID#Z*&\˳ >bi#[q^lIi +DLs+?kl(7aֲwtV߳oPc$ۑ,t3>a@wCc߃v{M.j~+Q&[6 +(]٧8t6{@6 a,_E'Մ~ƴr+:_C[X?~H#y 1|`: Q@_Gmt~sj҉/GIx#Bԟ8) 9"x~zsΏt +#dܺD5 +Ĺo]KVDӜLMd)ƟGor݋'˄U Mf1,:{$4hz.. d9Aw*ǨP=AgaUV0錾7dm9B1$OԠNm7kmD?qbs5z-6?Nl%%ڑ#17![Hq+ADr *rtć +] LblEtD_vw"ɡ]7S46rYј!./o>} ]W8/}o_=v59flT@ $8zX`*-ה&aQBJ +)v~_+g 8 ! k19T2V`1%S> K +YX&<Dӭ|r_0\*J)hx{aT@E@ A'C[L@cj&.ZI&;O,h*@U|uNjNHU4ZRDpܞ+8n +۝G4ˇθ?2G{w)B:;1cY7?כOg追/cO_ܢb**Z*Es ^9KǺ@TBy)(0.:D MPWo10K>um{nw?{gqr{3Xys柾ߗo>~`pmw_]ϿE\JN$G6}opTFb.~tiQXD3#IgrRʞ$&'+L< L>7\otnLoOq0r|,zp~a."oao[,?}%X9篣_E->ĺ(r4mEf?Tt蓜&IJ_`{c ~H-Y)V5NɃQvqKh$KH~Pt{&MTO2nP(Ltv<{Pi3/󔴉_x2(=#5ȚL)ݕ/O/'{$ϚǮww_|nٮ ֋@ ૴zp)9;͠i4wb۩w wׂ!実f'~勓jLĮO]gK!9%]Lj+ӌcʶ3~F콸E-:LC pGIzg򯮐_3^`Ɉ_rpؕV?W}1Y֓pV}R\kr.QUR)x!=z&[=?=$o&C J0}7&)1"Fk"Csp90_SL*!E~[. sȲ]#/җ)jJN JHtV7TjZ;و6IR۲Y)Ki'dbj84 + 5VrуRWKJ_|isp +ekr 0[If*ScaCXגDLãajbj`=VڡuJxVSysO+^M&ծ/3zP͢DKV&{Uz\)J䂼OX%LS:v$u-W'd\U4iu9%%9":5ƪ(ݬJ+WPgGjuС@1b \F>t E$u8H@ޮe-r器46>FX^ @29l1 .iH67Ϧz:},r=˼>e].N&fuJr$0Zir<-2x\%dB扪#Ԅ@M%H׊/3ߋR ذH>qk"G,}] f_,؎gvů8`q H?l[>mO{41ՃYZ9[ -x_:'ЬZM#!:lJےFSlFSqk<&Rm R-!Yn!?Vǥ r +VrCƣ|D9t6 + f [ 0JJx(<7਄MX ib6i֮.!indYٰ|X9Иna bt釃9;s)a4>,1 +WꦡOHrP]ib3\PU>iIRg+ՕtY@AW"lsl y=S 9RJm:DŽ{PKb/&A] Zv@GJ8ާTzuˊŬ6< iWZnxC"YSqD<| +[tY/;K⿇~J{ A(F +kVۙԣ~ڤeٹDuĜƤwoD\,2HYW' U)b'^4XtYDz*`rHaJX]#/Ĩ4M9hU8AWGuf~逭tyjвVQyHjÖaKڰ% +[Cɓ?lIp>ٶdw_g a4#=L&D3a!1s/";mA{wTʇ!%,-PKJ{̖ _MuXDs(JaZ_PQst`i@(ZGN[+sT=WuԽz(ݟ6&yf&L7O&"2H@vٮOɢhƘ21JW*@ϲ.opj\DVY] F]V w +z6J +΢Zvm#4 +VW4QyY~JO0ْ3%yqmpĮ(Ԝt4$= +xGV,I Ib(3؜-^HKcDxc*JM9'YH .#]lartm~RG@r}䵆,pK#Lx{oRokp⊒0|x@h-4d^ ė7K(Ht_=3ucAy@Q9>I` C@,g. +>H- Tł*`u%yOOzxΨX`o Ymq1 C{,ѳd  D>V^R"Uwt`D7HbR\:B8.'9*M 0b# ua if|ʎO2wd O +§c X\kOҬ8X m*(Ӕ@HW}~%jg<8JL!<&/4P8`p7{8uU AYqR7 ljz.O%QQdIad!50Qt6b/Wj|=ɚFg.5)eDG+ f +˛`C뺒0dB x$Nhg[cyV +3LM yHOY{AkA/&q+5Ki+-lvbm-gm`-^'n/?2peުBE{-E_M0*S.8C#IQ c*u@c@y {^!lTl,aiL4זD`0/N۰#7ל+5MvI뚇·RJvvh ⻵6Y6 lxA[%WD+Pr]L Y^Oe &P[͆5ˡ&I?n6s?6~^ny/[jSVl,[>`Ze靠e?rUJLO#ogy (ZiuDPJ 05Sk&$w]?=p.{ aZ.Ĺ23껵;?uZdU}W. Jl Vex8w)3h[z]$oM3X9˓>x4f͖ey1fy+,6VbuPj% |3~ftOmdceo/X#{%5(Tzju%Dw-C +x-vw/3yBAm^WR#l()՝Tc@!p3QNX8">3_H7RXF0L4+$%Ea`DOI5O"^'yʠ +MhA*]*L)(Mwũ`{T$A ҷ +n*33Z/2?26!!~uE=z9 *<a鎧M- >|^YKڮN9D .ڡP5SC]pJԟvSӌ!h#"cc DE4X'Lxn] a P; CV)i@d #`s~n?Zg)uW=)[ˀz-Mj-iЯWv>azE)~)ni`i0^vW0X8 bh|o +!"hObU1wM%1~9G;:@n@ +rF&ThEg،:iq)528f$ '_ӷ✚UG%qܠp&3PDB*#$%3vcg_+:z̤Eh].I@FuivRǖ.dHR(IXN!E~PA,7?m=u7 |OSw\QQY&^ׂ߆pтЄAb8^e$4="=wy~˻^glh(2@HSIB)(1/kKK`۸WLh8c՘k۳@:i(9Äiy.PLh|ceI4_8;Te|5/3N@gd0!|j,Q1ɧֻ~ eT"2yu5< [)X\@i)oJfxv'P>=?I+&+fVR#+9̬nCYJե0XR Ci{9)?^Jpo Z\ꥨʋW%=I#mIY\v9?80<̜m.53EJ`Lh5&jzB\_./ҸQ>9t8to÷i+ c)D0N\Ke(8MeY Y$5YU +;RjX68ܹ#iT K62-jyΜ8ce+ˡ)J+'Yj'їYaa!ǨyOqC6,@lwIW).ɕ|AE +$Aw(` hH)Z9Z9lt/XXWi d%2lQ!*摫GӼ0[|xK~LZua36lXOFw6P|~M:DЖaCTnC*zB 9_5ŨALlUg妃hL:Os'Ceċ4I!mlp|K,˻ڔGby{w,:XwCjaGu=@7]vL~[.5I5a}Μ2S߻yGdvZt) 'rân]#-h)b/:ef0[t?aGK4d/:(1Q{XS|$/ٜ1Ps56c0LiP(e;F:sŊ`ȽωSZyɐK- 3eIXҨ j+ҸG9? V܏XU%wԚɋl)F[ڃfA|Y c#:_ior viSPn|佉un7΂J#|hP\hF2`T۶yB +L3f|b~Kqd÷V_BRd>3D#g-|]ph{fL/lq5ŕ4K\+/`qagb$F%6EӖH_+OSJˬ5K dN+ZDOdGt\xkhUD;bL!-Z@mQ0-L䪒UֶjmB~vcݲe߼}avZXIҞ9&}Q9O,AedH',(Ktp kJz:*ԁ5@ʯ{s;] 21Ӳ_2PGڔ=50eԬ rsAArP +=<)s!6"Rg!wQ~ϥ(<]9:R-?ԧem7AD+]%Vm]Uص#kZ2Q.N\mG*5U]{v#RRX%WeM:6V*4mM 1&zmMj +eFG$CFT9дg3챸=.p~/d_Ў=H:m7<9Vl16`&%i2mϐI_w#;PB8!(v@"TǕIqyzQcsVN&}J5.B嘟<ŝWɖHD%-ldK$["'N67S J0L6-ìaޟի 7p&SQx6"m%ji:N/un%j#>Q̼FW쵩%0ʷ8F~ˬv7iwv7iwG<(_7 I2}qhkA[vutU6\aIXNT q_ht栁q|=MQTXvkhV}BL8Uc):UfnoMjK\:u#RE(# :WKO^s-Ek"PПN; +[ħD˃yMU̪qGҬ:NI,fTimMK SflNt?;Uvj%5b.i<6]y# iY\GQ3ű.1 `]znN2vdvU^VY>ISnGM>:f#As*{)uOi%lR9l *=t ro$wF-cI5_JgUVhһN53$Mt1#@,k~5,>?'<2cv7W3/Qý0XB|c~\93l;M.PQ{Qfigh^SɅuR:]l=N&Uok+2{dɰQ(8PEzURCێ0{Y'k2S͑NF_oc|ĥjmY}ʹz*C˱HM֥$>\r(f ?Edl<gm!Je[ʞ!!(iGBISZ y,3nR}C+t^OOѣGR-{طGڸ#ϩ]!2’#K}CܜJ(epE~Rl@lhUdU/]Ķ[eOy8)Jڌ,oTd@k2pVAmU-C=޻*޳g[]+f%n. gϭAY)f@m'ei6*`dOjrشWTGTOr/9KcY!@4# Xa+ f\ bTP@шo_2GZ 4 +4BK㓿y۠Hڊ .K^?@)c,~uK RHVW 2)/O\ nl)C?[M5NU8l׺<nj(ߑΆ/\WMPh'.R&;r;(Ees-<4wPD㛘o e{0)w/ஔv;޲gNkHjF3SJS RK;n|k%>[o⏟ѫ>ޝ۟o^t9bi KE S@Iޓr-p8tb0_ (M ZOUޏ.7r}$<`vóQ*)o_g?{?u?wFzw|SɫenWh$| #gm*U!64TuCT/t|&a#hhˎg~}΃(= Gk6>US?moE;:zYV(7Kp`nr1 okxu/XhSX`&Z>%.#q0\'tF^zOIap)%A5 +,d/g$짖$S@\m0hެU0$Oc 1uOql|hאa2JE4M\%KC4jG)@4XCqQ bc + p q9-G_‘ 7Z)RĒ|U*ÍRKl䣒-}CPd &/8"ht :=фX t&$!:hu2}zZW4JE /zv@ W0nF׸E8Yh~.3\&-h|f@314I 7K__˯n}\8X=]k4Neg.t`w͠٣o5`d'youًhכ^n\=m~-9Y}gt{$0{7S8#sz#ۛl4}ӟΟoџO_?_zK?߾_tm=k8?qcY昖x۳Akm+hjٷf0ݾ>>~x_{ٿBs_z~7?lV귥4 f7woQr |ϚLqwQ{zvj{~^Moh2{\m|鮢ߞ환cM]ߴMjшI +| Z$cifGA?=ۧm87/osN_6 ыg/?6_]?~ԕ1uFӉcfOkhVhQw3lNj{}ͮ뻽oon/?W`׷;Fzf&;&fojwV4 &S{z_ϜV7ı&h\=6t^:z~~󟯯?bsx߿~f~Q櫇?^0u'Su't5vmsimcڝ"}˵Nayn|i9;&=O]?>3uMƞ#2FSOz&jL;#m A80'~j{>L/G|Yr3WϿ4_?W+{nk6Cl:Jpħ >WPc ms{VzW\/ώȈKa=Rmt0)*1=N!!\(39qvuXOj32iPHw~҂ȋF:D.?%!Be?(LUhh7G4+\CƶX5Q gg'馰|Zpj "A)і\9Fm|9=F_ɷ9s!r5RN-OtlsТ'DPuZ7%Dk<%/Xo<X.1b"K0&"%b:D^OH i. TKd"B2 2JO81$s2ԡ 2a8('<3?}V)~ ^eY6Б\:XG!t.2+^yha ç*0U=bpA/8й v&*.[rO>jr8&peg= *73Irß +YvT)Oj0 W 8b +`5Q2/# ǹ/S~0 φ.r X- BQ{\:Wk\0k1ۉG%[EWPl8! IL)_*}6&s|m8A W7 Ja;Ml ]~;:+2oG2a\[!`As6E@pU aq|/^+?ïLvLv5[صO?C.E<񅲎-nmL~V  +kEQDڮ %aω:% AK\>('ɔ y<扬еt剟i6M/3*'pcTMzcm)A|ʶgn*񌣩DHϸz0Y!JqtRC]nȂ|_wdϕ)Y-/mvw?ɓ$dH&IbA`ȅdR[@ t#&6M;ߘv^$+53o=.W>%1-æ%YGbaѸKA1Չ iT la +hyuLF}弎':6B) iČdw`e +v5ǭxzN7ꌇkOGx4G aZf˴9{f]~9?quѝ~9xш鴆8Dkw֠3nzvkZVw4'=31ӏFSP㶇?^wy嫟O߿~o~tux5W_;҈NAka v~h ~o:;:hjQ0nGՏ/?}b6676'xԅ}lqwƝ([^=:QdȨk?b{*O%W^u21aDgFnȐIL8 aP'0NDx8Na>Mڝ_^}? gXs~鹌2aR=Y.k(QzM ;<yB*1VF4Ώ**IvP娩lE7MshX>찤;4M"mG |A@ ndSr!WXQ.``d%ʋGM 2Ǩ` 86Jpes# fzj/2̎@9akr4L.mҐB e%4ya܀OxJ3fE9aa!(KwH +IOxǩ+)7M<(g |MzSLmPD^qSo(z$15v1 +ORS ʡpI9*ӛpQJ.oWkv)^YLbg*ċ$+2\Hb<&)̡5 LvI1csDWskz hwx[▪fF;l;_d*um $ΨU8JU{, Y1ggJn4 `RKWJѫVU{7F_CWO{-&@d8iccஒ?LyhqesD؝ʝ8cG^0zBGOh`,ᶪT6\Բ' .I6׋Ɩd,ஏpATinDhl}w6>N˵VoٙmL8SW `E!fZc:yim!a r2ua:6"O4҃n,o:3Ǵl|:Në4.+[fQnFxeIHm5a)V0 [z]JqcoVJ>qeRgIDIs,OmgV0 7Aة' o9tn|ՠ̖'9sV|Z| ; +I`tjzg(&ؐf0E2֌j(^D 튳aq[HlnGJ &zCL5# !1h|3qIk)"ob 5wֿ(w@#b4{wVFltGwu[TR8JEm;&̮J5sc* ZD?5iGQgl\R[h(UEM{sf=ƣ=*C8\Df>Þ wܭ U O۬lsMdݷ+R" IRVIܤ};.INMzĆGMM +1.lc!O ++FR4pv`Ib[W#ӸUۉqDDS}.ź]!HaR~J (fi8M7E@4P{|HwE G + ^JA$!e2PU8+-ZevtNr+4Ë֪OtENG<[ &Ϧ|~ +pF*lx^!c; +fP5L-~GGnQX]q9UHЄL+1AB٦Oem6LS%]!F8ճabdiWy\hi\fD XM3VF;l SДs༮޸6o`h1 KB FYyǠ +-JϮ+DnKL 8Re0 *J*P()vўP6sTc"~mױ{S$$RP/a-IF*-{d1# +m`e2"܍FqfE<sDh%<1}TzYq,-L7h "^rA}/72MGtp4D36p%3BʠCYݱ%ëxd1yTC@Yq osx`|wlmZAo&ݺtq8`+r^(d4ZexE"6g̐L^p6: )"ky!,D]Z8Y- e^JG:Vy_qf)NOL| gj4@(A3P+fq3#U9D茕VGg, 90s2?2q4]h.hk3sMy~ ~qܫD* +TbT; +[+l[E!9aݢ|c+9^(;64cAHHյp$:V4Î]Ch.f.=9Á7G Li2>,+х,> ^d692;"t0:m 4EANkm .g$j h U(';=2K B8(M<ũh)e@KT(T Υ9gxːPhLp)[~BSRAÚҊ'*q\0H$d Q1W8 z bs^뻙,gޫe%1Ž@?zI=mtH>@]6FAAhjT:K + T,Wcv~td)J3WJv=tl6Ovw,0XYѶ%-"":?` .I <Рe%d/`:3!8~sB !AuX4<>f}KKҿMPoVd-M795y0^b]t1?3jG6Y `75Tc)Ҹhr~/|inڛs8-a ik:4xpicx&39fR4?l^E/3=TY|x`*K6AZ6&:QA,zFMng7(-w$FM܋[T/ rZ.HMjH\n.K"$[U0iDt +ut^d*`~Ȳ<봪W9DmZU<(惄MaofS;L#3+ ȻyTT^[!B00;\ZY]q>Y r3Jm]Kҿm9mB|i?kZivv>aΫ&.p|ݻ[$i<^#(ځPwVLA/D)ʉci_iH3UEɩqwYԩC +PX[[a{#]]z)ۮ\\4GH[oZ6obžoњ5]u'4cc  _bxn6Sm;IiN OㅸCO*-]f6FKQ!'hbr0⁉Xzb0jZ=#8r#ƹ7|H hü:_]*@be >//)ڜӌ/z"5-D ׅß؄)5aqsƩH +'2|āGvٱmg v 0z͚8[&pT( Wo);#+L " `$a%{:R%IEZ΂$dAfaLOZ8n6D. wmΒQUW +ck{H"Wng$r@DryoySLefɟ"^&/$eV6v^GTa=Jl[1K.c {FdFt[ m"P&eilnmdaa6x'Iz5fQaXfP_H){,Eu +bRz"IoP}`%aRGZ͆VL 1]8kHO]o *T+hy gK?hig4P)'Z- 48!dL0R'gn"x1hڏ9lԔ-KG3@sH|:0V:~P+xTQ{="KK#7`sHY J'#S6--q;HscKKT¢NLV8c.A't.o +z1ͼdƃtv<[zuҍHd@0թQ)*Tf1jXUM#3/*uA^u(ڝT(9w=w]tx"m C /s6Xd1PdNT$\O74 !Prt늎!5e9Pt219%yƠdcPKUv((Q ǹT<(xq_9SO3D>O͘ölG\!Q#dnńf2Zuj1]r[ⱽ]kTg?2a0(k/o/ZPkI7$4ijK@7j1`m­ OiC!da;'+ 7= a~nnlV?܎(<8w/}b', +pǢpNzyC*nQ".ǎJҿ&aM?h]FRoQ~'Vz%sa&)iHV`* ?k,Tp; :LmPU5} +jPfܴr:`gaɺ2UL{ncT[aT@W 6CSg}:χe'd*eg +2kQcmUۺSw7TT"%@/Ω*_̇ީӺ +48%q8azFF3Z2ug0lG\Vﯧl8Jh*u %^Xq&^yʕd{cO/4)n=v5p-h +Ik泂&X5&U c/_9G_6'q;8b3ir<22Gd1f-GYޕ7+FZ3XIQAYq 20|;TԠFv~O4fL ZS&0gK>`V"_d)(R4R6*L8742:#bnA刲]7iEX _5FvF8$Fp?DbS~QH@;fnM$H$=Ȳ1%,_Ğbi)o zTl(Lx\p*:fT^Sih2EU2c^ªځGfW]A;nD!b WG &'x %(#btx"F(&EmpDܖgXAE]yQH E#Y^s9Muj²"݊Pa݊h"ڻ6C`LwTcs̮8da#Vs~n57):59)hߌ`Z3Z*d3׻u4Cm`5f>+hUJsTBԗmH,t]ED[us%{ps8S}{є$.p~ Ĕ|4G 4Ǎ1h}˞qgl Ђ7nY'5u 7T^9q⑧.D{H>i#fF1WAuBz[1 s+K/4dU bKq6=8I:@:zq[QWc\)̑d{M㢑,`H8@7tB<+i:LlG{ BS0IÅdUL*ٗ7!ERz(Q U8nFj4CF2{z39XD~)HtE:HZhL@=.8}m<<0Ւ}Au9zԈ,b1Ҩ]zX4-'4 KƘM- aǰƱet{Ia(+1Q%~'&*%q>9vl(?A$iE.h)l!tie7ۿ?hV.Tj[8{ģ/<>$&dOX߲ʀcP!ֺ % :%X8TŻR> ZwMgB_N/*0h`O@=a$R:YY: +2^xUyG+ȷfD*3sjc@i\wb[J 3S*نd S]բy6_YlR8Ph-kQR-\lT,G SjEo.fL 튅ZM`GɢhμNV wD/OdD9-pw-qWШPItZ@bBՒlR +~`{|u_oO?gBJoqXǍy1>@$p:K.%"a:,~S3$?.?&ο FWp΋QY;׷Gd{K6ŏgg`<<> T޼Jt럏.!k/p6{ y6xk큺7a;G AfZ3UF"#( sv M -t4hmNɳ5$"!R v'6gͣ ~acqfil-y|72.Yj4^zu&+OE4StՂD~Y585q iҵ g=F<3g +Q:L!0-d|{vOb(|VB g"%A\^h/k'z-]M^&#%$WOS )_p~)旎p\j"yW#pUߥ`4a=!_)h`=mU4ɝ]̴Yy"vjv] Smߗsǯ_4+ ̷ZS_ {u4-5 u%nQ zy<FCSkRS52q(7Ut_$yu$.8W+Aن*1M?"UO)|E25 *\"BM5s$x-< l wRPBl ~ˮjlIaGT zZ0V ZkEkWUV@4s~N-~Pܖ~@ {6*zjɆP +x$mx8O;aDr:Y iȩ\p ih0:r!z,@;7nP +_& ` I +tE0ab\yQ[ѧ2po4T0YzSFGY kr@LduS*XZfPTY + 5|E Kd55~dBk{KWs0 i+mgNEڢC-Q>G-q3dEGUϜls450B5Cn\E9ΓيHt*j߭Iy6Ep:' +S wJ0֔G+%[tkӀ dD еU$;툁0Rnl+(vX<ĻTɲM}RSN0]o=Ofwp*oDF:D}nKӳ:G$F%gFg'B +D[sI]ÉD +11$] +IHQ$ JJ\6Tqo#RXmuRN@ⰀdK|N(qquL@n{Cq`dq4m,~jDVF;_A=[5MY1j^m.|LAa-7%['OQPlSۑaN5a7jC9GfGDŖ{xtlYɮi0L Scz qC/7亮Κ6隫_{eE?U..*ku]T}爱^x yc24ڍa`ƽeJlH('R/O.Ie:"m2;7a3YWhwE/֢׹-}~U6Q?:ܓ*QUw"GXpnWzC@B# *B\^_7@K]% +y/0w,}YuYuQj*rZ`9x4* tQ`tB!Ttq9lCM0!tNKUe7glfM=Uդ6[E%bPiQRLrf1t%:-{#![y l +:)l玌6W% +eD/`p=*( +|nU`hWf^W\q[,+qQrreINqFVEEUQ^e +q|(z[yk1nlSzLTnzwf=DQdBÍ(馇5.zc*PFƒMS&Zh1CBQ\j㠺0ܬ/j-#0-߹΋2~[hts07K 3KЪ9) 1V4_N\AJWb^{fyh{D{"ݱfgNQYrP"ѣQeg s<3FRx3e=DvQd4ĩѴƝZBqI`VJ,7٘ԕL,πϭlV0n!_,Smڜh\qzFO"1AsJϚb{oOcxZUd(2ʏ5rp#|u_ P=?ӗ|q=k^~_j1_&:63$/Z,}Q7'A/]Uv- tK$:Zz=6$˃YKп0R9C҇8]D$ab̀="VaNfDr2 2V +N +S^*ɫh/d*f/6nMKHr2n<nw=ŴW"Yam-F_bዻ$9"0 G5/+Zx9Oh4m>u!gY64`Sh1_1`4 |W2NK"Pp76.%Jպ[ xIʜ,<r@"ze:% Pؑn|Q_2=B5!a4!]Ǔȃ4w|(_ݰx @OB=_з*Dh RM"PK Z΋>~$[DV=ɲvGϟ'z/B0̮}5ft/KQD΢gzǿ~kI52x%DŽŃ`OE>(3ւ}z?_Oؕar^ V=' \~|?;߿3"![¡GٙSWCc޳@Hf.Ҍ_ǤO>o2VL}0M6?y|߄T:Ɉ&PNDh.%,qD%XjXP׿LYīYAk X +(YaPGឣ*<Rx# Ka}iŕ҇sJ\|em0sΉ-;UwXe ĒRcxmVm-RͪZA +K LՋq;UTU2)bݦghl< 9_nP\Kv#]/mg<ʉ,&D.~k>a>Hujn'Y$sduq?NI(ɢKkZ_E2Wm8WMBk +[ejj@ͪ-A,V}[s#V.$!ӵP\C/@I [.2w"LY[Ik5ȧ>p9qpR"ߕjA7]ugM볕*W4aiQ&C- X>\bͪ&t\B넬ovoo/:cֹ + +E YqYf{zFVePև?nW,Y^߈ob~S'GN3H2 neAXͶ[B68Őڄ>g_N5>4h +9AN@ݖ7/07 WpA]'%4`H0OTLbp~Wl\Wg[hNCOLr +W[Q'G>"NpG \Ɠ$*f^tpΖP`?u]g 8g *}7*A:~RW]7]֐N5}iҠP^Mtp#<'Bvك K:Y +\\$3"'V2xOIcDA C|],ŏI/{Cb>#*Z2/EVqL7@O"hQ!)К)>ŇvT#Ⱥp%,DTxb!E GE=8 Sr^hzL:.U dД+Z4ཌྷrۚ^Ĵ<}$Z2C}ERU/$ N~Q58Z1VIp1IdD\%͜?ɟGG>}#nxI <,j=~2֋*2hƵGVTl{_JGf3yw` +<+NCPs_Z8J|Et|B}C\7aۍ**;0}M*=۹SMM!7S`*ʯRS`&{Yr:6Xett ȩ[*_ }\~Ow 徸nT.;*GGz +bcd;Z3aMD5,fd?d*e#Bxx`Qra'Ey2DXJ" +挙Seq'E;v \1뤯"v8p +/ZWr50X4'hVKt)pӹɽ5@ϟC`ƴ޷ JF"7,^٦|QaxNSP1JѲ6YW ޸Oa;ʹQY+J^'8mLMٖ2SohNMb(|)<2Z:}#}}CUMLF݈FM7tˑZ1ƨb21n6hO[S)ؿ8?p:уszqv̖aǤFh`romOY7(ȓ܉ͯ{A3 ͅ+Y %سO8* &YLkǗ'QC6riig7US_ډ<};uS|b#ט7I\Ƶ' +/{ +EkNǓds?BM8Os=b=bzI3Rc>Zfomdc:NIy-7@3)01/ON/α1[wIqqV$77&dn"%ܓILȶrJ+X\dV#ahQإtc+y7I=Tڧ ܖaɑ$xD^d-[bHlь VnxCw- qĜrTɾKnuڏQ soԓɧo6MSp]QG0{2#K>prr-HD5Z .i:k?yEx +sfP!u}R\WhӠ zGYHV{xN& Nhiԓvm Zw<QU9tm,P9JŞMq%i=:::wڇǗHO1I__6EGlt7AH_'9=p\;<=ˁ:}&ayѝ 2ʋ%Cʓ`mo&z).MΞ'.dX^U8ֈ(Q(uO=}c5 A-ӾpMR(ВI0&|⸹KK2zȥ$kX(sN۠4?rlc*~`w63w";222ܸ:% 4I~MZ>HܮL3GFjDEu +|%|N}ݼ%fxYueR$hAǫ~%5Jlcqjyjhdq TMnive8\Cus/n$OfGLx_2>Ȥwx{pJQh#2rΘnF~Qܭ*} +̄9qIx܇:=i FUZe`2\v=?Ձߔ)ոf_Ƌt,;,(Z.=+2v[aFU:N~.=:vQ 6ޗn'MtMxve7V>q +`zͫrLW/&E}GxfWa%dx$QGطEGdj$b.7kyNFtvVpkƅxkn:v.fDԆx%tJzz4x=Nt%'qVI#Gw-7yX-[7۰2Rѷ] `)5%sd5f[zbZGycNqx>0<(~Ȁ{~GvojLWQ8+LjQ?1F12}T{xʖz,ȹluv츏塽|Xkػ?Fp?=%~kMә{=QS= {^A:ye{8V=5itfu :ɢX^*qbw@&4kF4"]W롮$yCdr,wiqZYHv?>~o:ɧoAz+~y>&c$ӧcJ/>o5a e$^q>dYK{D!YnM׿LzYZ*Of+*ϧh{$$&./_YK0GY$x5C,n-^U"Z0nLO9HQvrt +rJmct OxD]:B!V5+N3uiB3J#D\L4Kp p/ow8 +tI7rԡQ:2Gdq?PY&\(@$^]ba6du)i/ܙeet>[|\p9(26uM:FUT]sܵ0GٻRnq@D5#xBiT=A<Ag[VI{@o2_ 3ܳ-UsE%2 l}rfSPU2N3W6CϽ5&&A>WP m?ZNևCl2fvPqfOb/ivGby{=%]7ˢՆ>xBQ#ıxW}Oc\EN|8Yq1/\0o ̒!AF>Bct}LzTb1'lFdT1G4YA.pEvU s R|VSܵ I1.ِ{s*8 sI,iN˝ezMd|Q_b, +ͷC(GF);Ϻg_K ~d)3,jXU#7cv5;;Y[\Q~gg " wW+1zdn%'=BB`I<g +gڍ.OTD0TkOrn3Z4<lA+/A7uߔJZюjzTU(AA2x)H;j,$s:@ٷx82&w -K봋Y:\ţsP0*}3cPy8]ăDx8`NJr83!j=8c2bB7_d׏% _> HmWFNo  ׻0` \g4JczIY|-߈Н)&WziՕ=޳zS4M6!?i*y ڷ*}q? Ʉ&;x1|:)KV ҕf<>0Mަ񐕇dk1KbG"8I|&i~=w±ְ{ ңeBg$|J}RX(ڋ5Z|{>AH Wi0]:Xeq<080[_&Ahyɶ6)<^[؍Zܕŋ:dԫI%0뒥 Ez=oh)o^7t d=8OaX]}7nd#lbYCoB-`8yl汒wb^jxɇ'eq~Fh"`QY;M&KOؔbh!md{V!r =uH!nzTkPg!zWSMeZQr>jw.zTjyUTZ +_w6qn:xST`Edd{VSvipK9dNInlvbfI+j^VyKŋJ 9Kt eI,I"&NjKKtwA˹jo/am cAܙ\d4r +YK=^p Qg{~Z,.dݘ*S*jAviCjm>{jdgs: ON;OL;gֵE2ε"B#EC +n)H64ɒa, mBe˶"ʃ@9@ Z%PڅM)f9F.LE26,nB8 +#?fJ +T;ar4)ۂNBe +ʯ$Hfw y{ [2=g<0s6D`{O#OۯWcm2VU*%}B/uTcSP]vfNр^JR{ + YopD8 yfqsVkݪNyaF1X㮔neW=sepdq@[aq0͒Yag8fl\9c3khx>V+^ZNM)$0޻XaxkTޠJ*!ea+ E8&hÐ} B$Vh޳ZG#Ϛ3lc"!"cvɃ?{TP +@9KX.0 e5 |2"؝ǶB8|8W |AXޞXlrZj1U^evOn}(meǞ;{]&^o!d.(OpgxQ݇X5O)JsˉC/nmpa 3GzĩfG:8zľpblp-|X4 +)ӫPlp5cI5CȇFͶeVXYM ~v"qB*VOBxz Wb2sW9a9:tPK)qMW9U1y:x+x :K/rW8VNu{Bw3KnC1 '.\mWBBA=`x_I4ġ挮W.Ud +NŘLs!D4C *z0Kj% F7 I\!(:enE,JPB abVI!2F4uPЕ"3#c4 $5Ry:-ވREבw>'evry3>C>= +tlVnaRhu +GD$nJ&t!2^%wjƟ79 8+O82{eDPjJ㴿{V4@$@٦f1bQ`$zͳt7 Kg}vU7ȗ}G]uA(UsK؝-Wwn͵.\%YuPOA2}|o!<^`m,J]:ufz"DEkˆo^1 tfm!w'Ip듖90?hhk<_{m[k88<*p۾ѡX au"^&WEn(%}],AnpVgo Pai +N7{ud6myS߸qs[].%H +C!:@C_wn% x?(qwW`y~xy۹e1a=Vʡ9`UDC7eNPgmA1cb +H nP4,)n`ktA<ajl RقqYԒV7q#G``zP]NUӵeW!@S.e*!)]xݗWD/ P&1jA2B+Œ-naVYwoăQx!}Mw@_Q!(qt_J?ژ8.TJw8EGm٧W+T*Jc4EFx^ +g[:)c8O9#[iE ɝڟ uE8%3sǢMKhSbR)܆*GחU)M-`ޥ6A%tP=l `q{`77XE5 |v3* '[Ii^59shqMu8΍kjM`3=іt+4K*`mo0V +BW@巅BZ(+#_~|0R˼k5G4-zϲZ5by+6*J4m‘ ]GJ0 =pCk`L֮2p)h$jղܬd:2bOl6 q'iDdΛK1N.%gc~YN;lP>O鵯jJ-4_\\!c_ ./bj+ ++C[X7*r +& o{t:+3W<ȮS@׊ ++^]u9>O~E \Ҋ2V0d^мyŤ/ +O؛[1h^ؼeU W $@Q +^19~%#V5bUUW"8_0|U:t8*|UCUW2,_`*|yD }E0}@}UzsȾbоz!x_5++W<ċSG[՞)]sP:\d"ȿZd[+VJ*X\`UyEF2|QR J ]ZڏXvZVK m^ \@;o^T(Esnsh'c$˾wocM;EF|}Sǔ,92%KAŻ)Bӎkgfr+^zd #t2X +i9!}9]6ù޸@U`1ogB:9)LEF/8;C󊤠0@Sּmf,`L1 ?廿Lu.0 f];$ϣ5oKl@V=ֻl:o#|h:oʇ]4Gg2~sQ6`ԓ1'7 +u|+t+b-@r ijDX6iB}7ǝq'qЇ;йw !ث>ts;;La\3DEpmN5QH 4h fzQYF* Xnc`I1uM>6粣d(-Q1L՟"ثF/1:;-syx]2ŏd]YL#6nIޯDϾ=56N֭YYq2if-Lv?_-*WT_=6 * x't=X#FR'%θw8(pO?>ÖH+4-!sHqJC~zN^9jK GkZ[J^b[ii] Rμd6X{{ɛdDsWr <('L\7DnC[L&phD:Tԡg]DZaWM|x֠Yj>̼Fi'Vd/ZJGb%p<7tq3HJc kZBI}@Û~\~;%"͔|hB}ũ,@ )MK6j A¦)qgִ"w4 Hbn@QaR\tsK' [BJU|a;ߝ#D p+['{$0{,oOXpKU @ Z /!F\m$m9Vֈ(;#$8lw#4@cz'<۔`SO0@ej_*cDTD/TK~:|P EWrLvMnEڄ\T;6i *:,AԠ]RtWoHRi_WRWR)@~3UZ_Ǐr2c+> k9N?<0goY8rE(XJJ.|zO:%pu:b`x^or&\!͝3qIib&w`Mk]II"[k _=4I5$VD/oj+HAB d +3bfhN(saJs]3RzFKYI,\Tu^kxQ2-tAcHtik室ꡂn7f Cr;hv +:UhnM;]WrFGon$[#8\ERUmDnۆp8Rq +a +=w\2fGVfo< .yNY#)`XUBA.io$N +\ 2AX *x`E"ݸJW\d =cpI|X$c-Z)n"" + u&Bm"5A&kҾd$ٯ̓jyٯ[m%bf[#%`*aS^GWT^fC~s{(LѫAlS|䫹{Ҿ"R4c9t8<=6?'O7-><ѠOK>\s5+I:@+r\ahOH^EHjP*:mztS>.k3^ҋ╀>=,xxGcB _F]||JɄZ+DR'eU[<]jԥ$EjDdu7H]wi^5J2:^cDYn +B؋vs ,D@;D݀ byЎԜL sFL8-Zvs!.niy>I˝zU6>E/%Y$ٓ,zCrM_dQ{h<+66A4^`ۑ?#jΤ %+{ uwn *j.zKK{fs.uJ(]$80_Yi^Aʑ7G(^ ScpHG袟>wjV9[j`V[% ;"{`tv*Lѵ8K#)?kx\K(]뫱X JPҏ"PBuD/˺a)D1HKx!kԛLRw fVXV;e%-׃"ϡp4 ҋ1qεRK{N}܆h>Or0| >l\:.A܀ i Sz2qUt[_[M]ҮZˊuD`nS1J' -Q.`ܺ,HP 8Lc +i,FYO^xH'P_\RFY'aR> IN%5ɸV5c@C@#,T3\F֋=ϯ]4__v - k on0"@'+ H Hlc6O8/qy3>,^Y†Rhz36@w%mjl>wv3́Us9̧asmz1zW{KDm!9})E.}(/ڍ+ӉELDzJOxAtdV+uX 0])t;MG'] g+UaE kI{^vfa}aL]B>˘,A8 INN#B-cшIn" +1I6ZLr@^ `y5cDN˂<"\u(DOvLJndc?nC)?NC/uۃ`pA**o l`jжڲŌn%k몿ot[&5} C +W5<8q8VE+)d2pQ+C+{i_p?=B]H $vsUily]tV&iɐy0YIHP*$XMD[햒 2A݇S㑱]j66eR#Uag싢ϩAĀ?A2l-Uuc"A'hvtSϛtӤ4O͜3$ K{ӳ960Ul+ykdxI{}yi:;=}^7?Rrn frů! MvCyj&pY"5Ve< =uqiyt]\iEoHaTrI:j]5+̕@5vqOJa/|PGŽeh:u{ 9ے;5tʨw-k3"L8Su4նjWe9Yk{uz1]Y +?YDK-*kj\I/;P +Ih-4v7dvQu~{b+Rd oD+0v%wLͽ4ե}ZP-E6 rG;}j,׵U-HB.WWj㝨u 9M_m3(vw@~;طjV ,~ +<7z+6kL I7 +A mL۵\zY:k.axvl֜^|SK#*tvލܞ2|3T߲7j3hX>jW"EN}e&u~ͷ2@Ԛ7@p6l0x+6@t*VyZB `p)/HE/H(X& 82#e&LyZ& d k;web4Ghqpߦ +?}4b[y7L9X!յm$iFalaaET%w,`>E2R Եm +@H[*G!NE.$ϖp'y(p =Ʃd[XpqrjaYnw? ]! =bg_]V!;)'HwA ҝthXN4'Tw.)C3#9Ҽ+xB [Ly҄}ҢeV]Z/͘:lŸ΄f $I@! &%:Z eWs:Đ5sh5d1pqd)#) rZzo_@+؟cѦXL(߳͡p^VLX=5]KX#$EpJѼ`5>~زj'I?wILVoa9y4bNYDzB>"y"\T{N䪎uA +r湦QgrIz~8HHj*$e#Ƞ$[({OZr$?s1-Rx˜^H&];ȌhO,Fu}Q|Ym+m!@3+ܯՕ1͵o8#}Y:o-JVۇٿ=Z]*Kri0Xd ]%&w1Lw.5,/t/b?W<5`^ëOAk< hA +ZEz➯uRS ϗD%Km<`V0ف,K<`Kj0ar( ɺ:\b~;%,<.1 f]`%OAx tKaZ=uϾ/}17eu1_p]2mX)g-N#Đ 94b.ͩ5qp.-N171gVPrUׂ (A JDv'bёdS G固NGҚ IwdtAP=W4Me/s D_!R*Of Gh?}yϻLRW3ˉ<r5˳S r(JV]Xԏ`E{ tr +pї,zN_8ǁfY>x ?foRKyESw]Tk*ZڨC(Ğ->;rz:/Ykޚ"zWʥ8u6: f#3T,/'V[IUL-Ni??\*LR+d։N/RǷŲм[|dpj] ļN$9OD:PAٲ}B\ +Jxlg rݟ1KU +v~*^۴>N RȓgmQxTDxF~7{BocSLڍ!3%u!B3*'\;c Gc\ǹFin3[u'k;7Y{gQߩN[~:9xtԭðyό9؟-[s[=(kWyboDz9a|R}@\olyV_ԅ909]cd|̝z@seh2ҏ vz+"["͔i`PZi(#W6fgĻ{M3~Rfk`$cb/.OdѤ Ĭؑq5Ȟ̎\ 2E6JlNaq@sg Tnfau/{U~w(ߩjS'4 pQ*65 jM$ftԩ6mfkrn7މbN~, eHO8]y78rhuLYҒi)6)0:65p ۦe>Sr7͗>XS"Jp~62ї ѣJr#Qs{GuϹȟf_DS>UAbbZ(;[Ơ ӹzZnW)BǛvkV._>ڔ❃˻Pbv76\`p/ ^MoO *%3\|qG+$s8%[ Pńn%i< +jKhbnQf r{ @y[2E_сh_ХM_R@hќ՘?%*z3["+y4epoJv`, +KC6Z#%XuW=GV1<{@!YH&_Jϛ 06O\-y"-@?.MP-M3-h>Bϙт,Rg Op)Ky&i{y9h  -}GGFl7 ;=30Hէ3k4s[pIN0̵4ίr`ޞ Fjɬ=`Djj͖i,rheq2KXZ$u6_`?[i|Og_lR/cU5(AX%aadZɽ:Ah [1^#d3hai}0yO'*dh>gOE|x[>4Gm90Je^cccY$BP5\|}{xR{ٯm-.2%c׷W?;~l!`ҤU;*gotN f8ڃD 4 +ݍ^DO/Gx u6i&ΛWӦ8S+S=Ve0O$7~})m%-Jf_3Jl|B׈ƻjTPF[X~ʕy^3oU ɹSN%̸T`b%wKq[ +V5z7Vp O~Ŵo>:7$!_Mw6BO5VRF\qtWlmؒY&3}V$⣒֔3Yfb-ča(\>#5{"9~#謫xŕ;7N1L9c^ztTTtwcѫ(@XG͂v&l-wK5j78w͗6q䏮Xo趜?\ɧ|?̙Kkô$S~/l'ȦY 6S CjOxTxHD"a_XEݍetǣ ?Ϟ8yd).R.CykKǚ|<91=rCFddh/JLˍ'퓚 <[s{I=nWԬ~tT`7[kNۥ O*3"'F:"=6J;5ď ǫ ӵG%AuX k85WlRgѤ\@P`1)W߇z]֌G\ >T;Nn䉘aҞs$ag>ڄѼߝ(&88p4qC3r +pId"Pc[>62ic}gx>^=Є>"{GrzMۧb+]Y9Gs:#Zv2|@f˚gh+͸+]TE.VX@YERCЧk4)Ž]&oq n۳ϥ678NiB{ [;5 Wk,lH !WAp +ډy0Х! +s!HLh?/ǻJnordg] I{%䍀ҖOfkdp#+tCC&,A^lҲ,T,G S^epyVBH:NMZƓڵO>ڽސF26&Po*W99p9)䦻}2o>N|)UIY{b~k;sbY{\7]GYehQCazՃ:rFE"4?-:UqBpc(-wZibWexе*JZ2N|?}Yn6ۭas,v[I08w7t֪[VmЗ>HJLӜ;Yp)P hy>М?1&Mv;?fIw$RmqM,aKo]rp\5x&\yzaqs]-rI\Z`RFgAJߊX|:fV ?b@(x +TiJkkgpv(kDJq + b<" Vd6# ޓóa]'[GųEHq'3n xm&{ALPp/e/79}x+CtP. z&yh jϐҟ+e/j{7rݵg#ظ?#:sr&L!4mfM٦ؑ͠LRD<v-ukEy3ҋ/qE8[QŸ}Oy8Ӈ7QMEm_'dJ`]a8F{,i,mwN %5Xy읳huJqa8ڊ ҥ߈\4uE=f̢.dacc$dY. d&ӠB@L UmCS.xzvQ MЧk̡zWT,`/逷@Js +Fҹ"!Nsmu_L^rYPxo m#RP8T!<=Y- x`Uw.G^Bt>֎duR` ˣ2/A9PyJa*pK|NyC3 CJWT{{1p+X #("8'!pK0'qs2sH-7;\Fe$um@5I<6ϗ>ؼd7RF2%)gn\|Ř~O\.q:1l̽WQ'\twVQvNaƦ/"kk-:Out{U6mvO|Jtyn/.#>,Q)`, ww.s'QO'mZƱ 'qYg]2.wCG_aw|8W'^AK͘K u(ý]`!; :\3\h͡9@@"*QG );CHWo=/~6ש\R؛`*9>s =h{SQ列JxƐ~6@VAi;#b&}ӃBSFԺAƠG# '$ 8eLj:i?$._Q;Va\2~x8(\TiU*{7߫RRQ#i#քm8ІX 9 3&yyJm_E +LlOY@塢@adoJj%t0qZ23kJ.؆ P1{={azxIq*s0FӓI^DÎ8c_4p*ncpZ(:.R:e_J#mjd~F)HZQWIގhg٘ y*v%X}!ZztvFQyi@2NڠSj%!U`h H8mp1ˎ ХLS__SV`^ ΫK3˟ܓslгB};Dk鐁Y9P62[C3q%O~* !!;p 砆B|ڴkJjCd6x o0lhR_Otgצ+dVBS +`xε6VӚRܤ8* 3`Z|_@@3 z5R4%$$X=]d4?pN@@z-ˏˎQJ'ǚ )cH+"ۍX О+Ȗ[ rӂE(m j¼+ظ8W?dܷr , $@\gq&LΘ<ʴc)`/g&M$H߃?C_j`4}|> )KV+otݼ{'vkSX3!&exS/jYH$(/0hZ+$ M?V?`}#-4Pq^X +x֡ \𹺡BP-%" A}O.sbdYayD~Gth-ߢ}7aM6 u}< 1ps6C12B-B-iS\3ٓ,eR5J$N͑NЇdGV~4$DR-R"߰!Xx\*dV1ƒbw'J:7Y D-(/}0]S͎MqzЗh@MwӯmLIb"Z/`Hf@u'o2N$:uF--pZt*vc]c8/2%s*e ߦBd}]Yh $J +ÚA'I Z*^,웢\Z@b Nڜp M4sw,wQsoh9/=ǖőohE"w|<Gd`{[`j bKvfJn|u|9p|ȇk7sR|B7hxI[[l־$y\iV>1y2M`G2,"#wM" 4bA:}e^B8 $(IJ +c$ >fP>Iryg&܄Y/H)rHA &TneA *wt-zKWR?mu҈P6L.3@$ 03ڏ@<רsZEr(fx) Z驳Z4svD:?a]u*T +z@RE4[z+ + pFS1mqCѩK@>mQT ^@6Y*el=Z3`B+j,yl,tȏǃ' )$H!̶( u:BFeȑEd džHO,_ЅMEӯ\o_pB).CG5_a~׸zUl/ qVf #yZ@ZhRy!Q 4i᰹ݑ5)`VQJJ \KbEs(ahWYYr% +&I ³%厹w8h2YMSp Ƭ`D1h zRy谗SXbX[ +%GՊ5 2Y-.¯,~pW )I7I8>^8k5TIUҦ0v/`Ibbq/ Z8g%2}B>řPU±:A( 0^#8ApGl=+ݮJMP:]n~\4X~lp -ȍKJܜvP@ws%r{G"J~xQzYQ6_'RIwXA}<>f"G"JV.RI1Hqv"e0Ij$J~yNnRQx@E+DK\Ud|_>Jn&w>_} +#H`/{+;E{i3ZF%LEǰ,6/ TЃY}QLnu>;xx&܅ "9uNjT tmOשK茯mޮGfа[u1[+p, y 0ҧ~#`N' +HwՌ>~qۨqA{3nRʶ_Ъ5q_Ӕ4l4hy 7%@C&.C& G5pxol5nCJ0Dt洎kE4w~Ip2iRaXaٟ`q~ai-^Umå2}D5(NX#a(H5B&ւn1իO,9DӨͷSu!ǖ{bˮ.^&,䯍!"`#Z"WViinM&x$ʡw`SG`s1-;zKs!{FH@OqRiIZ{8Y<&!DuCHk ! + !;bYrľ;R>TCރaaψÈ0"n #wbHM5M^`wî9^%֭tiu_k7-i6 ,FعޅXX~\UsFZ$XOO{tJSvͷۤ%7]D:Hd`"p>.t|E|Yt#-V!Z=Cai{!>?CC8Cv%#8RG{_mVѢ 8AhZzN\yFU[$5f y| N)^BNȝtKoÝ`< +;'ܙVrLM@ϊ>Iv}^}VH}^N"**?bFUo:i[*L(gPGivr|?L4o esP $cb]tW`PP8߭X>dsݷO1 bB%tT ;y*_hX^^zo^ +fzYF| OpC|.MDTc_(%ߥ82~<~YO=~ioI8#mK'TeTߐM+QqVa*Qvl5 n0 >-~bd'q^FpL R냦HF`{3V<7 `0!0ʃ4h쳶 ~#n-Jf)–Bisc=Lw5it@'tYfvbh'⒙T=Z<`}/I tb:Z? gswX`Pi;[3%S9Ӈ6|΅,^Fob jb+put8/c_@$\HIЪJCPW7'ZPG JάqE9좌%Fx/-/eKWYU`z R"Rlۀ. A{Jhv|jJzUfɆhv}ڋdL0;h4Q: IQkH ;|FaVkjbݗݗ=9os65hfWπ"=p89B}tZ&:,/[gK0{.nt#1!(|Ze?ρmr翍m26 DO)K풭% F&y)/7_yV R`7 +٪qRBa"H4š037p6lam+ "Nk\[> nUlZ4#47 q@_'NT1!{B=шي!$pr4K5ג*j`%5@d gYCCB#1VH2Xπ TW5g#*BSI>Ҷ %%w-@LÖC|UL_[RǼ=s`Y!1C" +G~C>mzY{#ao_mi*PUEi~Ѿ?*"e^<+5hOGOz\gP{>~5KL9vm?PNzEs@y=qպn}wj(v&f/ip5zEX: s=GFi!{FAr- xp 53ϥI _KɈTGã9dGcP,Qڥ2y=}bV(I#+"o~cy!#LPWm)UH ݑt mBYLBǿ6? X/4y'-yݚbDi:tuC|nh9|#G`# )N + ^ǫ=:ݴ{zGtay5kY˱o_5Oy.Ug7CZB193tkAtM#;1+y->2@"^IHe%¥]fȁ=*cN *6,h5.V + ܤe:+y2:9g7ߏ:>mੋ]YY 1M:֠j67X_ln߄˲GovuB/-gRiLlE7:RXpRnvnQn!s40-0ډvV`?_ t#Q_Ry&7_[w{,MߘDUhQ$ yAՀk!T+1>/;3@~`wϻz_PtZm]qVN|] w>4Itā7sٱ +C9w?J=<. NUTd 4|\;!3u4xC̗eww΢fj|ԮQ* ~=s9tH^_C3g;޳e<3n&y6j+\(LU_vľrVfw Ck#\K< w\K~E*N}l[ ؒ[-r`K-l =[6|D6vex- [7ȖzCttȖvl?KFCTE ~أgN)Z.pKƱ} +x5b͇ZGX /\bNH/TH|"#|uwOcubNKyfmj×N0 q;>ԍGO-G)\ ࿆kmn/$PD.~mCF`)i 9>'JtKFxs ,HK"kMQvYZ"{֏A\V`y RʈTӈSo(%`?„F,ɵO8fMy$”$j4)!E?<y7 z{y7V6k-u143:5ӿf\_H/\vyPA'%aN/pvXs\my3n0 8WFbz; ]=>8M>RƱ"WY~C9< (m.²Kt]xcI'Wk<788*0dEO~A t*Eč" DYV8o'Gmva}+&gEUW௤6ЀDzG.]LI%F]tb7%u /:7W=-w!n*=7xA]Фdb.ZMU ;21؜^\nJvgB.wMȼ[a-/lٱ dkl>8*p}TmYr+D]#@p ~'ZΫ kxl$_ӟfGsaqt,˿ ݡyXrPHJqY@^D ]"2}ZYv.OJNщY>H_VJٷa}'[b8^rMn..:<@g1ͬD{ʤk3Zt^9}xKD@P +&QcגBVKV U{/iwy<ݟ?}zޕYOOdJ#WfRǬxϭѰv׬$F!~' +2p׊S|Nw3E9a(.ҏ=玉kKp t tEJԿBp\ew8a3@1l}YčCe'$D~jG t HyEZ`Y{)h.Q'e˲[58GV+ fP{M}QzeqQx +HL>|5*.P 7JAY2R%@(+q}9mn$C TcEApn)&?VbTw]QZNJ􎢢n$]U@_cxϠ:V7P/8_~#x?w_eU/k&gp\ZXRGӣWN cW'_7ɳ?%E90qVJ+|a6. +}z1]V.W.蝱#)?O9/eZ7a}]eN8ejqTBd搯%h+f7W3&CSB`]"* ߟxı{'w'm\|c+;.4b}PUCxj(¾$LZAmjϧ9 wdMo& }#0#ф\^[</54,|ꪴo]ߵ#2<] > Զn͊ N].ݔzT Sb+͕iZg,\EXC+_2(p/ (ef8(s;M +dAA'|Rh/=qHQZ5ΒX۞( ե<<2s68.߿?C@pJgtEӠYy xMٸ.̾+_H4Zi<}EC?MukZ(F^wяkdg&m/G;[5ߝvWO^6;zMw-ޣky>+̼kK=K5^F| \gwcG[e,;n[`x֖exr.( _Fmη攈dPD1" !7V)_=s%Qk=/wD_N7O_HI<ɵAhK{?BI0s6/A8R1ԙH p6, 8oQ??7[ 3fᢓ3oii3fþmcIviL/4(:b?ZM*W.U{5=N]5 xKf>_vj%ɿE tFڜ،wX8ν0-.:4xNnU7e_~ARυ ؖO vT-1?2 ;4x6I'YkPv_:_CY.?~9(a(iC:p`#'wnmE^_D^ryUuxtf6!zm=Ƅ͡^o><t|bzZp#7 +q{ |!zk}%.DOC<<ۂNx?5E6"{K0"n5-xKpQ B5ju,1`먎ю_=<;m(˛ J~0ni~f@;vυ/gҊ;I^=:p~ FWf-7zٙ@\Ic_7eO|N$!ܓ-%6Bcd-3TѾ_rlrzGFbcPX.d޶a;w G07Y^IK| +o?Wcr8Bߜ%W,Wjş*¸m`Sh"Awm>4XU@y^a]vt Z+z0p`J.~ܩ,\8cutD90Vo^]ʼnGSmN_dy yk}TN,xl9lLꄦmQqZȼ'HҷAb\‡gCU 22av-~[yQ%W( i)jI_o'ANu4g4gn aAV"V+׳C-@!KlN(l[yI9H2!s d{k:Qvh렣3u&X[y:&/Co1ӊ64G"<[ 6xЛ^S9dG6m%^te6K17hFƠ;LE¼CDňKb116Y7fl` +fjStj/U\`㈇{pV>Ki'ᰛ֮4p# 6bT&ka(f_x!cl2v//ě 816n^JjFΆ vGNt 9Y`=rw؟z_¼N\Y3^^ůǿjM)7<[o 7)@}'R` +WWkqL.qS2yBpڸNjMWN9yKsW1fL@na妷X]jC=q o?%9<Խśf~6N٥ӯ6{Fh糲<emzZ9X6C?@G1]={2Sj1|[(|Go ًcVlqGPZ0RU#vA^m3. (pvw$[n~p|X";̬Glm# K8@&]H}І% TbC[jQԹ-P/-/z+̹`܊/U1}/ekFΊC3g 5Up!+ \)0_ɈCLot_ք@_N} ͽc5Z}2~7yR~C̲tLFWJ߫}*͍W=^i)7O c%V`xry"N Rs|x[HCT4Ҧo6{:.}Q`$\\;/,!&:TuUp\^T2ϜuQOȱbw`L}I ٙpG!&3D?"X^*yQ̽t &-I]_N|Es.8*pz47E l"jASa\bqRiXaQ 7AP{PP&B e&ך^plȶfu`I1cWh3FY#v\cP[9d`pP6Btvz虃 W \߃K7o 3+FSGtQrL +i9AX͌"o:Ze[Ћo,n_8cˠTƅiyw|9>&T&=GJ6ȹ/[V*s5]f 6ЍgP!yzҗ5!78U >;@,xV>0!_UPnUkߓ"ޡƋӁʊXX-)e!zI,, +p$(ȟPI72D_M>ߚTzK5lQɢ̇oPhljZh~ovId2%{~[:fY'v#~Q#m srN8g"qhŏ’9j1-irpLHLaLYXyI Js)"^l$Vo,MVQ, 351'vf#Wh_O尝`@ +يyoEʱQ.[z6L3QG~) pU%PU~T]k1aDꨑ𪍓2Pml˨OUajTt)ȸΑl qtm*@|y_ tyi z/kvtc̩cǫ 7py|M(9rF8KHE\ guWK_sA"ב`ϗ񴛒_[hkp"zo`!F8|#{.O ۲4WMrMw BQ~4m ,// &c?b0~?a"v"3,؜>HC>/hŴc?2xeE'\`wyKCp60ldNg-9bahUKmcl`D⮤w=.)łW3e7uci!k3kc:*䰈avuIfbrՁ]?u7~7@llT%zw5)~p:~2)|cӇr.@Y+篌W -|1Lr9㢉03J20=asb4T=iNKɯpf@*f2A4xb@LDL{q8{= 滳Lɥw^ f &N:8ЭXH9a|R$MЃt֎BoyވnLx&4}w_h;P\46)XE]/+hv-d,*;N""[p5#Lӌ f4$NtFGJ +эZB0ź3E  MOy"iJ',[e ,GE^;I> IKKw@܄$G_)JMSl)C0טV>#EC7oJj,W)߶7I7C,Ց]jL.M>n»w{k8hA3^ +L>+.rRwp֡moqZZD}rnrZS#!mT:/ {zBLߓn^1ť7е|sWS+~W+9%`շ͈kd0BMI\p7ߡx3vOt-ޞ{$ǧE"MOtv"I׏y1誘[I=hM^JBGK*WNouҪ *ްZПFzYoIL$Y*q)ɹ/~@lC7|I=,tx9_LOB{s%HRXX5 ӺygAw3dY&eu%&|œp2<}}ITs$0p55y6[_o$^ۦ_ߦ_+3'ŅA^y}`[$n htdhN㦑+l/X!QH[IMRq…T]xb~A"-qS?uWݥBXN'| |AO:zSG{Mo+ !lɊnN_~sXqwyM)6'w^cPl=M~MgZ,D7"ن֨tm'ⅯS>AT*PRA * Tz_6V )DQy +L#ȟM/j6H!A +yNR"IT;ozűҪtIT ]i=}{vOKW +5!B21Iz/]|>>9Ǎ"a{YKXz𷾷վ` YԻ(ΐWLsqyщVё {EEV; +L06kMX}ļ8(셊FHF@ؑ Bjx HM c0iy9QZc+q֜Fz9X]۬iGyNx^ЗH& N{h!&(Ж0^@@3 &'V ȧN/@P[i2c>Ä&puZX3›}tlUJ /󖜊!t=`B7%Xͭ@xpj4(2^1Q1 +>=dOX +C<@#EDkYz PК CGD*( HG e{<(4A;HBJ +)=/P0 QQL2,ËT"A)~BR徼~.+ .2{w&nUe2|>>>,} +.#%D%z퓫|s DaU&7ym|,t{[7vܣxΰBx +,)#HU잎Kցh9%n)Y?ʸơVWOhZ_Z)3ۻ n۠sp*MLL I>UjKPd@]zS\{1"TT%)+JihZg vH'~G얅#Uq0R +# 8#ɸE豤[3}ͨF$t.5؄I\Yم8&pYr?&pA{#cz}*, :$UZ̩-,E396]GO8؟ u;ʬ!$CY8mD Q7. 6K܈ѧ҉ +DyT]'PtWwiSB/ +YUMUKflS3[CxV=TD=I[o h;5wZkY&7 +qQy $j˥']E[ ޱ>oHG}K^l +Z% +:\"[= z r7$:aNyLJ ǫ +]͸k͍onSjݪ$P2"?{l_k~1';T#7M}~_|H0#[zm|=0ut <<ȖkHb`eDV(N ,xV]‰+(NJT?q~Ŀ]d$J8YGKpϐ+sFJőF0)MZj*lv~kyjIb@M5{b6P ڠluچpZ'U7sЌxމn o-V49šㆁ6:pܟbO2 I$^wÁkZy36Ym5$Rzx$tC \2ܸC}ᑤ3Obps!p3E%}L.1c“9 l%xj}dۗb gX[J"1g 7H!W@}Df~fX4&xN_)T64bz&x6p2c:K)/h^*&% bF ir wZ~SIΞp'=':O /#9KfRm=Ti +h˝=r4_jnd"tލ{XH|rO?1%+TH>^Uju,L哜 !S|+R gߝY9.D81+X}2KvnJj|}#9FԨ_;]rs'S3(9፦_Zb)SK[M-FYXͤMzUf*ᕷJ9QUhD:( -E'tY +>W~²k+!q&)~0}4"()p܋VqE)哚xL_tl[:/1+yPZIiNK $g}E0{w|vˇ7`>^ִ/7z{hG(B(濧84H*dzE>@im Dg3T-8>ɀ#OE';obq]C9x&]xVj[Vx~HsetLabel('Internal field') ->setInternal(TRUE); } + if ($entity_type->id() === 'entity_test_mul' && \Drupal::state()->get('entity_test.required_default_field')) { + $fields['required_default_field'] = BaseFieldDefinition::create('string') + ->setLabel('Required field with default value') + ->setRequired(TRUE) + ->setDefaultValue('this is a default value'); + } + if ($entity_type->id() === 'entity_test_mul' && \Drupal::state()->get('entity_test.required_multi_default_field')) { + $fields['required_multi_default_field'] = BaseFieldDefinition::create('string') + ->setLabel('Required field with default value') + ->setRequired(TRUE) + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) + ->setDefaultValue([ + ['value' => 'this is the first default field item'], + ['value' => 'this is the second default value'], + ['value' => 'you get the idea...'], + ]); + } if ($entity_type->id() == 'entity_test_mulrev' && \Drupal::state()->get('entity_test.field_test_item')) { $fields['field_test_item'] = BaseFieldDefinition::create('field_test') ->setLabel(t('Field test')) diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraintViolation.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraintViolation.php index e12c74a..ea23d79 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraintViolation.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraintViolation.php @@ -3,6 +3,7 @@ namespace Drupal\entity_test\Entity; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; /** * Defines the test entity class for testing entity constraint violations. @@ -39,6 +40,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ]); $fields['name']->addConstraint('FieldWidgetConstraint', []); + // Add a field that uses a widget with a custom implementation for + // \Drupal\Core\Field\WidgetInterface::errorElement(). + $fields['test_field'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Test field')) + ->setDisplayOptions('form', [ + 'type' => 'number', + 'weight' => 1, + ]) + ->addConstraint('FieldWidgetConstraint', []); + return $fields; } diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php index fb69ccb..809eb1b 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php @@ -8,12 +8,17 @@ * @ContentEntityType( * id = "entity_test_no_label", * label = @Translation("Entity Test without label"), + * internal = TRUE, * persistent_cache = FALSE, * base_table = "entity_test_no_label", + * handlers = { + * "access" = "Drupal\entity_test\EntityTestAccessControlHandler", + * }, * entity_keys = { * "id" = "id", - * "bundle" = "type" - * } + * "uuid" = "uuid", + * "bundle" = "type", + * }, * ) */ class EntityTestNoLabel extends EntityTest { diff --git a/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestMulWithRevisionLog.php b/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestMulWithRevisionLog.php index 5a17b85..cb9c7bb 100644 --- a/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestMulWithRevisionLog.php +++ b/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestMulWithRevisionLog.php @@ -5,6 +5,9 @@ /** * Defines the test entity class. * + * This entity type does not define revision_metadata_keys on purpose to test + * the BC layer. + * * @ContentEntityType( * id = "entity_test_mul_revlog", * label = @Translation("Test entity - data table, revisions log"), @@ -21,11 +24,6 @@ * "label" = "name", * "langcode" = "langcode", * }, - * revision_metadata_keys = { - * "revision_user" = "revision_user", - * "revision_created" = "revision_created", - * "revision_log_message" = "revision_log_message" - * }, * ) */ class EntityTestMulWithRevisionLog extends EntityTestWithRevisionLog { diff --git a/core/modules/system/tests/src/Functional/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php b/core/modules/system/tests/src/Functional/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php index 8634b16..b4888db 100644 --- a/core/modules/system/tests/src/Functional/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php +++ b/core/modules/system/tests/src/Functional/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\system\Functional\Entity\Update; +use Drupal\Core\Entity\ContentEntityType; use Drupal\FunctionalTests\Update\UpdatePathTestBase; use Drupal\views\Entity\View; @@ -79,4 +80,156 @@ public function testSystemUpdate8400() { } } + /** + * Tests the addition of required revision metadata keys. + * + * This test ensures that already cached entity instances will only return the + * required revision metadata keys they have been cached with and only new + * instances will return all the new required revision metadata keys. + */ + public function testAddingRequiredRevisionMetadataKeys() { + // Ensure that cached entity types without required revision metadata keys + // will not return any of the newly added required revision metadata keys. + // Contains no revision metadata keys and the property holding the required + // metadata keys is empty, the entity type id is "entity_test_mul_revlog". + $cached_with_no_metadata_keys = 'Tzo4MjoiRHJ1cGFsXFRlc3RzXHN5c3RlbVxGdW5jdGlvbmFsXEVudGl0eVxVcGRhdGVcVGVzdFJldmlzaW9uTWV0YWRhdGFCY0xheWVyRW50aXR5VHlwZSI6Mzk6e3M6MjU6IgAqAHJldmlzaW9uX21ldGFkYXRhX2tleXMiO2E6MDp7fXM6MzE6IgAqAHJlcXVpcmVkUmV2aXNpb25NZXRhZGF0YUtleXMiO2E6MDp7fXM6MTU6IgAqAHN0YXRpY19jYWNoZSI7YjoxO3M6MTU6IgAqAHJlbmRlcl9jYWNoZSI7YjoxO3M6MTk6IgAqAHBlcnNpc3RlbnRfY2FjaGUiO2I6MTtzOjE0OiIAKgBlbnRpdHlfa2V5cyI7YTo1OntzOjg6InJldmlzaW9uIjtzOjA6IiI7czo2OiJidW5kbGUiO3M6MDoiIjtzOjg6Imxhbmdjb2RlIjtzOjA6IiI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoyOToicmV2aXNpb25fdHJhbnNsYXRpb25fYWZmZWN0ZWQiO3M6Mjk6InJldmlzaW9uX3RyYW5zbGF0aW9uX2FmZmVjdGVkIjt9czo1OiIAKgBpZCI7czoyMjoiZW50aXR5X3Rlc3RfbXVsX3JldmxvZyI7czoxNjoiACoAb3JpZ2luYWxDbGFzcyI7TjtzOjExOiIAKgBoYW5kbGVycyI7YTozOntzOjY6ImFjY2VzcyI7czo0NToiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eUFjY2Vzc0NvbnRyb2xIYW5kbGVyIjtzOjc6InN0b3JhZ2UiO3M6NDY6IkRydXBhbFxDb3JlXEVudGl0eVxTcWxcU3FsQ29udGVudEVudGl0eVN0b3JhZ2UiO3M6MTI6InZpZXdfYnVpbGRlciI7czozNjoiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eVZpZXdCdWlsZGVyIjt9czoxOToiACoAYWRtaW5fcGVybWlzc2lvbiI7TjtzOjI1OiIAKgBwZXJtaXNzaW9uX2dyYW51bGFyaXR5IjtzOjExOiJlbnRpdHlfdHlwZSI7czo4OiIAKgBsaW5rcyI7YTowOnt9czoxNzoiACoAbGFiZWxfY2FsbGJhY2siO047czoyMToiACoAYnVuZGxlX2VudGl0eV90eXBlIjtOO3M6MTI6IgAqAGJ1bmRsZV9vZiI7TjtzOjE1OiIAKgBidW5kbGVfbGFiZWwiO047czoxMzoiACoAYmFzZV90YWJsZSI7TjtzOjIyOiIAKgByZXZpc2lvbl9kYXRhX3RhYmxlIjtOO3M6MTc6IgAqAHJldmlzaW9uX3RhYmxlIjtOO3M6MTM6IgAqAGRhdGFfdGFibGUiO047czoxNToiACoAdHJhbnNsYXRhYmxlIjtiOjA7czoxOToiACoAc2hvd19yZXZpc2lvbl91aSI7YjowO3M6ODoiACoAbGFiZWwiO3M6MDoiIjtzOjE5OiIAKgBsYWJlbF9jb2xsZWN0aW9uIjtzOjA6IiI7czoxNzoiACoAbGFiZWxfc2luZ3VsYXIiO3M6MDoiIjtzOjE1OiIAKgBsYWJlbF9wbHVyYWwiO3M6MDoiIjtzOjE0OiIAKgBsYWJlbF9jb3VudCI7YTowOnt9czoxNToiACoAdXJpX2NhbGxiYWNrIjtOO3M6ODoiACoAZ3JvdXAiO047czoxNDoiACoAZ3JvdXBfbGFiZWwiO047czoyMjoiACoAZmllbGRfdWlfYmFzZV9yb3V0ZSI7TjtzOjI2OiIAKgBjb21tb25fcmVmZXJlbmNlX3RhcmdldCI7YjowO3M6MjI6IgAqAGxpc3RfY2FjaGVfY29udGV4dHMiO2E6MDp7fXM6MTg6IgAqAGxpc3RfY2FjaGVfdGFncyI7YToxOntpOjA7czo5OiJ0ZXN0X2xpc3QiO31zOjE0OiIAKgBjb25zdHJhaW50cyI7YTowOnt9czoxMzoiACoAYWRkaXRpb25hbCI7YTowOnt9czo4OiIAKgBjbGFzcyI7TjtzOjExOiIAKgBwcm92aWRlciI7TjtzOjIwOiIAKgBzdHJpbmdUcmFuc2xhdGlvbiI7Tjt9'; + /** @var \Drupal\Tests\system\Functional\Entity\Update\TestRevisionMetadataBcLayerEntityType $entity_type */ + $entity_type = unserialize(base64_decode($cached_with_no_metadata_keys)); + $required_revision_metadata_keys_no_bc = []; + $this->assertEquals($required_revision_metadata_keys_no_bc, $entity_type->getRevisionMetadataKeys(FALSE)); + $required_revision_metadata_keys_with_bc = $required_revision_metadata_keys_no_bc + [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message', + ]; + $this->assertEquals($required_revision_metadata_keys_with_bc, $entity_type->getRevisionMetadataKeys(TRUE)); + + // Ensure that cached entity types with only one required revision metadata + // key will return only that one after a second required revision metadata + // key has been added. + // Contains one revision metadata key - revision_default which is also + // contained in the property holding the required revision metadata keys, + // the entity type id is "entity_test_mul_revlog". + $cached_with_metadata_key_revision_default = 'Tzo4MjoiRHJ1cGFsXFRlc3RzXHN5c3RlbVxGdW5jdGlvbmFsXEVudGl0eVxVcGRhdGVcVGVzdFJldmlzaW9uTWV0YWRhdGFCY0xheWVyRW50aXR5VHlwZSI6Mzk6e3M6MjU6IgAqAHJldmlzaW9uX21ldGFkYXRhX2tleXMiO2E6MTp7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7fXM6MzE6IgAqAHJlcXVpcmVkUmV2aXNpb25NZXRhZGF0YUtleXMiO2E6MTp7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7czoxNjoicmV2aXNpb25fZGVmYXVsdCI7fXM6MTU6IgAqAHN0YXRpY19jYWNoZSI7YjoxO3M6MTU6IgAqAHJlbmRlcl9jYWNoZSI7YjoxO3M6MTk6IgAqAHBlcnNpc3RlbnRfY2FjaGUiO2I6MTtzOjE0OiIAKgBlbnRpdHlfa2V5cyI7YTo1OntzOjg6InJldmlzaW9uIjtzOjA6IiI7czo2OiJidW5kbGUiO3M6MDoiIjtzOjg6Imxhbmdjb2RlIjtzOjA6IiI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoxNjoiZGVmYXVsdF9sYW5nY29kZSI7czoyOToicmV2aXNpb25fdHJhbnNsYXRpb25fYWZmZWN0ZWQiO3M6Mjk6InJldmlzaW9uX3RyYW5zbGF0aW9uX2FmZmVjdGVkIjt9czo1OiIAKgBpZCI7czoyMjoiZW50aXR5X3Rlc3RfbXVsX3JldmxvZyI7czoxNjoiACoAb3JpZ2luYWxDbGFzcyI7TjtzOjExOiIAKgBoYW5kbGVycyI7YTozOntzOjY6ImFjY2VzcyI7czo0NToiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eUFjY2Vzc0NvbnRyb2xIYW5kbGVyIjtzOjc6InN0b3JhZ2UiO3M6NDY6IkRydXBhbFxDb3JlXEVudGl0eVxTcWxcU3FsQ29udGVudEVudGl0eVN0b3JhZ2UiO3M6MTI6InZpZXdfYnVpbGRlciI7czozNjoiRHJ1cGFsXENvcmVcRW50aXR5XEVudGl0eVZpZXdCdWlsZGVyIjt9czoxOToiACoAYWRtaW5fcGVybWlzc2lvbiI7TjtzOjI1OiIAKgBwZXJtaXNzaW9uX2dyYW51bGFyaXR5IjtzOjExOiJlbnRpdHlfdHlwZSI7czo4OiIAKgBsaW5rcyI7YTowOnt9czoxNzoiACoAbGFiZWxfY2FsbGJhY2siO047czoyMToiACoAYnVuZGxlX2VudGl0eV90eXBlIjtOO3M6MTI6IgAqAGJ1bmRsZV9vZiI7TjtzOjE1OiIAKgBidW5kbGVfbGFiZWwiO047czoxMzoiACoAYmFzZV90YWJsZSI7TjtzOjIyOiIAKgByZXZpc2lvbl9kYXRhX3RhYmxlIjtOO3M6MTc6IgAqAHJldmlzaW9uX3RhYmxlIjtOO3M6MTM6IgAqAGRhdGFfdGFibGUiO047czoxNToiACoAdHJhbnNsYXRhYmxlIjtiOjA7czoxOToiACoAc2hvd19yZXZpc2lvbl91aSI7YjowO3M6ODoiACoAbGFiZWwiO3M6MDoiIjtzOjE5OiIAKgBsYWJlbF9jb2xsZWN0aW9uIjtzOjA6IiI7czoxNzoiACoAbGFiZWxfc2luZ3VsYXIiO3M6MDoiIjtzOjE1OiIAKgBsYWJlbF9wbHVyYWwiO3M6MDoiIjtzOjE0OiIAKgBsYWJlbF9jb3VudCI7YTowOnt9czoxNToiACoAdXJpX2NhbGxiYWNrIjtOO3M6ODoiACoAZ3JvdXAiO047czoxNDoiACoAZ3JvdXBfbGFiZWwiO047czoyMjoiACoAZmllbGRfdWlfYmFzZV9yb3V0ZSI7TjtzOjI2OiIAKgBjb21tb25fcmVmZXJlbmNlX3RhcmdldCI7YjowO3M6MjI6IgAqAGxpc3RfY2FjaGVfY29udGV4dHMiO2E6MDp7fXM6MTg6IgAqAGxpc3RfY2FjaGVfdGFncyI7YToxOntpOjA7czo5OiJ0ZXN0X2xpc3QiO31zOjE0OiIAKgBjb25zdHJhaW50cyI7YTowOnt9czoxMzoiACoAYWRkaXRpb25hbCI7YTowOnt9czo4OiIAKgBjbGFzcyI7TjtzOjExOiIAKgBwcm92aWRlciI7TjtzOjIwOiIAKgBzdHJpbmdUcmFuc2xhdGlvbiI7Tjt9'; + $entity_type = unserialize(base64_decode($cached_with_metadata_key_revision_default)); + $required_revision_metadata_keys_no_bc = [ + 'revision_default' => 'revision_default', + ]; + $this->assertEquals($required_revision_metadata_keys_no_bc, $entity_type->getRevisionMetadataKeys(FALSE)); + $required_revision_metadata_keys_with_bc = $required_revision_metadata_keys_no_bc + [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message', + ]; + $this->assertEquals($required_revision_metadata_keys_with_bc, $entity_type->getRevisionMetadataKeys(TRUE)); + + // Ensure that newly instantiated entity types will return the two required + // revision metadata keys. + $entity_type = new TestRevisionMetadataBcLayerEntityType(['id' => 'test']); + $required_revision_metadata_keys = [ + 'revision_default' => 'revision_default', + 'second_required_key' => 'second_required_key', + ]; + $this->assertEquals($required_revision_metadata_keys, $entity_type->getRevisionMetadataKeys(FALSE)); + + // Load an entity type from the cache with no revision metadata keys in the + // annotation. + $entity_last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); + $entity_type = $entity_last_installed_schema_repository->getLastInstalledDefinition('entity_test_mul_revlog'); + $revision_metadata_keys = []; + $this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(FALSE)); + $revision_metadata_keys = [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message' + ]; + $this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE)); + + // Load an entity type without using the cache with no revision metadata + // keys in the annotation. + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_type_manager->useCaches(FALSE); + $entity_type = $entity_type_manager->getDefinition('entity_test_mul_revlog'); + $revision_metadata_keys = [ + 'revision_default' => 'revision_default', + ]; + $this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(FALSE)); + $revision_metadata_keys = [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message', + 'revision_default' => 'revision_default', + ]; + $this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE)); + + // Ensure that the BC layer will not be triggered if one of the required + // revision metadata keys is defined in the annotation. + $definition = [ + 'id' => 'entity_test_mul_revlog', + 'revision_metadata_keys' => [ + 'revision_default' => 'revision_default' + ], + ]; + $entity_type = new ContentEntityType($definition); + $revision_metadata_keys = [ + 'revision_default' => 'revision_default', + ]; + $this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE)); + + // Ensure that the BC layer will be triggered if no revision metadata keys + // have been defined in the annotation. + $definition = [ + 'id' => 'entity_test_mul_revlog', + ]; + $entity_type = new ContentEntityType($definition); + $revision_metadata_keys = [ + 'revision_default' => 'revision_default', + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message', + ]; + $this->assertEquals($revision_metadata_keys, $entity_type->getRevisionMetadataKeys(TRUE)); + } + + /** + * Tests that the revision metadata key BC layer was updated correctly. + */ + public function testSystemUpdate8501() { + $this->runUpdates(); + + /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $definition_update_manager */ + $definition_update_manager = $this->container->get('entity.definition_update_manager'); + foreach (['block_content', 'node'] as $entity_type_id) { + $installed_entity_type = $definition_update_manager->getEntityType($entity_type_id); + $revision_metadata_keys = $installed_entity_type->get('revision_metadata_keys'); + $this->assertTrue(isset($revision_metadata_keys['revision_default'])); + $required_revision_metadata_keys = $installed_entity_type->get('requiredRevisionMetadataKeys'); + $this->assertTrue(isset($required_revision_metadata_keys['revision_default'])); + } + } + +} + +/** + * Test entity type class for adding new required revision metadata keys. + */ +class TestRevisionMetadataBcLayerEntityType extends ContentEntityType { + + /** + * {@inheritdoc} + */ + public function __construct($definition) { + // Only new instances should provide the required revision metadata keys. + // The cached instances should return only what already has been stored + // under the property $revision_metadata_keys. The BC layer in + // ::getRevisionMetadataKeys() has to detect if the revision metadata keys + // have been provided by the entity type annotation, therefore we add keys + // to the property $requiredRevisionMetadataKeys only if those keys aren't + // set in the entity type annotation. + if (!isset($definition['revision_metadata_keys']['second_required_key'])) { + $this->requiredRevisionMetadataKeys['second_required_key'] = 'second_required_key'; + } + parent::__construct($definition); + } + } diff --git a/core/modules/system/tests/src/Kernel/System/SystemGetInfoTest.php b/core/modules/system/tests/src/Kernel/System/SystemGetInfoTest.php new file mode 100644 index 0000000..cd42fa4 --- /dev/null +++ b/core/modules/system/tests/src/Kernel/System/SystemGetInfoTest.php @@ -0,0 +1,44 @@ +assertSame('System', $system_module_info['name']); + $this->assertSame(['system' => $system_module_info], system_get_info('module')); + + // The User module is not installed so system_get_info() should return + // an empty array. + $this->assertSame([], system_get_info('module', 'user')); + + // Install the User module and check system_get_info() returns the correct + // information. + $this->container->get('module_installer')->install(['user']); + $user_module_info = system_get_info('module', 'user'); + $this->assertSame('User', $user_module_info['name']); + $this->assertSame(['system' => $system_module_info, 'user' => $user_module_info], system_get_info('module')); + + // Test theme info. There are no themes installed yet. + $this->assertSame([], system_get_info('theme', 'stable')); + $this->assertSame([], system_get_info('theme')); + $this->container->get('theme_installer')->install(['stable']); + $stable_theme_info = system_get_info('theme', 'stable'); + $this->assertSame('Stable', $stable_theme_info['name']); + $this->assertSame(['stable' => $stable_theme_info], system_get_info('theme')); + } + +} diff --git a/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php b/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php index 1cbe8b4..a86ce6b 100644 --- a/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php +++ b/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php @@ -65,23 +65,23 @@ public function testTaxonomyUpdateParents() { $term = Term::load(3); $this->assertCount(1, $term->parent); // Target ID is returned as string. - $this->assertSame((int) $term->get('parent')[0]->target_id, 0); + $this->assertSame(0, (int) $term->get('parent')[0]->target_id); // Test if the view has been converted to use the {taxonomy_term__parent} // table instead of the {taxonomy_term_hierarchy} table. $view = $this->config("views.view.test_taxonomy_parent"); $relationship_base_path = 'display.default.display_options.relationships.parent'; - $this->assertSame($view->get("$relationship_base_path.table"), 'taxonomy_term__parent'); - $this->assertSame($view->get("$relationship_base_path.field"), 'parent_target_id'); + $this->assertSame('taxonomy_term__parent', $view->get("$relationship_base_path.table")); + $this->assertSame('parent_target_id', $view->get("$relationship_base_path.field")); $filters_base_path_1 = 'display.default.display_options.filters.parent'; - $this->assertSame($view->get("$filters_base_path_1.table"), 'taxonomy_term__parent'); - $this->assertSame($view->get("$filters_base_path_1.field"), 'parent_target_id'); + $this->assertSame('taxonomy_term__parent', $view->get("$filters_base_path_1.table")); + $this->assertSame('parent_target_id', $view->get("$filters_base_path_1.field")); $filters_base_path_2 = 'display.default.display_options.filters.parent'; - $this->assertSame($view->get("$filters_base_path_2.table"), 'taxonomy_term__parent'); - $this->assertSame($view->get("$filters_base_path_2.field"), 'parent_target_id'); + $this->assertSame('taxonomy_term__parent', $view->get("$filters_base_path_2.table")); + $this->assertSame('parent_target_id', $view->get("$filters_base_path_2.field")); // The {taxonomy_term_hierarchy} table has been removed. $this->assertFalse($this->db->schema()->tableExists('taxonomy_term_hierarchy')); diff --git a/core/modules/text/src/Plugin/Field/FieldWidget/TextareaWithSummaryWidget.php b/core/modules/text/src/Plugin/Field/FieldWidget/TextareaWithSummaryWidget.php index f41e5b1..8f37305 100644 --- a/core/modules/text/src/Plugin/Field/FieldWidget/TextareaWithSummaryWidget.php +++ b/core/modules/text/src/Plugin/Field/FieldWidget/TextareaWithSummaryWidget.php @@ -87,15 +87,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen */ public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) { $element = parent::errorElement($element, $violation, $form, $form_state); - if ($element === FALSE) { - return FALSE; - } - elseif (isset($violation->arrayPropertyPath[0])) { - return $element[$violation->arrayPropertyPath[0]]; - } - else { - return $element; - } + return ($element === FALSE) ? FALSE : $element[$violation->arrayPropertyPath[0]]; } } diff --git a/core/modules/toolbar/css/toolbar.menu.css b/core/modules/toolbar/css/toolbar.menu.css index b4e8b2e..1687ad3 100644 --- a/core/modules/toolbar/css/toolbar.menu.css +++ b/core/modules/toolbar/css/toolbar.menu.css @@ -61,38 +61,38 @@ */ .toolbar .level-2 > ul { background-color: #fafafa; - border-bottom-color: #cccccc; + border-bottom-color: #ccc; border-top-color: #e5e5e5; } .toolbar .level-3 > ul { background-color: #f5f5f5; border-bottom-color: #c5c5c5; - border-top-color: #dddddd; + border-top-color: #ddd; } .toolbar .level-4 > ul { - background-color: #eeeeee; - border-bottom-color: #bbbbbb; + background-color: #eee; + border-bottom-color: #bbb; border-top-color: #d5d5d5; } .toolbar .level-5 > ul { background-color: #e5e5e5; border-bottom-color: #b5b5b5; - border-top-color: #cccccc; + border-top-color: #ccc; } .toolbar .level-6 > ul { - background-color: #eeeeee; - border-bottom-color: #aaaaaa; + background-color: #eee; + border-bottom-color: #aaa; border-top-color: #c5c5c5; } .toolbar .level-7 > ul { background-color: #fafafa; border-bottom-color: #b5b5b5; - border-top-color: #cccccc; + border-top-color: #ccc; } .toolbar .level-8 > ul { - background-color: #dddddd; - border-bottom-color: #cccccc; - border-top-color: #dddddd; + background-color: #ddd; + border-bottom-color: #ccc; + border-top-color: #ddd; } /** diff --git a/core/modules/toolbar/css/toolbar.module.css b/core/modules/toolbar/css/toolbar.module.css index d7bfc90..9c03d53 100644 --- a/core/modules/toolbar/css/toolbar.module.css +++ b/core/modules/toolbar/css/toolbar.module.css @@ -247,7 +247,6 @@ body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { } [dir="rtl"] body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-left: auto; - margin-left: auto; margin-right: 240px; margin-right: 15rem; } diff --git a/core/modules/toolbar/css/toolbar.theme.css b/core/modules/toolbar/css/toolbar.theme.css index a003678..f119a0e 100644 --- a/core/modules/toolbar/css/toolbar.theme.css +++ b/core/modules/toolbar/css/toolbar.theme.css @@ -31,13 +31,13 @@ .toolbar .toolbar-bar { background-color: #0f0f0f; box-shadow: -1px 0 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */ - color: #dddddd; + color: #ddd; } [dir="rtl"] .toolbar .toolbar-bar { box-shadow: 1px 0 3px 1px rgba(0, 0, 0, 0.3333); } .toolbar .toolbar-bar .toolbar-item { - color: #ffffff; + color: #fff; } .toolbar .toolbar-bar .toolbar-tab > .toolbar-item { font-weight: bold; @@ -56,7 +56,7 @@ * Toolbar tray. */ .toolbar .toolbar-tray { - background-color: #ffffff; + background-color: #fff; } .toolbar-horizontal .toolbar-tray > .toolbar-lining { padding-right: 5em; /* LTR */ @@ -67,16 +67,16 @@ } .toolbar .toolbar-tray-vertical { background-color: #f5f5f5; - border-right: 1px solid #aaaaaa; /* LTR */ + border-right: 1px solid #aaa; /* LTR */ box-shadow: -1px 0 5px 2px rgba(0, 0, 0, 0.3333); /* LTR */ } [dir="rtl"] .toolbar .toolbar-tray-vertical { - border-left: 1px solid #aaaaaa; + border-left: 1px solid #aaa; border-right: 0 none; box-shadow: 1px 0 5px 2px rgba(0, 0, 0, 0.3333); } .toolbar-horizontal .toolbar-tray { - border-bottom: 1px solid #aaaaaa; + border-bottom: 1px solid #aaa; box-shadow: -2px 1px 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */ } [dir="rtl"] .toolbar-horizontal .toolbar-tray { @@ -99,33 +99,33 @@ text-decoration: underline; } .toolbar .toolbar-menu { - background-color: #ffffff; + background-color: #fff; } .toolbar-horizontal .toolbar-tray .menu-item + .menu-item { - border-left: 1px solid #dddddd; /* LTR */ + border-left: 1px solid #ddd; /* LTR */ } [dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item + .menu-item { border-left: 0 none; - border-right: 1px solid #dddddd; + border-right: 1px solid #ddd; } .toolbar-horizontal .toolbar-tray .menu-item:last-child { - border-right: 1px solid #dddddd; /* LTR */ + border-right: 1px solid #ddd; /* LTR */ } [dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item:last-child { - border-left: 1px solid #dddddd; + border-left: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item + .menu-item { - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item:last-child { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item .menu-item { border: 0 none; } .toolbar .toolbar-tray-vertical .toolbar-menu ul ul { - border-bottom: 1px solid #dddddd; - border-top: 1px solid #dddddd; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item:last-child > ul { border-bottom: 0; diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 54b53e8..761a4c4 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -274,8 +274,7 @@ function toolbar_prerender_toolbar_administration_tray(array $element) { } /** - * Renders the toolbar's Add content tray - * in the same way as the Administration tray + * Renders the toolbar's Add tray in the same way as the Administration tray. */ function toolbar_prerender_toolbar_add_tray(array $element) { $menu_tree = \Drupal::service('toolbar.menu_tree'); diff --git a/core/modules/user/css/user.admin.css b/core/modules/user/css/user.admin.css index 10358c2..21ed153 100644 --- a/core/modules/user/css/user.admin.css +++ b/core/modules/user/css/user.admin.css @@ -18,5 +18,5 @@ /* Account settings */ .user-admin-settings .details-description { font-size: 0.85em; - padding-bottom: .5em; + padding-bottom: 0.5em; } diff --git a/core/modules/user/src/ContextProvider/CurrentUserContext.php b/core/modules/user/src/ContextProvider/CurrentUserContext.php index 73be2ca..aacc5f7 100644 --- a/core/modules/user/src/ContextProvider/CurrentUserContext.php +++ b/core/modules/user/src/ContextProvider/CurrentUserContext.php @@ -50,9 +50,11 @@ public function __construct(AccountInterface $account, EntityManagerInterface $e public function getRuntimeContexts(array $unqualified_context_ids) { $current_user = $this->userStorage->load($this->account->id()); - // @todo Do not validate protected fields to avoid bug in TypedData, remove - // this in https://www.drupal.org/project/drupal/issues/2934192. - $current_user->_skipProtectedUserFieldConstraint = TRUE; + if ($current_user) { + // @todo Do not validate protected fields to avoid bug in TypedData, + // remove this in https://www.drupal.org/project/drupal/issues/2934192. + $current_user->_skipProtectedUserFieldConstraint = TRUE; + } $context = new Context(new ContextDefinition('entity:user', $this->t('Current user')), $current_user); $cacheability = new CacheableMetadata(); diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index c82d64c..4e1595e 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -387,7 +387,7 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { views_invalidate_cache(); $this->invalidateCaches(); - // Rebuild the router if this is a new view, or it's status changed. + // Rebuild the router if this is a new view, or its status changed. if (!isset($this->original) || ($this->status() != $this->original->status())) { \Drupal::service('router.builder')->setRebuildNeeded(); } diff --git a/core/modules/views/src/Plugin/views/ViewsPluginInterface.php b/core/modules/views/src/Plugin/views/ViewsPluginInterface.php index 83d9c89..ddc5341 100644 --- a/core/modules/views/src/Plugin/views/ViewsPluginInterface.php +++ b/core/modules/views/src/Plugin/views/ViewsPluginInterface.php @@ -118,7 +118,7 @@ public function getAvailableGlobalTokens($prepared = FALSE, array $types = []); /** * Flattens the structure of form elements. * - * If a form element has #flatten = TRUE, then all of it's children get moved + * If a form element has #flatten = TRUE, then all of its children get moved * to the same level as the element itself. So $form['to_be_flattened'][$key] * becomes $form[$key], and $form['to_be_flattened'] gets unset. * diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index ef23c8d..78c6a12 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -1056,7 +1056,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#type' => 'textarea', '#title' => $this->t('No results text'), '#default_value' => $this->options['empty'], - '#description' => $this->t('Provide text to display if this field contains an empty result. You may include HTML. You may enter data from this view as per the "Replacement patterns" in the "Rewrite Results" section below.'), + '#description' => $this->t('Provide text to display if this field contains an empty result. You may include HTML. You may enter data from this view as per the "Replacement patterns" in the "Rewrite Results" section above.'), '#fieldset' => 'empty_field_behavior', ]; diff --git a/core/modules/views/src/Plugin/views/row/Fields.php b/core/modules/views/src/Plugin/views/row/Fields.php index 711704e..92a1acc 100644 --- a/core/modules/views/src/Plugin/views/row/Fields.php +++ b/core/modules/views/src/Plugin/views/row/Fields.php @@ -23,7 +23,7 @@ class Fields extends RowPluginBase { /** - * Does the row plugin support to add fields to it's output. + * Does the row plugin support to add fields to its output. * * @var bool */ diff --git a/core/modules/views/src/Plugin/views/row/OpmlFields.php b/core/modules/views/src/Plugin/views/row/OpmlFields.php index aaeecbf..56dfadc 100644 --- a/core/modules/views/src/Plugin/views/row/OpmlFields.php +++ b/core/modules/views/src/Plugin/views/row/OpmlFields.php @@ -18,7 +18,7 @@ class OpmlFields extends RowPluginBase { /** - * Does the row plugin support to add fields to it's output. + * Does the row plugin support to add fields to its output. * * @var bool */ diff --git a/core/modules/views/src/Plugin/views/row/RowPluginBase.php b/core/modules/views/src/Plugin/views/row/RowPluginBase.php index 3c09eaf..3335118 100644 --- a/core/modules/views/src/Plugin/views/row/RowPluginBase.php +++ b/core/modules/views/src/Plugin/views/row/RowPluginBase.php @@ -43,7 +43,7 @@ protected $usesOptions = TRUE; /** - * Does the row plugin support to add fields to it's output. + * Does the row plugin support to add fields to its output. * * @var bool */ diff --git a/core/modules/views/src/Plugin/views/row/RssFields.php b/core/modules/views/src/Plugin/views/row/RssFields.php index 1b56aae..9a31a02 100644 --- a/core/modules/views/src/Plugin/views/row/RssFields.php +++ b/core/modules/views/src/Plugin/views/row/RssFields.php @@ -19,7 +19,7 @@ class RssFields extends RowPluginBase { /** - * Does the row plugin support to add fields to it's output. + * Does the row plugin support to add fields to its output. * * @var bool */ diff --git a/core/modules/views/src/Plugin/views/style/StylePluginBase.php b/core/modules/views/src/Plugin/views/style/StylePluginBase.php index 1d6b491..89e3dfb 100644 --- a/core/modules/views/src/Plugin/views/style/StylePluginBase.php +++ b/core/modules/views/src/Plugin/views/style/StylePluginBase.php @@ -71,7 +71,7 @@ protected $usesGrouping = TRUE; /** - * Does the style plugin for itself support to add fields to it's output. + * Does the style plugin for itself support to add fields to its output. * * This option only makes sense on style plugins without row plugins, like * for example table. diff --git a/core/modules/views/src/Plugin/views/style/Table.php b/core/modules/views/src/Plugin/views/style/Table.php index bbc9a1e..b369f82 100644 --- a/core/modules/views/src/Plugin/views/style/Table.php +++ b/core/modules/views/src/Plugin/views/style/Table.php @@ -23,7 +23,7 @@ class Table extends StylePluginBase implements CacheableDependencyInterface { /** - * Does the style plugin for itself support to add fields to it's output. + * Does the style plugin for itself support to add fields to its output. * * @var bool */ diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index 0fda8c8..16b89f3 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -331,7 +331,7 @@ class ViewExecutable { protected $request; /** - * Does this view already have loaded it's handlers. + * Does this view already have loaded its handlers. * * @todo Group with other static properties. * diff --git a/core/modules/views_ui/css/views_ui.admin.theme.css b/core/modules/views_ui/css/views_ui.admin.theme.css index e1f4d7a..ed21b89 100644 --- a/core/modules/views_ui/css/views_ui.admin.theme.css +++ b/core/modules/views_ui/css/views_ui.admin.theme.css @@ -624,10 +624,10 @@ td.group-title { border: none; } .views-ui-dialog .views-offset-top { - border-bottom: 1px solid #CCC; + border-bottom: 1px solid #ccc; } .views-ui-dialog .views-offset-bottom { - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; } .views-ui-dialog .views-override > * { margin: 0; diff --git a/core/modules/views_ui/tests/src/Functional/DisplayTest.php b/core/modules/views_ui/tests/src/Functional/DisplayTest.php index 44724b4..9b447dd 100644 --- a/core/modules/views_ui/tests/src/Functional/DisplayTest.php +++ b/core/modules/views_ui/tests/src/Functional/DisplayTest.php @@ -184,7 +184,7 @@ public function testViewStatus() { $view = $this->randomView(); $id = $view['id']; - // The view should initially have the enabled class on it's form wrapper. + // The view should initially have the enabled class on its form wrapper. $this->drupalGet('admin/structure/views/view/' . $id); $elements = $this->xpath('//div[contains(@class, :edit) and contains(@class, :status)]', [':edit' => 'views-edit-view', ':status' => 'enabled']); $this->assertTrue($elements, 'The enabled class was found on the form wrapper'); diff --git a/core/package.json b/core/package.json index b33263a..ac76b6d 100644 --- a/core/package.json +++ b/core/package.json @@ -15,23 +15,23 @@ "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0" }, "devDependencies": { - "babel-core": "6.24.1", - "babel-plugin-add-header-comment": "1.0.3", - "babel-preset-env": "1.4.0", - "chalk": "^1.1.3", - "chokidar": "1.6.1", - "cross-env": "^4.0.0", - "eslint": "3.19.0", - "eslint-config-airbnb": "14.1.0", - "eslint-plugin-import": "2.2.0", - "eslint-plugin-jsx-a11y": "4.0.0", - "eslint-plugin-react": "6.10.3", - "glob": "7.1.1", + "babel-core": "^6.26.0", + "babel-plugin-add-header-comment": "^1.0.3", + "babel-preset-env": "^1.4.0", + "chalk": "^2.3.0", + "chokidar": "^2.0.0", + "cross-env": "^5.1.3", + "eslint": "^3.19.0", + "eslint-config-airbnb": "^14.1.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^4.0.0", + "eslint-plugin-react": "^6.10.3", + "glob": "^7.1.1", "minimist": "^1.2.0", - "stylelint": "^7.10.1", - "stylelint-checkstyle-formatter": "^0.1.0", + "stylelint": "^7.13.0", + "stylelint-checkstyle-formatter": "^0.1.1", "stylelint-config-standard": "^16.0.0", - "stylelint-no-browser-hacks": "^1.0.2" + "stylelint-no-browser-hacks": "^1.1.0" }, "babel": { "presets": [ diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 963d921..2a55d68 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -30,8 +30,10 @@ + + diff --git a/core/profiles/demo_umami/config/install/block_content.type.disclaimer_block.yml b/core/profiles/demo_umami/config/install/block_content.type.disclaimer_block.yml new file mode 100644 index 0000000..d4e08a3 --- /dev/null +++ b/core/profiles/demo_umami/config/install/block_content.type.disclaimer_block.yml @@ -0,0 +1,7 @@ +langcode: en +status: true +dependencies: { } +id: disclaimer_block +label: 'Disclaimer block' +revision: 0 +description: 'A disclaimer block contains disclaimer and copyright text.' diff --git a/core/profiles/demo_umami/config/install/block_content.type.footer_promo_block.yml b/core/profiles/demo_umami/config/install/block_content.type.footer_promo_block.yml new file mode 100644 index 0000000..3c4df97 --- /dev/null +++ b/core/profiles/demo_umami/config/install/block_content.type.footer_promo_block.yml @@ -0,0 +1,7 @@ +langcode: en +status: true +dependencies: { } +id: footer_promo_block +label: 'Footer promo block' +revision: 0 +description: 'A footer promo block contains a title, promo text, and a "find out more" link.' diff --git a/core/profiles/demo_umami/config/install/core.entity_form_display.block_content.disclaimer_block.default.yml b/core/profiles/demo_umami/config/install/core.entity_form_display.block_content.disclaimer_block.default.yml new file mode 100644 index 0000000..a64f7c2 --- /dev/null +++ b/core/profiles/demo_umami/config/install/core.entity_form_display.block_content.disclaimer_block.default.yml @@ -0,0 +1,39 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.disclaimer_block + - field.field.block_content.disclaimer_block.field_copyright + - field.field.block_content.disclaimer_block.field_disclaimer + module: + - text +id: block_content.disclaimer_block.default +targetEntityType: block_content +bundle: disclaimer_block +mode: default +content: + field_copyright: + weight: 28 + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + type: text_textarea + region: content + field_disclaimer: + weight: 27 + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + type: text_textarea + region: content + info: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } +hidden: { } diff --git a/core/profiles/demo_umami/config/install/core.entity_form_display.block_content.footer_promo_block.default.yml b/core/profiles/demo_umami/config/install/core.entity_form_display.block_content.footer_promo_block.default.yml new file mode 100644 index 0000000..041af37 --- /dev/null +++ b/core/profiles/demo_umami/config/install/core.entity_form_display.block_content.footer_promo_block.default.yml @@ -0,0 +1,59 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.footer_promo_block + - field.field.block_content.footer_promo_block.field_content_link + - field.field.block_content.footer_promo_block.field_promo_image + - field.field.block_content.footer_promo_block.field_summary + - field.field.block_content.footer_promo_block.field_title + - image.style.thumbnail + module: + - image + - link +id: block_content.footer_promo_block.default +targetEntityType: block_content +bundle: footer_promo_block +mode: default +content: + field_content_link: + weight: 3 + settings: + placeholder_url: '' + placeholder_title: '' + third_party_settings: { } + type: link_default + region: content + field_promo_image: + weight: 4 + settings: + progress_indicator: throbber + preview_image_style: thumbnail + third_party_settings: { } + type: image_image + region: content + field_summary: + weight: 2 + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + type: string_textarea + region: content + field_title: + weight: 1 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + type: string_textfield + region: content + info: + type: string_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } +hidden: { } diff --git a/core/profiles/demo_umami/config/install/core.entity_view_display.block_content.disclaimer_block.default.yml b/core/profiles/demo_umami/config/install/core.entity_view_display.block_content.disclaimer_block.default.yml new file mode 100644 index 0000000..6e8808c --- /dev/null +++ b/core/profiles/demo_umami/config/install/core.entity_view_display.block_content.disclaimer_block.default.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.disclaimer_block + - field.field.block_content.disclaimer_block.field_copyright + - field.field.block_content.disclaimer_block.field_disclaimer + module: + - text +id: block_content.disclaimer_block.default +targetEntityType: block_content +bundle: disclaimer_block +mode: default +content: + field_copyright: + weight: 2 + label: hidden + settings: { } + third_party_settings: { } + type: text_default + region: content + field_disclaimer: + weight: 1 + label: hidden + settings: { } + third_party_settings: { } + type: text_default + region: content +hidden: { } diff --git a/core/profiles/demo_umami/config/install/core.entity_view_display.block_content.footer_promo_block.default.yml b/core/profiles/demo_umami/config/install/core.entity_view_display.block_content.footer_promo_block.default.yml new file mode 100644 index 0000000..d20a80d --- /dev/null +++ b/core/profiles/demo_umami/config/install/core.entity_view_display.block_content.footer_promo_block.default.yml @@ -0,0 +1,55 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.footer_promo_block + - field.field.block_content.footer_promo_block.field_content_link + - field.field.block_content.footer_promo_block.field_promo_image + - field.field.block_content.footer_promo_block.field_summary + - field.field.block_content.footer_promo_block.field_title + - image.style.medium_8_7 + module: + - image + - link +id: block_content.footer_promo_block.default +targetEntityType: block_content +bundle: footer_promo_block +mode: default +content: + field_content_link: + weight: 3 + label: hidden + settings: + trim_length: 80 + url_only: false + url_plain: false + rel: '' + target: '' + third_party_settings: { } + type: link + region: content + field_promo_image: + weight: 0 + label: hidden + settings: + image_style: medium_8_7 + image_link: '' + third_party_settings: { } + type: image + region: content + field_summary: + weight: 2 + label: hidden + settings: { } + third_party_settings: { } + type: basic_string + region: content + field_title: + weight: 1 + label: hidden + settings: + link_to_entity: false + third_party_settings: { } + type: string + region: content +hidden: { } diff --git a/core/profiles/demo_umami/config/install/field.field.block_content.disclaimer_block.field_copyright.yml b/core/profiles/demo_umami/config/install/field.field.block_content.disclaimer_block.field_copyright.yml new file mode 100644 index 0000000..964a888 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.field.block_content.disclaimer_block.field_copyright.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.disclaimer_block + - field.storage.block_content.field_copyright + module: + - text +id: block_content.disclaimer_block.field_copyright +field_name: field_copyright +entity_type: block_content +bundle: disclaimer_block +label: Copyright +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: text_long diff --git a/core/profiles/demo_umami/config/install/field.field.block_content.disclaimer_block.field_disclaimer.yml b/core/profiles/demo_umami/config/install/field.field.block_content.disclaimer_block.field_disclaimer.yml new file mode 100644 index 0000000..d2d3c91 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.field.block_content.disclaimer_block.field_disclaimer.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.disclaimer_block + - field.storage.block_content.field_disclaimer + module: + - text +id: block_content.disclaimer_block.field_disclaimer +field_name: field_disclaimer +entity_type: block_content +bundle: disclaimer_block +label: Disclaimer +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: text_long diff --git a/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_content_link.yml b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_content_link.yml new file mode 100644 index 0000000..06e5a85 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_content_link.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.footer_promo_block + - field.storage.block_content.field_content_link + module: + - link +id: block_content.footer_promo_block.field_content_link +field_name: field_content_link +entity_type: block_content +bundle: footer_promo_block +label: 'Find out more link' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + link_type: 17 + title: 2 +field_type: link diff --git a/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_promo_image.yml b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_promo_image.yml new file mode 100644 index 0000000..b1664c5 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_promo_image.yml @@ -0,0 +1,37 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.footer_promo_block + - field.storage.block_content.field_promo_image + module: + - image +id: block_content.footer_promo_block.field_promo_image +field_name: field_promo_image +entity_type: block_content +bundle: footer_promo_block +label: 'Promo image' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + file_directory: '[date:custom:Y]-[date:custom:m]' + file_extensions: 'png gif jpg jpeg' + max_filesize: '' + max_resolution: '' + min_resolution: '' + alt_field: true + alt_field_required: false + title_field: false + title_field_required: false + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null + handler: 'default:file' + handler_settings: { } +field_type: image diff --git a/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_summary.yml b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_summary.yml new file mode 100644 index 0000000..9a6f240 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_summary.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.footer_promo_block + - field.storage.block_content.field_summary +id: block_content.footer_promo_block.field_summary +field_name: field_summary +entity_type: block_content +bundle: footer_promo_block +label: 'Promo text' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: string_long diff --git a/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_title.yml b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_title.yml new file mode 100644 index 0000000..0d51c80 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.field.block_content.footer_promo_block.field_title.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - block_content.type.footer_promo_block + - field.storage.block_content.field_title +id: block_content.footer_promo_block.field_title +field_name: field_title +entity_type: block_content +bundle: footer_promo_block +label: 'Promo title' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/core/profiles/demo_umami/config/install/field.storage.block_content.field_copyright.yml b/core/profiles/demo_umami/config/install/field.storage.block_content.field_copyright.yml new file mode 100644 index 0000000..4f87da1 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.storage.block_content.field_copyright.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - text +id: block_content.field_copyright +field_name: field_copyright +entity_type: block_content +type: text_long +settings: { } +module: text +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/profiles/demo_umami/config/install/field.storage.block_content.field_disclaimer.yml b/core/profiles/demo_umami/config/install/field.storage.block_content.field_disclaimer.yml new file mode 100644 index 0000000..85202e2 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.storage.block_content.field_disclaimer.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - text +id: block_content.field_disclaimer +field_name: field_disclaimer +entity_type: block_content +type: text_long +settings: { } +module: text +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/profiles/demo_umami/config/install/field.storage.block_content.field_promo_image.yml b/core/profiles/demo_umami/config/install/field.storage.block_content.field_promo_image.yml new file mode 100644 index 0000000..4f5f344 --- /dev/null +++ b/core/profiles/demo_umami/config/install/field.storage.block_content.field_promo_image.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - file + - image +id: block_content.field_promo_image +field_name: field_promo_image +entity_type: block_content +type: image +settings: + uri_scheme: public + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null + target_type: file + display_field: false + display_default: false +module: image +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/profiles/demo_umami/config/install/image.style.medium_8_7.yml b/core/profiles/demo_umami/config/install/image.style.medium_8_7.yml new file mode 100644 index 0000000..c8af145 --- /dev/null +++ b/core/profiles/demo_umami/config/install/image.style.medium_8_7.yml @@ -0,0 +1,13 @@ +langcode: en +status: true +dependencies: { } +name: medium_8_7 +label: 'Medium 8:7 (266x236)' +effects: + a7d919ee-5a34-476b-b893-c9649a621e60: + uuid: a7d919ee-5a34-476b-b893-c9649a621e60 + id: image_scale_and_crop + weight: 1 + data: + width: 266 + height: 236 diff --git a/core/profiles/demo_umami/config/optional/block.block.umami_disclaimer.yml b/core/profiles/demo_umami/config/optional/block.block.umami_disclaimer.yml index 747a287..178024c 100644 --- a/core/profiles/demo_umami/config/optional/block.block.umami_disclaimer.yml +++ b/core/profiles/demo_umami/config/optional/block.block.umami_disclaimer.yml @@ -1,8 +1,13 @@ langcode: en status: true dependencies: + content: + - 'block_content:disclaimer_block:9b4dcd67-99f3-48d0-93c9-2c46648b29de' + enforced: + module: + - demo_umami_content module: - - demo_umami + - block_content theme: - umami id: umami_disclaimer @@ -10,16 +15,13 @@ theme: umami region: bottom weight: 10 provider: null -plugin: umami_disclaimer +plugin: 'block_content:9b4dcd67-99f3-48d0-93c9-2c46648b29de' settings: - id: umami_disclaimer + id: 'block_content:9b4dcd67-99f3-48d0-93c9-2c46648b29de' label: 'Umami disclaimer' - provider: demo_umami + provider: block_content label_display: '0' - umami_disclaimer: - value: 'Umami Magazine & Umami Publications is a fictional magazine and publisher for illustrative purposes only.' - format: basic_html - umami_copyright: - value: '© 2018 Terms & Conditions' - format: basic_html + status: true + info: '' + view_mode: full visibility: { } diff --git a/core/profiles/demo_umami/config/optional/block.block.umami_footer_promo.yml b/core/profiles/demo_umami/config/optional/block.block.umami_footer_promo.yml index a85ebdb..1e8b82b 100644 --- a/core/profiles/demo_umami/config/optional/block.block.umami_footer_promo.yml +++ b/core/profiles/demo_umami/config/optional/block.block.umami_footer_promo.yml @@ -1,8 +1,13 @@ langcode: en status: true dependencies: + content: + - 'block_content:footer_promo_block:924ab293-8f5f-45a1-9c7f-2423ae61a241' + enforced: + module: + - demo_umami_content module: - - demo_umami + - block_content theme: - umami id: umami_footer_promo @@ -10,14 +15,13 @@ theme: umami region: footer weight: -1 provider: null -plugin: umami_footer_promo +plugin: 'block_content:924ab293-8f5f-45a1-9c7f-2423ae61a241' settings: - id: umami_footer_promo - label: 'Umami Footer promo' - provider: demo_umami + id: 'block_content:924ab293-8f5f-45a1-9c7f-2423ae61a241' + label: 'Umami footer promo' + provider: block_content label_display: '0' - promo_title: 'Umami Food Magazine' - promo_text: 'Skills and know-how. Magazine exclusive articles, recipes and plenty of reasons to get your copy today.' - findmore_url: '/about-umami' - findmore_text: 'Find out more' + status: true + info: '' + view_mode: full visibility: { } diff --git a/core/profiles/demo_umami/config/schema/demo_umami.schema.yml b/core/profiles/demo_umami/config/schema/demo_umami.schema.yml deleted file mode 100644 index 7ccb017..0000000 --- a/core/profiles/demo_umami/config/schema/demo_umami.schema.yml +++ /dev/null @@ -1,27 +0,0 @@ -block.settings.umami_disclaimer: - type: block_settings - label: 'Disclaimer block' - mapping: - umami_disclaimer: - type: text_format - label: 'Disclaimer' - umami_copyright: - type: text_format - label: 'Copyright' - -block.settings.umami_footer_promo: - type: block_settings - label: 'Footer promo block' - mapping: - promo_title: - type: string - label: 'Promo title' - promo_text: - type: string - label: 'Promo text' - findmore_url: - type: string - label: 'Find more URL' - findmore_text: - type: string - label: 'Find more text' diff --git a/core/profiles/demo_umami/demo_umami.info.yml b/core/profiles/demo_umami/demo_umami.info.yml index aa9367e..8e10250 100644 --- a/core/profiles/demo_umami/demo_umami.info.yml +++ b/core/profiles/demo_umami/demo_umami.info.yml @@ -1,6 +1,6 @@ -name: Umami Demo - Experimental +name: 'Demo: Umami Food Magazine (Experimental)' type: profile -description: 'Install with the Umami food magazine demonstration website, a sample Drupal website that shows off some of the features of what is possible with Drupal "Out of the Box".
' +description: 'Install an example site that shows off some of Drupal’s capabilities.' version: VERSION core: 8.x dependencies: diff --git a/core/profiles/demo_umami/demo_umami.install b/core/profiles/demo_umami/demo_umami.install index 867067d..c5f0f6a 100644 --- a/core/profiles/demo_umami/demo_umami.install +++ b/core/profiles/demo_umami/demo_umami.install @@ -15,24 +15,14 @@ function demo_umami_requirements($phase) { $requirements = []; if ($phase == 'runtime') { - $demo_umami_installed_drupal = \Drupal::state()->get('demo_umami_drupal_version'); - + $profile = \Drupal::installProfile(); + $info = system_get_info('module', $profile); $requirements['experimental_profile_used'] = [ - 'title' => t('Experimental profile used'), - 'value' => \Drupal::VERSION, - 'description' => t('Demo Umami is an experimental profile to be used for demonstration purposes only, and should not be used for a production/live site. To start building a new site, you should re-install Drupal and choose another profile, for example "Standard".'), + 'title' => t('Experimental installation profile used'), + 'value' => $info['name'], + 'description' => t('Experimental profiles are provided for testing purposes only. Use at your own risk. To start building a new site, reinstall Drupal and choose a non-experimental profile.'), 'severity' => REQUIREMENT_WARNING, ]; - - // Check if Drupal version has changed since demo_umami was installed. - if ($demo_umami_installed_drupal != \Drupal::VERSION) { - $requirements['demo_umami_drupal_version'] = [ - 'title' => t('Demo Umami Drupal Version'), - 'value' => \Drupal::VERSION, - 'description' => t('Drupal has been updated since this demo was installed, which could cause issues with this site. It is recommended that you re-install the demo to evaluate the latest changes.'), - 'severity' => REQUIREMENT_ERROR, - ]; - } } return $requirements; } @@ -92,7 +82,4 @@ function demo_umami_install() { // in the demo_umami.info.yml file, as it requires configuration provided by // the profile (fields etc.). \Drupal::service('module_installer')->install(['demo_umami_content'], TRUE); - - // Store the version of Drupal installed. - \Drupal::state()->set('demo_umami_drupal_version', \Drupal::VERSION); } diff --git a/core/profiles/demo_umami/demo_umami.profile b/core/profiles/demo_umami/demo_umami.profile index 02a5853..cdbfe49 100644 --- a/core/profiles/demo_umami/demo_umami.profile +++ b/core/profiles/demo_umami/demo_umami.profile @@ -34,18 +34,26 @@ function demo_umami_toolbar() { // @todo: This can be removed once a generic warning for experimental profiles has been introduced. // @see https://www.drupal.org/project/drupal/issues/2934374 $items['experimental-profile-warning'] = [ - '#type' => 'toolbar_item', - 'tab' => [ + '#weight' => 999, + '#cache' => [ + 'contexts' => ['route'], + ], + ]; + + // Show warning only on administration pages. + $admin_context = \Drupal::service('router.admin_context'); + if ($admin_context->isAdminRoute()) { + $items['experimental-profile-warning']['#type'] = 'toolbar_item'; + $items['experimental-profile-warning']['tab'] = [ '#type' => 'inline_template', '#template' => 'This installation is for demonstration purposes only.', '#context' => [ - 'more_info_link' => 'https://www.drupal.org/project/drupal/issues/2829101', + 'more_info_link' => 'https://www.drupal.org/node/2941833', ], '#attached' => [ 'library' => ['demo_umami/toolbar-warning'], ], - ], - '#weight' => 999, - ]; + ]; + } return $items; } diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/.htaccess b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/.htaccess new file mode 100644 index 0000000..bdcdd2f --- /dev/null +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/.htaccess @@ -0,0 +1,11 @@ +# Deny all requests from Apache 2.4+. + + Require all denied + + +# Deny all requests from Apache 2.0-2.2. + + Deny from all + +# Turn off all options we don't need. +Options -Indexes -ExecCGI -Includes -MultiViews diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/give-it-a-go-and-grow-your-own-herbs.html b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/give-it-a-go-and-grow-your-own-herbs.html new file mode 100644 index 0000000..c2b7c45 --- /dev/null +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/give-it-a-go-and-grow-your-own-herbs.html @@ -0,0 +1,15 @@ +

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use whilst cooking. Whether you have a large allotment or a small kitchen window sill, there's always enough room for something home grown.

+

Outdoors

+

Mint

+

Mint is a great plant to grow as its hardy and can grow in almost any soil. Mint can go totally wild though, so keep it contained in a pot or it might spread and take over your whole garden or allotment.

+

Sage

+

Like mint, sage is another prolific growing plant and will take over your garden if you let it. Highly aromatic, the sage plant can be planted in a pot or flower bed in well drained soil. The best way to store the herb is to sun dry the leaves and store in a cool, dark cupboard in a sealed container.

+

Rosemary

+

Rosemary plants grow into lovely shrubs. Easily grown from cuttings, rosemary plants do not like freezing temperatures so keep pots or planted bushes near the home to shelter them from the cold. It grows well in pots as it likes dry soil, but can survive well in the ground too. If pruning rosemary to encourage it into a better shape, save the branches and hang them upside down to preserve the flavour and use in food.

+

Indoors

+

Basil

+

Perfect in sunny spot on a kitchen window sill. Basil is an annual plant, so will die off in the autumn, so it's a good idea to harvest it in the summer if you have an abundance and dry it. Picked basil stays fresh longer if it is placed in water (like fresh flowers). A great way to store basil is to make it into pesto!

+

Chives

+

A versatile herb, chives can grow well indoors. Ensure the plant is watered well, and gets plenty of light. Remember to regularly trim the chives. This prevents the flowers from developing and encourages new growth.

+

Coriander (Cilantro)

+

Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun in the middle of the day. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don't forget to water it! Cut coriander is best stored in the fridge.

diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/lets-hear-it-for-carrots.html b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/lets-hear-it-for-carrots.html new file mode 100644 index 0000000..7a1422a --- /dev/null +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/lets-hear-it-for-carrots.html @@ -0,0 +1,11 @@ +

Let's hear it for the humble carrot! This sweet and healthy ‘everyday’ veg packs it all in. Great flavour, fantastic colour, and if you're one for believing the old story, they can even help you to see better in the dark.

+

Who doesn't love cooking with this super versatile root veg? We roast them, boil them, blend them into soups and grate them into salads. The humble carrot has to be one of our favourite veg choices and it's been grown for thousands of years. But back then you were more likely to find a purple, red, yellow or white carrot and not the orange one we are all so familiar with today.

+

So what happened? When did orange become the preferred colour?

+

It was the Dutch during the 17th century who cultivated and made popular the orange variety, most likely because of its brilliant colour and higher levels of beta carotene. And it has also been suggested that they were cultivated in tribute to William of Orange, who led the struggle during the Dutch battle for independence.

+

For whatever reason, the orange variety has stuck but look out for the ‘heritage’ varieties at farmers markets and grocers, their mix of purple, yellow, orange and white are especially appealing to cook with and look absolutely great served as a side dish.

+

Nutrition

+

Carrots are rich in beta carotene which your body converts into vitamin A. It's often tricky to know whether cooking vegetables will enhance or reduce their nutritious value and unfortunately there's no simple rule. But in the case of carrots, nutrition is enhanced by consuming them cooked. In fact, it only takes 100 grams of carrots to get more than your daily value of vitamin A.

+

Get them at their best

+

Young carrots, harvested when they are small have an especially sweet flavour and they are absolutely delicious. To cook them you can skip the peeling, give them a good wash and pop them in the steamer for just a few minutes. Carrots will taste the best when they are fresh, so make sure they are firm and bright in colour when buying.

+

And that thing about carrots helping you see more in the dark?

+

Of course it's a myth. During World War II the U.K. Ministry of Food promoted carrots as a super healthy veg that would improve your ability to see during the blackouts and as an explanation for the succesfull night missions of UK fighter pilots. In reality, the only truth in the connection between carrots and improved eye sight is that vitamin A does indeed help to maintain vision.

diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/the-real-deal-for-supermarket-savvy-shopping.html b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/the-real-deal-for-supermarket-savvy-shopping.html new file mode 100644 index 0000000..0b06d17 --- /dev/null +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/the-real-deal-for-supermarket-savvy-shopping.html @@ -0,0 +1,16 @@ +

This may not surprise you - but your supermarket is a hot bed of marketing mayhem, designed to improve their profit and to encourage the consumer to spend more than they intended. The tricks that all supermarkets employ are sometimes sensible ploys that any retailer should do to improve sales - but some may be more subtle and less obvious than you might think.

+

With consumer awareness articles and documentaries frequently picking up on this topic, it's likely the case that retailers find it harder to get away with the more obvious ploys. We are becoming ever more savvy consumers and there's probably not a great deal that gets past us. But here's a few retail tricks to keep in mind when you are rushing around the weekly supermarket stock-up.

+

Lost essentials

+

The layout of your supermarket may make sense to you when you have shopped there for a while, but for newcomers, trying to find essentials, it may make very little sense at all. Some supermarkets have noted that people come to their store to buy milk, bread or eggs and that by hiding these essentials in the far reaches of the store, they encourage the newcomer to wander the aisles - picking up other items as they go.

+

Sure, this can be great for nudging the memory on essentials you might otherwise forget, but for saving the pennies it's tough to stick to grabbing only the things you came for and the supermarkets know it!

+
+Our tip: Make your shopping list before leaving the house, checking what you need and sticking to that list. You could be amazed by what you'll save over time. +
+

Nonsensical multibuys

+

Buy one, get one free; two for £2 and meal deals. They all seem like a great deal. But in some cases these are loss leaders that are positioned to encourage you to take up the deal and buy other stuff while you are there. In other cases, deals for multi-buy or discounts on specific pack sizes might seem like a bargain, until you compare the pricing like-for-like on similar brands or with pack sizes for the same brand. These deals can mean you end up paying less but is it less for something you don't really need and in some cases you can end up paying more for the item. Remember, the supermarkets know you are often in a hurry and might not have the time to take in the full picture.

+
+

Our tip: Don't be rushed, take the time to read the small print. The large print will draw you in but if you read the label small print, you should find the price per 100 grams or per litre and you'll be surprised how often the headline deals are actually more expensive than just buying a different package type or size of the product.

+
+

Understanding our shopping habits

+

The cheapest products in a supermarket are almost always positioned on the bottom of the shelving where you'll need to bend over to pick it up. You also may not be able to easily read the price ticket. Most people will shop on the middle rows because it is easier and often quicker. These are where the highest profit items are kept and they are the ones the supermarkets want you to buy.

+

The layout, the music, the colours and the product types are all decided based on principles laid down by industry experts on people - psychologists and behavioural experts who know how we think. And so the savvy shopper will certainly be able to take advantage of great deals in their weekly shop, but it takes a little time and effort just to be more aware of what we are being encouraged to reach for in the aisles.

diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/the-umami-guide-to-our-favourite-mushrooms.html b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/the-umami-guide-to-our-favourite-mushrooms.html new file mode 100644 index 0000000..acc9ba7 --- /dev/null +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/article_body/the-umami-guide-to-our-favourite-mushrooms.html @@ -0,0 +1,7 @@ +

We think mushrooms are one of the most enjoyable ingredients to cook with. There are plenty of edible varieties to try, each with their own distinctive shape, size and taste. And with curious names such as chanterelle, the gypsy, horn of plenty or hen of the woods, who wouldn't want to know more about cooking with the mighty mushroom?

+

One of the best things about mushrooms is their versatility. They can be fried, roasted, grilled, steamed or even cooked in the microwave, and they can be served as the main ingredient for a dish, or simply added as part of the mix. This makes mushrooms an ideal choice for creating absolutely delicious vegetarian dishes.

+

So let's take a look at some of our favourite types of mushroom. You might not have tried cooking with them before but don't let that put you off. With their delicious, distinctive flavours you can easily transform soups, starters, sauces and create amazing pasta or stir-fry dishes.

+

Try the lovely shiitake. Used in Asian cooking, these can be purchased dried and rehydrated for a strong, deep flavour. Or buy fresh and add to soups and stir-fries. Not only does this mushroom have an intense flavour, it looks lovely too. The deep brown and smooth shapes will provide texture to your meal. In their dried form and rehydrated, these are the perfect addition for a deep and flavourful stock for a risotto.

+

The gorgeous sunny chanterelle with its yellow flesh has a fruity flavour - but it is worth mentioning that there are many lookalikes out there and care should be taken to ensure you're eating the right ones. These look great in an omelette or an asian soup to complement the yellow tones.

+

The brown morel offers a meaty and distinctive flavour and you'll probably love how extraordinary they look in a meal. The morel is a more popular mushroom during the spring, when their availability is high.

+

For delicacy try the enoki with its tiny white heads that grow in a bunch. These can even be eaten raw in salads. Finally, you can choose the popular oyster mushroom. They are named thus because they look nothing like a mushroom and resemble the innards of an oyster and their sweet flavour is delicious.

diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv b/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv index 7b24c5f..bd64187 100644 --- a/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/articles.csv @@ -1,91 +1,5 @@ -title,body,author,slug,image,alt,tags -Give it a go and grow your own herbs,"There’s nothing like having your own supply of fresh herbs, readily available and close at hand to use whilst cooking. Whether you have a large allotment or a small kitchen window sill, there’s always enough room for something home grown. - -

Outdoors

- -

Mint

- -Mint is a great plant to grow as its hardy and can grow in almost any soil. Mint can go totally wild though, so keep it contained in a pot or it might spread and take over your whole garden or allotment. - -

Sage

- -Like mint, sage is another prolific growing plant and will take over your garden if you let it. Highly aromatic, the sage plant can be planted in a pot or flower bed in well drained soil. The best way to store the herb is to sun dry the leaves and store in a cool, dark cupboard in a sealed container. - -

Rosemary

- -Rosemary plants grow into lovely shrubs. Easily grown from cuttings, rosemary plants do not like freezing temperatures so keep pots or planted bushes near the home to shelter them from the cold. It grows well in pots as it likes dry soil, but can survive well in the ground too. If pruning rosemary to encourage it into a better shape, save the branches and hang them upside down to preserve the flavour and use in food.  - -

Indoors

- -

Basil

- -Perfect in sunny spot on a kitchen window sill. Basil is an annual plant, so will die off in the autumn, so it’s a good idea to harvest it in the summer if you have an abundance and dry it. Picked basil stays fresh longer if it is placed in water (like fresh flowers). A great way to store basil is to make it into pesto! - -

Chives

- -A versatile herb, chives can grow well indoors. Ensure the plant is watered well, and gets plenty of light. Remember to regularly trim the chives. This prevents the flowers from developing and encourages new growth. - -

Coriander (Cilantro)

- -Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun in the middle of the day. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don’t forget to water it! Cut coriander is best stored in the fridge.",Holly Foat,articles/give-it-a-go-and-grow-your-own-herbs,home-grown-herbs.jpg,"Fresh cut herbs including mint, parsley, thyme and dill","Grow your own,Seasonal,Herbs" -The real deal for supermarket savvy shopping,"This may not surprise you - but your supermarket is a hot bed of marketing mayhem, designed to improve their profit and to encourage the consumer to spend more than they intended. The tricks that all supermarkets employ are sometimes sensible ploys that any retailer should do to improve sales - but some may be more subtle and less obvious than you might think. - -With consumer awareness articles and documentaries frequently picking up on this topic, it’s likely the case that retailers find it harder to get away with the more obvious ploys. We are becoming ever more savvy consumers and there’s probably not a great deal that gets past us. But here’s a few retail tricks to keep in mind when you are rushing around the weekly supermarket stock-up. - -

Lost essentials

- -The layout of your supermarket may make sense to you when you have shopped there for a while, but for newcomers, trying to find essentials, it may make very little sense at all. Some supermarkets have noted that people come to their store to buy milk, bread or eggs and that by hiding these essentials in the far reaches of the store, they encourage the newcomer to wander the aisles - picking up other items as they go. - -Sure, this can be great for nudging the memory on essentials you might otherwise forget, but for saving the pennies it’s tough to stick to grabbing only the things you came for and the supermarkets know it! - -
-Our tip: Make your shopping list before leaving the house, checking what you need and sticking to that list. You could be amazed by what you’ll save over time. -
- -

Nonsensical multibuys

- -Buy one, get one free; two for £2 and meal deals. They all seem like a great deal. But in some cases these are loss leaders that are positioned to encourage you to take up the deal and buy other stuff while you are there. In other cases, deals for multi-buy or discounts on specific pack sizes might seem like a bargain, until you compare the pricing like-for-like on similar brands or with pack sizes for the same brand. These deals can mean you end up paying less but is it less for something you don’t really need and in some cases you can end up paying more for the item. Remember, the supermarkets know you are often in a hurry and might not have the time to take in the full picture. - -
-Our tip: Don’t be rushed, take the time to read the small print. The large print will draw you in but if you read the label small print, you should find the price per 100 grams or per litre and you’ll be surprised how often the headline deals are actually more expensive than just buying a different package type or size of the product. -
- -

Understanding our shopping habits

- -The cheapest products in a supermarket are almost always positioned on the bottom of the shelving where you’ll need to bend over to pick it up. You also may not be able to easily read the price ticket. Most people will shop on the middle rows because it is easier and often quicker. These are where the highest profit items are kept and they are the ones the supermarkets want you to buy. - -The layout, the music, the colours and the product types are all decided based on principles laid down by industry experts on people - psychologists and behavioural experts who know how we think. And so the savvy shopper will certainly be able to take advantage of great deals in their weekly shop, but it takes a little time and effort just to be more aware of what we are being encouraged to reach for in the aisles.",Megan Collins Quinlan,articles/the-real-deal-for-supermarket-savvy-shopping,supermarket-savvy-umami.jpg,Products presented on supermarket shelving.,"Supermarkets,Shopping" -The umami guide to our favourite mushrooms,"We think mushrooms are one of the most enjoyable ingredients to cook with. There are plenty of edible varieties to try, each with their own distinctive shape, size and taste. And with curious names such as chanterelle, the gypsy, horn of plenty or hen of the woods, who wouldn't want to know more about cooking with the mighty mushroom? - -One of the best things about mushrooms is their versatility. They can be fried, roasted, grilled, steamed or even cooked in the microwave, and they can be served as the main ingredient for a dish, or simply added as part of the mix. This makes mushrooms an ideal choice for creating absolutely delicious vegetarian dishes. - -So let's take a look at some of our favourite types of mushroom. You might not have tried cooking with them before but don't let that put you off. With their delicious, distinctive flavours you can easily transform soups, starters, sauces and create amazing pasta or stir-fry dishes. - -Try the lovely shiitake. Used in Asian cooking, these can be purchased dried and rehydrated for a strong, deep flavour. Or buy fresh and add to soups and stir-fries. Not only does this mushroom have an intense flavour, it looks lovely too. The deep brown and smooth shapes will provide texture to your meal. In their dried form and rehydrated, these are the perfect addition for a deep and flavourful stock for a risotto. - -The gorgeous sunny chanterelle with its yellow flesh has a fruity flavour - but it is worth mentioning that there are many lookalikes out there and care should be taken to ensure you're eating the right ones. These look great in an omelette or an asian soup to complement the yellow tones. - -The brown morel offers a meaty and distinctive flavour and you'll probably love how extraordinary they look in a meal. The morel is a more popular mushroom during the spring, when their availability is high. - -For delicacy try the enoki with its tiny white heads that grow in a bunch. These can even be eaten raw in salads. Finally, you can choose the popular oyster mushroom. They are named thus because they look nothing like a mushroom and resemble the innards of an oyster and their sweet flavour is delicious.",Umami,articles/the-umami-guide-to-our-favourite-mushrooms,mushrooms-umami.jpg,A delightful selection of mushroom varieties laid out on a simple wooden plate.,"Mushrooms,Vegetarian" -Let's hear it for carrots,"Let's hear it for the humble carrot! This sweet and healthy ‘everyday’ veg packs it all in. Great flavour, fantastic colour, and if you're one for believing the old story, they can even help you to see better in the dark. - -Who doesn't love cooking with this super versatile root veg? We roast them, boil them, blend them into soups and grate them into salads. The humble carrot has to be one of our favourite veg choices and it's been grown for thousands of years. But back then you were more likely to find a purple, red, yellow or white carrot and not the orange one we are all so familiar with today. - -

So what happened? When did orange become the preferred colour?

- -It was the Dutch during the 17th century who cultivated and made popular the orange variety, most likely because of its brilliant colour and higher levels of beta carotene. And it has also been suggested that they were cultivated in tribute to William of Orange, who led the struggle during the Dutch battle for independence. - -For whatever reason, the orange variety has stuck but look out for the ‘heritage’ varieties at farmers markets and grocers, their mix of purple, yellow, orange and white are especially appealing to cook with and look absolutely great served as a side dish. - -

Nutrition

- -Carrots are rich in beta carotene which your body converts into vitamin A. It's often tricky to know whether cooking vegetables will enhance or reduce their nutritious value and unfortunately there's no simple rule. But in the case of carrots, nutrition is enhanced by consuming them cooked. In fact, it only takes 100 grams of carrots to get more than your daily value of vitamin A. - -

Get them at their best

- -Young carrots, harvested when they are small have an especially sweet flavour and they are absolutely delicious. To cook them you can skip the peeling, give them a good wash and pop them in the steamer for just a few minutes. Carrots will taste the best when they are fresh, so make sure they are firm and bright in colour when buying. - -

And that thing about carrots helping you see more in the dark?

- -Of course it's a myth. During World War II the U.K. Ministry of Food promoted carrots as a super healthy veg that would improve your ability to see during the blackouts and as an explanation for the succesfull night missions of UK fighter pilots. In reality, the only truth in the connection between carrots and improved eye sight is that vitamin A does indeed help to maintain vision.",Umami,articles/lets-hear-it-for-carrots,heritage-carrots.jpg,"Purple, orange, yellow and white heritage carrots.","Carrots,Vegetarian,Healthy" \ No newline at end of file +title,body,author,slug,image,alt,tags +Give it a go and grow your own herbs,give-it-a-go-and-grow-your-own-herbs.html,Holly Foat,articles/give-it-a-go-and-grow-your-own-herbs,home-grown-herbs.jpg,"Fresh cut herbs including mint, parsley, thyme and dill","Grow your own,Seasonal,Herbs" +The real deal for supermarket savvy shopping,the-real-deal-for-supermarket-savvy-shopping.html,Megan Collins Quinlan,articles/the-real-deal-for-supermarket-savvy-shopping,supermarket-savvy-umami.jpg,Products presented on supermarket shelving.,"Supermarkets,Shopping" +The umami guide to our favourite mushrooms,the-umami-guide-to-our-favourite-mushrooms.html,Umami,articles/the-umami-guide-to-our-favourite-mushrooms,mushrooms-umami.jpg,A delightful selection of mushroom varieties laid out on a simple wooden plate.,"Mushrooms,Vegetarian" +Let's hear it for carrots,lets-hear-it-for-carrots.html,Umami,articles/lets-hear-it-for-carrots,heritage-carrots.jpg,"Purple, orange, yellow and white heritage carrots.","Carrots,Vegetarian,Healthy" diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/images/.htaccess b/core/profiles/demo_umami/modules/demo_umami_content/default_content/images/.htaccess new file mode 100644 index 0000000..ae4e251 --- /dev/null +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/images/.htaccess @@ -0,0 +1,12 @@ +# Deny all requests from Apache 2.4+. + + Require all denied + + +# Deny all requests from Apache 2.0-2.2. + + Deny from all + +# Turn off all options we don't need. +Options None +Options +FollowSymLinks diff --git a/core/profiles/demo_umami/modules/demo_umami_content/default_content/images/chocolate-brownie-umami.jpg b/core/profiles/demo_umami/modules/demo_umami_content/default_content/images/chocolate-brownie-umami.jpg index 94fc918..758b328 100644 --- a/core/profiles/demo_umami/modules/demo_umami_content/default_content/images/chocolate-brownie-umami.jpg +++ b/core/profiles/demo_umami/modules/demo_umami_content/default_content/images/chocolate-brownie-umami.jpg @@ -1,90 +1,122 @@ -JFIFHHtExifMM*  (12Їi%NIKON CORPORATIONNIKON D3200HHPixelmator 3.72018:01:10 13:01:52""'0 -’  -ʒ,Ғ808080K  - <2018:01:10 09:19:082018:01:10 09:19:082ASCII N<WT3'dd http://ns.adobe.com/xap/1.0/ Brownie image by Keith Jay for Umami Photoshop 3.08BIMhZ%G>20180110?091908$Brownie image by Keith Jay for Umami720180110<0919088BIM%ܧq糯Y6" +JFIFC +  +  + %!'&$!$#).;2),8,#$3F48=?BCB(1HMH@M;AB?C  ?*$*??????????????????????????????????????????????????" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr -$4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzC - - - - - - - - - C ` ?H0r߅[H#j1*T?Ѷ֑pr*DHS$\sՔɳXy8aa2ŞEe#[2GNĀdƭ:5ibqȬ-y# -jq)8RYDAT1o`J#'M&ƙQS֤.Vsӭ(J&TـN1J\=3C!CEcZ]ՠ(U1)=TLmBAIsW/R$8CA;VۯLufFrGZ_$u/4?ZB#ۚ<)|x4\3 ғ=E9CNC<)V h@3ӹCsKzh(9O:U/8'.AE>!SVHۦ8ʞ^qON;V$50sUeͭ+TGj֧t5nC(Nrx! ^Xs t%Ȣ;Z"=lVw -_+(ܭ^: VaڢLw(8RqӁW>2: B],8匑_r=i@e$hgja \*zcm (jeU95 K7)Jb"j9{Rr vh*CTr10+M`XjFOikd[RsKjGL-[K qSڡ|Zޗc=+|b(}߅/0tb۷fC{Ck,7}z>́ғÌj"s9fjcYgfl1#[XSCric֛u=eJ$K7"t+0ژm8S%?8:'L6V4{b]zaW) ( =덧n1P\M4epVgU6\a͠:T_dPl䍟SZǜ]Y=1aU 34߱`Jˑ)ӌcc9t}JMcA)$.n,ָmOSRm B1MG.Ǧ)ԷtJͮzs6884mֺck3PaN9^`#] 0zsMk|61ApNϳpxcɦy~zoن " mGQ5~Xm ַ۞EDm>2}}=k6rj3nzR:b0cskk\HI4]@b#tmn8wRFxҡhO=k}EFmy)JB8#ka={Sv&")2Fz1uңe=AoDm+sQ5 -4(ze'V[WhO֪f[ RE=eocTQM++d=$ NU=?u2lĔHT1޷ U)-&r1cUH8Y t5`ylkKn3V6<֊4B?*WLZ,tZ+Tb$U~w-$qQljrǑ̷5>dSYr8Ef'j]R*Ǡdo/=jEOLU)B{4s9t+ {_)=Ʈl?w9yG(&RECG=:UjS ~t\dg|V&gq<*,~uc]3:զ2 ڔGtN?:33RvkImcNLU$Z+ J!<§[~RF|*@ȫ {V8jU )8iy9EH"tV#XyP(9@veu`S)<}90s_!zӰ^W\CҮq4{ Q vZ޴19G'gϽ_+"CC(4@p8as%9kBtc(:ڡ0sӥj 5Nؠ2'Jg֙FT`l.f}4ƀ1Z'ޢ`6ޝkAQxSӜ$ɨ3zT VǭB֩=d'E08tf4X*w>NLrj!2D >49'5ǿJJTxA?J֓U&q+DO6-Vb9߭i-6H=sRN3LhU}IUvQZ$K@2IUۭh ڨ^3\Lʅr j#X08U:~UDI#ڢ`=i~ZĹQTG)6UX\8>T[dqA^0 6l8*1 rzfs:ҏ1Ok18M&xa6qR >R8}hm]oտSKڗ2C_`VҘ).:9O=utS{-#S[3,NMu+1ǵH47 NCۭ7LWj4FVmjā&z~Ink`~? -OFy43rp@*F'ʻVmvdgpMPһџͤ6x#Y 5]lpCϤVDJ Wda]g W}-ҵTc(MWel]k٪ͦ.ڕJtȩUsޠku>8r 8|Ŭ<ӸߑG)Iӂ{o4zA<:GO94DH:p ;㏭Z-Nד4OjsC>њqpw>QnN }횃=p{zTpըd8^U<\ԨFj w% -8c*a3S(2EQR ~N$gV7)Êb$#;vP08=jez%H<h " $q)8SqcHy硩\ǭ7bAjU L9pbenՕQs\j}gsL2zBB)nsqL'sPydR?,Ĺc5"=i<^i|ņlPj2G(pJaawפޕ)YI<Ӱ܉i#Wa8ҢcZv0a>R3QqN¸>j,3Ì -vi'Ja$ Ι\8i5c4n$gryo^ 09i9wR8:sH}T)#j"OΧ0ɜeBjD.yP1⬘%=m!)y"6Ь';OB:uVi`t5Td֡vt$ft R)0ANSbOT,z/]+t6' ?.*bTyXJIUV==k\ۏª:gӧUK^n@_Ҙ|2WOG'9m)ȯto p2SBuF~xDtV`t)S?'`@t6m'_A* %4xWJtU.h- -a[igg'ړ#?j qtM&:o"L~UJd)<'Z)$OZA#S'U}QHzWЃl?˜as#v<쯡oڬ'/hÙ>/OqVp} - ªⓨÜxq=WJ|*? -WK0UdžR Z)<,:S >Aҗ;;:ajOF ϵ}P3*699WCv>’ҭIΑxlc=ѯg sP7Ͼ)S{m;J'6 qU$S
diff --git a/core/profiles/demo_umami/themes/umami/templates/components/footer/block--umami-footer-promo.html.twig b/core/profiles/demo_umami/themes/umami/templates/components/footer/block--umami-footer-promo.html.twig deleted file mode 100644 index 884298f..0000000 --- a/core/profiles/demo_umami/themes/umami/templates/components/footer/block--umami-footer-promo.html.twig +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "block.html.twig" %} -{# -/** - * @file - * Theme override for Umami footer Promo block. - */ -#} -{% set attributes = attributes.addClass('footer-promo') %} diff --git a/core/profiles/demo_umami/themes/umami/templates/components/search/block--search-form-block.html.twig b/core/profiles/demo_umami/themes/umami/templates/components/search/block--search-form-block.html.twig index 391b464..01599d5 100644 --- a/core/profiles/demo_umami/themes/umami/templates/components/search/block--search-form-block.html.twig +++ b/core/profiles/demo_umami/themes/umami/templates/components/search/block--search-form-block.html.twig @@ -36,7 +36,8 @@ diff --git a/core/profiles/demo_umami/themes/umami/templates/layout/page.html.twig b/core/profiles/demo_umami/themes/umami/templates/layout/page.html.twig index 220efec..0cce8e4 100644 --- a/core/profiles/demo_umami/themes/umami/templates/layout/page.html.twig +++ b/core/profiles/demo_umami/themes/umami/templates/layout/page.html.twig @@ -58,7 +58,7 @@ - {% if page.tabs|render|striptags|trim is not empty %} + {% if page.tabs %}
{{ page.tabs }} diff --git a/core/profiles/demo_umami/themes/umami/umami.info.yml b/core/profiles/demo_umami/themes/umami/umami.info.yml index 2fe2d0f..cd324a8 100644 --- a/core/profiles/demo_umami/themes/umami/umami.info.yml +++ b/core/profiles/demo_umami/themes/umami/umami.info.yml @@ -1,7 +1,7 @@ name: Umami type: theme base theme: classy -description: 'The theme used for the out of the box initiative.' +description: 'The theme used for the Umami food magazine demonstration site.' version: VERSION core: 8.x libraries: diff --git a/core/profiles/demo_umami/themes/umami/umami.libraries.yml b/core/profiles/demo_umami/themes/umami/umami.libraries.yml index c4379fb..b0afce0 100644 --- a/core/profiles/demo_umami/themes/umami/umami.libraries.yml +++ b/core/profiles/demo_umami/themes/umami/umami.libraries.yml @@ -5,6 +5,7 @@ global: css/base.css: {} css/components/blocks/banner/banner.css: {} css/components/blocks/branding/branding.css: {} + css/components/blocks/disclaimer/disclaimer.css: {} css/components/blocks/page-title/page-title.css: {} css/components/blocks/footer-promo/footer-promo.css: {} css/components/blocks/search/search.css: {} diff --git a/core/profiles/demo_umami/themes/umami/umami.theme b/core/profiles/demo_umami/themes/umami/umami.theme index 4f744ea..d8b61ff 100644 --- a/core/profiles/demo_umami/themes/umami/umami.theme +++ b/core/profiles/demo_umami/themes/umami/umami.theme @@ -5,6 +5,7 @@ * Functions to support theming in the Umami theme. */ +use Drupal\Component\Utility\Html; use Drupal\Core\Form\FormStateInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; @@ -45,6 +46,10 @@ function umami_preprocess_field(&$variables, $hook) { */ function umami_preprocess_block(&$variables) { $variables['title_attributes']['class'][] = 'block__title'; + // Add a class indicating the custom block bundle. + if (isset($variables['elements']['content']['#block_content'])) { + $variables['attributes']['class'][] = Html::getClass('block-type-' . $variables['elements']['content']['#block_content']->bundle()); + } } /** diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml index 9082f2d..99d0f60 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml @@ -51,7 +51,10 @@ content: type: entity_reference_autocomplete_tags weight: 3 region: content - settings: { } + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' third_party_settings: { } path: type: path diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml index 5c43252..5019b65 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml @@ -55,6 +55,6 @@ content: links: weight: 100 region: content -hidden: - field_image: true - field_tags: true + settings: { } + third_party_settings: { } +hidden: { } diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 5a8ecd9..e1ef7db 100644 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Timer; use Drupal\Component\Uuid\Php; +use Drupal\Core\Composer\Composer; use Drupal\Core\Asset\AttachedAssets; use Drupal\Core\Database\Database; use Drupal\Core\StreamWrapper\PublicStream; @@ -18,10 +19,9 @@ use Drupal\simpletest\TestBase; use Drupal\simpletest\TestDiscovery; use PHPUnit\Framework\TestCase; +use PHPUnit\Runner\Version; use Symfony\Component\HttpFoundation\Request; -$autoloader = require_once __DIR__ . '/../../autoload.php'; - // Define some colors for display. // A nice calming green. const SIMPLETEST_SCRIPT_COLOR_PASS = 32; @@ -37,11 +37,6 @@ const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1; const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2; -if (!class_exists(TestCase::class)) { - echo "\nrun-tests.sh requires the PHPUnit testing framework. Please use 'composer install --dev' to ensure that it is present.\n\n"; - exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); -} - // Set defaults and get overrides. list($args, $count) = simpletest_script_parse_args(); @@ -52,14 +47,9 @@ simpletest_script_init(); -try { - $request = Request::createFromGlobals(); - $kernel = TestRunnerKernel::createFromRequest($request, $autoloader); - $kernel->prepareLegacyRequest($request); -} -catch (Exception $e) { - echo (string) $e; - exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); +if (!class_exists(TestCase::class)) { + echo "\nrun-tests.sh requires the PHPUnit testing framework. Please use 'composer install --dev' to ensure that it is present.\n\n"; + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } if ($args['execute-test']) { @@ -142,6 +132,18 @@ exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS); } +// Ensure we have the correct PHPUnit version for the version of PHP. +if (class_exists('\PHPUnit_Runner_Version')) { + $phpunit_version = \PHPUnit_Runner_Version::id(); +} +else { + $phpunit_version = Version::id(); +} +if (!Composer::upgradePHPUnitCheck($phpunit_version)) { + simpletest_script_print_error("PHPUnit testing framework version 6 or greater is required when running on PHP 7.2 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this."); + exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); +} + $test_list = simpletest_script_get_test_list(); // Try to allocate unlimited time to run the tests. @@ -463,6 +465,25 @@ function simpletest_script_init() { exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } + // Detect if we're in the top-level process using the private 'execute-test' + // argument. Determine if being run on drupal.org's testing infrastructure + // using the presence of 'drupaltestbot' in the database url. + // @todo https://www.drupal.org/project/drupalci_testbot/issues/2860941 Use + // better environment variable to detect DrupalCI. + // @todo https://www.drupal.org/project/drupal/issues/2942473 Remove when + // dropping PHPUnit 4 and PHP 5 support. + if (!$args['execute-test'] && preg_match('/drupalci/', $args['sqlite'])) { + // Update PHPUnit if needed and possible. There is a later check once the + // autoloader is in place to ensure we're on the correct version. We need to + // do this before the autoloader is in place to ensure that it is correct. + $composer = ($composer = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar`)) + ? $php . ' ' . escapeshellarg($composer) + : 'composer'; + passthru("$composer run-script drupal-phpunit-upgrade-check"); + } + + $autoloader = require_once __DIR__ . '/../../autoload.php'; + // Get URL from arguments. if (!empty($args['url'])) { $parsed_url = parse_url($args['url']); @@ -521,6 +542,17 @@ function simpletest_script_init() { } chdir(realpath(__DIR__ . '/../..')); + + // Prepare the kernel. + try { + $request = Request::createFromGlobals(); + $kernel = TestRunnerKernel::createFromRequest($request, $autoloader); + $kernel->prepareLegacyRequest($request); + } + catch (Exception $e) { + echo (string) $e; + exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION); + } } /** diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Installer/Form/SelectProfileFormTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Installer/Form/SelectProfileFormTest.php new file mode 100644 index 0000000..efad210 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Installer/Form/SelectProfileFormTest.php @@ -0,0 +1,132 @@ +setupBaseUrl(); + + $this->prepareDatabasePrefix(); + + // Install Drupal test site. + $this->prepareEnvironment(); + + // Define information about the user 1 account. + $this->rootUser = new UserSession([ + 'uid' => 1, + 'name' => 'admin', + 'mail' => 'admin@example.com', + 'pass_raw' => $this->randomMachineName(), + ]); + + // If any $settings are defined for this test, copy and prepare an actual + // settings.php, so as to resemble a regular installation. + if (!empty($this->settings)) { + // Not using File API; a potential error must trigger a PHP warning. + copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php'); + $this->writeSettings($this->settings); + } + + // Note that FunctionalTestSetupTrait::installParameters() returns form + // input values suitable for a programmed + // \Drupal::formBuilder()->submitForm(). + // @see InstallerTestBase::translatePostValues() + $this->parameters = $this->installParameters(); + + // Set up a minimal container (required by BrowserTestBase). Set cookie and + // server information so that XDebug works. + // @see install_begin_request() + $request = Request::create($GLOBALS['base_url'] . '/core/install.php', 'GET', [], $_COOKIE, [], $_SERVER); + $this->container = new ContainerBuilder(); + $request_stack = new RequestStack(); + $request_stack->push($request); + $this->container + ->set('request_stack', $request_stack); + $this->container + ->setParameter('language.default_values', Language::$defaultValues); + $this->container + ->register('language.default', 'Drupal\Core\Language\LanguageDefault') + ->addArgument('%language.default_values%'); + $this->container + ->register('string_translation', 'Drupal\Core\StringTranslation\TranslationManager') + ->addArgument(new Reference('language.default')); + $this->container + ->register('http_client', 'GuzzleHttp\Client') + ->setFactory('http_client_factory:fromOptions'); + $this->container + ->register('http_client_factory', 'Drupal\Core\Http\ClientFactory') + ->setArguments([new Reference('http_handler_stack')]); + $handler_stack = HandlerStack::create(); + $test_http_client_middleware = new TestHttpClientMiddleware(); + $handler_stack->push($test_http_client_middleware(), 'test.http_client.middleware'); + $this->container + ->set('http_handler_stack', $handler_stack); + + $this->container + ->set('app.root', DRUPAL_ROOT); + \Drupal::setContainer($this->container); + + // Setup Mink. + $this->initMink(); + } + + /** + * {@inheritdoc} + */ + protected function initMink() { + // The temporary files directory doesn't exist yet, as install_base_system() + // has not run. We need to create the template cache directory recursively. + $path = $this->tempFilesDirectory . DIRECTORY_SEPARATOR . 'browsertestbase-templatecache'; + if (!file_exists($path)) { + mkdir($path, 0777, TRUE); + } + + parent::initMink(); + } + + /** + * {@inheritdoc} + * + * BrowserTestBase::refreshVariables() tries to operate on persistent storage, + * which is only available after the installer completed. + */ + protected function refreshVariables() { + // Intentionally empty as the site is not yet installed. + } + + /** + * Tests a warning message is displayed when the Umami profile is selected. + */ + public function testUmamiProfileWarningMessage() { + $this->drupalGet($GLOBALS['base_url'] . '/core/install.php'); + $edit = [ + 'langcode' => 'en', + ]; + $this->drupalPostForm(NULL, $edit, 'Save and continue'); + $page = $this->getSession()->getPage(); + $warning_message = $page->find('css', '.description .messages--warning'); + $this->assertFalse($warning_message->isVisible()); + $page->selectFieldOption('profile', 'demo_umami'); + $this->assertTrue($warning_message->isVisible()); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php new file mode 100644 index 0000000..5f243af --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/DrupalSelenium2Driver.php @@ -0,0 +1,35 @@ +getWebDriverSession()->deleteCookie($name); + return; + } + + $cookieArray = [ + 'name' => $name, + 'value' => urlencode($value), + 'secure' => FALSE, + // Unlike \Behat\Mink\Driver\Selenium2Driver::setCookie we set a domain + // and an expire date, as otherwise cookies leak from one test site into + // another. + 'domain' => parse_url($this->getWebDriverSession()->url(), PHP_URL_HOST), + 'expires' => time() + 80000, + ]; + + $this->getWebDriverSession()->setCookie($cookieArray); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php index 10ae701..281cb78 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php @@ -172,7 +172,7 @@ public function waitOnAutocomplete() { } /** - * Test that a node, or it's specific corner, is visible in the viewport. + * Test that a node, or its specific corner, is visible in the viewport. * * Note: Always set the viewport size. This can be done with a PhantomJS * startup parameter or in your test with \Behat\Mink\Session->resizeWindow(). @@ -255,7 +255,7 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner = } /** - * Check the visibility of a node, or it's specific corner. + * Check the visibility of a node, or its specific corner. * * @param \Behat\Mink\Element\NodeElement $node * A valid node. diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php index d49d46e..55b1284 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptTestBase.php @@ -15,6 +15,9 @@ /** * {@inheritdoc} + * + * To use a webdriver based approach, please use DrupalSelenium2Driver::class. + * We will switch the default later. */ protected $minkDefaultDriverClass = PhantomJSDriver::class; @@ -22,14 +25,19 @@ * {@inheritdoc} */ protected function initMink() { - // Set up the template cache used by the PhantomJS mink driver. - $path = $this->tempFilesDirectory . DIRECTORY_SEPARATOR . 'browsertestbase-templatecache'; - $this->minkDefaultDriverArgs = [ - 'http://127.0.0.1:8510', - $path, - ]; - if (!file_exists($path)) { - mkdir($path); + if ($this->minkDefaultDriverClass === DrupalSelenium2Driver::class) { + $this->minkDefaultDriverArgs = ['chrome', NULL, 'http://localhost:4444/']; + } + elseif ($this->minkDefaultDriverClass === PhantomJSDriver::class) { + // Set up the template cache used by the PhantomJS mink driver. + $path = $this->tempFilesDirectory . DIRECTORY_SEPARATOR . 'browsertestbase-templatecache'; + $this->minkDefaultDriverArgs = [ + 'http://127.0.0.1:8510', + $path, + ]; + if (!file_exists($path)) { + mkdir($path); + } } try { @@ -67,7 +75,13 @@ protected function tearDown() { * {@inheritdoc} */ protected function getMinkDriverArgs() { - return getenv('MINK_DRIVER_ARGS_PHANTOMJS') ?: parent::getMinkDriverArgs(); + if ($this->minkDefaultDriverClass === DrupalSelenium2Driver::class) { + return getenv('MINK_DRIVER_ARGS_WEBDRIVER') ?: getenv('MINK_DRIVER_ARGS_PHANTOMJS') ?: parent::getMinkDriverArgs(); + } + elseif ($this->minkDefaultDriverClass === PhantomJSDriver::class) { + return getenv('MINK_DRIVER_ARGS_PHANTOMJS') ?: parent::getMinkDriverArgs(); + } + return parent::getMinkDriverArgs(); } /** @@ -176,4 +190,12 @@ protected function getDrupalSettings() { return $this->getSession()->evaluateScript($script) ?: []; } + /** + * {@inheritdoc} + */ + protected function getHtmlOutputHeaders() { + // The webdriver API does not support fetching headers. + return ''; + } + } diff --git a/core/tests/Drupal/FunctionalJavascriptTests/LegacyJavascriptTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/LegacyJavascriptTestBase.php index 7cef5b8..b3baf6e 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/LegacyJavascriptTestBase.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/LegacyJavascriptTestBase.php @@ -2,6 +2,8 @@ namespace Drupal\FunctionalJavascriptTests; +use Zumba\Mink\Driver\PhantomJSDriver; + /** * Runs a browser test using PhantomJS. * @@ -12,6 +14,11 @@ /** * {@inheritdoc} */ + protected $minkDefaultDriverClass = PhantomJSDriver::class; + + /** + * {@inheritdoc} + */ public function assertSession($name = NULL) { // Return a WebAssert that supports status code and header assertions. return new JSWebAssert($this->getSession($name), $this->baseUrl); diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebWithWebDriverAssertTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebWithWebDriverAssertTest.php new file mode 100644 index 0000000..d31a9a5 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Tests/JSWebWithWebDriverAssertTest.php @@ -0,0 +1,19 @@ +createContentType(['type' => 'page']); + + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('page_title_block'); + + $permissions = [ + 'access administration pages', + 'administer content translation', + 'administer content types', + 'administer languages', + 'administer url aliases', + 'create content translations', + 'create page content', + 'create url aliases', + 'edit any page content', + 'translate any entity', + ]; + // Create and log in user. + $this->webUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->webUser); + + // Enable French language. + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Enable translation for page node. + $edit = [ + 'entity_types[node]' => 1, + 'settings[node][page][translatable]' => 1, + 'settings[node][page][fields][path]' => 1, + 'settings[node][page][fields][body]' => 1, + 'settings[node][page][settings][language][language_alterable]' => 1, + ]; + $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); + + // Create a field with settings to validate. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_link', + 'entity_type' => 'node', + 'type' => 'link', + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'page', + 'settings' => [ + 'title' => DRUPAL_OPTIONAL, + 'link_type' => LinkItemInterface::LINK_GENERIC, + ], + ]); + $field->save(); + + entity_get_form_display('node', 'page', 'default') + ->setComponent('field_link', [ + 'type' => 'link_default', + ]) + ->save(); + entity_get_display('node', 'page', 'full') + ->setComponent('field_link', [ + 'type' => 'link', + ]) + ->save(); + + // Enable URL language detection and selection and set a prefix for both + // languages. + $edit = ['language_interface[enabled][language-url]' => 1]; + $this->drupalPostForm('admin/config/regional/language/detection', $edit, 'Save settings'); + $edit = ['prefix[en]' => 'en']; + $this->drupalPostForm('admin/config/regional/language/detection/url', $edit, 'Save configuration'); + + // Reset the cache after changing the negotiation settings as that changes + // how links are built. + $this->resetAll(); + + $definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page'); + $this->assertTrue($definitions['path']->isTranslatable(), 'Node path is translatable.'); + $this->assertTrue($definitions['body']->isTranslatable(), 'Node body is translatable.'); + } + + /** + * Creates content with a link field pointing to an alias of another language. + * + * @dataProvider providerLanguage + */ + public function testLinkTranslationWithAlias($source_langcode) { + $source_url_options = [ + 'language' => ConfigurableLanguage::load($source_langcode), + ]; + + // Create a target node in the source language that is the link target. + $edit = [ + 'langcode[0][value]' => $source_langcode, + 'title[0][value]' => 'Target page', + 'path[0][alias]' => '/target-page', + ]; + $this->drupalPostForm('node/add/page', $edit, t('Save'), $source_url_options); + + // Confirm that the alias works. + $assert_session = $this->assertSession(); + $assert_session->addressEquals($source_langcode . '/target-page'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('Target page'); + + // Create a second node that links to the first through the link field. + $edit = [ + 'langcode[0][value]' => $source_langcode, + 'title[0][value]' => 'Link page', + 'field_link[0][uri]' => '/target-page', + 'field_link[0][title]' => 'Target page', + 'path[0][alias]' => '/link-page', + ]; + $this->drupalPostForm('node/add/page', $edit, t('Save'), $source_url_options); + + // Make sure the link node is displayed with a working link. + $assert_session->pageTextContains('Link page'); + $this->clickLink('Target page'); + $assert_session->addressEquals($source_langcode . '/target-page'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('Target page'); + + // Clear all caches, then add a translation for the link node. + $this->resetAll(); + + $this->drupalGet('link-page', $source_url_options); + $this->clickLink('Translate'); + $this->clickLink(t('Add')); + + // Do not change the link field. + $edit = [ + 'title[0][value]' => 'Translated link page', + 'path[0][alias]' => '/translated-link-page', + ]; + $this->drupalPostForm(NULL, $edit, 'Save (this translation)'); + + $assert_session->pageTextContains('Translated link page'); + + // @todo Clicking on the link does not include the language prefix. + $this->drupalGet('target-page', $source_url_options); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('Target page'); + } + + /** + * Data provider for testFromUri(). + */ + public function providerLanguage() { + return [ + ['en'], + ['fr'], + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Routing/RouteCachingNonPathLanguageNegotiationTest.php b/core/tests/Drupal/FunctionalTests/Routing/RouteCachingNonPathLanguageNegotiationTest.php new file mode 100644 index 0000000..0728612 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Routing/RouteCachingNonPathLanguageNegotiationTest.php @@ -0,0 +1,95 @@ +adminUser = $this->drupalCreateUser(['administer blocks', 'administer languages', 'access administration pages']); + $this->drupalLogin($this->adminUser); + + // Add language. + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Enable session language detection and selection. + $edit = [ + 'language_interface[enabled][language-url]' => FALSE, + 'language_interface[enabled][language-session]' => TRUE, + ]; + $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); + + // A more common scenario is domain-based negotiation but that can not be + // tested. Session negotiation by default is not considered by the URL + // language type that is used to resolve the alias. Explicitly enable + // that to be able to test this scenario. + // @todo Improve in https://www.drupal.org/project/drupal/issues/1125428. + $this->config('language.types') + ->set('negotiation.language_url.enabled', ['language-session' => 0]) + ->save(); + + // Enable the language switching block. + $this->drupalPlaceBlock('language_block:' . LanguageInterface::TYPE_INTERFACE, [ + 'id' => 'test_language_block', + ]); + + } + + /** + * Tests aliases when the negotiated language is not in the path. + */ + public function testAliases() { + // Switch to French and try to access the now inaccessible block. + $this->drupalGet(''); + + // Create an alias for user/UID just for en, make sure that this is a 404 + // on the french page exist in english, no matter which language is + // checked first. Create the alias after visiting frontpage to make sure + // there is no existing cache entry for this that affects the tests. + \Drupal::service('path.alias_storage')->save('/user/' . $this->adminUser->id(), '/user-page', 'en'); + + $this->clickLink('French'); + $this->drupalGet('user-page'); + $this->assertSession()->statusCodeEquals(404); + + // Switch to english, make sure it works now. + $this->clickLink('English'); + $this->drupalGet('user-page'); + $this->assertSession()->statusCodeEquals(200); + + // Clear cache and repeat the check, this time with english first. + $this->resetAll(); + $this->drupalGet('user-page'); + $this->assertSession()->statusCodeEquals(200); + + $this->clickLink('French'); + $this->drupalGet('user-page'); + $this->assertSession()->statusCodeEquals(404); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php index 4aca500..744f527 100644 --- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php +++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php @@ -374,7 +374,6 @@ protected function runUpdates() { // Ensure that the update hooks updated all entity schema. $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates(); - $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.'); if ($needs_updates) { foreach (\Drupal::entityDefinitionUpdateManager() ->getChangeSummary() as $entity_type_id => $summary) { @@ -382,6 +381,9 @@ protected function runUpdates() { $this->fail($message); } } + // The above calls to `fail()` should prevent this from ever being + // called, but it is here in case something goes really wrong. + $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.'); } } } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigFileContentTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigFileContentTest.php index b449064..4f106da 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigFileContentTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigFileContentTest.php @@ -125,7 +125,7 @@ public function testReadWriteConfig() { $this->assertIdentical($config->get('null'), NULL); // Read false that had been nested in an array value. - $this->assertSame($config->get($casting_array_false_value_key), FALSE, "Nested boolean FALSE value returned FALSE."); + $this->assertSame(FALSE, $config->get($casting_array_false_value_key), "Nested boolean FALSE value returned FALSE."); // Unset a top level value. $config->clear($key); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php index 6788209..ece711b 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php @@ -2,6 +2,8 @@ namespace Drupal\KernelTests\Core\Entity; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -138,4 +140,36 @@ public function testEntityReferences() { $this->assertEqual(serialize($reference_definition2), serialize($reference_definition)); } + /** + * Tests that an entity annotation can mark the data definition as internal. + * + * @dataProvider entityDefinitionIsInternalProvider + */ + public function testEntityDefinitionIsInternal($internal, $expected) { + $entity_type_id = $this->randomMachineName(); + + $entity_type = $this->prophesize(EntityTypeInterface::class); + $entity_type->getLabel()->willReturn($this->randomString()); + $entity_type->getConstraints()->willReturn([]); + $entity_type->isInternal()->willReturn($internal); + + $entity_manager = $this->prophesize(EntityManagerInterface::class); + $entity_manager->getDefinitions()->willReturn([$entity_type_id => $entity_type->reveal()]); + $this->container->set('entity.manager', $entity_manager->reveal()); + + $entity_data_definition = EntityDataDefinition::create($entity_type_id); + $this->assertSame($expected, $entity_data_definition->isInternal()); + } + + /** + * Provides test cases for testEntityDefinitionIsInternal. + */ + public function entityDefinitionIsInternalProvider() { + return [ + 'internal' => [TRUE, TRUE], + 'external' => [FALSE, FALSE], + 'undefined' => [NULL, FALSE], + ]; + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/FieldWidgetConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Entity/FieldWidgetConstraintValidatorTest.php index 041b062..21eb4d9 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/FieldWidgetConstraintValidatorTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/FieldWidgetConstraintValidatorTest.php @@ -15,7 +15,7 @@ */ class FieldWidgetConstraintValidatorTest extends KernelTestBase { - public static $modules = ['entity_test', 'field', 'user', 'system']; + public static $modules = ['entity_test', 'field', 'field_test', 'user', 'system']; /** * {@inheritdoc} @@ -54,7 +54,8 @@ public function testValidation() { $display->validateFormValues($entity, $form, $form_state); $errors = $form_state->getErrors(); - $this->assertEqual($errors['name'], 'Widget constraint has failed.', 'Constraint violation is generated correctly'); + $this->assertEqual($errors['name'], 'Widget constraint has failed.', 'Constraint violation at the field items list level is generated correctly'); + $this->assertEqual($errors['test_field'], 'Widget constraint has failed.', 'Constraint violation at the field items list level is generated correctly for an advanced widget'); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Path/PathValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Path/PathValidatorTest.php index 1ecd524..6e8042c 100644 --- a/core/tests/Drupal/KernelTests/Core/Path/PathValidatorTest.php +++ b/core/tests/Drupal/KernelTests/Core/Path/PathValidatorTest.php @@ -63,7 +63,7 @@ public function testGetUrlIfValidWithoutAccessCheck() { $url = $pathValidator->getUrlIfValidWithoutAccessCheck($entity->toUrl()->toString(TRUE)->getGeneratedUrl()); $this->assertEquals($method, $requestContext->getMethod()); $this->assertInstanceOf(Url::class, $url); - $this->assertSame($url->getRouteParameters(), ['entity_test' => $entity->id()]); + $this->assertSame(['entity_test' => $entity->id()], $url->getRouteParameters()); } } diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php index 674214b..c11b23d 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php @@ -18,6 +18,7 @@ use Drupal\Core\Routing\RouteProvider; use Drupal\Core\State\State; use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\Core\Routing\RoutingFixtures; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -36,7 +37,7 @@ class RouteProviderTest extends KernelTestBase { /** * Modules to enable. */ - public static $modules = ['url_alter_test', 'system']; + public static $modules = ['url_alter_test', 'system', 'language']; /** * A collection of shared fixture data for tests. @@ -544,7 +545,8 @@ public function testOutlinePathNoMatch() { */ public function testRouteCaching() { $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + $language_manager = \Drupal::languageManager(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes', $language_manager); $this->fixtures->createTables($connection); @@ -558,7 +560,7 @@ public function testRouteCaching() { $request = Request::create($path, 'GET'); $provider->getRouteCollectionForRequest($request); - $cache = $this->cache->get('route:/path/add/one:'); + $cache = $this->cache->get('route:en:/path/add/one:'); $this->assertEqual('/path/add/one', $cache->data['path']); $this->assertEqual([], $cache->data['query']); $this->assertEqual(3, count($cache->data['routes'])); @@ -568,7 +570,7 @@ public function testRouteCaching() { $request = Request::create($path, 'GET'); $provider->getRouteCollectionForRequest($request); - $cache = $this->cache->get('route:/path/add/one:foo=bar'); + $cache = $this->cache->get('route:en:/path/add/one:foo=bar'); $this->assertEqual('/path/add/one', $cache->data['path']); $this->assertEqual(['foo' => 'bar'], $cache->data['query']); $this->assertEqual(3, count($cache->data['routes'])); @@ -578,7 +580,7 @@ public function testRouteCaching() { $request = Request::create($path, 'GET'); $provider->getRouteCollectionForRequest($request); - $cache = $this->cache->get('route:/path/1/one:'); + $cache = $this->cache->get('route:en:/path/1/one:'); $this->assertEqual('/path/1/one', $cache->data['path']); $this->assertEqual([], $cache->data['query']); $this->assertEqual(2, count($cache->data['routes'])); @@ -595,10 +597,25 @@ public function testRouteCaching() { $request = Request::create($path, 'GET'); $provider->getRouteCollectionForRequest($request); - $cache = $this->cache->get('route:/path/add-one:'); + $cache = $this->cache->get('route:en:/path/add-one:'); $this->assertEqual('/path/add/one', $cache->data['path']); $this->assertEqual([], $cache->data['query']); $this->assertEqual(3, count($cache->data['routes'])); + + // Test with a different current language by switching out the default + // language. + $swiss = ConfigurableLanguage::createFromLangcode('gsw-berne'); + $language_manager->reset(); + \Drupal::service('language.default')->set($swiss); + + $path = '/path/add-one'; + $request = Request::create($path, 'GET'); + $provider->getRouteCollectionForRequest($request); + + $cache = $this->cache->get('route:gsw-berne:/path/add-one:'); + $this->assertEquals('/path/add/one', $cache->data['path']); + $this->assertEquals([], $cache->data['query']); + $this->assertEquals(3, count($cache->data['routes'])); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php index 40d31e6..edb6693 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/StableTemplateOverrideTest.php @@ -24,7 +24,6 @@ class StableTemplateOverrideTest extends KernelTestBase { */ protected $templatesToSkip = [ 'views-form-views-form', - 'entity-moderation-form' ]; /** diff --git a/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php b/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php index 185a3fe..9f26f4a 100644 --- a/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php @@ -22,7 +22,7 @@ class UpdaterTest extends KernelTestBase { * @see https://drupal.org/node/2409515 */ public function testGetProjectTitleWithChild() { - // Get the project title from it's directory. If it can't find the title + // Get the project title from its directory. If it can't find the title // it will choose the first project title in the directory. $directory = \Drupal::root() . '/core/modules/system/tests/modules/module_handler_test_multiple'; $title = Updater::getProjectTitle($directory); diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php index f44c965..2ba85e9 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php +++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php @@ -137,6 +137,10 @@ public function testRegister() { $this->assertInstanceOf('Symfony\Component\HttpFoundation\Request', $new_request); $this->assertSame($new_request, \Drupal::request()); $this->assertSame($request, $new_request); + + // Ensure getting the router.route_provider does not trigger a deprecation + // message that errors. + $this->container->get('router.route_provider'); } /** diff --git a/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageReadOnlyTest.php b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageReadOnlyTest.php index 46ef590..991006b 100644 --- a/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageReadOnlyTest.php +++ b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageReadOnlyTest.php @@ -63,14 +63,14 @@ public function testReadOnly() { // Write out a PHP file and ensure it's successfully loaded. $code = "save($name, $code); - $this->assertSame($success, TRUE); + $this->assertSame(TRUE, $success); $php_read = new FileReadOnlyStorage($this->readonlyStorage); $php_read->load($name); $this->assertTrue($GLOBALS[$random]); // If the file was successfully loaded, it must also exist, but ensure the // exists() method returns that correctly. - $this->assertSame($php_read->exists($name), TRUE); + $this->assertSame(TRUE, $php_read->exists($name)); // Saving and deleting should always fail. $this->assertFalse($php_read->save($name, $code)); $this->assertFalse($php_read->delete($name)); diff --git a/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php b/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php index e787510..38fcc60 100644 --- a/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php +++ b/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php @@ -89,8 +89,8 @@ public function testSecurity() { // minimal permissions. fileperms() can return high bits unrelated to // permissions, so mask with 0777. $this->assertTrue(file_exists($expected_filename)); - $this->assertSame(fileperms($expected_filename) & 0777, 0444); - $this->assertSame(fileperms($expected_directory) & 0777, 0777); + $this->assertSame(0444, fileperms($expected_filename) & 0777); + $this->assertSame(0777, fileperms($expected_directory) & 0777); // Ensure the root directory for the bin has a .htaccess file denying web // access. @@ -121,9 +121,9 @@ public function testSecurity() { chmod($expected_filename, 0400); chmod($expected_directory, 0100); $this->assertSame(file_get_contents($expected_filename), $untrusted_code); - $this->assertSame($php->exists($name), $this->expected[$i]); - $this->assertSame($php->load($name), $this->expected[$i]); - $this->assertSame($GLOBALS['hacked'], $this->expected[$i]); + $this->assertSame($this->expected[$i], $php->exists($name)); + $this->assertSame($this->expected[$i], $php->load($name)); + $this->assertSame($this->expected[$i], $GLOBALS['hacked']); } unset($GLOBALS['hacked']); } diff --git a/core/tests/Drupal/Tests/Component/Serialization/JsonTest.php b/core/tests/Drupal/Tests/Component/Serialization/JsonTest.php index a3a32ad..d12ec61 100644 --- a/core/tests/Drupal/Tests/Component/Serialization/JsonTest.php +++ b/core/tests/Drupal/Tests/Component/Serialization/JsonTest.php @@ -58,7 +58,7 @@ protected function setUp() { */ public function testEncodingAscii() { // Verify there aren't character encoding problems with the source string. - $this->assertSame(strlen($this->string), 127, 'A string with the full ASCII table has the correct length.'); + $this->assertSame(127, strlen($this->string), 'A string with the full ASCII table has the correct length.'); foreach ($this->htmlUnsafe as $char) { $this->assertTrue(strpos($this->string, $char) > 0, sprintf('A string with the full ASCII table includes %s.', $char)); } diff --git a/core/tests/Drupal/Tests/Component/Transliteration/PhpTransliterationTest.php b/core/tests/Drupal/Tests/Component/Transliteration/PhpTransliterationTest.php index 2800c51..dc71bd4 100644 --- a/core/tests/Drupal/Tests/Component/Transliteration/PhpTransliterationTest.php +++ b/core/tests/Drupal/Tests/Component/Transliteration/PhpTransliterationTest.php @@ -182,7 +182,7 @@ public function testSafeInclude() { ]); $transliteration = new PhpTransliteration(vfsStream::url('transliteration/dir')); $transliterated = $transliteration->transliterate(chr(0xC2) . chr(0x82), '../index'); - $this->assertSame($transliterated, 'safe'); + $this->assertSame('safe', $transliterated); } } diff --git a/core/tests/Drupal/Tests/Component/Utility/ColorTest.php b/core/tests/Drupal/Tests/Component/Utility/ColorTest.php index aea1779..10664ff 100644 --- a/core/tests/Drupal/Tests/Component/Utility/ColorTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/ColorTest.php @@ -123,4 +123,42 @@ public function providerTestRbgToHex() { return $tests; } + /** + * Data provider for testNormalizeHexLength(). + * + * @see testNormalizeHexLength() + * + * @return array + * An array of arrays containing: + * - The hex color value. + * - The 6 character length hex color value. + */ + public function providerTestNormalizeHexLength() { + $data = [ + ['#000', '#000000'], + ['#FFF', '#FFFFFF'], + ['#abc', '#aabbcc'], + ['cba', '#ccbbaa'], + ['#000000', '#000000'], + ['ffffff', '#ffffff'], + ['#010203', '#010203'], + ]; + + return $data; + } + + /** + * Tests Color::normalizeHexLength(). + * + * @param string $value + * The input hex color value. + * @param string $expected + * The expected normalized hex color value. + * + * @dataProvider providerTestNormalizeHexLength + */ + public function testNormalizeHexLength($value, $expected) { + $this->assertSame($expected, Color::normalizeHexLength($value)); + } + } diff --git a/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php b/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php index ba1757f..960d181 100644 --- a/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php @@ -376,7 +376,7 @@ public function testTruncate($text, $max_length, $expected, $wordsafe = FALSE, $ * - (optional) Boolean for the $add_ellipsis flag. Defaults to FALSE. */ public function providerTruncate() { - return [ + $tests = [ ['frànçAIS is über-åwesome', 24, 'frànçAIS is über-åwesome'], ['frànçAIS is über-åwesome', 23, 'frànçAIS is über-åwesom'], ['frànçAIS is über-åwesome', 17, 'frànçAIS is über-'], @@ -422,6 +422,24 @@ public function providerTruncate() { ['Help! Help! Help!', 3, 'He…', TRUE, TRUE], ['Help! Help! Help!', 2, 'H…', TRUE, TRUE], ]; + + // Test truncate on text with multiple lines. + $multi_line = <<root . '/composer.lock'; + if (!file_exists($lockfile)) { + $this->markTestSkipped('/composer.lock is not available.'); + } + + $lock = json_decode(file_get_contents($lockfile), TRUE); + + // Check the PHP version for each installed non-development package. The + // testing infrastructure uses the uses the development packages, and may + // update them for particular environment configurations. In particular, + // PHP 7.2+ require an updated version of phpunit, which is incompatible + // with Drupal's minimum PHP requirement. + foreach ($lock['packages'] as $package) { + if (isset($package['require']['php'])) { + $this->assertTrue(Semver::satisfies(static::MIN_PHP_VERSION, $package['require']['php']), $package['name'] . ' has a PHP dependency requirement of "' . $package['require']['php'] . '"'); + } + } + } + // @codingStandardsIgnoreStart /** * The following method is copied from \Composer\Package\Locker. diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php index 880aaec..cdc1c64 100644 --- a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php +++ b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php @@ -52,9 +52,9 @@ public function testCommands() { // Ensure that the added commands are in the right order. $commands =& $this->ajaxResponse->getCommands(); - $this->assertSame($commands[1], ['command' => 'one']); - $this->assertSame($commands[2], ['command' => 'two']); - $this->assertSame($commands[0], ['command' => 'three']); + $this->assertSame(['command' => 'one'], $commands[1]); + $this->assertSame(['command' => 'two'], $commands[2]); + $this->assertSame(['command' => 'three'], $commands[0]); // Remove one and change one element from commands and ensure the reference // worked as expected. @@ -62,9 +62,9 @@ public function testCommands() { $commands[0]['class'] = 'test-class'; $commands = $this->ajaxResponse->getCommands(); - $this->assertSame($commands[1], ['command' => 'one']); + $this->assertSame(['command' => 'one'], $commands[1]); $this->assertFalse(isset($commands[2])); - $this->assertSame($commands[0], ['command' => 'three', 'class' => 'test-class']); + $this->assertSame(['command' => 'three', 'class' => 'test-class'], $commands[0]); } /** diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php index b5e6bc8..badb981 100644 --- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionGrouperUnitTest.php @@ -104,52 +104,52 @@ public function testGrouper() { $groups = $this->grouper->group($css_assets); - $this->assertSame(count($groups), 5, "5 groups created."); + $this->assertSame(5, count($groups), "5 groups created."); // Check group 1. $group = $groups[0]; - $this->assertSame($group['group'], -100); - $this->assertSame($group['type'], 'file'); - $this->assertSame($group['media'], 'all'); - $this->assertSame($group['preprocess'], TRUE); - $this->assertSame(count($group['items']), 3); + $this->assertSame(-100, $group['group']); + $this->assertSame('file', $group['type']); + $this->assertSame('all', $group['media']); + $this->assertSame(TRUE, $group['preprocess']); + $this->assertSame(3, count($group['items'])); $this->assertContains($css_assets['system.base.css'], $group['items']); $this->assertContains($css_assets['js.module.css'], $group['items']); // Check group 2. $group = $groups[1]; - $this->assertSame($group['group'], 0); - $this->assertSame($group['type'], 'file'); - $this->assertSame($group['media'], 'all'); - $this->assertSame($group['preprocess'], TRUE); - $this->assertSame(count($group['items']), 1); + $this->assertSame(0, $group['group']); + $this->assertSame('file', $group['type']); + $this->assertSame('all', $group['media']); + $this->assertSame(TRUE, $group['preprocess']); + $this->assertSame(1, count($group['items'])); $this->assertContains($css_assets['field.css'], $group['items']); // Check group 3. $group = $groups[2]; - $this->assertSame($group['group'], 0); - $this->assertSame($group['type'], 'external'); - $this->assertSame($group['media'], 'all'); - $this->assertSame($group['preprocess'], TRUE); - $this->assertSame(count($group['items']), 1); + $this->assertSame(0, $group['group']); + $this->assertSame('external', $group['type']); + $this->assertSame('all', $group['media']); + $this->assertSame(TRUE, $group['preprocess']); + $this->assertSame(1, count($group['items'])); $this->assertContains($css_assets['external.css'], $group['items']); // Check group 4. $group = $groups[3]; - $this->assertSame($group['group'], 100); - $this->assertSame($group['type'], 'file'); - $this->assertSame($group['media'], 'all'); - $this->assertSame($group['preprocess'], TRUE); - $this->assertSame(count($group['items']), 1); + $this->assertSame(100, $group['group']); + $this->assertSame('file', $group['type']); + $this->assertSame('all', $group['media']); + $this->assertSame(TRUE, $group['preprocess']); + $this->assertSame(1, count($group['items'])); $this->assertContains($css_assets['elements.css'], $group['items']); // Check group 5. $group = $groups[4]; - $this->assertSame($group['group'], 100); - $this->assertSame($group['type'], 'file'); - $this->assertSame($group['media'], 'print'); - $this->assertSame($group['preprocess'], TRUE); - $this->assertSame(count($group['items']), 1); + $this->assertSame(100, $group['group']); + $this->assertSame('file', $group['type']); + $this->assertSame('print', $group['media']); + $this->assertSame(TRUE, $group['preprocess']); + $this->assertSame(1, count($group['items'])); $this->assertContains($css_assets['print.css'], $group['items']); } diff --git a/core/tests/Drupal/Tests/Core/Common/AttributesTest.php b/core/tests/Drupal/Tests/Core/Common/AttributesTest.php index d06c678..00a5eb2 100644 --- a/core/tests/Drupal/Tests/Core/Common/AttributesTest.php +++ b/core/tests/Drupal/Tests/Core/Common/AttributesTest.php @@ -78,8 +78,8 @@ public function testAttributeValueBaseCopy() { $attributes['selected'] = $original_attributes['checked']; $attributes['id'] = $original_attributes['id']; $attributes = new Attribute($attributes); - $this->assertSame((string) $original_attributes, ' checked class="who is on" id="first"', 'Original boolean value used with original name.'); - $this->assertSame((string) $attributes, ' selected id="first"', 'Original boolean value used with new name.'); + $this->assertSame(' checked class="who is on" id="first"', (string) $original_attributes, 'Original boolean value used with original name.'); + $this->assertSame(' selected id="first"', (string) $attributes, 'Original boolean value used with new name.'); } } diff --git a/core/tests/Drupal/Tests/Core/Common/DiffArrayTest.php b/core/tests/Drupal/Tests/Core/Common/DiffArrayTest.php index 2493ab5..407fa5b 100644 --- a/core/tests/Drupal/Tests/Core/Common/DiffArrayTest.php +++ b/core/tests/Drupal/Tests/Core/Common/DiffArrayTest.php @@ -66,7 +66,7 @@ public function testDiffAssocRecursive() { 'new' => 'new', ]; - $this->assertSame(DiffArray::diffAssocRecursive($this->array1, $this->array2), $expected); + $this->assertSame($expected, DiffArray::diffAssocRecursive($this->array1, $this->array2)); } } diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/StackedKernelPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/StackedKernelPassTest.php index 9890ff9..017dedb 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/StackedKernelPassTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/StackedKernelPassTest.php @@ -51,12 +51,12 @@ public function testProcessWithStackedKernel() { $stacked_kernel_args = $this->containerBuilder->getDefinition('http_kernel')->getArguments(); // Check the stacked kernel args. - $this->assertSame((string) $stacked_kernel_args[0], 'http_kernel.one'); + $this->assertSame('http_kernel.one', (string) $stacked_kernel_args[0]); $this->assertCount(4, $stacked_kernel_args[1]); - $this->assertSame((string) $stacked_kernel_args[1][0], 'http_kernel.one'); - $this->assertSame((string) $stacked_kernel_args[1][1], 'http_kernel.two'); - $this->assertSame((string) $stacked_kernel_args[1][2], 'http_kernel.three'); - $this->assertSame((string) $stacked_kernel_args[1][3], 'http_kernel.basic'); + $this->assertSame('http_kernel.one', (string) $stacked_kernel_args[1][0]); + $this->assertSame('http_kernel.two', (string) $stacked_kernel_args[1][1]); + $this->assertSame('http_kernel.three', (string) $stacked_kernel_args[1][2]); + $this->assertSame('http_kernel.basic', (string) $stacked_kernel_args[1][3]); // Check the modified definitions. $definition = $this->containerBuilder->getDefinition('http_kernel.one'); diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php index 08465d6..e8de5ec 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/ContainerBuilderTest.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Tests\UnitTestCase; use Drupal\Tests\Core\DependencyInjection\Fixture\BarClass; +use Symfony\Component\DependencyInjection\Definition; /** * @coversDefaultClass \Drupal\Core\DependencyInjection\ContainerBuilder @@ -71,6 +72,34 @@ public function testRegister() { } /** + * @covers ::setDefinition + */ + public function testSetDefinition() { + // Test a service with defaults. + $container = new ContainerBuilder(); + $definition = new Definition(); + $service = $container->setDefinition('foo', $definition); + $this->assertTrue($service->isPublic()); + $this->assertFalse($service->isPrivate()); + + // Test a service with public set to false. + $definition = new Definition(); + $definition->setPublic(FALSE); + $service = $container->setDefinition('foo', $definition); + $this->assertFalse($service->isPublic()); + $this->assertFalse($service->isPrivate()); + + // Test a service with private set to true. Drupal does not support this. + // We only support using setPublic() to make things not available outside + // the container. + $definition = new Definition(); + $definition->setPrivate(TRUE); + $service = $container->setDefinition('foo', $definition); + $this->assertTrue($service->isPublic()); + $this->assertFalse($service->isPrivate()); + } + + /** * @covers ::setAlias */ public function testSetAlias() { @@ -89,4 +118,27 @@ public function testSerialize() { serialize($container); } + /** + * Tests constructor and resource tracking disabling. + * + * This test runs in a separate process to ensure the aliased class does not + * affect any other tests. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConstructor() { + class_alias(testInterface::class, 'Symfony\Component\Config\Resource\ResourceInterface'); + $container = new ContainerBuilder(); + $this->assertFalse($container->isTrackingResources()); + } + +} + +/** + * A test interface for testing ContainerBuilder::__construct(). + * + * @see \Drupal\Tests\Core\DependencyInjection\ContainerBuilderTest::testConstructor() + */ +interface testInterface { } diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php index fdcb9e6..cdd41e6 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php @@ -476,138 +476,6 @@ protected function setupEntityTypes() { })); } - /** - * @covers ::setLatestRevisionFlag - * - * @dataProvider setLatestRevisionFlagTestCases - */ - public function testSetLatestRevisionFlag($defaults, $parameters, $expected_parameters = FALSE) { - $route = new Route('/foo/{entity_test}', $defaults, [], [ - 'parameters' => $parameters, - ]); - $this->setupEntityTypes(); - $this->entityResolverManager->setRouteOptions($route); - // If expected parameters have not been provided, assert they are unchanged. - $this->assertEquals($expected_parameters ?: $parameters, $route->getOption('parameters')); - } - - /** - * Data provider for ::testSetLatestRevisionFlag. - */ - public function setLatestRevisionFlagTestCases() { - return [ - 'Entity parameter not on an entity form' => [ - [], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test_rev', - ], - ], - ], - 'Entity parameter on an entity form' => [ - [ - '_entity_form' => 'entity_test_rev.edit' - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - ], - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - 'load_latest_revision' => TRUE, - ], - ], - ], - 'Entity form with no operation' => [ - [ - '_entity_form' => 'entity_test_rev' - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - ], - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - 'load_latest_revision' => TRUE, - ], - ], - ], - 'Multiple entity parameters on an entity form' => [ - [ - '_entity_form' => 'entity_test_rev.edit' - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - ], - 'node' => [ - 'type' => 'entity:node', - ], - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - 'load_latest_revision' => TRUE, - ], - 'node' => [ - 'type' => 'entity:node', - ], - ], - ], - 'Overriden load_latest_revision flag does not change' => [ - [ - '_entity_form' => 'entity_test_rev.edit' - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - 'load_latest_revision' => FALSE, - ], - ], - ], - 'Non-revisionable entity type will not change' => [ - [ - '_entity_form' => 'entity_test.edit' - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - ], - ], - FALSE, - FALSE, - ], - 'Overriden load_latest_revision flag does not change with multiple parameters' => [ - [ - '_entity_form' => 'entity_test_rev.edit' - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - ], - 'node' => [ - 'type' => 'entity:node', - 'load_latest_revision' => FALSE, - ], - ], - [ - 'entity_test_rev' => [ - 'type' => 'entity:entity_test_rev', - 'load_latest_revision' => TRUE, - ], - 'node' => [ - 'type' => 'entity:node', - 'load_latest_revision' => FALSE, - ], - ], - ], - ]; - } - } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php index 5c97afa..b28a72c 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php @@ -128,6 +128,18 @@ public function providerTestGetKeys() { } /** + * Tests the isInternal() method. + */ + public function testIsInternal() { + $entity_type = $this->setUpEntityType(['internal' => TRUE]); + $this->assertTrue($entity_type->isInternal()); + $entity_type = $this->setUpEntityType(['internal' => FALSE]); + $this->assertFalse($entity_type->isInternal()); + $entity_type = $this->setUpEntityType([]); + $this->assertFalse($entity_type->isInternal()); + } + + /** * Tests the isRevisionable() method. */ public function testIsRevisionable() { diff --git a/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php index 8d0f252..8e909d2 100644 --- a/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php @@ -29,20 +29,34 @@ class EntityAdapterUnitTest extends UnitTestCase { protected $bundle; /** - * The entity used for testing. + * The content entity used for testing. * * @var \Drupal\Core\Entity\ContentEntityBase|\PHPUnit_Framework_MockObject_MockObject */ protected $entity; /** - * The entity adapter under test. + * The config entity used for testing. + * + * @var \Drupal\Core\Entity\ConfigtEntityBase|\PHPUnit_Framework_MockObject_MockObject + */ + protected $configEntity; + + /** + * The content entity adapter under test. * * @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter */ protected $entityAdapter; /** + * The config entity adapter under test. + * + * @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter + */ + protected $configEntityAdapter; + + /** * The entity type used for testing. * * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject @@ -228,6 +242,10 @@ protected function setUp() { $this->entity = $this->getMockForAbstractClass('\Drupal\Core\Entity\ContentEntityBase', [$values, $this->entityTypeId, $this->bundle]); $this->entityAdapter = EntityAdapter::createFromEntity($this->entity); + + $this->configEntity = $this->getMockForAbstractClass('\Drupal\Core\Config\Entity\ConfigEntityBase', [$values, $this->entityTypeId, $this->bundle]); + + $this->configEntityAdapter = EntityAdapter::createFromEntity($this->configEntity); } /** @@ -428,6 +446,7 @@ public function testApplyDefaultValue() { * @covers ::getIterator */ public function testGetIterator() { + // Content entity test. $iterator = $this->entityAdapter->getIterator(); $fields = iterator_to_array($iterator); $this->assertArrayHasKey('id', $fields); @@ -436,6 +455,11 @@ public function testGetIterator() { $this->entityAdapter->setValue(NULL); $this->assertEquals(new \ArrayIterator([]), $this->entityAdapter->getIterator()); + + // Config entity test. + $iterator = $this->configEntityAdapter->getIterator(); + $this->configEntityAdapter->setValue(NULL); + $this->assertEquals(new \ArrayIterator([]), $this->entityAdapter->getIterator()); } } diff --git a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php b/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php index d237ffa..a4e57ea 100644 --- a/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php +++ b/core/tests/Drupal/Tests/Core/Password/PasswordHashingTest.php @@ -96,7 +96,7 @@ public function testPasswordNeedsUpdate() { * @covers ::needsRehash */ public function testPasswordHashing() { - $this->assertSame($this->passwordHasher->getCountLog2($this->hashedPassword), PhpassHashedPassword::MIN_HASH_COUNT, 'Hashed password has the minimum number of log2 iterations.'); + $this->assertSame(PhpassHashedPassword::MIN_HASH_COUNT, $this->passwordHasher->getCountLog2($this->hashedPassword), 'Hashed password has the minimum number of log2 iterations.'); $this->assertNotEquals($this->hashedPassword, $this->md5HashedPassword, 'Password hashes not the same.'); $this->assertTrue($this->passwordHasher->check($this->password, $this->md5HashedPassword), 'Password check succeeds.'); $this->assertTrue($this->passwordHasher->check($this->password, $this->hashedPassword), 'Password check succeeds.'); @@ -119,7 +119,7 @@ public function testPasswordRehashing() { $this->assertTrue($password_hasher->needsRehash($this->hashedPassword), 'Needs a new hash after incrementing the log2 count.'); // Re-hash the password. $rehashed_password = $password_hasher->hash($this->password); - $this->assertSame($password_hasher->getCountLog2($rehashed_password), PhpassHashedPassword::MIN_HASH_COUNT + 1, 'Re-hashed password has the correct number of log2 iterations.'); + $this->assertSame(PhpassHashedPassword::MIN_HASH_COUNT + 1, $password_hasher->getCountLog2($rehashed_password), 'Re-hashed password has the correct number of log2 iterations.'); $this->assertNotEquals($rehashed_password, $this->hashedPassword, 'Password hash changed again.'); // Now the hash should be OK. diff --git a/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php index db67ad7..1fa0dda 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php @@ -47,10 +47,10 @@ protected function setUp() { * @covers ::getCategories */ public function testGetCategories() { - $this->assertSame(array_values($this->pluginManager->getCategories()), [ + $this->assertSame([ 'fruits', 'vegetables', - ]); + ], array_values($this->pluginManager->getCategories())); } /** @@ -58,7 +58,7 @@ public function testGetCategories() { */ public function testGetSortedDefinitions() { $sorted = $this->pluginManager->getSortedDefinitions(); - $this->assertSame(array_keys($sorted), ['apple', 'mango', 'cucumber']); + $this->assertSame(['apple', 'mango', 'cucumber'], array_keys($sorted)); } /** @@ -66,9 +66,9 @@ public function testGetSortedDefinitions() { */ public function testGetGroupedDefinitions() { $grouped = $this->pluginManager->getGroupedDefinitions(); - $this->assertSame(array_keys($grouped), ['fruits', 'vegetables']); - $this->assertSame(array_keys($grouped['fruits']), ['apple', 'mango']); - $this->assertSame(array_keys($grouped['vegetables']), ['cucumber']); + $this->assertSame(['fruits', 'vegetables'], array_keys($grouped)); + $this->assertSame(['apple', 'mango'], array_keys($grouped['fruits'])); + $this->assertSame(['cucumber'], array_keys($grouped['vegetables'])); } /** @@ -82,7 +82,7 @@ public function testProcessDefinitionCategory() { 'category' => 'bag', ]; $this->pluginManager->processDefinition($definition, 'some'); - $this->assertSame($definition['category'], 'bag'); + $this->assertSame('bag', $definition['category']); // No category, provider without label. $definition = [ @@ -90,7 +90,7 @@ public function testProcessDefinitionCategory() { 'provider' => 'core', ]; $this->pluginManager->processDefinition($definition, 'some'); - $this->assertSame($definition['category'], 'core'); + $this->assertSame('core', $definition['category']); // No category, provider is module with label. $definition = [ @@ -98,7 +98,7 @@ public function testProcessDefinitionCategory() { 'provider' => 'node', ]; $this->pluginManager->processDefinition($definition, 'some'); - $this->assertSame($definition['category'], 'Node'); + $this->assertSame('Node', $definition['category']); } } diff --git a/core/tests/Drupal/Tests/Core/Session/WriteSafeSessionHandlerTest.php b/core/tests/Drupal/Tests/Core/Session/WriteSafeSessionHandlerTest.php index 0ddae48..f8bba35 100644 --- a/core/tests/Drupal/Tests/Core/Session/WriteSafeSessionHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Session/WriteSafeSessionHandlerTest.php @@ -43,7 +43,7 @@ public function testConstructWriteSafeSessionHandlerDefaultArgs() { $session_id = 'some-id'; $session_data = 'serialized-session-data'; - $this->assertSame($this->sessionHandler->isSessionWritable(), TRUE); + $this->assertSame(TRUE, $this->sessionHandler->isSessionWritable()); // Writing should be enabled, return value passed to the caller by default. $this->wrappedSessionHandler->expects($this->at(0)) @@ -57,10 +57,10 @@ public function testConstructWriteSafeSessionHandlerDefaultArgs() { ->will($this->returnValue(FALSE)); $result = $this->sessionHandler->write($session_id, $session_data); - $this->assertSame($result, TRUE); + $this->assertSame(TRUE, $result); $result = $this->sessionHandler->write($session_id, $session_data); - $this->assertSame($result, FALSE); + $this->assertSame(FALSE, $result); } /** @@ -77,10 +77,10 @@ public function testConstructWriteSafeSessionHandlerDisableWriting() { // Disable writing upon construction. $this->sessionHandler = new WriteSafeSessionHandler($this->wrappedSessionHandler, FALSE); - $this->assertSame($this->sessionHandler->isSessionWritable(), FALSE); + $this->assertSame(FALSE, $this->sessionHandler->isSessionWritable()); $result = $this->sessionHandler->write($session_id, $session_data); - $this->assertSame($result, TRUE); + $this->assertSame(TRUE, $result); } /** @@ -93,22 +93,22 @@ public function testSetSessionWritable() { $session_id = 'some-id'; $session_data = 'serialized-session-data'; - $this->assertSame($this->sessionHandler->isSessionWritable(), TRUE); + $this->assertSame(TRUE, $this->sessionHandler->isSessionWritable()); // Disable writing after construction. $this->sessionHandler->setSessionWritable(FALSE); - $this->assertSame($this->sessionHandler->isSessionWritable(), FALSE); + $this->assertSame(FALSE, $this->sessionHandler->isSessionWritable()); $this->sessionHandler = new WriteSafeSessionHandler($this->wrappedSessionHandler, FALSE); - $this->assertSame($this->sessionHandler->isSessionWritable(), FALSE); + $this->assertSame(FALSE, $this->sessionHandler->isSessionWritable()); $result = $this->sessionHandler->write($session_id, $session_data); - $this->assertSame($result, TRUE); + $this->assertSame(TRUE, $result); // Enable writing again. $this->sessionHandler->setSessionWritable(TRUE); - $this->assertSame($this->sessionHandler->isSessionWritable(), TRUE); + $this->assertSame(TRUE, $this->sessionHandler->isSessionWritable()); // Writing should be enabled, return value passed to the caller by default. $this->wrappedSessionHandler->expects($this->at(0)) @@ -122,10 +122,10 @@ public function testSetSessionWritable() { ->will($this->returnValue(FALSE)); $result = $this->sessionHandler->write($session_id, $session_data); - $this->assertSame($result, TRUE); + $this->assertSame(TRUE, $result); $result = $this->sessionHandler->write($session_id, $session_data); - $this->assertSame($result, FALSE); + $this->assertSame(FALSE, $result); } /** @@ -148,13 +148,13 @@ public function testOtherMethods($method, $expected_result, $args) { call_user_func_array([$invocation, 'with'], $args); // Test with writable session. - $this->assertSame($this->sessionHandler->isSessionWritable(), TRUE); + $this->assertSame(TRUE, $this->sessionHandler->isSessionWritable()); $actual_result = call_user_func_array([$this->sessionHandler, $method], $args); $this->assertSame($expected_result, $actual_result); // Test with non-writable session. $this->sessionHandler->setSessionWritable(FALSE); - $this->assertSame($this->sessionHandler->isSessionWritable(), FALSE); + $this->assertSame(FALSE, $this->sessionHandler->isSessionWritable()); $actual_result = call_user_func_array([$this->sessionHandler, $method], $args); $this->assertSame($expected_result, $actual_result); } diff --git a/core/tests/Drupal/Tests/Core/Test/TestSuiteBaseTest.php b/core/tests/Drupal/Tests/Core/Test/TestSuiteBaseTest.php new file mode 100644 index 0000000..ede32eb --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Test/TestSuiteBaseTest.php @@ -0,0 +1,154 @@ + [ + 'modules' => [], + 'profiles' => [], + 'tests' => [ + 'Drupal' => [ + 'NotUnitTests' => [ + 'CoreNotUnitTest.php' => ' [ + 'CoreUnitTest.php' => ' [ + 'Listener.php' => ' [ + 'Listener.php' => 'getFilesystem(); + return [ + 'unit-tests' => [ + $filesystem, + 'Unit', + [ + 'Drupal\Tests\CoreUnitTest' => 'vfs://root/core/tests/Drupal/Tests/CoreUnitTest.php', + ], + ], + 'not-unit-tests' => [ + $filesystem, + 'NotUnit', + [ + 'Drupal\NotUnitTests\CoreNotUnitTest' => 'vfs://root/core/tests/Drupal/NotUnitTests/CoreNotUnitTest.php', + ], + ], + ]; + } + + /** + * Tests for special case behavior of unit test suite namespaces in core. + * + * @covers ::addTestsBySuiteNamespace + * + * @dataProvider provideCoreTests + */ + public function testAddTestsBySuiteNamespaceCore($filesystem, $suite_namespace, $expected_tests) { + // Set up the file system. + $vfs = vfsStream::setup('root'); + vfsStream::create($filesystem, $vfs); + + // Make a stub suite base to test. + $stub = new StubTestSuiteBase('test_me'); + + // Access addTestsBySuiteNamespace(). + $ref_add_tests = new \ReflectionMethod($stub, 'addTestsBySuiteNamespace'); + $ref_add_tests->setAccessible(TRUE); + + // Invoke addTestsBySuiteNamespace(). + $ref_add_tests->invokeArgs($stub, [vfsStream::url('root'), $suite_namespace]); + + // Determine if we loaded the expected test files. + $this->assertEquals($expected_tests, $stub->testFiles); + } + + /** + * Tests the assumption that local time is in 'Australia/Sydney'. + */ + public function testLocalTimeZone() { + // The 'Australia/Sydney' time zone is set in core/tests/bootstrap.php + $this->assertEquals('Australia/Sydney', date_default_timezone_get()); + } + +} + +/** + * Stub subclass of TestSuiteBase. + * + * We use this class to alter the behavior of TestSuiteBase so it can be + * testable. + */ +class StubTestSuiteBase extends TestSuiteBase { + + /** + * Test files discovered by addTestsBySuiteNamespace(). + * + * @var string[] + */ + public $testFiles = []; + + /** + * {@inheritdoc} + */ + protected function findExtensionDirectories($root) { + // We have to stub findExtensionDirectories() because we can't inject a + // vfsStream filesystem into drupal_phpunit_find_extension_directories(), + // which uses \SplFileInfo->getRealPath(). getRealPath() resolves + // stream-based paths to an empty string. See + // https://github.com/mikey179/vfsStream/wiki/Known-Issues + return []; + } + + /** + * {@inheritdoc} + */ + public function addTestFiles($filenames) { + // We stub addTestFiles() because the parent implementation can't deal with + // vfsStream-based filesystems due to an error in + // stream_resolve_include_path(). See + // https://github.com/mikey179/vfsStream/issues/5 Here we just store the + // test file being added in $this->testFiles. + $this->testFiles = array_merge($this->testFiles, $filenames); + } + +} diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index e71d7df..d96193e 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -180,144 +180,6 @@ public static function getSkippedDeprecations() { 'Drupal\node\Plugin\Action\PublishNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\PublishAction instead. See https://www.drupal.org/node/2919303.', 'Drupal\node\Plugin\Action\SaveNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\SaveAction instead. See https://www.drupal.org/node/2919303.', 'Drupal\node\Plugin\Action\UnpublishNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\Core\Action\Plugin\Action\UnpublishAction instead. See https://www.drupal.org/node/2919303.', - "The 'rest.entity.entity_test.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead.", - "The 'rest.entity.entity_test.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead.", - "The 'rest.entity.entity_test.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test.GET' route instead.", - "The 'rest.entity.action.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.action.GET' route instead.", - "The 'rest.entity.action.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.action.GET' route instead.", - "The 'rest.entity.action.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.action.GET' route instead.", - "The 'rest.entity.base_field_override.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.base_field_override.GET' route instead.", - "The 'rest.entity.base_field_override.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.base_field_override.GET' route instead.", - "The 'rest.entity.base_field_override.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.base_field_override.GET' route instead.", - "The 'rest.entity.block_content_type.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block_content_type.GET' route instead.", - "The 'rest.entity.block_content_type.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block_content_type.GET' route instead.", - "The 'rest.entity.block_content_type.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block_content_type.GET' route instead.", - "The 'rest.entity.block_content.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block_content.GET' route instead.", - "The 'rest.entity.block_content.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block_content.GET' route instead.", - "The 'rest.entity.block_content.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block_content.GET' route instead.", - "The 'rest.entity.block.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block.GET' route instead.", - "The 'rest.entity.block.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block.GET' route instead.", - "The 'rest.entity.block.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.block.GET' route instead.", - "The 'rest.entity.comment_type.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.comment_type.GET' route instead.", - "The 'rest.entity.comment_type.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.comment_type.GET' route instead.", - "The 'rest.entity.comment_type.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.comment_type.GET' route instead.", - "The 'rest.entity.comment.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.comment.GET' route instead.", - "The 'rest.entity.comment.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.comment.GET' route instead.", - "The 'rest.entity.comment.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.comment.GET' route instead.", - "The 'rest.entity.config_test.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.config_test.GET' route instead.", - "The 'rest.entity.config_test.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.config_test.GET' route instead.", - "The 'rest.entity.config_test.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.config_test.GET' route instead.", - "The 'rest.entity.configurable_language.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.configurable_language.GET' route instead.", - "The 'rest.entity.configurable_language.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.configurable_language.GET' route instead.", - "The 'rest.entity.configurable_language.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.configurable_language.GET' route instead.", - "The 'rest.entity.contact_form.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.contact_form.GET' route instead.", - "The 'rest.entity.contact_form.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.contact_form.GET' route instead.", - "The 'rest.entity.contact_form.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.contact_form.GET' route instead.", - "The 'rest.entity.language_content_settings.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.language_content_settings.GET' route instead.", - "The 'rest.entity.language_content_settings.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.language_content_settings.GET' route instead.", - "The 'rest.entity.language_content_settings.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.language_content_settings.GET' route instead.", - "The 'rest.entity.date_format.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.date_format.GET' route instead.", - "The 'rest.entity.date_format.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.date_format.GET' route instead.", - "The 'rest.entity.date_format.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.date_format.GET' route instead.", - "The 'rest.entity.editor.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.editor.GET' route instead.", - "The 'rest.entity.editor.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.editor.GET' route instead.", - "The 'rest.entity.editor.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.editor.GET' route instead.", - "The 'rest.entity.entity_form_display.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_form_display.GET' route instead.", - "The 'rest.entity.entity_form_display.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_form_display.GET' route instead.", - "The 'rest.entity.entity_form_display.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_form_display.GET' route instead.", - "The 'rest.entity.entity_form_mode.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_form_mode.GET' route instead.", - "The 'rest.entity.entity_form_mode.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_form_mode.GET' route instead.", - "The 'rest.entity.entity_form_mode.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_form_mode.GET' route instead.", - "The 'rest.entity.entity_test_bundle.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test_bundle.GET' route instead.", - "The 'rest.entity.entity_test_bundle.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test_bundle.GET' route instead.", - "The 'rest.entity.entity_test_bundle.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test_bundle.GET' route instead.", - "The 'rest.entity.entity_test_label.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test_label.GET' route instead.", - "The 'rest.entity.entity_test_label.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test_label.GET' route instead.", - "The 'rest.entity.entity_test_label.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_test_label.GET' route instead.", - "The 'rest.entity.entity_view_display.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_view_display.GET' route instead.", - "The 'rest.entity.entity_view_display.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_view_display.GET' route instead.", - "The 'rest.entity.entity_view_display.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_view_display.GET' route instead.", - "The 'rest.entity.entity_view_mode.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_view_mode.GET' route instead.", - "The 'rest.entity.entity_view_mode.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_view_mode.GET' route instead.", - "The 'rest.entity.entity_view_mode.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.entity_view_mode.GET' route instead.", - "The 'rest.entity.aggregator_feed.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.aggregator_feed.GET' route instead.", - "The 'rest.entity.aggregator_feed.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.aggregator_feed.GET' route instead.", - "The 'rest.entity.aggregator_feed.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.aggregator_feed.GET' route instead.", - "The 'rest.entity.field_config.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.field_config.GET' route instead.", - "The 'rest.entity.field_config.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.field_config.GET' route instead.", - "The 'rest.entity.field_config.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.field_config.GET' route instead.", - "The 'rest.entity.field_storage_config.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.field_storage_config.GET' route instead.", - "The 'rest.entity.field_storage_config.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.field_storage_config.GET' route instead.", - "The 'rest.entity.field_storage_config.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.field_storage_config.GET' route instead.", - "The 'rest.entity.file.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.file.GET' route instead.", - "The 'rest.entity.file.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.file.GET' route instead.", - "The 'rest.entity.file.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.file.GET' route instead.", - "The 'rest.entity.filter_format.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.filter_format.GET' route instead.", - "The 'rest.entity.filter_format.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.filter_format.GET' route instead.", - "The 'rest.entity.filter_format.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.filter_format.GET' route instead.", - "The 'rest.entity.image_style.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.image_style.GET' route instead.", - "The 'rest.entity.image_style.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.image_style.GET' route instead.", - "The 'rest.entity.image_style.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.image_style.GET' route instead.", - "The 'rest.entity.aggregator_item.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.aggregator_item.GET' route instead.", - "The 'rest.entity.aggregator_item.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.aggregator_item.GET' route instead.", - "The 'rest.entity.aggregator_item.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.aggregator_item.GET' route instead.", - "The 'rest.entity.media_type.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.media_type.GET' route instead.", - "The 'rest.entity.media_type.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.media_type.GET' route instead.", - "The 'rest.entity.media_type.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.media_type.GET' route instead.", - "The 'rest.entity.media.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.media.GET' route instead.", - "The 'rest.entity.media.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.media.GET' route instead.", - "The 'rest.entity.media.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.media.GET' route instead.", - "The 'rest.entity.menu_link_content.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.menu_link_content.GET' route instead.", - "The 'rest.entity.menu_link_content.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.menu_link_content.GET' route instead.", - "The 'rest.entity.menu_link_content.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.menu_link_content.GET' route instead.", - "The 'rest.entity.menu.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.menu.GET' route instead.", - "The 'rest.entity.menu.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.menu.GET' route instead.", - "The 'rest.entity.menu.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.menu.GET' route instead.", - "The 'rest.entity.node_type.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.node_type.GET' route instead.", - "The 'rest.entity.node_type.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.node_type.GET' route instead.", - "The 'rest.entity.node_type.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.node_type.GET' route instead.", - "The 'rest.entity.node.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.node.GET' route instead.", - "The 'rest.entity.node.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.node.GET' route instead.", - "The 'rest.entity.node.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.node.GET' route instead.", - "The 'rest.entity.rdf_mapping.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.rdf_mapping.GET' route instead.", - "The 'rest.entity.rdf_mapping.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.rdf_mapping.GET' route instead.", - "The 'rest.entity.rdf_mapping.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.rdf_mapping.GET' route instead.", - "The 'rest.entity.responsive_image_style.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.responsive_image_style.GET' route instead.", - "The 'rest.entity.responsive_image_style.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.responsive_image_style.GET' route instead.", - "The 'rest.entity.responsive_image_style.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.responsive_image_style.GET' route instead.", - "The 'rest.entity.rest_resource_config.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.rest_resource_config.GET' route instead.", - "The 'rest.entity.rest_resource_config.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.rest_resource_config.GET' route instead.", - "The 'rest.entity.rest_resource_config.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.rest_resource_config.GET' route instead.", - "The 'rest.entity.user_role.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.user_role.GET' route instead.", - "The 'rest.entity.user_role.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.user_role.GET' route instead.", - "The 'rest.entity.user_role.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.user_role.GET' route instead.", - "The 'rest.entity.search_page.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.search_page.GET' route instead.", - "The 'rest.entity.search_page.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.search_page.GET' route instead.", - "The 'rest.entity.search_page.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.search_page.GET' route instead.", - "The 'rest.entity.shortcut_set.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.shortcut_set.GET' route instead.", - "The 'rest.entity.shortcut_set.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.shortcut_set.GET' route instead.", - "The 'rest.entity.shortcut_set.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.shortcut_set.GET' route instead.", - "The 'rest.entity.shortcut.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.shortcut.GET' route instead.", - "The 'rest.entity.shortcut.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.shortcut.GET' route instead.", - "The 'rest.entity.shortcut.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.shortcut.GET' route instead.", - "The 'rest.entity.taxonomy_term.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.taxonomy_term.GET' route instead.", - "The 'rest.entity.taxonomy_term.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.taxonomy_term.GET' route instead.", - "The 'rest.entity.taxonomy_term.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.taxonomy_term.GET' route instead.", - "The 'rest.entity.tour.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.tour.GET' route instead.", - "The 'rest.entity.tour.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.tour.GET' route instead.", - "The 'rest.entity.tour.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.tour.GET' route instead.", - "The 'rest.entity.user.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.user.GET' route instead.", - "The 'rest.entity.user.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.user.GET' route instead.", - "The 'rest.entity.user.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.user.GET' route instead.", - "The 'rest.entity.view.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.view.GET' route instead.", - "The 'rest.entity.view.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.view.GET' route instead.", - "The 'rest.entity.view.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.view.GET' route instead.", - "The 'rest.entity.taxonomy_vocabulary.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.taxonomy_vocabulary.GET' route instead.", - "The 'rest.entity.taxonomy_vocabulary.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.taxonomy_vocabulary.GET' route instead.", - "The 'rest.entity.taxonomy_vocabulary.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.taxonomy_vocabulary.GET' route instead.", - "The 'rest.entity.workflow.GET.json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.workflow.GET' route instead.", - "The 'rest.entity.workflow.GET.xml' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.workflow.GET' route instead.", - "The 'rest.entity.workflow.GET.hal_json' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the 'rest.entity.workflow.GET' route instead.", 'The Symfony\Component\ClassLoader\ApcClassLoader class is deprecated since Symfony 3.3 and will be removed in 4.0. Use `composer install --apcu-autoloader` instead.', 'The Symfony\Component\ClassLoader\WinCacheClassLoader class is deprecated since Symfony 3.3 and will be removed in 4.0. Use `composer install --apcu-autoloader` instead.', 'The Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher class is deprecated since Symfony 3.3 and will be removed in 4.0. Use EventDispatcher with closure factories instead.', diff --git a/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php b/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php deleted file mode 100644 index 41a9841..0000000 --- a/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php +++ /dev/null @@ -1,153 +0,0 @@ - [ - 'modules' => [], - 'profiles' => [], - 'tests' => [ - 'Drupal' => [ - 'NotUnitTests' => [ - 'CoreNotUnitTest.php' => ' [ - 'CoreUnitTest.php' => ' [ - 'Listener.php' => ' [ - 'Listener.php' => 'getFilesystem(); - return [ - 'unit-tests' => [ - $filesystem, - 'Unit', - [ - 'Drupal\Tests\CoreUnitTest' => 'vfs://root/core/tests/Drupal/Tests/CoreUnitTest.php', - ], - ], - 'not-unit-tests' => [ - $filesystem, - 'NotUnit', - [ - 'Drupal\NotUnitTests\CoreNotUnitTest' => 'vfs://root/core/tests/Drupal/NotUnitTests/CoreNotUnitTest.php', - ], - ], - ]; - } - - /** - * Tests for special case behavior of unit test suite namespaces in core. - * - * @covers ::addTestsBySuiteNamespace - * - * @dataProvider provideCoreTests - */ - public function testAddTestsBySuiteNamespaceCore($filesystem, $suite_namespace, $expected_tests) { - // Set up the file system. - $vfs = vfsStream::setup('root'); - vfsStream::create($filesystem, $vfs); - - // Make a stub suite base to test. - $stub = new StubTestSuiteBase('test_me'); - - // Access addTestsBySuiteNamespace(). - $ref_add_tests = new \ReflectionMethod($stub, 'addTestsBySuiteNamespace'); - $ref_add_tests->setAccessible(TRUE); - - // Invoke addTestsBySuiteNamespace(). - $ref_add_tests->invokeArgs($stub, [vfsStream::url('root'), $suite_namespace]); - - // Determine if we loaded the expected test files. - $this->assertEquals($expected_tests, $stub->testFiles); - } - - /** - * Tests the assumption that local time is in 'Australia/Sydney'. - */ - public function testLocalTimeZone() { - // The 'Australia/Sydney' time zone is set in core/tests/bootstrap.php - $this->assertEquals('Australia/Sydney', date_default_timezone_get()); - } - -} - -/** - * Stub subclass of TestSuiteBase. - * - * We use this class to alter the behavior of TestSuiteBase so it can be - * testable. - */ -class StubTestSuiteBase extends TestSuiteBase { - - /** - * Test files discovered by addTestsBySuiteNamespace(). - * - * @var string[] - */ - public $testFiles = []; - - /** - * {@inheritdoc} - */ - protected function findExtensionDirectories($root) { - // We have to stub findExtensionDirectories() because we can't inject a - // vfsStream filesystem into drupal_phpunit_find_extension_directories(), - // which uses \SplFileInfo->getRealPath(). getRealPath() resolves - // stream-based paths to an empty string. See - // https://github.com/mikey179/vfsStream/wiki/Known-Issues - return []; - } - - /** - * {@inheritdoc} - */ - public function addTestFiles($filenames) { - // We stub addTestFiles() because the parent implementation can't deal with - // vfsStream-based filesystems due to an error in - // stream_resolve_include_path(). See - // https://github.com/mikey179/vfsStream/issues/5 Here we just store the - // test file being added in $this->testFiles. - $this->testFiles = array_merge($this->testFiles, $filenames); - } - -} diff --git a/core/tests/README.md b/core/tests/README.md index 5dd6cc6..fcaa5cc 100644 --- a/core/tests/README.md +++ b/core/tests/README.md @@ -2,16 +2,11 @@ ## Functional tests -* Start PhantomJS: - ``` - phantomjs --ssl-protocol=any --ignore-ssl-errors=true ./vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /dev/null & - ``` * Run the functional tests: ``` export SIMPLETEST_DB='mysql://root@localhost/dev_d8' export SIMPLETEST_BASE_URL='http://d8.dev' ./vendor/bin/phpunit -c core --testsuite functional - ./vendor/bin/phpunit -c core --testsuite functional-javascript ``` Note: functional tests have to be invoked with a user in the same group as the @@ -37,6 +32,67 @@ User Group ``` +## Functional javascript tests + +Javascript tests use the Selenium2Driver which allows you to control a +big range of browsers. By default Drupal uses chromedriver to run tests. +For help installing and starting selenium, see http://mink.behat.org/en/latest/drivers/selenium2.html + +* Make sure you have a recent version of chrome installed + +* Install selenium-server-standalone and chromedriver + +Example for Mac: + +``` +brew install selenium-server-standalone +brew install chromedriver +``` + +* Before running tests make sure that selenium-server is running +``` +selenium-server -port 4444 +``` + +* Set the correct driver args and run the tests: +``` +export MINK_DRIVER_ARGS_WEBDRIVER='["chrome", null, "http://localhost:4444/wd/hub"]' +./vendor/bin/phpunit -c core --testsuite functional-javascript +``` + +* It is possible to use alternate browsers if the required dependencies are +installed. For example to use Firefox: + +``` +export MINK_DRIVER_ARGS_WEBDRIVER='["firefox", null, "http://localhost:4444/wd/hub"]' +./vendor/bin/phpunit -c core --testsuite functional-javascript +``` + +* To force all BrowserTestBase (including legacy JavascriptTestBase) tests to use +webdriver: + +``` +export MINK_DRIVER_CLASS='Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver' +./vendor/bin/phpunit -c core --testsuite functional-javascript +``` + +## Running legacy javascript tests + +Older javascript test may use the PhantomJSDriver. To run these tests you will +have to install and start PhantomJS. + +* Start PhantomJS: + ``` + phantomjs --ssl-protocol=any --ignore-ssl-errors=true ./vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /dev/null & + ``` + +* Then you can run the test: +``` +./vendor/bin/phpunit -c core --testsuite functional-javascript +``` + +## Running tests with a different user + If the default user is e.g. `www-data`, the above functional tests will have to be invoked with sudo instead: diff --git a/core/tests/TestSuites/TestSuiteBase.php b/core/tests/TestSuites/TestSuiteBase.php index e5925de..41ed616 100644 --- a/core/tests/TestSuites/TestSuiteBase.php +++ b/core/tests/TestSuites/TestSuiteBase.php @@ -42,10 +42,11 @@ protected function addTestsBySuiteNamespace($root, $suite_namespace) { // to this is Unit tests for historical reasons. if ($suite_namespace == 'Unit') { $tests = TestDiscovery::scanDirectory("Drupal\\Tests\\", "$root/core/tests/Drupal/Tests"); - $tests = array_filter($tests, function ($test) use ($root) { - // The Listeners directory does not contain tests. - return !preg_match("@^$root/core/tests/Drupal/Tests/Listeners(/|$)@", dirname($test)); - }); + $tests = array_flip(array_filter(array_flip($tests), function ($test_class) { + // The Listeners directory does not contain tests. Use the class name + // to be compatible with all operating systems. + return !preg_match('/^Drupal\\\\Tests\\\\Listeners\\\\/', $test_class); + })); $this->addTestFiles($tests); } else { diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index 50fef61..7eb6ecb 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -8,6 +8,7 @@ */ use Drupal\Component\Assertion\Handle; +use Drupal\Core\Composer\Composer; use PHPUnit\Runner\Version; /** @@ -150,6 +151,19 @@ function drupal_phpunit_populate_class_loader() { // Do class loader population. drupal_phpunit_populate_class_loader(); +// Ensure we have the correct PHPUnit version for the version of PHP. +if (class_exists('\PHPUnit_Runner_Version')) { + $phpunit_version = \PHPUnit_Runner_Version::id(); +} +else { + $phpunit_version = Version::id(); +} +if (!Composer::upgradePHPUnitCheck($phpunit_version)) { + $message = "PHPUnit testing framework version 6 or greater is required when running on PHP 7.2 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this."; + echo "\033[31m" . $message . "\n\033[0m"; + exit(1); +} + // Set sane locale settings, to ensure consistent string, dates, times and // numbers handling. // @see \Drupal\Core\DrupalKernel::bootEnvironment() @@ -170,7 +184,7 @@ function drupal_phpunit_populate_class_loader() { // PHPUnit 4 to PHPUnit 6 bridge. Tests written for PHPUnit 4 need to work on // PHPUnit 6 with a minimum of fuss. -if (class_exists('PHPUnit\Runner\Version') && version_compare(Version::id(), '6.1', '>=')) { +if (version_compare($phpunit_version, '6.1', '>=')) { class_alias('\PHPUnit\Framework\AssertionFailedError', '\PHPUnit_Framework_AssertionFailedError'); class_alias('\PHPUnit\Framework\Constraint\Count', '\PHPUnit_Framework_Constraint_Count'); class_alias('\PHPUnit\Framework\Error\Error', '\PHPUnit_Framework_Error'); diff --git a/core/themes/bartik/color/color.inc b/core/themes/bartik/color/color.inc index 55464f2..67b092f 100644 --- a/core/themes/bartik/color/color.inc +++ b/core/themes/bartik/color/color.inc @@ -31,7 +31,7 @@ 'footer' => '#292929', 'titleslogan' => '#fffeff', 'text' => '#3b3b3b', - 'link' => '#0071B3', + 'link' => '#0071b3', ], ], 'firehouse' => [ diff --git a/core/themes/bartik/css/colors.css b/core/themes/bartik/css/colors.css index 41dc054..6d71a9e 100644 --- a/core/themes/bartik/css/colors.css +++ b/core/themes/bartik/css/colors.css @@ -8,14 +8,14 @@ body { #main-wrapper, .region-primary-menu .menu-item a.is-active, .region-primary-menu .menu-item--active-trail a { - background: #ffffff; + background: #fff; } .tabs ul.primary li a.is-active { - background-color: #ffffff; + background-color: #fff; } .tabs ul.primary li.is-active a { - background-color: #ffffff; - border-bottom-color: #ffffff; + background-color: #fff; + border-bottom-color: #fff; } #header { background-color: #1d84c3; diff --git a/core/themes/bartik/css/components/captions.css b/core/themes/bartik/css/components/captions.css index cd24809..1564e8b 100644 --- a/core/themes/bartik/css/components/captions.css +++ b/core/themes/bartik/css/components/captions.css @@ -3,12 +3,12 @@ margin-bottom: 1.2em; } .caption > * { - background: #F3F3F3; + background: #f3f3f3; padding: 0.5ex; - border: 1px solid #CCC; + border: 1px solid #ccc; } .caption > figcaption { - border: 1px solid #CCC; + border: 1px solid #ccc; border-top: none; padding-top: 0.5ex; font-size: small; diff --git a/core/themes/bartik/css/components/demo-block.css b/core/themes/bartik/css/components/demo-block.css index 44a801f..07da3e6 100644 --- a/core/themes/bartik/css/components/demo-block.css +++ b/core/themes/bartik/css/components/demo-block.css @@ -4,7 +4,7 @@ */ .demo-block { - background: #ffff66; + background: #ff6; border: 1px dotted #9f9e00; color: #000; font: 90% "Lucida Grande", "Lucida Sans Unicode", sans-serif; diff --git a/core/themes/bartik/css/components/node-preview.css b/core/themes/bartik/css/components/node-preview.css index e391dd1..7638ba7 100644 --- a/core/themes/bartik/css/components/node-preview.css +++ b/core/themes/bartik/css/components/node-preview.css @@ -10,12 +10,11 @@ padding: 5px 10px; } .node-preview-backlink { - background-color: #419ff1; background: url(../../../../misc/icons/000000/chevron-left.svg) left no-repeat, -webkit-linear-gradient(top, #419ff1, #1076d5); background: url(../../../../misc/icons/000000/chevron-left.svg) left no-repeat, linear-gradient(to bottom, #419ff1, #1076d5); /* LTR */ border: 1px solid #0048c8; - border-radius: .4em; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .4); + border-radius: 0.4em; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4); color: #fff; font-size: 0.9em; line-height: normal; @@ -33,7 +32,6 @@ } .node-preview-backlink:focus, .node-preview-backlink:hover { - background-color: #419cf1; background: url(../../../../misc/icons/000000/chevron-left.svg) left no-repeat, -webkit-linear-gradient(top, #59abf3, #2a90ef); background: url(../../../../misc/icons/000000/chevron-left.svg) left no-repeat, linear-gradient(to bottom, #59abf3, #2a90ef); /* LTR */ border: 1px solid #0048c8; @@ -46,11 +44,10 @@ background: url(../../../../misc/icons/000000/chevron-right.svg) right no-repeat, linear-gradient(to bottom, #59abf3, #2a90ef); } .node-preview-backlink:active { - background-color: #0e69be; background: url(../../../../misc/icons/000000/chevron-left.svg) left no-repeat, -webkit-linear-gradient(top, #0e69be, #2a93ef); background: url(../../../../misc/icons/000000/chevron-left.svg) left no-repeat, linear-gradient(to bottom, #0e69be, #2a93ef); /* LTR */ border: 1px solid #0048c8; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .25); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25); } [dir="rtl"] .node-preview-backlink:active { background: url(../../../../misc/icons/000000/chevron-right.svg) right no-repeat, -webkit-linear-gradient(top, #0e69be, #2a93ef); diff --git a/core/themes/bartik/css/components/tabs.css b/core/themes/bartik/css/components/tabs.css index 499b99e..250c936 100644 --- a/core/themes/bartik/css/components/tabs.css +++ b/core/themes/bartik/css/components/tabs.css @@ -21,7 +21,7 @@ div.tabs { text-shadow: 0 1px 0 #fff; } .tabs ul.primary li.is-active a { - background-color: #ffffff; + background-color: #fff; border: 1px solid #bbb; } diff --git a/core/themes/bartik/css/components/views.css b/core/themes/bartik/css/components/views.css index 6171257..1e45f57 100644 --- a/core/themes/bartik/css/components/views.css +++ b/core/themes/bartik/css/components/views.css @@ -9,7 +9,7 @@ } .views-displays .tabs .open > a:hover, .views-displays .tabs .open > a:focus { - color: #0071B3; + color: #0071b3; } .views-displays .secondary .form-submit { font-size: 0.846em; @@ -22,14 +22,14 @@ /* Contextual filter options styles */ .views-filterable-options .filterable-option:nth-of-type(even) .form-type-checkbox { - background-color: #F9F9F9; + background-color: #f9f9f9; } /* Views action dropbutton styles */ .views-ui-display-tab-actions .dropbutton .form-submit { - color: #0071B3; + color: #0071b3; } .views-ui-display-tab-actions .dropbutton .form-submit:hover, .views-ui-display-tab-actions .dropbutton .form-submit:focus { - color: #018FE2; + color: #018fe2; } diff --git a/core/themes/classy/css/components/dialog.css b/core/themes/classy/css/components/dialog.css index ed9fbb0..f488432 100644 --- a/core/themes/classy/css/components/dialog.css +++ b/core/themes/classy/css/components/dialog.css @@ -33,7 +33,7 @@ .ui-dialog .ui-dialog-buttonpane { margin-top: 0; background: #f3f4ee; - padding: .3em 1em; + padding: 0.3em 1em; border-width: 1px 0 0 0; border-color: #ccc; } diff --git a/core/themes/classy/css/components/dropbutton.css b/core/themes/classy/css/components/dropbutton.css index cf8c40a..45e457b 100644 --- a/core/themes/classy/css/components/dropbutton.css +++ b/core/themes/classy/css/components/dropbutton.css @@ -5,7 +5,7 @@ .js .dropbutton-widget { background-color: white; - border: 1px solid #cccccc; + border: 1px solid #ccc; } .js .dropbutton-widget:hover { border-color: #b8b8b8; diff --git a/core/themes/classy/css/components/form.css b/core/themes/classy/css/components/form.css index aaf7022..e66f7b8 100644 --- a/core/themes/classy/css/components/form.css +++ b/core/themes/classy/css/components/form.css @@ -14,14 +14,14 @@ form .field-multiple-table .field-multiple-drag { padding-left: 0; } form .field-multiple-table .field-multiple-drag .tabledrag-handle { - padding-right: .5em; /* LTR */ + padding-right: 0.5em; /* LTR */ } [dir="rtl"] form .field-multiple-table .field-multiple-drag .tabledrag-handle { padding-right: 0; - padding-left: .5em; + padding-left: 0.5em; } form .field-add-more-submit { - margin: .5em 0 0; + margin: 0.5em 0 0; } /** diff --git a/core/themes/seven/css/base/elements.css b/core/themes/seven/css/base/elements.css index 3364f6e..9cc5ee8 100644 --- a/core/themes/seven/css/base/elements.css +++ b/core/themes/seven/css/base/elements.css @@ -23,7 +23,7 @@ hr { padding: 0; border: none; height: 1px; - background: #cccccc; + background: #ccc; } summary, .fieldgroup:not(.form-composite) > legend { @@ -148,7 +148,7 @@ ol { margin-right: 2em; } code { - margin: .5em 0; + margin: 0.5em 0; } pre { margin: 0.5em 0; diff --git a/core/themes/seven/css/components/buttons.css b/core/themes/seven/css/components/buttons.css index 6d0d50f..79b0fe4 100644 --- a/core/themes/seven/css/components/buttons.css +++ b/core/themes/seven/css/components/buttons.css @@ -62,7 +62,7 @@ /* Prevent focus ring being covered by next siblings. */ .button:focus { z-index: 10; - border: 1px solid #3AB2FF; + border: 1px solid #3ab2ff; box-shadow: 0 0 0.5em 0.1em hsla(203, 100%, 60%, 0.7); } .button:active { @@ -94,7 +94,7 @@ color: #fff; } .button--primary:focus { - border: 1px solid #1280DF; + border: 1px solid #1280df; } .button--primary:hover { box-shadow: 0 1px 2px hsla(203, 10%, 10%, 0.25); diff --git a/core/themes/seven/css/components/dialog.css b/core/themes/seven/css/components/dialog.css index 1660fc3..b4c5b1a 100644 --- a/core/themes/seven/css/components/dialog.css +++ b/core/themes/seven/css/components/dialog.css @@ -30,7 +30,7 @@ font-size: 1.231em; font-weight: 600; margin: 0; - color: #ffffff; + color: #fff; -webkit-font-smoothing: antialiased; } .ui-dialog .ui-dialog-titlebar-close { @@ -49,7 +49,7 @@ } .ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { - border-color: #ffffff; + border-color: #fff; } [dir="rtl"] .ui-dialog .ui-dialog-titlebar-close { right: auto; @@ -60,7 +60,7 @@ margin-top: -8px; } .ui-dialog .ui-widget-content.ui-dialog-content { - background: #ffffff; + background: #fff; overflow: auto; padding: 1em; } diff --git a/core/themes/seven/css/components/dropbutton.component.css b/core/themes/seven/css/components/dropbutton.component.css index 751ec2c..b7dd2e0 100644 --- a/core/themes/seven/css/components/dropbutton.component.css +++ b/core/themes/seven/css/components/dropbutton.component.css @@ -9,7 +9,7 @@ .js .dropbutton .dropbutton-action > input, .js .dropbutton .dropbutton-action > a, .js .dropbutton .dropbutton-action > button { - color: #333333; + color: #333; text-decoration: none; padding: 0; margin: 0; @@ -173,7 +173,7 @@ background-color: #f2f1eb; background-image: -webkit-linear-gradient(top, #f6f6f3, #e7e7df); background-image: linear-gradient(to bottom, #f6f6f3, #e7e7df); - color: #333333; + color: #333; text-decoration: none; text-shadow: 0 1px hsla(0, 0%, 100%, 0.6); font-weight: 600; diff --git a/core/themes/seven/css/components/field-ui.css b/core/themes/seven/css/components/field-ui.css index 9dd396e..85ade83 100644 --- a/core/themes/seven/css/components/field-ui.css +++ b/core/themes/seven/css/components/field-ui.css @@ -4,10 +4,10 @@ padding: 1px 8px; } #field-display-overview tr.field-plugin-settings-changed { - background: #ffffbb; + background: #ffb; } #field-display-overview tr.drag { - background: #ffee77; + background: #fe7; } #field-display-overview tr.field-plugin-settings-editing { background: #d5e9f2; diff --git a/core/themes/seven/css/components/form.css b/core/themes/seven/css/components/form.css index 9273288..9f27d31 100644 --- a/core/themes/seven/css/components/form.css +++ b/core/themes/seven/css/components/form.css @@ -84,7 +84,7 @@ label[for] { .form-disabled textarea.form-textarea, .form-disabled select.form-select { border-color: #d4d4d4; - background-color: hsla(0, 0%, 0%, .08); + background-color: hsla(0, 0%, 0%, 0.08); box-shadow: none; } .form-item input.error, @@ -105,7 +105,7 @@ label[for] { .form-item select.error:focus { border-color: #e62600; outline: 0; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 0 8px 1px #e62600; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 8px 1px #e62600; background-color: #fcf4f2; } .form-required:after { @@ -175,7 +175,7 @@ input.form-date, input.form-time, textarea.form-textarea { box-sizing: border-box; - padding: .3em .4em .3em .5em; /* LTR */ + padding: 0.3em 0.4em 0.3em 0.5em; /* LTR */ max-width: 100%; border: 1px solid #b8b8b8; border-top-color: #999; @@ -183,14 +183,14 @@ textarea.form-textarea { color: #333; border-radius: 2px; background: #fcfcfa; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .125); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.125); font-size: 1em; color: #595959; -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; transition: border linear 0.2s, box-shadow linear 0.2s; } [dir="rtl"] textarea.form-textarea { - padding: .3em .5em .3em .4em; + padding: 0.3em 0.5em 0.3em 0.4em; } .form-text:focus, .form-tel:focus, @@ -205,7 +205,7 @@ textarea.form-textarea { .form-time:focus { border-color: #40b6ff; outline: 0; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 0 8px #40b6ff; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 8px #40b6ff; background-color: #fff; } @@ -241,6 +241,7 @@ select { background: url(../../../../misc/icons/333333/caret-down.svg) no-repeat 99% 63%, -webkit-linear-gradient(top, #f6f6f3, #e7e7df); /* LTR */ + text-shadow: 0 1px hsla(0, 0%, 100%, 0.6); font-size: 0.875rem; -webkit-transition: all 0.1s; diff --git a/core/themes/seven/css/components/jquery.ui/theme.css b/core/themes/seven/css/components/jquery.ui/theme.css index cd899e1..4c05757 100644 --- a/core/themes/seven/css/components/jquery.ui/theme.css +++ b/core/themes/seven/css/components/jquery.ui/theme.css @@ -42,12 +42,12 @@ } .ui-state-disabled, .ui-widget-content .ui-state-disabled { - opacity: .35; + opacity: 0.35; filter: alpha(Opacity=35); } .ui-priority-secondary, .ui-widget-content .ui-priority-secondary { - opacity: .7; + opacity: 0.7; filter: alpha(Opacity=70); } @@ -331,7 +331,7 @@ */ .ui-widget-overlay { background: #000; - opacity: .7; + opacity: 0.7; filter: alpha(Opacity=70); } @@ -347,8 +347,8 @@ .ui-slider .ui-slider-handle { border: 1px solid #e4e4e4; border-bottom: 1px solid #b4b4b4; - border-left-color: #D2D2D2; - border-right-color: #D2D2D2; + border-left-color: #d2d2d2; + border-right-color: #d2d2d2; background-color: #e4e4e4; border-radius: 4px; } @@ -375,16 +375,16 @@ * Date Picker */ .ui-datepicker { - border: 1px solid #A6A6A6; - background: #FFF; + border: 1px solid #a6a6a6; + background: #fff; /* Override datepicker.css */ padding: 0; } /* Override tables.css */ .ui-datepicker-calendar thead tr { - border-bottom: 1px solid #A6A6A6; - border-top: 1px solid #A6A6A6; + border-bottom: 1px solid #a6a6a6; + border-top: 1px solid #a6a6a6; } .ui-datepicker-calendar tr:hover { background: transparent; diff --git a/core/themes/seven/css/components/system-status-counter.css b/core/themes/seven/css/components/system-status-counter.css index 8d71965..86f9668 100644 --- a/core/themes/seven/css/components/system-status-counter.css +++ b/core/themes/seven/css/components/system-status-counter.css @@ -11,7 +11,7 @@ display: inline-block; width: 100%; white-space: nowrap; - background: #FCFCFA; + background: #fcfcfa; } .system-status-counter__status-icon { display: inline-block; @@ -21,12 +21,12 @@ border-right: 1px solid #e6e4df; /* LTR */ border-left: 0; /* LTR */ background-color: #faf9f5; - box-shadow: 0 1px 1px rgba(0, 0, 0, .1) inset; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1) inset; } [dir="rtl"] .system-status-counter__status-icon { border-right: 0; border-left: 1px solid #e6e4df; - box-shadow: 0 1px 1px rgba(0, 0, 0, .1) inset; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1) inset; } .system-status-counter__status-icon:before { content: ""; diff --git a/core/themes/seven/css/components/system-status-report.css b/core/themes/seven/css/components/system-status-report.css index 7869fbd..2f4b338 100644 --- a/core/themes/seven/css/components/system-status-report.css +++ b/core/themes/seven/css/components/system-status-report.css @@ -34,9 +34,6 @@ color: inherit; text-transform: none; } -html:not(.details) .system-status-report__status-title { - padding-left: 0; -} .system-status-report__status-title .details-title { padding-left: 3em; /* LTR */ } @@ -132,7 +129,7 @@ html:not(.details) .system-status-report__status-title { [dir="rtl"] .system-status-report__status-title { float: right; } - .system-status-report__status-title::-webkit-details-marker { + html.js .system-status-report__status-title::-webkit-details-marker { display: none; } .collapse-processed > .system-status-report__status-title:before { diff --git a/core/themes/seven/css/components/tabs.css b/core/themes/seven/css/components/tabs.css index e9aa26a..f8b9966 100644 --- a/core/themes/seven/css/components/tabs.css +++ b/core/themes/seven/css/components/tabs.css @@ -9,7 +9,7 @@ .is-horizontal .tabs:before { content: ''; display: block; - background-color: #A6A6A6; + background-color: #a6a6a6; height: 1px; position: absolute; bottom: 0; @@ -86,7 +86,7 @@ li.tabs__tab a { z-index: 15; border-color: #a6a6a6; border-radius: 4px 0 0 0; /* LTR */ - background-color: #ffffff; + background-color: #fff; color: #004f80; } [dir="rtl"] .tabs.primary .tabs__tab.is-active { @@ -174,7 +174,7 @@ li.tabs__tab a { } .is-open .tabs__tab.is-active { border-color: #a6a6a6; - background-color: #ffffff; + background-color: #fff; color: #004f80; border-bottom: 1px solid #a6a6a6; } diff --git a/core/themes/seven/css/components/tour.theme.css b/core/themes/seven/css/components/tour.theme.css index 80348e0..6509ff8 100644 --- a/core/themes/seven/css/components/tour.theme.css +++ b/core/themes/seven/css/components/tour.theme.css @@ -95,7 +95,7 @@ } .joyride-expose-wrapper { - background-color: #ffffff; + background-color: #fff; } .joyride-expose-cover { diff --git a/core/themes/seven/css/components/views-ui.css b/core/themes/seven/css/components/views-ui.css index d212dd0..a9face2 100644 --- a/core/themes/seven/css/components/views-ui.css +++ b/core/themes/seven/css/components/views-ui.css @@ -157,7 +157,7 @@ details.fieldset-no-legend { /* @group Attachment details */ #edit-display-settings-title { - color: #008BCB; + color: #008bcb; } /* @end */ @@ -189,7 +189,7 @@ details.fieldset-no-legend { .views-displays .secondary .open > a:hover, .views-displays .secondary .open > a:focus { background-color: #f1f1f1; - color: #008BCB; + color: #008bcb; } .views-displays .secondary .action-list li:first-child { @@ -266,11 +266,11 @@ details.fieldset-no-legend { } .views-ui-rearrange-filter-form tr.drag td { - background-color: #FFEE77 !important; + background-color: #fe7 !important; } .views-ui-rearrange-filter-form tr.drag-previous td { - background-color: #FFFFBB !important; + background-color: #ffb !important; } /* @end */ diff --git a/core/themes/seven/css/theme/ckeditor-dialog.css b/core/themes/seven/css/theme/ckeditor-dialog.css index a7870d8..e791a2e 100644 --- a/core/themes/seven/css/theme/ckeditor-dialog.css +++ b/core/themes/seven/css/theme/ckeditor-dialog.css @@ -196,7 +196,7 @@ } .cke_reset_all .cke_dialog_footer_buttons a.cke_dialog_ui_button:focus { z-index: 10; - border: 1px solid #3AB2FF; + border: 1px solid #3ab2ff; box-shadow: 0 0 0.5em 0.1em hsla(203, 100%, 60%, 0.7); } .cke_reset_all .cke_dialog_footer_buttons a.cke_dialog_ui_button:active { diff --git a/core/themes/seven/templates/status-report-grouped.html.twig b/core/themes/seven/templates/status-report-grouped.html.twig index c5d6303..91aa298 100644 --- a/core/themes/seven/templates/status-report-grouped.html.twig +++ b/core/themes/seven/templates/status-report-grouped.html.twig @@ -24,7 +24,7 @@

{{ group.title }}

{% for requirement in group.items %} -
+
{% set summary_classes = [ 'system-status-report__status-title', diff --git a/core/themes/stable/css/ckeditor/ckeditor.admin.css b/core/themes/stable/css/ckeditor/ckeditor.admin.css index cc801e7..9a8a48b 100644 --- a/core/themes/stable/css/ckeditor/ckeditor.admin.css +++ b/core/themes/stable/css/ckeditor/ckeditor.admin.css @@ -180,7 +180,7 @@ padding: 4px 6px; position: relative; text-decoration: none; - text-shadow: 0 1px 0 rgba(255, 255, 255, .5); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); white-space: nowrap; } .ckeditor-toolbar-dividers { diff --git a/core/themes/stable/css/ckeditor/ckeditor.css b/core/themes/stable/css/ckeditor/ckeditor.css index b4b1e16..bc2c43a 100644 --- a/core/themes/stable/css/ckeditor/ckeditor.css +++ b/core/themes/stable/css/ckeditor/ckeditor.css @@ -7,7 +7,7 @@ .ckeditor-dialog-loading-link { border-radius: 0 0 5px 5px; - border: 1px solid #B6B6B6; + border: 1px solid #b6b6b6; border-top: none; background: white; padding: 3px 10px; diff --git a/core/themes/stable/css/core/dialog/off-canvas.base.css b/core/themes/stable/css/core/dialog/off-canvas.base.css index 9868ced..917dd23 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.base.css +++ b/core/themes/stable/css/core/dialog/off-canvas.base.css @@ -20,7 +20,7 @@ font-weight: normal; color: #85bef4; text-decoration: none; - transition: color .5s ease; + transition: color 0.5s ease; } #drupal-off-canvas a:focus, @@ -31,7 +31,7 @@ } #drupal-off-canvas hr { height: 1px; - background: #cccccc; + background: #ccc; } #drupal-off-canvas summary, #drupal-off-canvas .fieldgroup:not(.form-composite) > legend { diff --git a/core/themes/stable/css/core/dialog/off-canvas.button.css b/core/themes/stable/css/core/dialog/off-canvas.button.css index 96a6823..7345bd7 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.button.css +++ b/core/themes/stable/css/core/dialog/off-canvas.button.css @@ -24,7 +24,7 @@ background: transparent; font-size: 14px; color: #85bef4; - transition: color .5s ease; + transition: color 0.5s ease; } #drupal-off-canvas button.link:hover, #drupal-off-canvas button.link:focus { @@ -45,7 +45,7 @@ color: #f5f5f5; text-align: center; cursor: pointer; - transition: background .5s ease; + transition: background 0.5s ease; } #drupal-off-canvas input[type="submit"].button:hover, #drupal-off-canvas input[type="submit"].button:focus, diff --git a/core/themes/stable/css/core/dialog/off-canvas.details.css b/core/themes/stable/css/core/dialog/off-canvas.details.css index dcaea5e..1bd5097 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.details.css +++ b/core/themes/stable/css/core/dialog/off-canvas.details.css @@ -35,7 +35,7 @@ text-shadow: none; padding: 10px 20px; font-size: 14px; - transition: all .5s ease; + transition: all 0.5s ease; } #drupal-off-canvas summary:hover, #drupal-off-canvas summary:focus { diff --git a/core/themes/stable/css/core/dialog/off-canvas.dropbutton.css b/core/themes/stable/css/core/dialog/off-canvas.dropbutton.css index 30c4479..4a140be 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.dropbutton.css +++ b/core/themes/stable/css/core/dialog/off-canvas.dropbutton.css @@ -24,7 +24,7 @@ text-align: center; line-height: normal; cursor: pointer; - transition: background .5s ease; + transition: background 0.5s ease; } #drupal-off-canvas .dropbutton-widget:hover { background: #2b8bd8; @@ -224,7 +224,7 @@ background: transparent; } -/* Prevent list item from expanding it's container. */ +/* Prevent list item from expanding its container. */ #drupal-off-canvas td ul.dropbutton li.edit { width: 2em; height: 2em; diff --git a/core/themes/stable/css/core/dialog/off-canvas.form.css b/core/themes/stable/css/core/dialog/off-canvas.form.css index f1def25..a393f15 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.form.css +++ b/core/themes/stable/css/core/dialog/off-canvas.form.css @@ -84,7 +84,7 @@ #drupal-off-canvas .form-textarea, #drupal-off-canvas .form-date, #drupal-off-canvas .form-time { - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .125); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.125); background-color: #eee; border-color: #333; color: #595959; @@ -101,7 +101,7 @@ #drupal-off-canvas .form-date:focus, #drupal-off-canvas .form-time:focus { border-color: #40b6ff; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .125), 0 0 8px #40b6ff; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.125), 0 0 8px #40b6ff; background-color: #fff; } #drupal-off-canvas td .form-item, diff --git a/core/themes/stable/css/core/dialog/off-canvas.motion.css b/core/themes/stable/css/core/dialog/off-canvas.motion.css index 2d5ea5f..b3158e9 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.motion.css +++ b/core/themes/stable/css/core/dialog/off-canvas.motion.css @@ -7,5 +7,5 @@ */ .dialog-off-canvas-main-canvas { - transition: all .7s ease; + transition: all 0.7s ease; } diff --git a/core/themes/stable/css/core/dialog/off-canvas.tabledrag.css b/core/themes/stable/css/core/dialog/off-canvas.tabledrag.css index 33f670a..b901fb3 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.tabledrag.css +++ b/core/themes/stable/css/core/dialog/off-canvas.tabledrag.css @@ -53,7 +53,7 @@ text-decoration: none; } #drupal-off-canvas tr td { - transition: background .3s ease; + transition: background 0.3s ease; } #drupal-off-canvas tr td abbr { diff --git a/core/themes/stable/css/core/dialog/off-canvas.theme.css b/core/themes/stable/css/core/dialog/off-canvas.theme.css index 8074d91..351ef47 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.theme.css +++ b/core/themes/stable/css/core/dialog/off-canvas.theme.css @@ -40,12 +40,12 @@ position: absolute; top: calc(50% - 6px); right: 1em; - transition: all .5s ease; + transition: all 0.5s ease; } .ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar-close:hover, .ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar-close:focus { background-image: url(../../../images/core/icons/ffffff/ex.svg); - border: 3px solid #ffffff; + border: 3px solid #fff; } [dir="rtl"] .ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar-close { left: 1em; diff --git a/core/themes/stable/css/dblog/dblog.module.css b/core/themes/stable/css/dblog/dblog.module.css index ed531b7..88cccca 100644 --- a/core/themes/stable/css/dblog/dblog.module.css +++ b/core/themes/stable/css/dblog/dblog.module.css @@ -5,12 +5,12 @@ .dblog-filter-form .form-item-type, .dblog-filter-form .form-item-severity { display: inline-block; - margin: .1em .9em .1em .1em; /* LTR */ + margin: 0.1em 0.9em 0.1em 0.1em; /* LTR */ max-width: 30%; } [dir="rtl"] .dblog-filter-form .form-item-type, [dir="rtl"] .dblog-filter-form .form-item-severity { - margin: .1em .1em .1em .9em; + margin: 0.1em 0.1em 0.1em 0.9em; } .dblog-filter-form .form-actions { display: inline-block; diff --git a/core/themes/stable/css/field_ui/field_ui.admin.css b/core/themes/stable/css/field_ui/field_ui.admin.css index 08f6c2d..f171649 100644 --- a/core/themes/stable/css/field_ui/field_ui.admin.css +++ b/core/themes/stable/css/field_ui/field_ui.admin.css @@ -17,7 +17,7 @@ } .field-ui-overview .field-plugin-summary { float: left; /* LTR */ - font-size: .9em; + font-size: 0.9em; } [dir="rtl"] .field-ui-overview .field-plugin-summary { float: right; @@ -25,7 +25,7 @@ .field-ui-overview .field-plugin-summary-cell .warning { display: block; float: left; /* LTR */ - margin-right: .5em; + margin-right: 0.5em; } [dir="rtl"] .field-ui-overview .field-plugin-summary-cell .warning { float: right; diff --git a/core/themes/stable/css/image/editors/image.theme.css b/core/themes/stable/css/image/editors/image.theme.css index f7a1b3a..feaa87f 100644 --- a/core/themes/stable/css/image/editors/image.theme.css +++ b/core/themes/stable/css/image/editors/image.theme.css @@ -5,12 +5,12 @@ .quickedit-image-dropzone { background: rgba(116, 183, 255, 0.8); - transition: background .2s; + transition: background 0.2s; } .quickedit-image-icon { margin: 0 0 10px 0; - transition: margin .5s; + transition: margin 0.5s; } .quickedit-image-dropzone.hover { diff --git a/core/themes/stable/css/locale/locale.admin.css b/core/themes/stable/css/locale/locale.admin.css index 759a76c..221507d 100644 --- a/core/themes/stable/css/locale/locale.admin.css +++ b/core/themes/stable/css/locale/locale.admin.css @@ -72,12 +72,12 @@ vertical-align: top; } .locale-translation-update__wrapper { - background: transparent url(../../images/core/menu-collapsed.png) left .6em no-repeat; + background: transparent url(../../images/core/menu-collapsed.png) left 0.6em no-repeat; margin-left: -12px; padding-left: 12px; } .expanded .locale-translation-update__wrapper { - background: transparent url(../../images/core/menu-expanded.png) left .6em no-repeat; + background: transparent url(../../images/core/menu-expanded.png) left 0.6em no-repeat; } #locale-translation-status-form .description { cursor: pointer; diff --git a/core/themes/stable/css/quickedit/quickedit.icons.theme.css b/core/themes/stable/css/quickedit/quickedit.icons.theme.css index 856a78b..1b2234a 100644 --- a/core/themes/stable/css/quickedit/quickedit.icons.theme.css +++ b/core/themes/stable/css/quickedit/quickedit.icons.theme.css @@ -48,7 +48,7 @@ font-size: 1em; } .quickedit .icon-pencil { - margin-left: .5em; + margin-left: 0.5em; padding-left: 1.5em; } diff --git a/core/themes/stable/css/quickedit/quickedit.theme.css b/core/themes/stable/css/quickedit/quickedit.theme.css index 69fb7f3..cc65e8f 100644 --- a/core/themes/stable/css/quickedit/quickedit.theme.css +++ b/core/themes/stable/css/quickedit/quickedit.theme.css @@ -45,7 +45,7 @@ margin: 0; } .quickedit-form .form-wrapper { - margin: .5em; + margin: 0.5em; } /** @@ -55,35 +55,35 @@ opacity: 0; } .quickedit-animate-default { - -webkit-transition: all .4s ease; - transition: all .4s ease; + -webkit-transition: all 0.4s ease; + transition: all 0.4s ease; } .quickedit-animate-slow { - -webkit-transition: all .6s ease; - transition: all .6s ease; + -webkit-transition: all 0.6s ease; + transition: all 0.6s ease; } .quickedit-animate-delay-veryfast { - -webkit-transition-delay: .05s; - transition-delay: .05s; + -webkit-transition-delay: 0.05s; + transition-delay: 0.05s; } .quickedit-animate-delay-fast { - -webkit-transition-delay: .2s; - transition-delay: .2s; + -webkit-transition-delay: 0.2s; + transition-delay: 0.2s; } .quickedit-animate-disable-width { -webkit-transition: width 0s; transition: width 0s; } .quickedit-animate-only-visibility { - -webkit-transition: opacity .2s ease; - transition: opacity .2s ease; + -webkit-transition: opacity 0.2s ease; + transition: opacity 0.2s ease; } /** * In-place editors that don't use a popup. */ .quickedit-validation-errors .messages.error { - box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5); + box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, 0.5); background-color: white; } @@ -208,8 +208,8 @@ margin: 0; opacity: 1; padding: 0.345em; - -webkit-transition: opacity .1s ease; - transition: opacity .1s ease; + -webkit-transition: opacity 0.1s ease; + transition: opacity 0.1s ease; } .quickedit-button[aria-hidden="true"] { visibility: hidden; diff --git a/core/themes/stable/css/settings_tray/settings_tray.module.css b/core/themes/stable/css/settings_tray/settings_tray.module.css new file mode 100644 index 0000000..75ba047 --- /dev/null +++ b/core/themes/stable/css/settings_tray/settings_tray.module.css @@ -0,0 +1,23 @@ +/** + * @file + * Styling for Settings Tray module. + */ +/* + * Position the edit toolbar tab. + * @todo Move changes into contextual module when Settings Tray is not + * experimental: https://www.drupal.org/node/2784591. + */ +.toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab { + float: left; +} +[dir="rtl"] .toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab { + float: right; +} + +.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode a, +.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode input { + pointer-events: none; +} +.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode .contextual-links a { + pointer-events: inherit; +} diff --git a/core/themes/stable/css/settings_tray/settings_tray.motion.css b/core/themes/stable/css/settings_tray/settings_tray.motion.css new file mode 100644 index 0000000..820f708 --- /dev/null +++ b/core/themes/stable/css/settings_tray/settings_tray.motion.css @@ -0,0 +1,19 @@ +/** + * @file + * Motion effects for Settings Tray module. + * + * Motion effects are in a separate file so that they can be easily turned off + * to improve performance if desired. + */ + +/* Transition the edit icon in the toolbar. */ +#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before { + transition: all 0.7s ease; +} + +/* Transition the editables on the page, their contextual links and their hover states. */ +.dialog-off-canvas-main-canvas .contextual, +.dialog-off-canvas-main-canvas .js-settings-tray-edit-mode .settings-tray-editable, +.dialog-off-canvas-main-canvas.js-off-canvas-dialog-open .js-settings-tray-edit-mode .settings-tray-editable { + transition: all 0.7s ease; +} diff --git a/core/themes/stable/css/settings_tray/settings_tray.theme.css b/core/themes/stable/css/settings_tray/settings_tray.theme.css new file mode 100644 index 0000000..4a8a1f3 --- /dev/null +++ b/core/themes/stable/css/settings_tray/settings_tray.theme.css @@ -0,0 +1,70 @@ +/** + * @file + * Visual styling for Settings Tray module. + */ + +/* @todo remove the @imports when we find a better way to load these styles last. + * https://www.drupal.org/node/1945262. + */ + +/* Style the edit mode toolbar and tabs. */ +#toolbar-bar.js-settings-tray-edit-mode { + background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4); +} +.js-settings-tray-edit-mode .toolbar-item:not(.toolbar-icon-edit) { + color: #999; +} +.js-settings-tray-edit-mode .toolbar-item:not(.toolbar-icon-edit) .is-active { + color: #333; +} + +/* Style both the edit and editing states of the contextual links toggle tab. */ +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item, +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active, +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:focus { + background-color: #0066a1; + background-image: linear-gradient(to bottom, #0066a1, #005b98); + color: #eee; + text-shadow: none; + font-weight: bold; + outline: none; +} +/* Make the hover of the inactive state the same as the active state. */ +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:hover, +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active { + background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4); + color: #fff; +} +/* Make the hover of the active state the same as the inactive state. */ +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { + background-color: #0066a1; + background-image: linear-gradient(to bottom, #0066a1, #005b98); + color: #fff; +} +/* Make the inactive icon grey. */ +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:before { + background-image: url(../../../../misc/icons/bebebe/pencil.svg); +} +/* Make the active icon white. */ +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:before { + background-image: url(../../../../misc/icons/ffffff/pencil.svg); +} +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:hover:before { + background-image: url(../../../../misc/icons/ffffff/pencil.svg); +} +.toolbar-tab > .toolbar-icon.toolbar-icon-edit.toolbar-item:hover > .toolbar-icon-edit:before { + background-image: url(../../../../misc/icons/ffffff/pencil.svg); +} +.toolbar-tab > .button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before { + background-image: url(../../../../misc/icons/ffffff/pencil.svg); +} + +/* Style the editables while in edit mode. */ +.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode .settings-tray-editable { + outline: 1px dashed rgba(0, 0, 0, 0.5); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7); +} +.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode .settings-tray-editable:hover, +.dialog-off-canvas-main-canvas.js-settings-tray-edit-mode .settings-tray-editable.settings-tray-active-editable { + background-color: rgba(0, 0, 0, 0.2); +} diff --git a/core/themes/stable/css/settings_tray/settings_tray.toolbar.css b/core/themes/stable/css/settings_tray/settings_tray.toolbar.css new file mode 100644 index 0000000..68aa550 --- /dev/null +++ b/core/themes/stable/css/settings_tray/settings_tray.toolbar.css @@ -0,0 +1,66 @@ +/** + * @file + * Visual styling for the toolbar when Settings Tray module is enabled. + */ + +/* @todo Move this into toolbar when module is not experimental: + * https://www.drupal.org/node/2784593. + */ + +/* Style the edit mode toolbar and tabs. */ +#toolbar-bar.js-settings-tray-edit-mode { + background-color: #fff; +} +#toolbar-bar.js-settings-tray-edit-mode .toolbar-item { + color: #999; +} +#toolbar-bar.js-settings-tray-edit-mode .toolbar-item .is-active { + color: #333; +} + +/* Style both the edit and editing states of the contextual links toggle tab. */ +.toolbar-icon-edit.toolbar-item { + background-color: #0066a1; + background-image: linear-gradient(to bottom, #0066a1, #005b98); + color: #eee; + text-shadow: 0 1px hsla(0, 0%, 0%, 0.5); + font-weight: 700; + -webkit-font-smoothing: antialiased; +} +.toolbar-icon-edit.toolbar-item.is-active { + background-color: #0a7bc1; + background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4); + color: #fff; + text-shadow: 0 1px hsla(0, 0%, 0%, 0.5); + font-weight: 700; + -webkit-font-smoothing: antialiased; +} +.toolbar-tab:hover > .toolbar-icon-edit, +.toolbar-icon-edit:focus .toolbar-item { + background-color: #0a7bc1; + background-image: linear-gradient(to bottom, #0a7bc1, #0a6eb4); + border-color: #1e5c90; + color: #fff; + outline: none; +} +.toolbar-icon.toolbar-icon-edit.toolbar-item:before, +button.toolbar-icon.toolbar-icon-edit.toolbar-item:before { + background-image: url(../../../../misc/icons/bebebe/pencil.svg); +} +.toolbar-icon.toolbar-icon-edit.toolbar-item:before:hover, +button.toolbar-icon.toolbar-icon-edit.toolbar-item:before:focus { + background-image: url(../../../../misc/icons/ffffff/pencil.svg); +} +.toolbar-icon.toolbar-icon-edit.toolbar-item:hover > .toolbar-icon-edit:before { + background-image: url(../../../../misc/icons/ffffff/pencil.svg); +} +#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before { + background-image: url(../../../../misc/icons/ffffff/pencil.svg); +} + +#toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active { + color: #fff; +} +#toolbar-bar.js-settings-tray-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { + background-image: linear-gradient(to bottom, #0a6fb4, #0a65aa); +} diff --git a/core/themes/stable/css/shortcut/shortcut.theme.css b/core/themes/stable/css/shortcut/shortcut.theme.css index 73d58c6..3fe631c 100644 --- a/core/themes/stable/css/shortcut/shortcut.theme.css +++ b/core/themes/stable/css/shortcut/shortcut.theme.css @@ -32,11 +32,11 @@ margin-right: 0.3em; } .shortcut-action__message { - background: #000000; + background: #000; background: rgba(0, 0, 0, 0.5); border-radius: 5px; padding: 0 5px; - color: #ffffff; + color: #fff; display: inline-block; margin-left: 0.3em; /* LTR */ opacity: 0; diff --git a/core/themes/stable/css/system/components/progress.module.css b/core/themes/stable/css/system/components/progress.module.css index 80042af..3ae22ae 100644 --- a/core/themes/stable/css/system/components/progress.module.css +++ b/core/themes/stable/css/system/components/progress.module.css @@ -27,7 +27,7 @@ .progress__percentage { color: #555; overflow: hidden; - font-size: .875em; + font-size: 0.875em; margin-top: 0.2em; } .progress__description { diff --git a/core/themes/stable/css/system/components/system-status-report-counters.css b/core/themes/stable/css/system/components/system-status-report-counters.css index 1a4e240..2a2ef6b 100644 --- a/core/themes/stable/css/system/components/system-status-report-counters.css +++ b/core/themes/stable/css/system/components/system-status-report-counters.css @@ -5,11 +5,11 @@ .system-status-report-counters__item { width: 100%; - padding: .5em 0; + padding: 0.5em 0; text-align: center; white-space: nowrap; background-color: rgba(0, 0, 0, 0.063); - margin-bottom: .5em; + margin-bottom: 0.5em; } @media screen and (min-width: 60em) { diff --git a/core/themes/stable/css/system/system.admin.css b/core/themes/stable/css/system/system.admin.css index 39d11b5..24b41dc 100644 --- a/core/themes/stable/css/system/system.admin.css +++ b/core/themes/stable/css/system/system.admin.css @@ -117,7 +117,7 @@ small .admin-link:after { text-transform: none; } .system-modules td details a { - color: #5C5C5B; + color: #5c5c5b; border: 0; } .system-modules td details { @@ -234,7 +234,7 @@ small .admin-link:after { background-image: url(../../images/core/icons/e29700/warning.svg); } .system-status-report__entry__value { - padding: 1em .5em; + padding: 1em 0.5em; } /** diff --git a/core/themes/stable/css/toolbar/toolbar.menu.css b/core/themes/stable/css/toolbar/toolbar.menu.css index b4e8b2e..1687ad3 100644 --- a/core/themes/stable/css/toolbar/toolbar.menu.css +++ b/core/themes/stable/css/toolbar/toolbar.menu.css @@ -61,38 +61,38 @@ */ .toolbar .level-2 > ul { background-color: #fafafa; - border-bottom-color: #cccccc; + border-bottom-color: #ccc; border-top-color: #e5e5e5; } .toolbar .level-3 > ul { background-color: #f5f5f5; border-bottom-color: #c5c5c5; - border-top-color: #dddddd; + border-top-color: #ddd; } .toolbar .level-4 > ul { - background-color: #eeeeee; - border-bottom-color: #bbbbbb; + background-color: #eee; + border-bottom-color: #bbb; border-top-color: #d5d5d5; } .toolbar .level-5 > ul { background-color: #e5e5e5; border-bottom-color: #b5b5b5; - border-top-color: #cccccc; + border-top-color: #ccc; } .toolbar .level-6 > ul { - background-color: #eeeeee; - border-bottom-color: #aaaaaa; + background-color: #eee; + border-bottom-color: #aaa; border-top-color: #c5c5c5; } .toolbar .level-7 > ul { background-color: #fafafa; border-bottom-color: #b5b5b5; - border-top-color: #cccccc; + border-top-color: #ccc; } .toolbar .level-8 > ul { - background-color: #dddddd; - border-bottom-color: #cccccc; - border-top-color: #dddddd; + background-color: #ddd; + border-bottom-color: #ccc; + border-top-color: #ddd; } /** diff --git a/core/themes/stable/css/toolbar/toolbar.theme.css b/core/themes/stable/css/toolbar/toolbar.theme.css index a003678..f119a0e 100644 --- a/core/themes/stable/css/toolbar/toolbar.theme.css +++ b/core/themes/stable/css/toolbar/toolbar.theme.css @@ -31,13 +31,13 @@ .toolbar .toolbar-bar { background-color: #0f0f0f; box-shadow: -1px 0 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */ - color: #dddddd; + color: #ddd; } [dir="rtl"] .toolbar .toolbar-bar { box-shadow: 1px 0 3px 1px rgba(0, 0, 0, 0.3333); } .toolbar .toolbar-bar .toolbar-item { - color: #ffffff; + color: #fff; } .toolbar .toolbar-bar .toolbar-tab > .toolbar-item { font-weight: bold; @@ -56,7 +56,7 @@ * Toolbar tray. */ .toolbar .toolbar-tray { - background-color: #ffffff; + background-color: #fff; } .toolbar-horizontal .toolbar-tray > .toolbar-lining { padding-right: 5em; /* LTR */ @@ -67,16 +67,16 @@ } .toolbar .toolbar-tray-vertical { background-color: #f5f5f5; - border-right: 1px solid #aaaaaa; /* LTR */ + border-right: 1px solid #aaa; /* LTR */ box-shadow: -1px 0 5px 2px rgba(0, 0, 0, 0.3333); /* LTR */ } [dir="rtl"] .toolbar .toolbar-tray-vertical { - border-left: 1px solid #aaaaaa; + border-left: 1px solid #aaa; border-right: 0 none; box-shadow: 1px 0 5px 2px rgba(0, 0, 0, 0.3333); } .toolbar-horizontal .toolbar-tray { - border-bottom: 1px solid #aaaaaa; + border-bottom: 1px solid #aaa; box-shadow: -2px 1px 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */ } [dir="rtl"] .toolbar-horizontal .toolbar-tray { @@ -99,33 +99,33 @@ text-decoration: underline; } .toolbar .toolbar-menu { - background-color: #ffffff; + background-color: #fff; } .toolbar-horizontal .toolbar-tray .menu-item + .menu-item { - border-left: 1px solid #dddddd; /* LTR */ + border-left: 1px solid #ddd; /* LTR */ } [dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item + .menu-item { border-left: 0 none; - border-right: 1px solid #dddddd; + border-right: 1px solid #ddd; } .toolbar-horizontal .toolbar-tray .menu-item:last-child { - border-right: 1px solid #dddddd; /* LTR */ + border-right: 1px solid #ddd; /* LTR */ } [dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item:last-child { - border-left: 1px solid #dddddd; + border-left: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item + .menu-item { - border-top: 1px solid #dddddd; + border-top: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item:last-child { - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item .menu-item { border: 0 none; } .toolbar .toolbar-tray-vertical .toolbar-menu ul ul { - border-bottom: 1px solid #dddddd; - border-top: 1px solid #dddddd; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; } .toolbar .toolbar-tray-vertical .menu-item:last-child > ul { border-bottom: 0; diff --git a/core/themes/stable/css/user/user.admin.css b/core/themes/stable/css/user/user.admin.css index 10358c2..21ed153 100644 --- a/core/themes/stable/css/user/user.admin.css +++ b/core/themes/stable/css/user/user.admin.css @@ -18,5 +18,5 @@ /* Account settings */ .user-admin-settings .details-description { font-size: 0.85em; - padding-bottom: .5em; + padding-bottom: 0.5em; } diff --git a/core/themes/stable/css/views_ui/views_ui.admin.theme.css b/core/themes/stable/css/views_ui/views_ui.admin.theme.css index d405ba6..7ceec0a 100644 --- a/core/themes/stable/css/views_ui/views_ui.admin.theme.css +++ b/core/themes/stable/css/views_ui/views_ui.admin.theme.css @@ -624,10 +624,10 @@ td.group-title { border: none; } .views-ui-dialog .views-offset-top { - border-bottom: 1px solid #CCC; + border-bottom: 1px solid #ccc; } .views-ui-dialog .views-offset-bottom { - border-top: 1px solid #CCC; + border-top: 1px solid #ccc; } .views-ui-dialog .views-override > * { margin: 0; diff --git a/core/themes/stable/stable.info.yml b/core/themes/stable/stable.info.yml index 3f019d0..0411069 100644 --- a/core/themes/stable/stable.info.yml +++ b/core/themes/stable/stable.info.yml @@ -169,6 +169,15 @@ libraries-override: css/quickedit.theme.css: css/quickedit/quickedit.theme.css css/quickedit.icons.theme.css: css/quickedit/quickedit.icons.theme.css + settings_tray/drupal.settings_tray: + css: + component: + css/settings_tray.module.css: css/settings_tray/settings_tray.module.css + css/settings_tray.motion.css: css/settings_tray/settings_tray.motion.css + css/settings_tray.toolbar.css: css/settings_tray/settings_tray.toolbar.css + theme: + css/settings_tray.theme.css: css/settings_tray/settings_tray.theme.css + shortcut/drupal.shortcut: css: theme: diff --git a/core/themes/stable/templates/admin/status-report-grouped.html.twig b/core/themes/stable/templates/admin/status-report-grouped.html.twig index bbeaa47..1914c16 100644 --- a/core/themes/stable/templates/admin/status-report-grouped.html.twig +++ b/core/themes/stable/templates/admin/status-report-grouped.html.twig @@ -23,7 +23,7 @@

{{ group.title }}

{% for requirement in group.items %} -
+
{% set summary_classes = [ 'system-status-report__status-title', diff --git a/core/themes/stable/templates/content/media-reference-help.html.twig b/core/themes/stable/templates/content/media-reference-help.html.twig new file mode 100644 index 0000000..9243c5d --- /dev/null +++ b/core/themes/stable/templates/content/media-reference-help.html.twig @@ -0,0 +1,66 @@ +{# +/** + * @file + * Theme override for media reference fields. + * + * @see template_preprocess_field_multiple_value_form() + * @see core/themes/classy/templates/form/fieldset.html.twig + */ +#} +{% + set classes = [ + 'js-form-item', + 'form-item', + 'js-form-wrapper', + 'form-wrapper', + ] +%} + + {% + set legend_span_classes = [ + 'fieldset-legend', + required ? 'js-form-required', + required ? 'form-required', + ] + %} + {# Always wrap fieldset legends in a for CSS positioning. #} + + {{ original_label }} + + +
+ {% if media_add_help %} + + {% trans %} + Create new media + {% endtrans %} +
+
+ {{ media_add_help }} +
+ {% endif %} + + {% if multiple %} + {{ table }} + {% else %} + {% for element in elements %} + {{ element }} + {% endfor %} + {% endif %} + + + {% if multiple and description.content %} +
    +
  • {{ media_list_help }} {{ media_list_link }} {{ allowed_types_help }}
  • +
  • {{ description.content }}
  • +
+ {% else %} + {{ media_list_help }} {{ media_list_link }} {{ allowed_types_help }} + {% endif %} + {% if multiple and button %} +
{{ button }}
+ {% endif %} +
+ +
+ diff --git a/core/themes/stark/css/layout.css b/core/themes/stark/css/layout.css index e69de29..ea49f69 100644 --- a/core/themes/stark/css/layout.css +++ b/core/themes/stark/css/layout.css @@ -0,0 +1,8 @@ +/** + * @file + * Presentational styles for Drupal stark theme layout. + * + * It is left empty for testing purposes. + * + * @see https://www.drupal.org/project/drupal/issues/2349711 + */ diff --git a/core/yarn.lock b/core/yarn.lock index d4bab4a..1eda0e7 100644 --- a/core/yarn.lock +++ b/core/yarn.lock @@ -31,6 +31,10 @@ ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" @@ -38,6 +42,15 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.2.3: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -50,16 +63,26 @@ ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -anymatch@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: - arrify "^1.0.0" - micromatch "^2.1.5" + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" aproba@^1.0.3: version "1.1.1" @@ -90,9 +113,17 @@ arr-diff@^2.0.0: dependencies: arr-flatten "^1.0.1" -arr-flatten@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" array-differ@^1.0.0: version "1.0.0" @@ -102,6 +133,10 @@ array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" +array-iterate@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-iterate/-/array-iterate-1.1.1.tgz#865bf7f8af39d6b0982c60902914ac76bc0108f6" + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -116,6 +151,10 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + array.prototype.find@^2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90" @@ -123,7 +162,7 @@ array.prototype.find@^2.0.1: define-properties "^1.1.2" es-abstract "^1.7.0" -arrify@^1.0.0: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -139,6 +178,10 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + ast-types-flow@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" @@ -151,6 +194,10 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +atob@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" + autoprefixer@^6.0.0: version "6.7.7" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" @@ -162,6 +209,17 @@ autoprefixer@^6.0.0: postcss "^5.2.16" postcss-value-parser "^3.2.3" +autoprefixer@^7.1.2: + version "7.2.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.2.5.tgz#04ccbd0c6a61131b6d13f53d371926092952d192" + dependencies: + browserslist "^2.11.1" + caniuse-lite "^1.0.30000791" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^6.0.16" + postcss-value-parser "^3.2.3" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -178,41 +236,49 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" -babel-core@6.24.1, babel-core@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.24.1.tgz#8c428564dce1e1f41fb337ec34f4c3b022b5ad83" +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: - babel-code-frame "^6.22.0" - babel-generator "^6.24.1" + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" babel-helpers "^6.24.1" babel-messages "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - babylon "^6.11.0" - convert-source-map "^1.1.0" - debug "^2.1.1" - json5 "^0.5.0" - lodash "^4.2.0" - minimatch "^3.0.2" - path-is-absolute "^1.0.0" - private "^0.1.6" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" slash "^1.0.0" - source-map "^0.5.0" + source-map "^0.5.6" -babel-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" +babel-generator@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" dependencies: babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-types "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" detect-indent "^4.0.0" jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" + lodash "^4.17.4" + source-map "^0.5.6" trim-right "^1.0.1" babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: @@ -322,7 +388,7 @@ babel-messages@^6.23.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-add-header-comment@1.0.3: +babel-plugin-add-header-comment@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/babel-plugin-add-header-comment/-/babel-plugin-add-header-comment-1.0.3.tgz#511c4901062640d5a480b4ac3edd6944195850ec" @@ -541,9 +607,9 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-preset-env@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.4.0.tgz#c8e02a3bcc7792f23cded68e0355b9d4c28f0f7a" +babel-preset-env@^1.4.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" dependencies: babel-plugin-check-es2015-constants "^6.22.0" babel-plugin-syntax-trailing-function-commas "^6.22.0" @@ -572,20 +638,21 @@ babel-preset-env@1.4.0: babel-plugin-transform-es2015-unicode-regex "^6.22.0" babel-plugin-transform-exponentiation-operator "^6.22.0" babel-plugin-transform-regenerator "^6.22.0" - browserslist "^1.4.0" + browserslist "^2.1.2" invariant "^2.2.2" + semver "^5.3.0" -babel-register@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" dependencies: - babel-core "^6.24.1" - babel-runtime "^6.22.0" - core-js "^2.4.0" + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" home-or-tmp "^2.0.0" - lodash "^4.2.0" + lodash "^4.17.4" mkdirp "^0.5.1" - source-map-support "^0.4.2" + source-map-support "^0.4.15" babel-runtime@^6.18.0, babel-runtime@^6.22.0: version "6.23.0" @@ -594,6 +661,13 @@ babel-runtime@^6.18.0, babel-runtime@^6.22.0: core-js "^2.4.0" regenerator-runtime "^0.10.0" +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + babel-template@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" @@ -604,6 +678,16 @@ babel-template@^6.24.1: babylon "^6.11.0" lodash "^4.2.0" +babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + babel-traverse@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" @@ -618,6 +702,20 @@ babel-traverse@^6.24.1: invariant "^2.2.0" lodash "^4.2.0" +babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + babel-types@^6.19.0, babel-types@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" @@ -627,14 +725,47 @@ babel-types@^6.19.0, babel-types@^6.24.1: lodash "^4.2.0" to-fast-properties "^1.0.1" +babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + babylon@^6.11.0, babylon@^6.15.0: version "6.17.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.0.tgz#37da948878488b9c4e3c4038893fa3314b3fc932" -balanced-match@^0.4.0, balanced-match@^0.4.1: +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +bail@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.2.tgz#f7d6c1731630a9f9f0d4d35ed1f962e2074a1764" + +balanced-match@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -657,11 +788,11 @@ boom@2.x.x: dependencies: hoek "2.x.x" -brace-expansion@^1.0.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" +brace-expansion@^1.0.0, brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" dependencies: - balanced-match "^0.4.1" + balanced-match "^1.0.0" concat-map "0.0.1" braces@^1.8.2: @@ -672,13 +803,36 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -browserslist@^1.1.1, browserslist@^1.1.3, browserslist@^1.4.0, browserslist@^1.7.6: +braces@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + define-property "^1.0.0" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +browserslist@^1.1.1, browserslist@^1.1.3, browserslist@^1.7.6: version "1.7.7" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" dependencies: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" +browserslist@^2.1.2, browserslist@^2.11.1: + version "2.11.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2" + dependencies: + caniuse-lite "^1.0.30000792" + electron-to-chromium "^1.3.30" + buffer-shims@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" @@ -687,6 +841,20 @@ builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -704,18 +872,38 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" +camelcase-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" + dependencies: + camelcase "^4.1.0" + map-obj "^2.0.0" + quick-lru "^1.0.0" + camelcase@^2.0.0, camelcase@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + caniuse-db@^1.0.30000187, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000664" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000664.tgz#e16316e5fdabb9c7209b2bf0744ffc8a14201f22" + version "1.0.30000793" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000793.tgz#3c00c66e423a7a1907c7dd96769a78b2afa8a72e" + +caniuse-lite@^1.0.30000791, caniuse-lite@^1.0.30000792: + version "1.0.30000792" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000792.tgz#d0cea981f8118f3961471afbb43c9a1e5bbf0332" caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +ccount@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.2.tgz#53b6a2f815bb77b9c2871f7b9a72c3a25f1d8e89" + chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -726,24 +914,59 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chokidar@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" +chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +character-entities-html4@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.1.tgz#359a2a4a0f7e29d3dc2ac99bdbe21ee39438ea50" + +character-entities-legacy@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.1.tgz#f40779df1a101872bb510a3d295e1fccf147202f" + +character-entities@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.1.tgz#f76871be5ef66ddb7f8f8e3478ecc374c27d6dca" + +character-reference-invalid@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.1.tgz#942835f750e4ec61a308e60c2ef8cc1011202efc" + +chokidar@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.0.tgz#6686313c541d3274b2a5c01233342037948c911b" dependencies: - anymatch "^1.3.0" + anymatch "^2.0.0" async-each "^1.0.0" - glob-parent "^2.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" inherits "^2.0.1" is-binary-path "^1.0.0" - is-glob "^2.0.0" + is-glob "^4.0.0" + normalize-path "^2.1.1" path-is-absolute "^1.0.0" readdirp "^2.0.0" optionalDependencies: fsevents "^1.0.0" circular-json@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" cli-cursor@^1.0.1: version "1.0.2" @@ -778,13 +1001,34 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" +collapse-white-space@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + color-diff@^0.1.3: version "0.1.7" resolved "https://registry.yarnpkg.com/color-diff/-/color-diff-0.1.7.tgz#6db78cd9482a8e459d40821eaf4b503283dcb8e2" +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + colorguard@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/colorguard/-/colorguard-1.2.0.tgz#f3facaf5caaeba4ef54653d9fb25bb73177c0d84" + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorguard/-/colorguard-1.2.1.tgz#249647c9702481d9143384fc9813662311afde98" dependencies: chalk "^1.1.1" color-diff "^0.1.3" @@ -803,6 +1047,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -823,21 +1071,29 @@ contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" -convert-source-map@^1.1.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" +convert-source-map@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" core-js@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" +core-js@^2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" cosmiconfig@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.3.tgz#952771eb0dddc1cb3fa2f6fbe51a522e93b3ee0a" + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" dependencies: is-directory "^0.3.1" js-yaml "^3.4.3" @@ -847,9 +1103,18 @@ cosmiconfig@^2.1.1: parse-json "^2.2.0" require-from-string "^1.1.0" -cross-env@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-4.0.0.tgz#16083862d08275a4628b0b243b121bedaa55dd80" +cosmiconfig@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^3.0.0" + require-from-string "^2.0.1" + +cross-env@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.3.tgz#f8ae18faac87692b0a8b4d2f7000d4ec3a85dfd7" dependencies: cross-spawn "^5.1.0" is-windows "^1.0.0" @@ -910,22 +1175,39 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -debug@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - -debug@^2.1.1, debug@^2.2.0, debug@^2.6.0: +debug@^2.1.1, debug@^2.2.0: version "2.6.6" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.6.tgz#a9fa6fbe9ca43cf1e79f73b75c0189cbb7d6db5a" dependencies: ms "0.7.3" -decamelize@^1.1.1, decamelize@^1.1.2: +debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decamelize-keys@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + deep-extend@~0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" @@ -941,6 +1223,18 @@ define-properties@^1.1.2: foreach "^2.0.5" object-keys "^1.0.8" +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + del@^2.0.2: version "2.2.2" resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" @@ -967,6 +1261,13 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +dir-glob@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + doctrine@1.5.0, doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -998,6 +1299,40 @@ doiuse@^2.4.1: through2 "^0.6.3" yargs "^3.5.4" +dom-serializer@0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domelementtype@1, domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + dependencies: + is-obj "^1.0.0" + duplexer2@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" @@ -1014,15 +1349,19 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" -electron-to-chromium@^1.2.7: - version "1.3.8" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.8.tgz#b2c8a2c79bb89fbbfd3724d9555e15095b5f5fb6" +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30: + version "1.3.31" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.31.tgz#00d832cba9fe2358652b0c48a8816c8e3a037e9f" emoji-regex@^6.1.0: version "6.4.2" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.2.tgz#a30b6fee353d406d96cfb9fa765bdc82897eff6e" -error-ex@^1.2.0: +entities@^1.1.1, entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" dependencies: @@ -1114,43 +1453,42 @@ eslint-config-airbnb-base@^11.1.0: version "11.1.3" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.1.3.tgz#0e8db71514fa36b977fbcf977c01edcf863e0cf0" -eslint-config-airbnb@14.1.0: +eslint-config-airbnb@^14.1.0: version "14.1.0" resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-14.1.0.tgz#355d290040bbf8e00bf8b4b19f4b70cbe7c2317f" dependencies: eslint-config-airbnb-base "^11.1.0" -eslint-import-resolver-node@^0.2.0: - version "0.2.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz#5add8106e8c928db2cba232bcd9efa846e3da16c" +eslint-import-resolver-node@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" dependencies: - debug "^2.2.0" - object-assign "^4.0.1" - resolve "^1.1.6" + debug "^2.6.9" + resolve "^1.5.0" -eslint-module-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.0.0.tgz#a6f8c21d901358759cdc35dbac1982ae1ee58bce" +eslint-module-utils@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449" dependencies: - debug "2.2.0" + debug "^2.6.8" pkg-dir "^1.0.0" -eslint-plugin-import@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.2.0.tgz#72ba306fad305d67c4816348a4699a4229ac8b4e" +eslint-plugin-import@^2.2.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz#fa1b6ef31fcb3c501c09859c1b86f1fc5b986894" dependencies: builtin-modules "^1.1.1" contains-path "^0.1.0" - debug "^2.2.0" + debug "^2.6.8" doctrine "1.5.0" - eslint-import-resolver-node "^0.2.0" - eslint-module-utils "^2.0.0" + eslint-import-resolver-node "^0.3.1" + eslint-module-utils "^2.1.1" has "^1.0.1" lodash.cond "^4.3.0" minimatch "^3.0.3" - pkg-up "^1.0.0" + read-pkg-up "^2.0.0" -eslint-plugin-jsx-a11y@4.0.0: +eslint-plugin-jsx-a11y@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-4.0.0.tgz#779bb0fe7b08da564a422624911de10061e048ee" dependencies: @@ -1161,7 +1499,7 @@ eslint-plugin-jsx-a11y@4.0.0: jsx-ast-utils "^1.0.0" object-assign "^4.0.1" -eslint-plugin-react@6.10.3: +eslint-plugin-react@^6.10.3: version "6.10.3" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78" dependencies: @@ -1171,7 +1509,7 @@ eslint-plugin-react@6.10.3: jsx-ast-utils "^1.3.4" object.assign "^4.0.4" -eslint@3.19.0: +eslint@^3.19.0: version "3.19.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" dependencies: @@ -1222,6 +1560,10 @@ esprima@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + esquery@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" @@ -1270,13 +1612,38 @@ expand-brackets@^0.1.4: dependencies: is-posix-bracket "^0.1.0" +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + expand-range@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" dependencies: fill-range "^2.1.0" -extend@~3.0.0: +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0, extend@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -1286,10 +1653,31 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" +extglob@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -1322,6 +1710,15 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -1329,9 +1726,15 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + flat-cache@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + version "1.3.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" dependencies: circular-json "^0.3.1" del "^2.0.2" @@ -1342,7 +1745,7 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" -for-in@^1.0.1: +for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -1368,6 +1771,12 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1431,10 +1840,14 @@ get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" -get-stdin@^5.0.0: +get-stdin@^5.0.0, get-stdin@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -1454,7 +1867,14 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5: +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -1465,10 +1885,25 @@ glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^9.0.0, globals@^9.14.0: version "9.17.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + globby@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" @@ -1490,10 +1925,27 @@ globby@^6.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + globjoin@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" +gonzales-pe@^4.0.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.3.tgz#41091703625433285e0aee3aa47829fc1fbeb6f2" + dependencies: + minimist "1.1.x" + graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -1519,10 +1971,41 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + has@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" @@ -1550,14 +2033,25 @@ home-or-tmp@^2.0.0: os-tmpdir "^1.0.1" hosted-git-info@^2.1.4: - version "2.4.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.2.tgz#0076b9f46a270506ddbaaea56496897460612a67" + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" -html-tags@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-1.1.1.tgz#869f43859f12d9bdc3892419e494a628aa1b204e" +html-tags@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" -http-signature@~1.1.0: +htmlparser2@^3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + +http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" dependencies: @@ -1565,9 +2059,9 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" -ignore@^3.2.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" +ignore@^3.2.0, ignore@^3.3.3, ignore@^3.3.5: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" imurmurhash@^0.1.4: version "0.1.4" @@ -1579,6 +2073,10 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -1631,8 +2129,35 @@ invert-kv@^1.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" irregular-plurals@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.2.0.tgz#38f299834ba8c00c30be9c554e137269752ff3ac" + version "1.4.0" + resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.4.0.tgz#2ca9b033651111855412f16be5d77c62a458a766" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-alphabetical@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.1.tgz#c77079cc91d4efac775be1034bf2d243f95e6f08" + +is-alphanumeric@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4" + +is-alphanumerical@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.1.tgz#dfb4aa4d1085e33bdb61c2dee9c80e9c6c19f53b" + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" is-arrayish@^0.2.1: version "0.2.1" @@ -1644,9 +2169,9 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" +is-buffer@^1.1.4, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" is-builtin-module@^1.0.0: version "1.0.0" @@ -1658,17 +2183,49 @@ is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" +is-decimal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.1.tgz#f5fb6a94996ad9e8e3761fbfbd091f1fca8c4e82" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + is-directory@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" is-dotfile@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" is-equal-shallow@^0.1.3: version "0.1.3" @@ -1676,14 +2233,24 @@ is-equal-shallow@^0.1.3: dependencies: is-primitive "^2.0.0" -is-extendable@^0.1.1: +is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -1706,6 +2273,22 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + +is-hexadecimal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" + is-my-json-valid@^2.10.0: version "2.16.0" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693" @@ -1715,12 +2298,28 @@ is-my-json-valid@^2.10.0: jsonpointer "^4.0.0" xtend "^4.0.0" -is-number@^2.0.2, is-number@^2.1.0: +is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" dependencies: kind-of "^3.0.2" +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-odd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088" + dependencies: + is-number "^3.0.0" + is-path-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" @@ -1732,11 +2331,21 @@ is-path-in-cwd@^1.0.0: is-path-inside "^1.0.0" is-path-inside@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" dependencies: path-is-inside "^1.0.1" +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + is-posix-bracket@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" @@ -1781,10 +2390,18 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" +is-whitespace-character@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.1.tgz#9ae0176f3282b65457a1992cdb084f8a5f833e3b" + is-windows@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" +is-word-character@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.1.tgz#5a03fa1ea91ace8a6eb0c7cd770eb86d65c8befb" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -1803,6 +2420,10 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -1814,14 +2435,25 @@ jodid25519@^1.0.0: jsbn "~0.1.0" js-base64@^2.1.9: - version "2.1.9" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce" + version "2.4.1" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.1.tgz#e02813181cd53002888e918935467acb2910e596" js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-yaml@^3.4.3, js-yaml@^3.5.1: +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.4.3, js-yaml@^3.9.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^3.5.1: version "3.8.3" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" dependencies: @@ -1840,6 +2472,14 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +json-parse-better-errors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -1854,7 +2494,7 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json5@^0.5.0: +json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -1892,15 +2532,39 @@ jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" -kind-of@^3.0.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.0.tgz#b58abe4d5c044ad33726a8c1525b48cf891bff07" +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" dependencies: is-buffer "^1.1.5" -known-css-properties@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.0.7.tgz#9104343a2adfd8ef3b07bdee7a325e4d44ed9371" +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0, kind-of@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + +known-css-properties@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.2.0.tgz#899c94be368e55b42d7db8d5be7d73a4a4a41454" + +known-css-properties@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.5.0.tgz#6ff66943ed4a5b55657ee095779a91f4536f8084" + +lazy-cache@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" + dependencies: + set-getter "^0.1.0" lcid@^1.0.0: version "1.0.0" @@ -1932,15 +2596,36 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" -lodash@^3.0.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - -lodash@^4.0.0, lodash@^4.1.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: +lodash@4.17.4, lodash@^4.0.0, lodash@^4.1.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -1950,6 +2635,16 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" +log-symbols@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + dependencies: + chalk "^2.0.1" + +longest-streak@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.2.tgz#2421b6ba939a443bb9ffebf596585a50b4c38e2e" + loose-envify@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" @@ -1970,10 +2665,43 @@ lru-cache@^4.0.1: pseudomap "^1.0.1" yallist "^2.0.0" +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" +map-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + +markdown-escapes@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.1.tgz#1994df2d3af4811de59a6714934c2b2292734518" + +markdown-table@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.1.tgz#4b3dd3a133d1518b8ef0dbc709bf2a1b4824bc8c" + +mathml-tag-names@^2.0.0, mathml-tag-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz#8d41268168bf86d1102b98109e28e531e7a34578" + +mdast-util-compact@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.1.tgz#cdb5f84e2b6a2d3114df33bd05d9cb32e3c4083a" + dependencies: + unist-util-modify-children "^1.0.0" + unist-util-visit "^1.1.0" + meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -1989,7 +2717,21 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -micromatch@^2.1.5, micromatch@^2.3.11: +meow@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-4.0.0.tgz#fd5855dd008db5b92c552082db1c307cba20b29d" + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist "^1.1.3" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + +micromatch@^2.3.11: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" dependencies: @@ -2007,6 +2749,24 @@ micromatch@^2.1.5, micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" +micromatch@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.0" + define-property "^1.0.0" + extend-shallow "^2.0.1" + extglob "^2.0.2" + fragment-cache "^0.2.1" + kind-of "^6.0.0" + nanomatch "^1.2.5" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + mime-db@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" @@ -2017,34 +2777,58 @@ mime-types@^2.1.12, mime-types@~2.1.7: dependencies: mime-db "~1.27.0" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: brace-expansion "^1.0.0" +minimist-options@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" +minimist@1.1.x: + version "1.1.3" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" + minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +mixin-deep@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.0.tgz#47a8732ba97799457c8c1eca28f95132d7e8150a" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - ms@0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + multimatch@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" @@ -2062,6 +2846,22 @@ nan@^2.3.0: version "2.6.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" +nanomatch@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^1.0.0" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + is-odd "^1.0.0" + kind-of "^5.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2088,15 +2888,15 @@ nopt@^4.0.1: osenv "^0.1.4" normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.3.8" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.8.tgz#d819eda2a9dedbd1ffa563ea4071d936782295bb" + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" dependencies: hosted-git-info "^2.1.4" is-builtin-module "^1.0.0" semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.1: +normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" dependencies: @@ -2135,10 +2935,24 @@ object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + object-keys@^1.0.10, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + object.assign@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" @@ -2154,6 +2968,12 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + once@^1.3.0, once@^1.3.3: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2161,8 +2981,8 @@ once@^1.3.0, once@^1.3.3: wrappy "1" onecolor@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.4.tgz#75a46f80da6c7aaa5b4daae17a47198bd9652494" + version "3.0.5" + resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.5.tgz#36eff32201379efdf1180fb445e51a8e2425f9f6" onetime@^1.0.0: version "1.1.0" @@ -2200,6 +3020,33 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +parse-entities@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.1.1.tgz#8112d88471319f27abae4d64964b122fe4e1b890" + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -2215,13 +3062,38 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" + dependencies: + error-ex "^1.3.1" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" dependencies: pinkie-promise "^2.0.0" -path-is-absolute@^1.0.0: +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -2241,14 +3113,30 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" -pify@^2.0.0: +pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -2272,12 +3160,6 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" -pkg-up@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26" - dependencies: - find-up "^1.0.0" - plur@^2.0.0, plur@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a" @@ -2288,13 +3170,31 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + +postcss-html@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.12.0.tgz#39b6adb4005dfc5464df7999c0f81c95bced7e50" + dependencies: + htmlparser2 "^3.9.2" + remark "^8.0.0" + unist-util-find-all-after "^1.0.1" + postcss-less@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-0.14.0.tgz#c631b089c6cce422b9a10f3a958d2bedd3819324" dependencies: postcss "^5.0.21" -postcss-media-query-parser@^0.2.0: +postcss-less@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-1.1.3.tgz#6930525271bfe38d5793d33ac09c1a546b87bb51" + dependencies: + postcss "^5.2.16" + +postcss-media-query-parser@^0.2.0, postcss-media-query-parser@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" @@ -2316,16 +3216,44 @@ postcss-reporter@^3.0.0: log-symbols "^1.0.2" postcss "^5.0.0" +postcss-reporter@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-5.0.0.tgz#a14177fd1342829d291653f2786efd67110332c3" + dependencies: + chalk "^2.0.1" + lodash "^4.17.4" + log-symbols "^2.0.0" + postcss "^6.0.8" + postcss-resolve-nested-selector@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" +postcss-safe-parser@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-3.0.1.tgz#b753eff6c7c0aea5e8375fbe4cde8bf9063ff142" + dependencies: + postcss "^6.0.6" + +postcss-sass@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.2.0.tgz#e55516441e9526ba4b380a730d3a02e9eaa78c7a" + dependencies: + gonzales-pe "^4.0.3" + postcss "^6.0.6" + postcss-scss@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-0.4.1.tgz#ad771b81f0f72f5f4845d08aa60f93557653d54c" dependencies: postcss "^5.2.13" +postcss-scss@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-1.0.3.tgz#4c00ab440fc1c994134e3d4e600c23341af6cd27" + dependencies: + postcss "^6.0.15" + postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.1.1: version "2.2.3" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" @@ -2334,19 +3262,35 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.1.1: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-value-parser@^3.1.1, postcss-value-parser@^3.2.3: +postcss-selector-parser@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" + dependencies: + dot-prop "^4.1.1" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-value-parser@^3.1.1, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" postcss@^5.0.0, postcss@^5.0.18, postcss@^5.0.20, postcss@^5.0.21, postcss@^5.0.4, postcss@^5.0.8, postcss@^5.2.13, postcss@^5.2.16, postcss@^5.2.4: - version "5.2.17" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" dependencies: chalk "^1.1.3" js-base64 "^2.1.9" source-map "^0.5.6" supports-color "^3.2.3" +postcss@^6.0.14, postcss@^6.0.15, postcss@^6.0.16, postcss@^6.0.6, postcss@^6.0.8: + version "6.0.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.16.tgz#112e2fe2a6d2109be0957687243170ea5589e146" + dependencies: + chalk "^2.3.0" + source-map "^0.6.1" + supports-color "^5.1.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -2359,6 +3303,10 @@ private@^0.1.6: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" +private@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -2379,12 +3327,16 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +quick-lru@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" + randomatic@^1.1.3: - version "1.1.6" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" dependencies: - is-number "^2.0.2" - kind-of "^3.0.2" + is-number "^3.0.0" + kind-of "^4.0.0" rc@^1.1.7: version "1.2.1" @@ -2408,6 +3360,20 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -2416,6 +3382,22 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + "readable-stream@>=1.0.33-1 <1.1.0-0": version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -2476,6 +3458,13 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" + dependencies: + indent-string "^3.0.0" + strip-indent "^2.0.0" + regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" @@ -2484,6 +3473,10 @@ regenerator-runtime@^0.10.0: version "0.10.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + regenerator-transform@0.9.11: version "0.9.11" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283" @@ -2493,11 +3486,16 @@ regenerator-transform@0.9.11: private "^0.1.6" regex-cache@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" dependencies: is-equal-shallow "^0.1.3" - is-primitive "^2.0.0" + +regex-not@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9" + dependencies: + extend-shallow "^2.0.1" regexpu-core@^2.0.0: version "2.0.0" @@ -2517,15 +3515,62 @@ regjsparser@^0.1.4: dependencies: jsesc "~0.5.0" +remark-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-4.0.0.tgz#99f1f049afac80382366e2e0d0bd55429dd45d8b" + dependencies: + collapse-white-space "^1.0.2" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^1.0.2" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim "0.0.1" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^1.0.0" + vfile-location "^2.0.0" + xtend "^4.0.1" + +remark-stringify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-4.0.0.tgz#4431884c0418f112da44991b4e356cfe37facd87" + dependencies: + ccount "^1.0.0" + is-alphanumeric "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + longest-streak "^2.0.1" + markdown-escapes "^1.0.0" + markdown-table "^1.1.0" + mdast-util-compact "^1.0.0" + parse-entities "^1.0.2" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + stringify-entities "^1.0.1" + unherit "^1.0.4" + xtend "^4.0.1" + +remark@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/remark/-/remark-8.0.0.tgz#287b6df2fe1190e263c1d15e486d3fa835594d6d" + dependencies: + remark-parse "^4.0.0" + remark-stringify "^4.0.0" + unified "^6.0.0" + remove-trailing-separator@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz#615ebb96af559552d4bf4057c8436d486ab63cc4" + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" repeat-element@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" -repeat-string@^1.5.2: +repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -2535,6 +3580,10 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +replace-ext@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + request@^2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" @@ -2566,6 +3615,10 @@ require-from-string@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" +require-from-string@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" + require-uncached@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -2577,9 +3630,17 @@ resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" -resolve-from@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" resolve@^1.1.6: version "1.3.3" @@ -2587,6 +3648,12 @@ resolve@^1.1.6: dependencies: path-parse "^1.0.5" +resolve@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -2594,12 +3661,18 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: +rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" +rimraf@^2.2.8: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -2614,7 +3687,11 @@ safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" -"semver@2 || 3 || 4 || 5", semver@^5.3.0: +"semver@2 || 3 || 4 || 5": + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -2622,10 +3699,34 @@ set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" +set-getter@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" + dependencies: + to-object-path "^0.3.0" + set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -2656,27 +3757,78 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + dependencies: + is-fullwidth-code-point "^2.0.0" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^2.0.0" + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" dependencies: hoek "2.x.x" -source-map-support@^0.4.2: - version "0.4.15" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" +source-map-resolve@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" + dependencies: + atob "^2.0.0" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" dependencies: source-map "^0.5.6" +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + source-map@^0.4.2: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.6: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" spdx-correct@~1.0.0: version "1.0.2" @@ -2692,9 +3844,15 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" -specificity@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.0.tgz#332472d4e5eb5af20821171933998a6bc3b1ce6f" +specificity@^0.3.0, specificity@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.2.tgz#99e6511eceef0f8d9b57924937aac2cb13d13c42" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" split2@^0.2.1: version "0.2.1" @@ -2721,6 +3879,17 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +state-toggle@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + stream-combiner@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" @@ -2736,12 +3905,12 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" +string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: is-fullwidth-code-point "^2.0.0" - strip-ansi "^3.0.0" + strip-ansi "^4.0.0" string_decoder@~0.10.x: version "0.10.31" @@ -2753,6 +3922,15 @@ string_decoder@~1.0.0: dependencies: buffer-shims "~1.0.0" +stringify-entities@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-1.3.1.tgz#b150ec2d72ac4c1b5f324b51fb6b28c9cdff058c" + dependencies: + character-entities-html4 "^1.0.0" + character-entities-legacy "^1.0.0" + is-alphanumerical "^1.0.0" + is-hexadecimal "^1.0.0" + stringstream@~0.0.4: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -2763,6 +3941,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -2779,6 +3963,10 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" +strip-indent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -2787,7 +3975,7 @@ style-search@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" -stylehacks@^2.3.1, stylehacks@^2.3.2: +stylehacks@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-2.3.2.tgz#64c83e0438a68c9edf449e8c552a7d9ab6009b0b" dependencies: @@ -2803,30 +3991,30 @@ stylehacks@^2.3.1, stylehacks@^2.3.2: text-table "^0.2.0" write-file-stdout "0.0.2" -stylelint-checkstyle-formatter@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stylelint-checkstyle-formatter/-/stylelint-checkstyle-formatter-0.1.0.tgz#8c402853e92a0aae83715670caf63e79f21199a7" +stylelint-checkstyle-formatter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/stylelint-checkstyle-formatter/-/stylelint-checkstyle-formatter-0.1.1.tgz#9d650e892bc30c6037341dc951e3dbafd13d0d67" dependencies: - lodash "^3.0.0" + lodash "4.17.4" stylelint-config-standard@^16.0.0: version "16.0.0" resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-16.0.0.tgz#bb7387bff1d7dd7186a52b3ebf885b2405d691bf" -stylelint-no-browser-hacks@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stylelint-no-browser-hacks/-/stylelint-no-browser-hacks-1.0.2.tgz#2a94ac8021ade88706afbc64ef5b940c5fef12f2" +stylelint-no-browser-hacks@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stylelint-no-browser-hacks/-/stylelint-no-browser-hacks-1.1.0.tgz#1444b37459c9c799fa9905429b6d76d2bc82158f" dependencies: - stylehacks "^2.3.1" - stylelint "^7.8.0" + stylehacks "^2.3.2" + stylelint "^8.0.0" -stylelint@^7.10.1, stylelint@^7.8.0: - version "7.10.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-7.10.1.tgz#209a7ce5e781fc2a62489fbb31ec0201ec675db2" +stylelint@^7.13.0: + version "7.13.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-7.13.0.tgz#111f97b6da72e775c80800d6bb6f5f869997785d" dependencies: autoprefixer "^6.0.0" balanced-match "^0.4.0" - chalk "^1.1.1" + chalk "^2.0.1" colorguard "^1.2.0" cosmiconfig "^2.1.1" debug "^2.6.0" @@ -2836,15 +4024,17 @@ stylelint@^7.10.1, stylelint@^7.8.0: get-stdin "^5.0.0" globby "^6.0.0" globjoin "^0.1.4" - html-tags "^1.1.1" + html-tags "^2.0.0" ignore "^3.2.0" imurmurhash "^0.1.4" - known-css-properties "^0.0.7" + known-css-properties "^0.2.0" lodash "^4.17.4" log-symbols "^1.0.2" + mathml-tag-names "^2.0.0" meow "^3.3.0" micromatch "^2.3.11" normalize-selector "^0.2.0" + pify "^2.3.0" postcss "^5.0.20" postcss-less "^0.14.0" postcss-media-query-parser "^0.2.0" @@ -2853,7 +4043,7 @@ stylelint@^7.10.1, stylelint@^7.8.0: postcss-scss "^0.4.0" postcss-selector-parser "^2.1.1" postcss-value-parser "^3.1.1" - resolve-from "^2.0.0" + resolve-from "^3.0.0" specificity "^0.3.0" string-width "^2.0.0" style-search "^0.1.0" @@ -2862,12 +4052,62 @@ stylelint@^7.10.1, stylelint@^7.8.0: svg-tags "^1.0.0" table "^4.0.1" +stylelint@^8.0.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-8.4.0.tgz#c2dbaeb17236917819f9206e1c0df5fddf6f83c3" + dependencies: + autoprefixer "^7.1.2" + balanced-match "^1.0.0" + chalk "^2.0.1" + cosmiconfig "^3.1.0" + debug "^3.0.0" + execall "^1.0.0" + file-entry-cache "^2.0.0" + get-stdin "^5.0.1" + globby "^7.0.0" + globjoin "^0.1.4" + html-tags "^2.0.0" + ignore "^3.3.3" + imurmurhash "^0.1.4" + known-css-properties "^0.5.0" + lodash "^4.17.4" + log-symbols "^2.0.0" + mathml-tag-names "^2.0.1" + meow "^4.0.0" + micromatch "^2.3.11" + normalize-selector "^0.2.0" + pify "^3.0.0" + postcss "^6.0.6" + postcss-html "^0.12.0" + postcss-less "^1.1.0" + postcss-media-query-parser "^0.2.3" + postcss-reporter "^5.0.0" + postcss-resolve-nested-selector "^0.1.1" + postcss-safe-parser "^3.0.1" + postcss-sass "^0.2.0" + postcss-scss "^1.0.2" + postcss-selector-parser "^3.1.0" + postcss-value-parser "^3.3.0" + resolve-from "^4.0.0" + specificity "^0.3.1" + string-width "^2.1.0" + style-search "^0.1.0" + sugarss "^1.0.0" + svg-tags "^1.0.0" + table "^4.0.1" + sugarss@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-0.2.0.tgz#ac34237563327c6ff897b64742bf6aec190ad39e" dependencies: postcss "^5.2.4" +sugarss@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.1.tgz#be826d9003e0f247735f92365dc3fd7f1bae9e44" + dependencies: + postcss "^6.0.14" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -2878,6 +4118,18 @@ supports-color@^3.2.3: dependencies: has-flag "^1.0.0" +supports-color@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +supports-color@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" + dependencies: + has-flag "^2.0.0" + svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" @@ -2900,15 +4152,15 @@ table@^3.7.8: string-width "^2.0.0" table@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435" + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" dependencies: - ajv "^4.7.0" - ajv-keywords "^1.0.0" - chalk "^1.1.1" - lodash "^4.0.0" - slice-ansi "0.0.4" - string-width "^2.0.0" + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" tar-pack@^3.4.0: version "3.4.0" @@ -2950,6 +4202,31 @@ to-fast-properties@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae" + dependencies: + define-property "^0.2.5" + extend-shallow "^2.0.1" + regex-not "^1.0.0" + tough-cookie@~2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" @@ -2960,10 +4237,26 @@ trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" +trim-newlines@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +trim-trailing-lines@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.0.tgz#7aefbb7808df9d669f6da2e438cac8c46ada7684" + +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + +trough@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.1.tgz#a9fd8b0394b0ae8fff82e0633a0a36ccad5b5f86" + tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" @@ -2992,10 +4285,89 @@ uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +unherit@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.0.tgz#6b9aaedfbf73df1756ad9e316dd981885840cd7d" + dependencies: + inherits "^2.0.1" + xtend "^4.0.1" + +unified@^6.0.0: + version "6.1.6" + resolved "https://registry.yarnpkg.com/unified/-/unified-6.1.6.tgz#5ea7f807a0898f1f8acdeefe5f25faa010cc42b1" + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-plain-obj "^1.1.0" + trough "^1.0.0" + vfile "^2.0.0" + x-is-function "^1.0.4" + x-is-string "^0.1.0" + +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" +unist-util-find-all-after@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-1.0.1.tgz#4e5512abfef7e0616781aecf7b1ed751c00af908" + dependencies: + unist-util-is "^2.0.0" + +unist-util-is@^2.0.0, unist-util-is@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.1.tgz#0c312629e3f960c66e931e812d3d80e77010947b" + +unist-util-modify-children@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-1.1.1.tgz#66d7e6a449e6f67220b976ab3cb8b5ebac39e51d" + dependencies: + array-iterate "^1.0.0" + +unist-util-remove-position@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.1.tgz#5a85c1555fc1ba0c101b86707d15e50fa4c871bb" + dependencies: + unist-util-visit "^1.1.0" + +unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.1.tgz#3ccbdc53679eed6ecf3777dd7f5e3229c1b6aa3c" + +unist-util-visit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.3.0.tgz#41ca7c82981fd1ce6c762aac397fc24e35711444" + dependencies: + unist-util-is "^2.1.1" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +use@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" + dependencies: + define-property "^0.2.5" + isobject "^3.0.0" + lazy-cache "^2.0.2" + user-home@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" @@ -3023,6 +4395,25 @@ verror@1.3.6: dependencies: extsprintf "1.0.2" +vfile-location@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.2.tgz#d3675c59c877498e492b4756ff65e4af1a752255" + +vfile-message@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.0.0.tgz#a6adb0474ea400fa25d929f1d673abea6a17e359" + dependencies: + unist-util-stringify-position "^1.1.1" + +vfile@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a" + dependencies: + is-buffer "^1.1.4" + replace-ext "1.0.0" + unist-util-stringify-position "^1.0.0" + vfile-message "^1.0.0" + which@^1.2.9: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" @@ -3064,7 +4455,15 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0: +x-is-function@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/x-is-function/-/x-is-function-1.0.4.tgz#5d294dc3d268cbdd062580e0c5df77a391d1fa1e" + +x-is-string@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82" + +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"