From ea75488842205a599426a158bc02e3ed2ebef114 Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Fri, 8 Dec 2017 11:48:35 -0800 Subject: [PATCH 001/232] nightwatch --- core/js_tests/index.js | 13 ++ core/nightwatch.json | 38 ++++ core/package.json | 6 +- core/yarn.lock | 503 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 546 insertions(+), 14 deletions(-) create mode 100644 core/js_tests/index.js create mode 100644 core/nightwatch.json diff --git a/core/js_tests/index.js b/core/js_tests/index.js new file mode 100644 index 000000000000..4481c72acd8c --- /dev/null +++ b/core/js_tests/index.js @@ -0,0 +1,13 @@ +module.exports = { + 'Demo test Google' : function (browser) { + browser + .url('http://www.google.com') + .waitForElementVisible('body', 1000) + .setValue('input[type=text]', 'nightwatch') + .waitForElementVisible('button[name=btnG]', 1000) + .click('button[name=btnG]') + .pause(1000) + .assert.containsText('#main', 'Night Watch') + .end(); + } +}; diff --git a/core/nightwatch.json b/core/nightwatch.json new file mode 100644 index 000000000000..5de1bddc351a --- /dev/null +++ b/core/nightwatch.json @@ -0,0 +1,38 @@ +{ + "src_folders" : ["js_tests"], + "output_folder" : "reports", + "custom_commands_path" : "", + "custom_assertions_path" : "", + "page_objects_path" : "", + "globals_path" : "", + "selenium" : { + "start_process" : false, + "server_path" : "", + "log_path" : "", + "port" : 4444, + "cli_args" : { + "webdriver.chrome.driver" : "" + } + }, + "test_settings" : { + "default" : { + "launch_url" : "http://localhost", + "selenium_port" : 4444, + "selenium_host" : "localhost", + "silent": true, + "screenshots" : { + "enabled" : false, + "path" : "" + }, + "desiredCapabilities": { + "browserName": "firefox", + "marionette": true + } + }, + "chrome" : { + "desiredCapabilities": { + "browserName": "chrome" + } + } + } +} diff --git a/core/package.json b/core/package.json index b33263a3a4f2..f894d177e311 100644 --- a/core/package.json +++ b/core/package.json @@ -12,7 +12,8 @@ "lint:core-js-passing": "node ./node_modules/eslint/bin/eslint.js --quiet --config=.eslintrc.passing.json --ext=.es6.js . || exit 0", "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", "lint:css": "stylelint \"**/*.css\" || exit 0", - "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0" + "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", + "test:js": "./node_modules/.bin/nightwatch" }, "devDependencies": { "babel-core": "6.24.1", @@ -52,5 +53,8 @@ } ] ] + }, + "dependencies": { + "nightwatch": "^0.9.19" } } diff --git a/core/yarn.lock b/core/yarn.lock index d4bab4acd4ae..860daca44171 100644 --- a/core/yarn.lock +++ b/core/yarn.lock @@ -27,6 +27,13 @@ acorn@^5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" +agent-base@2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7" + dependencies: + extend "~3.0.0" + semver "~5.0.1" + ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -139,10 +146,18 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" +assertion-error@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.0.tgz#c7f85438fdd466bc7ca16ab90c81513797a5d23b" + 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" +ast-types@0.x.x: + version "0.10.1" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -672,6 +687,10 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + browserslist@^1.1.1, browserslist@^1.1.3, browserslist@^1.4.0, browserslist@^1.7.6: version "1.7.7" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" @@ -687,6 +706,10 @@ 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" +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -716,6 +739,13 @@ caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +chai-nightwatch@~0.1.x: + version "0.1.1" + resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9" + dependencies: + assertion-error "1.0.0" + deep-eql "0.1.3" + 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" @@ -774,6 +804,10 @@ co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" +co@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/co/-/co-3.0.6.tgz#1445f226c5eb956138e68c9ac30167ea7d2e6bda" + 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" @@ -803,6 +837,12 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +commander@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -910,6 +950,16 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" + +debug@2: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + debug@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" @@ -926,6 +976,12 @@ decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-eql@0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" + dependencies: + type-detect "0.1.1" + 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 +997,14 @@ define-properties@^1.1.2: foreach "^2.0.5" object-keys "^1.0.8" +degenerator@~1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095" + dependencies: + ast-types "0.x.x" + escodegen "1.x.x" + esprima "3.x.x" + del@^2.0.2: version "2.2.2" resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" @@ -961,12 +1025,20 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + detect-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" dependencies: repeating "^2.0.0" +diff@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + doctrine@1.5.0, doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -1014,6 +1086,10 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ejs@2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" + 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" @@ -1097,10 +1173,21 @@ es6-weak-map@^2.0.1: es6-iterator "^2.0.1" es6-symbol "^3.1.1" -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" +escodegen@1.x.x: + version "1.9.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.5.6" + escope@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" @@ -1218,7 +1305,7 @@ espree@^3.4.0: acorn "^5.0.1" acorn-jsx "^3.0.0" -esprima@^3.1.1: +esprima@3.x.x, esprima@^3.1.1, esprima@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -1276,7 +1363,7 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -extend@~3.0.0: +extend@3, extend@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -1308,6 +1395,10 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" +file-uri-to-path@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" @@ -1396,6 +1487,13 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" +ftp@~0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" + dependencies: + readable-stream "1.1.x" + xregexp "2.0.0" + function-bind@^1.0.2, function-bind@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" @@ -1435,6 +1533,17 @@ get-stdin@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" +get-uri@2: + version "2.0.1" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.1.tgz#dbdcacacd8c608a38316869368117697a1631c59" + dependencies: + data-uri-to-buffer "1" + debug "2" + extend "3" + file-uri-to-path "1" + ftp "~0.3.10" + readable-stream "2" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -1454,6 +1563,17 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" +glob@7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" @@ -1498,6 +1618,14 @@ graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +growl@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" + har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" @@ -1557,6 +1685,23 @@ html-tags@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-1.1.1.tgz#869f43859f12d9bdc3892419e494a628aa1b204e" +http-errors@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-proxy-agent@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-1.0.0.tgz#cc1ce38e453bf984a0f7702d2dd59c73d081284a" + dependencies: + agent-base "2" + debug "2" + extend "3" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -1565,6 +1710,18 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" + dependencies: + agent-base "2" + debug "2" + extend "3" + +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + ignore@^3.2.0: version "3.2.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" @@ -1590,7 +1747,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -1630,6 +1787,14 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +ip@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.0.1.tgz#c7e356cdea225ae71b36d70f2e71a92ba4e42590" + +ip@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + irregular-plurals@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.2.0.tgz#38f299834ba8c00c30be9c554e137269752ff3ac" @@ -1854,6 +2019,10 @@ 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" +json3@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + json5@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -1932,10 +2101,127 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +lodash._arraycopy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" + +lodash._arrayeach@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._baseclone@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" + dependencies: + lodash._arraycopy "^3.0.0" + lodash._arrayeach "^3.0.0" + lodash._baseassign "^3.0.0" + lodash._basefor "^3.0.0" + lodash.isarray "^3.0.0" + lodash.keys "^3.0.0" + +lodash._baseclone@^4.0.0: + version "4.5.7" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-4.5.7.tgz#ce42ade08384ef5d62fa77c30f61a46e686f8434" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basecreate@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" + +lodash._basefor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._stack@^4.0.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lodash._stack/-/lodash._stack-4.1.3.tgz#751aa76c1b964b047e76d14fc72a093fcb5e2dd0" + +lodash.clone@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-3.0.3.tgz#84688c73d32b5a90ca25616963f189252a997043" + dependencies: + lodash._baseclone "^3.0.0" + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^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.create@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" + dependencies: + lodash._baseassign "^3.0.0" + lodash._basecreate "^3.0.0" + lodash._isiterateecall "^3.0.0" + +lodash.defaultsdeep@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.3.2.tgz#6c1a586e6c5647b0e64e2d798141b8836158be8a" + dependencies: + lodash._baseclone "^4.0.0" + lodash._stack "^4.0.0" + lodash.isplainobject "^4.0.0" + lodash.keysin "^4.0.0" + lodash.mergewith "^4.0.0" + lodash.rest "^4.0.0" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isplainobject@^4.0.0: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.keysin@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-4.2.0.tgz#8cc3fb35c2d94acc443a1863e02fa40799ea6f28" + +lodash.mergewith@^4.0.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" + +lodash.rest@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/lodash.rest/-/lodash.rest-4.0.5.tgz#954ef75049262038c96d1fc98b28fdaf9f0772aa" + lodash@^3.0.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -1970,6 +2256,10 @@ lru-cache@^4.0.1: pseudomap "^1.0.1" yallist "^2.0.0" +lru-cache@~2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" + 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" @@ -2017,7 +2307,7 @@ 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.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: @@ -2031,12 +2321,36 @@ 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" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +mkdirp@0.5.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" +mkpath@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-1.0.0.tgz#ebb3a977e7af1c683ae6fda12b545a6ba6c5853d" + +mocha-nightwatch@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mocha-nightwatch/-/mocha-nightwatch-3.2.2.tgz#91bcb9b3bde057dd7677c78125e491e58d66647c" + dependencies: + browser-stdout "1.3.0" + commander "2.9.0" + debug "2.2.0" + diff "1.4.0" + escape-string-regexp "1.0.5" + glob "7.0.5" + growl "1.9.2" + json3 "3.3.2" + lodash.create "3.1.1" + mkdirp "0.5.1" + supports-color "3.1.2" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -2045,6 +2359,10 @@ 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" @@ -2066,6 +2384,25 @@ natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" +netmask@~1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" + +nightwatch@^0.9.19: + version "0.9.19" + resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-0.9.19.tgz#4bd9757273d30b845f04847a98b71be9bb7c4b3b" + dependencies: + chai-nightwatch "~0.1.x" + ejs "2.5.7" + lodash.clone "3.0.3" + lodash.defaultsdeep "4.3.2" + minimatch "3.0.3" + mkpath "1.0.0" + mocha-nightwatch "3.2.2" + optimist "0.6.1" + proxy-agent "2.0.0" + q "1.4.1" + node-pre-gyp@^0.6.29: version "0.6.34" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.34.tgz#94ad1c798a11d7fc67381b50d47f8cc18d9799f7" @@ -2168,7 +2505,14 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" -optionator@^0.8.2: +optimist@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" dependencies: @@ -2200,6 +2544,30 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +pac-proxy-agent@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-1.1.0.tgz#34a385dfdf61d2f0ecace08858c745d3e791fd4d" + dependencies: + agent-base "2" + debug "2" + extend "3" + get-uri "2" + http-proxy-agent "1" + https-proxy-agent "1" + pac-resolver "~2.0.0" + raw-body "2" + socks-proxy-agent "2" + +pac-resolver@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-2.0.0.tgz#99b88d2f193fbdeefc1c9a529c1f3260ab5277cd" + dependencies: + co "~3.0.6" + degenerator "~1.0.2" + ip "1.0.1" + netmask "~1.0.4" + thunkify "~2.1.1" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -2367,6 +2735,19 @@ progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +proxy-agent@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-2.0.0.tgz#57eb5347aa805d74ec681cb25649dba39c933499" + dependencies: + agent-base "2" + debug "2" + extend "3" + http-proxy-agent "1" + https-proxy-agent "1" + lru-cache "~2.6.5" + pac-proxy-agent "1" + socks-proxy-agent "2" + pseudomap@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -2375,6 +2756,10 @@ punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +q@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" + qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -2386,6 +2771,15 @@ randomatic@^1.1.3: is-number "^2.0.2" kind-of "^3.0.2" +raw-body@2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + rc@^1.1.7: version "1.2.1" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" @@ -2416,18 +2810,30 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.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" +readable-stream@1.1.x, readable-stream@^1.0.33, readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" dependencies: core-util-is "~1.0.0" inherits "~2.0.1" isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^1.0.33, readable-stream@~1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" +readable-stream@2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +"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" dependencies: core-util-is "~1.0.0" inherits "~2.0.1" @@ -2614,10 +3020,18 @@ safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + "semver@2 || 3 || 4 || 5", semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +semver@~5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" + set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -2626,6 +3040,10 @@ 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" +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -2656,12 +3074,31 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" +smart-buffer@^1.0.13: + version "1.1.15" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" + 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" +socks-proxy-agent@2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-2.1.1.tgz#86ebb07193258637870e13b7bd99f26c663df3d3" + dependencies: + agent-base "2" + extend "3" + socks "~1.1.5" + +socks@~1.1.5: + version "1.1.10" + resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a" + dependencies: + ip "^1.1.4" + smart-buffer "^1.0.13" + 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" @@ -2678,6 +3115,10 @@ 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" + spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -2721,6 +3162,10 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +"statuses@>= 1.3.1 < 2": + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + stream-combiner@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" @@ -2753,6 +3198,12 @@ string_decoder@~1.0.0: dependencies: buffer-shims "~1.0.0" +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + stringstream@~0.0.4: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -2868,6 +3319,12 @@ sugarss@^0.2.0: dependencies: postcss "^5.2.4" +supports-color@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" + dependencies: + has-flag "^1.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -2946,6 +3403,10 @@ through2@^0.6.1, through2@^0.6.3, through2@~0.6.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" +thunkify@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d" + 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" @@ -2984,6 +3445,10 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -2996,6 +3461,10 @@ uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + user-home@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" @@ -3039,6 +3508,10 @@ window-size@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -3064,6 +3537,10 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" +xregexp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" + "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From e7553325d70894a10429399b47a47ed185e26f2a Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Fri, 8 Dec 2017 13:06:29 -0800 Subject: [PATCH 002/232] nightwatch updates --- core/js_tests/index.js | 13 -- core/nightwatch.json | 49 +++--- core/package.json | 7 +- core/tests/Drupal/Nightwatch/index.js | 9 ++ core/tests/nightwatch_bootstrap.js | 11 ++ core/yarn.lock | 224 +++++++++++++++++++++++++- 6 files changed, 258 insertions(+), 55 deletions(-) delete mode 100644 core/js_tests/index.js create mode 100644 core/tests/Drupal/Nightwatch/index.js create mode 100644 core/tests/nightwatch_bootstrap.js diff --git a/core/js_tests/index.js b/core/js_tests/index.js deleted file mode 100644 index 4481c72acd8c..000000000000 --- a/core/js_tests/index.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - 'Demo test Google' : function (browser) { - browser - .url('http://www.google.com') - .waitForElementVisible('body', 1000) - .setValue('input[type=text]', 'nightwatch') - .waitForElementVisible('button[name=btnG]', 1000) - .click('button[name=btnG]') - .pause(1000) - .assert.containsText('#main', 'Night Watch') - .end(); - } -}; diff --git a/core/nightwatch.json b/core/nightwatch.json index 5de1bddc351a..439f5b5ae7fa 100644 --- a/core/nightwatch.json +++ b/core/nightwatch.json @@ -1,37 +1,24 @@ { - "src_folders" : ["js_tests"], - "output_folder" : "reports", - "custom_commands_path" : "", - "custom_assertions_path" : "", - "page_objects_path" : "", - "globals_path" : "", - "selenium" : { - "start_process" : false, - "server_path" : "", - "log_path" : "", - "port" : 4444, - "cli_args" : { - "webdriver.chrome.driver" : "" - } + "src_folders": ["tests/Drupal/Nightwatch"], + "output_folder": "reports", + "custom_commands_path": "", + "custom_assertions_path": "", + "page_objects_path": "", + "globals_path": "tests/nightwatch_bootstrap.js", + "selenium": { + "start_process": false }, - "test_settings" : { - "default" : { - "launch_url" : "http://localhost", - "selenium_port" : 4444, - "selenium_host" : "localhost", - "silent": true, - "screenshots" : { - "enabled" : false, - "path" : "" - }, - "desiredCapabilities": { - "browserName": "firefox", - "marionette": true - } - }, - "chrome" : { + "test_settings": { + "default": { + "selenium_port": 9515, + "selenium_host": "localhost", + "default_path_prefix": "", "desiredCapabilities": { - "browserName": "chrome" + "browserName": "chrome", + "chromeOptions": { + "args": ["--no-sandbox"] + }, + "acceptSslCerts": true } } } diff --git a/core/package.json b/core/package.json index f894d177e311..2e13a539fa48 100644 --- a/core/package.json +++ b/core/package.json @@ -13,6 +13,7 @@ "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", "lint:css": "stylelint \"**/*.css\" || exit 0", "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", + "test:js:ci": "./node_modules/.bin/nightwatch -o $NIGHTWATCH_OUTPUT -r junit", "test:js": "./node_modules/.bin/nightwatch" }, "devDependencies": { @@ -21,6 +22,7 @@ "babel-preset-env": "1.4.0", "chalk": "^1.1.3", "chokidar": "1.6.1", + "chromedriver": "^2.33.2", "cross-env": "^4.0.0", "eslint": "3.19.0", "eslint-config-airbnb": "14.1.0", @@ -29,6 +31,7 @@ "eslint-plugin-react": "6.10.3", "glob": "7.1.1", "minimist": "^1.2.0", + "nightwatch": "^0.9.19", "stylelint": "^7.10.1", "stylelint-checkstyle-formatter": "^0.1.0", "stylelint-config-standard": "^16.0.0", @@ -54,7 +57,5 @@ ] ] }, - "dependencies": { - "nightwatch": "^0.9.19" - } + "dependencies": {} } diff --git a/core/tests/Drupal/Nightwatch/index.js b/core/tests/Drupal/Nightwatch/index.js new file mode 100644 index 000000000000..b94607280c3b --- /dev/null +++ b/core/tests/Drupal/Nightwatch/index.js @@ -0,0 +1,9 @@ +module.exports = { + 'Demo Drupal.org' : function (browser) { + browser + .url('https://www.drupal.org/') + .waitForElementVisible('body', 1000) + .assert.containsText('body', 'Launch, manage, and scale ambitious digital experiences—with the flexibility to build great websites or push beyond the browser. Proudly open source.') + .end(); + } +}; diff --git a/core/tests/nightwatch_bootstrap.js b/core/tests/nightwatch_bootstrap.js new file mode 100644 index 000000000000..dcaa13bba6ad --- /dev/null +++ b/core/tests/nightwatch_bootstrap.js @@ -0,0 +1,11 @@ +const chromedriver = require('chromedriver'); +module.exports = { + before: function(done) { + chromedriver.start(); + done(); + }, + after: function(done) { + chromedriver.stop(); + done(); + } +}; diff --git a/core/yarn.lock b/core/yarn.lock index 860daca44171..af9c48c2eb0c 100644 --- a/core/yarn.lock +++ b/core/yarn.lock @@ -45,6 +45,15 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.1.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2" + 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" @@ -181,7 +190,11 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" -aws4@^1.2.1: +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -672,6 +685,18 @@ boom@2.x.x: dependencies: hoek "2.x.x" +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.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" @@ -771,6 +796,16 @@ chokidar@1.6.1: optionalDependencies: fsevents "^1.0.0" +chromedriver@^2.33.2: + version "2.33.2" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-2.33.2.tgz#8fc779d54b6e45bef55d264a1eceed52427a9b49" + dependencies: + del "^3.0.0" + extract-zip "^1.6.5" + kew "^0.7.0" + mkdirp "^0.5.1" + request "^2.83.0" + circular-json@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" @@ -847,7 +882,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.5.2: +concat-stream@1.6.0, concat-stream@^1.5.2: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -908,6 +943,12 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + css-color-names@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.3.tgz#de0cef16f4d8aa8222a320d5b6d7e9bbada7b9f6" @@ -954,7 +995,7 @@ data-uri-to-buffer@1: version "1.2.0" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" -debug@2: +debug@2, debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -1017,6 +1058,17 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1363,7 +1415,7 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -extend@3, extend@~3.0.0: +extend@3, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -1373,14 +1425,37 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" +extract-zip@^1.6.5: + version "1.6.6" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" + dependencies: + concat-stream "1.6.0" + debug "2.6.9" + mkdirp "0.5.0" + yauzl "2.4.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" +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -1459,6 +1534,14 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1600,7 +1683,7 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^6.0.0: +globby@^6.0.0, globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" dependencies: @@ -1630,6 +1713,10 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -1637,6 +1724,13 @@ har-validator@~4.2.1: ajv "^4.9.1" har-schema "^1.0.5" +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -1666,10 +1760,23 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -1710,6 +1817,14 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + https-proxy-agent@1: version "1.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" @@ -2005,6 +2120,10 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +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" @@ -2061,6 +2180,10 @@ 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" +kew@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + kind-of@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.0.tgz#b58abe4d5c044ad33726a8c1525b48cf891bff07" @@ -2301,12 +2424,22 @@ mime-db@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + mime-types@^2.1.12, mime-types@~2.1.7: version "2.1.15" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" dependencies: mime-db "~1.27.0" +mime-types@~2.1.17: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" @@ -2325,6 +2458,12 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" +mkdirp@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" + dependencies: + minimist "0.0.8" + mkdirp@0.5.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" @@ -2464,7 +2603,7 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -oauth-sign@~0.8.1: +oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -2544,6 +2683,10 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + pac-proxy-agent@1: version "1.1.0" resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-1.1.0.tgz#34a385dfdf61d2f0ecace08858c745d3e791fd4d" @@ -2609,14 +2752,26 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + pify@^2.0.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" @@ -2764,6 +2919,10 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + randomatic@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" @@ -2968,6 +3127,33 @@ request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +request@^2.83.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + 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" @@ -3020,7 +3206,7 @@ safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -3084,6 +3270,12 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + socks-proxy-agent@2: version "2.1.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-2.1.1.tgz#86ebb07193258637870e13b7bd99f26c663df3d3" @@ -3204,7 +3396,7 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" -stringstream@~0.0.4: +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -3417,6 +3609,12 @@ tough-cookie@~2.3.0: dependencies: punycode "^1.4.1" +tough-cookie@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + dependencies: + punycode "^1.4.1" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -3479,6 +3677,10 @@ uuid@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" +uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" @@ -3568,3 +3770,9 @@ yargs@^3.5.4: string-width "^1.0.1" window-size "^0.1.4" y18n "^3.2.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + dependencies: + fd-slicer "~1.0.1" From 188c81e150f4390e9dcfbaaa912f43ecc1019481 Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Fri, 8 Dec 2017 13:39:22 -0800 Subject: [PATCH 003/232] do not start chromedriver on ci env --- core/nightwatch.conf.js | 33 ++++++++++++++++++++++++++++++ core/nightwatch.json | 25 ---------------------- core/package.json | 4 ++-- core/tests/nightwatch_bootstrap.js | 32 +++++++++++++++++++++-------- 4 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 core/nightwatch.conf.js delete mode 100644 core/nightwatch.json diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js new file mode 100644 index 000000000000..04cdf4ff5efa --- /dev/null +++ b/core/nightwatch.conf.js @@ -0,0 +1,33 @@ +const testingMode = process.env.TESTING_MODE || 'local'; + +const options = { + src_folders: ['tests/Drupal/Nightwatch'], + output_folder: false, + custom_commands_path: '', + custom_assertions_path: '', + page_objects_path: '', + globals_path: 'tests/nightwatch_bootstrap.js', + selenium: { + start_process: false, + }, + test_settings: { + default: { + selenium_port: 9515, + selenium_host: 'localhost', + default_path_prefix: '', + desiredCapabilities: { + browserName: 'chrome', + chromeOptions: { + args: ['--no-sandbox'], + }, + acceptSslCerts: true, + }, + }, + }, +}; + +if (testingMode === 'ci') { + options.output_folder = process.env.NIGHTWATCH_OUTPUT; +} + +module.exports = options; diff --git a/core/nightwatch.json b/core/nightwatch.json deleted file mode 100644 index 439f5b5ae7fa..000000000000 --- a/core/nightwatch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "src_folders": ["tests/Drupal/Nightwatch"], - "output_folder": "reports", - "custom_commands_path": "", - "custom_assertions_path": "", - "page_objects_path": "", - "globals_path": "tests/nightwatch_bootstrap.js", - "selenium": { - "start_process": false - }, - "test_settings": { - "default": { - "selenium_port": 9515, - "selenium_host": "localhost", - "default_path_prefix": "", - "desiredCapabilities": { - "browserName": "chrome", - "chromeOptions": { - "args": ["--no-sandbox"] - }, - "acceptSslCerts": true - } - } - } -} diff --git a/core/package.json b/core/package.json index 2e13a539fa48..76707e7bc454 100644 --- a/core/package.json +++ b/core/package.json @@ -13,8 +13,8 @@ "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", "lint:css": "stylelint \"**/*.css\" || exit 0", "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", - "test:js:ci": "./node_modules/.bin/nightwatch -o $NIGHTWATCH_OUTPUT -r junit", - "test:js": "./node_modules/.bin/nightwatch" + "test:js:ci": "TESTING_MODE=ci ./node_modules/.bin/nightwatch -r junit", + "test:js": "TESTING_MODE=local ./node_modules/.bin/nightwatch" }, "devDependencies": { "babel-core": "6.24.1", diff --git a/core/tests/nightwatch_bootstrap.js b/core/tests/nightwatch_bootstrap.js index dcaa13bba6ad..cdedae862d6c 100644 --- a/core/tests/nightwatch_bootstrap.js +++ b/core/tests/nightwatch_bootstrap.js @@ -1,11 +1,25 @@ const chromedriver = require('chromedriver'); -module.exports = { - before: function(done) { - chromedriver.start(); - done(); - }, - after: function(done) { - chromedriver.stop(); - done(); +const testingMode = process.env.TESTING_MODE || 'local'; + +if (testingMode === 'local') { + module.exports = { + before: function(done) { + chromedriver.start(); + done(); + }, + after: function(done) { + chromedriver.stop(); + done(); + } + }; +} +else { + module.exports = { + before: function (done) { + done(); + }, + after: function (done) { + done(); + } } -}; +} From 65c809ca4ea672ed8af968943bb68661355aacd6 Mon Sep 17 00:00:00 2001 From: Sally Young Date: Fri, 8 Dec 2017 22:16:57 +0000 Subject: [PATCH 004/232] Shift files around and have a separate environment for the testbot --- core/.gitignore | 3 + core/nightwatch.conf.js | 48 ++++++ core/nightwatch.json | 25 --- core/package.json | 11 +- .../Drupal/Nightwatch/{ => Tests}/index.js | 0 .../Nightwatch/globals.js} | 0 core/tests/README.md | 16 ++ core/yarn.lock | 158 +++++++++++++++++- 8 files changed, 231 insertions(+), 30 deletions(-) create mode 100644 core/nightwatch.conf.js delete mode 100644 core/nightwatch.json rename core/tests/Drupal/Nightwatch/{ => Tests}/index.js (100%) rename core/tests/{nightwatch_bootstrap.js => Drupal/Nightwatch/globals.js} (100%) diff --git a/core/.gitignore b/core/.gitignore index be42d48852f0..db29d420b98a 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -9,3 +9,6 @@ phpunit.xml # Ignore package-lock.json that is automatically created when adding # dependencies by users of NPMv5. package-lock.json + +# Ignore test reports +reports diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js new file mode 100644 index 000000000000..91d0193f0f4b --- /dev/null +++ b/core/nightwatch.conf.js @@ -0,0 +1,48 @@ +const chromeArgs = ['--disable-notifications']; +if (!process.env.HEADLESS_CHROME_DISABLED) { + chromeArgs.push('--headless'); +} + +const outputFolder = process.env.NIGHTWATCH_OUTPUT ? process.env.NIGHTWATCH_OUTPUT : 'reports/nightwatch'; + +module.exports = { + 'src_folders': ['tests/Drupal/Nightwatch/Tests'], + 'output_folder': outputFolder, + 'custom_commands_path': '', + 'custom_assertions_path': '', + 'page_objects_path': '', + 'globals_path': 'tests/Drupal/Nightwatch/globals.js', + 'selenium': { + 'start_process': false, + }, + 'test_settings': { + 'default': { + 'selenium_port': 9515, + 'selenium_host': 'localhost', + 'default_path_prefix': '', + 'desiredCapabilities': { + 'browserName': 'chrome', + 'acceptSslCerts': true, + 'args': [ + '--headless', + '--disable-notifications', + ] + }, + 'screenshots' : { + 'enabled' : true, + 'on_failure' : true, + 'on_error' : true, + 'path' : 'core/reports/nightwatch/screenshots' + }, + }, + 'testbot': { + 'desiredCapabilities': { + 'browserName': 'chrome', + 'chromeOptions': { + 'args': [ ...chromeArgs, '--no-sandbox'], + }, + 'acceptSslCerts': true + } + } + } +}; diff --git a/core/nightwatch.json b/core/nightwatch.json deleted file mode 100644 index 439f5b5ae7fa..000000000000 --- a/core/nightwatch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "src_folders": ["tests/Drupal/Nightwatch"], - "output_folder": "reports", - "custom_commands_path": "", - "custom_assertions_path": "", - "page_objects_path": "", - "globals_path": "tests/nightwatch_bootstrap.js", - "selenium": { - "start_process": false - }, - "test_settings": { - "default": { - "selenium_port": 9515, - "selenium_host": "localhost", - "default_path_prefix": "", - "desiredCapabilities": { - "browserName": "chrome", - "chromeOptions": { - "args": ["--no-sandbox"] - }, - "acceptSslCerts": true - } - } - } -} diff --git a/core/package.json b/core/package.json index 2e13a539fa48..e9f51781ea67 100644 --- a/core/package.json +++ b/core/package.json @@ -3,6 +3,10 @@ "description": "Drupal is an open source content management platform powering millions of websites and applications.", "license": "GPL-2.0", "private": true, + "engines": { + "yarn": "1.x", + "node": "8.x" + }, "scripts": { "build:js": "node ./scripts/js/babel-es6-build.js", "build:js-dev": "cross-env NODE_ENV=development node ./scripts/js/babel-es6-build.js", @@ -13,13 +17,16 @@ "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", "lint:css": "stylelint \"**/*.css\" || exit 0", "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", - "test:js:ci": "./node_modules/.bin/nightwatch -o $NIGHTWATCH_OUTPUT -r junit", - "test:js": "./node_modules/.bin/nightwatch" + "nightwatch": "node $NODE_DEBUG_OPTION -r babel-register ./node_modules/.bin/nightwatch", + "test": "if [ \"$NODE_ENV\" = \"testbot\" ] ; then yarn test:testbot; else yarn test:default; fi", + "test:default": "yarn nightwatch", + "test:testbot": "yarn nightwatch --env testbot" }, "devDependencies": { "babel-core": "6.24.1", "babel-plugin-add-header-comment": "1.0.3", "babel-preset-env": "1.4.0", + "babel-register": "^6.26.0", "chalk": "^1.1.3", "chokidar": "1.6.1", "chromedriver": "^2.33.2", diff --git a/core/tests/Drupal/Nightwatch/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js similarity index 100% rename from core/tests/Drupal/Nightwatch/index.js rename to core/tests/Drupal/Nightwatch/Tests/index.js diff --git a/core/tests/nightwatch_bootstrap.js b/core/tests/Drupal/Nightwatch/globals.js similarity index 100% rename from core/tests/nightwatch_bootstrap.js rename to core/tests/Drupal/Nightwatch/globals.js diff --git a/core/tests/README.md b/core/tests/README.md index 5dd6cc63944e..5a601f191e98 100644 --- a/core/tests/README.md +++ b/core/tests/README.md @@ -46,3 +46,19 @@ export SIMPLETEST_BASE_URL='http://d8.dev' sudo -u www-data -E ./vendor/bin/phpunit -c core --testsuite functional sudo -u www-data -E ./vendor/bin/phpunit -c core --testsuite functional-javascript ``` + +## Nightwatch tests + +- Install [Node.js](https://nodejs.org/en/download/) and [yarn](https://yarnpkg.com/en/docs/install). The versions required are specificed inside core/package.json in the `engines` field +- Install [Google Chrome](https://www.google.com/chrome/browser/desktop/index.html) +- Inside the `core` folder, run `yarn install` +- Again inside the `core` folder, run `yarn nightwatch` to run the tests. By default this will output reports to `core/reports` + +Some settings can be overridden with environment variables: + +| Variable | Default Value | Description | +|------------|---------------|-------------| +| `HEADLESS_CHROME_DISABLED` | `false` | If set to `true`, this will remove the `--headless` option passed to Chrome, allowing you to see tests running in realtime in the browser | +| `NIGHTWATCH_OUTPUT` | `reports/nightwatch` | This will output the test results into `core/reports/nightwatch`. Passing a value here is relative to the `core` directory | +| `NODE_ENV` | none | Setting this to `testbot` will cause Nightwatch to run with the settings specified in the `testbot` environment in `core/nightwatch.conf.js` | + diff --git a/core/yarn.lock b/core/yarn.lock index af9c48c2eb0c..4830783c259e 100644 --- a/core/yarn.lock +++ b/core/yarn.lock @@ -206,6 +206,14 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" +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: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + 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" @@ -230,6 +238,30 @@ babel-core@6.24.1, babel-core@^6.24.1: slash "^1.0.0" source-map "^0.5.0" +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.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.6" + babel-generator@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" @@ -243,6 +275,19 @@ babel-generator@^6.24.1: source-map "^0.5.0" trim-right "^1.0.1" +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.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.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: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" @@ -615,6 +660,18 @@ babel-register@^6.24.1: mkdirp "^0.5.1" source-map-support "^0.4.2" +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.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + babel-runtime@^6.18.0, babel-runtime@^6.22.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" @@ -622,6 +679,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" @@ -632,6 +696,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" @@ -646,6 +720,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" @@ -655,14 +743,31 @@ 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" +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + balanced-match@^0.4.0, balanced-match@^0.4.1: 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" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -704,6 +809,13 @@ brace-expansion@^1.0.0: balanced-match "^0.4.1" concat-map "0.0.1" +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 "^1.0.0" + concat-map "0.0.1" + braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" @@ -902,10 +1014,18 @@ 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" + 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.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" + 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" @@ -995,7 +1115,7 @@ data-uri-to-buffer@1: version "1.2.0" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" -debug@2, debug@2.6.9: +debug@2, debug@2.6.9, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -1672,6 +1792,10 @@ 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" @@ -2101,6 +2225,10 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" +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.5.1: version "3.8.3" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" @@ -2142,7 +2270,7 @@ json3@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" -json5@^0.5.0: +json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -2446,6 +2574,12 @@ minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: dependencies: brace-expansion "^1.0.0" +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" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -2732,7 +2866,7 @@ path-exists@^2.0.0: dependencies: pinkie-promise "^2.0.0" -path-is-absolute@^1.0.0: +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" @@ -2882,6 +3016,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" @@ -3049,6 +3187,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" @@ -3291,6 +3433,12 @@ socks@~1.1.5: ip "^1.1.4" smart-buffer "^1.0.13" +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-support@^0.4.2: version "0.4.15" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" @@ -3603,6 +3751,10 @@ 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" + tough-cookie@~2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" From 86d511fe8b24aea1f90f27be65b6d871fa89292e Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Fri, 8 Dec 2017 14:40:22 -0800 Subject: [PATCH 005/232] linting cleanup --- core/.eslintignore | 2 + core/nightwatch.conf.js | 64 ++++++++++----------- core/tests/Drupal/Nightwatch/Tests/index.js | 4 +- core/tests/Drupal/Nightwatch/globals.js | 15 ++--- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/core/.eslintignore b/core/.eslintignore index fae394892bb7..874d687495a3 100644 --- a/core/.eslintignore +++ b/core/.eslintignore @@ -4,3 +4,5 @@ node_modules/**/* *.js !*.es6.js modules/locale/tests/locale_test.es6.js +!nightwatch.conf.js +!tests/Drupal/Nightwatch/**/*.js diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index 91d0193f0f4b..c659e63e6baf 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -6,43 +6,43 @@ if (!process.env.HEADLESS_CHROME_DISABLED) { const outputFolder = process.env.NIGHTWATCH_OUTPUT ? process.env.NIGHTWATCH_OUTPUT : 'reports/nightwatch'; module.exports = { - 'src_folders': ['tests/Drupal/Nightwatch/Tests'], - 'output_folder': outputFolder, - 'custom_commands_path': '', - 'custom_assertions_path': '', - 'page_objects_path': '', - 'globals_path': 'tests/Drupal/Nightwatch/globals.js', - 'selenium': { - 'start_process': false, + src_folders: ['tests/Drupal/Nightwatch/Tests'], + output_folder: outputFolder, + custom_commands_path: '', + custom_assertions_path: '', + page_objects_path: '', + globals_path: 'tests/Drupal/Nightwatch/globals.js', + selenium: { + start_process: false, }, - 'test_settings': { - 'default': { - 'selenium_port': 9515, - 'selenium_host': 'localhost', - 'default_path_prefix': '', - 'desiredCapabilities': { - 'browserName': 'chrome', - 'acceptSslCerts': true, - 'args': [ + test_settings: { + defaul: { + selenium_port: 9515, + selenium_host: 'localhost', + default_path_prefix: '', + desiredCapabilities: { + browserName: 'chrome', + acceptSslCerts: true, + arg: [ '--headless', '--disable-notifications', - ] + ], }, - 'screenshots' : { - 'enabled' : true, - 'on_failure' : true, - 'on_error' : true, - 'path' : 'core/reports/nightwatch/screenshots' + screenshots: { + enabled: true, + on_failure: true, + on_error: true, + path: 'core/reports/nightwatch/screenshots', }, }, - 'testbot': { - 'desiredCapabilities': { - 'browserName': 'chrome', - 'chromeOptions': { - 'args': [ ...chromeArgs, '--no-sandbox'], + testbot: { + desiredCapabilities: { + browserName: 'chrome', + chromeOptions: { + args: [...chromeArgs, '--no-sandbox'], }, - 'acceptSslCerts': true - } - } - } + acceptSslCerts: true, + }, + }, + }, }; diff --git a/core/tests/Drupal/Nightwatch/Tests/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js index b94607280c3b..4c9aa83b9538 100644 --- a/core/tests/Drupal/Nightwatch/Tests/index.js +++ b/core/tests/Drupal/Nightwatch/Tests/index.js @@ -1,9 +1,9 @@ module.exports = { - 'Demo Drupal.org' : function (browser) { + 'Demo Drupal.org': (browser) => { browser .url('https://www.drupal.org/') .waitForElementVisible('body', 1000) .assert.containsText('body', 'Launch, manage, and scale ambitious digital experiences—with the flexibility to build great websites or push beyond the browser. Proudly open source.') .end(); - } + }, }; diff --git a/core/tests/Drupal/Nightwatch/globals.js b/core/tests/Drupal/Nightwatch/globals.js index cdedae862d6c..0385469e1b38 100644 --- a/core/tests/Drupal/Nightwatch/globals.js +++ b/core/tests/Drupal/Nightwatch/globals.js @@ -1,25 +1,26 @@ const chromedriver = require('chromedriver'); + const testingMode = process.env.TESTING_MODE || 'local'; if (testingMode === 'local') { module.exports = { - before: function(done) { + before: (done) => { chromedriver.start(); done(); }, - after: function(done) { + after: (done) => { chromedriver.stop(); done(); - } + }, }; } else { module.exports = { - before: function (done) { + before: (done) => { done(); }, - after: function (done) { + after: (done) => { done(); - } - } + }, + }; } From ff614af4b9086bb724d1d3ad5200622cbb35140d Mon Sep 17 00:00:00 2001 From: Sally Young Date: Fri, 8 Dec 2017 22:53:54 +0000 Subject: [PATCH 006/232] Doesn't need a separate ci command as nightwatch is already running in a container --- core/nightwatch.conf.js | 13 ++------- core/package.json | 7 ++--- core/tests/Drupal/Nightwatch/globals.js | 36 +++++++++---------------- core/tests/README.md | 2 +- 4 files changed, 18 insertions(+), 40 deletions(-) diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index c659e63e6baf..5234f6a76e2e 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -16,7 +16,7 @@ module.exports = { start_process: false, }, test_settings: { - defaul: { + default: { selenium_port: 9515, selenium_host: 'localhost', default_path_prefix: '', @@ -32,16 +32,7 @@ module.exports = { enabled: true, on_failure: true, on_error: true, - path: 'core/reports/nightwatch/screenshots', - }, - }, - testbot: { - desiredCapabilities: { - browserName: 'chrome', - chromeOptions: { - args: [...chromeArgs, '--no-sandbox'], - }, - acceptSslCerts: true, + path: `${outputFolder}/core/reports/nightwatch/screenshots`, }, }, }, diff --git a/core/package.json b/core/package.json index e9f51781ea67..2515d61853ff 100644 --- a/core/package.json +++ b/core/package.json @@ -18,9 +18,7 @@ "lint:css": "stylelint \"**/*.css\" || exit 0", "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", "nightwatch": "node $NODE_DEBUG_OPTION -r babel-register ./node_modules/.bin/nightwatch", - "test": "if [ \"$NODE_ENV\" = \"testbot\" ] ; then yarn test:testbot; else yarn test:default; fi", - "test:default": "yarn nightwatch", - "test:testbot": "yarn nightwatch --env testbot" + "test": "yarn nightwatch" }, "devDependencies": { "babel-core": "6.24.1", @@ -63,6 +61,5 @@ } ] ] - }, - "dependencies": {} + } } diff --git a/core/tests/Drupal/Nightwatch/globals.js b/core/tests/Drupal/Nightwatch/globals.js index 0385469e1b38..e69b8d184a9c 100644 --- a/core/tests/Drupal/Nightwatch/globals.js +++ b/core/tests/Drupal/Nightwatch/globals.js @@ -1,26 +1,16 @@ -const chromedriver = require('chromedriver'); +import 'chromedriver' from chromedriver; -const testingMode = process.env.TESTING_MODE || 'local'; - -if (testingMode === 'local') { - module.exports = { - before: (done) => { +module.exports = { + before: (done) => { + if (process.env.NODE_ENV !== 'testbot') { chromedriver.start(); - done(); - }, - after: (done) => { + } + done(); + }, + after: (done) => { + if (process.env.NODE_ENV !== 'testbot') { chromedriver.stop(); - done(); - }, - }; -} -else { - module.exports = { - before: (done) => { - done(); - }, - after: (done) => { - done(); - }, - }; -} + } + done(); + }, +}; diff --git a/core/tests/README.md b/core/tests/README.md index 5a601f191e98..7723a6680d7a 100644 --- a/core/tests/README.md +++ b/core/tests/README.md @@ -60,5 +60,5 @@ Some settings can be overridden with environment variables: |------------|---------------|-------------| | `HEADLESS_CHROME_DISABLED` | `false` | If set to `true`, this will remove the `--headless` option passed to Chrome, allowing you to see tests running in realtime in the browser | | `NIGHTWATCH_OUTPUT` | `reports/nightwatch` | This will output the test results into `core/reports/nightwatch`. Passing a value here is relative to the `core` directory | -| `NODE_ENV` | none | Setting this to `testbot` will cause Nightwatch to run with the settings specified in the `testbot` environment in `core/nightwatch.conf.js` | +| `NODE_ENV` | none | Setting this to `testbot` will cause Nightwatch to run with settings specific to the Drupal.org testbot | From a54317ee64f883744bf37d2658d5139eb0f89236 Mon Sep 17 00:00:00 2001 From: Sally Young Date: Fri, 8 Dec 2017 22:55:32 +0000 Subject: [PATCH 007/232] fix path --- core/nightwatch.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index 5234f6a76e2e..dbc799ade19c 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -32,7 +32,7 @@ module.exports = { enabled: true, on_failure: true, on_error: true, - path: `${outputFolder}/core/reports/nightwatch/screenshots`, + path: `${outputFolder}/screenshots`, }, }, }, From df054bf83e12b55a8c05ad33f13cc56dfc5d0b52 Mon Sep 17 00:00:00 2001 From: Sally Young Date: Fri, 8 Dec 2017 23:09:20 +0000 Subject: [PATCH 008/232] fight me --- core/package.json | 2 +- core/tests/Drupal/Nightwatch/globals.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/package.json b/core/package.json index 2515d61853ff..277e4d568de8 100644 --- a/core/package.json +++ b/core/package.json @@ -47,7 +47,7 @@ [ "env", { - "modules": false, + "modules": "commonjs", "targets": { "browsers": [ "ie >= 9", diff --git a/core/tests/Drupal/Nightwatch/globals.js b/core/tests/Drupal/Nightwatch/globals.js index e69b8d184a9c..6880cc31248b 100644 --- a/core/tests/Drupal/Nightwatch/globals.js +++ b/core/tests/Drupal/Nightwatch/globals.js @@ -1,4 +1,4 @@ -import 'chromedriver' from chromedriver; +import chromedriver from 'chromedriver'; module.exports = { before: (done) => { From c5d30c2abe236b7cc964b9fcc01487c00820d3ab Mon Sep 17 00:00:00 2001 From: Sally Young Date: Fri, 8 Dec 2017 23:10:49 +0000 Subject: [PATCH 009/232] srsly --- core/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/package.json b/core/package.json index 277e4d568de8..cd96731a933c 100644 --- a/core/package.json +++ b/core/package.json @@ -56,7 +56,8 @@ "opera >= 12", "safari >= 5", "chrome >= 56" - ] + ], + "node": "current" } } ] From 9b587cab805af5f7830593b05ae1e16778691bf7 Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Fri, 8 Dec 2017 23:28:27 +0000 Subject: [PATCH 010/232] add a way to configure the hostname --- core/nightwatch.conf.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index dbc799ade19c..b66447aa34a9 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -4,6 +4,7 @@ if (!process.env.HEADLESS_CHROME_DISABLED) { } const outputFolder = process.env.NIGHTWATCH_OUTPUT ? process.env.NIGHTWATCH_OUTPUT : 'reports/nightwatch'; +const hostname = process.env.NIGHTWATCH_HOSTNAME ? process.env.NIGHTWATCH_HOSTNAME : 'localhost'; module.exports = { src_folders: ['tests/Drupal/Nightwatch/Tests'], @@ -18,7 +19,7 @@ module.exports = { test_settings: { default: { selenium_port: 9515, - selenium_host: 'localhost', + selenium_host: hostname, default_path_prefix: '', desiredCapabilities: { browserName: 'chrome', From 8b6b087eb2464958a1c63f4c7ce8eeb4ee007483 Mon Sep 17 00:00:00 2001 From: Sally Young Date: Fri, 8 Dec 2017 23:32:09 +0000 Subject: [PATCH 011/232] add babel environments --- core/package.json | 63 ++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/core/package.json b/core/package.json index cd96731a933c..dac158b20a9e 100644 --- a/core/package.json +++ b/core/package.json @@ -8,10 +8,10 @@ "node": "8.x" }, "scripts": { - "build:js": "node ./scripts/js/babel-es6-build.js", - "build:js-dev": "cross-env NODE_ENV=development node ./scripts/js/babel-es6-build.js", - "watch:js": "node ./scripts/js/babel-es6-watch.js", - "watch:js-dev": "cross-env NODE_ENV=development node ./scripts/js/babel-es6-watch.js", + "build:js": "BABEL_ENV=legacy node ./scripts/js/babel-es6-build.js", + "build:js-dev": "cross-env NODE_ENV=development node BABEL_ENV=legacy ./scripts/js/babel-es6-build.js", + "watch:js": "BABEL_ENV=legacy node ./scripts/js/babel-es6-watch.js", + "watch:js-dev": "cross-env NODE_ENV=development BABEL_ENV=legacy node ./scripts/js/babel-es6-watch.js", "lint:core-js": "node ./node_modules/eslint/bin/eslint.js --ext=.es6.js . || exit 0", "lint:core-js-passing": "node ./node_modules/eslint/bin/eslint.js --quiet --config=.eslintrc.passing.json --ext=.es6.js . || exit 0", "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", @@ -42,25 +42,42 @@ "stylelint-config-standard": "^16.0.0", "stylelint-no-browser-hacks": "^1.0.2" }, + "//": "'development is the default environment, and legacy is for transpiling the old jQuery codebase", "babel": { - "presets": [ - [ - "env", - { - "modules": "commonjs", - "targets": { - "browsers": [ - "ie >= 9", - "edge >= 13", - "firefox >= 5", - "opera >= 12", - "safari >= 5", - "chrome >= 56" - ], - "node": "current" - } - } - ] - ] + "env": { + "development": { + "presets": [ + [ + "env", + { + "modules": "commonjs", + "targets": { + "node": "current" + } + } + ] + ] + }, + "legacy": { + "presets": [ + [ + "env", + { + "modules": "commonjs", + "targets": { + "browsers": [ + "ie >= 9", + "edge >= 13", + "firefox >= 5", + "opera >= 12", + "safari >= 5", + "chrome >= 56" + ] + } + } + ] + ] + } + } } } From 53824a431e05c316ba52cdf4ea8b2f806e4752f9 Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Fri, 8 Dec 2017 15:40:01 -0800 Subject: [PATCH 012/232] do not adjust old codebase with commonjs modules settings --- core/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/package.json b/core/package.json index dac158b20a9e..b088b716c4ec 100644 --- a/core/package.json +++ b/core/package.json @@ -8,9 +8,9 @@ "node": "8.x" }, "scripts": { - "build:js": "BABEL_ENV=legacy node ./scripts/js/babel-es6-build.js", + "build:js": "cross-env BABEL_ENV=legacy node ./scripts/js/babel-es6-build.js", "build:js-dev": "cross-env NODE_ENV=development node BABEL_ENV=legacy ./scripts/js/babel-es6-build.js", - "watch:js": "BABEL_ENV=legacy node ./scripts/js/babel-es6-watch.js", + "watch:js": "cross-env BABEL_ENV=legacy node ./scripts/js/babel-es6-watch.js", "watch:js-dev": "cross-env NODE_ENV=development BABEL_ENV=legacy node ./scripts/js/babel-es6-watch.js", "lint:core-js": "node ./node_modules/eslint/bin/eslint.js --ext=.es6.js . || exit 0", "lint:core-js-passing": "node ./node_modules/eslint/bin/eslint.js --quiet --config=.eslintrc.passing.json --ext=.es6.js . || exit 0", @@ -63,7 +63,7 @@ [ "env", { - "modules": "commonjs", + "modules": false, "targets": { "browsers": [ "ie >= 9", From b7de174f62b723ce3dc6f39f27d3db2301ed3a26 Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Mon, 11 Dec 2017 07:01:02 -0800 Subject: [PATCH 013/232] temporarily remove babel-register --- core/package.json | 6 +- core/tests/Drupal/Nightwatch/globals.js | 2 +- core/yarn.lock | 158 +----------------------- 3 files changed, 6 insertions(+), 160 deletions(-) diff --git a/core/package.json b/core/package.json index b088b716c4ec..e4a41317061c 100644 --- a/core/package.json +++ b/core/package.json @@ -17,14 +17,12 @@ "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", "lint:css": "stylelint \"**/*.css\" || exit 0", "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", - "nightwatch": "node $NODE_DEBUG_OPTION -r babel-register ./node_modules/.bin/nightwatch", - "test": "yarn nightwatch" + "test:js": "./node_modules/.bin/nightwatch" }, "devDependencies": { "babel-core": "6.24.1", "babel-plugin-add-header-comment": "1.0.3", "babel-preset-env": "1.4.0", - "babel-register": "^6.26.0", "chalk": "^1.1.3", "chokidar": "1.6.1", "chromedriver": "^2.33.2", @@ -50,7 +48,7 @@ [ "env", { - "modules": "commonjs", + "modules": "false", "targets": { "node": "current" } diff --git a/core/tests/Drupal/Nightwatch/globals.js b/core/tests/Drupal/Nightwatch/globals.js index 6880cc31248b..3786eeaa5c9e 100644 --- a/core/tests/Drupal/Nightwatch/globals.js +++ b/core/tests/Drupal/Nightwatch/globals.js @@ -1,4 +1,4 @@ -import chromedriver from 'chromedriver'; +const chromedriver = require('chromedriver'); module.exports = { before: (done) => { diff --git a/core/yarn.lock b/core/yarn.lock index 4830783c259e..af9c48c2eb0c 100644 --- a/core/yarn.lock +++ b/core/yarn.lock @@ -206,14 +206,6 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" -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: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - 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" @@ -238,30 +230,6 @@ babel-core@6.24.1, babel-core@^6.24.1: slash "^1.0.0" source-map "^0.5.0" -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.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.6" - babel-generator@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" @@ -275,19 +243,6 @@ babel-generator@^6.24.1: source-map "^0.5.0" trim-right "^1.0.1" -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.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.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: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" @@ -660,18 +615,6 @@ babel-register@^6.24.1: mkdirp "^0.5.1" source-map-support "^0.4.2" -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.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - babel-runtime@^6.18.0, babel-runtime@^6.22.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" @@ -679,13 +622,6 @@ 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" @@ -696,16 +632,6 @@ 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" @@ -720,20 +646,6 @@ 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" @@ -743,31 +655,14 @@ 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" -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - balanced-match@^0.4.0, balanced-match@^0.4.1: 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" - bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -809,13 +704,6 @@ brace-expansion@^1.0.0: balanced-match "^0.4.1" concat-map "0.0.1" -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 "^1.0.0" - concat-map "0.0.1" - braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" @@ -1014,18 +902,10 @@ 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" - 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.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" - 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" @@ -1115,7 +995,7 @@ data-uri-to-buffer@1: version "1.2.0" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" -debug@2, debug@2.6.9, debug@^2.6.8: +debug@2, debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -1792,10 +1672,6 @@ 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" @@ -2225,10 +2101,6 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -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.5.1: version "3.8.3" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" @@ -2270,7 +2142,7 @@ json3@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" -json5@^0.5.0, json5@^0.5.1: +json5@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -2574,12 +2446,6 @@ minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: dependencies: brace-expansion "^1.0.0" -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" - minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -2866,7 +2732,7 @@ path-exists@^2.0.0: dependencies: pinkie-promise "^2.0.0" -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: +path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -3016,10 +2882,6 @@ 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" @@ -3187,10 +3049,6 @@ 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" @@ -3433,12 +3291,6 @@ socks@~1.1.5: ip "^1.1.4" smart-buffer "^1.0.13" -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-support@^0.4.2: version "0.4.15" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" @@ -3751,10 +3603,6 @@ 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" - tough-cookie@~2.3.0: version "2.3.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" From 899414a5cafed0ec5bfad1f708b472439d3e35cc Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Mon, 11 Dec 2017 08:27:11 -0800 Subject: [PATCH 014/232] adjust args to actually pass correctly. --- core/nightwatch.conf.js | 16 +++++++--------- core/package.json | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index b66447aa34a9..4e699de7fcc0 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -1,8 +1,3 @@ -const chromeArgs = ['--disable-notifications']; -if (!process.env.HEADLESS_CHROME_DISABLED) { - chromeArgs.push('--headless'); -} - const outputFolder = process.env.NIGHTWATCH_OUTPUT ? process.env.NIGHTWATCH_OUTPUT : 'reports/nightwatch'; const hostname = process.env.NIGHTWATCH_HOSTNAME ? process.env.NIGHTWATCH_HOSTNAME : 'localhost'; @@ -24,10 +19,13 @@ module.exports = { desiredCapabilities: { browserName: 'chrome', acceptSslCerts: true, - arg: [ - '--headless', - '--disable-notifications', - ], + chromeOptions: { + args: [ + '--headless', + '--disable-gpu', + '--disable-notifications', + ], + }, }, screenshots: { enabled: true, diff --git a/core/package.json b/core/package.json index e4a41317061c..cf87761f7ce7 100644 --- a/core/package.json +++ b/core/package.json @@ -17,7 +17,7 @@ "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", "lint:css": "stylelint \"**/*.css\" || exit 0", "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", - "test:js": "./node_modules/.bin/nightwatch" + "test:js": "./node_modules/.bin/nightwatch " }, "devDependencies": { "babel-core": "6.24.1", From 05a1417229bb69a8d3e9b0b4eb95a922e11eec35 Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Mon, 11 Dec 2017 08:34:43 -0800 Subject: [PATCH 015/232] actually use chromeArgs array --- core/nightwatch.conf.js | 11 ++++++----- core/package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index 4e699de7fcc0..46de26f1fd93 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -1,3 +1,8 @@ +const chromeArgs = ['--disable-gpu']; +if (!process.env.HEADLESS_CHROME_DISABLED) { + chromeArgs.push('--headless'); +} + const outputFolder = process.env.NIGHTWATCH_OUTPUT ? process.env.NIGHTWATCH_OUTPUT : 'reports/nightwatch'; const hostname = process.env.NIGHTWATCH_HOSTNAME ? process.env.NIGHTWATCH_HOSTNAME : 'localhost'; @@ -20,11 +25,7 @@ module.exports = { browserName: 'chrome', acceptSslCerts: true, chromeOptions: { - args: [ - '--headless', - '--disable-gpu', - '--disable-notifications', - ], + args: chromeArgs, }, }, screenshots: { diff --git a/core/package.json b/core/package.json index cf87761f7ce7..e4a41317061c 100644 --- a/core/package.json +++ b/core/package.json @@ -17,7 +17,7 @@ "lint:core-js-stats": "node ./node_modules/eslint/bin/eslint.js --format=./scripts/js/eslint-stats-by-type.js --ext=.es6.js . || exit 0", "lint:css": "stylelint \"**/*.css\" || exit 0", "lint:css-checkstyle": "stylelint \"**/*.css\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0", - "test:js": "./node_modules/.bin/nightwatch " + "test:js": "./node_modules/.bin/nightwatch" }, "devDependencies": { "babel-core": "6.24.1", From 20877b539cfd0453d8839027f25bc6daacb3146d Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Mon, 11 Dec 2017 10:46:05 -0800 Subject: [PATCH 016/232] custom command for adding baseurl prefix --- core/nightwatch.conf.js | 2 +- core/tests/Drupal/Nightwatch/Commands/drupalURL.js | 8 ++++++++ core/tests/Drupal/Nightwatch/Tests/index.js | 4 ++-- core/tests/README.md | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 core/tests/Drupal/Nightwatch/Commands/drupalURL.js diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index 46de26f1fd93..081a9e013876 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -9,7 +9,7 @@ const hostname = process.env.NIGHTWATCH_HOSTNAME ? process.env.NIGHTWATCH_HOSTNA module.exports = { src_folders: ['tests/Drupal/Nightwatch/Tests'], output_folder: outputFolder, - custom_commands_path: '', + custom_commands_path: ['tests/Drupal/Nightwatch/Commands'], custom_assertions_path: '', page_objects_path: '', globals_path: 'tests/Drupal/Nightwatch/globals.js', diff --git a/core/tests/Drupal/Nightwatch/Commands/drupalURL.js b/core/tests/Drupal/Nightwatch/Commands/drupalURL.js new file mode 100644 index 000000000000..29e4b39b454c --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Commands/drupalURL.js @@ -0,0 +1,8 @@ +exports.command = function baseurl(relativeURL) { + if (!process.env.BASE_URL || process.env.BASE_URL === '') { + throw new Error('Missing a BASE_URL environment variable.'); + } + this + .url(`${process.env.BASE_URL}${relativeURL}`); + return this; +}; diff --git a/core/tests/Drupal/Nightwatch/Tests/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js index 4c9aa83b9538..32732d8398e7 100644 --- a/core/tests/Drupal/Nightwatch/Tests/index.js +++ b/core/tests/Drupal/Nightwatch/Tests/index.js @@ -1,9 +1,9 @@ module.exports = { 'Demo Drupal.org': (browser) => { browser - .url('https://www.drupal.org/') + .drupalURL('/community') .waitForElementVisible('body', 1000) - .assert.containsText('body', 'Launch, manage, and scale ambitious digital experiences—with the flexibility to build great websites or push beyond the browser. Proudly open source.') + .assert.containsText('body', 'Where is the Drupal Community?') .end(); }, }; diff --git a/core/tests/README.md b/core/tests/README.md index 7723a6680d7a..d2bb1c6c8b53 100644 --- a/core/tests/README.md +++ b/core/tests/README.md @@ -61,4 +61,4 @@ Some settings can be overridden with environment variables: | `HEADLESS_CHROME_DISABLED` | `false` | If set to `true`, this will remove the `--headless` option passed to Chrome, allowing you to see tests running in realtime in the browser | | `NIGHTWATCH_OUTPUT` | `reports/nightwatch` | This will output the test results into `core/reports/nightwatch`. Passing a value here is relative to the `core` directory | | `NODE_ENV` | none | Setting this to `testbot` will cause Nightwatch to run with settings specific to the Drupal.org testbot | - +| `BASE_URL` | none | Set this to the base URL for your Drupal install. Do not include the trailing slash. | From 802011db0dfcace831fe6236bd45616395604a6f Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Mon, 11 Dec 2017 11:58:21 -0800 Subject: [PATCH 017/232] use relativeURL over drupalURL --- .../Drupal/Nightwatch/Commands/drupalURL.js | 8 -------- .../Drupal/Nightwatch/Commands/relativeURL.js | 18 ++++++++++++++++++ core/tests/Drupal/Nightwatch/Tests/index.js | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) delete mode 100644 core/tests/Drupal/Nightwatch/Commands/drupalURL.js create mode 100644 core/tests/Drupal/Nightwatch/Commands/relativeURL.js diff --git a/core/tests/Drupal/Nightwatch/Commands/drupalURL.js b/core/tests/Drupal/Nightwatch/Commands/drupalURL.js deleted file mode 100644 index 29e4b39b454c..000000000000 --- a/core/tests/Drupal/Nightwatch/Commands/drupalURL.js +++ /dev/null @@ -1,8 +0,0 @@ -exports.command = function baseurl(relativeURL) { - if (!process.env.BASE_URL || process.env.BASE_URL === '') { - throw new Error('Missing a BASE_URL environment variable.'); - } - this - .url(`${process.env.BASE_URL}${relativeURL}`); - return this; -}; diff --git a/core/tests/Drupal/Nightwatch/Commands/relativeURL.js b/core/tests/Drupal/Nightwatch/Commands/relativeURL.js new file mode 100644 index 000000000000..082c8913a409 --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Commands/relativeURL.js @@ -0,0 +1,18 @@ +/** + * Concatenate a BASE_URL variable and a pathname. + * + * This provides a custom command, .relativeURL() + * + * @param {string} pathname + * The relative path to append to BASE_URL + * @return {object} + * The 'browser' object. + */ +exports.command = function relativeURL(pathname) { + if (!process.env.BASE_URL || process.env.BASE_URL === '') { + throw new Error('Missing a BASE_URL environment variable.'); + } + this + .url(`${process.env.BASE_URL}${pathname}`); + return this; +}; diff --git a/core/tests/Drupal/Nightwatch/Tests/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js index 32732d8398e7..6ef292acdb03 100644 --- a/core/tests/Drupal/Nightwatch/Tests/index.js +++ b/core/tests/Drupal/Nightwatch/Tests/index.js @@ -1,7 +1,7 @@ module.exports = { 'Demo Drupal.org': (browser) => { browser - .drupalURL('/community') + .relativeURL('/community') .waitForElementVisible('body', 1000) .assert.containsText('body', 'Where is the Drupal Community?') .end(); From c06cd72b50d94285b434e91624821963a964deaa Mon Sep 17 00:00:00 2001 From: Matthew Grill Date: Mon, 11 Dec 2017 14:06:39 -0800 Subject: [PATCH 018/232] use nightwatch.settings.json for config. needs docs --- core/.gitignore | 3 +++ core/nightwatch.conf.js | 19 +++++++++---------- core/nightwatch.settings.json.default | 7 +++++++ .../Drupal/Nightwatch/Commands/relativeURL.js | 10 +++++++--- 4 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 core/nightwatch.settings.json.default diff --git a/core/.gitignore b/core/.gitignore index db29d420b98a..bc921b6d8396 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -12,3 +12,6 @@ package-lock.json # Ignore test reports reports + +# Ignore local Nightwatch settings +nightwatch.settings.json diff --git a/core/nightwatch.conf.js b/core/nightwatch.conf.js index 081a9e013876..2a9747232ee8 100644 --- a/core/nightwatch.conf.js +++ b/core/nightwatch.conf.js @@ -1,14 +1,13 @@ -const chromeArgs = ['--disable-gpu']; -if (!process.env.HEADLESS_CHROME_DISABLED) { - chromeArgs.push('--headless'); -} +const settings = require('./nightwatch.settings.json'); -const outputFolder = process.env.NIGHTWATCH_OUTPUT ? process.env.NIGHTWATCH_OUTPUT : 'reports/nightwatch'; -const hostname = process.env.NIGHTWATCH_HOSTNAME ? process.env.NIGHTWATCH_HOSTNAME : 'localhost'; +const args = ['--disable-gpu', ...settings.CHROME_ARGS]; +if (settings.HEADLESS_CHROME) { + args.push('--headless'); +} module.exports = { src_folders: ['tests/Drupal/Nightwatch/Tests'], - output_folder: outputFolder, + output_folder: settings.NIGHTWATCH_OUTPUT, custom_commands_path: ['tests/Drupal/Nightwatch/Commands'], custom_assertions_path: '', page_objects_path: '', @@ -19,20 +18,20 @@ module.exports = { test_settings: { default: { selenium_port: 9515, - selenium_host: hostname, + selenium_host: settings.NIGHTWATCH_HOSTNAME, default_path_prefix: '', desiredCapabilities: { browserName: 'chrome', acceptSslCerts: true, chromeOptions: { - args: chromeArgs, + args, }, }, screenshots: { enabled: true, on_failure: true, on_error: true, - path: `${outputFolder}/screenshots`, + path: `${settings.NIGHTWATCH_OUTPUT}/screenshots`, }, }, }, diff --git a/core/nightwatch.settings.json.default b/core/nightwatch.settings.json.default new file mode 100644 index 000000000000..3be8c5ab1ee2 --- /dev/null +++ b/core/nightwatch.settings.json.default @@ -0,0 +1,7 @@ +{ + "BASE_URL": "", + "NIGHTWATCH_OUTPUT": "reports/nightwatch", + "NIGHTWATCH_HOSTNAME": "", + "HEADLESS_CHROME": true, + "CHROME_ARGS": [] +} diff --git a/core/tests/Drupal/Nightwatch/Commands/relativeURL.js b/core/tests/Drupal/Nightwatch/Commands/relativeURL.js index 082c8913a409..b07c9b5dc378 100644 --- a/core/tests/Drupal/Nightwatch/Commands/relativeURL.js +++ b/core/tests/Drupal/Nightwatch/Commands/relativeURL.js @@ -1,3 +1,5 @@ +const settings = require('../../../../nightwatch.settings.json'); + /** * Concatenate a BASE_URL variable and a pathname. * @@ -9,10 +11,12 @@ * The 'browser' object. */ exports.command = function relativeURL(pathname) { - if (!process.env.BASE_URL || process.env.BASE_URL === '') { - throw new Error('Missing a BASE_URL environment variable.'); + if ( + (!settings.BASE_URL || settings.BASE_URL === '') && + (!process.env.SIMPLETEST_BASE_URL || process.env.SIMPLETEST_BASE_URL === '')) { + throw new Error('Missing a BASE_URL or SIMPLETEST_BASE_URL configuration item.'); } this - .url(`${process.env.BASE_URL}${pathname}`); + .url(`${settings.BASE_URL !== '' ? settings.BASE_URL : process.env.SIMPLETEST_BASE_URL}${pathname}`); return this; }; From c9a5dac7a5e3e4a0ddc04665b30a296190ea2839 Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Mon, 11 Dec 2017 21:43:44 +0000 Subject: [PATCH 019/232] Apply 2926633 --- core/scripts/setup-drupal-test.php | 31 +++ .../SetupDrupalTestScriptTest.php | 29 +++ .../TestInstallationSetupApplication.php | 39 ++++ .../Commands/TestInstallationSetupCommand.php | 43 ++++ core/tests/Drupal/Setup/ExampleTestSetup.php | 22 ++ .../Drupal/Setup/TestInstallationSetup.php | 195 ++++++++++++++++++ .../tests/Drupal/Setup/TestSetupInterface.php | 23 +++ core/tests/bootstrap.php | 1 + 8 files changed, 383 insertions(+) create mode 100644 core/scripts/setup-drupal-test.php create mode 100644 core/tests/Drupal/FunctionalTests/SetupDrupalTestScriptTest.php create mode 100644 core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php create mode 100644 core/tests/Drupal/Setup/Commands/TestInstallationSetupCommand.php create mode 100644 core/tests/Drupal/Setup/ExampleTestSetup.php create mode 100644 core/tests/Drupal/Setup/TestInstallationSetup.php create mode 100644 core/tests/Drupal/Setup/TestSetupInterface.php diff --git a/core/scripts/setup-drupal-test.php b/core/scripts/setup-drupal-test.php new file mode 100644 index 000000000000..44b0f9eaf1fd --- /dev/null +++ b/core/scripts/setup-drupal-test.php @@ -0,0 +1,31 @@ +#!/usr/bin/env php +getAppRoot()); + +Settings::initialize(dirname(dirname(__DIR__)), + DrupalKernel::findSitePath($request), $autoloader); + +require_once __DIR__ . '/../tests/bootstrap.php'; + +(new TestInstallationSetupApplication()) + ->run(); diff --git a/core/tests/Drupal/FunctionalTests/SetupDrupalTestScriptTest.php b/core/tests/Drupal/FunctionalTests/SetupDrupalTestScriptTest.php new file mode 100644 index 000000000000..3a0487092cf0 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/SetupDrupalTestScriptTest.php @@ -0,0 +1,29 @@ +find(); + + $db_url = getenv('SIMPLETEST_DB'); + $base_url = getenv('SIMPLETEST_BASE_URL'); + $process = new Process("$php {$this->root}/core/scripts/setup-drupal-test.php --db_url={$db_url} --base_url={$base_url}"); + $process->run(function ($type, $data) { + // @todo How does one test an async API? + // This code might happen after the test function is done. + }); + } + +} diff --git a/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php b/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php new file mode 100644 index 000000000000..312d3db757d8 --- /dev/null +++ b/core/tests/Drupal/Setup/Commands/TestInstallationSetupApplication.php @@ -0,0 +1,39 @@ +setName('setup-drupal-test') + ->addOption('setup_file', NULL, InputOption::VALUE_OPTIONAL) + ->addOption('db_url', NULL, InputOption::VALUE_OPTIONAL, '', getenv('SIMPLETEST_DB')) + ->addOption('base_url', NULL, InputOption::VALUE_OPTIONAL, '', getenv('SIMPLETEST_BASE_URL')); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $test = new TestInstallationSetup(); + $test->setup('testing', $input->getOption('setup_file')); + + $db_url = $input->getOption('db_url'); + $base_url = $input->getOption('base_url'); + putenv("SIMPLETEST_DB=$db_url"); + putenv("SIMPLETEST_BASE_URL=$base_url"); + + $output->writeln(drupal_generate_test_ua($test->getDatabasePrefix())); + } + +} diff --git a/core/tests/Drupal/Setup/ExampleTestSetup.php b/core/tests/Drupal/Setup/ExampleTestSetup.php new file mode 100644 index 000000000000..ed94c18778aa --- /dev/null +++ b/core/tests/Drupal/Setup/ExampleTestSetup.php @@ -0,0 +1,22 @@ +install(['node']); + + Node::create(['type' => 'page', 'title' => 'Test tile']) + ->save(); + } + +} diff --git a/core/tests/Drupal/Setup/TestInstallationSetup.php b/core/tests/Drupal/Setup/TestInstallationSetup.php new file mode 100644 index 000000000000..c0bde3851f21 --- /dev/null +++ b/core/tests/Drupal/Setup/TestInstallationSetup.php @@ -0,0 +1,195 @@ +profile = $profile; + $this->setupBaseUrl(); + $this->prepareEnvironment(); + $this->installDrupal(); + + if ($setup_file) { + $this->executeSetupFile($setup_file); + } + } + + /** + * Gets the database prefix. + * + * @return string + */ + public function getDatabasePrefix() { + return $this->databasePrefix; + } + + /** + * Installs Drupal into the Simpletest site. + */ + protected function installDrupal() { + $this->initUserSession(); + $this->prepareSettings(); + $this->doInstall(); + $this->initSettings(); + $container = $this->initKernel(\Drupal::request()); + $this->initConfig($container); + $this->installModulesFromClassProperty($container); + $this->rebuildAll(); + } + + /** + * Uses the setup file to configure Drupal. + * + * @param string $setup_file + * The setup file. + */ + protected function executeSetupFile($setup_file) { + $classes = static::fileGetPhpClasses($setup_file); + + if (empty($classes)) { + throw new \InvalidArgumentException(sprintf('You need to define a class implementing \Drupal\Setup\TestSetupInterface')); + } + if (count($classes) > 1) { + throw new \InvalidArgumentException(sprintf('You need to define a single class implementing \Drupal\Setup\TestSetupInterface')); + } + if (!is_subclass_of($classes[0], TestSetupInterface::class)) { + throw new \InvalidArgumentException(sprintf('You need to define a class implementing \Drupal\Setup\TestSetupInterface')); + } + + require_once $setup_file; + + /** @var \Drupal\Setup\TestSetupInterface $instance */ + $instance = new $classes[0]; + $instance->setup(); + } + + /** + * Gets the PHP classes contained in a php file. + * + * @param string $filepath + * The file path. + * + * @return string[] + * An array of PHP classes. + */ + protected static function fileGetPhpClasses($filepath) { + $php_code = file_get_contents($filepath); + $classes = static::extractClassesFromPhp($php_code); + return $classes; + } + + /** + * @param string $php_code + * PHP code to parse. + * + * @return string[] + * An array of PHP classes. + */ + protected static function extractClassesFromPhp($php_code) { + $classes = array(); + $tokens = token_get_all($php_code); + $count = count($tokens); + for ($i = 2; $i < $count; $i++) { + if ($tokens[$i - 2][0] == T_CLASS + && $tokens[$i - 1][0] == T_WHITESPACE + && $tokens[$i][0] == T_STRING + ) { + + $class_name = $tokens[$i][1]; + $classes[] = $class_name; + } + } + return $classes; + } + + /** + * {@inheritdoc} + */ + protected function installParameters() { + $connection_info = Database::getConnectionInfo(); + $driver = $connection_info['default']['driver']; + $connection_info['default']['prefix'] = $connection_info['default']['prefix']['default']; + unset($connection_info['default']['driver']); + unset($connection_info['default']['namespace']); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + $parameters = [ + 'interactive' => FALSE, + 'parameters' => [ + 'profile' => $this->profile, + 'langcode' => 'en', + ], + 'forms' => [ + 'install_settings_form' => [ + 'driver' => $driver, + $driver => $connection_info['default'], + ], + 'install_configure_form' => [ + 'site_name' => 'Drupal', + 'site_mail' => 'simpletest@example.com', + 'account' => [ + 'name' => $this->rootUser->name, + 'mail' => $this->rootUser->getEmail(), + 'pass' => [ + 'pass1' => $this->rootUser->pass_raw, + 'pass2' => $this->rootUser->pass_raw, + ], + ], + // form_type_checkboxes_value() requires NULL instead of FALSE values + // for programmatic form submissions to disable a checkbox. + 'enable_update_status_module' => NULL, + 'enable_update_status_emails' => NULL, + ], + ], + ]; + return $parameters; + } + +} diff --git a/core/tests/Drupal/Setup/TestSetupInterface.php b/core/tests/Drupal/Setup/TestSetupInterface.php new file mode 100644 index 000000000000..8bf2c2e96acf --- /dev/null +++ b/core/tests/Drupal/Setup/TestSetupInterface.php @@ -0,0 +1,23 @@ +install(['my_module']) + * @endcode + * + * Check out 'core/tests/Drupal/Setup/ExampleTestSetup.php' for an example. + */ + public function setup(); + +} diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index f78b69ff0498..77b270fe232a 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -127,6 +127,7 @@ function drupal_phpunit_populate_class_loader() { // Start with classes in known locations. $loader->add('Drupal\\Tests', __DIR__); + $loader->add('Drupal\\Setup', __DIR__); $loader->add('Drupal\\KernelTests', __DIR__); $loader->add('Drupal\\FunctionalTests', __DIR__); $loader->add('Drupal\\FunctionalJavascriptTests', __DIR__); From 168484060197a89f1607d3945dd109cb1540b25d Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Mon, 11 Dec 2017 22:04:50 +0000 Subject: [PATCH 020/232] Fix test to actually work against a drupal site --- core/tests/Drupal/Nightwatch/Tests/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/tests/Drupal/Nightwatch/Tests/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js index 6ef292acdb03..41c4a8833a52 100644 --- a/core/tests/Drupal/Nightwatch/Tests/index.js +++ b/core/tests/Drupal/Nightwatch/Tests/index.js @@ -1,9 +1,9 @@ module.exports = { 'Demo Drupal.org': (browser) => { browser - .relativeURL('/community') + .relativeURL('/user/login') .waitForElementVisible('body', 1000) - .assert.containsText('body', 'Where is the Drupal Community?') + .assert.containsText('body', 'Powered by Drupal') .end(); }, }; From 84f21dab4b2e676dd98cc0bc944d7d5d8a16af2c Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Mon, 11 Dec 2017 22:32:24 +0000 Subject: [PATCH 021/232] Improve initial setup --- core/tests/Drupal/Nightwatch/globals.js | 2 ++ core/tests/README.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/tests/Drupal/Nightwatch/globals.js b/core/tests/Drupal/Nightwatch/globals.js index 3786eeaa5c9e..61c956af95af 100644 --- a/core/tests/Drupal/Nightwatch/globals.js +++ b/core/tests/Drupal/Nightwatch/globals.js @@ -2,6 +2,8 @@ const chromedriver = require('chromedriver'); module.exports = { before: (done) => { + // Setting up + process.env.SIMPLETEST_BASE_URL = process.env.SIMPLETEST_BASE_URL || process.env.BASE_URL; if (process.env.NODE_ENV !== 'testbot') { chromedriver.start(); } diff --git a/core/tests/README.md b/core/tests/README.md index d2bb1c6c8b53..36390b88cdf2 100644 --- a/core/tests/README.md +++ b/core/tests/README.md @@ -52,7 +52,8 @@ sudo -u www-data -E ./vendor/bin/phpunit -c core --testsuite functional-javascri - Install [Node.js](https://nodejs.org/en/download/) and [yarn](https://yarnpkg.com/en/docs/install). The versions required are specificed inside core/package.json in the `engines` field - Install [Google Chrome](https://www.google.com/chrome/browser/desktop/index.html) - Inside the `core` folder, run `yarn install` -- Again inside the `core` folder, run `yarn nightwatch` to run the tests. By default this will output reports to `core/reports` +- Configure the nightwatch settings by copying `nightwatch.settings.json.default` to `nightwatch.settings.json` +- Again inside the `core` folder, run `yarn test:js` to run the tests. By default this will output reports to `core/reports` Some settings can be overridden with environment variables: From 58c9c80683a722e2bed687bb730f085d4c7f70a9 Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Mon, 11 Dec 2017 22:38:21 +0000 Subject: [PATCH 022/232] Use the install command --- .../Nightwatch/Commands/installDrupal.js | 39 +++++++++++++++++++ core/tests/Drupal/Nightwatch/Tests/index.js | 3 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 core/tests/Drupal/Nightwatch/Commands/installDrupal.js diff --git a/core/tests/Drupal/Nightwatch/Commands/installDrupal.js b/core/tests/Drupal/Nightwatch/Commands/installDrupal.js new file mode 100644 index 000000000000..ef703d7ab53f --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Commands/installDrupal.js @@ -0,0 +1,39 @@ +const exec = require('child_process').exec; + +/** + * @param browser + * @param cookieValue + * @returns {*} + */ +const setupCookie = function (browser, cookieValue, done) { + const matches = process.env.BASE_URL.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i); + const domain = matches[1]; + const path = matches[2]; + + return browser + // See https://bugs.chromium.org/p/chromedriver/issues/detail?id=728#c10 + .url(BASE_URL) + .setCookie({ + name: 'SIMPLETEST_USER_AGENT', + // Colons needs to be URL encoded to be valid. + value: encodeURIComponent(cookieValue), + path: path, + domain: domain, + }, done); +}; + +exports.command = function installDrupal(profile = 'testing', setupFile = '', done) { + const self = this; + + exec(`php ./scripts/setup-drupal-test.php ${profile} ${setupFile}`, (err, simpleTestCookie) => { + if (err) { + console.error(err); + return done(err); + } + + setupCookie(self, simpleTestCookie, simpleTestCookie); + }); + + + return this; +}; diff --git a/core/tests/Drupal/Nightwatch/Tests/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js index 41c4a8833a52..aee9920e246d 100644 --- a/core/tests/Drupal/Nightwatch/Tests/index.js +++ b/core/tests/Drupal/Nightwatch/Tests/index.js @@ -1,6 +1,7 @@ module.exports = { - 'Demo Drupal.org': (browser) => { + 'Demo Drupal.org': (browser, done) => { browser + .installDrupal() .relativeURL('/user/login') .waitForElementVisible('body', 1000) .assert.containsText('body', 'Powered by Drupal') From 4f6a28e2d9053c94722c5143604a9d2c583fdab6 Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Mon, 11 Dec 2017 22:54:09 +0000 Subject: [PATCH 023/232] Wire everything together --- core/tests/Drupal/Nightwatch/Commands/installDrupal.js | 4 ++-- core/tests/Drupal/Nightwatch/Commands/relativeURL.js | 8 +------- core/tests/Drupal/Nightwatch/Tests/index.js | 2 +- core/tests/Drupal/Nightwatch/globals.js | 9 ++++++++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/core/tests/Drupal/Nightwatch/Commands/installDrupal.js b/core/tests/Drupal/Nightwatch/Commands/installDrupal.js index ef703d7ab53f..f62297b8c9bd 100644 --- a/core/tests/Drupal/Nightwatch/Commands/installDrupal.js +++ b/core/tests/Drupal/Nightwatch/Commands/installDrupal.js @@ -12,7 +12,7 @@ const setupCookie = function (browser, cookieValue, done) { return browser // See https://bugs.chromium.org/p/chromedriver/issues/detail?id=728#c10 - .url(BASE_URL) + .url(process.env.BASE_URL) .setCookie({ name: 'SIMPLETEST_USER_AGENT', // Colons needs to be URL encoded to be valid. @@ -31,7 +31,7 @@ exports.command = function installDrupal(profile = 'testing', setupFile = '', do return done(err); } - setupCookie(self, simpleTestCookie, simpleTestCookie); + setupCookie(self, simpleTestCookie, done); }); diff --git a/core/tests/Drupal/Nightwatch/Commands/relativeURL.js b/core/tests/Drupal/Nightwatch/Commands/relativeURL.js index b07c9b5dc378..867f9825e194 100644 --- a/core/tests/Drupal/Nightwatch/Commands/relativeURL.js +++ b/core/tests/Drupal/Nightwatch/Commands/relativeURL.js @@ -1,4 +1,3 @@ -const settings = require('../../../../nightwatch.settings.json'); /** * Concatenate a BASE_URL variable and a pathname. @@ -11,12 +10,7 @@ const settings = require('../../../../nightwatch.settings.json'); * The 'browser' object. */ exports.command = function relativeURL(pathname) { - if ( - (!settings.BASE_URL || settings.BASE_URL === '') && - (!process.env.SIMPLETEST_BASE_URL || process.env.SIMPLETEST_BASE_URL === '')) { - throw new Error('Missing a BASE_URL or SIMPLETEST_BASE_URL configuration item.'); - } this - .url(`${settings.BASE_URL !== '' ? settings.BASE_URL : process.env.SIMPLETEST_BASE_URL}${pathname}`); + .url(`${process.env.BASE_URL}${pathname}`); return this; }; diff --git a/core/tests/Drupal/Nightwatch/Tests/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js index aee9920e246d..c600b8033974 100644 --- a/core/tests/Drupal/Nightwatch/Tests/index.js +++ b/core/tests/Drupal/Nightwatch/Tests/index.js @@ -4,7 +4,7 @@ module.exports = { .installDrupal() .relativeURL('/user/login') .waitForElementVisible('body', 1000) - .assert.containsText('body', 'Powered by Drupal') + .assert.containsText('body', 'Skip to main content') .end(); }, }; diff --git a/core/tests/Drupal/Nightwatch/globals.js b/core/tests/Drupal/Nightwatch/globals.js index 61c956af95af..b5e53b41c1fa 100644 --- a/core/tests/Drupal/Nightwatch/globals.js +++ b/core/tests/Drupal/Nightwatch/globals.js @@ -1,9 +1,16 @@ const chromedriver = require('chromedriver'); +const settings = require('../../../nightwatch.settings.json'); module.exports = { before: (done) => { // Setting up - process.env.SIMPLETEST_BASE_URL = process.env.SIMPLETEST_BASE_URL || process.env.BASE_URL; + const baseUrl = settings.BASE_URL || process.env.SIMPLETEST_BASE_URL || process.env.BASE_URL; + + if (baseUrl === undefined) { + throw new Error('Missing a BASE_URL or SIMPLETEST_BASE_URL configuration item.'); + } + + process.env.BASE_URL = process.env.SIMPLETEST_BASE_URL = baseUrl; if (process.env.NODE_ENV !== 'testbot') { chromedriver.start(); } From e06f20c7a2b510836a0d9615145690f4e734dd94 Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Mon, 11 Dec 2017 23:25:53 +0000 Subject: [PATCH 024/232] Create a setup file --- .../Drupal/Nightwatch/Commands/installDrupal.js | 2 +- core/tests/Drupal/Nightwatch/Tests/index.js | 8 ++++---- core/tests/Drupal/Nightwatch/Tests/index.setup.php | 12 ++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 core/tests/Drupal/Nightwatch/Tests/index.setup.php diff --git a/core/tests/Drupal/Nightwatch/Commands/installDrupal.js b/core/tests/Drupal/Nightwatch/Commands/installDrupal.js index f62297b8c9bd..96de8a83e930 100644 --- a/core/tests/Drupal/Nightwatch/Commands/installDrupal.js +++ b/core/tests/Drupal/Nightwatch/Commands/installDrupal.js @@ -25,7 +25,7 @@ const setupCookie = function (browser, cookieValue, done) { exports.command = function installDrupal(profile = 'testing', setupFile = '', done) { const self = this; - exec(`php ./scripts/setup-drupal-test.php ${profile} ${setupFile}`, (err, simpleTestCookie) => { + exec(`php ./scripts/setup-drupal-test.php ${profile} --setup_file ${setupFile}`, (err, simpleTestCookie) => { if (err) { console.error(err); return done(err); diff --git a/core/tests/Drupal/Nightwatch/Tests/index.js b/core/tests/Drupal/Nightwatch/Tests/index.js index c600b8033974..acdcaedb40fa 100644 --- a/core/tests/Drupal/Nightwatch/Tests/index.js +++ b/core/tests/Drupal/Nightwatch/Tests/index.js @@ -1,10 +1,10 @@ module.exports = { - 'Demo Drupal.org': (browser, done) => { + 'Test page': (browser, done) => { browser - .installDrupal() - .relativeURL('/user/login') + .installDrupal('testing', __dirname + '/index.setup.php') + .relativeURL('/test-page') .waitForElementVisible('body', 1000) - .assert.containsText('body', 'Skip to main content') + .assert.containsText('body', 'Test page text') .end(); }, }; diff --git a/core/tests/Drupal/Nightwatch/Tests/index.setup.php b/core/tests/Drupal/Nightwatch/Tests/index.setup.php new file mode 100644 index 000000000000..6aa0cf230f23 --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Tests/index.setup.php @@ -0,0 +1,12 @@ +install(['test_page_test']); + } + +} From 0c1cc1b601e1009999de4f96d051b037958ccb55 Mon Sep 17 00:00:00 2001 From: Daniel Wehner Date: Mon, 11 Dec 2017 23:26:12 +0000 Subject: [PATCH 025/232] fix the test setup code --- core/tests/Drupal/Setup/TestInstallationSetup.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/tests/Drupal/Setup/TestInstallationSetup.php b/core/tests/Drupal/Setup/TestInstallationSetup.php index c0bde3851f21..714b2815e4fe 100644 --- a/core/tests/Drupal/Setup/TestInstallationSetup.php +++ b/core/tests/Drupal/Setup/TestInstallationSetup.php @@ -99,12 +99,13 @@ protected function executeSetupFile($setup_file) { if (count($classes) > 1) { throw new \InvalidArgumentException(sprintf('You need to define a single class implementing \Drupal\Setup\TestSetupInterface')); } + + require_once $setup_file; + if (!is_subclass_of($classes[0], TestSetupInterface::class)) { throw new \InvalidArgumentException(sprintf('You need to define a class implementing \Drupal\Setup\TestSetupInterface')); } - require_once $setup_file; - /** @var \Drupal\Setup\TestSetupInterface $instance */ $instance = new $classes[0]; $instance->setup(); From de9e2d7061c2fe2303ca926250817d4e125b91f9 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Tue, 12 Dec 2017 19:29:54 +1000 Subject: [PATCH 026/232] Issue #2865184 by chr.fritsch, phenaproxima, larowlan, marcoscano, xjm: Allow MediaSource plugins provide default field form/view display settings --- core/modules/media/src/MediaSourceBase.php | 16 ++++ .../media/src/MediaSourceInterface.php | 38 ++++++++++ core/modules/media/src/MediaTypeForm.php | 23 ++---- .../schema/media_test_source.schema.yml | 8 ++ .../media/Source/TestDifferentDisplays.php | 46 ++++++++++++ .../Source/TestWithHiddenSourceField.php | 42 +++++++++++ .../tests/src/Kernel/MediaSourceTest.php | 75 +++++++++++++++++++ 7 files changed, 231 insertions(+), 17 deletions(-) create mode 100644 core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestDifferentDisplays.php create mode 100644 core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestWithHiddenSourceField.php diff --git a/core/modules/media/src/MediaSourceBase.php b/core/modules/media/src/MediaSourceBase.php index dea01402609c..4f8301fbee75 100644 --- a/core/modules/media/src/MediaSourceBase.php +++ b/core/modules/media/src/MediaSourceBase.php @@ -3,6 +3,8 @@ namespace Drupal\media; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\Display\EntityFormDisplayInterface; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; @@ -317,4 +319,18 @@ protected function getSourceFieldName() { return $id; } + /** + * {@inheritdoc} + */ + public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) { + $display->setComponent($this->getSourceFieldDefinition($type)->getName()); + } + + /** + * {@inheritdoc} + */ + public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display) { + $display->setComponent($this->getSourceFieldDefinition($type)->getName()); + } + } diff --git a/core/modules/media/src/MediaSourceInterface.php b/core/modules/media/src/MediaSourceInterface.php index 723e0b9a8e05..43dd8aa73380 100644 --- a/core/modules/media/src/MediaSourceInterface.php +++ b/core/modules/media/src/MediaSourceInterface.php @@ -4,6 +4,8 @@ use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Entity\Display\EntityFormDisplayInterface; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Plugin\PluginFormInterface; /** @@ -139,4 +141,40 @@ public function getSourceFieldDefinition(MediaTypeInterface $type); */ public function createSourceField(MediaTypeInterface $type); + /** + * Prepares the media type fields for this source in the view display. + * + * This method should normally call + * \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent() or + * \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent() to + * configure the media type fields in the view display. + * + * @param \Drupal\media\MediaTypeInterface $type + * The media type which is using this source. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The display which should be prepared. + * + * @see \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent() + * @see \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent() + */ + public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display); + + /** + * Prepares the media type fields for this source in the form display. + * + * This method should normally call + * \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent() or + * \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent() to + * configure the media type fields in the form display. + * + * @param \Drupal\media\MediaTypeInterface $type + * The media type which is using this source. + * @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display + * The display which should be prepared. + * + * @see \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent() + * @see \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent() + */ + public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display); + } diff --git a/core/modules/media/src/MediaTypeForm.php b/core/modules/media/src/MediaTypeForm.php index 5f301d58ed0b..04ac4ba88ccf 100644 --- a/core/modules/media/src/MediaTypeForm.php +++ b/core/modules/media/src/MediaTypeForm.php @@ -327,30 +327,19 @@ public function save(array $form, FormStateInterface $form_state) { // Add the new field to the default form and view displays for this // media type. - $field_name = $source_field->getName(); - $field_type = $source_field->getType(); - if ($source_field->isDisplayConfigurable('form')) { - // Use the default widget and settings. - $component = \Drupal::service('plugin.manager.field.widget') - ->prepareConfiguration($field_type, []); - // @todo Replace entity_get_form_display() when #2367933 is done. // https://www.drupal.org/node/2872159. - entity_get_form_display('media', $media_type->id(), 'default') - ->setComponent($field_name, $component) - ->save(); + $display = entity_get_form_display('media', $media_type->id(), 'default'); + $source->prepareFormDisplay($media_type, $display); + $display->save(); } if ($source_field->isDisplayConfigurable('view')) { - // Use the default formatter and settings. - $component = \Drupal::service('plugin.manager.field.formatter') - ->prepareConfiguration($field_type, []); - // @todo Replace entity_get_display() when #2367933 is done. // https://www.drupal.org/node/2872159. - entity_get_display('media', $media_type->id(), 'default') - ->setComponent($field_name, $component) - ->save(); + $display = entity_get_display('media', $media_type->id(), 'default'); + $source->prepareViewDisplay($media_type, $display); + $display->save(); } } diff --git a/core/modules/media/tests/modules/media_test_source/config/schema/media_test_source.schema.yml b/core/modules/media/tests/modules/media_test_source/config/schema/media_test_source.schema.yml index 089aeac35f02..ab025640b400 100644 --- a/core/modules/media/tests/modules/media_test_source/config/schema/media_test_source.schema.yml +++ b/core/modules/media/tests/modules/media_test_source/config/schema/media_test_source.schema.yml @@ -13,3 +13,11 @@ media.source.test_translation: media.source.test_constraints: type: media.source.test label: 'Test media source with constraints configuration' + +media.source.test_hidden_source_field: + type: media.source.test + label: 'Test media source with hidden source field' + +media.source.test_different_displays: + type: media.source.test + label: 'Test media source with different source field displays' diff --git a/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestDifferentDisplays.php b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestDifferentDisplays.php new file mode 100644 index 000000000000..19dde8b3ac30 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestDifferentDisplays.php @@ -0,0 +1,46 @@ +setComponent($this->getSourceFieldDefinition($type)->getName(), [ + 'type' => 'entity_reference_entity_id', + ]); + } + + /** + * {@inheritdoc} + */ + public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display) { + $display->setComponent($this->getSourceFieldDefinition($type)->getName(), [ + 'type' => 'entity_reference_autocomplete_tags', + ]); + } + + /** + * {@inheritdoc} + */ + protected function getSourceFieldName() { + return 'field_media_different_display'; + } + +} diff --git a/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestWithHiddenSourceField.php b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestWithHiddenSourceField.php new file mode 100644 index 000000000000..e5ac525eb0e6 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestWithHiddenSourceField.php @@ -0,0 +1,42 @@ +removeComponent($this->getSourceFieldDefinition($type)->getName()); + } + + /** + * {@inheritdoc} + */ + public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display) { + $display->removeComponent($this->getSourceFieldDefinition($type)->getName()); + } + + /** + * {@inheritdoc} + */ + protected function getSourceFieldName() { + return 'field_media_hidden'; + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaSourceTest.php b/core/modules/media/tests/src/Kernel/MediaSourceTest.php index fc30326fb1dd..e985303566c2 100644 --- a/core/modules/media/tests/src/Kernel/MediaSourceTest.php +++ b/core/modules/media/tests/src/Kernel/MediaSourceTest.php @@ -442,4 +442,79 @@ public function testSourceConfigurationSubmit() { $this->assertEquals($expected, $source->getConfiguration(), 'Submitted values were saved correctly.'); } + /** + * Tests different display options for the source field. + */ + public function testDifferentSourceFieldDisplays() { + $id = 'test_different_displays'; + $field_name = 'field_media_different_display'; + + $this->createMediaTypeViaForm($id, $field_name); + + // Source field not in displays. + $display = entity_get_display('media', $id, 'default'); + $components = $display->getComponents(); + $this->assertArrayHasKey($field_name, $components); + $this->assertSame('entity_reference_entity_id', $components[$field_name]['type']); + + $display = entity_get_form_display('media', $id, 'default'); + $components = $display->getComponents(); + $this->assertArrayHasKey($field_name, $components); + $this->assertSame('entity_reference_autocomplete_tags', $components[$field_name]['type']); + } + + /** + * Tests hidden source field in media type. + */ + public function testHiddenSourceField() { + $id = 'test_hidden_source_field'; + $field_name = 'field_media_hidden'; + + $this->createMediaTypeViaForm($id, $field_name); + + // Source field not in displays. + $display = entity_get_display('media', $id, 'default'); + $this->assertArrayNotHasKey($field_name, $display->getComponents()); + + $display = entity_get_form_display('media', $id, 'default'); + $this->assertArrayNotHasKey($field_name, $display->getComponents()); + } + + /** + * Creates a media type via form submit. + * + * @param string $source_plugin_id + * Source plugin ID. + * @param string $field_name + * Source field name. + */ + protected function createMediaTypeViaForm($source_plugin_id, $field_name) { + /** @var \Drupal\media\MediaTypeInterface $type */ + $type = MediaType::create(['source' => $source_plugin_id]); + + $form = $this->container->get('entity_type.manager') + ->getFormObject('media_type', 'add') + ->setEntity($type); + + $form_state = new FormState(); + $form_state->setValues([ + 'label' => 'Test type', + 'id' => $source_plugin_id, + 'op' => t('Save'), + ]); + + /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */ + $field_manager = \Drupal::service('entity_field.manager'); + + // Source field not created yet. + $fields = $field_manager->getFieldDefinitions('media', $source_plugin_id); + $this->assertArrayNotHasKey($field_name, $fields); + + \Drupal::formBuilder()->submitForm($form, $form_state); + + // Source field exists now. + $fields = $field_manager->getFieldDefinitions('media', $source_plugin_id); + $this->assertArrayHasKey($field_name, $fields); + } + } From b5b3949b1454d535f876e29b8d0453d31416179c Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Tue, 12 Dec 2017 19:41:20 +1000 Subject: [PATCH 027/232] Issue #2928256 by marcoscano, seanB: Users shouldn't be able to change the media source plugin after the media type is created --- core/modules/media/src/MediaTypeForm.php | 11 ++++++++++- .../FunctionalJavascript/MediaTypeCreationTest.php | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/modules/media/src/MediaTypeForm.php b/core/modules/media/src/MediaTypeForm.php index 04ac4ba88ccf..170a34fa5c2e 100644 --- a/core/modules/media/src/MediaTypeForm.php +++ b/core/modules/media/src/MediaTypeForm.php @@ -118,14 +118,23 @@ public function form(array $form, FormStateInterface $form_state) { '#attributes' => ['id' => 'source-dependent'], ]; + if ($source) { + $source_description = $this->t('The media source cannot be changed after the media type is created.'); + } + else { + $source_description = $this->t('Media source that is responsible for additional logic related to this media type.'); + } $form['source_dependent']['source'] = [ '#type' => 'select', '#title' => $this->t('Media source'), '#default_value' => $source ? $source->getPluginId() : NULL, '#options' => $options, - '#description' => $this->t('Media source that is responsible for additional logic related to this media type.'), + '#description' => $source_description, '#ajax' => ['callback' => '::ajaxHandlerData'], '#required' => TRUE, + // Once the media type is created, its source plugin cannot be changed + // anymore. + '#disabled' => !empty($source), ]; if (!$source) { diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php index efbe28849b19..652aca8c6be1 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php @@ -49,6 +49,10 @@ public function testMediaTypeCreationFormWithDefaultField() { $this->drupalGet("admin/structure/media/manage/{$mediaTypeMachineName}"); $assert_session->pageTextContains('Test source field is used to store the essential information about the media item.'); + + // Check that the plugin cannot be changed after it is set on type creation. + $assert_session->fieldDisabled('Media source'); + $assert_session->pageTextContains('The media source cannot be changed after the media type is created.'); } /** From f320141ddab659fdfe497aaa684ddfb50b1aa3a3 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Wed, 13 Dec 2017 06:40:44 +1000 Subject: [PATCH 028/232] Issue #2795317 by hswong3i, alexpott, Lendude, bircher, dawehner, martin107, Jo Fitzgerald, mondrake: Allow PHPUnit 6+ support for object mocking --- .../tests/src/Unit/UpdateFetcherTest.php | 2 +- .../Drupal/KernelTests/KernelTestBase.php | 2 + core/tests/Drupal/Tests/BrowserTestBase.php | 1 + .../Tests/PhpunitCompatibilityTrait.php | 142 ++++++++++++++++++ .../Tests/PhpunitCompatibilityTraitTest.php | 115 ++++++++++++++ core/tests/Drupal/Tests/UnitTestCase.php | 12 +- 6 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php create mode 100644 core/tests/Drupal/Tests/PhpunitCompatibilityTraitTest.php diff --git a/core/modules/update/tests/src/Unit/UpdateFetcherTest.php b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php index c3e447d1fcfb..61c767deda67 100644 --- a/core/modules/update/tests/src/Unit/UpdateFetcherTest.php +++ b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php @@ -28,7 +28,7 @@ class UpdateFetcherTest extends UnitTestCase { */ protected function setUp() { $config_factory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]); - $http_client_mock = $this->getMock('\GuzzleHttp\ClientInterface'); + $http_client_mock = $this->createMock('\GuzzleHttp\ClientInterface'); $this->updateFetcher = new UpdateFetcher($config_factory, $http_client_mock); } diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index 32c97b63b555..ca1a517d6abc 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -20,6 +20,7 @@ use Drupal\simpletest\AssertContentTrait; use Drupal\Tests\AssertHelperTrait; use Drupal\Tests\ConfigTestTrait; +use Drupal\Tests\PhpunitCompatibilityTrait; use Drupal\Tests\RandomGeneratorTrait; use Drupal\Tests\TestRequirementsTrait; use Drupal\simpletest\TestServiceProvider; @@ -76,6 +77,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa use RandomGeneratorTrait; use ConfigTestTrait; use TestRequirementsTrait; + use PhpunitCompatibilityTrait; /** * {@inheritdoc} diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index b12ba60b8a45..93df3dbf1319 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -66,6 +66,7 @@ abstract class BrowserTestBase extends TestCase { createUser as drupalCreateUser; } use XdebugRequestTrait; + use PhpunitCompatibilityTrait; /** * The database prefix of this test run. diff --git a/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php new file mode 100644 index 000000000000..5cf020a8df6b --- /dev/null +++ b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php @@ -0,0 +1,142 @@ +supports('getMock')) { + $mock = $this->getMockBuilder($originalClassName) + ->setMethods($methods) + ->setConstructorArgs($arguments) + ->setMockClassName($mockClassName) + ->setProxyTarget($proxyTarget); + if ($callOriginalConstructor) { + $mock->enableOriginalConstructor(); + } + else { + $mock->disableOriginalConstructor(); + } + if ($callOriginalClone) { + $mock->enableOriginalClone(); + } + else { + $mock->disableOriginalClone(); + } + if ($callAutoload) { + $mock->enableAutoload(); + } + else { + $mock->disableAutoload(); + } + if ($cloneArguments) { + $mock->enableArgumentCloning(); + } + else { + $mock->disableArgumentCloning(); + } + if ($callOriginalMethods) { + $mock->enableProxyingToOriginalMethods(); + } + else { + $mock->disableProxyingToOriginalMethods(); + } + return $mock->getMock(); + } + else { + return parent::getMock($originalClassName, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods, $proxyTarget); + } + } + + /** + * Returns a mock object for the specified class using the available method. + * + * The createMock method does not exist in PHPUnit 4. To provide forward + * compatibility this trait provides the createMock method and uses createMock + * if this method is available on the parent class or falls back to getMock if + * it isn't. + * + * @param string $originalClassName + * Name of the class to mock. + * + * @see \PHPUnit_Framework_TestCase::getMock + * + * @return \PHPUnit_Framework_MockObject_MockObject + */ + public function createMock($originalClassName) { + if ($this->supports('createMock')) { + return parent::createMock($originalClassName); + } + else { + return $this->getMock($originalClassName, [], [], '', FALSE, FALSE); + } + } + + /** + * Checks if the trait is used in a class that has a method. + * + * @param string $method + * Method to check. + * + * @return bool + * TRUE if the method is supported, FALSE if not. + */ + private function supports($method) { + // Get the parent class of the currently running test class. + $parent = get_parent_class($this); + // Ensure that the method_exists() check on the createMock method is carried + // out on the first parent of $this that does not have access to this + // trait's methods. This is because the trait also has a method called + // createMock(). Most often the check will be made on + // \PHPUnit\Framework\TestCase. + while (method_exists($parent, 'supports')) { + $parent = get_parent_class($parent); + } + return method_exists($parent, $method); + } + +} diff --git a/core/tests/Drupal/Tests/PhpunitCompatibilityTraitTest.php b/core/tests/Drupal/Tests/PhpunitCompatibilityTraitTest.php new file mode 100644 index 000000000000..145980bfe531 --- /dev/null +++ b/core/tests/Drupal/Tests/PhpunitCompatibilityTraitTest.php @@ -0,0 +1,115 @@ +assertSame($expected, $class->getMock($this->randomMachineName())); + } + + /** + * Tests that createMock is available and calls the correct parent method. + * + * @covers ::createMock + * @dataProvider providerMockVersions + */ + public function testCreateMock($className, $expected) { + $class = new $className(); + $this->assertSame($expected, $class->createMock($this->randomMachineName())); + } + + /** + * Returns the class names and the string they return. + * + * @return array + */ + public function providerMockVersions() { + return [ + [UnitTestCasePhpunit4TestClass::class, 'PHPUnit 4'], + [UnitTestCasePhpunit4TestClassExtends::class, 'PHPUnit 4'], + [UnitTestCasePhpunit6TestClass::class, 'PHPUnit 6'], + [UnitTestCasePhpunit6TestClassExtends::class, 'PHPUnit 6'], + ]; + } + +} + +/** + * Test class for \PHPUnit\Framework\TestCase in PHPUnit 4. + */ +class Phpunit4TestClass { + public function getMock($originalClassName) { + return 'PHPUnit 4'; + } + +} + +/** + * Test class for \PHPUnit\Framework\TestCase in PHPUnit 6. + */ +class Phpunit6TestClass { + public function createMock($originalClassName) { + return 'PHPUnit 6'; + } + + public function getMockbuilder() { + return new Mockbuilder(); + } + +} + +/** + * Test double for PHPUnit_Framework_MockObject_MockBuilder. + */ +class Mockbuilder { + public function __call($name, $arguments) { + return $this; + } + + public function getMock() { + return 'PHPUnit 6'; + } + +} + +/** + * Test class for \Drupal\Tests\UnitTestCase with PHPUnit 4. + */ +class UnitTestCasePhpunit4TestClass extends Phpunit4TestClass { + use PhpunitCompatibilityTrait; + +} + +/** + * Test class for \Drupal\Tests\UnitTestCase with PHPUnit 4. + */ +class UnitTestCasePhpunit4TestClassExtends extends UnitTestCasePhpunit4TestClass { +} + +/** + * Test class for \Drupal\Tests\UnitTestCase with PHPUnit 6. + */ +class UnitTestCasePhpunit6TestClass extends Phpunit6TestClass { + use PhpunitCompatibilityTrait; + +} + +/** + * Test class for \Drupal\Tests\UnitTestCase with PHPUnit 6. + */ +class UnitTestCasePhpunit6TestClassExtends extends UnitTestCasePhpunit6TestClass { +} diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php index 24ad6801bf8e..58173e540ff8 100644 --- a/core/tests/Drupal/Tests/UnitTestCase.php +++ b/core/tests/Drupal/Tests/UnitTestCase.php @@ -17,6 +17,8 @@ */ abstract class UnitTestCase extends TestCase { + use PhpunitCompatibilityTrait; + /** * The random generator. * @@ -135,7 +137,7 @@ public function getConfigFactoryStub(array $configs = []) { } // Construct a config factory with the array of configuration object stubs // as its return map. - $config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface'); + $config_factory = $this->createMock('Drupal\Core\Config\ConfigFactoryInterface'); $config_factory->expects($this->any()) ->method('get') ->will($this->returnValueMap($config_get_map)); @@ -157,7 +159,7 @@ public function getConfigFactoryStub(array $configs = []) { * A mocked config storage. */ public function getConfigStorageStub(array $configs) { - $config_storage = $this->getMock('Drupal\Core\Config\NullStorage'); + $config_storage = $this->createMock('Drupal\Core\Config\NullStorage'); $config_storage->expects($this->any()) ->method('listAll') ->will($this->returnValue(array_keys($configs))); @@ -211,7 +213,7 @@ protected function getBlockMockWithMachineName($machine_name) { * A mock translation object. */ public function getStringTranslationStub() { - $translation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface'); + $translation = $this->createMock('Drupal\Core\StringTranslation\TranslationInterface'); $translation->expects($this->any()) ->method('translate') ->willReturnCallback(function ($string, array $args = [], array $options = []) use ($translation) { @@ -241,7 +243,7 @@ public function getStringTranslationStub() { * The container with the cache tags invalidator service. */ protected function getContainerWithCacheTagsInvalidator(CacheTagsInvalidatorInterface $cache_tags_validator) { - $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface'); $container->expects($this->any()) ->method('get') ->with('cache_tags.invalidator') @@ -258,7 +260,7 @@ protected function getContainerWithCacheTagsInvalidator(CacheTagsInvalidatorInte * The class resolver stub. */ protected function getClassResolverStub() { - $class_resolver = $this->getMock('Drupal\Core\DependencyInjection\ClassResolverInterface'); + $class_resolver = $this->createMock('Drupal\Core\DependencyInjection\ClassResolverInterface'); $class_resolver->expects($this->any()) ->method('getInstanceFromDefinition') ->will($this->returnCallback(function ($class) { From 89e64c1fe0ef2bf6395453b372546ae58df9b59b Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Wed, 13 Dec 2017 06:55:11 +1000 Subject: [PATCH 029/232] Issue #2928249 by alexpott, mondrake: Introduce a PHPUnit 6+ compatibility layer for Drupal\Tests\Listeners classes --- core/modules/simpletest/src/WebTestBase.php | 4 +- core/phpunit.xml.dist | 9 +- ...tener.php => DeprecationListenerTrait.php} | 12 ++- ...p => DrupalComponentTestListenerTrait.php} | 18 ++-- .../Drupal/Tests/Listeners/DrupalListener.php | 36 ++++++++ ...r.php => DrupalStandardsListenerTrait.php} | 59 ++++++++++--- .../Tests/Listeners/HtmlOutputPrinter.php | 87 +++++++------------ .../Listeners/HtmlOutputPrinterTrait.php | 72 +++++++++++++++ .../Tests/Listeners/Legacy/DrupalListener.php | 29 +++++++ .../Listeners/Legacy/HtmlOutputPrinter.php | 33 +++++++ .../Tests/TestSuites/TestSuiteBaseTest.php | 10 ++- core/tests/TestSuites/TestSuiteBase.php | 10 ++- 12 files changed, 290 insertions(+), 89 deletions(-) rename core/tests/Drupal/Tests/Listeners/{DeprecationListener.php => DeprecationListenerTrait.php} (97%) rename core/tests/Drupal/Tests/Listeners/{DrupalComponentTestListener.php => DrupalComponentTestListenerTrait.php} (50%) create mode 100644 core/tests/Drupal/Tests/Listeners/DrupalListener.php rename core/tests/Drupal/Tests/Listeners/{DrupalStandardsListener.php => DrupalStandardsListenerTrait.php} (78%) create mode 100644 core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php create mode 100644 core/tests/Drupal/Tests/Listeners/Legacy/DrupalListener.php create mode 100644 core/tests/Drupal/Tests/Listeners/Legacy/HtmlOutputPrinter.php diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 4b6f421e5524..441b2398f4b9 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -18,7 +18,7 @@ use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait; use Drupal\Tests\EntityViewTrait; use Drupal\Tests\block\Traits\BlockCreationTrait as BaseBlockCreationTrait; -use Drupal\Tests\Listeners\DeprecationListener; +use Drupal\Tests\Listeners\DeprecationListenerTrait; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\Tests\Traits\Core\CronRunTrait; @@ -698,7 +698,7 @@ protected function curlHeaderCallback($curlHandler, $header) { if ($parameters[1] === 'User deprecated function') { if (getenv('SYMFONY_DEPRECATIONS_HELPER') !== 'disabled') { $message = (string) $parameters[0]; - if (!in_array($message, DeprecationListener::getSkippedDeprecations())) { + if (!in_array($message, DeprecationListenerTrait::getSkippedDeprecations())) { call_user_func_array([&$this, 'error'], $parameters); } } diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index bfe74673b251..750c6e2b502e 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -49,16 +49,11 @@ - + - + - - - - diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListener.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php similarity index 97% rename from core/tests/Drupal/Tests/Listeners/DeprecationListener.php rename to core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index 80b3d31c97e1..c73b6e59df40 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListener.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -9,12 +9,18 @@ * This class will be removed once all the deprecation notices have been * fixed. */ -class DeprecationListener extends \PHPUnit_Framework_BaseTestListener { +trait DeprecationListenerTrait { /** - * {@inheritdoc} + * Reacts to the end of a test. + * + * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test + * The test object that has ended its test run. + * @param float $time + * The time the test took. */ - public function endTest(\PHPUnit_Framework_Test $test, $time) { + protected function deprecationEndTest($test, $time) { + /** @var \PHPUnit\Framework\Test $test */ // Need to edit the file of deprecations. if ($file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) { $deprecations = file_get_contents($file); diff --git a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListener.php b/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php similarity index 50% rename from core/tests/Drupal/Tests/Listeners/DrupalComponentTestListener.php rename to core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php index c3496773057b..70fa23604141 100644 --- a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListener.php +++ b/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php @@ -5,20 +5,28 @@ use Drupal\KernelTests\KernelTestBase;; use Drupal\Tests\BrowserTestBase;; use Drupal\Tests\UnitTestCase; -use PHPUnit\Framework\BaseTestListener; +use PHPUnit\Framework\AssertionFailedError; /** * Ensures that no component tests are extending a core test base class. + * + * @internal */ -class DrupalComponentTestListener extends BaseTestListener { +trait DrupalComponentTestListenerTrait { /** - * {@inheritdoc} + * Reacts to the end of a test. + * + * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test + * The test object that has ended its test run. + * @param float $time + * The time the test took. */ - public function endTest(\PHPUnit_Framework_Test $test, $time) { + protected function componentEndTest($test, $time) { + /** @var \PHPUnit\Framework\Test $test */ if (substr($test->toString(), 0, 22) == 'Drupal\Tests\Component') { if ($test instanceof BrowserTestBase || $test instanceof KernelTestBase || $test instanceof UnitTestCase) { - $error = new \PHPUnit_Framework_AssertionFailedError('Component tests should not extend a core test base class.'); + $error = new AssertionFailedError('Component tests should not extend a core test base class.'); $test->getTestResultObject()->addFailure($test, $error, $time); } } diff --git a/core/tests/Drupal/Tests/Listeners/DrupalListener.php b/core/tests/Drupal/Tests/Listeners/DrupalListener.php new file mode 100644 index 000000000000..9ed976f76c44 --- /dev/null +++ b/core/tests/Drupal/Tests/Listeners/DrupalListener.php @@ -0,0 +1,36 @@ +deprecationEndTest($test, $time); + $this->componentEndTest($test, $time); + $this->standardsEndTest($test, $time); + } + + } +} diff --git a/core/tests/Drupal/Tests/Listeners/DrupalStandardsListener.php b/core/tests/Drupal/Tests/Listeners/DrupalStandardsListenerTrait.php similarity index 78% rename from core/tests/Drupal/Tests/Listeners/DrupalStandardsListener.php rename to core/tests/Drupal/Tests/Listeners/DrupalStandardsListenerTrait.php index fd15f8218bc1..2676c46a4fe6 100644 --- a/core/tests/Drupal/Tests/Listeners/DrupalStandardsListener.php +++ b/core/tests/Drupal/Tests/Listeners/DrupalStandardsListenerTrait.php @@ -2,15 +2,18 @@ namespace Drupal\Tests\Listeners; -use PHPUnit\Framework\BaseTestListener; +use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestSuite; /** * Listens for PHPUnit tests and fails those with invalid coverage annotations. * * Enforces various coding standards within test runs. + * + * @internal */ -class DrupalStandardsListener extends BaseTestListener { +trait DrupalStandardsListenerTrait { /** * Signals a coding standards failure to the user. @@ -21,10 +24,10 @@ class DrupalStandardsListener extends BaseTestListener { * The message to add to the failure notice. The test class name and test * name will be appended to this message automatically. */ - protected function fail(TestCase $test, $message) { + private function fail(TestCase $test, $message) { // Add the report to the test's results. $message .= ': ' . get_class($test) . '::' . $test->getName(); - $fail = new \PHPUnit_Framework_AssertionFailedError($message); + $fail = new AssertionFailedError($message); $result = $test->getTestResultObject(); $result->addFailure($test, $fail, 0); } @@ -38,7 +41,7 @@ protected function fail(TestCase $test, $message) { * @return bool * TRUE if the class exists, FALSE otherwise. */ - protected function classExists($class) { + private function classExists($class) { return class_exists($class, TRUE) || trait_exists($class, TRUE) || interface_exists($class, TRUE); } @@ -50,7 +53,7 @@ protected function classExists($class) { * @param \PHPUnit\Framework\TestCase $test * The test to examine. */ - public function checkValidCoversForTest(TestCase $test) { + private function checkValidCoversForTest(TestCase $test) { // If we're generating a coverage report already, don't do anything here. if ($test->getTestResultObject() && $test->getTestResultObject()->getCollectCodeCoverageInformation()) { return; @@ -141,7 +144,7 @@ public function checkValidCoversForTest(TestCase $test) { } /** - * {@inheritdoc} + * Reacts to the end of a test. * * We must mark this method as belonging to the special legacy group because * it might trigger an E_USER_DEPRECATED error during coverage annotation @@ -151,22 +154,58 @@ public function checkValidCoversForTest(TestCase $test) { * * @group legacy * + * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test + * The test object that has ended its test run. + * @param float $time + * The time the test took. + * * @see http://symfony.com/doc/current/components/phpunit_bridge.html#mark-tests-as-legacy */ - public function endTest(\PHPUnit_Framework_Test $test, $time) { + private function doEndTest($test, $time) { // \PHPUnit_Framework_Test does not have any useful methods of its own for // our purpose, so we have to distinguish between the different known // subclasses. if ($test instanceof TestCase) { $this->checkValidCoversForTest($test); } - elseif ($test instanceof \PHPUnit_Framework_TestSuite) { + elseif ($this->isTestSuite($test)) { foreach ($test->getGroupDetails() as $tests) { foreach ($tests as $test) { - $this->endTest($test, $time); + $this->doEndTest($test, $time); } } } } + /** + * Determine if a test object is a test suite regardless of PHPUnit version. + * + * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test + * The test object to test if it is a test suite. + * + * @return bool + * TRUE if it is a test suite, FALSE if not. + */ + private function isTestSuite($test) { + if (class_exists('\PHPUnit_Framework_TestSuite') && $test instanceof \PHPUnit_Framework_TestSuite) { + return TRUE; + } + if (class_exists('PHPUnit\Framework\TestSuite') && $test instanceof TestSuite) { + return TRUE; + } + return FALSE; + } + + /** + * Reacts to the end of a test. + * + * @param \PHPUnit\Framework\Test|\PHPUnit_Framework_Test $test + * The test object that has ended its test run. + * @param float $time + * The time the test took. + */ + protected function standardsEndTest($test, $time) { + $this->doEndTest($test, $time); + } + } diff --git a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php index ac22072d1636..80219898682f 100644 --- a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php +++ b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php @@ -2,70 +2,41 @@ namespace Drupal\Tests\Listeners; -/** - * Defines a class for providing html output results for functional tests. - */ -class HtmlOutputPrinter extends \PHPUnit_TextUI_ResultPrinter { - +use PHPUnit\Framework\TestResult; +use PHPUnit\TextUI\ResultPrinter; + +if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { + class_alias('Drupal\Tests\Listeners\Legacy\HtmlOutputPrinter', 'Drupal\Tests\Listeners\HtmlOutputPrinter'); + // Using an early return instead of a else does not work when using the + // PHPUnit phar due to some weird PHP behavior (the class gets defined without + // executing the code before it and so the definition is not properly + // conditional). +} +else { /** - * File to write html links to. + * Defines a class for providing html output results for functional tests. * - * @var string + * @internal */ - protected $browserOutputFile; - - /** - * {@inheritdoc} - */ - public function __construct($out, $verbose, $colors, $debug, $numberOfColumns) { - parent::__construct($out, $verbose, $colors, $debug, $numberOfColumns); - if ($html_output_directory = getenv('BROWSERTEST_OUTPUT_DIRECTORY')) { - // Initialize html output debugging. - $html_output_directory = rtrim($html_output_directory, '/'); - - // Check if directory exists. - if (!is_dir($html_output_directory) || !is_writable($html_output_directory)) { - $this->writeWithColor('bg-red, fg-black', "HTML output directory $html_output_directory is not a writable directory."); - } - else { - // Convert to a canonicalized absolute pathname just in case the current - // working directory is changed. - $html_output_directory = realpath($html_output_directory); - $this->browserOutputFile = tempnam($html_output_directory, 'browser_output_'); - if ($this->browserOutputFile) { - touch($this->browserOutputFile); - } - else { - $this->writeWithColor('bg-red, fg-black', "Unable to create a temporary file in $html_output_directory."); - } - } - } - - if ($this->browserOutputFile) { - putenv('BROWSERTEST_OUTPUT_FILE=' . $this->browserOutputFile); - } - else { - // Remove any environment variable. - putenv('BROWSERTEST_OUTPUT_FILE'); + class HtmlOutputPrinter extends ResultPrinter { + use HtmlOutputPrinterTrait; + /** + * {@inheritdoc} + */ + public function __construct($out = NULL, $verbose = FALSE, $colors = self::COLOR_DEFAULT, $debug = FALSE, $numberOfColumns = 80, $reverse = FALSE) { + parent::__construct($out, $verbose, $colors, $debug, $numberOfColumns, $reverse); + + $this->setUpHtmlOutput(); } - } - /** - * {@inheritdoc} - */ - public function printResult(\PHPUnit_Framework_TestResult $result) { - parent::printResult($result); + /** + * {@inheritdoc} + */ + public function printResult(TestResult $result) { + parent::printResult($result); - if ($this->browserOutputFile) { - $contents = file_get_contents($this->browserOutputFile); - if ($contents) { - $this->writeNewLine(); - $this->writeWithColor('bg-yellow, fg-black', 'HTML output was generated'); - $this->write($contents); - } - // No need to keep the file around any more. - unlink($this->browserOutputFile); + $this->printHtmlOutput(); } - } + } } diff --git a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php new file mode 100644 index 000000000000..1dd67eb9e9a3 --- /dev/null +++ b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php @@ -0,0 +1,72 @@ +writeWithColor('bg-red, fg-black', "HTML output directory $html_output_directory is not a writable directory."); + } + else { + // Convert to a canonicalized absolute pathname just in case the current + // working directory is changed. + $html_output_directory = realpath($html_output_directory); + $this->browserOutputFile = tempnam($html_output_directory, 'browser_output_'); + if ($this->browserOutputFile) { + touch($this->browserOutputFile); + } + else { + $this->writeWithColor('bg-red, fg-black', "Unable to create a temporary file in $html_output_directory."); + } + } + } + + if ($this->browserOutputFile) { + putenv('BROWSERTEST_OUTPUT_FILE=' . $this->browserOutputFile); + } + else { + // Remove any environment variable. + putenv('BROWSERTEST_OUTPUT_FILE'); + } + } + + /** + * Prints the list of HTML output generated during the test. + */ + protected function printHtmlOutput() { + if ($this->browserOutputFile) { + $contents = file_get_contents($this->browserOutputFile); + if ($contents) { + $this->writeNewLine(); + $this->writeWithColor('bg-yellow, fg-black', 'HTML output was generated'); + $this->write($contents); + } + // No need to keep the file around any more. + unlink($this->browserOutputFile); + } + } + +} diff --git a/core/tests/Drupal/Tests/Listeners/Legacy/DrupalListener.php b/core/tests/Drupal/Tests/Listeners/Legacy/DrupalListener.php new file mode 100644 index 000000000000..f7c2c76668cb --- /dev/null +++ b/core/tests/Drupal/Tests/Listeners/Legacy/DrupalListener.php @@ -0,0 +1,29 @@ +deprecationEndTest($test, $time); + $this->componentEndTest($test, $time); + $this->standardsEndTest($test, $time); + } + +} diff --git a/core/tests/Drupal/Tests/Listeners/Legacy/HtmlOutputPrinter.php b/core/tests/Drupal/Tests/Listeners/Legacy/HtmlOutputPrinter.php new file mode 100644 index 000000000000..7c1f45e38f90 --- /dev/null +++ b/core/tests/Drupal/Tests/Listeners/Legacy/HtmlOutputPrinter.php @@ -0,0 +1,33 @@ +setUpHtmlOutput(); + } + + /** + * {@inheritdoc} + */ + public function printResult(\PHPUnit_Framework_TestResult $result) { + parent::printResult($result); + + $this->printHtmlOutput(); + } + +} diff --git a/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php b/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php index 4c289e4d78bb..41a98419ee03 100644 --- a/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php +++ b/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php @@ -34,6 +34,13 @@ protected function getFilesystem() { ], 'Tests' => [ 'CoreUnitTest.php' => ' [ + 'Listener.php' => ' [ + 'Listener.php' => 'invokeArgs($stub, [vfsStream::url('root'), $suite_namespace]); // Determine if we loaded the expected test files. - $this->assertNotEmpty($stub->testFiles); - $this->assertEmpty(array_diff_assoc($expected_tests, $stub->testFiles)); + $this->assertEquals($expected_tests, $stub->testFiles); } /** diff --git a/core/tests/TestSuites/TestSuiteBase.php b/core/tests/TestSuites/TestSuiteBase.php index 82a13ba25574..e5925debb901 100644 --- a/core/tests/TestSuites/TestSuiteBase.php +++ b/core/tests/TestSuites/TestSuiteBase.php @@ -3,11 +3,12 @@ namespace Drupal\Tests\TestSuites; use Drupal\simpletest\TestDiscovery; +use PHPUnit\Framework\TestSuite; /** * Base class for Drupal test suites. */ -abstract class TestSuiteBase extends \PHPUnit_Framework_TestSuite { +abstract class TestSuiteBase extends TestSuite { /** * Finds extensions in a Drupal installation. @@ -40,7 +41,12 @@ protected function addTestsBySuiteNamespace($root, $suite_namespace) { // always inside of core/tests/Drupal/${suite_namespace}Tests. The exception // to this is Unit tests for historical reasons. if ($suite_namespace == 'Unit') { - $this->addTestFiles(TestDiscovery::scanDirectory("Drupal\\Tests\\", "$root/core/tests/Drupal/Tests")); + $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)); + }); + $this->addTestFiles($tests); } else { $this->addTestFiles(TestDiscovery::scanDirectory("Drupal\\${suite_namespace}Tests\\", "$root/core/tests/Drupal/${suite_namespace}Tests")); From 8c44f2085ebcdd488aaeace5d6183e80ded1b0b2 Mon Sep 17 00:00:00 2001 From: Francesco Placella Date: Wed, 13 Dec 2017 00:17:41 +0100 Subject: [PATCH 030/232] Issue #2930197 by mondrake, amateescu: EntityDefinitionUpdateTest fails with contrib db driver (again) --- .../Core/Entity/EntityDefinitionUpdateTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index 4842149dacbd..fa55935bbad4 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -465,7 +465,9 @@ public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_ent // Only one row will be created for non-revisionable base fields. $this->assertCount($base_field_revisionable ? 2 : 1, $result); - $this->assertSame([ + // Use assertEquals and not assertSame here to prevent that a different + // sequence of the columns in the table will affect the check. + $this->assertEquals([ 'bundle' => $entity->bundle(), 'deleted' => '1', 'entity_id' => $entity->id(), @@ -477,7 +479,9 @@ public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_ent // Two rows only exist if the base field is revisionable. if ($base_field_revisionable) { - $this->assertSame([ + // Use assertEquals and not assertSame here to prevent that a different + // sequence of the columns in the table will affect the check. + $this->assertEquals([ 'bundle' => $entity->bundle(), 'deleted' => '1', 'entity_id' => $entity->id(), From 4d629c4d811f305dbfebadc3dfdf34cd9630ad1d Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 13 Dec 2017 11:47:56 +0000 Subject: [PATCH 031/232] Issue #2627512 by gambry, jhedstrom, mpdonadio, Jo Fitzgerald, jibran, tedbow, bkosborne, alexpott, vprocessor, xjm: Datetime Views plugins don't support timezones --- core/modules/datetime/datetime.views.inc | 2 + .../src/Plugin/views/argument/Date.php | 31 ++- .../datetime/src/Plugin/views/filter/Date.php | 82 ++++++- .../datetime/src/Plugin/views/sort/Date.php | 32 ++- .../src/Kernel/Views/ArgumentDateTimeTest.php | 25 ++ .../Kernel/Views/DateTimeHandlerTestBase.php | 15 ++ .../tests/src/Kernel/Views/FilterDateTest.php | 231 +++++++++++++----- .../src/Kernel/Views/FilterDateTimeTest.php | 3 + .../Plugin/views/query/DateSqlInterface.php | 62 +++++ .../src/Plugin/views/query/MysqlDateSql.php | 93 +++++++ .../Plugin/views/query/PostgresqlDateSql.php | 93 +++++++ .../Plugin/views/query/QueryPluginBase.php | 38 ++- .../views/src/Plugin/views/query/Sql.php | 186 +++----------- .../src/Plugin/views/query/SqliteDateSql.php | 122 +++++++++ .../src/Plugin/views/query/QueryTest.php | 5 + .../tests/src/Unit/Plugin/query/SqlTest.php | 28 ++- .../Plugin/views/query/MysqlDateSqlTest.php | 97 ++++++++ .../views/query/PostgresqlDateSqlTest.php | 97 ++++++++ .../Plugin/views/query/SqliteDateSqlTest.php | 98 ++++++++ core/modules/views/views.services.yml | 13 + 20 files changed, 1115 insertions(+), 238 deletions(-) create mode 100644 core/modules/views/src/Plugin/views/query/DateSqlInterface.php create mode 100644 core/modules/views/src/Plugin/views/query/MysqlDateSql.php create mode 100644 core/modules/views/src/Plugin/views/query/PostgresqlDateSql.php create mode 100644 core/modules/views/src/Plugin/views/query/SqliteDateSql.php create mode 100644 core/modules/views/tests/src/Unit/Plugin/views/query/MysqlDateSqlTest.php create mode 100644 core/modules/views/tests/src/Unit/Plugin/views/query/PostgresqlDateSqlTest.php create mode 100644 core/modules/views/tests/src/Unit/Plugin/views/query/SqliteDateSqlTest.php diff --git a/core/modules/datetime/datetime.views.inc b/core/modules/datetime/datetime.views.inc index d3b0d18617d0..93d6cd4d3038 100644 --- a/core/modules/datetime/datetime.views.inc +++ b/core/modules/datetime/datetime.views.inc @@ -39,6 +39,8 @@ function datetime_field_views_data(FieldStorageConfigInterface $field_storage) { 'argument' => [ 'field' => $field_storage->getName() . '_value', 'id' => 'datetime_' . $argument_type, + 'entity_type' => $field_storage->getTargetEntityTypeId(), + 'field_name' => $field_storage->getName(), ], 'group' => $group, ]; diff --git a/core/modules/datetime/src/Plugin/views/argument/Date.php b/core/modules/datetime/src/Plugin/views/argument/Date.php index 3e7d461adff5..4b8caa57c6aa 100644 --- a/core/modules/datetime/src/Plugin/views/argument/Date.php +++ b/core/modules/datetime/src/Plugin/views/argument/Date.php @@ -2,6 +2,9 @@ namespace Drupal\datetime\Plugin\views\argument; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; +use Drupal\views\FieldAPIHandlerTrait; use Drupal\views\Plugin\views\argument\Date as NumericDate; /** @@ -22,12 +25,36 @@ */ class Date extends NumericDate { + use FieldAPIHandlerTrait; + + /** + * Determines if the timezone offset is calculated. + * + * @var bool + */ + protected $calculateOffset = TRUE; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $route_match); + + $definition = $this->getFieldStorageDefinition(); + if ($definition->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) { + // Timezone offset calculation is not applicable to dates that are stored + // as date-only. + $this->calculateOffset = FALSE; + } + } + /** * {@inheritdoc} */ public function getDateField() { - // Return the real field, since it is already in string format. - return "$this->tableAlias.$this->realField"; + // Use string date storage/formatting since datetime fields are stored as + // strings rather than UNIX timestamps. + return $this->query->getDateField("$this->tableAlias.$this->realField", TRUE, $this->calculateOffset); } /** diff --git a/core/modules/datetime/src/Plugin/views/filter/Date.php b/core/modules/datetime/src/Plugin/views/filter/Date.php index 378f33fe84c8..14215206c58e 100644 --- a/core/modules/datetime/src/Plugin/views/filter/Date.php +++ b/core/modules/datetime/src/Plugin/views/filter/Date.php @@ -2,6 +2,7 @@ namespace Drupal\datetime\Plugin\views\filter; +use Drupal\Component\Datetime\DateTimePlus; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; @@ -41,6 +42,13 @@ class Date extends NumericDate implements ContainerFactoryPluginInterface { */ protected $dateFormat = DateTimeItemInterface::DATETIME_STORAGE_FORMAT; + /** + * Determines if the timezone offset is calculated. + * + * @var bool + */ + protected $calculateOffset = TRUE; + /** * The request stack used to determin current time. * @@ -67,10 +75,13 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition $this->dateFormatter = $date_formatter; $this->requestStack = $request_stack; - // Date format depends on field storage format. $definition = $this->getFieldStorageDefinition(); if ($definition->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) { + // Date format depends on field storage format. $this->dateFormat = DateTimeItemInterface::DATE_STORAGE_FORMAT; + // Timezone offset calculation is not applicable to dates that are stored + // as date-only. + $this->calculateOffset = FALSE; } } @@ -91,20 +102,23 @@ public static function create(ContainerInterface $container, array $configuratio * Override parent method, which deals with dates as integers. */ protected function opBetween($field) { - $origin = ($this->value['type'] == 'offset') ? $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME') : 0; - $a = intval(strtotime($this->value['min'], $origin)); - $b = intval(strtotime($this->value['max'], $origin)); + $timezone = $this->getTimezone(); + $origin_offset = $this->getOffset($this->value['min'], $timezone); - // Formatting will vary on date storage. + // Although both 'min' and 'max' values are required, default empty 'min' + // value as UNIX timestamp 0. + $min = (!empty($this->value['min'])) ? $this->value['min'] : '@0'; // Convert to ISO format and format for query. UTC timezone is used since // dates are stored in UTC. - $a = $this->query->getDateFormat("'" . $this->dateFormatter->format($a, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE); - $b = $this->query->getDateFormat("'" . $this->dateFormatter->format($b, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE); + $a = new DateTimePlus($min, new \DateTimeZone($timezone)); + $a = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($a->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE); + $b = new DateTimePlus($this->value['max'], new \DateTimeZone($timezone)); + $b = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($b->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE); // This is safe because we are manually scrubbing the values. $operator = strtoupper($this->operator); - $field = $this->query->getDateFormat($field, $this->dateFormat, TRUE); + $field = $this->query->getDateFormat($this->query->getDateField($field, TRUE, $this->calculateOffset), $this->dateFormat, TRUE); $this->query->addWhereExpression($this->options['group'], "$field $operator $a AND $b"); } @@ -112,15 +126,57 @@ protected function opBetween($field) { * Override parent method, which deals with dates as integers. */ protected function opSimple($field) { - $origin = (!empty($this->value['type']) && $this->value['type'] == 'offset') ? $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME') : 0; - $value = intval(strtotime($this->value['value'], $origin)); + $timezone = $this->getTimezone(); + $origin_offset = $this->getOffset($this->value['value'], $timezone); - // Convert to ISO. UTC is used since dates are stored in UTC. - $value = $this->query->getDateFormat("'" . $this->dateFormatter->format($value, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", $this->dateFormat, TRUE); + // Convert to ISO. UTC timezone is used since dates are stored in UTC. + $value = new DateTimePlus($this->value['value'], new \DateTimeZone($timezone)); + $value = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($value->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE); // This is safe because we are manually scrubbing the value. - $field = $this->query->getDateFormat($field, $this->dateFormat, TRUE); + $field = $this->query->getDateFormat($this->query->getDateField($field, TRUE, $this->calculateOffset), $this->dateFormat, TRUE); $this->query->addWhereExpression($this->options['group'], "$field $this->operator $value"); } + /** + * Get the proper time zone to use in computations. + * + * Date-only fields do not have a time zone associated with them, so the + * filter input needs to use UTC for reference. Otherwise, use the time zone + * for the current user. + * + * @return string + * The time zone name. + */ + protected function getTimezone() { + return $this->dateFormat === DateTimeItemInterface::DATE_STORAGE_FORMAT + ? DateTimeItemInterface::STORAGE_TIMEZONE + : drupal_get_user_timezone(); + } + + /** + * Get the proper offset from UTC to use in computations. + * + * @param string $time + * A date/time string compatible with \DateTime. It is used as the + * reference for computing the offset, which can vary based on the time + * zone rules. + * @param string $timezone + * The time zone that $time is in. + * + * @return int + * The computed offset in seconds. + */ + protected function getOffset($time, $timezone) { + // Date-only fields do not have a time zone or offset from UTC associated + // with them. For relative (i.e. 'offset') comparisons, we need to compute + // the user's offset from UTC for use in the query. + $origin_offset = 0; + if ($this->dateFormat === DateTimeItemInterface::DATE_STORAGE_FORMAT && $this->value['type'] === 'offset') { + $origin_offset = $origin_offset + timezone_offset_get(new \DateTimeZone(drupal_get_user_timezone()), new \DateTime($time, new \DateTimeZone($timezone))); + } + + return $origin_offset; + } + } diff --git a/core/modules/datetime/src/Plugin/views/sort/Date.php b/core/modules/datetime/src/Plugin/views/sort/Date.php index 2c8338ad2599..0049e867feb7 100644 --- a/core/modules/datetime/src/Plugin/views/sort/Date.php +++ b/core/modules/datetime/src/Plugin/views/sort/Date.php @@ -2,6 +2,8 @@ namespace Drupal\datetime\Plugin\views\sort; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; +use Drupal\views\FieldAPIHandlerTrait; use Drupal\views\Plugin\views\sort\Date as NumericDate; /** @@ -14,12 +16,38 @@ */ class Date extends NumericDate { + use FieldAPIHandlerTrait; + /** + * Determines if the timezone offset is calculated. + * + * @var bool + */ + protected $calculateOffset = TRUE; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $definition = $this->getFieldStorageDefinition(); + if ($definition->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) { + // Timezone offset calculation is not applicable to dates that are stored + // as date-only. + $this->calculateOffset = FALSE; + } + } + + /** + * {@inheritdoc} + * * Override to account for dates stored as strings. */ public function getDateField() { - // Return the real field, since it is already in string format. - return "$this->tableAlias.$this->realField"; + // Use string date storage/formatting since datetime fields are stored as + // strings rather than UNIX timestamps. + return $this->query->getDateField("$this->tableAlias.$this->realField", TRUE, $this->calculateOffset); } /** diff --git a/core/modules/datetime/tests/src/Kernel/Views/ArgumentDateTimeTest.php b/core/modules/datetime/tests/src/Kernel/Views/ArgumentDateTimeTest.php index 614548395876..8261b27793f8 100644 --- a/core/modules/datetime/tests/src/Kernel/Views/ArgumentDateTimeTest.php +++ b/core/modules/datetime/tests/src/Kernel/Views/ArgumentDateTimeTest.php @@ -28,6 +28,9 @@ protected function setUp($import_test_views = TRUE) { '2000-10-10', '2001-10-10', '2002-01-01', + // Add a date that is the year 2002 in UTC, but 2003 in the site's time + // zone (Australia/Sydney). + '2002-12-31T23:00:00', ]; foreach ($dates as $date) { $node = Node::create([ @@ -64,6 +67,25 @@ public function testDatetimeArgumentYear() { $expected[] = ['nid' => $this->nodes[2]->id()]; $this->assertIdenticalResultset($view, $expected, $this->map); $view->destroy(); + + $view->setDisplay('default'); + $this->executeView($view, ['2003']); + $expected = []; + $expected[] = ['nid' => $this->nodes[3]->id()]; + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + + // Tests different system timezone with the same nodes. + $this->setSiteTimezone('America/Vancouver'); + + $view->setDisplay('default'); + $this->executeView($view, ['2002']); + $expected = []; + // Only the 3rd node is returned here since UTC 2002-01-01T00:00:00 is still + // in 2001 for this user timezone. + $expected[] = ['nid' => $this->nodes[3]->id()]; + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); } /** @@ -87,6 +109,7 @@ public function testDatetimeArgumentMonth() { $this->executeView($view, ['01']); $expected = []; $expected[] = ['nid' => $this->nodes[2]->id()]; + $expected[] = ['nid' => $this->nodes[3]->id()]; $this->assertIdenticalResultset($view, $expected, $this->map); $view->destroy(); } @@ -112,6 +135,7 @@ public function testDatetimeArgumentDay() { $this->executeView($view, ['01']); $expected = []; $expected[] = ['nid' => $this->nodes[2]->id()]; + $expected[] = ['nid' => $this->nodes[3]->id()]; $this->assertIdenticalResultset($view, $expected, $this->map); $view->destroy(); } @@ -157,6 +181,7 @@ public function testDatetimeArgumentWeek() { $this->executeView($view, ['01']); $expected = []; $expected[] = ['nid' => $this->nodes[2]->id()]; + $expected[] = ['nid' => $this->nodes[3]->id()]; $this->assertIdenticalResultset($view, $expected, $this->map); $view->destroy(); } diff --git a/core/modules/datetime/tests/src/Kernel/Views/DateTimeHandlerTestBase.php b/core/modules/datetime/tests/src/Kernel/Views/DateTimeHandlerTestBase.php index f0c9c786144a..20a3542319f8 100644 --- a/core/modules/datetime/tests/src/Kernel/Views/DateTimeHandlerTestBase.php +++ b/core/modules/datetime/tests/src/Kernel/Views/DateTimeHandlerTestBase.php @@ -41,6 +41,7 @@ abstract class DateTimeHandlerTestBase extends ViewsKernelTestBase { protected function setUp($import_test_views = TRUE) { parent::setUp($import_test_views); + $this->installSchema('node', 'node_access'); $this->installEntitySchema('node'); $this->installEntitySchema('user'); @@ -76,4 +77,18 @@ protected function setUp($import_test_views = TRUE) { ViewTestData::createTestViews(get_class($this), ['datetime_test']); } + /** + * Sets the site timezone to a given timezone. + * + * @param string $timezone + * The timezone identifier to set. + */ + protected function setSiteTimezone($timezone) { + // Set an explicit site timezone, and disallow per-user timezones. + $this->config('system.date') + ->set('timezone.user.configurable', 0) + ->set('timezone.default', $timezone) + ->save(); + } + } diff --git a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php index 23f40948e868..f4a6342956a7 100644 --- a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php +++ b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php @@ -2,8 +2,8 @@ namespace Drupal\Tests\datetime\Kernel\Views; +use Drupal\Component\Datetime\DateTimePlus; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; -use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; use Drupal\field\Entity\FieldStorageConfig; use Drupal\node\Entity\Node; use Drupal\views\Views; @@ -21,9 +21,26 @@ class FilterDateTest extends DateTimeHandlerTestBase { public static $testViews = ['test_filter_datetime']; /** - * For offset tests, set to the current time. + * An array of timezone extremes to test. + * + * @var string[] */ - protected static $date; + protected static $timezones = [ + // UTC-12, no DST. + 'Pacific/Kwajalein', + // UTC-11, no DST. + 'Pacific/Midway', + // UTC-7, no DST. + 'America/Phoenix', + // UTC. + 'UTC', + // UTC+5:30, no DST. + 'Asia/Kolkata', + // UTC+12, no DST. + 'Pacific/Funafuti', + // UTC+13, no DST. + 'Pacific/Tongatapu', + ]; /** * {@inheritdoc} @@ -33,30 +50,24 @@ class FilterDateTest extends DateTimeHandlerTestBase { protected function setUp($import_test_views = TRUE) { parent::setUp($import_test_views); - // Set to 'today'. - static::$date = REQUEST_TIME; - // Change field storage to date-only. $storage = FieldStorageConfig::load('node.' . static::$field_name); $storage->setSetting('datetime_type', DateTimeItem::DATETIME_TYPE_DATE); $storage->save(); - $dates = [ - // Tomorrow. - \Drupal::service('date.formatter')->format(static::$date + 86400, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE), - // Today. - \Drupal::service('date.formatter')->format(static::$date, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE), - // Yesterday. - \Drupal::service('date.formatter')->format(static::$date - 86400, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE), - ]; + // Retrieve tomorrow, today and yesterday dates just to create the nodes. + $timestamp = $this->getUTCEquivalentOfUserNowAsTimestamp(); + $dates = $this->getRelativeDateValuesFromTimestamp($timestamp); + // Clean the nodes on setUp. + $this->nodes = []; foreach ($dates as $date) { $node = Node::create([ 'title' => $this->randomMachineName(8), 'type' => 'page', 'field_date' => [ 'value' => $date, - ] + ], ]); $node->save(); $this->nodes[] = $node; @@ -70,48 +81,156 @@ public function testDateOffsets() { $view = Views::getView('test_filter_datetime'); $field = static::$field_name . '_value'; - // Test simple operations. - $view->initHandlers(); - - // A greater than or equal to 'now', should return the 'today' and - // the 'tomorrow' node. - $view->filter[$field]->operator = '>='; - $view->filter[$field]->value['type'] = 'offset'; - $view->filter[$field]->value['value'] = 'now'; - $view->setDisplay('default'); - $this->executeView($view); - $expected_result = [ - ['nid' => $this->nodes[0]->id()], - ['nid' => $this->nodes[1]->id()], - ]; - $this->assertIdenticalResultset($view, $expected_result, $this->map); - $view->destroy(); - - // Only dates in the past. - $view->initHandlers(); - $view->filter[$field]->operator = '<'; - $view->filter[$field]->value['type'] = 'offset'; - $view->filter[$field]->value['value'] = 'now'; - $view->setDisplay('default'); - $this->executeView($view); - $expected_result = [ - ['nid' => $this->nodes[2]->id()], - ]; - $this->assertIdenticalResultset($view, $expected_result, $this->map); - $view->destroy(); - - // Test offset for between operator. Only the 'tomorrow' node should appear. - $view->initHandlers(); - $view->filter[$field]->operator = 'between'; - $view->filter[$field]->value['type'] = 'offset'; - $view->filter[$field]->value['max'] = '+2 days'; - $view->filter[$field]->value['min'] = '+1 day'; - $view->setDisplay('default'); - $this->executeView($view); - $expected_result = [ - ['nid' => $this->nodes[0]->id()], + foreach (static::$timezones as $timezone) { + + $this->setSiteTimezone($timezone); + $timestamp = $this->getUTCEquivalentOfUserNowAsTimestamp(); + $dates = $this->getRelativeDateValuesFromTimestamp($timestamp); + $this->updateNodesDateFieldsValues($dates); + + // Test simple operations. + $view->initHandlers(); + + // A greater than or equal to 'now', should return the 'today' and the + // 'tomorrow' node. + $view->filter[$field]->operator = '>='; + $view->filter[$field]->value['type'] = 'offset'; + $view->filter[$field]->value['value'] = 'now'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = [ + ['nid' => $this->nodes[0]->id()], + ['nid' => $this->nodes[1]->id()], + ]; + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Only dates in the past. + $view->initHandlers(); + $view->filter[$field]->operator = '<'; + $view->filter[$field]->value['type'] = 'offset'; + $view->filter[$field]->value['value'] = 'now'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = [ + ['nid' => $this->nodes[2]->id()], + ]; + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test offset for between operator. Only 'tomorrow' node should appear. + $view->initHandlers(); + $view->filter[$field]->operator = 'between'; + $view->filter[$field]->value['type'] = 'offset'; + $view->filter[$field]->value['max'] = '+2 days'; + $view->filter[$field]->value['min'] = '+1 day'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = [ + ['nid' => $this->nodes[0]->id()], + ]; + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + } + } + + /** + * Test date filter with date-only fields. + */ + public function testDateIs() { + $view = Views::getView('test_filter_datetime'); + $field = static::$field_name . '_value'; + + foreach (static::$timezones as $timezone) { + + $this->setSiteTimezone($timezone); + $timestamp = $this->getUTCEquivalentOfUserNowAsTimestamp(); + $dates = $this->getRelativeDateValuesFromTimestamp($timestamp); + $this->updateNodesDateFieldsValues($dates); + + // Test simple operations. + $view->initHandlers(); + + // Filtering with nodes date-only values (format: Y-m-d) to test UTC + // conversion does NOT change the day. + $view->filter[$field]->operator = '='; + $view->filter[$field]->value['type'] = 'date'; + $view->filter[$field]->value['value'] = $this->nodes[2]->field_date->first()->getValue()['value']; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = [ + ['nid' => $this->nodes[2]->id()], + ]; + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test offset for between operator. Only 'today' and 'tomorrow' nodes + // should appear. + $view->initHandlers(); + $view->filter[$field]->operator = 'between'; + $view->filter[$field]->value['type'] = 'date'; + $view->filter[$field]->value['max'] = $this->nodes[0]->field_date->first()->getValue()['value']; + $view->filter[$field]->value['min'] = $this->nodes[1]->field_date->first()->getValue()['value']; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = [ + ['nid' => $this->nodes[0]->id()], + ['nid' => $this->nodes[1]->id()], + ]; + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + } + } + + /** + * Returns UTC timestamp of user's TZ 'now'. + * + * The date field stores date_only values without conversion, considering them + * already as UTC. This method returns the UTC equivalent of user's 'now' as a + * unix timestamp, so they match using Y-m-d format. + * + * @return int + * Unix timestamp. + */ + protected function getUTCEquivalentOfUserNowAsTimestamp() { + $user_now = new DateTimePlus('now', new \DateTimeZone(drupal_get_user_timezone())); + $utc_equivalent = new DateTimePlus($user_now->format('Y-m-d H:i:s'), new \DateTimeZone(DATETIME_STORAGE_TIMEZONE)); + + return $utc_equivalent->getTimestamp(); + } + + /** + * Returns an array formatted date_only values. + * + * @param int $timestamp + * Unix Timestamp equivalent to user's "now". + * + * @return array + * An array of DATETIME_DATE_STORAGE_FORMAT date values. In order tomorrow, + * today and yesterday. + */ + protected function getRelativeDateValuesFromTimestamp($timestamp) { + return [ + // Tomorrow. + \Drupal::service('date.formatter')->format($timestamp + 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE), + // Today. + \Drupal::service('date.formatter')->format($timestamp, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE), + // Yesterday. + \Drupal::service('date.formatter')->format($timestamp - 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE), ]; - $this->assertIdenticalResultset($view, $expected_result, $this->map); + } + + /** + * Updates tests nodes date fields values. + * + * @param array $dates + * An array of DATETIME_DATE_STORAGE_FORMAT date values. + */ + protected function updateNodesDateFieldsValues(array $dates) { + foreach ($dates as $index => $date) { + $this->nodes[$index]->{static::$field_name}->value = $date; + $this->nodes[$index]->save(); + } } } diff --git a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php index 5282da33ef77..7cb0fa040ab8 100644 --- a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php +++ b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php @@ -40,6 +40,9 @@ protected function setUp($import_test_views = TRUE) { // Set the timezone. date_default_timezone_set(static::$timezone); + $this->config('system.date') + ->set('timezone.default', static::$timezone) + ->save(); // Add some basic test nodes. $dates = [ diff --git a/core/modules/views/src/Plugin/views/query/DateSqlInterface.php b/core/modules/views/src/Plugin/views/query/DateSqlInterface.php new file mode 100644 index 000000000000..caa741939b30 --- /dev/null +++ b/core/modules/views/src/Plugin/views/query/DateSqlInterface.php @@ -0,0 +1,62 @@ + '%Y', + 'y' => '%y', + 'M' => '%b', + 'm' => '%m', + 'n' => '%c', + 'F' => '%M', + 'D' => '%a', + 'd' => '%d', + 'l' => '%W', + 'j' => '%e', + 'W' => '%v', + 'H' => '%H', + 'h' => '%h', + 'i' => '%i', + 's' => '%s', + 'A' => '%p', + ]; + + /** + * Constructs the MySQL-specific date sql class. + * + * @param \Drupal\Core\Database\Connection $database + * The database connection. + */ + public function __construct(Connection $database) { + $this->database = $database; + } + + /** + * {@inheritdoc} + */ + public function getDateField($field, $string_date) { + if ($string_date) { + return $field; + } + + // Base date field storage is timestamp, so the date to be returned here is + // epoch + stored value (seconds from epoch). + return "DATE_ADD('19700101', INTERVAL $field SECOND)"; + } + + /** + * {@inheritdoc} + */ + public function getDateFormat($field, $format) { + $format = strtr($format, static::$replace); + return "DATE_FORMAT($field, '$format')"; + } + + /** + * {@inheritdoc} + */ + public function setTimezoneOffset($offset) { + $this->database->query("SET @@session.time_zone = '$offset'"); + } + + /** + * {@inheritdoc} + */ + public function setFieldTimezoneOffset(&$field, $offset) { + if (!empty($offset)) { + $field = "($field + INTERVAL $offset SECOND)"; + } + } + +} diff --git a/core/modules/views/src/Plugin/views/query/PostgresqlDateSql.php b/core/modules/views/src/Plugin/views/query/PostgresqlDateSql.php new file mode 100644 index 000000000000..c03c416456a8 --- /dev/null +++ b/core/modules/views/src/Plugin/views/query/PostgresqlDateSql.php @@ -0,0 +1,93 @@ + 'YYYY', + 'y' => 'YY', + 'M' => 'Mon', + 'm' => 'MM', + // No format for Numeric representation of a month, without leading zeros. + 'n' => 'MM', + 'F' => 'Month', + 'D' => 'Dy', + 'd' => 'DD', + 'l' => 'Day', + // No format for Day of the month without leading zeros. + 'j' => 'DD', + 'W' => 'IW', + 'H' => 'HH24', + 'h' => 'HH12', + 'i' => 'MI', + 's' => 'SS', + 'A' => 'AM', + ]; + + /** + * Constructs the PostgreSQL-specific date sql class. + * + * @param \Drupal\Core\Database\Connection $database + * The database connection. + */ + public function __construct(Connection $database) { + $this->database = $database; + } + + /** + * {@inheritdoc} + */ + public function getDateField($field, $string_date) { + if ($string_date) { + // Ensures compatibility with field offset operation below. + return "TO_TIMESTAMP($field, 'YYYY-MM-DD HH24:MI:SS')"; + } + return "TO_TIMESTAMP($field)"; + } + + /** + * {@inheritdoc} + */ + public function getDateFormat($field, $format) { + $format = strtr($format, static::$replace); + return "TO_CHAR($field, '$format')"; + } + + /** + * {@inheritdoc} + */ + public function setFieldTimezoneOffset(&$field, $offset) { + $field = "($field + INTERVAL '$offset SECONDS')"; + } + + /** + * {@inheritdoc} + */ + public function setTimezoneOffset($offset) { + $this->database->query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE"); + } + +} diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php index 35453c1172dd..83e0bcfa2403 100644 --- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php +++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php @@ -206,11 +206,17 @@ public function loadEntities(&$results) {} * * @param string $field * The query field that will be used in the expression. + * @param bool $string_date + * For certain databases, date format functions vary depending on string or + * numeric storage. + * @param bool $calculate_offset + * If set to TRUE, the timezone offset will be included in the returned + * field. * * @return string * An expression representing a timestamp with time zone. */ - public function getDateField($field) { + public function getDateField($field, $string_date = FALSE, $calculate_offset = TRUE) { return $field; } @@ -346,6 +352,36 @@ public function getCacheTags() { return []; } + /** + * Applies a timezone offset to the given field. + * + * @param string &$field + * The date field, in string format. + * @param int $offset + * The timezone offset to apply to the field. + */ + public function setFieldTimezoneOffset(&$field, $offset) { + // No-op. Timezone offsets are implementation-specific and should implement + // this method as needed. + } + + /** + * Get the timezone offset in seconds. + * + * @return int + * The offset, in seconds, for the timezone being used. + */ + public function getTimezoneOffset() { + $timezone = $this->setupTimezone(); + $offset = 0; + if ($timezone) { + $dtz = new \DateTimeZone($timezone); + $dt = new \DateTime('now', $dtz); + $offset = $dtz->getOffset($dt); + } + return $offset; + } + } /** diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php index 7df6521b1cbb..2771a3d7c035 100644 --- a/core/modules/views/src/Plugin/views/query/Sql.php +++ b/core/modules/views/src/Plugin/views/query/Sql.php @@ -123,6 +123,13 @@ class Sql extends QueryPluginBase { */ protected $entityTypeManager; + /** + * The database-specific date handler. + * + * @var \Drupal\views\Plugin\views\query\DateSqlInterface + */ + protected $dateSql; + /** * Constructs a Sql object. * @@ -134,11 +141,14 @@ class Sql extends QueryPluginBase { * The plugin implementation definition. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\views\Plugin\views\query\DateSqlInterface $date_sql + * The database-specific date handler. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, DateSqlInterface $date_sql) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->entityTypeManager = $entity_type_manager; + $this->dateSql = $date_sql; } public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { @@ -146,7 +156,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('entity_type.manager') + $container->get('entity_type.manager'), + $container->get('views.date_sql') ); } @@ -1762,175 +1773,40 @@ public function aggregationMethodDistinct($group_type, $field) { /** * {@inheritdoc} */ - public function getDateField($field) { - $db_type = Database::getConnection()->databaseType(); - $offset = $this->setupTimezone(); - if (isset($offset) && !is_numeric($offset)) { - $dtz = new \DateTimeZone($offset); - $dt = new \DateTime('now', $dtz); - $offset_seconds = $dtz->getOffset($dt); - } - - switch ($db_type) { - case 'mysql': - $field = "DATE_ADD('19700101', INTERVAL $field SECOND)"; - if (!empty($offset)) { - $field = "($field + INTERVAL $offset_seconds SECOND)"; - } - break; - case 'pgsql': - $field = "TO_TIMESTAMP($field)"; - if (!empty($offset)) { - $field = "($field + INTERVAL '$offset_seconds SECONDS')"; - } - break; - case 'sqlite': - if (!empty($offset)) { - $field = "($field + $offset_seconds)"; - } - break; + public function getDateField($field, $string_date = FALSE, $calculate_offset = TRUE) { + $field = $this->dateSql->getDateField($field, $string_date); + if ($calculate_offset && $offset = $this->getTimezoneOffset()) { + $this->setFieldTimezoneOffset($field, $offset); } - return $field; } /** * {@inheritdoc} */ - public function setupTimezone() { - $timezone = drupal_get_user_timezone(); - - // set up the database timezone - $db_type = Database::getConnection()->databaseType(); - if (in_array($db_type, ['mysql', 'pgsql'])) { - $offset = '+00:00'; - static $already_set = FALSE; - if (!$already_set) { - if ($db_type == 'pgsql') { - Database::getConnection()->query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE"); - } - elseif ($db_type == 'mysql') { - Database::getConnection()->query("SET @@session.time_zone = '$offset'"); - } + public function setFieldTimezoneOffset(&$field, $offset) { + $this->dateSql->setFieldTimezoneOffset($field, $offset); + } - $already_set = TRUE; - } + /** + * {@inheritdoc} + */ + public function setupTimezone() { + // Set the database timezone offset. + static $already_set = FALSE; + if (!$already_set) { + $this->dateSql->setTimezoneOffset('+00:00'); + $already_set = TRUE; } - return $timezone; + return parent::setupTimezone(); } /** * {@inheritdoc} */ public function getDateFormat($field, $format, $string_date = FALSE) { - $db_type = Database::getConnection()->databaseType(); - switch ($db_type) { - case 'mysql': - $replace = [ - 'Y' => '%Y', - 'y' => '%y', - 'M' => '%b', - 'm' => '%m', - 'n' => '%c', - 'F' => '%M', - 'D' => '%a', - 'd' => '%d', - 'l' => '%W', - 'j' => '%e', - 'W' => '%v', - 'H' => '%H', - 'h' => '%h', - 'i' => '%i', - 's' => '%s', - 'A' => '%p', - ]; - $format = strtr($format, $replace); - return "DATE_FORMAT($field, '$format')"; - case 'pgsql': - $replace = [ - 'Y' => 'YYYY', - 'y' => 'YY', - 'M' => 'Mon', - 'm' => 'MM', - // No format for Numeric representation of a month, without leading - // zeros. - 'n' => 'MM', - 'F' => 'Month', - 'D' => 'Dy', - 'd' => 'DD', - 'l' => 'Day', - // No format for Day of the month without leading zeros. - 'j' => 'DD', - 'W' => 'IW', - 'H' => 'HH24', - 'h' => 'HH12', - 'i' => 'MI', - 's' => 'SS', - 'A' => 'AM', - ]; - $format = strtr($format, $replace); - if (!$string_date) { - return "TO_CHAR($field, '$format')"; - } - // In order to allow for partials (eg, only the year), transform to a - // date, back to a string again. - return "TO_CHAR(TO_TIMESTAMP($field, 'YYYY-MM-DD HH24:MI:SS'), '$format')"; - case 'sqlite': - $replace = [ - 'Y' => '%Y', - // No format for 2 digit year number. - 'y' => '%Y', - // No format for 3 letter month name. - 'M' => '%m', - 'm' => '%m', - // No format for month number without leading zeros. - 'n' => '%m', - // No format for full month name. - 'F' => '%m', - // No format for 3 letter day name. - 'D' => '%d', - 'd' => '%d', - // No format for full day name. - 'l' => '%d', - // no format for day of month number without leading zeros. - 'j' => '%d', - 'W' => '%W', - 'H' => '%H', - // No format for 12 hour hour with leading zeros. - 'h' => '%H', - 'i' => '%M', - 's' => '%S', - // No format for AM/PM. - 'A' => '', - ]; - $format = strtr($format, $replace); - - // Don't use the 'unixepoch' flag for string date comparisons. - $unixepoch = $string_date ? '' : ", 'unixepoch'"; - - // SQLite does not have a ISO week substitution string, so it needs - // special handling. - // @see http://wikipedia.org/wiki/ISO_week_date#Calculation - // @see http://stackoverflow.com/a/15511864/1499564 - if ($format === '%W') { - $expression = "((strftime('%j', date(strftime('%Y-%m-%d', $field" . $unixepoch . "), '-3 days', 'weekday 4')) - 1) / 7 + 1)"; - } - else { - $expression = "strftime('$format', $field" . $unixepoch . ")"; - } - // The expression yields a string, but the comparison value is an - // integer in case the comparison value is a float, integer, or numeric. - // All of the above SQLite format tokens only produce integers. However, - // the given $format may contain 'Y-m-d', which results in a string. - // @see \Drupal\Core\Database\Driver\sqlite\Connection::expandArguments() - // @see http://www.sqlite.org/lang_datefunc.html - // @see http://www.sqlite.org/lang_expr.html#castexpr - if (preg_match('/^(?:%\w)+$/', $format)) { - $expression = "CAST($expression AS NUMERIC)"; - } - return $expression; - } + return $this->dateSql->getDateFormat($field, $format); } } diff --git a/core/modules/views/src/Plugin/views/query/SqliteDateSql.php b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php new file mode 100644 index 000000000000..628e1c68faec --- /dev/null +++ b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php @@ -0,0 +1,122 @@ + '%Y', + // No format for 2 digit year number. + 'y' => '%Y', + // No format for 3 letter month name. + 'M' => '%m', + 'm' => '%m', + // No format for month number without leading zeros. + 'n' => '%m', + // No format for full month name. + 'F' => '%m', + // No format for 3 letter day name. + 'D' => '%d', + 'd' => '%d', + // No format for full day name. + 'l' => '%d', + // no format for day of month number without leading zeros. + 'j' => '%d', + 'W' => '%W', + 'H' => '%H', + // No format for 12 hour hour with leading zeros. + 'h' => '%H', + 'i' => '%M', + 's' => '%S', + // No format for AM/PM. + 'A' => '', + ]; + + /** + * Constructs the SQLite-specific date sql class. + * + * @param \Drupal\Core\Database\Connection $database + * The database connection. + */ + public function __construct(Connection $database) { + $this->database = $database; + } + + /** + * {@inheritdoc} + */ + public function getDateField($field, $string_date) { + if ($string_date) { + $field = "strftime('%s', $field)"; + } + return $field; + } + + /** + * {@inheritdoc} + */ + public function getDateFormat($field, $format) { + $format = strtr($format, static::$replace); + + // SQLite does not have a ISO week substitution string, so it needs special + // handling. + // @see http://wikipedia.org/wiki/ISO_week_date#Calculation + // @see http://stackoverflow.com/a/15511864/1499564 + if ($format === '%W') { + $expression = "((strftime('%j', date(strftime('%Y-%m-%d', $field, 'unixepoch'), '-3 days', 'weekday 4')) - 1) / 7 + 1)"; + } + else { + $expression = "strftime('$format', $field, 'unixepoch')"; + } + // The expression yields a string, but the comparison value is an integer in + // case the comparison value is a float, integer, or numeric. All of the + // above SQLite format tokens only produce integers. However, the given + // $format may contain 'Y-m-d', which results in a string. + // @see \Drupal\Core\Database\Driver\sqlite\Connection::expandArguments() + // @see http://www.sqlite.org/lang_datefunc.html + // @see http://www.sqlite.org/lang_expr.html#castexpr + if (preg_match('/^(?:%\w)+$/', $format)) { + $expression = "CAST($expression AS NUMERIC)"; + } + return $expression; + } + + /** + * {@inheritdoc} + */ + public function setTimezoneOffset($offset) { + // Nothing to do here. + } + + /** + * {@inheritdoc} + */ + public function setFieldTimezoneOffset(&$field, $offset, $string_date = FALSE) { + if (!empty($offset)) { + $field = "($field + $offset)"; + } + } + +} diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php index cf430af25c09..8868f75de667 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php @@ -151,4 +151,9 @@ public function calculateDependencies() { ]; } + /** + * {@inheritdoc} + */ + public function setFieldTimezoneOffset(&$field, $offset) {} + } diff --git a/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php index bae90240f005..80e8a334ef6a 100644 --- a/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityType; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Tests\UnitTestCase; +use Drupal\views\Plugin\views\query\DateSqlInterface; use Drupal\views\Plugin\views\query\Sql; use Drupal\views\Plugin\views\relationship\RelationshipPluginBase; use Drupal\views\ResultRow; @@ -29,8 +30,9 @@ class SqlTest extends UnitTestCase { public function testGetCacheTags() { $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal(); $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; @@ -75,8 +77,9 @@ public function testGetCacheTags() { public function testGetCacheMaxAge() { $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal(); $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $view->result = []; @@ -249,8 +252,9 @@ public function testLoadEntitiesWithEmptyResult() { $view->storage = $view_entity->reveal(); $entity_type_manager = $this->setupEntityTypes(); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; @@ -277,8 +281,9 @@ public function testLoadEntitiesWithNoRelationshipAndNoRevision() { ], ]; $entity_type_manager = $this->setupEntityTypes($entities); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; @@ -340,8 +345,9 @@ public function testLoadEntitiesWithRelationship() { ], ]; $entity_type_manager = $this->setupEntityTypes($entities); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; @@ -394,8 +400,9 @@ public function testLoadEntitiesWithNonEntityRelationship() { ], ]; $entity_type_manager = $this->setupEntityTypes($entities); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; @@ -444,8 +451,9 @@ public function testLoadEntitiesWithRevision() { ], ]; $entity_type_manager = $this->setupEntityTypes([], $entity_revisions); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; @@ -497,8 +505,9 @@ public function testLoadEntitiesWithRevisionOfSameEntityType() { ], ]; $entity_type_manager = $this->setupEntityTypes($entity, $entity_revisions); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; @@ -554,8 +563,9 @@ public function testLoadEntitiesWithRelationshipAndRevision() { ], ]; $entity_type_manager = $this->setupEntityTypes($entities, $entity_revisions); + $date_sql = $this->prophesize(DateSqlInterface::class); - $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal(), $date_sql->reveal()); $query->view = $view; $result = []; diff --git a/core/modules/views/tests/src/Unit/Plugin/views/query/MysqlDateSqlTest.php b/core/modules/views/tests/src/Unit/Plugin/views/query/MysqlDateSqlTest.php new file mode 100644 index 000000000000..575cc58cf690 --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/views/query/MysqlDateSqlTest.php @@ -0,0 +1,97 @@ +database = $this->prophesize(Connection::class)->reveal(); + } + + /** + * Tests the getDateField method. + * + * @covers ::getDateField + */ + public function testGetDateField() { + $date_sql = new MysqlDateSql($this->database); + + $expected = 'foo.field'; + $this->assertEquals($expected, $date_sql->getDateField('foo.field', TRUE)); + + $expected = "DATE_ADD('19700101', INTERVAL foo.field SECOND)"; + $this->assertEquals($expected, $date_sql->getDateField('foo.field', FALSE)); + } + + /** + * Tests date formatting replacement. + * + * @covers ::getDateFormat + * + * @dataProvider providerTestGetDateFormat + */ + public function testGetDateFormat($field, $format, $expected_format) { + $date_sql = new MysqlDateSql($this->database); + + $this->assertEquals("DATE_FORMAT($field, '$expected_format')", $date_sql->getDateFormat($field, $format)); + } + + /** + * Provider for date formatting test. + */ + public function providerTestGetDateFormat() { + return [ + ['foo.field', 'Y-y-M-m', '%Y-%y-%b-%m'], + ['bar.field', 'n-F D d l', '%c-%M %a %d %W'], + ['baz.bar_field', 'j/W/H-h i s A', '%e/%v/%H-%h %i %s %p'], + ]; + } + + /** + * Tests timezone offset formatting. + * + * @covers ::setFieldTimezoneOffset + */ + public function testSetFieldTimezoneOffset() { + $date_sql = new MysqlDateSql($this->database); + + $field = 'foobar.field'; + $date_sql->setFieldTimezoneOffset($field, 42); + $this->assertEquals("(foobar.field + INTERVAL 42 SECOND)", $field); + } + + /** + * Tests setting the database offset. + * + * @covers ::setTimezoneOffset + */ + public function testSetTimezoneOffset() { + $database = $this->prophesize(Connection::class); + $database->query("SET @@session.time_zone = '42'")->shouldBeCalledTimes(1); + $date_sql = new MysqlDateSql($database->reveal()); + $date_sql->setTimezoneOffset(42); + } + +} diff --git a/core/modules/views/tests/src/Unit/Plugin/views/query/PostgresqlDateSqlTest.php b/core/modules/views/tests/src/Unit/Plugin/views/query/PostgresqlDateSqlTest.php new file mode 100644 index 000000000000..14f367d261dc --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/views/query/PostgresqlDateSqlTest.php @@ -0,0 +1,97 @@ +database = $this->prophesize(Connection::class)->reveal(); + } + + /** + * Tests the getDateField method. + * + * @covers ::getDateField + */ + public function testGetDateField() { + $date_sql = new PostgresqlDateSql($this->database); + + $expected = "TO_TIMESTAMP(foo.field, 'YYYY-MM-DD HH24:MI:SS')"; + $this->assertEquals($expected, $date_sql->getDateField('foo.field', TRUE)); + + $expected = 'TO_TIMESTAMP(foo.field)'; + $this->assertEquals($expected, $date_sql->getDateField('foo.field', FALSE)); + } + + /** + * Tests date formatting replacement. + * + * @covers ::getDateFormat + * + * @dataProvider providerTestGetDateFormat + */ + public function testGetDateFormat($field, $format, $expected_format) { + $date_sql = new PostgresqlDateSql($this->database); + + $this->assertEquals("TO_CHAR($field, '$expected_format')", $date_sql->getDateFormat($field, $format)); + } + + /** + * Provider for date formatting test. + */ + public function providerTestGetDateFormat() { + return [ + ['foo.field', 'Y-y-M-m', 'YYYY-YY-Mon-MM'], + ['bar.field', 'n-F D d l', 'MM-Month Dy DD Day'], + ['baz.bar_field', 'j/W/H-h i s A', 'DD/IW/HH24-HH12 MI SS AM'], + ]; + } + + /** + * Tests timezone offset formatting. + * + * @covers ::setFieldTimezoneOffset + */ + public function testSetFieldTimezoneOffset() { + $date_sql = new PostgresqlDateSql($this->database); + + $field = 'foobar.field'; + $date_sql->setFieldTimezoneOffset($field, 42); + $this->assertEquals("(foobar.field + INTERVAL '42 SECONDS')", $field); + } + + /** + * Tests setting the database offset. + * + * @covers ::setTimezoneOffset + */ + public function testSetTimezoneOffset() { + $database = $this->prophesize(Connection::class); + $database->query("SET TIME ZONE INTERVAL '42' HOUR TO MINUTE")->shouldBeCalledTimes(1); + $date_sql = new PostgresqlDateSql($database->reveal()); + $date_sql->setTimezoneOffset(42); + } + +} diff --git a/core/modules/views/tests/src/Unit/Plugin/views/query/SqliteDateSqlTest.php b/core/modules/views/tests/src/Unit/Plugin/views/query/SqliteDateSqlTest.php new file mode 100644 index 000000000000..f29650796c56 --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/views/query/SqliteDateSqlTest.php @@ -0,0 +1,98 @@ +database = $this->prophesize(Connection::class)->reveal(); + } + + /** + * Tests the getDateField method. + * + * @covers ::getDateField + */ + public function testGetDateField() { + $date_sql = new SqliteDateSql($this->database); + + $expected = "strftime('%s', foo.field)"; + $this->assertEquals($expected, $date_sql->getDateField('foo.field', TRUE)); + + $expected = 'foo.field'; + $this->assertEquals($expected, $date_sql->getDateField('foo.field', FALSE)); + } + + /** + * Tests date formatting replacement. + * + * @covers ::getDateFormat + * + * @dataProvider providerTestGetDateFormat + */ + public function testGetDateFormat($field, $format, $expected) { + $date_sql = new SqliteDateSql($this->database); + + $this->assertEquals($expected, $date_sql->getDateFormat($field, $format)); + } + + /** + * Provider for date formatting test. + */ + public function providerTestGetDateFormat() { + return [ + ['foo.field', 'Y-y-M-m', "strftime('%Y-%Y-%m-%m', foo.field, 'unixepoch')"], + ['bar.field', 'n-F D d l', "strftime('%m-%m %d %d %d', bar.field, 'unixepoch')"], + ['baz.bar_field', 'j/W/H-h i s A', "strftime('%d/%W/%H-%H %M %S ', baz.bar_field, 'unixepoch')"], + ['foo.field', 'W', "CAST(((strftime('%j', date(strftime('%Y-%m-%d', foo.field, 'unixepoch'), '-3 days', 'weekday 4')) - 1) / 7 + 1) AS NUMERIC)"] + ]; + } + + /** + * Tests timezone offset formatting. + * + * @covers ::setFieldTimezoneOffset + */ + public function testSetFieldTimezoneOffset() { + $date_sql = new SqliteDateSql($this->database); + + $field = 'foobar.field'; + $date_sql->setFieldTimezoneOffset($field, 42); + $this->assertEquals("(foobar.field + 42)", $field); + } + + /** + * Tests setting the database offset. + * + * @covers ::setTimezoneOffset + */ + public function testSetTimezoneOffset() { + $database = $this->prophesize(Connection::class); + $database->query()->shouldNotBeCalled(); + $date_sql = new SqliteDateSql($database->reveal()); + $date_sql->setTimezoneOffset(42); + } + +} diff --git a/core/modules/views/views.services.yml b/core/modules/views/views.services.yml index 28f8d0d333d3..ffed39460f83 100644 --- a/core/modules/views/views.services.yml +++ b/core/modules/views/views.services.yml @@ -80,3 +80,16 @@ services: arguments: ['@entity.manager'] tags: - { name: 'event_subscriber' } + views.date_sql: + class: Drupal\views\Plugin\views\query\MysqlDateSql + arguments: ['@database'] + tags: + - { name: backend_overridable } + pgsql.views.date_sql: + class: Drupal\views\Plugin\views\query\PostgresqlDateSql + arguments: ['@database'] + public: false + sqlite.views.date_sql: + class: Drupal\views\Plugin\views\query\SqliteDateSql + arguments: ['@database'] + public: false From 0b67173a1f67cc8d87425298ccc1911b9e14c018 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 13 Dec 2017 16:00:18 +0000 Subject: [PATCH 032/232] Issue #2929464 by tedbow, mpdonadio: Tests under "core/modules/ckeditor/tests/modules/src/Kernel" are in the wrong folder and do not get tested --- .../tests/{modules => }/src/Kernel/CKEditorPluginManagerTest.php | 0 .../ckeditor/tests/{modules => }/src/Kernel/CKEditorTest.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename core/modules/ckeditor/tests/{modules => }/src/Kernel/CKEditorPluginManagerTest.php (100%) rename core/modules/ckeditor/tests/{modules => }/src/Kernel/CKEditorTest.php (100%) diff --git a/core/modules/ckeditor/tests/modules/src/Kernel/CKEditorPluginManagerTest.php b/core/modules/ckeditor/tests/src/Kernel/CKEditorPluginManagerTest.php similarity index 100% rename from core/modules/ckeditor/tests/modules/src/Kernel/CKEditorPluginManagerTest.php rename to core/modules/ckeditor/tests/src/Kernel/CKEditorPluginManagerTest.php diff --git a/core/modules/ckeditor/tests/modules/src/Kernel/CKEditorTest.php b/core/modules/ckeditor/tests/src/Kernel/CKEditorTest.php similarity index 100% rename from core/modules/ckeditor/tests/modules/src/Kernel/CKEditorTest.php rename to core/modules/ckeditor/tests/src/Kernel/CKEditorTest.php From 0ee253116f68ba970f8650f837f2c100087399b4 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 14 Dec 2017 11:26:56 +0000 Subject: [PATCH 033/232] Issue #2928778 by plach: Exception when trying to save a new revision after manually setting the original revision ID --- .../Drupal/Core/Entity/ContentEntityBase.php | 6 +++ .../Functional/Entity/EntityRevisionsTest.php | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 5376644c548c..3effe8b3c259 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -745,6 +745,12 @@ public function onChange($name) { elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) { unset($this->translatableEntityKeys[$key][$this->activeLangcode]); } + // If the revision identifier field is being populated with the original + // value, we need to make sure the "new revision" flag is reset + // accordingly. + if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId()) { + $this->newRevision = FALSE; + } } } diff --git a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php index 8b544c4abde3..863f95b332d9 100644 --- a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php +++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php @@ -212,4 +212,50 @@ public function testEntityRevisionParamConverter() { $this->assertNoText('pending revision - en'); } + /** + * Tests manual revert of the revision ID value. + * + * @covers \Drupal\Core\Entity\ContentEntityBase::getRevisionId + * @covers \Drupal\Core\Entity\ContentEntityBase::getLoadedRevisionId + * @covers \Drupal\Core\Entity\ContentEntityBase::setNewRevision + * @covers \Drupal\Core\Entity\ContentEntityBase::isNewRevision + */ + public function testNewRevisionRevert() { + $entity = EntityTestMulRev::create(['name' => 'EntityLoadedRevisionTest']); + $entity->save(); + + // Check that revision ID field is reset while the loaded revision ID is + // preserved when flagging a new revision. + $revision_id = $entity->getRevisionId(); + $entity->setNewRevision(); + $this->assertNull($entity->getRevisionId()); + $this->assertEquals($revision_id, $entity->getLoadedRevisionId()); + $this->assertTrue($entity->isNewRevision()); + + // Check that after manually restoring the original revision ID, the entity + // is stored without creating a new revision. + $key = $entity->getEntityType()->getKey('revision'); + $entity->set($key, $revision_id); + $entity->save(); + $this->assertEquals($revision_id, $entity->getRevisionId()); + $this->assertEquals($revision_id, $entity->getLoadedRevisionId()); + + // Check that manually restoring the original revision ID causes the "new + // revision" state to be reverted. + $entity->setNewRevision(); + $this->assertNull($entity->getRevisionId()); + $this->assertEquals($revision_id, $entity->getLoadedRevisionId()); + $this->assertTrue($entity->isNewRevision()); + $entity->set($key, $revision_id); + $this->assertFalse($entity->isNewRevision()); + $this->assertEquals($revision_id, $entity->getRevisionId()); + $this->assertEquals($revision_id, $entity->getLoadedRevisionId()); + + // Check that flagging a new revision again works correctly. + $entity->setNewRevision(); + $this->assertNull($entity->getRevisionId()); + $this->assertEquals($revision_id, $entity->getLoadedRevisionId()); + $this->assertTrue($entity->isNewRevision()); + } + } From 4a338bf83338c1810c279580d4f608b4bbda5582 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 14 Dec 2017 13:05:53 +0000 Subject: [PATCH 034/232] Issue #2928522 by yo30, dawehner: \Drupal\FunctionalJavascriptTests\WebDriverWebAssert misses some deprecations --- .../WebDriverWebAssert.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/core/tests/Drupal/FunctionalJavascriptTests/WebDriverWebAssert.php b/core/tests/Drupal/FunctionalJavascriptTests/WebDriverWebAssert.php index 16b1281d4d8b..e621c1a0725c 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/WebDriverWebAssert.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/WebDriverWebAssert.php @@ -55,4 +55,56 @@ public function responseHeaderNotEquals($name, $value) { parent::responseHeaderNotEquals($name, $value); } + /** + * The use of responseHeaderContains() is not available. + * + * @param string $name + * The name of the header. + * @param string $value + * The value to check the header against. + */ + public function responseHeaderContains($name, $value) { + @trigger_error('Support for responseHeaderContains is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.'); + parent::responseHeaderContains($name, $value); + } + + /** + * The use of responseHeaderNotContains() is not available. + * + * @param string $name + * The name of the header. + * @param string $value + * The value to check the header against. + */ + public function responseHeaderNotContains($name, $value) { + @trigger_error('Support for responseHeaderNotContains is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.'); + parent::responseHeaderNotContains($name, $value); + } + + /** + * The use of responseHeaderMatches() is not available. + * + * @param string $name + * The name of the header. + * @param string $regex + * The value to check the header against. + */ + public function responseHeaderMatches($name, $regex) { + @trigger_error('Support for responseHeaderMatches is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.'); + parent::responseHeaderMatches($name, $regex); + } + + /** + * The use of responseHeaderNotMatches() is not available. + * + * @param string $name + * The name of the header. + * @param string $regex + * The value to check the header against. + */ + public function responseHeaderNotMatches($name, $regex) { + @trigger_error('Support for responseHeaderNotMatches is to be dropped from Javascript tests. See https://www.drupal.org/node/2857562.'); + parent::responseHeaderNotMatches($name, $regex); + } + } From 6f6abec9641609669544b6cbf43f392d31ad136a Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Fri, 15 Dec 2017 09:56:46 +0000 Subject: [PATCH 035/232] Issue #2894014 by adriancid, Cottser: Additional space in a field setting schema label --- .../block_content/config/optional/views.view.block_content.yml | 2 +- core/modules/comment/config/schema/comment.schema.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/modules/block_content/config/optional/views.view.block_content.yml b/core/modules/block_content/config/optional/views.view.block_content.yml index 20fe0bf4f83f..1be5a0417c12 100644 --- a/core/modules/block_content/config/optional/views.view.block_content.yml +++ b/core/modules/block_content/config/optional/views.view.block_content.yml @@ -445,7 +445,7 @@ display: admin_label: '' empty: true tokenize: false - content: 'There are no custom blocks available. ' + content: 'There are no custom blocks available.' plugin_id: text_custom block_content_listing_empty: admin_label: '' diff --git a/core/modules/comment/config/schema/comment.schema.yml b/core/modules/comment/config/schema/comment.schema.yml index 045e9ad5448e..e4a5b75d4af3 100644 --- a/core/modules/comment/config/schema/comment.schema.yml +++ b/core/modules/comment/config/schema/comment.schema.yml @@ -105,7 +105,7 @@ field.field_settings.comment: label: 'Mode' form_location: type: boolean - label: ' Allow comment title' + label: 'Allow comment title' preview: type: integer label: 'Preview comment' From 8fb5919181c6e426a9dd061c1f211236b41acbd1 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 15 Dec 2017 20:08:48 +1000 Subject: [PATCH 036/232] Issue #2880445 by pjcdawkins, japerry, gargsuchi, q0rban: Config sync should not throw a warning when not being writable --- core/modules/config/config.install | 32 ------------------- .../src/Functional/ConfigInstallWebTest.php | 5 --- 2 files changed, 37 deletions(-) delete mode 100644 core/modules/config/config.install diff --git a/core/modules/config/config.install b/core/modules/config/config.install deleted file mode 100644 index c971ae6b5d22..000000000000 --- a/core/modules/config/config.install +++ /dev/null @@ -1,32 +0,0 @@ - t('Configuration directory: %type', ['%type' => CONFIG_SYNC_DIRECTORY]), - 'description' => t('The directory %directory is not writable.', ['%directory' => $directory]), - 'severity' => REQUIREMENT_WARNING, - ]; - } - return $requirements; -} diff --git a/core/modules/config/tests/src/Functional/ConfigInstallWebTest.php b/core/modules/config/tests/src/Functional/ConfigInstallWebTest.php index 5dc3119dfb8f..5ac93bb8abc1 100644 --- a/core/modules/config/tests/src/Functional/ConfigInstallWebTest.php +++ b/core/modules/config/tests/src/Functional/ConfigInstallWebTest.php @@ -204,11 +204,6 @@ public function testConfigModuleRequirements() { file_unmanaged_delete_recursive($directory); $this->drupalGet('/admin/reports/status'); $this->assertRaw(t('The directory %directory does not exist.', ['%directory' => $directory])); - - file_prepare_directory($directory, FILE_CREATE_DIRECTORY); - \Drupal::service('file_system')->chmod($directory, 0555); - $this->drupalGet('/admin/reports/status'); - $this->assertRaw(t('The directory %directory is not writable.', ['%directory' => $directory])); } } From ee22a47cd2d85212426f31e30961491534d33ebf Mon Sep 17 00:00:00 2001 From: webchick Date: Fri, 15 Dec 2017 11:58:10 -0800 Subject: [PATCH 037/232] Issue #2928702 by Wim Leers, borisson_, tedbow: Make EntityResourceTestBase's field_rest_test_multivalue test field less invasive: omit it from normalizations --- .../tests/modules/rest_test/rest_test.module | 17 +++++++- .../EntityResource/EntityResourceTestBase.php | 42 ++----------------- 2 files changed, 18 insertions(+), 41 deletions(-) diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.module b/core/modules/rest/tests/modules/rest_test/rest_test.module index 7df286370295..8897fb98116b 100644 --- a/core/modules/rest/tests/modules/rest_test/rest_test.module +++ b/core/modules/rest/tests/modules/rest_test/rest_test.module @@ -14,19 +14,32 @@ use Drupal\Core\Access\AccessResult; * Implements hook_entity_field_access(). * * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::setUp() - * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost() */ function rest_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { + // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost() + // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch() if ($field_definition->getName() === 'field_rest_test') { switch ($operation) { case 'view': - // Never ever allow this field to be viewed: this lets EntityResourceTestBase::testGet() test in a "vanilla" way. + // Never ever allow this field to be viewed: this lets + // EntityResourceTestBase::testGet() test in a "vanilla" way. return AccessResult::forbidden(); case 'edit': return AccessResult::forbidden(); } } + // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet() + // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch() + if ($field_definition->getName() === 'field_rest_test_multivalue') { + switch ($operation) { + case 'view': + // Never ever allow this field to be viewed: this lets + // EntityResourceTestBase::testGet() test in a "vanilla" way. + return AccessResult::forbidden(); + } + } + // No opinion. return AccessResult::neutral(); } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 5416d308e2ff..2962ef380ab2 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -451,18 +451,6 @@ public function testGet() { // for the keys with the array order the same (it needs to match with // identical comparison). $expected = $this->getExpectedNormalizedEntity(); - if ($this->entity instanceof FieldableEntityInterface) { - $expected += [ - 'field_rest_test_multivalue' => [ - 0 => [ - 'value' => 'One', - ], - 1 => [ - 'value' => 'Two', - ], - ] - ]; - } static::recursiveKSort($expected); $actual = $this->serializer->decode((string) $response->getBody(), static::$format); static::recursiveKSort($actual); @@ -533,18 +521,6 @@ public function testGet() { // normalized entity's values to strings. This ensures the BC layer for // bc_primitives_as_strings works as expected. $expected = $this->getExpectedNormalizedEntity(); - if ($this->entity instanceof FieldableEntityInterface) { - $expected += [ - 'field_rest_test_multivalue' => [ - 0 => [ - 'value' => 'One', - ], - 1 => [ - 'value' => 'Two', - ], - ] - ]; - } // Config entities are not affected. // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize() $expected = static::castToString($expected); @@ -578,18 +554,6 @@ public function testGet() { // ::formatExpectedTimestampValue() to generate the timestamp value. This // will take into account the above config setting. $expected = $this->getExpectedNormalizedEntity(); - if ($this->entity instanceof FieldableEntityInterface) { - $expected += [ - 'field_rest_test_multivalue' => [ - 0 => [ - 'value' => 'One', - ], - 1 => [ - 'value' => 'Two', - ], - ] - ]; - } // Config entities are not affected. // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize() static::recursiveKSort($expected); @@ -1088,13 +1052,13 @@ public function testPatch() { // Multi-value field: remove item 0. Then item 1 becomes item 0. $normalization_multi_value_tests = $this->getNormalizedPatchEntity(); - $normalization_multi_value_tests['field_rest_test_multivalue'] = $updated_entity_normalization['field_rest_test_multivalue']; + $normalization_multi_value_tests['field_rest_test_multivalue'] = $this->entity->get('field_rest_test_multivalue')->getValue(); $normalization_remove_item = $normalization_multi_value_tests; unset($normalization_remove_item['field_rest_test_multivalue'][0]); $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization_remove_item, static::$format); $response = $this->request('PATCH', $url, $request_options); $this->assertResourceResponse(200, FALSE, $response); - $this->assertSame([0 => ['value' => 'Two']], $this->serializer->decode((string) $response->getBody(), static::$format)['field_rest_test_multivalue']); + $this->assertSame([0 => ['value' => 'Two']], $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test_multivalue')->getValue()); // Multi-value field: add one item before the existing one, and one after. $normalization_add_items = $normalization_multi_value_tests; @@ -1102,7 +1066,7 @@ public function testPatch() { $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization_add_items, static::$format); $response = $this->request('PATCH', $url, $request_options); $this->assertResourceResponse(200, FALSE, $response); - $this->assertSame([0 => ['value' => 'One'], 1 => ['value' => 'Two'], 2 => ['value' => 'Three']], $this->serializer->decode((string) $response->getBody(), static::$format)['field_rest_test_multivalue']); + $this->assertSame([0 => ['value' => 'One'], 1 => ['value' => 'Two'], 2 => ['value' => 'Three']], $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test_multivalue')->getValue()); // BC: rest_update_8203(). $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE); From 66d19eaa71a661a78873a59b73100ebce7cb2d5c Mon Sep 17 00:00:00 2001 From: webchick Date: Fri, 15 Dec 2017 11:59:49 -0800 Subject: [PATCH 038/232] Issue #2927566 by Wim Leers: Unit test EntityReferenceFieldItemNormalizerTest mocks incorrectly --- .../EntityReferenceFieldItemNormalizerTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php index e9df564c5fd5..5cc6467e8b6e 100644 --- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php +++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php @@ -70,7 +70,7 @@ protected function setUp() { $this->serializer = $this->prophesize(Serializer::class); // Set up the serializer to return an entity property. $this->serializer->normalize(Argument::cetera()) - ->willReturn(['value' => 'test']); + ->willReturn('test'); $this->normalizer->setSerializer($this->serializer->reveal()); @@ -131,7 +131,7 @@ public function testNormalize() { $normalized = $this->normalizer->normalize($this->fieldItem->reveal()); $expected = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', 'target_type' => 'test_type', 'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb', 'url' => $test_url, @@ -159,7 +159,7 @@ public function testNormalizeWithNoEntity() { $normalized = $this->normalizer->normalize($this->fieldItem->reveal()); $expected = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', ]; $this->assertSame($expected, $normalized); } @@ -169,7 +169,7 @@ public function testNormalizeWithNoEntity() { */ public function testDenormalizeWithTypeAndUuid() { $data = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', 'target_type' => 'test_type', 'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb', ]; @@ -193,7 +193,7 @@ public function testDenormalizeWithTypeAndUuid() { */ public function testDenormalizeWithUuidWithoutType() { $data = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', 'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb', ]; @@ -218,7 +218,7 @@ public function testDenormalizeWithUuidWithIncorrectType() { $this->setExpectedException(UnexpectedValueException::class, 'The field "field_reference" property "target_type" must be set to "test_type" or omitted.'); $data = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', 'target_type' => 'wrong_type', 'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb', ]; @@ -238,7 +238,7 @@ public function testDenormalizeWithTypeWithIncorrectUuid() { $this->setExpectedException(InvalidArgumentException::class, 'No "test_type" entity found with UUID "unique-but-none-non-existent" for field "field_reference"'); $data = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', 'target_type' => 'test_type', 'target_uuid' => 'unique-but-none-non-existent', ]; @@ -261,7 +261,7 @@ public function testDenormalizeWithEmtpyUuid() { $this->setExpectedException(InvalidArgumentException::class, 'If provided "target_uuid" cannot be empty for field "test_type".'); $data = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', 'target_type' => 'test_type', 'target_uuid' => '', ]; @@ -278,7 +278,7 @@ public function testDenormalizeWithEmtpyUuid() { */ public function testDenormalizeWithId() { $data = [ - 'target_id' => ['value' => 'test'], + 'target_id' => 'test', ]; $this->fieldItem->setValue($data)->shouldBeCalled(); From 5ea62f1d3e218ed31b8e7480caf40891b3b802d2 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 16 Dec 2017 08:36:36 +1000 Subject: [PATCH 039/232] Issue #2876085 by heddn, maxocub, phenaproxima, Jo Fitzgerald, vasi, quietone, yoroy, masipila, larowlan, neclimdul, krystalcode, catch: Before upgrading, audit for potential ID conflicts --- .../migrations/d6_aggregator_feed.yml | 1 + .../migrations/d6_aggregator_item.yml | 1 + .../migrations/d7_aggregator_feed.yml | 1 + .../migrations/d7_aggregator_item.yml | 1 + .../migrations/d6_custom_block.yml | 1 + .../migrations/d7_custom_block.yml | 1 + .../modules/comment/migrations/d6_comment.yml | 1 + .../modules/comment/migrations/d7_comment.yml | 1 + core/modules/file/migrations/d6_file.yml | 1 + core/modules/file/migrations/d7_file.yml | 1 + .../file/migrations/d7_file_private.yml | 1 + .../migrations/d6_menu_links.yml | 1 + .../migrations/d7_menu_links.yml | 1 + .../migrate/src/Audit/AuditException.php | 27 +++ .../modules/migrate/src/Audit/AuditResult.php | 146 +++++++++++++ .../migrate/src/Audit/AuditorInterface.php | 42 ++++ .../migrate/src/Audit/HighestIdInterface.php | 26 +++ core/modules/migrate/src/Audit/IdAuditor.php | 62 ++++++ core/modules/migrate/src/Plugin/Migration.php | 11 + .../src/Plugin/migrate/destination/Entity.php | 4 + .../migrate/destination/EntityContentBase.php | 22 +- .../migrate/destination/EntityRevision.php | 30 +++ .../migrate/src/Plugin/migrate/id_map/Sql.php | 70 ++++++- .../destination/EntityContentBaseTest.php | 19 +- .../Unit/destination/EntityRevisionTest.php | 7 + .../Kernel/d6/MigrateDrupal6AuditIdsTest.php | 196 ++++++++++++++++++ .../Kernel/d7/MigrateDrupal7AuditIdsTest.php | 195 +++++++++++++++++ .../Traits/CreateTestContentEntitiesTrait.php | 131 ++++++++++++ .../src/Form/MigrateUpgradeForm.php | 154 ++++++++++++++ .../src/Functional/MigrateUpgradeTestBase.php | 50 +++-- .../src/Functional/d6/MigrateUpgrade6Test.php | 6 +- .../src/Functional/d7/MigrateUpgrade7Test.php | 4 +- core/modules/node/migrations/d6_node.yml | 1 + .../node/migrations/d6_node_revision.yml | 1 + core/modules/node/migrations/d7_node.yml | 1 + .../node/migrations/d7_node_revision.yml | 1 + .../taxonomy/migrations/d6_taxonomy_term.yml | 1 + .../migrations/d6_term_node_revision.yml | 1 + .../taxonomy/migrations/d7_taxonomy_term.yml | 1 + core/modules/user/migrations/d6_user.yml | 1 + core/modules/user/migrations/d7_user.yml | 1 + .../Plugin/migrate/destination/EntityUser.php | 14 ++ 42 files changed, 1206 insertions(+), 32 deletions(-) create mode 100644 core/modules/migrate/src/Audit/AuditException.php create mode 100644 core/modules/migrate/src/Audit/AuditResult.php create mode 100644 core/modules/migrate/src/Audit/AuditorInterface.php create mode 100644 core/modules/migrate/src/Audit/HighestIdInterface.php create mode 100644 core/modules/migrate/src/Audit/IdAuditor.php create mode 100644 core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php create mode 100644 core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php create mode 100644 core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php diff --git a/core/modules/aggregator/migrations/d6_aggregator_feed.yml b/core/modules/aggregator/migrations/d6_aggregator_feed.yml index cad155374a86..8689d5bcc8aa 100644 --- a/core/modules/aggregator/migrations/d6_aggregator_feed.yml +++ b/core/modules/aggregator/migrations/d6_aggregator_feed.yml @@ -1,5 +1,6 @@ id: d6_aggregator_feed label: Aggregator feeds +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/aggregator/migrations/d6_aggregator_item.yml b/core/modules/aggregator/migrations/d6_aggregator_item.yml index e14dbd60ed59..7c991eb76116 100644 --- a/core/modules/aggregator/migrations/d6_aggregator_item.yml +++ b/core/modules/aggregator/migrations/d6_aggregator_item.yml @@ -1,5 +1,6 @@ id: d6_aggregator_item label: Aggregator items +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/aggregator/migrations/d7_aggregator_feed.yml b/core/modules/aggregator/migrations/d7_aggregator_feed.yml index 5dbeb25eaf40..48eb29de8cf9 100644 --- a/core/modules/aggregator/migrations/d7_aggregator_feed.yml +++ b/core/modules/aggregator/migrations/d7_aggregator_feed.yml @@ -1,5 +1,6 @@ id: d7_aggregator_feed label: Aggregator feeds +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/aggregator/migrations/d7_aggregator_item.yml b/core/modules/aggregator/migrations/d7_aggregator_item.yml index 054ba439f5db..342c5c8cbbc6 100644 --- a/core/modules/aggregator/migrations/d7_aggregator_item.yml +++ b/core/modules/aggregator/migrations/d7_aggregator_item.yml @@ -1,5 +1,6 @@ id: d7_aggregator_item label: Aggregator items +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/block_content/migrations/d6_custom_block.yml b/core/modules/block_content/migrations/d6_custom_block.yml index 55fbcb5c9dc8..071e4de19ac5 100644 --- a/core/modules/block_content/migrations/d6_custom_block.yml +++ b/core/modules/block_content/migrations/d6_custom_block.yml @@ -1,5 +1,6 @@ id: d6_custom_block label: Custom blocks +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/block_content/migrations/d7_custom_block.yml b/core/modules/block_content/migrations/d7_custom_block.yml index ca06cf04f91a..1a9ea1978c48 100644 --- a/core/modules/block_content/migrations/d7_custom_block.yml +++ b/core/modules/block_content/migrations/d7_custom_block.yml @@ -1,5 +1,6 @@ id: d7_custom_block label: Custom blocks +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/comment/migrations/d6_comment.yml b/core/modules/comment/migrations/d6_comment.yml index 161820eeaacb..afab0cb4ca87 100644 --- a/core/modules/comment/migrations/d6_comment.yml +++ b/core/modules/comment/migrations/d6_comment.yml @@ -1,5 +1,6 @@ id: d6_comment label: Comments +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/comment/migrations/d7_comment.yml b/core/modules/comment/migrations/d7_comment.yml index dff4b64ab91d..0837df91b7e2 100644 --- a/core/modules/comment/migrations/d7_comment.yml +++ b/core/modules/comment/migrations/d7_comment.yml @@ -1,5 +1,6 @@ id: d7_comment label: Comments +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/file/migrations/d6_file.yml b/core/modules/file/migrations/d6_file.yml index 6544d7d2f6fa..5b8b67201874 100644 --- a/core/modules/file/migrations/d6_file.yml +++ b/core/modules/file/migrations/d6_file.yml @@ -2,6 +2,7 @@ # migration as an optional dependency. id: d6_file label: Public files +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/file/migrations/d7_file.yml b/core/modules/file/migrations/d7_file.yml index b63f13e9e06f..7e05a28aeb5a 100644 --- a/core/modules/file/migrations/d7_file.yml +++ b/core/modules/file/migrations/d7_file.yml @@ -2,6 +2,7 @@ # migration as an optional dependency. id: d7_file label: Public files +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/file/migrations/d7_file_private.yml b/core/modules/file/migrations/d7_file_private.yml index 197c7010313f..51de5338676a 100644 --- a/core/modules/file/migrations/d7_file_private.yml +++ b/core/modules/file/migrations/d7_file_private.yml @@ -1,5 +1,6 @@ id: d7_file_private label: Private files +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/menu_link_content/migrations/d6_menu_links.yml b/core/modules/menu_link_content/migrations/d6_menu_links.yml index 2c8ad4a45a03..e05efee4a75b 100644 --- a/core/modules/menu_link_content/migrations/d6_menu_links.yml +++ b/core/modules/menu_link_content/migrations/d6_menu_links.yml @@ -1,5 +1,6 @@ id: d6_menu_links label: Menu links +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/menu_link_content/migrations/d7_menu_links.yml b/core/modules/menu_link_content/migrations/d7_menu_links.yml index 200a79204791..81d4eb8530b3 100644 --- a/core/modules/menu_link_content/migrations/d7_menu_links.yml +++ b/core/modules/menu_link_content/migrations/d7_menu_links.yml @@ -1,5 +1,6 @@ id: d7_menu_links label: Menu links +audit: true migration_tags: - Drupal 7 source: diff --git a/core/modules/migrate/src/Audit/AuditException.php b/core/modules/migrate/src/Audit/AuditException.php new file mode 100644 index 000000000000..d2ef2eb5d9aa --- /dev/null +++ b/core/modules/migrate/src/Audit/AuditException.php @@ -0,0 +1,27 @@ +id(), $message); + parent::__construct($message, 0, $previous); + } + +} diff --git a/core/modules/migrate/src/Audit/AuditResult.php b/core/modules/migrate/src/Audit/AuditResult.php new file mode 100644 index 000000000000..d5260775e1d4 --- /dev/null +++ b/core/modules/migrate/src/Audit/AuditResult.php @@ -0,0 +1,146 @@ +migration = $migration; + $this->status = $status; + array_walk($reasons, [$this, 'addReason']); + } + + /** + * Returns the audited migration. + * + * @return \Drupal\migrate\Plugin\MigrationInterface + * The audited migration. + */ + public function getMigration() { + return $this->migration; + } + + /** + * Returns the boolean result of the audit. + * + * @return bool + * The result of the audit. TRUE if the migration passed the audit, FALSE + * otherwise. + */ + public function passed() { + return $this->status; + } + + /** + * Adds a reason why the migration passed or failed the audit. + * + * @param string|object $reason + * The reason to add. Can be a string or a string-castable object. + * + * @return $this + */ + public function addReason($reason) { + array_push($this->reasons, (string) $reason); + return $this; + } + + /** + * Creates a passing audit result for a migration. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The audited migration. + * @param string[] $reasons + * (optional) The reasons why the migration passed the audit. + * + * @return static + */ + public static function pass(MigrationInterface $migration, array $reasons = []) { + return new static($migration, TRUE, $reasons); + } + + /** + * Creates a failing audit result for a migration. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The audited migration. + * @param array $reasons + * (optional) The reasons why the migration failed the audit. + * + * @return static + */ + public static function fail(MigrationInterface $migration, array $reasons = []) { + return new static($migration, FALSE, $reasons); + } + + /** + * Implements \Countable::count() for Twig template compatibility. + * + * @return int + * + * @see \Drupal\Component\Render\MarkupInterface + */ + public function count() { + return count($this->reasons); + } + + /** + * Returns the reasons the migration passed or failed, as a string. + * + * @return string + * + * @see \Drupal\Component\Render\MarkupInterface + */ + public function __toString() { + return implode("\n", $this->reasons); + } + + /** + * Returns the reasons the migration passed or failed, for JSON serialization. + * + * @return string[] + */ + public function jsonSerialize() { + return $this->reasons; + } + +} diff --git a/core/modules/migrate/src/Audit/AuditorInterface.php b/core/modules/migrate/src/Audit/AuditorInterface.php new file mode 100644 index 000000000000..a61b792cfef8 --- /dev/null +++ b/core/modules/migrate/src/Audit/AuditorInterface.php @@ -0,0 +1,42 @@ +getPluginDefinition(); + + // If the migration does not opt into auditing, it passes. + // @todo Use $migration->isAuditable() when + // https://www.drupal.org/project/drupal/issues/2930832 is in. + if (empty($plugin_definition['audit'])) { + return AuditResult::pass($migration); + } + + $interface = HighestIdInterface::class; + + $destination = $migration->getDestinationPlugin(); + if (!$destination instanceof HighestIdInterface) { + throw new AuditException($migration, "Destination does not implement $interface"); + } + + $id_map = $migration->getIdMap(); + if (!$id_map instanceof HighestIdInterface) { + throw new AuditException($migration, "ID map does not implement $interface"); + } + + if ($destination->getHighestId() > $id_map->getHighestId()) { + return AuditResult::fail($migration, [ + $this->t('The destination system contains data which was not created by a migration.'), + ]); + } + return AuditResult::pass($migration); + } + + /** + * {@inheritdoc} + */ + public function auditMultiple(array $migrations) { + $conflicts = []; + + foreach ($migrations as $migration) { + $migration_id = $migration->getPluginId(); + $conflicts[$migration_id] = $this->audit($migration); + } + ksort($conflicts); + return $conflicts; + } + +} diff --git a/core/modules/migrate/src/Plugin/Migration.php b/core/modules/migrate/src/Plugin/Migration.php index 3bbe38ad7425..92f7864d5a80 100644 --- a/core/modules/migrate/src/Plugin/Migration.php +++ b/core/modules/migrate/src/Plugin/Migration.php @@ -154,6 +154,17 @@ class Migration extends PluginBase implements MigrationInterface, RequirementsIn */ protected $migration_tags = []; + /** + * Whether the migration is auditable. + * + * If set to TRUE, the migration's IDs will be audited. This means that, if + * the highest destination ID is greater than the highest source ID, a warning + * will be displayed that entities might be overwritten. + * + * @var bool + */ + protected $audit = FALSE; + /** * These migrations, if run, must be executed before this migration. * diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php index 12b0ed6d31d2..0cebbd07c824 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php @@ -94,6 +94,10 @@ abstract class Entity extends DestinationBase implements ContainerFactoryPluginI * The list of bundles this entity type has. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles) { + $plugin_definition += [ + 'label' => $storage->getEntityType()->getPluralLabel(), + ]; + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); $this->storage = $storage; $this->bundles = $bundles; diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php index cf3ae1dedb79..f8d4caeb4197 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\migrate\Audit\HighestIdInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateException; use Drupal\migrate\Plugin\MigrateIdMapInterface; @@ -18,7 +19,7 @@ /** * The destination class for all content entities lacking a specific class. */ -class EntityContentBase extends Entity { +class EntityContentBase extends Entity implements HighestIdInterface { /** * Entity manager. @@ -111,12 +112,9 @@ protected function save(ContentEntityInterface $entity, array $old_destination_i } /** - * Get whether this destination is for translations. - * - * @return bool - * Whether this destination is for translations. + * {@inheritdoc} */ - protected function isTranslationDestination() { + public function isTranslationDestination() { return !empty($this->configuration['translations']); } @@ -294,4 +292,16 @@ protected function getDefinitionFromEntity($key) { ] + $field_definition->getSettings(); } + /** + * {@inheritdoc} + */ + public function getHighestId() { + $values = $this->storage->getQuery() + ->accessCheck(FALSE) + ->sort($this->getKey('id'), 'DESC') + ->range(0, 1) + ->execute(); + return (int) current($values); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php index b0db4769877c..06c158ae1bd2 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php @@ -3,7 +3,12 @@ namespace Drupal\migrate\Plugin\migrate\destination; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\migrate\MigrateException; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; /** @@ -16,6 +21,16 @@ */ class EntityRevision extends EntityContentBase { + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) { + $plugin_definition += [ + 'label' => new TranslatableMarkup('@entity_type revisions', ['@entity_type' => $storage->getEntityType()->getSingularLabel()]), + ]; + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager); + } + /** * {@inheritdoc} */ @@ -78,4 +93,19 @@ public function getIds() { throw new MigrateException('This entity type does not support revisions.'); } + /** + * {@inheritdoc} + */ + public function getHighestId() { + $values = $this->storage->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->sort($this->getKey('revision'), 'DESC') + ->range(0, 1) + ->execute(); + // The array keys are the revision IDs. + // The array contains only one entry, so we can use key(). + return (int) key($values); + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php index 6bdd51ebf0b3..8bc17f19f502 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\migrate\MigrateMessage; +use Drupal\migrate\Audit\HighestIdInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Event\MigrateIdMapMessageEvent; use Drupal\migrate\MigrateException; @@ -27,7 +28,7 @@ * * @PluginID("sql") */ -class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface { +class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface, HighestIdInterface { /** * Column name of hashed source id values. @@ -152,6 +153,8 @@ class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryP * The configuration for the plugin. * @param \Drupal\migrate\Plugin\MigrationInterface $migration * The migration to do. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher) { parent::__construct($configuration, $plugin_id, $plugin_definition); @@ -925,4 +928,69 @@ public function valid() { return $this->currentRow !== FALSE; } + /** + * Returns the migration plugin manager. + * + * @todo Inject as a dependency in https://www.drupal.org/node/2919158. + * + * @return \Drupal\migrate\Plugin\MigrationPluginManagerInterface + * The migration plugin manager. + */ + protected function getMigrationPluginManager() { + return \Drupal::service('plugin.manager.migration'); + } + + /** + * {@inheritdoc} + */ + public function getHighestId() { + array_filter( + $this->migration->getDestinationPlugin()->getIds(), + function (array $id) { + if ($id['type'] !== 'integer') { + throw new \LogicException('Cannot determine the highest migrated ID without an integer ID column'); + } + } + ); + + // List of mapping tables to look in for the highest ID. + $map_tables = [ + $this->migration->id() => $this->mapTableName(), + ]; + + // If there's a bundle, it means we have a derived migration and we need to + // find all the mapping tables from the related derived migrations. + if ($base_id = substr($this->migration->id(), 0, strpos($this->migration->id(), static::DERIVATIVE_SEPARATOR))) { + $migration_manager = $this->getMigrationPluginManager(); + $migrations = $migration_manager->getDefinitions(); + foreach ($migrations as $migration_id => $migration) { + if ($migration['id'] === $base_id) { + // Get this derived migration's mapping table and add it to the list + // of mapping tables to look in for the highest ID. + $stub = $migration_manager->createInstance($migration_id); + $map_tables[$migration_id] = $stub->getIdMap()->mapTableName(); + } + } + } + + // Get the highest id from the list of map tables. + $ids = [0]; + foreach ($map_tables as $map_table) { + if (!$this->getDatabase()->schema()->tableExists($map_table)) { + break; + } + + $query = $this->getDatabase()->select($map_table, 'map') + ->fields('map', $this->destinationIdFields()) + ->range(0, 1); + foreach (array_values($this->destinationIdFields()) as $order_field) { + $query->orderBy($order_field, 'DESC'); + } + $ids[] = $query->execute()->fetchField(); + } + + // Return the highest of all the mapped IDs. + return (int) max($ids); + } + } diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php index 4fa08fed0a20..9f435235608e 100644 --- a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php +++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php @@ -8,9 +8,9 @@ namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\migrate\MigrateException; @@ -38,6 +38,11 @@ class EntityContentBaseTest extends UnitTestCase { */ protected $storage; + /** + * @var \Drupal\Core\Entity\EntityTypeInterface + */ + protected $entityType; + /** * @var \Drupal\Core\Entity\EntityManagerInterface */ @@ -51,6 +56,11 @@ protected function setUp() { $this->migration = $this->prophesize(MigrationInterface::class); $this->storage = $this->prophesize(EntityStorageInterface::class); + + $this->entityType = $this->prophesize(EntityTypeInterface::class); + $this->entityType->getPluralLabel()->willReturn('wonkiness'); + $this->storage->getEntityType()->willReturn($this->entityType->reveal()); + $this->entityManager = $this->prophesize(EntityManagerInterface::class); } @@ -104,14 +114,11 @@ public function testImportEntityLoadFailure() { */ public function testUntranslatable() { // An entity type without a language. - $entity_type = $this->prophesize(ContentEntityType::class); - $entity_type->getKey('langcode')->willReturn(''); - $entity_type->getKey('id')->willReturn('id'); + $this->entityType->getKey('langcode')->willReturn(''); + $this->entityType->getKey('id')->willReturn('id'); $this->entityManager->getBaseFieldDefinitions('foo') ->willReturn(['id' => BaseFieldDefinitionTest::create('integer')]); - $this->storage->getEntityType()->willReturn($entity_type->reveal()); - $destination = new EntityTestDestination( ['translations' => TRUE], '', diff --git a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php index f68c2bea1fb3..8a6fe9a43dbc 100644 --- a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php +++ b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\destination\EntityRevision as RealEntityRevision; use Drupal\migrate\Row; @@ -48,6 +49,12 @@ protected function setUp() { // Setup mocks to be used when creating a revision destination. $this->migration = $this->prophesize(MigrationInterface::class); $this->storage = $this->prophesize('\Drupal\Core\Entity\EntityStorageInterface'); + + $entity_type = $this->prophesize(EntityTypeInterface::class); + $entity_type->getSingularLabel()->willReturn('crazy'); + $entity_type->getPluralLabel()->willReturn('craziness'); + $this->storage->getEntityType()->willReturn($entity_type->reveal()); + $this->entityManager = $this->prophesize('\Drupal\Core\Entity\EntityManagerInterface'); $this->fieldTypeManager = $this->prophesize('\Drupal\Core\Field\FieldTypePluginManagerInterface'); } diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php new file mode 100644 index 000000000000..a0bb591c4c85 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6AuditIdsTest.php @@ -0,0 +1,196 @@ +coreModuleListDataProvider()); + parent::setUp(); + + // Install required entity schemas. + $this->installEntitySchemas(); + + // Install required schemas. + $this->installSchema('book', ['book']); + $this->installSchema('dblog', ['watchdog']); + $this->installSchema('forum', ['forum_index']); + $this->installSchema('node', ['node_access']); + $this->installSchema('search', ['search_dataset']); + $this->installSchema('tracker', ['tracker_node', 'tracker_user']); + + // Enable content moderation for nodes of type page. + $this->installEntitySchema('content_moderation_state'); + $this->installConfig('content_moderation'); + NodeType::create(['type' => 'page'])->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->save(); + } + + /** + * Tests multiple migrations to the same destination with no ID conflicts. + */ + public function testMultipleMigrationWithoutIdConflicts() { + // Create a node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // 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) + ->fields([ + 'source_ids_hash' => 1, + 'sourceid1' => 1, + 'destid1' => 1, + ]) + ->execute(); + + // Audit the IDs of the d6_node migrations for the page & article node type. + // There should be no conflicts since the highest destination ID should be + // equal to the highest migrated ID, as found in the aggregated mapping + // tables of the two node migrations. + $migrations = [ + $this->getMigration('d6_node:page'), + $this->getMigration('d6_node:article'), + ]; + + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * Tests all migrations with no ID conflicts. + */ + public function testAllMigrationsWithNoIdConflicts() { + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 6'); + + // Audit all Drupal 6 migrations that support it. There should be no + // conflicts since no content has been created. + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * Tests all migrations with ID conflicts. + */ + public function testAllMigrationsWithIdConflicts() { + // Get all Drupal 6 migrations. + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 6'); + + // Create content. + $this->createContent(); + + // Audit the IDs of all migrations. There should be conflicts since content + // has been created. + $conflicts = array_map( + function (AuditResult $result) { + return $result->passed() ? NULL : $result->getMigration()->getBaseId(); + }, + (new IdAuditor())->auditMultiple($migrations) + ); + + $expected = [ + 'd6_aggregator_feed', + 'd6_aggregator_item', + 'd6_comment', + 'd6_custom_block', + 'd6_file', + 'd6_menu_links', + 'd6_node', + 'd6_node_revision', + 'd6_taxonomy_term', + 'd6_term_node_revision', + 'd6_user', + ]; + $this->assertEmpty(array_diff(array_filter($conflicts), $expected)); + } + + /** + * Tests draft revisions ID conflicts. + */ + public function testDraftRevisionIdConflicts() { + // Create a published node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // Create a draft revision. + $node->moderation_state->value = 'draft'; + $node->setNewRevision(TRUE); + $node->save(); + + // 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) + ->fields([ + 'source_ids_hash' => 1, + 'sourceid1' => 1, + 'destid1' => 1, + ]) + ->execute(); + + // Audit the IDs of the d6_node_revision migration. There should be + // conflicts since a draft revision has been created. + /** @var \Drupal\migrate\Audit\AuditResult $result */ + $result = (new IdAuditor())->audit($this->getMigration('d6_node_revision:page')); + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertFalse($result->passed()); + } + + /** + * Tests ID conflicts for inaccessible nodes. + */ + public function testNodeGrantsIdConflicts() { + // Enable the node_test module to restrict access to page nodes. + $this->enableModules(['node_test']); + + // Create a published node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // Audit the IDs of the d6_node migration. There should be conflicts + // even though the new node is not accessible. + /** @var \Drupal\migrate\Audit\AuditResult $result */ + $result = (new IdAuditor())->audit($this->getMigration('d6_node:page')); + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertFalse($result->passed()); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php new file mode 100644 index 000000000000..1d7442383825 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/MigrateDrupal7AuditIdsTest.php @@ -0,0 +1,195 @@ +coreModuleListDataProvider()); + parent::setUp(); + + // Install required entity schemas. + $this->installEntitySchemas(); + + // Install required schemas. + $this->installSchema('book', ['book']); + $this->installSchema('dblog', ['watchdog']); + $this->installSchema('forum', ['forum_index']); + $this->installSchema('node', ['node_access']); + $this->installSchema('search', ['search_dataset']); + $this->installSchema('tracker', ['tracker_node', 'tracker_user']); + + // Enable content moderation for nodes of type page. + $this->installEntitySchema('content_moderation_state'); + $this->installConfig('content_moderation'); + NodeType::create(['type' => 'page'])->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->save(); + } + + /** + * Tests multiple migrations to the same destination with no ID conflicts. + */ + public function testMultipleMigrationWithoutIdConflicts() { + // Create a node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // 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) + ->fields([ + 'source_ids_hash' => 1, + 'sourceid1' => 1, + 'destid1' => 1, + ]) + ->execute(); + + // Audit the IDs of the d7_node migrations for the page & article node type. + // There should be no conflicts since the highest destination ID should be + // equal to the highest migrated ID, as found in the aggregated mapping + // tables of the two node migrations. + $migrations = [ + $this->getMigration('d7_node:page'), + $this->getMigration('d7_node:article'), + ]; + + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * Tests all migrations with no ID conflicts. + */ + public function testAllMigrationsWithNoIdConflicts() { + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 7'); + + // Audit the IDs of all Drupal 7 migrations. There should be no conflicts + // since no content has been created. + $results = (new IdAuditor())->auditMultiple($migrations); + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertTrue($result->passed()); + } + } + + /** + * Tests all migrations with ID conflicts. + */ + public function testAllMigrationsWithIdConflicts() { + $migrations = $this->container + ->get('plugin.manager.migration') + ->createInstancesByTag('Drupal 7'); + + // Create content. + $this->createContent(); + + // Audit the IDs of all Drupal 7 migrations. There should be conflicts since + // content has been created. + $conflicts = array_map( + function (AuditResult $result) { + return $result->passed() ? NULL : $result->getMigration()->getBaseId(); + }, + (new IdAuditor())->auditMultiple($migrations) + ); + + $expected = [ + 'd7_aggregator_feed', + 'd7_aggregator_item', + 'd7_comment', + 'd7_custom_block', + 'd7_file', + 'd7_file_private', + 'd7_menu_links', + 'd7_node', + 'd7_node_revision', + 'd7_taxonomy_term', + 'd7_user', + ]; + $this->assertEmpty(array_diff(array_filter($conflicts), $expected)); + } + + /** + * Tests draft revisions ID conflicts. + */ + public function testDraftRevisionIdConflicts() { + // Create a published node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // Create a draft revision. + $node->moderation_state->value = 'draft'; + $node->setNewRevision(TRUE); + $node->save(); + + // 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) + ->fields([ + 'source_ids_hash' => 1, + 'sourceid1' => 1, + 'destid1' => 1, + ]) + ->execute(); + + // Audit the IDs of the d7_node_revision migration. There should be + // conflicts since a draft revision has been created. + /** @var \Drupal\migrate\Audit\AuditResult $result */ + $result = (new IdAuditor())->audit($this->getMigration('d7_node_revision:page')); + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertFalse($result->passed()); + } + + /** + * Tests ID conflicts for inaccessible nodes. + */ + public function testNodeGrantsIdConflicts() { + // Enable the node_test module to restrict access to page nodes. + $this->enableModules(['node_test']); + + // Create a published node of type page. + $node = Node::create(['type' => 'page', 'title' => 'foo']); + $node->moderation_state->value = 'published'; + $node->save(); + + // Audit the IDs of the d7_node migration. There should be conflicts + // even though the new node is not accessible. + /** @var \Drupal\migrate\Audit\AuditResult $result */ + $result = (new IdAuditor())->audit($this->getMigration('d7_node:page')); + $this->assertInstanceOf(AuditResult::class, $result); + $this->assertFalse($result->passed()); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php new file mode 100644 index 000000000000..db720e8cb786 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php @@ -0,0 +1,131 @@ +installEntitySchema('aggregator_feed'); + $this->installEntitySchema('aggregator_item'); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('file'); + $this->installEntitySchema('menu_link_content'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('user'); + } + + /** + * Create several pieces of generic content. + */ + protected function createContent() { + // 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(); + + // Create a block content. + $block = BlockContent::create([ + 'info' => '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(); + + // Create a file. + $file = File::create([ + 'uri' => 'public://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(); + + // Create a taxonomy term. + $term = Term::create([ + 'name' => 'term', + 'vid' => 'term', + ]); + $term->save(); + + // Create a user. + $user = User::create([ + 'uid' => 2, + 'name' => 'user', + 'mail' => 'user@example.com', + ]); + $user->save(); + } + +} diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index 6bfaf42f1dcb..898a8a5568cd 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -9,6 +9,8 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Url; +use Drupal\migrate\Audit\IdAuditor; +use Drupal\migrate\Plugin\migrate\destination\EntityContentBase; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface; use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch; @@ -124,6 +126,9 @@ public function buildForm(array $form, FormStateInterface $form_state) { case 'credentials': return $this->buildCredentialForm($form, $form_state); + case 'confirm_id_conflicts': + return $this->buildIdConflictForm($form, $form_state); + case 'confirm': return $this->buildConfirmForm($form, $form_state); @@ -457,6 +462,155 @@ public function validateCredentialForm(array &$form, FormStateInterface $form_st */ public function submitCredentialForm(array &$form, FormStateInterface $form_state) { // Indicate the next step is confirmation. + $form_state->set('step', 'confirm_id_conflicts'); + $form_state->setRebuild(); + } + + /** + * Confirmation form for ID conflicts. + * + * @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 array + * The form structure. + */ + public function buildIdConflictForm(array &$form, FormStateInterface $form_state) { + // Check if there are conflicts. If none, just skip this form! + $migration_ids = array_keys($form_state->get('migrations')); + $migrations = $this->pluginManager->createInstances($migration_ids); + + $translated_content_conflicts = $content_conflicts = []; + + $results = (new IdAuditor())->auditMultiple($migrations); + + /** @var \Drupal\migrate\Audit\AuditResult $result */ + foreach ($results as $result) { + $destination = $result->getMigration()->getDestinationPlugin(); + if ($destination instanceof EntityContentBase && $destination->isTranslationDestination()) { + // Translations are not yet supperted by the audit system. For now, we + // only warn the user to be cautious when migrating translated content. + // I18n support should be added in https://www.drupal.org/node/2905759. + $translated_content_conflicts[] = $result; + } + elseif (!$result->passed()) { + $content_conflicts[] = $result; + } + + } + if (empty($content_conflicts) && empty($translated_content_conflicts)) { + $form_state->set('step', 'confirm'); + return $this->buildForm($form, $form_state); + } + + drupal_set_message($this->t('WARNING: Content may be overwritten on your new site.'), 'warning'); + + $form = parent::buildForm($form, $form_state); + $form['actions']['submit']['#submit'] = ['::submitConfirmIdConflictForm']; + $form['actions']['submit']['#value'] = $this->t('I acknowledge I may lose data. Continue anyway.'); + + if ($content_conflicts) { + $form = $this->conflictsForm($form, $form_state, $content_conflicts); + } + if ($translated_content_conflicts) { + $form = $this->i18nWarningForm($form, $form_state, $translated_content_conflicts); + } + return $form; + } + + /** + * Build the markup for conflict warnings. + * + * @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\migrate\Audit\AuditResult[] $conflicts + * The failing audit results. + * + * @return array + * The form structure. + */ + protected function conflictsForm(array &$form, FormStateInterface $form_state, array $conflicts) { + $form['conflicts'] = [ + '#title' => $this->t('There is conflicting content of these types:'), + '#theme' => 'item_list', + '#items' => $this->formatConflicts($conflicts), + ]; + + $form['warning'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t('It looks like you have content on your new site which may be overwritten if you continue to run this upgrade. The upgrade should be performed on a clean Drupal 8 installation. For more information see the upgrade handbook.', [':id-conflicts-handbook' => 'https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#id_conflicts']) . '

', + ]; + + return $form; + } + + /** + * Formats a set of failing audit results as strings. + * + * Each string is the label of the destination plugin of the migration that + * failed the audit, keyed by the destination plugin ID in order to prevent + * duplication. + * + * @param \Drupal\migrate\Audit\AuditResult[] $conflicts + * The failing audit results. + * + * @return string[] + * The formatted audit results. + */ + protected function formatConflicts(array $conflicts) { + $items = []; + + foreach ($conflicts as $conflict) { + $definition = $conflict->getMigration()->getDestinationPlugin()->getPluginDefinition(); + $id = $definition['id']; + $items[$id] = $definition['label']; + } + sort($items, SORT_STRING); + + return $items; + } + + /** + * Build the markup for i18n warnings. + * + * @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\migrate\Audit\AuditResult[] $conflicts + * The failing audit results. + * + * @return array + * The form structure. + */ + protected function i18nWarningForm(array &$form, FormStateInterface $form_state, array $conflicts) { + $form['i18n'] = [ + '#title' => $this->t('There is translated content of these types:'), + '#theme' => 'item_list', + '#items' => $this->formatConflicts($conflicts), + ]; + + $form['i18n_warning'] = [ + '#type' => 'markup', + '#markup' => '

' . $this->t('It looks like you are migrating translated content from your old site. Possible ID conflicts for translations are not automatically detected in the current version of Drupal. Refer to the upgrade handbook for instructions on how to avoid ID conflicts with translated content.', [':id-conflicts-handbook' => 'https://www.drupal.org/docs/8/upgrade/known-issues-when-upgrading-from-drupal-6-or-7-to-drupal-8#id_conflicts']) . '

', + ]; + + return $form; + } + + /** + * Submission handler for the confirmation 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 submitConfirmIdConflictForm(array &$form, FormStateInterface $form_state) { $form_state->set('step', 'confirm'); $form_state->setRebuild(); } 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 d5cc93908a8e..98c05aaef431 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php @@ -6,12 +6,15 @@ use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate_drupal\MigrationConfigurationTrait; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait; /** * Provides a base class for testing migration upgrades in the UI. */ abstract class MigrateUpgradeTestBase extends BrowserTestBase { + use MigrationConfigurationTrait; + use CreateTestContentEntitiesTrait; /** * Use the Standard profile to test help implementations of many core modules. @@ -54,6 +57,9 @@ protected function setUp() { // Log in as user 1. Migrations in the UI can only be performed as user 1. $this->drupalLogin($this->rootUser); + + // Create content. + $this->createContent(); } /** @@ -116,7 +122,8 @@ protected function tearDown() { public function testMigrateUpgrade() { $connection_options = $this->sourceDatabase->getConnectionOptions(); $this->drupalGet('/upgrade'); - $this->assertSession()->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); + $session = $this->assertSession(); + $session->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); $this->drupalPostForm(NULL, [], t('Continue')); $this->assertText('Provide credentials for the database of the Drupal site you want to upgrade.'); @@ -153,37 +160,52 @@ public function testMigrateUpgrade() { $this->assertText('Resolve the issue below to continue the upgrade.'); $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->drupalPostForm(NULL, [], t('I acknowledge I may lose data. Continue anyway.')); $this->assertResponse(200); $this->assertText('Upgrade analysis report'); // Ensure we get errors about missing modules. - $this->assertSession()->pageTextContains(t('Source module not found for migration_provider_no_annotation.')); - $this->assertSession()->pageTextContains(t('Source module not found for migration_provider_test.')); - $this->assertSession()->pageTextContains(t('Destination module not found for migration_provider_test')); + $session->pageTextContains(t('Source module not found for migration_provider_no_annotation.')); + $session->pageTextContains(t('Source module not found for migration_provider_test.')); + $session->pageTextContains(t('Destination module not found for migration_provider_test')); // Uninstall the module causing the missing module error messages. $this->container->get('module_installer')->uninstall(['migration_provider_test'], TRUE); // Restart the upgrade process. $this->drupalGet('/upgrade'); - $this->assertSession()->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); + $session->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal 8.'); $this->drupalPostForm(NULL, [], t('Continue')); - $this->assertSession()->pageTextContains('Provide credentials for the database of the Drupal site you want to upgrade.'); - $this->assertSession()->fieldExists('mysql[host]'); + $session->pageTextContains('Provide credentials for the database of the Drupal site you want to upgrade.'); + $session->fieldExists('mysql[host]'); $this->drupalPostForm(NULL, $edits, t('Review upgrade')); - $this->assertSession()->statusCodeEquals(200); - $this->assertSession()->pageTextContains('Upgrade analysis report'); + $session->pageTextContains('WARNING: Content may be overwritten on your new site.'); + $this->drupalPostForm(NULL, [], t('I acknowledge I may lose data. Continue anyway.')); + $session->statusCodeEquals(200); + $session->pageTextContains('Upgrade analysis report'); // Ensure there are no errors about the missing modules from the test module. - $this->assertSession()->pageTextNotContains(t('Source module not found for migration_provider_no_annotation.')); - $this->assertSession()->pageTextNotContains(t('Source module not found for migration_provider_test.')); - $this->assertSession()->pageTextNotContains(t('Destination module not found for migration_provider_test')); + $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. - $this->assertSession()->pageTextNotContains(t('module not found')); + $session->pageTextNotContains(t('module not found')); // Test the available migration paths. $all_available = $this->getAvailablePaths(); - $session = $this->assertSession(); foreach ($all_available as $available) { $session->elementExists('xpath', "//span[contains(@class, 'checked') and text() = '$available']"); $session->elementNotExists('xpath', "//span[contains(@class, 'warning') and text() = '$available']"); 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 3717606cd846..432b7036d72c 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 @@ -35,7 +35,7 @@ protected function getSourceBasePath() { protected function getEntityCounts() { return [ 'aggregator_item' => 1, - 'aggregator_feed' => 1, + 'aggregator_feed' => 2, 'block' => 35, 'block_content' => 2, 'block_content_type' => 1, @@ -48,7 +48,7 @@ protected function getEntityCounts() { 'editor' => 2, 'field_config' => 84, 'field_storage_config' => 58, - 'file' => 7, + 'file' => 8, 'filter_format' => 7, 'image_style' => 5, 'language_content_settings' => 2, @@ -68,7 +68,7 @@ protected function getEntityCounts() { 'tour' => 4, 'user' => 7, 'user_role' => 6, - 'menu_link_content' => 4, + 'menu_link_content' => 5, 'view' => 16, 'date_format' => 11, 'entity_form_display' => 29, 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 3cfe4bc6d711..608967c75411 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 @@ -39,7 +39,7 @@ protected function getSourceBasePath() { */ protected function getEntityCounts() { return [ - 'aggregator_item' => 10, + 'aggregator_item' => 11, 'aggregator_feed' => 1, 'block' => 25, 'block_content' => 1, @@ -72,7 +72,7 @@ protected function getEntityCounts() { 'tour' => 4, 'user' => 4, 'user_role' => 3, - 'menu_link_content' => 7, + 'menu_link_content' => 8, 'view' => 16, 'date_format' => 11, 'entity_form_display' => 17, diff --git a/core/modules/node/migrations/d6_node.yml b/core/modules/node/migrations/d6_node.yml index 56d0459a8126..84a4bf18720f 100644 --- a/core/modules/node/migrations/d6_node.yml +++ b/core/modules/node/migrations/d6_node.yml @@ -1,5 +1,6 @@ id: d6_node label: Nodes +audit: true migration_tags: - Drupal 6 deriver: Drupal\node\Plugin\migrate\D6NodeDeriver diff --git a/core/modules/node/migrations/d6_node_revision.yml b/core/modules/node/migrations/d6_node_revision.yml index f4ff3011c41b..74a42d430773 100644 --- a/core/modules/node/migrations/d6_node_revision.yml +++ b/core/modules/node/migrations/d6_node_revision.yml @@ -1,5 +1,6 @@ id: d6_node_revision label: Node revisions +audit: true migration_tags: - Drupal 6 deriver: Drupal\node\Plugin\migrate\D6NodeDeriver diff --git a/core/modules/node/migrations/d7_node.yml b/core/modules/node/migrations/d7_node.yml index 359be81cc429..80367971a3a5 100644 --- a/core/modules/node/migrations/d7_node.yml +++ b/core/modules/node/migrations/d7_node.yml @@ -1,5 +1,6 @@ id: d7_node label: Nodes +audit: true migration_tags: - Drupal 7 deriver: Drupal\node\Plugin\migrate\D7NodeDeriver diff --git a/core/modules/node/migrations/d7_node_revision.yml b/core/modules/node/migrations/d7_node_revision.yml index c6081ef11044..18c90b6f49d3 100644 --- a/core/modules/node/migrations/d7_node_revision.yml +++ b/core/modules/node/migrations/d7_node_revision.yml @@ -1,5 +1,6 @@ id: d7_node_revision label: Node revisions +audit: true migration_tags: - Drupal 7 deriver: Drupal\node\Plugin\migrate\D7NodeDeriver diff --git a/core/modules/taxonomy/migrations/d6_taxonomy_term.yml b/core/modules/taxonomy/migrations/d6_taxonomy_term.yml index e3c3e3d3420b..9eafee544cbf 100644 --- a/core/modules/taxonomy/migrations/d6_taxonomy_term.yml +++ b/core/modules/taxonomy/migrations/d6_taxonomy_term.yml @@ -1,5 +1,6 @@ id: d6_taxonomy_term label: Taxonomy terms +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/taxonomy/migrations/d6_term_node_revision.yml b/core/modules/taxonomy/migrations/d6_term_node_revision.yml index 91c8362e63b8..c3ebe3059faf 100644 --- a/core/modules/taxonomy/migrations/d6_term_node_revision.yml +++ b/core/modules/taxonomy/migrations/d6_term_node_revision.yml @@ -1,5 +1,6 @@ id: d6_term_node_revision label: Term/node relationship revisions +audit: true migration_tags: - Drupal 6 deriver: Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver diff --git a/core/modules/taxonomy/migrations/d7_taxonomy_term.yml b/core/modules/taxonomy/migrations/d7_taxonomy_term.yml index 46f9f20427f7..6033d4f875ec 100644 --- a/core/modules/taxonomy/migrations/d7_taxonomy_term.yml +++ b/core/modules/taxonomy/migrations/d7_taxonomy_term.yml @@ -1,5 +1,6 @@ id: d7_taxonomy_term label: Taxonomy terms +audit: true migration_tags: - Drupal 7 deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver diff --git a/core/modules/user/migrations/d6_user.yml b/core/modules/user/migrations/d6_user.yml index d58607b1507c..35d31da875ad 100644 --- a/core/modules/user/migrations/d6_user.yml +++ b/core/modules/user/migrations/d6_user.yml @@ -1,5 +1,6 @@ id: d6_user label: User accounts +audit: true migration_tags: - Drupal 6 source: diff --git a/core/modules/user/migrations/d7_user.yml b/core/modules/user/migrations/d7_user.yml index 54c880587515..ee70db782e11 100644 --- a/core/modules/user/migrations/d7_user.yml +++ b/core/modules/user/migrations/d7_user.yml @@ -1,5 +1,6 @@ id: d7_user label: User accounts +audit: true migration_tags: - Drupal 7 class: Drupal\user\Plugin\migrate\User diff --git a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php index aa74b84a437e..b5317beb8ceb 100644 --- a/core/modules/user/src/Plugin/migrate/destination/EntityUser.php +++ b/core/modules/user/src/Plugin/migrate/destination/EntityUser.php @@ -127,4 +127,18 @@ protected function processStubRow(Row $row) { } } + /** + * {@inheritdoc} + */ + public function getHighestId() { + $highest_id = parent::getHighestId(); + + // Every Drupal site must have a user with UID of 1 and it's normal for + // migrations to overwrite this user. + if ($highest_id === 1) { + return 0; + } + return $highest_id; + } + } From 40856ccd26233c2ad5345c623d55f44f149784b9 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 16 Dec 2017 08:41:56 +1000 Subject: [PATCH 040/232] Issue #2760167 by kim.pepper, markcarver, jibran, dawehner, znerol, Wim Leers, larowlan, xjm, tim.plunkett: Add \Drupal\Core\Messenger\Messenger --- core/core.services.yml | 4 +- core/includes/bootstrap.inc | 23 +- core/lib/Drupal.php | 34 ++- .../Drupal/Core/Messenger/LegacyMessenger.php | 260 ++++++++---------- core/lib/Drupal/Core/Messenger/Messenger.php | 112 ++++++++ .../Core/Messenger/MessengerInterface.php | 9 +- .../src/Controller/SystemTestController.php | 24 +- .../Core/Common/DrupalSetMessageTest.php | 6 - .../Core/Messenger/LegacyMessengerTest.php | 84 ++++++ .../Listeners/DeprecationListenerTrait.php | 3 + 10 files changed, 394 insertions(+), 165 deletions(-) create mode 100644 core/lib/Drupal/Core/Messenger/Messenger.php create mode 100644 core/tests/Drupal/KernelTests/Core/Messenger/LegacyMessengerTest.php diff --git a/core/core.services.yml b/core/core.services.yml index 459503e44ceb..49b27089b52d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1646,5 +1646,5 @@ services: tags: - { name: event_subscriber } messenger: - class: Drupal\Core\Messenger\LegacyMessenger - arguments: ['@page_cache_kill_switch'] + class: Drupal\Core\Messenger\Messenger + arguments: ['@session.flash_bag', '@page_cache_kill_switch'] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index d05298d4bf44..ae2f8915c3d4 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -466,11 +466,17 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia * * @see drupal_get_messages() * @see status-messages.html.twig + * @see https://www.drupal.org/node/2774931 + * + * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead. */ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) { - /* @var \Drupal\Core\Messenger\MessengerInterface $messenger */ - $messenger = \Drupal::service('messenger'); - $messenger->addMessage($message, $type, $repeat); + @trigger_error('drupal_set_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead. See https://www.drupal.org/node/2774931', E_USER_DEPRECATED); + $messenger = \Drupal::messenger(); + if (isset($message)) { + $messenger->addMessage($message, $type, $repeat); + } return $messenger->all(); } @@ -498,11 +504,16 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) * * @see drupal_set_message() * @see status-messages.html.twig + * @see https://www.drupal.org/node/2774931 + * + * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Messenger\MessengerInterface::all() or + * \Drupal\Core\Messenger\MessengerInterface::messagesByType() instead. */ function drupal_get_messages($type = NULL, $clear_queue = TRUE) { - /** @var \Drupal\Core\Messenger\MessengerInterface $messenger */ - $messenger = \Drupal::hasService('messenger') ? \Drupal::service('messenger') : NULL; - if ($messenger && ($messages = $messenger->all())) { + @trigger_error('drupal_get_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::all() or \Drupal\Core\Messenger\MessengerInterface::messagesByType() instead. See https://www.drupal.org/node/2774931', E_USER_DEPRECATED); + $messenger = \Drupal::messenger(); + if ($messages = $messenger->all()) { if ($type) { if ($clear_queue) { $messenger->deleteByType($type); diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 07dc12f1f314..8b98ccd1dc71 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -6,8 +6,9 @@ */ use Drupal\Core\DependencyInjection\ContainerNotInitializedException; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Messenger\LegacyMessenger; use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Static Service Container wrapper. @@ -100,6 +101,22 @@ class Drupal { */ protected static $container; + /** + * The LegacyMessenger instance. + * + * Note: this is merely used to ensure that the instance survives when + * \Drupal::messenger() is invoked. It is required to ensure that messages + * are properly transferred to the Messenger service once the container has + * been initialized. Do not store the Messenger service here. + * + * @todo Remove once LegacyMessenger has been removed before 9.0.0. + * + * @see https://www.drupal.org/node/2928994 + * + * @var \Drupal\Core\Messenger\LegacyMessenger|null + */ + protected static $legacyMessenger; + /** * Sets a new global container. * @@ -757,4 +774,19 @@ public static function time() { return static::getContainer()->get('datetime.time'); } + /** + * Returns the messenger. + * + * @return \Drupal\Core\Messenger\MessengerInterface + * The messenger. + */ + public static function messenger() { + // @todo Replace with service once LegacyMessenger is removed in 9.0.0. + // @see https://www.drupal.org/node/2928994 + if (!isset(static::$legacyMessenger)) { + static::$legacyMessenger = new LegacyMessenger(); + } + return static::$legacyMessenger; + } + } diff --git a/core/lib/Drupal/Core/Messenger/LegacyMessenger.php b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php index 8c9751f3d820..bdc66ffe39bc 100644 --- a/core/lib/Drupal/Core/Messenger/LegacyMessenger.php +++ b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php @@ -3,38 +3,64 @@ namespace Drupal\Core\Messenger; use Drupal\Component\Render\MarkupInterface; -use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; use Drupal\Core\Render\Markup; /** - * A legacy implementation of the messenger interface. + * Provides a LegacyMessenger implementation. * - * @internal + * This implementation is for handling messages in a backwards compatible way + * using core's previous $_SESSION storage method. + * + * You should not instantiate a new instance of this class directly. Instead, + * you should inject the "messenger" service into your own services or use + * \Drupal::messenger() in procedural functions. + * + * @see https://www.drupal.org/node/2774931 + * @see https://www.drupal.org/node/2928994 + * + * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. + * Use \Drupal\Core\Messenger\Messenger instead. */ class LegacyMessenger implements MessengerInterface { /** - * The page cache kill switch. + * The messages. * - * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch + * @var array */ - protected $killSwitch; + protected $messages; /** - * LegacyMessenger constructor. - * - * @param \Drupal\Core\PageCache\ResponsePolicy\KillSwitch $killSwitch - * (optional) The page cache kill switch. + * {@inheritdoc} */ - public function __construct(KillSwitch $killSwitch) { - $this->killSwitch = $killSwitch; + public function addError($message, $repeat = FALSE) { + return $this->addMessage($message, static::TYPE_ERROR); } /** * {@inheritdoc} */ public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) { - $this->setMessage($message, $type, $repeat); + // Proxy to the Messenger service, if it exists. + if ($messenger = $this->getMessengerService()) { + return $messenger->addMessage($message, $type, $repeat); + } + + if (!isset($this->messages[$type])) { + $this->messages[$type] = []; + } + + if (!($message instanceof Markup) && $message instanceof MarkupInterface) { + $message = Markup::create((string) $message); + } + + // Do not use strict type checking so that equivalent string and + // MarkupInterface objects are detected. + if ($repeat || !in_array($message, $this->messages[$type])) { + $this->messages[$type][] = $message; + } + + return $this; } /** @@ -44,13 +70,6 @@ public function addStatus($message, $repeat = FALSE) { return $this->addMessage($message, static::TYPE_STATUS); } - /** - * {@inheritdoc} - */ - public function addError($message, $repeat = FALSE) { - return $this->addMessage($message, static::TYPE_ERROR); - } - /** * {@inheritdoc} */ @@ -62,151 +81,104 @@ public function addWarning($message, $repeat = FALSE) { * {@inheritdoc} */ public function all() { - return $this->getMessages(NULL, FALSE); - } + // Proxy to the Messenger service, if it exists. + if ($messenger = $this->getMessengerService()) { + return $messenger->all(); + } - /** - * {@inheritdoc} - */ - public function messagesByType($type) { - return $this->getMessages($type, FALSE); + return $this->messages; } /** - * {@inheritdoc} + * Returns the Messenger service. + * + * @return \Drupal\Core\Messenger\MessengerInterface|null + * The Messenger service. */ - public function deleteAll() { - return $this->getMessages(NULL, TRUE); + protected function getMessengerService() { + // Use the Messenger service, if it exists. + if (\Drupal::hasService('messenger')) { + // Note: because the container has the potential to be rebuilt during + // requests, this service cannot be directly stored on this class. + $messenger = \Drupal::service('messenger'); + + // Transfer any messages into the service. + if (isset($this->messages)) { + foreach ($this->messages as $type => $messages) { + foreach ($messages as $message) { + $messenger->addMessage($message, $type); + } + } + unset($this->messages); + } + + return $messenger; + } + + // Otherwise, trigger an error. + @trigger_error('Adding or retrieving messages prior to the container being initialized was deprecated in Drupal 8.5.0 and this functionality will be removed before Drupal 9.0.0. Please report this usage at https://www.drupal.org/node/2928994.', E_USER_DEPRECATED); + + // Prematurely creating $_SESSION['messages'] in this class' constructor + // causes issues when the container attempts to initialize its own session + // later down the road. This can only be done after it has been determined + // the Messenger service is not available (i.e. no container). It is also + // reasonable to assume that if the container becomes available in a + // subsequent request, a new instance of this class will be created and + // this code will never be reached. This is merely for BC purposes. + if (!isset($this->messages)) { + // A "session" was already created, perhaps to simply allow usage of + // the previous method core used to store messages, use it. + if (isset($_SESSION)) { + if (!isset($_SESSION['messages'])) { + $_SESSION['messages'] = []; + } + $this->messages = &$_SESSION['messages']; + } + // Otherwise, just set an empty array. + else { + $this->messages = []; + } + } } /** * {@inheritdoc} */ - public function deleteByType($type) { - return $this->getMessages($type, TRUE); + public function messagesByType($type) { + // Proxy to the Messenger service, if it exists. + if ($messenger = $this->getMessengerService()) { + return $messenger->messagesByType($type); + } + + return $this->messages[$type]; } /** - * Sets a message to display to the user. - * - * Messages are stored in a session variable and displayed in the page template - * via the $messages theme variable. - * - * Example usage: - * @code - * drupal_set_message(t('An error occurred and processing did not complete.'), 'error'); - * @endcode - * - * @param string|\Drupal\Component\Render\MarkupInterface $message - * (optional) The translated message to be displayed to the user. For - * consistency with other messages, it should begin with a capital letter and - * end with a period. - * @param string $type - * (optional) The message's type. Defaults to 'status'. These values are - * supported: - * - 'status' - * - 'warning' - * - 'error' - * @param bool $repeat - * (optional) If this is FALSE and the message is already set, then the - * message won't be repeated. Defaults to FALSE. - * - * @return array|null - * A multidimensional array with keys corresponding to the set message types. - * The indexed array values of each contain the set messages for that type, - * and each message is an associative array with the following format: - * - safe: Boolean indicating whether the message string has been marked as - * safe. Non-safe strings will be escaped automatically. - * - message: The message string. - * So, the following is an example of the full return array structure: - * @code - * array( - * 'status' => array( - * array( - * 'safe' => TRUE, - * 'message' => 'A safe markup string.', - * ), - * array( - * 'safe' => FALSE, - * 'message' => "$arbitrary_user_input to escape.", - * ), - * ), - * ); - * @endcode - * If there are no messages set, the function returns NULL. - * - * @internal + * {@inheritdoc} */ - private function setMessage($message = NULL, $type = 'status', $repeat = FALSE) { - if (isset($message)) { - if (!isset($_SESSION['messages'][$type])) { - $_SESSION['messages'][$type] = []; - } - - // Convert strings which are safe to the simplest Markup objects. - if (!($message instanceof Markup) && $message instanceof MarkupInterface) { - $message = Markup::create((string) $message); - } - - // Do not use strict type checking so that equivalent string and - // MarkupInterface objects are detected. - if ($repeat || !in_array($message, $_SESSION['messages'][$type])) { - $_SESSION['messages'][$type][] = $message; - } - - // Mark this page as being uncacheable. - $this->killSwitch->trigger(); + public function deleteAll() { + // Proxy to the Messenger service, if it exists. + if ($messenger = $this->getMessengerService()) { + return $messenger->deleteAll(); } - // Messages not set when DB connection fails. - return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL; + $messages = $this->messages; + unset($this->messages); + return $messages; } /** - * Returns all messages that have been set with drupal_set_message(). - * - * @param string $type - * (optional) Limit the messages returned by type. Defaults to NULL, meaning - * all types. These values are supported: - * - NULL - * - 'status' - * - 'warning' - * - 'error' - * @param bool $clear_queue - * (optional) If this is TRUE, the queue will be cleared of messages of the - * type specified in the $type parameter. Otherwise the queue will be left - * intact. Defaults to TRUE. - * - * @return array - * An associative, nested array of messages grouped by message type, with - * the top-level keys as the message type. The messages returned are - * limited to the type specified in the $type parameter, if any. If there - * are no messages of the specified type, an empty array is returned. See - * drupal_set_message() for the array structure of individual messages. - * - * @see drupal_set_message() - * @see status-messages.html.twig - * - * @internal + * {@inheritdoc} */ - private function getMessages($type = NULL, $clear_queue = TRUE) { - if ($messages = $this->setMessage()) { - if ($type) { - if ($clear_queue) { - unset($_SESSION['messages'][$type]); - } - if (isset($messages[$type])) { - return [$type => $messages[$type]]; - } - } - else { - if ($clear_queue) { - unset($_SESSION['messages']); - } - return $messages; - } + public function deleteByType($type) { + // Proxy to the Messenger service, if it exists. + if ($messenger = $this->getMessengerService()) { + return $messenger->messagesByType($type); } - return []; + + $messages = $this->messages[$type]; + unset($this->messages[$type]); + return $messages; } } diff --git a/core/lib/Drupal/Core/Messenger/Messenger.php b/core/lib/Drupal/Core/Messenger/Messenger.php new file mode 100644 index 000000000000..b8b6c3ddf369 --- /dev/null +++ b/core/lib/Drupal/Core/Messenger/Messenger.php @@ -0,0 +1,112 @@ +flashBag = $flash_bag; + $this->killSwitch = $killSwitch; + } + + /** + * {@inheritdoc} + */ + public function addError($message, $repeat = FALSE) { + return $this->addMessage($message, static::TYPE_ERROR); + } + + /** + * {@inheritdoc} + */ + public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) { + if (!($message instanceof Markup) && $message instanceof MarkupInterface) { + $message = Markup::create((string) $message); + } + + // Do not use strict type checking so that equivalent string and + // MarkupInterface objects are detected. + if ($repeat || !in_array($message, $this->flashBag->peek($type))) { + $this->flashBag->add($type, $message); + } + + // Mark this page as being uncacheable. + $this->killSwitch->trigger(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addStatus($message, $repeat = FALSE) { + return $this->addMessage($message, static::TYPE_STATUS); + } + + /** + * {@inheritdoc} + */ + public function addWarning($message, $repeat = FALSE) { + return $this->addMessage($message, static::TYPE_WARNING); + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->flashBag->peekAll(); + } + + /** + * {@inheritdoc} + */ + public function deleteAll() { + return $this->flashBag->clear(); + } + + /** + * {@inheritdoc} + */ + public function deleteByType($type) { + // Flash bag gets and clears flash messages from the stack. + return $this->flashBag->get($type); + } + + /** + * {@inheritdoc} + */ + public function messagesByType($type) { + return $this->flashBag->peek($type); + } + +} diff --git a/core/lib/Drupal/Core/Messenger/MessengerInterface.php b/core/lib/Drupal/Core/Messenger/MessengerInterface.php index 216835bcea5e..0d5e63ac95f0 100644 --- a/core/lib/Drupal/Core/Messenger/MessengerInterface.php +++ b/core/lib/Drupal/Core/Messenger/MessengerInterface.php @@ -6,8 +6,6 @@ * Stores runtime messages sent out to individual users on the page. * * An example for these messages is for example: "Content X got saved". - * - * @internal */ interface MessengerInterface { @@ -109,11 +107,15 @@ public function all(); * or self::TYPE_ERROR. * * @return string[]|\Drupal\Component\Render\MarkupInterface[] + * The messages of given type. */ public function messagesByType($type); /** * Deletes all messages. + * + * @return string[]|\Drupal\Component\Render\MarkupInterface[] + * The deleted messages. */ public function deleteAll(); @@ -123,6 +125,9 @@ public function deleteAll(); * @param string $type * The messages' type. Either self::TYPE_STATUS, self::TYPE_WARNING, or * self::TYPE_ERROR. + * + * @return string[]|\Drupal\Component\Render\MarkupInterface[] + * The deleted messages of given type. */ public function deleteByType($type); diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php index 37eb87585d2f..e350bc2eaf37 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php @@ -5,6 +5,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\Markup; use Drupal\Core\Session\AccountInterface; @@ -48,6 +49,13 @@ class SystemTestController extends ControllerBase { */ protected $renderer; + /** + * The messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs the SystemTestController. * @@ -59,12 +67,15 @@ class SystemTestController extends ControllerBase { * The current user. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. */ - public function __construct(LockBackendInterface $lock, LockBackendInterface $persistent_lock, AccountInterface $current_user, RendererInterface $renderer) { + public function __construct(LockBackendInterface $lock, LockBackendInterface $persistent_lock, AccountInterface $current_user, RendererInterface $renderer, MessengerInterface $messenger) { $this->lock = $lock; $this->persistentLock = $persistent_lock; $this->currentUser = $current_user; $this->renderer = $renderer; + $this->messenger = $messenger; } /** @@ -75,7 +86,8 @@ public static function create(ContainerInterface $container) { $container->get('lock'), $container->get('lock.persistent'), $container->get('current_user'), - $container->get('renderer') + $container->get('renderer'), + $container->get('messenger') ); } @@ -99,9 +111,13 @@ public function drupalSetMessageTest() { // Set two messages. drupal_set_message('First message (removed).'); drupal_set_message(t('Second message with markup! (not removed).')); - + $messages = $this->messenger->deleteByType('status'); // Remove the first. - unset($_SESSION['messages']['status'][0]); + unset($messages[0]); + + foreach ($messages as $message) { + $this->messenger->addStatus($message); + } // Duplicate message check. drupal_set_message('Non Duplicated message', 'status', FALSE); diff --git a/core/tests/Drupal/KernelTests/Core/Common/DrupalSetMessageTest.php b/core/tests/Drupal/KernelTests/Core/Common/DrupalSetMessageTest.php index 59470e656306..7a15fc3e04ac 100644 --- a/core/tests/Drupal/KernelTests/Core/Common/DrupalSetMessageTest.php +++ b/core/tests/Drupal/KernelTests/Core/Common/DrupalSetMessageTest.php @@ -20,10 +20,4 @@ public function testDrupalSetMessage() { $this->assertEquals('A message: bar', (string) $messages['status'][0]); } - protected function tearDown() { - // Clear session to prevent global leakage. - unset($_SESSION['messages']); - parent::tearDown(); - } - } diff --git a/core/tests/Drupal/KernelTests/Core/Messenger/LegacyMessengerTest.php b/core/tests/Drupal/KernelTests/Core/Messenger/LegacyMessengerTest.php new file mode 100644 index 000000000000..13f10e9c2eb0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Messenger/LegacyMessengerTest.php @@ -0,0 +1,84 @@ +setAccessible(TRUE); + return $method->invoke($legacy_messenger); + } + + /** + * @covers \Drupal::messenger + * @covers ::getMessengerService + * @covers ::all + * @covers ::addMessage + * @covers ::addError + * @covers ::addStatus + * @covers ::addWarning + */ + public function testMessages() { + // Save the current container for later use. + $container = \Drupal::getContainer(); + + // Unset the container to mimic not having one. + \Drupal::unsetContainer(); + + /** @var \Drupal\Core\Messenger\LegacyMessenger $messenger */ + // Verify that the Messenger service doesn't exists. + $messenger = \Drupal::messenger(); + $this->assertNull($this->getMessengerService($messenger)); + + // Add messages. + $messenger->addMessage('Foobar'); + $messenger->addError('Foo'); + + // Verify that retrieving another instance and adding more messages works. + $messenger = \Drupal::messenger(); + $messenger->addStatus('Bar'); + $messenger->addWarning('Fiz'); + + // Restore the container. + \Drupal::setContainer($container); + + // Verify that the Messenger service exists. + $messenger = \Drupal::messenger(); + $this->assertInstanceOf(Messenger::class, $this->getMessengerService($messenger)); + + // Add more messages. + $messenger->addMessage('Platypus'); + $messenger->addError('Rhinoceros'); + $messenger->addStatus('Giraffe'); + $messenger->addWarning('Cheetah'); + + // Verify that all the messages are present and accounted for. + $messages = $messenger->all(); + $this->assertContains('Foobar', $messages[MessengerInterface::TYPE_STATUS]); + $this->assertContains('Foo', $messages[MessengerInterface::TYPE_ERROR]); + $this->assertContains('Bar', $messages[MessengerInterface::TYPE_STATUS]); + $this->assertContains('Fiz', $messages[MessengerInterface::TYPE_WARNING]); + $this->assertContains('Platypus', $messages[MessengerInterface::TYPE_STATUS]); + $this->assertContains('Rhinoceros', $messages[MessengerInterface::TYPE_ERROR]); + $this->assertContains('Giraffe', $messages[MessengerInterface::TYPE_STATUS]); + $this->assertContains('Cheetah', $messages[MessengerInterface::TYPE_WARNING]); + } + +} diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index c73b6e59df40..094fcf10174d 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -119,6 +119,9 @@ public static function getSkippedDeprecations() { 'Automatically creating the first item for computed fields is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\TypedData\ComputedItemListTrait instead.', '"\Drupal\Core\Entity\ContentEntityStorageBase::doLoadRevisionFieldItems()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. "\Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()" should be implemented instead. See https://www.drupal.org/node/2924915.', 'Passing a single revision ID to "\Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. An array of revision IDs should be given instead. See https://www.drupal.org/node/2924915.', + 'drupal_set_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead. See https://www.drupal.org/node/2774931', + 'drupal_get_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::all() or \Drupal\Core\Messenger\MessengerInterface::messagesByType() instead. See https://www.drupal.org/node/2774931', + 'Adding or retrieving messages prior to the container being initialized was deprecated in Drupal 8.5.0 and this functionality will be removed before Drupal 9.0.0. Please report this usage at https://www.drupal.org/node/2928994.', ]; } From 968da88d76bec562783be77df1055762d28a6e42 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Mon, 18 Dec 2017 13:39:09 +0000 Subject: [PATCH 041/232] Issue #2928846 by alexpott, Berdir: [PHP 7.2] count() parameter must be an array or an object that implements Countable --- .../BigPipeResponseAttachmentsProcessorTest.php | 4 ++-- .../Functional/ConfigTranslationOverviewTest.php | 4 ++-- .../EntityReference/EntityReferenceAdminTest.php | 14 ++++++++++++-- .../tour/tests/src/Functional/TourTestBase.php | 5 ++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php index a4cbacdf9e65..ba69bfee603f 100644 --- a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php +++ b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php @@ -101,8 +101,8 @@ public function attachmentsProvider() { 'random attachment type (unofficial), with random assigned value, to prove BigPipeResponseAttachmentsProcessor is a perfect decorator' => [$random_attachments], ]; - $big_pipe_placeholder_attachments = ['big_pipe_placeholders' => $this->randomMachineName()]; - $big_pipe_nojs_placeholder_attachments = ['big_pipe_nojs_placeholders' => $this->randomMachineName()]; + $big_pipe_placeholder_attachments = ['big_pipe_placeholders' => [$this->randomMachineName()]]; + $big_pipe_nojs_placeholder_attachments = ['big_pipe_nojs_placeholders' => [$this->randomMachineName()]]; $big_pipe_cases = [ 'only big_pipe_placeholders' => [$big_pipe_placeholder_attachments], 'only big_pipe_nojs_placeholders' => [$big_pipe_nojs_placeholder_attachments], diff --git a/core/modules/config_translation/tests/src/Functional/ConfigTranslationOverviewTest.php b/core/modules/config_translation/tests/src/Functional/ConfigTranslationOverviewTest.php index dff627024594..9ccbb135d2f2 100644 --- a/core/modules/config_translation/tests/src/Functional/ConfigTranslationOverviewTest.php +++ b/core/modules/config_translation/tests/src/Functional/ConfigTranslationOverviewTest.php @@ -77,7 +77,7 @@ public function testMapperListPage() { // Make sure there is only a single operation for each dropbutton, either // 'List' or 'Translate'. foreach ($this->cssSelect('ul.dropbutton') as $i => $dropbutton) { - $this->assertIdentical(1, count($dropbutton->find('xpath', 'li'))); + $this->assertIdentical(1, count($dropbutton->findAll('xpath', 'li'))); $this->assertTrue(($dropbutton->getText() === 'Translate') || ($dropbutton->getText() === 'List')); } @@ -103,7 +103,7 @@ public function testMapperListPage() { // Make sure there is only a single 'Translate' operation for each // dropbutton. foreach ($this->cssSelect('ul.dropbutton') as $i => $dropbutton) { - $this->assertIdentical(1, count($dropbutton->find('xpath', 'li'))); + $this->assertIdentical(1, count($dropbutton->findAll('xpath', 'li'))); $this->assertIdentical('Translate', $dropbutton->getText()); } diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php index 72a2160e2233..9bacb07d9642 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php @@ -332,6 +332,12 @@ public function testFieldAdminHandler() { $this->drupalPostForm(NULL, $edit, t('Save field settings')); $this->drupalGet($bundle_path . '/fields/' . $field_path); $term_name = $this->randomString(); + $result = \Drupal::entityQuery('taxonomy_term') + ->condition('name', $term_name) + ->condition('vid', 'tags') + ->accessCheck(FALSE) + ->execute(); + $this->assertIdentical(0, count($result), "No taxonomy terms exist with the name '$term_name'."); $edit = [ // This must be set before new entities will be auto-created. 'settings[handler_settings][auto_create]' => 1, @@ -344,8 +350,12 @@ public function testFieldAdminHandler() { ]; $this->drupalPostForm(NULL, $edit, t('Save settings')); // The term should now exist. - $term = taxonomy_term_load_multiple_by_name($term_name, 'tags')[1]; - $this->assertIdentical(1, count($term), 'Taxonomy term was auto created when set as field default.'); + $result = \Drupal::entityQuery('taxonomy_term') + ->condition('name', $term_name) + ->condition('vid', 'tags') + ->accessCheck(FALSE) + ->execute(); + $this->assertIdentical(1, count($result), 'Taxonomy term was auto created when set as field default.'); } /** diff --git a/core/modules/tour/tests/src/Functional/TourTestBase.php b/core/modules/tour/tests/src/Functional/TourTestBase.php index 522e8e6f3648..a8487def61ee 100644 --- a/core/modules/tour/tests/src/Functional/TourTestBase.php +++ b/core/modules/tour/tests/src/Functional/TourTestBase.php @@ -50,14 +50,13 @@ public function assertTourTips($tips = []) { // Check for corresponding page elements. $total = 0; $modals = 0; - $raw_content = $this->getSession()->getPage()->getContent(); foreach ($tips as $tip) { if (!empty($tip['data-id'])) { - $elements = \PHPUnit_Util_XML::cssSelect('#' . $tip['data-id'], TRUE, $raw_content, TRUE); + $elements = $this->getSession()->getPage()->findAll('css', '#' . $tip['data-id']); $this->assertTrue(!empty($elements) && count($elements) === 1, format_string('Found corresponding page element for tour tip with id #%data-id', ['%data-id' => $tip['data-id']])); } elseif (!empty($tip['data-class'])) { - $elements = \PHPUnit_Util_XML::cssSelect('.' . $tip['data-class'], TRUE, $raw_content, TRUE); + $elements = $this->getSession()->getPage()->findAll('css', '.' . $tip['data-class']); $this->assertFalse(empty($elements), format_string('Found corresponding page element for tour tip with class .%data-class', ['%data-class' => $tip['data-class']])); } else { From 522ed00526a604b21a951483e776889332d0b8c4 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Mon, 18 Dec 2017 14:08:10 +0000 Subject: [PATCH 042/232] Issue #2914938 by timmillwood, RajabNatshah, xjm, Manuel Garcia, amateescu, Wim Leers: Preview of content - Notice: Undefined offset: 0 in _quickedit_entity_is_latest_revision() (line 196 of core/modules/quickedit/quickedit.module) --- core/modules/quickedit/quickedit.module | 16 +++++++--------- .../quickedit/src/Tests/QuickEditLoadingTest.php | 8 ++++++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/modules/quickedit/quickedit.module b/core/modules/quickedit/quickedit.module index 3d270500789f..ad08fc1e0df6 100644 --- a/core/modules/quickedit/quickedit.module +++ b/core/modules/quickedit/quickedit.module @@ -180,18 +180,16 @@ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityVie * @internal */ function _quickedit_entity_is_latest_revision(ContentEntityInterface $entity) { - $entity_type_manager = \Drupal::entityTypeManager(); - $entity_definition = $entity_type_manager->getDefinition($entity->getEntityTypeId()); - if (!$entity_definition->isRevisionable()) { + if (!$entity->getEntityType()->isRevisionable() || $entity->isNew()) { return TRUE; } - $revision_ids = $entity_type_manager + + $latest_revision = \Drupal::entityTypeManager() ->getStorage($entity->getEntityTypeId()) ->getQuery() - ->allRevisions() - ->condition($entity_definition->getKey('id'), $entity->id()) - ->sort($entity_definition->getKey('revision'), 'DESC') - ->range(0, 1) + ->latestRevision() + ->condition($entity->getEntityType()->getKey('id'), $entity->id()) ->execute(); - return $entity->getLoadedRevisionId() == array_keys($revision_ids)[0]; + + return !empty($latest_revision) && $entity->getLoadedRevisionId() == key($latest_revision) ? TRUE : FALSE; } diff --git a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php index 9ca558a37bcd..6945610db1f6 100644 --- a/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php +++ b/core/modules/quickedit/src/Tests/QuickEditLoadingTest.php @@ -330,6 +330,13 @@ public function testUserWithPermission() { public function testWithPendingRevision() { $this->drupalLogin($this->editorUser); + // Verify that the preview is loaded correctly. + $this->drupalPostForm('node/add/article', ['title[0][value]' => 'foo'], 'Preview'); + $this->assertResponse(200); + // Verify that quickedit is not active on preview. + $this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"'); + $this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"'); + $this->drupalGet('node/' . $this->testNode->id()); $this->assertRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"'); $this->assertRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"'); @@ -340,6 +347,7 @@ public function testWithPendingRevision() { $this->testNode->save(); $this->drupalGet('node/' . $this->testNode->id()); + $this->assertResponse(200); $this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"'); $this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"'); } From a44a23e64fcf517b5b6a5e0286bf5cccd723b45f Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Mon, 18 Dec 2017 20:20:52 +0000 Subject: [PATCH 043/232] Issue #1489692 by Liam Morland, pfrenssen, YesCT, geekinpink, sudishth, josmera01, David_Rothstein: Incorrect handling of file upload limit exceeded - file widget disappears --- .../EventSubscriber/FormAjaxSubscriber.php | 4 +- .../src/Functional/FileFieldCreationTrait.php | 86 +++++++++++++ .../src/Functional/FileFieldTestBase.php | 72 +---------- .../MaximumFileSizeExceededUploadTest.php | 119 ++++++++++++++++++ .../FormAjaxSubscriberTest.php | 2 +- .../Drupal/Tests/TestFileCreationTrait.php | 3 +- 6 files changed, 212 insertions(+), 74 deletions(-) create mode 100644 core/modules/file/tests/src/Functional/FileFieldCreationTrait.php create mode 100644 core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php diff --git a/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php index cae43980274a..6c9c1788f233 100644 --- a/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php +++ b/core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php @@ -3,7 +3,7 @@ namespace Drupal\Core\Form\EventSubscriber; use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Ajax\PrependCommand; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Form\Exception\BrokenPostRequestException; use Drupal\Core\Form\FormAjaxException; @@ -78,7 +78,7 @@ public function onException(GetResponseForExceptionEvent $event) { $this->drupalSetMessage($this->t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', ['@size' => $this->formatSize($exception->getSize())]), 'error'); $response = new AjaxResponse(); $status_messages = ['#type' => 'status_messages']; - $response->addCommand(new ReplaceCommand(NULL, $status_messages)); + $response->addCommand(new PrependCommand(NULL, $status_messages)); $response->headers->set('X-Status-Code', 200); $event->setResponse($response); return; diff --git a/core/modules/file/tests/src/Functional/FileFieldCreationTrait.php b/core/modules/file/tests/src/Functional/FileFieldCreationTrait.php new file mode 100644 index 000000000000..89c713e0779d --- /dev/null +++ b/core/modules/file/tests/src/Functional/FileFieldCreationTrait.php @@ -0,0 +1,86 @@ + $entity_type, + 'field_name' => $name, + 'type' => 'file', + 'settings' => $storage_settings, + 'cardinality' => !empty($storage_settings['cardinality']) ? $storage_settings['cardinality'] : 1, + ]); + $field_storage->save(); + + $this->attachFileField($name, $entity_type, $bundle, $field_settings, $widget_settings); + return $field_storage; + } + + /** + * Attaches a file field to an entity. + * + * @param string $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param string $entity_type + * The entity type this field will be added to. + * @param string $bundle + * The bundle this field will be added to. + * @param array $field_settings + * A list of field settings that will be added to the defaults. + * @param array $widget_settings + * A list of widget settings that will be added to the widget defaults. + */ + public function attachFileField($name, $entity_type, $bundle, $field_settings = [], $widget_settings = []) { + $field = [ + 'field_name' => $name, + 'label' => $name, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'required' => !empty($field_settings['required']), + 'settings' => $field_settings, + ]; + FieldConfig::create($field)->save(); + + entity_get_form_display($entity_type, $bundle, 'default') + ->setComponent($name, [ + 'type' => 'file_generic', + 'settings' => $widget_settings, + ]) + ->save(); + // Assign display settings. + entity_get_display($entity_type, $bundle, 'default') + ->setComponent($name, [ + 'label' => 'hidden', + 'type' => 'file_default', + ]) + ->save(); + } + +} diff --git a/core/modules/file/tests/src/Functional/FileFieldTestBase.php b/core/modules/file/tests/src/Functional/FileFieldTestBase.php index 937bf212a239..f3d1c22b460a 100644 --- a/core/modules/file/tests/src/Functional/FileFieldTestBase.php +++ b/core/modules/file/tests/src/Functional/FileFieldTestBase.php @@ -13,6 +13,8 @@ */ abstract class FileFieldTestBase extends BrowserTestBase { + use FileFieldCreationTrait; + /** * Modules to enable. * @@ -57,76 +59,6 @@ public function getLastFileId() { return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField(); } - /** - * Creates a new file field. - * - * @param string $name - * The name of the new field (all lowercase), exclude the "field_" prefix. - * @param string $entity_type - * The entity type. - * @param string $bundle - * The bundle that this field will be added to. - * @param array $storage_settings - * A list of field storage settings that will be added to the defaults. - * @param array $field_settings - * A list of instance settings that will be added to the instance defaults. - * @param array $widget_settings - * A list of widget settings that will be added to the widget defaults. - */ - public function createFileField($name, $entity_type, $bundle, $storage_settings = [], $field_settings = [], $widget_settings = []) { - $field_storage = FieldStorageConfig::create([ - 'entity_type' => $entity_type, - 'field_name' => $name, - 'type' => 'file', - 'settings' => $storage_settings, - 'cardinality' => !empty($storage_settings['cardinality']) ? $storage_settings['cardinality'] : 1, - ]); - $field_storage->save(); - - $this->attachFileField($name, $entity_type, $bundle, $field_settings, $widget_settings); - return $field_storage; - } - - /** - * Attaches a file field to an entity. - * - * @param string $name - * The name of the new field (all lowercase), exclude the "field_" prefix. - * @param string $entity_type - * The entity type this field will be added to. - * @param string $bundle - * The bundle this field will be added to. - * @param array $field_settings - * A list of field settings that will be added to the defaults. - * @param array $widget_settings - * A list of widget settings that will be added to the widget defaults. - */ - public function attachFileField($name, $entity_type, $bundle, $field_settings = [], $widget_settings = []) { - $field = [ - 'field_name' => $name, - 'label' => $name, - 'entity_type' => $entity_type, - 'bundle' => $bundle, - 'required' => !empty($field_settings['required']), - 'settings' => $field_settings, - ]; - FieldConfig::create($field)->save(); - - entity_get_form_display($entity_type, $bundle, 'default') - ->setComponent($name, [ - 'type' => 'file_generic', - 'settings' => $widget_settings, - ]) - ->save(); - // Assign display settings. - entity_get_display($entity_type, $bundle, 'default') - ->setComponent($name, [ - 'label' => 'hidden', - 'type' => 'file_default', - ]) - ->save(); - } - /** * Updates an existing file field with new settings. */ diff --git a/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php b/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php new file mode 100644 index 000000000000..7ccd2b218737 --- /dev/null +++ b/core/modules/file/tests/src/FunctionalJavascript/MaximumFileSizeExceededUploadTest.php @@ -0,0 +1,119 @@ +fileSystem = $this->container->get('file_system'); + + // Create the Article node type. + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + + // Attach a file field to the node type. + $field_settings = ['file_extensions' => 'txt']; + $this->createFileField('field_file', 'node', 'article', [], $field_settings); + + // Log in as a content author who can create Articles. + $this->user = $this->drupalCreateUser([ + 'access content', + 'create article content', + ]); + $this->drupalLogin($this->user); + + // Disable the displaying of errors, so that the AJAX responses are not + // contaminated with error messages about exceeding the maximum POST size. + // @todo Remove this when issue #2905597 is fixed. + // @see https://www.drupal.org/node/2905597 + $this->originalDisplayErrorsValue = ini_set('display_errors', '0'); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + // Restore the displaying of errors to the original value. + // @todo Remove this when issue #2905597 is fixed. + // @see https://www.drupal.org/node/2905597 + ini_set('display_errors', $this->originalDisplayErrorsValue); + + parent::tearDown(); + } + + /** + * Tests that uploading files exceeding maximum size are handled correctly. + */ + public function testUploadFileExceedingMaximumFileSize() { + $session = $this->getSession(); + + // Create a test file that exceeds the maximum POST size with 1 kilobyte. + $post_max_size = Bytes::toInt(ini_get('post_max_size')); + $invalid_file = $this->generateFile('exceeding_post_max_size', ceil(($post_max_size + 1024) / 1024), 1024); + + // Go to the node creation form and try to upload the test file. + $this->drupalGet('node/add/article'); + $page = $session->getPage(); + $page->attachFileToField("files[field_file_0]", $this->fileSystem->realpath($invalid_file)); + + // An error message should appear informing the user that the file exceeded + // the maximum file size. + $this->assertSession()->waitForElement('css', '.messages--error'); + // The error message includes the actual file size limit which depends on + // the current environment, so we check for a part of the message. + $this->assertSession()->pageTextContains('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size'); + + // Now upload a valid file and check that the error message disappears. + $valid_file = $this->generateFile('not_exceeding_post_max_size', 8, 8); + $page->attachFileToField("files[field_file_0]", $this->fileSystem->realpath($valid_file)); + $this->assertSession()->waitForElement('named', ['id_or_name', 'field_file_0_remove_button']); + $this->assertSession()->elementNotExists('css', '.messages--error'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php b/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php index 050d74526508..278ec0641668 100644 --- a/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/Form/EventSubscriber/FormAjaxSubscriberTest.php @@ -181,7 +181,7 @@ public function testOnExceptionBrokenPostRequest() { $this->assertSame(200, $actual_response->headers->get('X-Status-Code')); $expected_commands[] = [ 'command' => 'insert', - 'method' => 'replaceWith', + 'method' => 'prepend', 'selector' => NULL, 'data' => $rendered_output, 'settings' => NULL, diff --git a/core/tests/Drupal/Tests/TestFileCreationTrait.php b/core/tests/Drupal/Tests/TestFileCreationTrait.php index 5bb5394be996..a2f119b65ca4 100644 --- a/core/tests/Drupal/Tests/TestFileCreationTrait.php +++ b/core/tests/Drupal/Tests/TestFileCreationTrait.php @@ -164,7 +164,8 @@ public static function generateFile($filename, $width, $lines, $type = 'binary-t } // Create filename. - file_put_contents('public://' . $filename . '.txt', $text); + $filename = 'public://' . $filename . '.txt'; + file_put_contents($filename, $text); return $filename; } From dad8d64eed13328445a58c1f570c66119406e0f3 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Tue, 19 Dec 2017 16:39:06 +0000 Subject: [PATCH 044/232] Issue #2913864 by Jo Fitzgerald, chiranjeeb2410, matslats, phenaproxima: badly constructred link in drupal_set_message --- core/modules/migrate_drupal_ui/migrate_drupal_ui.install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.install b/core/modules/migrate_drupal_ui/migrate_drupal_ui.install index 3ad813844721..c6925b9b8df5 100644 --- a/core/modules/migrate_drupal_ui/migrate_drupal_ui.install +++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.install @@ -11,6 +11,6 @@ use Drupal\Core\Url; * Implements hook_install(). */ function migrate_drupal_ui_install() { - $url = Url::fromUri('base:upgrade')->toString(); + $url = Url::fromRoute('migrate_drupal_ui.upgrade')->toString(); drupal_set_message(t('The Migrate Drupal UI module has been enabled. Proceed to the upgrade form.', [':url' => $url])); } From 3cf0815a5471af6dcc1791e42b10dbaacd3ed8ad Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Tue, 19 Dec 2017 17:23:03 +0000 Subject: [PATCH 045/232] Issue #2930072 by vaplas, Lendude: Module: Convert system functional tests to phpunit --- .../src/Functional}/Module/DependencyTest.php | 2 +- .../src/Functional}/Module/HookRequirementsTest.php | 2 +- .../src/Functional}/Module/InstallUninstallTest.php | 2 +- .../src/Functional}/Module/PrepareUninstallTest.php | 8 ++++---- .../src/Functional}/Module/RequiredTest.php | 2 +- .../Tests => tests/src/Functional}/Module/VersionTest.php | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) rename core/modules/system/{src/Tests => tests/src/Functional}/Module/DependencyTest.php (99%) rename core/modules/system/{src/Tests => tests/src/Functional}/Module/HookRequirementsTest.php (94%) rename core/modules/system/{src/Tests => tests/src/Functional}/Module/InstallUninstallTest.php (99%) rename core/modules/system/{src/Tests => tests/src/Functional}/Module/PrepareUninstallTest.php (97%) rename core/modules/system/{src/Tests => tests/src/Functional}/Module/RequiredTest.php (95%) rename core/modules/system/{src/Tests => tests/src/Functional}/Module/VersionTest.php (91%) diff --git a/core/modules/system/src/Tests/Module/DependencyTest.php b/core/modules/system/tests/src/Functional/Module/DependencyTest.php similarity index 99% rename from core/modules/system/src/Tests/Module/DependencyTest.php rename to core/modules/system/tests/src/Functional/Module/DependencyTest.php index 173380ebd632..0fc78caa953a 100644 --- a/core/modules/system/src/Tests/Module/DependencyTest.php +++ b/core/modules/system/tests/src/Functional/Module/DependencyTest.php @@ -1,6 +1,6 @@ drupalGet('admin/modules'); $checkbox = $this->xpath('//input[@id="edit-modules-module-test-enable"]'); - $this->assertEqual(!empty($checkbox[0]['disabled']), $i % 2, $dependencies[$i]); + $this->assertEqual(!empty($checkbox[0]->getAttribute('disabled')), $i % 2, $dependencies[$i]); } } From 2e16f2a35d87540f53eec8b13a9a7c25e03db07e Mon Sep 17 00:00:00 2001 From: xjm Date: Tue, 19 Dec 2017 09:25:18 -1000 Subject: [PATCH 046/232] Issue #2926914 by tim.plunkett, xjm, larowlan, tedbow, EclipseGc: Rewrite \Drupal\layout_builder\Section to represent the entire section, not just the block info --- .../src/Controller/AddSectionController.php | 9 +- .../Controller/LayoutBuilderController.php | 22 +- .../src/Controller/MoveBlockController.php | 21 +- .../src/Field/LayoutSectionItemInterface.php | 40 --- .../src/Field/LayoutSectionItemList.php | 61 +++- .../Field/LayoutSectionItemListInterface.php | 46 --- .../layout_builder/src/Form/AddBlockForm.php | 3 +- .../src/Form/ConfigureBlockFormBase.php | 9 +- .../src/Form/ConfigureSectionForm.php | 26 +- .../src/Form/RemoveBlockForm.php | 8 +- .../src/Form/UpdateBlockForm.php | 13 +- .../src/LayoutSectionBuilder.php | 106 +----- .../src/Plugin/DataType/SectionData.php | 36 ++ .../FieldFormatter/LayoutSectionFormatter.php | 6 +- .../Field/FieldType/LayoutSectionItem.php | 48 +-- core/modules/layout_builder/src/Section.php | 301 ++++++++++++----- .../layout_builder/src/SectionComponent.php | 314 ++++++++++++++++++ .../src/SectionStorageInterface.php | 71 ++++ .../src/Functional/LayoutSectionTest.php | 139 +++----- ...outBuilderFieldLayoutCompatibilityTest.php | 9 +- .../src/Kernel/LayoutSectionItemListTest.php | 39 +++ .../src/Kernel/LayoutSectionItemTest.php | 89 ----- .../src/Kernel/SectionStorageTestBase.php | 156 +++++++++ .../src/Unit/LayoutSectionBuilderTest.php | 122 ++----- .../tests/src/Unit/SectionTest.php | 271 ++++++--------- 25 files changed, 1142 insertions(+), 823 deletions(-) delete mode 100644 core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php delete mode 100644 core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php create mode 100644 core/modules/layout_builder/src/Plugin/DataType/SectionData.php create mode 100644 core/modules/layout_builder/src/SectionComponent.php create mode 100644 core/modules/layout_builder/src/SectionStorageInterface.php create mode 100644 core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php delete mode 100644 core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php create mode 100644 core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php diff --git a/core/modules/layout_builder/src/Controller/AddSectionController.php b/core/modules/layout_builder/src/Controller/AddSectionController.php index d6771082382b..fa522b547224 100644 --- a/core/modules/layout_builder/src/Controller/AddSectionController.php +++ b/core/modules/layout_builder/src/Controller/AddSectionController.php @@ -6,6 +6,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -63,13 +64,9 @@ public static function create(ContainerInterface $container) { * The controller response. */ public function build(EntityInterface $entity, $delta, $plugin_id) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $entity->layout_builder__layout; - $field_list->addItem($delta, [ - 'layout' => $plugin_id, - 'layout_settings' => [], - 'section' => [], - ]); + $field_list->insertSection($delta, new Section($plugin_id)); $this->layoutTempstoreRepository->set($entity); diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php index a4163a40a821..959ce1d5b414 100644 --- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php @@ -10,8 +10,8 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\layout_builder\LayoutSectionBuilder; -use Drupal\layout_builder\Field\LayoutSectionItemInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -111,20 +111,20 @@ public function layout(EntityInterface $entity, $is_rebuilding = FALSE) { $entity_id = $entity->id(); $entity_type_id = $entity->getEntityTypeId(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $entity->layout_builder__layout; // For a new layout override, begin with a single section of one column. - if (!$is_rebuilding && $field_list->isEmpty()) { - $field_list->addItem(0, ['layout' => 'layout_onecol']); + if (!$is_rebuilding && $field_list->count() === 0) { + $field_list->appendSection(new Section('layout_onecol')); $this->layoutTempstoreRepository->set($entity); } $output = []; $count = 0; - foreach ($field_list as $item) { + foreach ($field_list->getSections() as $section) { $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count); - $output[] = $this->buildAdministrativeSection($item, $entity, $count); + $output[] = $this->buildAdministrativeSection($section, $entity, $count); $count++; } $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count); @@ -179,8 +179,8 @@ protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) { /** * Builds the render array for the layout section while editing. * - * @param \Drupal\layout_builder\Field\LayoutSectionItemInterface $item - * The layout section item. + * @param \Drupal\layout_builder\Section $section + * The layout section. * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * @param int $delta @@ -189,12 +189,12 @@ protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) { * @return array * The render array for a given section. */ - protected function buildAdministrativeSection(LayoutSectionItemInterface $item, EntityInterface $entity, $delta) { + protected function buildAdministrativeSection(Section $section, EntityInterface $entity, $delta) { $entity_type_id = $entity->getEntityTypeId(); $entity_id = $entity->id(); - $layout = $this->layoutManager->createInstance($item->layout, $item->layout_settings); - $build = $this->builder->buildSectionFromLayout($layout, $item->section); + $layout = $section->getLayout(); + $build = $section->toRenderArray(); $layout_definition = $layout->getPluginDefinition(); foreach ($layout_definition->getRegions() as $region => $info) { diff --git a/core/modules/layout_builder/src/Controller/MoveBlockController.php b/core/modules/layout_builder/src/Controller/MoveBlockController.php index d648416d9e41..7a841a252d77 100644 --- a/core/modules/layout_builder/src/Controller/MoveBlockController.php +++ b/core/modules/layout_builder/src/Controller/MoveBlockController.php @@ -69,32 +69,29 @@ public static function create(ContainerInterface $container) { * An AJAX response. */ public function build(EntityInterface $entity, $delta_from, $delta_to, $region_from, $region_to, $block_uuid, $preceding_block_uuid = NULL) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $entity->layout_builder__layout->get($delta_from); - $section = $field->getSection(); + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $entity->layout_builder__layout; + $section = $field_list->getSection($delta_from); - $block = $section->getBlock($region_from, $block_uuid); - $section->removeBlock($region_from, $block_uuid); + $component = $section->getComponent($block_uuid); + $section->removeComponent($block_uuid); // If the block is moving from one section to another, update the original // section and load the new one. if ($delta_from !== $delta_to) { - $field->updateFromSection($section); - $field = $entity->layout_builder__layout->get($delta_to); - $section = $field->getSection(); + $section = $field_list->getSection($delta_to); } // If a preceding block was specified, insert after that. Otherwise add the // block to the front. + $component->setRegion($region_to); if (isset($preceding_block_uuid)) { - $section->insertBlock($region_to, $block_uuid, $block, $preceding_block_uuid); + $section->insertAfterComponent($preceding_block_uuid, $component); } else { - $section->addBlock($region_to, $block_uuid, $block); + $section->appendComponent($component); } - $field->updateFromSection($section); - $this->layoutTempstoreRepository->set($entity); return $this->rebuildLayout($entity); } diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php b/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php deleted file mode 100644 index 786b6b4b19ac..000000000000 --- a/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php +++ /dev/null @@ -1,40 +0,0 @@ -get($index)) { - $start = array_slice($this->list, 0, $index); - $end = array_slice($this->list, $index); - $item = $this->createItem($index, $value); + public function insertSection($delta, Section $section) { + if ($this->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 { - $item = $this->appendItem($value); + $this->appendSection($section); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function appendSection(Section $section) { + $this->appendItem()->section = $section; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSections() { + $sections = []; + /** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */ + foreach ($this->list as $delta => $item) { + $sections[$delta] = $item->section; } - return $item; + return $sections; + } + + /** + * {@inheritdoc} + */ + public function getSection($delta) { + /** @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())); + } + + return $item->section; + } + + /** + * {@inheritdoc} + */ + public function removeSection($delta) { + $this->removeItem($delta); + return $this; } } diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php b/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php deleted file mode 100644 index 81839d17747b..000000000000 --- a/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php +++ /dev/null @@ -1,46 +0,0 @@ -addBlock($region, $uuid, $configuration); + $section->appendComponent(new SectionComponent($uuid, $region, $configuration)); } } diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php index 7356ef4ccdf1..08ff3174167c 100644 --- a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php +++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php @@ -246,11 +246,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $configuration = $this->block->getConfiguration(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $this->entity->layout_builder__layout->get($this->delta); - $section = $field->getSection(); - $this->submitBlock($section, $this->region, $configuration['uuid'], ['block' => $configuration]); - $field->updateFromSection($section); + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $this->entity->layout_builder__layout; + $section = $field_list->getSection($this->delta); + $this->submitBlock($section, $this->region, $configuration['uuid'], $configuration); $this->layoutTempstoreRepository->set($this->entity); $form_state->setRedirectUrl($this->entity->toUrl('layout-builder')); diff --git a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php index 17913237d51d..6993d218be50 100644 --- a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php @@ -14,6 +14,7 @@ use Drupal\Core\Plugin\PluginWithFormsInterface; use Drupal\layout_builder\Controller\LayoutRebuildTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -121,14 +122,15 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt $this->delta = $delta; $this->isUpdate = is_null($plugin_id); - $configuration = []; if ($this->isUpdate) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $this->entity->layout_builder__layout->get($this->delta); - $plugin_id = $field->layout; - $configuration = $field->layout_settings; + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $this->entity->layout_builder__layout; + $section = $field_list->getSection($this->delta); } - $this->layout = $this->layoutManager->createInstance($plugin_id, $configuration); + else { + $section = new Section($plugin_id); + } + $this->layout = $section->getLayout(); $form['#tree'] = TRUE; $form['layout_settings'] = []; @@ -166,19 +168,13 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $plugin_id = $this->layout->getPluginId(); $configuration = $this->layout->getConfiguration(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $this->entity->layout_builder__layout; if ($this->isUpdate) { - $field = $field_list->get($this->delta); - $field->layout = $plugin_id; - $field->layout_settings = $configuration; + $field_list->getSection($this->delta)->setLayoutSettings($configuration); } else { - $field_list->addItem($this->delta, [ - 'layout' => $plugin_id, - 'layout_settings' => $configuration, - 'section' => [], - ]); + $field_list->insertSection($this->delta, new Section($plugin_id, $configuration)); } $this->layoutTempstoreRepository->set($this->entity); diff --git a/core/modules/layout_builder/src/Form/RemoveBlockForm.php b/core/modules/layout_builder/src/Form/RemoveBlockForm.php index 139186af6654..9c7eb2084dc5 100644 --- a/core/modules/layout_builder/src/Form/RemoveBlockForm.php +++ b/core/modules/layout_builder/src/Form/RemoveBlockForm.php @@ -60,11 +60,9 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt * {@inheritdoc} */ protected function handleEntity(EntityInterface $entity, FormStateInterface $form_state) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $entity->layout_builder__layout->get($this->delta); - $section = $field->getSection(); - $section->removeBlock($this->region, $this->uuid); - $field->updateFromSection($section); + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $this->entity->layout_builder__layout; + $field_list->getSection($this->delta)->removeComponent($this->uuid); } } diff --git a/core/modules/layout_builder/src/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php index 2f2aa600e44c..3cc36585a11a 100644 --- a/core/modules/layout_builder/src/Form/UpdateBlockForm.php +++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php @@ -40,14 +40,11 @@ public function getFormId() { * The form array. */ public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $uuid = NULL) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $entity->layout_builder__layout->get($delta); - $block = $field->getSection()->getBlock($region, $uuid); - if (empty($block['block']['id'])) { - throw new \InvalidArgumentException('Invalid UUID specified'); - } + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $entity->layout_builder__layout; + $plugin = $field_list->getSection($delta)->getComponent($uuid)->getPlugin(); - return parent::buildForm($form, $form_state, $entity, $delta, $region, $block['block']['id'], $block['block']); + return parent::buildForm($form, $form_state, $entity, $delta, $region, $plugin->getPluginId(), $plugin->getConfiguration()); } /** @@ -61,7 +58,7 @@ protected function submitLabel() { * {@inheritdoc} */ protected function submitBlock(Section $section, $region, $uuid, array $configuration) { - $section->updateBlock($region, $uuid, $configuration); + $section->getComponent($uuid)->setConfiguration($configuration); } } diff --git a/core/modules/layout_builder/src/LayoutSectionBuilder.php b/core/modules/layout_builder/src/LayoutSectionBuilder.php index 1682974f1134..525849d9cef3 100644 --- a/core/modules/layout_builder/src/LayoutSectionBuilder.php +++ b/core/modules/layout_builder/src/LayoutSectionBuilder.php @@ -2,14 +2,11 @@ namespace Drupal\layout_builder; -use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Block\BlockManagerInterface; -use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Layout\LayoutInterface; use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface; use Drupal\Core\Plugin\Context\ContextRepositoryInterface; -use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -17,6 +14,8 @@ * Builds the UI for layout sections. * * @internal + * + * @todo Remove in https://www.drupal.org/project/drupal/issues/2928450. */ class LayoutSectionBuilder { @@ -84,37 +83,21 @@ public function __construct(AccountInterface $account, LayoutPluginManagerInterf * * @param \Drupal\Core\Layout\LayoutInterface $layout * The ID of the layout. - * @param array $section - * An array of configuration, keyed first by region and then by block UUID. + * @param \Drupal\layout_builder\SectionComponent[] $components + * An array of components. * * @return array * The render array for a given section. */ - public function buildSectionFromLayout(LayoutInterface $layout, array $section) { - $cacheability = CacheableMetadata::createFromRenderArray([]); - + public function buildSectionFromLayout(LayoutInterface $layout, array $components) { $regions = []; - $weight = 0; - foreach ($section as $region => $blocks) { - if (!is_array($blocks)) { - throw new \InvalidArgumentException(sprintf('The "%s" region in the "%s" layout has invalid configuration', $region, $layout->getPluginId())); - } - - foreach ($blocks as $uuid => $configuration) { - if (!is_array($configuration) || !isset($configuration['block'])) { - throw new \InvalidArgumentException(sprintf('The block with UUID of "%s" has invalid configuration', $uuid)); - } - - if ($block_output = $this->buildBlock($uuid, $configuration['block'], $cacheability)) { - $block_output['#weight'] = $weight++; - $regions[$region][$uuid] = $block_output; - } + foreach ($components as $component) { + if ($output = $component->toRenderArray()) { + $regions[$component->getRegion()][$component->getUuid()] = $output; } } - $result = $layout->build($regions); - $cacheability->applyTo($result); - return $result; + return $layout->build($regions); } /** @@ -124,78 +107,15 @@ public function buildSectionFromLayout(LayoutInterface $layout, array $section) * The ID of the layout. * @param array $layout_settings * The configuration for the layout. - * @param array $section - * An array of configuration, keyed first by region and then by block UUID. + * @param \Drupal\layout_builder\SectionComponent[] $components + * An array of components. * * @return array * The render array for a given section. */ - public function buildSection($layout_id, array $layout_settings, array $section) { + public function buildSection($layout_id, array $layout_settings, array $components) { $layout = $this->layoutPluginManager->createInstance($layout_id, $layout_settings); - return $this->buildSectionFromLayout($layout, $section); - } - - /** - * Builds the render array for a given block. - * - * @param string $uuid - * The UUID of this block instance. - * @param array $configuration - * An array of configuration relevant to the block instance. Must contain - * the plugin ID with the key 'id'. - * @param \Drupal\Core\Cache\CacheableMetadata $cacheability - * The cacheability metadata. - * - * @return array|null - * The render array representing this block, if accessible. NULL otherwise. - */ - protected function buildBlock($uuid, array $configuration, CacheableMetadata $cacheability) { - $block = $this->getBlock($uuid, $configuration); - - $access = $block->access($this->account, TRUE); - $cacheability->addCacheableDependency($access); - - $block_output = NULL; - if ($access->isAllowed()) { - $block_output = [ - '#theme' => 'block', - '#configuration' => $block->getConfiguration(), - '#plugin_id' => $block->getPluginId(), - '#base_plugin_id' => $block->getBaseId(), - '#derivative_plugin_id' => $block->getDerivativeId(), - 'content' => $block->build(), - ]; - $cacheability->addCacheableDependency($block); - } - return $block_output; - } - - /** - * Gets a block instance. - * - * @param string $uuid - * The UUID of this block instance. - * @param array $configuration - * An array of configuration relevant to the block instance. Must contain - * the plugin ID with the key 'id'. - * - * @return \Drupal\Core\Block\BlockPluginInterface - * The block instance. - * - * @throws \Drupal\Component\Plugin\Exception\PluginException - * Thrown when the configuration parameter does not contain 'id'. - */ - protected function getBlock($uuid, array $configuration) { - if (!isset($configuration['id'])) { - throw new PluginException(sprintf('No plugin ID specified for block with "%s" UUID', $uuid)); - } - - $block = $this->blockManager->createInstance($configuration['id'], $configuration); - if ($block instanceof ContextAwarePluginInterface) { - $contexts = $this->contextRepository->getRuntimeContexts(array_values($block->getContextMapping())); - $this->contextHandler->applyContextMapping($block, $contexts); - } - return $block; + return $this->buildSectionFromLayout($layout, $components); } } diff --git a/core/modules/layout_builder/src/Plugin/DataType/SectionData.php b/core/modules/layout_builder/src/Plugin/DataType/SectionData.php new file mode 100644 index 000000000000..353c53fe7cca --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/DataType/SectionData.php @@ -0,0 +1,36 @@ +getName())); + } + parent::setValue($value, $notify); + } + +} diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php index 4951d01c4b9b..c321585fdf55 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php @@ -78,9 +78,9 @@ public static function create(ContainerInterface $container, array $configuratio public function viewElements(FieldItemListInterface $items, $langcode) { $elements = []; - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface[] $items */ - foreach ($items as $delta => $item) { - $elements[$delta] = $this->builder->buildSection($item->layout, $item->layout_settings, $item->section); + /** @var \Drupal\layout_builder\SectionStorageInterface $items */ + foreach ($items->getSections() as $delta => $section) { + $elements[$delta] = $section->toRenderArray(); } return $elements; 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 fc1c63413fa1..2001d1b5ff19 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php @@ -7,8 +7,6 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataDefinition; -use Drupal\Core\TypedData\MapDataDefinition; -use Drupal\layout_builder\Field\LayoutSectionItemInterface; use Drupal\layout_builder\Section; /** @@ -25,22 +23,16 @@ * no_ui = TRUE, * cardinality = \Drupal\Core\Field\FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED * ) + * + * @property \Drupal\layout_builder\Section section */ -class LayoutSectionItem extends FieldItemBase implements LayoutSectionItemInterface { +class LayoutSectionItem extends FieldItemBase { /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { - // Prevent early t() calls by using the TranslatableMarkup. - $properties['layout'] = DataDefinition::create('string') - ->setLabel(new TranslatableMarkup('Layout')) - ->setSetting('case_sensitive', FALSE) - ->setRequired(TRUE); - $properties['layout_settings'] = MapDataDefinition::create('map') - ->setLabel(new TranslatableMarkup('Layout Settings')) - ->setRequired(FALSE); - $properties['section'] = MapDataDefinition::create('map') + $properties['section'] = DataDefinition::create('layout_section') ->setLabel(new TranslatableMarkup('Layout Section')) ->setRequired(FALSE); @@ -73,17 +65,6 @@ public static function mainPropertyName() { public static function schema(FieldStorageDefinitionInterface $field_definition) { $schema = [ 'columns' => [ - 'layout' => [ - 'type' => 'varchar', - 'length' => '255', - 'binary' => FALSE, - ], - 'layout_settings' => [ - 'type' => 'blob', - 'size' => 'normal', - // @todo Address in https://www.drupal.org/node/2914503. - 'serialize' => TRUE, - ], 'section' => [ 'type' => 'blob', 'size' => 'normal', @@ -100,10 +81,8 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) * {@inheritdoc} */ public static function generateSampleValue(FieldDefinitionInterface $field_definition) { - $values['layout'] = 'layout_onecol'; - $values['layout_settings'] = []; // @todo Expand this in https://www.drupal.org/node/2912331. - $values['section'] = []; + $values['section'] = new Section('layout_onecol'); return $values; } @@ -111,22 +90,7 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin * {@inheritdoc} */ public function isEmpty() { - return empty($this->layout); - } - - /** - * {@inheritdoc} - */ - public function getSection() { - return new Section($this->section); - } - - /** - * {@inheritdoc} - */ - public function updateFromSection(Section $section) { - $this->section = $section->getValue(); - return $this; + return empty($this->section); } } diff --git a/core/modules/layout_builder/src/Section.php b/core/modules/layout_builder/src/Section.php index f5e19003b58a..55204ddc9eda 100644 --- a/core/modules/layout_builder/src/Section.php +++ b/core/modules/layout_builder/src/Section.php @@ -5,158 +5,303 @@ /** * Provides a domain object for layout sections. * - * A section is a multi-dimensional array, keyed first by region machine name, - * then by block UUID, containing block configuration values. + * A section consists of three parts: + * - The layout plugin ID for the layout applied to the section (for example, + * 'layout_onecol'). + * - An array of settings for the layout plugin. + * - An array of components that can be rendered in the section. + * + * @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\Core\Layout\LayoutDefinition + * @see \Drupal\layout_builder\SectionComponent + * + * @todo Determine whether an interface will be provided for this in + * https://www.drupal.org/project/drupal/issues/2930334. */ class Section { /** - * The section data. + * The layout plugin ID. + * + * @var string + */ + protected $layoutId; + + /** + * The layout plugin settings. * * @var array */ - protected $section; + protected $layoutSettings = []; + + /** + * An array of components, keyed by UUID. + * + * @var \Drupal\layout_builder\SectionComponent[] + */ + protected $components = []; /** * Constructs a new Section. * - * @param array $section - * The section data. + * @param string $layout_id + * The layout plugin ID. + * @param array $layout_settings + * (optional) The layout plugin settings. + * @param \Drupal\layout_builder\SectionComponent[] $components + * (optional) The components. */ - public function __construct(array $section) { - $this->section = $section; + public function __construct($layout_id, array $layout_settings = [], array $components = []) { + $this->layoutId = $layout_id; + $this->layoutSettings = $layout_settings; + foreach ($components as $component) { + $this->setComponent($component); + } } /** - * Returns the value of the section. + * Returns the renderable array for this section. * * @return array - * The section data. + * A renderable array representing the content of the section. */ - public function getValue() { - return $this->section; + public function toRenderArray() { + $regions = []; + foreach ($this->getComponents() as $component) { + if ($output = $component->toRenderArray()) { + $regions[$component->getRegion()][$component->getUuid()] = $output; + } + } + + return $this->getLayout()->build($regions); } /** - * Gets the configuration of a given block from a region. + * Gets the layout plugin for this section. * - * @param string $region - * The region name. - * @param string $uuid - * The UUID of the block to retrieve. + * @return \Drupal\Core\Layout\LayoutInterface + * The layout plugin. + */ + public function getLayout() { + return $this->layoutPluginManager()->createInstance($this->getLayoutId(), $this->getLayoutSettings()); + } + + /** + * Gets the layout plugin ID for this section. * - * @return array - * The block configuration. + * @return string + * The layout plugin ID. * - * @throws \InvalidArgumentException - * Thrown when the expected region or UUID do not exist. + * @internal + * This method should only be used by code responsible for storing the data. */ - public function getBlock($region, $uuid) { - if (!isset($this->section[$region])) { - throw new \InvalidArgumentException('Invalid region'); - } + public function getLayoutId() { + return $this->layoutId; + } - if (!isset($this->section[$region][$uuid])) { - throw new \InvalidArgumentException('Invalid UUID'); - } + /** + * Gets the layout plugin settings for this section. + * + * @return mixed[] + * The layout plugin settings. + * + * @internal + * This method should only be used by code responsible for storing the data. + */ + public function getLayoutSettings() { + return $this->layoutSettings; + } + + /** + * Sets the layout plugin settings for this section. + * + * @param mixed[] $layout_settings + * The layout plugin settings. + * + * @return $this + */ + public function setLayoutSettings(array $layout_settings) { + $this->layoutSettings = $layout_settings; + return $this; + } - return $this->section[$region][$uuid]; + /** + * Returns the components of the section. + * + * @return \Drupal\layout_builder\SectionComponent[] + * The components. + */ + public function getComponents() { + return $this->components; } /** - * Updates the configuration of a given block from a region. + * Gets the component for a given UUID. * - * @param string $region - * The region name. * @param string $uuid - * The UUID of the block to retrieve. - * @param array $configuration - * The block configuration. + * The UUID of the component to retrieve. * - * @return $this + * @return \Drupal\layout_builder\SectionComponent + * The component. * * @throws \InvalidArgumentException - * Thrown when the expected region or UUID do not exist. + * Thrown when the expected UUID does not exist. */ - public function updateBlock($region, $uuid, array $configuration) { - if (!isset($this->section[$region])) { - throw new \InvalidArgumentException('Invalid region'); + public function getComponent($uuid) { + if (!isset($this->components[$uuid])) { + throw new \InvalidArgumentException(sprintf('Invalid UUID "%s"', $uuid)); } - if (!isset($this->section[$region][$uuid])) { - throw new \InvalidArgumentException('Invalid UUID'); - } - - $this->section[$region][$uuid] = $configuration; + return $this->components[$uuid]; + } + /** + * Helper method to set a component. + * + * @param \Drupal\layout_builder\SectionComponent $component + * The component. + * + * @return $this + */ + protected function setComponent(SectionComponent $component) { + $this->components[$component->getUuid()] = $component; return $this; } /** - * Removes a given block from a region. + * Removes a given component from a region. * - * @param string $region - * The region name. * @param string $uuid - * The UUID of the block to remove. + * The UUID of the component to remove. * * @return $this */ - public function removeBlock($region, $uuid) { - unset($this->section[$region][$uuid]); - $this->section = array_filter($this->section); + public function removeComponent($uuid) { + unset($this->components[$uuid]); return $this; } /** - * Adds a block to the front of a region. + * Appends a component to the end of a region. * - * @param string $region - * The region name. - * @param string $uuid - * The UUID of the block to add. - * @param array $configuration - * The block configuration. + * @param \Drupal\layout_builder\SectionComponent $component + * The component being appended. * * @return $this */ - public function addBlock($region, $uuid, array $configuration) { - $this->section += [$region => []]; - $this->section[$region] = array_merge([$uuid => $configuration], $this->section[$region]); + public function appendComponent(SectionComponent $component) { + $component->setWeight($this->getNextHighestWeight($component->getRegion())); + $this->setComponent($component); return $this; } /** - * Inserts a block after a specified existing block in a region. + * Returns the next highest weight of the component in a region. * * @param string $region * The region name. - * @param string $uuid - * The UUID of the block to insert. - * @param array $configuration - * The block configuration. + * + * @return int + * A number higher than the highest weight of the component in the region. + */ + protected function getNextHighestWeight($region) { + $components = $this->getComponentsByRegion($region); + $weights = array_map(function (SectionComponent $component) { + return $component->getWeight(); + }, $components); + return $weights ? max($weights) + 1 : 0; + } + + /** + * Gets the components for a specific region. + * + * @param string $region + * The region name. + * + * @return \Drupal\layout_builder\SectionComponent[] + * An array of components in the specified region, sorted by weight. + */ + protected function getComponentsByRegion($region) { + $components = array_filter($this->getComponents(), function (SectionComponent $component) use ($region) { + return $component->getRegion() === $region; + }); + uasort($components, function (SectionComponent $a, SectionComponent $b) { + return $a->getWeight() > $b->getWeight() ? 1 : -1; + }); + return $components; + } + + /** + * Inserts a component after a specified existing component. + * * @param string $preceding_uuid - * The UUID of the existing block to insert after. + * The UUID of the existing component to insert after. + * @param \Drupal\layout_builder\SectionComponent $component + * The component being inserted. * * @return $this * * @throws \InvalidArgumentException - * Thrown when the expected region does not exist. + * Thrown when the expected UUID does not exist. + */ + public function insertAfterComponent($preceding_uuid, SectionComponent $component) { + // Find the delta of the specified UUID. + $uuids = array_keys($this->getComponentsByRegion($component->getRegion())); + $delta = array_search($preceding_uuid, $uuids, TRUE); + if ($delta === FALSE) { + throw new \InvalidArgumentException(sprintf('Invalid preceding UUID "%s"', $preceding_uuid)); + } + return $this->insertComponent($delta + 1, $component); + } + + /** + * Inserts a component at a specified delta. + * + * @param int $delta + * The zero-based delta in which to insert the component. + * @param \Drupal\layout_builder\SectionComponent $new_component + * The component being inserted. + * + * @return $this + * + * @throws \OutOfBoundsException + * Thrown when the specified delta is invalid. */ - public function insertBlock($region, $uuid, array $configuration, $preceding_uuid) { - if (!isset($this->section[$region])) { - throw new \InvalidArgumentException('Invalid region'); + public function insertComponent($delta, SectionComponent $new_component) { + $components = $this->getComponentsByRegion($new_component->getRegion()); + $count = count($components); + if ($delta > $count) { + throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" component', $delta, $new_component->getUuid())); } - $slice_id = array_search($preceding_uuid, array_keys($this->section[$region])); - if ($slice_id === FALSE) { - throw new \InvalidArgumentException('Invalid preceding UUID'); + // If the delta is the end of the list, append the component instead. + if ($delta === $count) { + return $this->appendComponent($new_component); } - $before = array_slice($this->section[$region], 0, $slice_id + 1); - $after = array_slice($this->section[$region], $slice_id + 1); - $this->section[$region] = array_merge($before, [$uuid => $configuration], $after); + // Find the weight of the component that exists at the specified delta. + $weight = array_values($components)[$delta]->getWeight(); + $this->setComponent($new_component->setWeight($weight++)); + + // Increase the weight of every subsequent component. + foreach (array_slice($components, $delta) as $component) { + $component->setWeight($weight++); + } return $this; } + /** + * Wraps the layout plugin manager. + * + * @return \Drupal\Core\Layout\LayoutPluginManagerInterface + * The layout plugin manager. + */ + protected function layoutPluginManager() { + return \Drupal::service('plugin.manager.core.layout'); + } + } diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php new file mode 100644 index 000000000000..caad0a2b61db --- /dev/null +++ b/core/modules/layout_builder/src/SectionComponent.php @@ -0,0 +1,314 @@ +uuid = $uuid; + $this->region = $region; + $this->configuration = $configuration; + $this->additional = $additional; + } + + /** + * Returns the renderable array for this component. + * + * @return array + * A renderable array representing the content of the component. + */ + public function toRenderArray() { + $output = []; + + $plugin = $this->getPlugin(); + // @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); + + if ($access->isAllowed()) { + $cacheability->addCacheableDependency($plugin); + // @todo Move this to BlockBase in https://www.drupal.org/node/2931040. + $output = [ + '#theme' => 'block', + '#configuration' => $plugin->getConfiguration(), + '#plugin_id' => $plugin->getPluginId(), + '#base_plugin_id' => $plugin->getBaseId(), + '#derivative_plugin_id' => $plugin->getDerivativeId(), + '#weight' => $this->getWeight(), + 'content' => $plugin->build(), + ]; + } + $cacheability->applyTo($output); + } + return $output; + } + + /** + * Gets any arbitrary property for the component. + * + * @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 for the component. + * + * @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; + } + + /** + * Gets the region for the component. + * + * @return string + * The region. + */ + public function getRegion() { + return $this->region; + } + + /** + * Sets the region for the component. + * + * @param string $region + * The region. + * + * @return $this + */ + public function setRegion($region) { + $this->region = $region; + return $this; + } + + /** + * Gets the weight of the component. + * + * @return int + * The zero-based weight of the component. + * + * @throws \UnexpectedValueException + * Thrown if the weight was never set. + */ + public function getWeight() { + return $this->weight; + } + + /** + * Sets the weight of the component. + * + * @param int $weight + * The zero-based weight of the component. + * + * @return $this + */ + public function setWeight($weight) { + $this->weight = $weight; + return $this; + } + + /** + * Gets the component plugin configuration. + * + * @return mixed[] + * The component plugin configuration. + */ + protected function getConfiguration() { + return $this->configuration; + } + + /** + * Sets the plugin configuration. + * + * @param mixed[] $configuration + * The plugin configuration. + * + * @return $this + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + return $this; + } + + /** + * Gets the plugin ID. + * + * @return string + * The plugin ID. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * Thrown if the plugin ID cannot be found. + */ + protected function getPluginId() { + if (empty($this->configuration['id'])) { + throw new PluginException(sprintf('No plugin ID specified for component with "%s" UUID', $this->uuid)); + } + return $this->configuration['id']; + } + + /** + * Gets the UUID for this component. + * + * @return string + * The UUID. + */ + public function getUuid() { + return $this->uuid; + } + + /** + * Gets the plugin for this component. + * + * @return \Drupal\Component\Plugin\PluginInspectionInterface + * The plugin. + */ + public function getPlugin() { + $plugin = $this->pluginManager()->createInstance($this->getPluginId(), $this->getConfiguration()); + if ($plugin instanceof ContextAwarePluginInterface) { + $contexts = $this->contextRepository()->getRuntimeContexts(array_values($plugin->getContextMapping())); + $this->contextHandler()->applyContextMapping($plugin, $contexts); + } + return $plugin; + } + + /** + * Wraps the component plugin manager. + * + * @return \Drupal\Core\Block\BlockManagerInterface + * The plugin manager. + */ + protected function pluginManager() { + return \Drupal::service('plugin.manager.block'); + } + + /** + * Wraps the context repository. + * + * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface + * The context repository. + */ + protected function contextRepository() { + return \Drupal::service('context.repository'); + } + + /** + * Wraps the context handler. + * + * @return \Drupal\Core\Plugin\Context\ContextHandlerInterface + * The context handler. + */ + protected function contextHandler() { + return \Drupal::service('context.handler'); + } + + /** + * Wraps the current user. + * + * @return \Drupal\Core\Session\AccountInterface + * The current user. + */ + protected function currentUser() { + return \Drupal::currentUser(); + } + +} diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php new file mode 100644 index 000000000000..c4da487eba6b --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -0,0 +1,71 @@ + 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'test_context_aware', - 'context_mapping' => [ - 'user' => '@user.current_user_context:current_user', - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'test_context_aware', + 'context_mapping' => [ + 'user' => '@user.current_user_context:current_user', ], - ], - ], + ]), + ]), ], ], [ @@ -86,16 +83,11 @@ public function providerTestLayoutSectionFormatter() { $data['single_section_single_block'] = [ [ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'system_powered_by_block', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'system_powered_by_block', + ]), + ]), ], ], '.layout--onecol', @@ -107,37 +99,23 @@ public function providerTestLayoutSectionFormatter() { $data['multiple_sections'] = [ [ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'system_powered_by_block', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'system_powered_by_block', + ]), + ]), ], [ - 'layout' => 'layout_twocol', - 'section' => [ - 'first' => [ - 'foo' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'foo text', - ], - ], - ], - 'second' => [ - 'bar' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'bar text', - ], - ], - ], - ], + 'section' => new Section('layout_twocol', [], [ + 'foo' => new SectionComponent('foo', 'first', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'foo text', + ]), + 'bar' => new SectionComponent('bar', 'second', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'bar text', + ]), + ]), ], ], [ @@ -177,16 +155,11 @@ public function testLayoutSectionFormatter($layout_data, $expected_selector, $ex public function testLayoutSectionFormatterAccess() { $node = $this->createSectionNode([ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'test_access', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'test_access', + ]), + ]), ], ]); @@ -216,41 +189,27 @@ public function testMultilingualLayoutSectionFormatter() { $entity = $this->createSectionNode([ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'system_powered_by_block', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'system_powered_by_block', + ]), + ]), ], ]); $entity->addTranslation('es', [ 'title' => 'Translated node title', $this->fieldName => [ [ - 'layout' => 'layout_twocol', - 'section' => [ - 'first' => [ - 'foo' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'foo text', - ], - ], - ], - 'second' => [ - 'bar' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'bar text', - ], - ], - ], - ], + 'section' => new Section('layout_twocol', [], [ + 'foo' => new SectionComponent('foo', 'first', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'foo text', + ]), + 'bar' => new SectionComponent('bar', 'second', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'bar text', + ]), + ]), ], ], ]); diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php index c3a23c78710f..c1b340dea411 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php @@ -8,6 +8,7 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; +use Drupal\layout_builder\Section; /** * Ensures that Layout Builder and Field Layout are compatible with each other. @@ -108,13 +109,9 @@ public function testCompatibility() { $this->assertSame($original_markup, $new_markup); // Add a layout override. - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $entity->layout_builder__layout; - $field_list->appendItem([ - 'layout' => 'layout_onecol', - 'layout_settings' => [], - 'section' => [], - ]); + $field_list->appendSection(new Section('layout_onecol')); $entity->save(); // The rendered entity has now changed. The non-configurable field is shown diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php new file mode 100644 index 000000000000..4c8ae2e6c9a8 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php @@ -0,0 +1,39 @@ +installEntitySchema('entity_test_base_field_display'); + layout_builder_add_layout_section_field('entity_test_base_field_display', 'entity_test_base_field_display'); + + $entity = EntityTestBaseFieldDisplay::create([ + 'name' => 'The test entity', + 'layout_builder__layout' => $section_data, + ]); + $entity->save(); + return $entity->get('layout_builder__layout'); + } + +} diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php deleted file mode 100644 index 0e13471c1558..000000000000 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php +++ /dev/null @@ -1,89 +0,0 @@ -layout_builder__layout; - - // Test sample item generation. - $field_list->generateSampleItems(); - $this->entityValidateAndSave($entity); - - $field = $field_list->get(0); - $this->assertInstanceOf(LayoutSectionItemInterface::class, $field); - $this->assertInstanceOf(FieldItemInterface::class, $field); - $this->assertSame('section', $field->mainPropertyName()); - $this->assertSame('layout_onecol', $field->layout); - $this->assertSame([], $field->layout_settings); - $this->assertSame([], $field->section); - } - - /** - * {@inheritdoc} - */ - public function testLayoutSectionItemList() { - layout_builder_add_layout_section_field('entity_test', 'entity_test'); - - $entity = EntityTest::create(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ - $field_list = $entity->layout_builder__layout; - $this->assertInstanceOf(LayoutSectionItemListInterface::class, $field_list); - $this->assertInstanceOf(FieldItemListInterface::class, $field_list); - $entity->save(); - - $field_list->appendItem(['layout' => 'layout_twocol']); - $field_list->appendItem(['layout' => 'layout_onecol']); - $field_list->appendItem(['layout' => 'layout_threecol_25_50_25']); - $this->assertSame([ - ['layout' => 'layout_twocol'], - ['layout' => 'layout_onecol'], - ['layout' => 'layout_threecol_25_50_25'], - ], $field_list->getValue()); - - $field_list->addItem(1, ['layout' => 'layout_threecol_33_34_33']); - $this->assertSame([ - ['layout' => 'layout_twocol'], - ['layout' => 'layout_threecol_33_34_33'], - ['layout' => 'layout_onecol'], - ['layout' => 'layout_threecol_25_50_25'], - ], $field_list->getValue()); - - $field_list->addItem($field_list->count(), ['layout' => 'layout_twocol_bricks']); - $this->assertSame([ - ['layout' => 'layout_twocol'], - ['layout' => 'layout_threecol_33_34_33'], - ['layout' => 'layout_onecol'], - ['layout' => 'layout_threecol_25_50_25'], - ['layout' => 'layout_twocol_bricks'], - ], $field_list->getValue()); - } - -} diff --git a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php new file mode 100644 index 000000000000..b8e8b3462c35 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php @@ -0,0 +1,156 @@ + new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + ], + [ + 'section' => new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ], + ]; + $this->sectionStorage = $this->getEntity($section_data); + } + + /** + * Sets up the section storage entity. + * + * @param array $section_data + * An array of section data. + * + * @return \Drupal\Core\Entity\EntityInterface + * The entity. + */ + abstract protected function getEntity(array $section_data); + + /** + * @covers ::getSections + */ + public function testGetSections() { + $expected = [ + new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ]; + $this->assertSections($expected); + } + + /** + * @covers ::getSection + */ + public function testGetSection() { + $this->assertInstanceOf(Section::class, $this->sectionStorage->getSection(0)); + } + + /** + * @covers ::getSection + */ + public function testGetSectionInvalidDelta() { + $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2" for the "The test entity"'); + $this->sectionStorage->getSection(2); + } + + /** + * @covers ::insertSection + */ + public function testInsertSection() { + $expected = [ + new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + new Section('setting_1'), + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ]; + + $this->sectionStorage->insertSection(1, new Section('setting_1')); + $this->assertSections($expected); + } + + /** + * @covers ::appendSection + */ + public function testAppendSection() { + $expected = [ + new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + new Section('foo'), + ]; + + $this->sectionStorage->appendSection(new Section('foo')); + $this->assertSections($expected); + } + + /** + * @covers ::removeSection + */ + public function testRemoveSection() { + $expected = [ + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ]; + + $this->sectionStorage->removeSection(0); + $this->assertSections($expected); + } + + /** + * Asserts that the field list has the expected sections. + * + * @param \Drupal\layout_builder\Section[] $expected + * The expected sections. + */ + protected function assertSections(array $expected) { + $result = $this->sectionStorage->getSections(); + $this->assertEquals($expected, $result); + $this->assertSame(array_keys($expected), array_keys($result)); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php index 3e5b2ffad148..28f22557e75b 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php @@ -7,6 +7,8 @@ use Drupal\Core\Block\BlockManagerInterface; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Cache\Cache; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Layout\LayoutDefinition; use Drupal\Core\Layout\LayoutInterface; use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface; @@ -14,6 +16,7 @@ use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\layout_builder\LayoutSectionBuilder; +use Drupal\layout_builder\SectionComponent; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; @@ -86,7 +89,16 @@ protected function setUp() { $this->layoutSectionBuilder = new LayoutSectionBuilder($this->account->reveal(), $this->layoutPluginManager->reveal(), $this->blockManager->reveal(), $this->contextHandler->reveal(), $this->contextRepository->reveal()); $this->layout = $this->prophesize(LayoutInterface::class); + $this->layout->getPluginDefinition()->willReturn(new LayoutDefinition([])); + $this->layout->build(Argument::type('array'))->willReturnArgument(0); $this->layoutPluginManager->createInstance('layout_onecol', [])->willReturn($this->layout->reveal()); + + $container = new ContainerBuilder(); + $container->set('current_user', $this->account->reveal()); + $container->set('plugin.manager.block', $this->blockManager->reveal()); + $container->set('context.handler', $this->contextHandler->reveal()); + $container->set('context.repository', $this->contextRepository->reveal()); + \Drupal::setContainer($container); } /** @@ -102,8 +114,12 @@ public function testBuildSection() { '#base_plugin_id' => 'block_plugin_id', '#derivative_plugin_id' => NULL, 'content' => $block_content, + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => -1, + ], ]; - $this->layout->build(['content' => ['some_uuid' => $render_array]])->willReturnArgument(0); $block = $this->prophesize(BlockPluginInterface::class); $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); @@ -120,20 +136,9 @@ public function testBuildSection() { $block->getConfiguration()->willReturn([]); $section = [ - 'content' => [ - 'some_uuid' => [ - 'block' => [ - 'id' => 'block_plugin_id', - ], - ], - ], + new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), ]; $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], 'content' => [ 'some_uuid' => $render_array, ], @@ -146,7 +151,6 @@ public function testBuildSection() { * @covers ::buildSection */ public function testBuildSectionAccessDenied() { - $this->layout->build([])->willReturn([]); $block = $this->prophesize(BlockPluginInterface::class); $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); @@ -156,21 +160,19 @@ public function testBuildSectionAccessDenied() { $block->build()->shouldNotBeCalled(); $section = [ + new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), + ]; + $expected = [ 'content' => [ 'some_uuid' => [ - 'block' => [ - 'id' => 'block_plugin_id', + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => -1, ], ], ], ]; - $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], - ]; $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); $this->assertEquals($expected, $result); } @@ -179,23 +181,14 @@ public function testBuildSectionAccessDenied() { * @covers ::buildSection */ public function testBuildSectionEmpty() { - $this->layout->build([])->willReturn([]); - $section = []; - $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], - ]; + $expected = []; $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); $this->assertEquals($expected, $result); } /** * @covers ::buildSection - * @covers ::getBlock */ public function testContextAwareBlock() { $render_array = [ @@ -206,8 +199,12 @@ public function testContextAwareBlock() { '#base_plugin_id' => 'block_plugin_id', '#derivative_plugin_id' => NULL, 'content' => [], + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => -1, + ], ]; - $this->layout->build(['content' => ['some_uuid' => $render_array]])->willReturnArgument(0); $block = $this->prophesize(BlockPluginInterface::class)->willImplement(ContextAwarePluginInterface::class); $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); @@ -228,20 +225,9 @@ public function testContextAwareBlock() { $this->contextHandler->applyContextMapping($block->reveal(), [])->shouldBeCalled(); $section = [ - 'content' => [ - 'some_uuid' => [ - 'block' => [ - 'id' => 'block_plugin_id', - ], - ], - ], + new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), ]; $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], 'content' => [ 'some_uuid' => $render_array, ], @@ -252,50 +238,10 @@ public function testContextAwareBlock() { /** * @covers ::buildSection - * @covers ::getBlock */ public function testBuildSectionMissingPluginId() { - $section = [ - 'content' => [ - 'some_uuid' => [ - 'block' => [], - ], - ], - ]; - $this->setExpectedException(PluginException::class, 'No plugin ID specified for block with "some_uuid" UUID'); - $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); - } - - /** - * @covers ::buildSection - * - * @dataProvider providerTestBuildSectionMalformedData - */ - public function testBuildSectionMalformedData($section, $message) { - $this->layout->build(Argument::type('array'))->willReturnArgument(0); - $this->layout->getPluginId()->willReturn('the_plugin_id'); - $this->setExpectedException(\InvalidArgumentException::class, $message); - $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); - } - - /** - * Provides test data for ::testBuildSectionMalformedData(). - */ - public function providerTestBuildSectionMalformedData() { - $data = []; - $data['invalid_region'] = [ - ['content' => 'bar'], - 'The "content" region in the "the_plugin_id" layout has invalid configuration', - ]; - $data['invalid_configuration'] = [ - ['content' => ['some_uuid' => 'bar']], - 'The block with UUID of "some_uuid" has invalid configuration', - ]; - $data['invalid_blocks'] = [ - ['content' => ['some_uuid' => []]], - 'The block with UUID of "some_uuid" has invalid configuration', - ]; - return $data; + $this->setExpectedException(PluginException::class, 'No plugin ID specified for component with "some_uuid" UUID'); + $this->layoutSectionBuilder->buildSection('layout_onecol', [], [new SectionComponent('some_uuid', 'content')]); } } diff --git a/core/modules/layout_builder/tests/src/Unit/SectionTest.php b/core/modules/layout_builder/tests/src/Unit/SectionTest.php index 33a338706e27..eff239507d2b 100644 --- a/core/modules/layout_builder/tests/src/Unit/SectionTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\layout_builder\Unit; use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionComponent; use Drupal\Tests\UnitTestCase; /** @@ -24,242 +25,158 @@ class SectionTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $this->section = new Section([ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + $this->section = new Section('layout_onecol', [], [ + new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']), + (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), ]); } /** * @covers ::__construct - * @covers ::getValue + * @covers ::setComponent + * @covers ::getComponents */ - public function testGetValue() { + public function testGetComponents() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), ]; - $result = $this->section->getValue(); - $this->assertSame($expected, $result); - } - /** - * @covers ::getBlock - */ - public function testGetBlockInvalidRegion() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region'); - $this->section->getBlock('invalid-region', 'existing-uuid'); + $this->assertComponents($expected, $this->section); } /** - * @covers ::getBlock + * @covers ::getComponent */ - public function testGetBlockInvalidUuid() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID'); - $this->section->getBlock('some-region', 'invalid-uuid'); + public function testGetComponentInvalidUuid() { + $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID "invalid-uuid"'); + $this->section->getComponent('invalid-uuid'); } /** - * @covers ::getBlock + * @covers ::getComponent */ - public function testGetBlock() { - $expected = ['block' => ['id' => 'existing-block-id']]; + public function testGetComponent() { + $expected = new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']); - $block = $this->section->getBlock('some-region', 'existing-uuid'); - $this->assertSame($expected, $block); + $this->assertEquals($expected, $this->section->getComponent('existing-uuid')); } /** - * @covers ::removeBlock + * @covers ::removeComponent + * @covers ::getComponentsByRegion */ - public function testRemoveBlock() { - $this->section->removeBlock('some-region', 'existing-uuid'); + public function testRemoveComponent() { $expected = [ - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->removeComponent('first-uuid'); + $this->assertComponents($expected, $this->section); } /** - * @covers ::addBlock + * @covers ::appendComponent + * @covers ::getNextHighestWeight + * @covers ::getComponentsByRegion */ - public function testAddBlock() { - $this->section->addBlock('some-region', 'new-uuid', []); + public function testAppendComponent() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'new-uuid' => [], - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), + 'new-uuid' => (new SectionComponent('new-uuid', 'some-region', []))->setWeight(1), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->appendComponent(new SectionComponent('new-uuid', 'some-region')); + $this->assertComponents($expected, $this->section); } /** - * @covers ::insertBlock + * @covers ::insertAfterComponent */ - public function testInsertBlock() { - $this->section->insertBlock('ordered-region', 'new-uuid', [], 'first-uuid'); + public function testInsertAfterComponent() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'new-uuid' => [], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(4), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), + 'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(3), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->insertAfterComponent('first-uuid', new SectionComponent('new-uuid', 'ordered-region')); + $this->assertComponents($expected, $this->section); } /** - * @covers ::insertBlock + * @covers ::insertAfterComponent */ - public function testInsertBlockInvalidRegion() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region'); - $this->section->insertBlock('invalid-region', 'new-uuid', [], 'first-uuid'); + public function testInsertAfterComponentValidUuidRegionMismatch() { + $this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID "existing-uuid"'); + $this->section->insertAfterComponent('existing-uuid', new SectionComponent('new-uuid', 'ordered-region')); } /** - * @covers ::insertBlock + * @covers ::insertAfterComponent */ - public function testInsertBlockInvalidUuid() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID'); - $this->section->insertBlock('ordered-region', 'new-uuid', [], 'invalid-uuid'); + public function testInsertAfterComponentInvalidUuid() { + $this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID "invalid-uuid"'); + $this->section->insertAfterComponent('invalid-uuid', new SectionComponent('new-uuid', 'ordered-region')); } /** - * @covers ::updateBlock + * @covers ::insertComponent + * @covers ::getComponentsByRegion */ - public function testUpdateBlock() { - $this->section->updateBlock('some-region', 'existing-uuid', [ - 'block' => [ - 'id' => 'existing-block-id', - 'settings' => [ - 'foo' => 'bar', - ], - ], - ]); + public function testInsertComponent() { + $expected = [ + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(4), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(3), + 'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(2), + ]; + + $this->section->insertComponent(0, new SectionComponent('new-uuid', 'ordered-region')); + $this->assertComponents($expected, $this->section); + } + /** + * @covers ::insertComponent + */ + public function testInsertComponentAppend() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - 'settings' => [ - 'foo' => 'bar', - ], - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), + 'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(4), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->insertComponent(2, new SectionComponent('new-uuid', 'ordered-region')); + $this->assertComponents($expected, $this->section); } /** - * @covers ::updateBlock + * @covers ::insertComponent */ - public function testUpdateBlockInvalidRegion() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region'); - $this->section->updateBlock('invalid-region', 'new-uuid', []); + public function testInsertComponentInvalidDelta() { + $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "7" for the "new-uuid" component'); + $this->section->insertComponent(7, new SectionComponent('new-uuid', 'ordered-region')); } /** - * @covers ::updateBlock + * Asserts that the section has the expected components. + * + * @param \Drupal\layout_builder\SectionComponent[] $expected + * The expected sections. + * @param \Drupal\layout_builder\Section $section + * The section storage to check. */ - public function testUpdateBlockInvalidUuid() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID'); - $this->section->updateBlock('ordered-region', 'new-uuid', []); + protected function assertComponents(array $expected, Section $section) { + $result = $section->getComponents(); + $this->assertEquals($expected, $result); + $this->assertSame(array_keys($expected), array_keys($result)); } } From 8e9f1b61208087991afb783bd7d920f1e39fbcae Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Wed, 20 Dec 2017 09:53:54 -0800 Subject: [PATCH 047/232] Issue #2929496 by plach, hchonov, timmillwood, catch, Wim Leers, amateescu, Calystod, andypost: Add dedicated interfaces to group methods dealing with revision translation and clean up the related documentation --- .../Core/Entity/ContentEntityInterface.php | 67 +------------------ .../Entity/ContentEntityStorageInterface.php | 19 +----- .../Core/Entity/Entity/EntityViewDisplay.php | 4 +- .../Drupal/Core/Entity/EntityRepository.php | 4 +- .../Drupal/Core/Entity/EntityViewBuilder.php | 4 +- .../Core/Entity/TranslatableInterface.php | 20 ++++++ .../TranslatableRevisionableInterface.php | 65 ++++++++++++++++++ ...anslatableRevisionableStorageInterface.php | 9 +++ .../Entity/TranslatableStorageInterface.php | 30 +++++++++ core/lib/Drupal/Core/Entity/entity.api.php | 66 +++++++++++++++--- 10 files changed, 187 insertions(+), 101 deletions(-) create mode 100644 core/lib/Drupal/Core/Entity/TranslatableInterface.php create mode 100644 core/lib/Drupal/Core/Entity/TranslatableRevisionableInterface.php create mode 100644 core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php create mode 100644 core/lib/Drupal/Core/Entity/TranslatableStorageInterface.php diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php index 3053d23694d9..f43bc3b453d2 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php @@ -2,8 +2,6 @@ namespace Drupal\Core\Entity; -use Drupal\Core\TypedData\TranslatableInterface; - /** * Defines a common interface for all content entity objects. * @@ -20,70 +18,7 @@ * * @ingroup entity_api */ -interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, RevisionableInterface, TranslatableInterface { - - /** - * Determines if the current translation of the entity has unsaved changes. - * - * @return bool - * TRUE if the current translation of the entity has changes. - */ - public function hasTranslationChanges(); - - /** - * Marks the current revision translation as affected. - * - * Setting the revision translation affected flag through the setter or - * through the field directly will always enforce it, which will be used by - * the entity storage to determine if the flag should be recomputed or the set - * value should be used instead. - * @see \Drupal\Core\Entity\ContentEntityStorageBase::populateAffectedRevisionTranslations() - * - * @param bool|null $affected - * The flag value. A NULL value can be specified to reset the current value - * and make sure a new value will be computed by the system. - * - * @return $this - */ - public function setRevisionTranslationAffected($affected); - - /** - * Checks whether the current translation is affected by the current revision. - * - * @return bool - * TRUE if the entity object is affected by the current revision, FALSE - * otherwise. - */ - public function isRevisionTranslationAffected(); - - /** - * Checks if the revision translation affected flag value has been enforced. - * - * @return bool - * TRUE if revision translation affected flag is enforced, FALSE otherwise. - * - * @internal - */ - public function isRevisionTranslationAffectedEnforced(); - - /** - * Enforces the revision translation affected flag value. - * - * Note that this method call will not have any influence on the storage if - * the value of the revision translation affected flag is NULL which is used - * as an indication for the storage to recompute the flag. - * @see \Drupal\Core\Entity\ContentEntityInterface::setRevisionTranslationAffected() - * - * @param bool $enforced - * If TRUE, the value of the revision translation affected flag will be - * enforced so that on entity save the entity storage will not recompute it. - * Otherwise the storage will recompute it. - * - * @return $this - * - * @internal - */ - public function setRevisionTranslationAffectedEnforced($enforced); +interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, TranslatableRevisionableInterface { /** * Gets the loaded Revision ID of the entity. diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php index 678937bdc004..9b35a84bef19 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php @@ -5,24 +5,7 @@ /** * A storage that supports content entity types. */ -interface ContentEntityStorageInterface extends EntityStorageInterface, RevisionableStorageInterface { - - /** - * Constructs a new entity translation object, without permanently saving it. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The entity object being translated. - * @param string $langcode - * The translation language code. - * @param array $values - * (optional) An associative array of initial field values keyed by field - * name. If none is provided default values will be applied. - * - * @return \Drupal\Core\Entity\ContentEntityInterface - * A new entity translation object. - */ - public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []); - +interface ContentEntityStorageInterface extends EntityStorageInterface, TranslatableRevisionableStorageInterface { /** * Creates an entity with sample field values. diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php index 1604e311132d..7c7d62e04da3 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php @@ -8,7 +8,7 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\EntityDisplayBase; -use Drupal\Core\TypedData\TranslatableInterface; +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; /** * Configuration entity that contains display options for all components of a @@ -253,7 +253,7 @@ public function buildMultiple(array $entities) { // those values using: // - the entity language if the entity is translatable, // - the current "content language" otherwise. - if ($entity instanceof TranslatableInterface && $entity->isTranslatable()) { + if ($entity instanceof TranslatableDataInterface && $entity->isTranslatable()) { $view_langcode = $entity->language()->getId(); } else { diff --git a/core/lib/Drupal/Core/Entity/EntityRepository.php b/core/lib/Drupal/Core/Entity/EntityRepository.php index 986cc50b5433..37a89d793ebd 100644 --- a/core/lib/Drupal/Core/Entity/EntityRepository.php +++ b/core/lib/Drupal/Core/Entity/EntityRepository.php @@ -5,7 +5,7 @@ use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\TypedData\TranslatableInterface; +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; /** * Provides several mechanisms for retrieving entities. @@ -82,7 +82,7 @@ public function loadEntityByConfigTarget($entity_type_id, $target) { public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = []) { $translation = $entity; - if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { + if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) { if (empty($langcode)) { $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); $entity->addCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]); diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index e95c01b2c843..faeb1473df48 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -11,7 +11,7 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\Element; use Drupal\Core\Theme\Registry; -use Drupal\Core\TypedData\TranslatableInterface; +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -189,7 +189,7 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode) { 'bin' => $this->cacheBin, ]; - if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) { + if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) { $build['#cache']['keys'][] = $entity->language()->getId(); } } diff --git a/core/lib/Drupal/Core/Entity/TranslatableInterface.php b/core/lib/Drupal/Core/Entity/TranslatableInterface.php new file mode 100644 index 000000000000..573310b6c2cb --- /dev/null +++ b/core/lib/Drupal/Core/Entity/TranslatableInterface.php @@ -0,0 +1,20 @@ + Date: Wed, 20 Dec 2017 20:49:05 +0000 Subject: [PATCH 048/232] Issue #2551259 by Mile23, gaurav.kapoor, Jo Fitzgerald, googletorp, Jeremy, xjm, webchick: Deprecate dead code locale_translation_manual_status() --- core/modules/locale/locale.pages.inc | 7 ++++- .../src/Kernel/LocaleDeprecationsTest.php | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 core/modules/locale/tests/src/Kernel/LocaleDeprecationsTest.php diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index d8d7659bf9ca..2f8957cc8814 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -13,9 +13,14 @@ use Symfony\Component\HttpFoundation\RedirectResponse; * * Manually checks the translation status without the use of cron. * - * @see locale_menu() + * @deprecated in Drupal 8.5.0 and will be removed before 9.0.0. It is unused by + * Drupal core. Duplicate this function in your own extension if you need its + * behavior. + * + * @see https://www.drupal.org/node/2931188 */ function locale_translation_manual_status() { + @trigger_error('locale_translation_manual_status() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. It is unused by Drupal core. Duplicate this function in your own extension if you need its behavior.', E_USER_DEPRECATED); module_load_include('compare.inc', 'locale'); // Check the translation status of all translatable projects in all languages. diff --git a/core/modules/locale/tests/src/Kernel/LocaleDeprecationsTest.php b/core/modules/locale/tests/src/Kernel/LocaleDeprecationsTest.php new file mode 100644 index 000000000000..b98bc0552a7a --- /dev/null +++ b/core/modules/locale/tests/src/Kernel/LocaleDeprecationsTest.php @@ -0,0 +1,30 @@ +assertNotNull(\locale_translation_manual_status()); + } + +} From be64202643a512a694c3bcb48d6fc6d38970abe3 Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Wed, 20 Dec 2017 14:14:36 -0800 Subject: [PATCH 049/232] Issue #2825487 by damiankloip, Wim Leers, garphy, cburschka, tedbow, dpovshed, tstoeckler, Munavijayalakshmi, Berdir, dawehner, e0ipso: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field --- core/modules/file/file.module | 10 ++ core/modules/file/src/ComputedFileUrl.php | 47 ++++++++++ core/modules/file/src/Entity/File.php | 2 +- .../Field/FieldFormatter/FileUriFormatter.php | 3 +- .../Plugin/Field/FieldType/FileUriItem.php | 39 ++++++++ .../tests/src/Kernel/ComputedFileUrlTest.php | 91 +++++++++++++++++++ .../file/tests/src/Kernel/FileUriItemTest.php | 40 ++++++++ .../hal/config/install/hal.settings.yml | 8 ++ core/modules/hal/config/schema/hal.schema.yml | 3 + core/modules/hal/hal.install | 12 +++ core/modules/hal/hal.services.yml | 3 +- .../src/Normalizer/FileEntityNormalizer.php | 24 ++++- .../File/FileHalJsonAnonTest.php | 38 +++++++- .../Media/MediaHalJsonAnonTest.php | 18 ++-- .../src/Functional/FileDenormalizeTest.php | 14 +++ .../tests/src/Kernel/FileNormalizeTest.php | 5 +- .../EntityResource/EntityResourceTestBase.php | 9 +- .../File/FileResourceTestBase.php | 1 + .../Listeners/DeprecationListenerTrait.php | 1 + 19 files changed, 348 insertions(+), 20 deletions(-) create mode 100644 core/modules/file/src/ComputedFileUrl.php create mode 100644 core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php create mode 100644 core/modules/file/tests/src/Kernel/ComputedFileUrlTest.php create mode 100644 core/modules/file/tests/src/Kernel/FileUriItemTest.php diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 88bc2990fcd9..013201119a71 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -48,6 +48,16 @@ function file_help($route_name, RouteMatchInterface $route_match) { } } +/** + * Implements hook_field_widget_info_alter(). + */ +function file_field_widget_info_alter(array &$info) { + // Allows using the 'uri' widget for the 'file_uri' field type, which uses it + // as the default widget. + // @see \Drupal\file\Plugin\Field\FieldType\FileUriItem + $info['uri']['field_types'][] = 'file_uri'; +} + /** * Loads file entities from the database. * diff --git a/core/modules/file/src/ComputedFileUrl.php b/core/modules/file/src/ComputedFileUrl.php new file mode 100644 index 000000000000..2eb012edc659 --- /dev/null +++ b/core/modules/file/src/ComputedFileUrl.php @@ -0,0 +1,47 @@ +url !== NULL) { + return $this->url; + } + + assert($this->getParent()->getEntity() instanceof FileInterface); + + $uri = $this->getParent()->getEntity()->getFileUri(); + $this->url = file_url_transform_relative(file_create_url($uri)); + + return $this->url; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->url = $value; + + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + +} diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index a9ade9e7d6ee..4060b773eed1 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -243,7 +243,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Filename')) ->setDescription(t('Name of the file with no path components.')); - $fields['uri'] = BaseFieldDefinition::create('uri') + $fields['uri'] = BaseFieldDefinition::create('file_uri') ->setLabel(t('URI')) ->setDescription(t('The URI to access the file (either local or remote).')) ->setSetting('max_length', 255) diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php index 055e32dc8b12..0facb7e03388 100644 --- a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php @@ -13,7 +13,8 @@ * id = "file_uri", * label = @Translation("File URI"), * field_types = { - * "uri" + * "uri", + * "file_uri", * } * ) */ diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php new file mode 100644 index 000000000000..33bc15ef03f1 --- /dev/null +++ b/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php @@ -0,0 +1,39 @@ +setLabel(t('Root-relative file URL')) + ->setComputed(TRUE) + ->setInternal(FALSE) + ->setClass(ComputedFileUrl::class); + + return $properties; + } + +} diff --git a/core/modules/file/tests/src/Kernel/ComputedFileUrlTest.php b/core/modules/file/tests/src/Kernel/ComputedFileUrlTest.php new file mode 100644 index 000000000000..d0f1e616ecc3 --- /dev/null +++ b/core/modules/file/tests/src/Kernel/ComputedFileUrlTest.php @@ -0,0 +1,91 @@ +prophesize(FileInterface::class); + $entity->getFileUri() + ->willReturn($this->testUrl); + + $parent = $this->prophesize(FieldItemInterface::class); + $parent->getEntity() + ->shouldBeCalledTimes(2) + ->willReturn($entity->reveal()); + + $definition = $this->prophesize(DataDefinitionInterface::class); + + $typed_data = new ComputedFileUrl($definition->reveal(), $this->randomMachineName(), $parent->reveal()); + + $expected = base_path() . $this->siteDirectory . '/files/druplicon.txt'; + + $this->assertSame($expected, $typed_data->getValue()); + // Do this a second time to confirm the same value is returned but the value + // isn't retrieved from the parent entity again. + $this->assertSame($expected, $typed_data->getValue()); + } + + /** + * @covers ::setValue + */ + public function testSetValue() { + $name = $this->randomMachineName(); + $parent = $this->prophesize(FieldItemInterface::class); + $parent->onChange($name) + ->shouldBeCalled(); + + $definition = $this->prophesize(DataDefinitionInterface::class); + $typed_data = new ComputedFileUrl($definition->reveal(), $name, $parent->reveal()); + + // Setting the value explicitly should mean the parent entity is never + // called into. + $typed_data->setValue($this->testUrl); + + $this->assertSame($this->testUrl, $typed_data->getValue()); + // Do this a second time to confirm the same value is returned but the value + // isn't retrieved from the parent entity again. + $this->assertSame($this->testUrl, $typed_data->getValue()); + } + + /** + * @covers ::setValue + */ + public function testSetValueNoNotify() { + $name = $this->randomMachineName(); + $parent = $this->prophesize(FieldItemInterface::class); + $parent->onChange($name) + ->shouldNotBeCalled(); + + $definition = $this->prophesize(DataDefinitionInterface::class); + $typed_data = new ComputedFileUrl($definition->reveal(), $name, $parent->reveal()); + + // Setting the value should explicitly should mean the parent entity is + // never called into. + $typed_data->setValue($this->testUrl, FALSE); + + $this->assertSame($this->testUrl, $typed_data->getValue()); + } + +} diff --git a/core/modules/file/tests/src/Kernel/FileUriItemTest.php b/core/modules/file/tests/src/Kernel/FileUriItemTest.php new file mode 100644 index 000000000000..d67eba07070c --- /dev/null +++ b/core/modules/file/tests/src/Kernel/FileUriItemTest.php @@ -0,0 +1,40 @@ + 1, + 'filename' => 'druplicon.txt', + 'uri' => $uri, + 'filemime' => 'text/plain', + 'status' => FILE_STATUS_PERMANENT, + ]); + file_put_contents($file->getFileUri(), 'hello world'); + + $file->save(); + + $this->assertSame($uri, $file->uri->value); + $expected_url = base_path() . $this->siteDirectory . '/files/druplicon.txt'; + $this->assertSame($expected_url, $file->uri->url); + } + +} diff --git a/core/modules/hal/config/install/hal.settings.yml b/core/modules/hal/config/install/hal.settings.yml index 67107af00d34..c62cb6a04819 100644 --- a/core/modules/hal/config/install/hal.settings.yml +++ b/core/modules/hal/config/install/hal.settings.yml @@ -1,3 +1,11 @@ # Set the domain for HAL type and relation links. # If left blank, the site's domain will be used. link_domain: ~ +# Before Drupal 8.5, the File entity 'uri' field value was overridden to return +# the absolute file URL instead of the actual (stream wrapper) URI. The default +# for new sites is now to return the actual URI as well as a root-relative file +# URL. Enable this setting to use the previous behavior. For existing sites, +# the previous behavior is kept by default. +# @see hal_update_8501() +# @see https://www.drupal.org/node/2925783 +bc_file_uri_as_url_normalizer: false diff --git a/core/modules/hal/config/schema/hal.schema.yml b/core/modules/hal/config/schema/hal.schema.yml index 3192d6758052..cad1bdb722b8 100644 --- a/core/modules/hal/config/schema/hal.schema.yml +++ b/core/modules/hal/config/schema/hal.schema.yml @@ -6,3 +6,6 @@ hal.settings: link_domain: type: string label: 'Domain of the relation' + bc_file_uri_as_url_normalizer: + type: boolean + label: 'Whether to retain pre Drupal 8.5 behavior of normalizing the File entity "uri" field value to an absolute URL.' diff --git a/core/modules/hal/hal.install b/core/modules/hal/hal.install index 78810e37f534..87944844325e 100644 --- a/core/modules/hal/hal.install +++ b/core/modules/hal/hal.install @@ -31,3 +31,15 @@ function hal_update_8301() { $hal_settings->set('link_domain', $link_domain); $hal_settings->save(TRUE); } + +/** + * Add hal.settings::bc_file_uri_as_url_normalizer configuration. + */ +function hal_update_8501() { + $config_factory = \Drupal::configFactory(); + $config_factory->getEditable('hal.settings') + ->set('bc_file_uri_as_url_normalizer', TRUE) + ->save(TRUE); + + return t('Backwards compatibility mode has been enabled for File entities\' HAL normalization of the "uri" field. Like before, it will continue to return only the absolute file URL. If you want the new behavior, which returns both the stored URI and a root-relative file URL, read the change record to learn how to opt in.'); +} diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml index b2c898fc5608..a877163c08de 100644 --- a/core/modules/hal/hal.services.yml +++ b/core/modules/hal/hal.services.yml @@ -14,9 +14,10 @@ services: - { name: normalizer, priority: 10 } serializer.normalizer.file_entity.hal: class: Drupal\hal\Normalizer\FileEntityNormalizer + deprecated: 'The "%service_id%" normalizer service is deprecated: it is obsolete, it only remains available for backwards compatibility.' + arguments: ['@entity.manager', '@http_client', '@hal.link_manager', '@module_handler', '@config.factory'] tags: - { name: normalizer, priority: 20 } - arguments: ['@entity.manager', '@http_client', '@hal.link_manager', '@module_handler'] serializer.normalizer.timestamp_item.hal: class: Drupal\hal\Normalizer\TimestampItemNormalizer tags: diff --git a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php index ec870e9e14c6..5186f8b25594 100644 --- a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php @@ -2,6 +2,7 @@ namespace Drupal\hal\Normalizer; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\hal\LinkManager\LinkManagerInterface; @@ -9,6 +10,8 @@ /** * Converts the Drupal entity object structure to a HAL array structure. + * + * @deprecated in Drupal 8.5.0, to be removed before Drupal 9.0.0. */ class FileEntityNormalizer extends ContentEntityNormalizer { @@ -26,6 +29,13 @@ class FileEntityNormalizer extends ContentEntityNormalizer { */ protected $httpClient; + /** + * The HAL settings config. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + protected $halSettings; + /** * Constructs a FileEntityNormalizer object. * @@ -37,11 +47,14 @@ class FileEntityNormalizer extends ContentEntityNormalizer { * The hypermedia link manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. */ - public function __construct(EntityManagerInterface $entity_manager, ClientInterface $http_client, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler) { + public function __construct(EntityManagerInterface $entity_manager, ClientInterface $http_client, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) { parent::__construct($link_manager, $entity_manager, $module_handler); $this->httpClient = $http_client; + $this->halSettings = $config_factory->get('hal.settings'); } /** @@ -49,8 +62,13 @@ public function __construct(EntityManagerInterface $entity_manager, ClientInterf */ public function normalize($entity, $format = NULL, array $context = []) { $data = parent::normalize($entity, $format, $context); - // Replace the file url with a full url for the file. - $data['uri'][0]['value'] = $this->getEntityUri($entity); + + $this->addCacheableDependency($context, $this->halSettings); + + if ($this->halSettings->get('bc_file_uri_as_url_normalizer')) { + // Replace the file url with a full url for the file. + $data['uri'][0]['value'] = $this->getEntityUri($entity); + } return $data; } diff --git a/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php index ff89f745903f..f3036a9de868 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\hal\Functional\EntityResource\File; +use Drupal\Core\Cache\Cache; use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\File\FileResourceTestBase; @@ -38,7 +39,11 @@ protected function getExpectedNormalizedEntity() { $normalization = $this->applyHalFieldNormalization($default_normalization); $url = file_create_url($this->entity->getFileUri()); - $normalization['uri'][0]['value'] = $url; + // @see \Drupal\Tests\hal\Functional\EntityResource\File\FileHalJsonAnonTest::testGetBcUriField() + if ($this->config('hal.settings')->get('bc_file_uri_as_url_normalizer')) { + $normalization['uri'][0]['value'] = $url; + } + $uid = $this->author->id(); return $normalization + [ @@ -90,6 +95,13 @@ protected function getNormalizedPostEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:hal.settings']); + } + /** * {@inheritdoc} */ @@ -100,6 +112,30 @@ protected function getExpectedCacheContexts() { ]; } + /** + * @see hal_update_8501() + */ + public function testGetBcUriField() { + $this->config('hal.settings')->set('bc_file_uri_as_url_normalizer', TRUE)->save(TRUE); + + $this->initAuthentication(); + $url = $this->getEntityResourceUrl(); + $url->setOption('query', ['_format' => static::$format]); + $request_options = $this->getAuthenticationRequestOptions('GET'); + $this->provisionEntityResource(); + $this->setUpAuthorization('GET'); + $response = $this->request('GET', $url, $request_options); + $expected = $this->getExpectedNormalizedEntity(); + static::recursiveKSort($expected); + $actual = $this->serializer->decode((string) $response->getBody(), static::$format); + static::recursiveKSort($actual); + $this->assertSame($expected, $actual); + + // Explicitly assert that $file->uri->value is an absolute file URL, unlike + // the default normalization. + $this->assertSame($this->baseUrl . '/' . $this->siteDirectory . '/files/drupal.txt', $actual['uri'][0]['value']); + } + /** * {@inheritdoc} */ diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php index c71b54e6e6c8..c9ee7733cfa3 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\hal\Functional\EntityResource\Media; +use Drupal\Core\Cache\Cache; use Drupal\file\Entity\File; use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; @@ -86,11 +87,6 @@ protected function getExpectedNormalizedEntity() { ], ], 'lang' => 'en', - 'uri' => [ - [ - 'value' => $file->url(), - ], - ], 'uuid' => [ [ 'value' => $file->uuid(), @@ -126,11 +122,6 @@ protected function getExpectedNormalizedEntity() { ], ], 'lang' => 'en', - 'uri' => [ - [ - 'value' => $thumbnail->url(), - ], - ], 'uuid' => [ [ 'value' => $thumbnail->uuid(), @@ -173,4 +164,11 @@ protected function getNormalizedPostEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:hal.settings']); + } + } diff --git a/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php index 05ee23489394..9c00902988d4 100644 --- a/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php +++ b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php @@ -20,6 +20,20 @@ class FileDenormalizeTest extends BrowserTestBase { */ public static $modules = ['hal', 'file', 'node']; + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // @todo Remove this work-around in https://www.drupal.org/node/1927648. + // @see hal_update_8501() + \Drupal::configFactory() + ->getEditable('hal.settings') + ->set('bc_file_uri_as_url_normalizer', TRUE) + ->save(TRUE); + } + /** * Tests file entity denormalization. */ diff --git a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php index c4b67f54e345..355c09d1a67e 100644 --- a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php @@ -44,7 +44,10 @@ public function testNormalize() { $expected_array = [ 'uri' => [ - ['value' => file_create_url($file->getFileUri())], + [ + 'value' => $file->getFileUri(), + 'url' => file_url_transform_relative(file_create_url($file->getFileUri())), + ], ], ]; diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 2962ef380ab2..6d0d0bd18070 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -461,8 +461,13 @@ public function testGet() { // Note: deserialization of the XML format is not supported, so only test // this for other formats. if (static::$format !== 'xml') { - $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format); - $this->assertSame($unserialized->uuid(), $this->entity->uuid()); + // @todo Work-around for HAL's FileEntityNormalizer::denormalize() being + // broken, being fixed in https://www.drupal.org/node/1927648, where this + // if-test should be removed. + if (!(static::$entityTypeId === 'file' && static::$format === 'hal_json')) { + $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format); + $this->assertSame($unserialized->uuid(), $this->entity->uuid()); + } } // Finally, assert that the expected 'Link' headers are present. if ($this->entity->getEntityType()->getLinkTemplates()) { diff --git a/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php index c63853e0bf43..1fa5a3856bb7 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php @@ -154,6 +154,7 @@ protected function getExpectedNormalizedEntity() { ], 'uri' => [ [ + 'url' => base_path() . $this->siteDirectory . '/files/drupal.txt', 'value' => 'public://drupal.txt', ], ], diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index 094fcf10174d..b7445fb89965 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -122,6 +122,7 @@ public static function getSkippedDeprecations() { 'drupal_set_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead. See https://www.drupal.org/node/2774931', 'drupal_get_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::all() or \Drupal\Core\Messenger\MessengerInterface::messagesByType() instead. See https://www.drupal.org/node/2774931', 'Adding or retrieving messages prior to the container being initialized was deprecated in Drupal 8.5.0 and this functionality will be removed before Drupal 9.0.0. Please report this usage at https://www.drupal.org/node/2928994.', + 'The "serializer.normalizer.file_entity.hal" normalizer service is deprecated: it is obsolete, it only remains available for backwards compatibility.', ]; } From 4851f311e25ef59dc0b0742fe05e7560e4b1b79d Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 21 Dec 2017 10:49:53 +0000 Subject: [PATCH 050/232] Issue #2862671 by masipila, Jo Fitzgerald, kleog, phenaproxima, quietone: Add documentation to SqlBase source plugin --- .../src/Plugin/migrate/source/SqlBase.php | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php index 35ab8abd859e..08653b2332b6 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php @@ -17,35 +17,48 @@ /** * Sources whose data may be fetched via a database connection. * - * Database configuration, which may appear either within the source plugin - * configuration or in state, is structured as follows: + * Available configuration keys: + * - database_state_key: (optional) Name of the state key which contains an + * array with database connection information. + * - key: (optional) The database key name. Defaults to 'migrate'. + * - target: (optional) The database target name. Defaults to 'default'. + * - batch_size: (optional) Number of records to fetch from the database during + * each batch. If omitted, all records are fetched in a single query. + * - ignore_map: (optional) Source data is joined to the map table by default. + * If set to TRUE, the map table will not be joined. * - * 'key' - The database key name (defaults to 'migrate'). - * 'target' - The database target name (defaults to 'default'). - * 'database' - Database connection information as accepted by - * Database::addConnectionInfo(). If not present, the key/target is assumed - * to already be defined (e.g., in settings.php). + * For other optional configuration keys inherited from the parent class, refer + * to \Drupal\migrate\Plugin\migrate\source\SourcePluginBase. * - * This configuration info is obtained in the following order: + * About the source database determination: + * - If the source plugin configuration contains 'database_state_key', its value + * is taken as the name of a state key which contains an array with the + * database configuration. + * - Otherwise, if the source plugin configuration contains 'key', the database + * configuration with that name is used. + * - If both 'database_state_key' and 'key' are omitted in the source plugin + * configuration, the database connection named 'migrate' is used by default. + * - If all of the above steps fail, RequirementsException is thrown. * - * 1. If the source plugin configuration contains a key 'database_state_key', - * its value is taken as the name of a state key which contains an array - * with the above database configuration. - * 2. Otherwise, if the source plugin configuration contains 'key', the above - * database configuration is obtained directly from the plugin configuration. - * 3. Otherwise, if the state 'migrate.fallback_state_key' exists, its value is - * taken as the name of a state key which contains an array with the above - * database configuration. - * 4. Otherwise, if a connection named 'migrate' exists, that is used as the - * database connection. - * 5. Otherwise, RequirementsException is thrown. + * Drupal Database API supports multiple database connections. The connection + * parameters are defined in $databases array in settings.php or + * settings.local.php. It is also possible to modify the $databases array in + * runtime. For example, Migrate Drupal, which provides the migrations from + * Drupal 6 / 7, asks for the source database connection parameters in the UI + * and then adds the $databases['migrate'] connection in runtime before the + * migrations are executed. * - * It is strongly recommended that database connections be explicitly defined - * via 'database_state_key' or in the source plugin configuration. Defining - * migrate.fallback_state_key or a 'migrate' connection affects not only any - * migrations intended to use that particular connection, but all - * SqlBase-derived source plugins which do not have explicit database - * configuration. + * As described above, the default source database is $databases['migrate']. If + * the source plugin needs another source connection, the database connection + * parameters should be added to the $databases array as, for instance, + * $databases['foo']. The source plugin can then use this connection by setting + * 'key' to 'foo' in its configuration. + * + * For a complete example on migrating data from an SQL source, refer to + * https://www.drupal.org/docs/8/api/migrate-api/migrating-data-from-sql-source + * + * @see https://www.drupal.org/docs/8/api/database-api + * @see \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase */ abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPluginInterface, RequirementsInterface { From aee6bea5cd929088daa019639fae1b9ddda452f1 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 21 Dec 2017 10:52:32 +0000 Subject: [PATCH 051/232] Issue #2921033 by Jo Fitzgerald, masipila, phenaproxima, xjm, Wim Leers: Improve API documentation of DrupalSqlBase source plugin --- .../Plugin/migrate/source/DrupalSqlBase.php | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php index 445441befd60..c8b44f894e46 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php @@ -13,10 +13,19 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * A base source class for Drupal migrate sources. + * A base class for source plugins using a Drupal database as a source. * - * Mainly to let children retrieve information from the origin system in an - * easier way. + * Provides general purpose helper methods that are commonly needed + * when writing source plugins that use a Drupal database as a source, for + * example: + * - Check if the given module exists in the source database. + * - Read Drupal configuration variables from the source database. + * + * For a full list, refer to the methods of this class. + * + * For available configuration keys, refer to the parent classes: + * @see \Drupal\migrate\Plugin\migrate\source\SqlBase + * @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase */ abstract class DrupalSqlBase extends SqlBase implements ContainerFactoryPluginInterface, DependentPluginInterface { @@ -52,7 +61,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition } /** - * Retrieves all system data information from origin system. + * Retrieves all system data information from the source Drupal database. * * @return array * List of system table information keyed by type and name. @@ -109,7 +118,7 @@ public function checkRequirements() { } /** - * Get a module schema_version value in the source installation. + * Retrieves a module schema_version from the source Drupal database. * * @param string $module * Name of module. @@ -124,7 +133,7 @@ protected function getModuleSchemaVersion($module) { } /** - * Check to see if a given module is enabled in the source installation. + * Checks if a given module is enabled in the source Drupal database. * * @param string $module * Name of module to check. @@ -138,7 +147,7 @@ protected function moduleExists($module) { } /** - * Read a variable from a Drupal database. + * Reads a variable from a source Drupal database. * * @param $name * Name of the variable. From 6ac6325024a2e7bcf996115db4f8a16474c55c36 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 21 Dec 2017 10:55:00 +0000 Subject: [PATCH 052/232] Issue #2931339 by manuel.adan: Unused variable $admin_permission in EntityAccessControlHandler::checkAccess --- core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php index ac364115ebd6..c67c2ed3c30f 100644 --- a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php +++ b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php @@ -158,7 +158,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter return AccessResult::forbidden()->addCacheableDependency($entity); } if ($admin_permission = $this->entityType->getAdminPermission()) { - return AccessResult::allowedIfHasPermission($account, $this->entityType->getAdminPermission()); + return AccessResult::allowedIfHasPermission($account, $admin_permission); } else { // No opinion. From 93b9f5842664971cc82920a219503a28b0a69096 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 21 Dec 2017 10:57:43 +0000 Subject: [PATCH 053/232] Issue #2932044 by alexpott: Remove \PHPUnit_Util_XML::cssSelect() from \Drupal\tour\Tests\TourTestBase --- core/modules/tour/src/Tests/TourTest.php | 83 ++++++++++++++++++++ core/modules/tour/src/Tests/TourTestBase.php | 4 +- 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 core/modules/tour/src/Tests/TourTest.php diff --git a/core/modules/tour/src/Tests/TourTest.php b/core/modules/tour/src/Tests/TourTest.php new file mode 100644 index 000000000000..738bae8e2ae9 --- /dev/null +++ b/core/modules/tour/src/Tests/TourTest.php @@ -0,0 +1,83 @@ + [ + 'data-id' => 'tour-test-1', + 'data-class' => 'tour-test-1', + ], + ]; + + /** + * An admin user with administrative permissions for tour. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * The permissions required for a logged in user to test tour tips. + * + * @var array + * A list of permissions. + */ + protected $permissions = ['access tour']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Make sure we are using distinct default and administrative themes for + // the duration of these tests. + $this->container->get('theme_handler')->install(['bartik', 'seven']); + $this->config('system.theme') + ->set('default', 'bartik') + ->set('admin', 'seven') + ->save(); + + $this->permissions[] = 'view the administration theme'; + + // Create an admin user to view tour tips. + $this->adminUser = $this->drupalCreateUser($this->permissions); + $this->drupalLogin($this->adminUser); + + $this->drupalPlaceBlock('local_actions_block', [ + 'theme' => 'seven', + 'region' => 'content' + ]); + } + + /** + * A simple tip test. + */ + public function testTips() { + foreach ($this->tips as $path => $attributes) { + $this->drupalGet($path); + $this->assertTourTips($attributes); + } + } + +} diff --git a/core/modules/tour/src/Tests/TourTestBase.php b/core/modules/tour/src/Tests/TourTestBase.php index e13965070678..eca2ef8a1574 100644 --- a/core/modules/tour/src/Tests/TourTestBase.php +++ b/core/modules/tour/src/Tests/TourTestBase.php @@ -55,11 +55,11 @@ public function assertTourTips($tips = []) { $modals = 0; foreach ($tips as $tip) { if (!empty($tip['data-id'])) { - $elements = \PHPUnit_Util_XML::cssSelect('#' . $tip['data-id'], TRUE, $this->content, TRUE); + $elements = $this->xpath('//*[@id="' . $tip['data-id'] . '"]'); $this->assertTrue(!empty($elements) && count($elements) === 1, format_string('Found corresponding page element for tour tip with id #%data-id', ['%data-id' => $tip['data-id']])); } elseif (!empty($tip['data-class'])) { - $elements = \PHPUnit_Util_XML::cssSelect('.' . $tip['data-class'], TRUE, $this->content, TRUE); + $elements = $this->xpath('//*[contain(@class, "' . $tip['data-id'] . '")]'); $this->assertFalse(empty($elements), format_string('Found corresponding page element for tour tip with class .%data-class', ['%data-class' => $tip['data-class']])); } else { From 962122efc7d524ffb288791b33f18c22870334d9 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 21 Dec 2017 11:09:36 +0000 Subject: [PATCH 054/232] Issue #2884675 by rodrigoaguilera, joelpittet, TR: Remove twig uses of Twig_Node::getLine to Twig_Node::getTemplateLine --- core/lib/Drupal/Core/Template/TwigNodeTrans.php | 7 +++++-- core/lib/Drupal/Core/Template/TwigNodeVisitor.php | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/lib/Drupal/Core/Template/TwigNodeTrans.php b/core/lib/Drupal/Core/Template/TwigNodeTrans.php index 264a511172bf..c0691dbca6ad 100644 --- a/core/lib/Drupal/Core/Template/TwigNodeTrans.php +++ b/core/lib/Drupal/Core/Template/TwigNodeTrans.php @@ -157,7 +157,7 @@ protected function compileString(\Twig_Node $body) { if (!is_null($args)) { $argName = $args->getAttribute('name'); } - $expr = new \Twig_Node_Expression_Name($argName, $n->getLine()); + $expr = new \Twig_Node_Expression_Name($argName, $n->getTemplateLine()); } $placeholder = sprintf('%s%s', $argPrefix, $argName); $text .= $placeholder; @@ -176,7 +176,10 @@ protected function compileString(\Twig_Node $body) { $text = $body->getAttribute('data'); } - return [new \Twig_Node([new \Twig_Node_Expression_Constant(trim($text), $body->getLine())]), $tokens]; + return [ + new \Twig_Node([new \Twig_Node_Expression_Constant(trim($text), $body->getTemplateLine())]), + $tokens, + ]; } } diff --git a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php index 1ebfa57de92a..8a5dc05d7508 100644 --- a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php +++ b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php @@ -33,7 +33,7 @@ protected function doLeaveNode(\Twig_Node $node, \Twig_Environment $env) { return $node; } $class = get_class($node); - $line = $node->getLine(); + $line = $node->getTemplateLine(); return new $class( new \Twig_Node_Expression_Function('render_var', new \Twig_Node([$node->getNode('expr')]), $line), $line @@ -46,7 +46,8 @@ protected function doLeaveNode(\Twig_Node $node, \Twig_Environment $env) { // Use our own escape filter that is SafeMarkup aware. $node->getNode('filter')->setAttribute('value', 'drupal_escape'); - // Store that we have a filter active already that knows how to deal with render arrays. + // Store that we have a filter active already that knows + // how to deal with render arrays. $this->skipRenderVarFunction = TRUE; } } From 4e5b4b09bde155d50ed3b84ee1fa399347bb83ef Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 21 Dec 2017 11:49:17 +0000 Subject: [PATCH 055/232] Issue #2931368 by opdavies: Add missing param documentation for hook_migrate_prepare_row, document hook_migrate_MIGRATION_ID_prepare_row --- core/modules/migrate/migrate.api.php | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/modules/migrate/migrate.api.php b/core/modules/migrate/migrate.api.php index c6130334d09e..117c5e406205 100644 --- a/core/modules/migrate/migrate.api.php +++ b/core/modules/migrate/migrate.api.php @@ -98,6 +98,13 @@ * * hook_migrate_MIGRATION_ID_prepare_row() is also available. * + * @param \Drupal\migrate\Row $row + * The row being imported. + * @param \Drupal\migrate\Plugin\MigrateSourceInterface $source + * The source migration. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The current migration. + * * @ingroup migration */ function hook_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) { @@ -109,6 +116,28 @@ function hook_migrate_prepare_row(Row $row, MigrateSourceInterface $source, Migr } } +/** + * Allows adding data to a row for a migration with the specified ID. + * + * This provides the same functionality as hook_migrate_prepare_row() but + * removes the need to check the value of $migration->id(). + * + * @param \Drupal\migrate\Row $row + * The row being imported. + * @param \Drupal\migrate\Plugin\MigrateSourceInterface $source + * The source migration. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The current migration. + * + * @ingroup migration + */ +function hook_migrate_MIGRATION_ID_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) { + $value = $source->getDatabase()->query('SELECT value FROM {variable} WHERE name = :name', [':name' => 'mymodule_filter_foo_' . $row->getSourceProperty('format')])->fetchField(); + if ($value) { + $row->setSourceProperty('settings:mymodule:foo', unserialize($value)); + } +} + /** * Allows altering the list of discovered migration plugins. * From 03fd77c842e3606da859e5bb7c04b2da0b85c720 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Fri, 22 Dec 2017 12:47:46 +0000 Subject: [PATCH 056/232] Issue #2927806 by alexpott, mondrake, jibran, Mile23: Use PHPUnit 6 for testing when PHP version >= 7.2 --- composer.json | 6 +- core/composer.json | 2 +- core/lib/Drupal/Core/Composer/Composer.php | 26 ++++ .../DisplayVariant/BlockPageVariantTest.php | 4 +- .../tests/src/Unit/CommentLinkBuilderTest.php | 2 +- .../tests/src/Unit/MailHandlerTest.php | 2 +- .../EntityStateChangeValidationTest.php | 4 +- .../Kernel/Migrate/process/d6/CckFileTest.php | 1 + .../Plugin/migrate/process/d6/CckFileTest.php | 2 + .../migrate/cckfield/d7/LinkCckTest.php | 2 + .../Plugin/migrate/cckfield/LinkCckTest.php | 2 + .../tests/src/Kernel/process/DownloadTest.php | 7 -- .../tests/src/Unit/MigrateTestCase.php | 5 - .../PathBasedBreadcrumbBuilderTest.php | 2 - .../{LegacyTest.php => EarlyDateTest.php} | 4 +- ...mUpdateTest.php => BulkFormUpdateTest.php} | 2 +- .../src/Unit/Plugin/Block/ViewsBlockTest.php | 2 +- core/phpunit.xml.dist | 5 +- core/scripts/run-tests.sh | 6 +- .../FunctionalTests/BrowserTestBaseTest.php | 12 +- ...acyMessengerTest.php => MessengerTest.php} | 2 +- core/tests/Drupal/Tests/BrowserTestBase.php | 5 + .../Component/Datetime/DateTimePlusTest.php | 37 +++++- .../Tests/Component/Datetime/TimeTest.php | 3 +- .../DependencyInjection/ContainerTest.php | 112 +++++++++++++++--- .../Dumper/OptimizedPhpArrayDumperTest.php | 28 ++++- .../Component/Diff/Engine/DiffOpTest.php | 8 +- .../Discovery/YamlDirectoryDiscoveryTest.php | 16 ++- .../ContainerAwareEventDispatcherTest.php | 4 +- .../FileCache/FileCacheFactoryTest.php | 8 +- .../Component/PhpStorage/FileStorageTest.php | 10 +- .../Component/Plugin/Context/ContextTest.php | 14 ++- .../Component/Plugin/DefaultFactoryTest.php | 56 +++++++-- .../Plugin/Discovery/DiscoveryTraitTest.php | 14 ++- .../StaticDiscoveryDecoratorTest.php | 7 +- .../Plugin/Factory/ReflectionFactoryTest.php | 7 +- .../Component/Serialization/YamlPeclTest.php | 7 +- .../Serialization/YamlSymfonyTest.php | 15 ++- .../Utility/ArgumentsResolverTest.php | 32 +++-- .../Tests/Component/Utility/ColorTest.php | 7 +- .../Tests/Component/Utility/CryptTest.php | 7 +- .../Tests/Component/Utility/HtmlTest.php | 7 +- .../Tests/Component/Utility/RandomTest.php | 14 ++- .../Tests/Component/Utility/RectangleTest.php | 14 ++- .../Component/Utility/SafeMarkupTest.php | 2 +- .../Tests/Component/Utility/UnicodeTest.php | 7 +- .../Tests/Component/Utility/UrlHelperTest.php | 7 +- ...ityFormDisplayAccessControlHandlerTest.php | 4 - .../Core/Entity/EntityListBuilderTest.php | 3 - .../KeyValueEntityStorageTest.php | 2 +- ...DrupalStandardsListenerDeprecationTest.php | 3 + .../Tests/Core/Logger/LoggerChannelTest.php | 6 +- .../Core/PathProcessor/PathProcessorTest.php | 6 - .../Tests/Core/Plugin/Context/ContextTest.php | 1 + .../Core/Render/BubbleableMetadataTest.php | 1 + .../Tests/PhpunitCompatibilityTrait.php | 25 ++++ core/tests/bootstrap.php | 17 +++ 57 files changed, 485 insertions(+), 131 deletions(-) rename core/modules/taxonomy/tests/src/Functional/{LegacyTest.php => EarlyDateTest.php} (95%) rename core/modules/views/tests/src/Functional/Update/{LegacyBulkFormUpdateTest.php => BulkFormUpdateTest.php} (94%) rename core/tests/Drupal/KernelTests/Core/Messenger/{LegacyMessengerTest.php => MessengerTest.php} (98%) diff --git a/composer.json b/composer.json index 71dcc9605cc5..3bfb0c777948 100644 --- a/composer.json +++ b/composer.json @@ -49,11 +49,11 @@ }, "scripts": { "pre-autoload-dump": "Drupal\\Core\\Composer\\Composer::preAutoloadDump", - "post-autoload-dump": [ - "Drupal\\Core\\Composer\\Composer::ensureHtaccess" - ], + "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": "@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/core/composer.json b/core/composer.json index 31bc51a0fa84..c4bb384b92a5 100644 --- a/core/composer.json +++ b/core/composer.json @@ -44,7 +44,7 @@ "jcalderonzumba/gastonjs": "^1.0.2", "jcalderonzumba/mink-phantomjs-driver": "^0.3.1", "mikey179/vfsStream": "^1.2", - "phpunit/phpunit": ">=4.8.35 <5", + "phpunit/phpunit": "^4.8.35 || ^6.1", "phpspec/prophecy": "^1.4", "symfony/css-selector": "~3.2.8", "symfony/phpunit-bridge": "^3.4.0@beta" diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php index 016f93a34856..5f3a4a92ab89 100644 --- a/core/lib/Drupal/Core/Composer/Composer.php +++ b/core/lib/Drupal/Core/Composer/Composer.php @@ -143,6 +143,32 @@ public static function ensureHtaccess(Event $event) { } } + /** + * Fires the drupal-phpunit-upgrade script event if necessary. + * + * @param \Composer\Script\Event $event + */ + public static function upgradePHPUnit(Event $event) { + $repository = $event->getComposer()->getRepositoryManager()->getLocalRepository(); + // This is, essentially, a null constraint. We only care whether the package + // is present in the vendor directory yet, but findPackage() requires it. + $constraint = new Constraint('>', ''); + $phpunit_package = $repository->findPackage('phpunit/phpunit', $constraint); + if (!$phpunit_package) { + // There is nothing to do. The user is probably installing using the + // --no-dev flag. + return; + } + + // 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) { + $event->getComposer() + ->getEventDispatcher() + ->dispatchScript('drupal-phpunit-upgrade'); + } + } + /** * Remove possibly problematic test files from vendored projects. * diff --git a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php index 15694113534b..5d3a20b6a6d4 100644 --- a/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php +++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php @@ -49,6 +49,7 @@ public function setUpDisplayVariant($configuration = [], $definition = []) { $container = new Container(); $cache_context_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager') ->disableOriginalConstructor() + ->setMethods(['assertValidTokens']) ->getMock(); $container->set('cache_contexts_manager', $cache_context_manager); $cache_context_manager->expects($this->any()) @@ -209,9 +210,6 @@ public function testBuild(array $blocks_config, $visible_block_count, array $exp $title_block_plugin = $this->getMock('Drupal\Core\Block\TitleBlockPluginInterface'); foreach ($blocks_config as $block_id => $block_config) { $block = $this->getMock('Drupal\block\BlockInterface'); - $block->expects($this->any()) - ->method('getContexts') - ->willReturn([]); $block->expects($this->atLeastOnce()) ->method('getPlugin') ->willReturn($block_config[1] ? $main_content_block_plugin : ($block_config[2] ? $messages_block_plugin : ($block_config[3] ? $title_block_plugin : $block_plugin))); diff --git a/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php b/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php index 8a0a0f5871f2..8aaae7560981 100644 --- a/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php +++ b/core/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php @@ -269,7 +269,7 @@ public function getLinkCombinations() { */ protected function getMockNode($has_field, $comment_status, $form_location, $comment_count) { $node = $this->getMock('\Drupal\node\NodeInterface'); - $node->expects($this->once()) + $node->expects($this->any()) ->method('hasField') ->willReturn($has_field); diff --git a/core/modules/contact/tests/src/Unit/MailHandlerTest.php b/core/modules/contact/tests/src/Unit/MailHandlerTest.php index 04a12b86df58..a96b0c69e124 100644 --- a/core/modules/contact/tests/src/Unit/MailHandlerTest.php +++ b/core/modules/contact/tests/src/Unit/MailHandlerTest.php @@ -367,7 +367,7 @@ protected function getAuthenticatedMockMessage($copy_sender = FALSE) { $recipient->expects($this->once()) ->method('getEmail') ->willReturn('user2@drupal.org'); - $recipient->expects($this->once()) + $recipient->expects($this->any()) ->method('getDisplayName') ->willReturn('user2'); $recipient->expects($this->once()) diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php index 2fefc0148c56..d72000b868be 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php @@ -217,7 +217,7 @@ public function testInvalidStateMultilingual() { /** * Tests that content without prior moderation information can be moderated. */ - public function testLegacyContent() { + public function testExistingContentWithNoModeration() { $node_type = NodeType::create([ 'type' => 'example', ]); @@ -251,7 +251,7 @@ public function testLegacyContent() { /** * Tests that content without prior moderation information can be translated. */ - public function testLegacyMultilingualContent() { + public function testExistingMultilingualContentWithNoModeration() { // Enable French. ConfigurableLanguage::createFromLangcode('fr')->save(); diff --git a/core/modules/file/tests/src/Kernel/Migrate/process/d6/CckFileTest.php b/core/modules/file/tests/src/Kernel/Migrate/process/d6/CckFileTest.php index e0e193450285..0ab8ba44fd9b 100644 --- a/core/modules/file/tests/src/Kernel/Migrate/process/d6/CckFileTest.php +++ b/core/modules/file/tests/src/Kernel/Migrate/process/d6/CckFileTest.php @@ -20,6 +20,7 @@ class CckFileTest extends MigrateDrupalTestBase { * Tests configurability of file migration name. * * @covers ::__construct + * @expectedDeprecation CckFile is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\process\d6\FieldFile instead. */ public function testConfigurableFileMigration() { $migration = Migration::create($this->container, [], 'custom_migration', []); diff --git a/core/modules/file/tests/src/Unit/Plugin/migrate/process/d6/CckFileTest.php b/core/modules/file/tests/src/Unit/Plugin/migrate/process/d6/CckFileTest.php index 5bad279c0dce..8781ef11b470 100644 --- a/core/modules/file/tests/src/Unit/Plugin/migrate/process/d6/CckFileTest.php +++ b/core/modules/file/tests/src/Unit/Plugin/migrate/process/d6/CckFileTest.php @@ -17,6 +17,8 @@ class CckFileTest extends UnitTestCase { /** * Tests that alt and title attributes are included in transformed values. + * + * @expectedDeprecation CckFile is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\process\d6\FieldFile instead. */ public function testTransformAltTitle() { $executable = $this->prophesize(MigrateExecutableInterface::class)->reveal(); diff --git a/core/modules/link/tests/src/Kernel/Plugin/migrate/cckfield/d7/LinkCckTest.php b/core/modules/link/tests/src/Kernel/Plugin/migrate/cckfield/d7/LinkCckTest.php index 896c15c9fca0..14345a60ef28 100644 --- a/core/modules/link/tests/src/Kernel/Plugin/migrate/cckfield/d7/LinkCckTest.php +++ b/core/modules/link/tests/src/Kernel/Plugin/migrate/cckfield/d7/LinkCckTest.php @@ -53,6 +53,8 @@ protected function setUp() { /** * @covers ::processCckFieldValues + * @expectedDeprecation CckFieldPluginBase is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase instead. + * @expectedDeprecation MigrateCckFieldInterface is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Annotation\MigrateField instead. */ public function testProcessCckFieldValues() { $this->plugin->processFieldInstance($this->migration); diff --git a/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php b/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php index 7029d72412e9..dc3fe7731ee4 100644 --- a/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php +++ b/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php @@ -46,6 +46,8 @@ protected function setUp() { /** * @covers ::processCckFieldValues + * @expectedDeprecation CckFieldPluginBase is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase instead. + * @expectedDeprecation MigrateCckFieldInterface is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Annotation\MigrateField instead. */ public function testProcessCckFieldValues() { $this->plugin->processCckFieldValues($this->migration, 'somefieldname', []); diff --git a/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php b/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php index 576539eb3fed..ee56e32c525d 100644 --- a/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php +++ b/core/modules/migrate/tests/src/Kernel/process/DownloadTest.php @@ -9,7 +9,6 @@ use Drupal\migrate\MigrateExecutableInterface; use Drupal\migrate\Row; use GuzzleHttp\Client; -use GuzzleHttp\Psr7\Response; /** * Tests the download process plugin. @@ -100,14 +99,8 @@ public function testWriteProtectedDestination() { * The local URI of the downloaded file. */ protected function doTransform($destination_uri, $configuration = []) { - // The HTTP client will return a file with contents 'It worked!' - $body = fopen('data://text/plain;base64,SXQgd29ya2VkIQ==', 'r'); - // Prepare a mock HTTP client. $this->container->set('http_client', $this->getMock(Client::class)); - $this->container->get('http_client') - ->method('get') - ->willReturn(new Response(200, [], $body)); // Instantiate the plugin statically so it can pull dependencies out of // the container. diff --git a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php index 20d7662d2901..558cecb94af0 100644 --- a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php +++ b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php @@ -78,11 +78,6 @@ protected function getMigration() { $configuration = &$this->migrationConfiguration; - $migration->method('getHighWaterProperty') - ->willReturnCallback(function () use ($configuration) { - return isset($configuration['high_water_property']) ? $configuration['high_water_property'] : ''; - }); - $migration->method('set') ->willReturnCallback(function ($argument, $value) use (&$configuration) { $configuration[$argument] = $value; diff --git a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php index 2fe4a8335d0a..a24313f4affb 100644 --- a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php +++ b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php @@ -134,8 +134,6 @@ protected function setUp() { ->disableOriginalConstructor() ->getMock(); $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE); - $cache_contexts_manager->expects($this->any()) - ->method('validate_tokens'); $container = new Container(); $container->set('cache_contexts_manager', $cache_contexts_manager); \Drupal::setContainer($container); diff --git a/core/modules/taxonomy/tests/src/Functional/LegacyTest.php b/core/modules/taxonomy/tests/src/Functional/EarlyDateTest.php similarity index 95% rename from core/modules/taxonomy/tests/src/Functional/LegacyTest.php rename to core/modules/taxonomy/tests/src/Functional/EarlyDateTest.php index 667ed5542fc6..e4490f3431aa 100644 --- a/core/modules/taxonomy/tests/src/Functional/LegacyTest.php +++ b/core/modules/taxonomy/tests/src/Functional/EarlyDateTest.php @@ -11,7 +11,7 @@ * * @group taxonomy */ -class LegacyTest extends TaxonomyTestBase { +class EarlyDateTest extends TaxonomyTestBase { /** * Modules to enable. @@ -51,7 +51,7 @@ protected function setUp() { /** * Test taxonomy functionality with nodes prior to 1970. */ - public function testTaxonomyLegacyNode() { + public function testTaxonomyEarlyDateNode() { // Posts an article with a taxonomy term and a date prior to 1970. $date = new DrupalDateTime('1969-01-01 00:00:00'); $edit = []; diff --git a/core/modules/views/tests/src/Functional/Update/LegacyBulkFormUpdateTest.php b/core/modules/views/tests/src/Functional/Update/BulkFormUpdateTest.php similarity index 94% rename from core/modules/views/tests/src/Functional/Update/LegacyBulkFormUpdateTest.php rename to core/modules/views/tests/src/Functional/Update/BulkFormUpdateTest.php index 75e2b54b65ec..5db54d2c67ec 100644 --- a/core/modules/views/tests/src/Functional/Update/LegacyBulkFormUpdateTest.php +++ b/core/modules/views/tests/src/Functional/Update/BulkFormUpdateTest.php @@ -10,7 +10,7 @@ * * @group views */ -class LegacyBulkFormUpdateTest extends UpdatePathTestBase { +class BulkFormUpdateTest extends UpdatePathTestBase { /** * {@inheritdoc} diff --git a/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php b/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php index 9323b04438b2..f9bd6d2f1b72 100644 --- a/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php @@ -70,7 +70,7 @@ protected function setUp() { $this->executable = $this->getMockBuilder('Drupal\views\ViewExecutable') ->disableOriginalConstructor() - ->setMethods(['buildRenderable', 'setDisplay', 'setItemsPerPage']) + ->setMethods(['buildRenderable', 'setDisplay', 'setItemsPerPage', 'getShowAdminLinks']) ->getMock(); $this->executable->expects($this->any()) ->method('setDisplay') diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 750c6e2b502e..963d921a7e82 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -8,8 +8,7 @@ + beStrictAboutChangesToGlobalState="true"> - + diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 5ebf9f0c9fba..407d87a34f6c 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -793,7 +793,11 @@ function simpletest_script_run_one_test($test_id, $test_class) { putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); } else { - putenv('SYMFONY_DEPRECATIONS_HELPER=strict'); + // Prevent deprecations caused by vendor code calling deprecated code. + // This also prevents mock objects in PHPUnit 6 triggering silenced + // deprecations from breaking the test suite. We should consider changing + // this to 'strict' once PHPUnit 4 is no longer used. + putenv('SYMFONY_DEPRECATIONS_HELPER=weak_vendors'); } if (is_subclass_of($test_class, TestCase::class)) { $status = simpletest_script_run_phpunit($test_id, $test_class); diff --git a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php index 95ee39fd3a78..38e610c3b566 100644 --- a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php +++ b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php @@ -188,7 +188,7 @@ public function testInvalidLinkNotExistsExact() { /** * Tests legacy text asserts. */ - public function testLegacyTextAsserts() { + public function testTextAsserts() { $this->drupalGet('test-encoded'); $dangerous = 'Bad html '; $sanitized = Html::escape($dangerous); @@ -202,7 +202,7 @@ public function testLegacyTextAsserts() { /** * Tests legacy field asserts which use xpath directly. */ - public function testLegacyXpathAsserts() { + public function testXpathAsserts() { $this->drupalGet('test-field-xpath'); $this->assertFieldsByValue($this->xpath("//h1[@class = 'page-title']"), NULL); $this->assertFieldsByValue($this->xpath('//table/tbody/tr[2]/td[1]'), 'one'); @@ -245,7 +245,7 @@ public function testLegacyXpathAsserts() { /** * Tests legacy field asserts using textfields. */ - public function testLegacyFieldAssertsForTextfields() { + public function testFieldAssertsForTextfields() { $this->drupalGet('test-field-xpath'); // *** 1. assertNoField(). @@ -387,7 +387,7 @@ public function testLegacyFieldAssertsForTextfields() { /** * Tests legacy field asserts for options field type. */ - public function testLegacyFieldAssertsForOptions() { + public function testFieldAssertsForOptions() { $this->drupalGet('test-field-xpath'); // Option field type. @@ -443,7 +443,7 @@ public function testLegacyFieldAssertsForOptions() { /** * Tests legacy field asserts for button field type. */ - public function testLegacyFieldAssertsForButton() { + public function testFieldAssertsForButton() { $this->drupalGet('test-field-xpath'); $this->assertFieldById('edit-save', NULL); @@ -485,7 +485,7 @@ public function testLegacyFieldAssertsForButton() { /** * Tests legacy field asserts for checkbox field type. */ - public function testLegacyFieldAssertsForCheckbox() { + public function testFieldAssertsForCheckbox() { $this->drupalGet('test-field-xpath'); // Part 1 - Test by name. diff --git a/core/tests/Drupal/KernelTests/Core/Messenger/LegacyMessengerTest.php b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php similarity index 98% rename from core/tests/Drupal/KernelTests/Core/Messenger/LegacyMessengerTest.php rename to core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php index 13f10e9c2eb0..362069756eae 100644 --- a/core/tests/Drupal/KernelTests/Core/Messenger/LegacyMessengerTest.php +++ b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php @@ -11,7 +11,7 @@ * @group Messenger * @coversDefaultClass \Drupal\Core\Messenger\LegacyMessenger */ -class LegacyMessengerTest extends KernelTestBase { +class MessengerTest extends KernelTestBase { /** * Retrieves the Messenger service from LegacyMessenger. diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index 93df3dbf1319..3260f6ea016b 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -497,6 +497,11 @@ protected function setUp() { if ($disable_gc) { gc_enable(); } + + // Ensure that the test is not marked as risky because of no assertions. In + // PHPUnit 6 tests that only make assertions using $this->assertSession() + // can be marked as risky. + $this->addToAssertionCount(1); } /** diff --git a/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php b/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php index 297125bf63ed..b89d351c755c 100644 --- a/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php +++ b/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php @@ -87,7 +87,13 @@ public function testDateDiff($input1, $input2, $absolute, \DateInterval $expecte * @dataProvider providerTestInvalidDateDiff */ public function testInvalidDateDiff($input1, $input2, $absolute) { - $this->setExpectedException(\BadMethodCallException::class, 'Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object'); + if (method_exists($this, 'expectException')) { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object'); + } + else { + $this->setExpectedException(\BadMethodCallException::class, 'Method Drupal\Component\Datetime\DateTimePlus::diff expects parameter 1 to be a \DateTime or \Drupal\Component\Datetime\DateTimePlus object'); + } $interval = $input1->diff($input2, $absolute); } @@ -104,7 +110,12 @@ public function testInvalidDateDiff($input1, $input2, $absolute) { * @dataProvider providerTestInvalidDateArrays */ public function testInvalidDateArrays($input, $timezone, $class) { - $this->setExpectedException($class); + if (method_exists($this, 'expectException')) { + $this->expectException($class); + } + else { + $this->setExpectedException($class); + } $this->assertInstanceOf( '\Drupal\Component\DateTimePlus', DateTimePlus::createFromArray($input, $timezone) @@ -242,7 +253,12 @@ public function testDateFormat($input, $timezone, $format, $format_date, $expect * @dataProvider providerTestInvalidDates */ public function testInvalidDates($input, $timezone, $format, $message, $class) { - $this->setExpectedException($class); + if (method_exists($this, 'expectException')) { + $this->expectException($class); + } + else { + $this->setExpectedException($class); + } DateTimePlus::createFromFormat($format, $input, $timezone); } @@ -800,7 +816,12 @@ public function testValidateFormat() { // Parse the same date with ['validate_format' => TRUE] and make sure we // get the expected exception. - $this->setExpectedException(\UnexpectedValueException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(\UnexpectedValueException::class); + } + else { + $this->setExpectedException(\UnexpectedValueException::class); + } $date = DateTimePlus::createFromFormat('Y-m-d H:i:s', '11-03-31 17:44:00', 'UTC', ['validate_format' => TRUE]); } @@ -859,7 +880,13 @@ public function testChainableNonChainable() { * @covers ::__call */ public function testChainableNonCallable() { - $this->setExpectedException(\BadMethodCallException::class, 'Call to undefined method Drupal\Component\Datetime\DateTimePlus::nonexistent()'); + if (method_exists($this, 'expectException')) { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Call to undefined method Drupal\Component\Datetime\DateTimePlus::nonexistent()'); + } + else { + $this->setExpectedException(\BadMethodCallException::class, 'Call to undefined method Drupal\Component\Datetime\DateTimePlus::nonexistent()'); + } $date = new DateTimePlus('now', 'Australia/Sydney'); $date->setTimezone(new \DateTimeZone('America/New_York'))->nonexistent(); } diff --git a/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php b/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php index 4a5fa80205c2..ee0a2af36f26 100644 --- a/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php +++ b/core/tests/Drupal/Tests/Component/Datetime/TimeTest.php @@ -37,8 +37,7 @@ class TimeTest extends TestCase { protected function setUp() { parent::setUp(); - $this->requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack'); - + $this->requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); $this->time = new Time($this->requestStack); } diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php index 0e6a1472cdc5..1b5b23f347e4 100644 --- a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php @@ -70,7 +70,12 @@ protected function setUp() { public function testConstruct() { $container_definition = $this->getMockContainerDefinition(); $container_definition['machine_format'] = !$this->machineFormat; - $this->setExpectedException(InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidArgumentException::class); + } + else { + $this->setExpectedException(InvalidArgumentException::class); + } $container = new $this->containerClass($container_definition); } @@ -93,7 +98,12 @@ public function testGetParameter() { * @covers ::getAlternatives */ public function testGetParameterIfNotFound() { - $this->setExpectedException(ParameterNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(ParameterNotFoundException::class); + } + else { + $this->setExpectedException(ParameterNotFoundException::class); + } $this->container->getParameter('parameter_that_does_not_exist'); } @@ -103,7 +113,12 @@ public function testGetParameterIfNotFound() { * @covers ::getParameter */ public function testGetParameterIfNotFoundBecauseNull() { - $this->setExpectedException(ParameterNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(ParameterNotFoundException::class); + } + else { + $this->setExpectedException(ParameterNotFoundException::class); + } $this->container->getParameter(NULL); } @@ -137,7 +152,12 @@ public function testSetParameterWithUnfrozenContainer() { */ public function testSetParameterWithFrozenContainer() { $this->container = new $this->containerClass($this->containerDefinition); - $this->setExpectedException(LogicException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(LogicException::class); + } + else { + $this->setExpectedException(LogicException::class); + } $this->container->setParameter('some_config', 'new_value'); } @@ -242,7 +262,12 @@ public function testHasForAliasedService() { * @covers ::createService */ public function testGetForCircularServices() { - $this->setExpectedException(ServiceCircularReferenceException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(ServiceCircularReferenceException::class); + } + else { + $this->setExpectedException(ServiceCircularReferenceException::class); + } $this->container->get('circular_dependency'); } @@ -255,7 +280,12 @@ public function testGetForCircularServices() { * @covers ::getServiceAlternatives */ public function testGetForNonExistantService() { - $this->setExpectedException(ServiceNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(ServiceNotFoundException::class); + } + else { + $this->setExpectedException(ServiceNotFoundException::class); + } $this->container->get('service_not_exists'); } @@ -304,7 +334,12 @@ public function testGetForParameterDependencyWithExceptionOnSecondCall() { // Reset the service. $this->container->set('service_parameter_not_exists', NULL); - $this->setExpectedException(InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidArgumentException::class); + } + else { + $this->setExpectedException(InvalidArgumentException::class); + } $this->container->get('service_parameter_not_exists'); } @@ -316,7 +351,12 @@ public function testGetForParameterDependencyWithExceptionOnSecondCall() { * @covers ::resolveServicesAndParameters */ public function testGetForNonExistantParameterDependencyWithException() { - $this->setExpectedException(InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidArgumentException::class); + } + else { + $this->setExpectedException(InvalidArgumentException::class); + } $this->container->get('service_parameter_not_exists'); } @@ -341,7 +381,12 @@ public function testGetForNonExistantServiceDependency() { * @covers ::getAlternatives */ public function testGetForNonExistantServiceDependencyWithException() { - $this->setExpectedException(ServiceNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(ServiceNotFoundException::class); + } + else { + $this->setExpectedException(ServiceNotFoundException::class); + } $this->container->get('service_dependency_not_exists'); } @@ -361,7 +406,12 @@ public function testGetForNonExistantServiceWhenUsingNull() { * @covers ::createService */ public function testGetForNonExistantNULLService() { - $this->setExpectedException(ServiceNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(ServiceNotFoundException::class); + } + else { + $this->setExpectedException(ServiceNotFoundException::class); + } $this->container->get(NULL); } @@ -387,7 +437,12 @@ public function testGetForNonExistantServiceMultipleTimes() { */ public function testGetForNonExistantServiceWithExceptionOnSecondCall() { $this->assertNull($this->container->get('service_not_exists', ContainerInterface::NULL_ON_INVALID_REFERENCE), 'Not found service does nto throw exception.'); - $this->setExpectedException(ServiceNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(ServiceNotFoundException::class); + } + else { + $this->setExpectedException(ServiceNotFoundException::class); + } $this->container->get('service_not_exists'); } @@ -423,7 +478,12 @@ public function testGetForSyntheticService() { * @covers ::createService */ public function testGetForSyntheticServiceWithException() { - $this->setExpectedException(RuntimeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(RuntimeException::class); + } + else { + $this->setExpectedException(RuntimeException::class); + } $this->container->get('synthetic'); } @@ -462,7 +522,12 @@ public function testGetForInstantiationWithVariousArgumentLengths() { * @covers ::createService */ public function testGetForWrongFactory() { - $this->setExpectedException(RuntimeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(RuntimeException::class); + } + else { + $this->setExpectedException(RuntimeException::class); + } $this->container->get('wrong_factory'); } @@ -500,7 +565,12 @@ public function testGetForFactoryClass() { * @covers ::createService */ public function testGetForConfiguratorWithException() { - $this->setExpectedException(InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidArgumentException::class); + } + else { + $this->setExpectedException(InvalidArgumentException::class); + } $this->container->get('configurable_service_exception'); } @@ -598,7 +668,12 @@ public function testResolveServicesAndParametersForOptionalServiceDependencies() * @covers ::resolveServicesAndParameters */ public function testResolveServicesAndParametersForInvalidArgument() { - $this->setExpectedException(InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidArgumentException::class); + } + else { + $this->setExpectedException(InvalidArgumentException::class); + } $this->container->get('invalid_argument_service'); } @@ -612,7 +687,12 @@ public function testResolveServicesAndParametersForInvalidArgument() { public function testResolveServicesAndParametersForInvalidArguments() { // In case the machine-optimized format is not used, we need to simulate the // test failure. - $this->setExpectedException(InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidArgumentException::class); + } + else { + $this->setExpectedException(InvalidArgumentException::class); + } if (!$this->machineFormat) { throw new InvalidArgumentException('Simulating the test failure.'); } diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php index 3c447527487c..c1b70954d11b 100644 --- a/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/Dumper/OptimizedPhpArrayDumperTest.php @@ -545,7 +545,12 @@ public function testGetServiceDefinitionForDecoratedService() { $services['bar'] = $bar_definition; $this->containerBuilder->getDefinitions()->willReturn($services); - $this->setExpectedException(InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidArgumentException::class); + } + else { + $this->setExpectedException(InvalidArgumentException::class); + } $this->dumper->getArray(); } @@ -562,7 +567,12 @@ public function testGetServiceDefinitionForExpression() { $services['bar'] = $bar_definition; $this->containerBuilder->getDefinitions()->willReturn($services); - $this->setExpectedException(RuntimeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(RuntimeException::class); + } + else { + $this->setExpectedException(RuntimeException::class); + } $this->dumper->getArray(); } @@ -579,7 +589,12 @@ public function testGetServiceDefinitionForObject() { $services['bar'] = $bar_definition; $this->containerBuilder->getDefinitions()->willReturn($services); - $this->setExpectedException(RuntimeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(RuntimeException::class); + } + else { + $this->setExpectedException(RuntimeException::class); + } $this->dumper->getArray(); } @@ -596,7 +611,12 @@ public function testGetServiceDefinitionForResource() { $services['bar'] = $bar_definition; $this->containerBuilder->getDefinitions()->willReturn($services); - $this->setExpectedException(RuntimeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(RuntimeException::class); + } + else { + $this->setExpectedException(RuntimeException::class); + } $this->dumper->getArray(); } diff --git a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php index 1a649ae510c7..dbbb6ec08176 100644 --- a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php +++ b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php @@ -4,6 +4,7 @@ use Drupal\Component\Diff\Engine\DiffOp; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Error\Error; /** * Test DiffOp base class. @@ -24,7 +25,12 @@ class DiffOpTest extends TestCase { * @covers ::reverse */ public function testReverse() { - $this->setExpectedException(\PHPUnit_Framework_Error::class); + if (method_exists($this, 'expectException')) { + $this->expectException(Error::class); + } + else { + $this->setExpectedException(\PHPUnit_Framework_Error::class); + } $op = new DiffOp(); $result = $op->reverse(); } diff --git a/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php b/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php index 86134a7bdf10..9ac807d744d0 100644 --- a/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php +++ b/core/tests/Drupal/Tests/Component/Discovery/YamlDirectoryDiscoveryTest.php @@ -124,7 +124,13 @@ public function testDiscoveryAlternateId() { * @covers ::getIdentifier */ public function testDiscoveryNoIdException() { - $this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains no data in the identifier key \'id\''); + if (method_exists($this, 'expectException')) { + $this->expectException(DiscoveryException::class); + $this->expectExceptionMessage('The vfs://modules/test_1/item_1.test.yml contains no data in the identifier key \'id\''); + } + else { + $this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains no data in the identifier key \'id\''); + } vfsStream::setup('modules', NULL, [ 'test_1' => [ 'item_1.test.yml' => "", @@ -144,7 +150,13 @@ public function testDiscoveryNoIdException() { * @covers ::findAll */ public function testDiscoveryInvalidYamlException() { - $this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains invalid YAML'); + if (method_exists($this, 'expectException')) { + $this->expectException(DiscoveryException::class); + $this->expectExceptionMessage('The vfs://modules/test_1/item_1.test.yml contains invalid YAML'); + } + else { + $this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains invalid YAML'); + } vfsStream::setup('modules', NULL, [ 'test_1' => [ 'item_1.test.yml' => "id: invalid\nfoo : [bar}", diff --git a/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php index ea0685a7171e..a750ecaf097b 100644 --- a/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php +++ b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php @@ -38,7 +38,7 @@ public function testGetListenersWithCallables() // When passing in callables exclusively as listeners into the event // dispatcher constructor, the event dispatcher must not attempt to // resolve any services. - $container = $this->getMock(ContainerInterface::class); + $container = $this->getMockBuilder(ContainerInterface::class)->getMock(); $container->expects($this->never())->method($this->anything()); $firstListener = new CallableClass(); @@ -73,7 +73,7 @@ public function testDispatchWithCallables() // When passing in callables exclusively as listeners into the event // dispatcher constructor, the event dispatcher must not attempt to // resolve any services. - $container = $this->getMock(ContainerInterface::class); + $container = $this->getMockBuilder(ContainerInterface::class)->getMock(); $container->expects($this->never())->method($this->anything()); $firstListener = new CallableClass(); diff --git a/core/tests/Drupal/Tests/Component/FileCache/FileCacheFactoryTest.php b/core/tests/Drupal/Tests/Component/FileCache/FileCacheFactoryTest.php index 995f5fc8510e..f9195984341a 100644 --- a/core/tests/Drupal/Tests/Component/FileCache/FileCacheFactoryTest.php +++ b/core/tests/Drupal/Tests/Component/FileCache/FileCacheFactoryTest.php @@ -59,7 +59,13 @@ public function testGet() { */ public function testGetNoPrefix() { FileCacheFactory::setPrefix(NULL); - $this->setExpectedException(\InvalidArgumentException::class, 'Required prefix configuration is missing'); + if (method_exists($this, 'expectException')) { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Required prefix configuration is missing'); + } + else { + $this->setExpectedException(\InvalidArgumentException::class, 'Required prefix configuration is missing'); + } FileCacheFactory::get('test_foo_settings', []); } diff --git a/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php index 52e1b83c032e..168d1603e3ce 100644 --- a/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php +++ b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageTest.php @@ -5,7 +5,7 @@ use Drupal\Component\PhpStorage\FileStorage; use Drupal\Component\Utility\Random; use org\bovigo\vfs\vfsStreamDirectory; -use PHPUnit_Framework_Error_Warning; +use PHPUnit\Framework\Error\Warning; /** * @coversDefaultClass \Drupal\Component\PhpStorage\FileStorage @@ -99,7 +99,13 @@ public function testCreateDirectoryFailWarning() { 'bin' => 'test', ]); $code = "setExpectedException(PHPUnit_Framework_Error_Warning::class, 'mkdir(): Permission Denied'); + if (method_exists($this, 'expectException')) { + $this->expectException(Warning::class); + $this->expectExceptionMessage('mkdir(): Permission Denied'); + } + else { + $this->setExpectedException(\PHPUnit_Framework_Error_Warning::class, 'mkdir(): Permission Denied'); + } $storage->save('subdirectory/foo.php', $code); } diff --git a/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php b/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php index 6a2cf4667321..aee8d0931893 100644 --- a/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php +++ b/core/tests/Drupal/Tests/Component/Plugin/Context/ContextTest.php @@ -71,10 +71,16 @@ public function testGetContextValue($expected, $context_value, $is_required, $da // Set expectation for exception. if ($is_required) { - $this->setExpectedException( - 'Drupal\Component\Plugin\Exception\ContextException', - sprintf("The %s context is required and not present.", $data_type) - ); + if (method_exists($this, 'expectException')) { + $this->expectException('Drupal\Component\Plugin\Exception\ContextException'); + $this->expectExceptionMessage(sprintf("The %s context is required and not present.", $data_type)); + } + else { + $this->setExpectedException( + 'Drupal\Component\Plugin\Exception\ContextException', + sprintf("The %s context is required and not present.", $data_type) + ); + } } // Exercise getContextValue(). diff --git a/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php b/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php index b6bb32c84154..de24172904be 100644 --- a/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php +++ b/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php @@ -35,7 +35,7 @@ public function testGetPluginClassWithValidArrayPluginDefinition() { */ public function testGetPluginClassWithValidObjectPluginDefinition() { $plugin_class = Cherry::class; - $plugin_definition = $this->getMock(PluginDefinitionInterface::class); + $plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock(); $plugin_definition->expects($this->atLeastOnce()) ->method('getClass') ->willReturn($plugin_class); @@ -50,7 +50,13 @@ public function testGetPluginClassWithValidObjectPluginDefinition() { * @covers ::getPluginClass */ public function testGetPluginClassWithMissingClassWithArrayPluginDefinition() { - $this->setExpectedException(PluginException::class, 'The plugin (cherry) did not specify an instance class.'); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginException::class); + $this->expectExceptionMessage('The plugin (cherry) did not specify an instance class.'); + } + else { + $this->setExpectedException(PluginException::class, 'The plugin (cherry) did not specify an instance class.'); + } DefaultFactory::getPluginClass('cherry', []); } @@ -60,8 +66,14 @@ public function testGetPluginClassWithMissingClassWithArrayPluginDefinition() { * @covers ::getPluginClass */ public function testGetPluginClassWithMissingClassWithObjectPluginDefinition() { - $plugin_definition = $this->getMock(PluginDefinitionInterface::class); - $this->setExpectedException(PluginException::class, 'The plugin (cherry) did not specify an instance class.'); + $plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock(); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginException::class); + $this->expectExceptionMessage('The plugin (cherry) did not specify an instance class.'); + } + else { + $this->setExpectedException(PluginException::class, 'The plugin (cherry) did not specify an instance class.'); + } DefaultFactory::getPluginClass('cherry', $plugin_definition); } @@ -71,7 +83,13 @@ public function testGetPluginClassWithMissingClassWithObjectPluginDefinition() { * @covers ::getPluginClass */ public function testGetPluginClassWithNotExistingClassWithArrayPluginDefinition() { - $this->setExpectedException(PluginException::class, 'Plugin (kiwifruit) instance class "\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit" does not exist.'); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginException::class); + $this->expectExceptionMessage('Plugin (kiwifruit) instance class "\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit" does not exist.'); + } + else { + $this->setExpectedException(PluginException::class, 'Plugin (kiwifruit) instance class "\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit" does not exist.'); + } DefaultFactory::getPluginClass('kiwifruit', ['class' => '\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit']); } @@ -82,11 +100,16 @@ public function testGetPluginClassWithNotExistingClassWithArrayPluginDefinition( */ public function testGetPluginClassWithNotExistingClassWithObjectPluginDefinition() { $plugin_class = '\Drupal\plugin_test\Plugin\plugin_test\fruit\Kiwifruit'; - $plugin_definition = $this->getMock(PluginDefinitionInterface::class); + $plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock(); $plugin_definition->expects($this->atLeastOnce()) ->method('getClass') ->willReturn($plugin_class); - $this->setExpectedException(PluginException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginException::class); + } + else { + $this->setExpectedException(PluginException::class); + } DefaultFactory::getPluginClass('kiwifruit', $plugin_definition); } @@ -109,7 +132,7 @@ public function testGetPluginClassWithInterfaceWithArrayPluginDefinition() { */ public function testGetPluginClassWithInterfaceWithObjectPluginDefinition() { $plugin_class = Cherry::class; - $plugin_definition = $this->getMock(PluginDefinitionInterface::class); + $plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock(); $plugin_definition->expects($this->atLeastOnce()) ->method('getClass') ->willReturn($plugin_class); @@ -125,7 +148,13 @@ public function testGetPluginClassWithInterfaceWithObjectPluginDefinition() { */ public function testGetPluginClassWithInterfaceAndInvalidClassWithArrayPluginDefinition() { $plugin_class = Kale::class; - $this->setExpectedException(PluginException::class, 'Plugin "cherry" (Drupal\plugin_test\Plugin\plugin_test\fruit\Kale) must implement interface Drupal\plugin_test\Plugin\plugin_test\fruit\FruitInterface.'); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginException::class); + $this->expectExceptionMessage('Plugin "cherry" (Drupal\plugin_test\Plugin\plugin_test\fruit\Kale) must implement interface Drupal\plugin_test\Plugin\plugin_test\fruit\FruitInterface.'); + } + else { + $this->setExpectedException(PluginException::class, 'Plugin "cherry" (Drupal\plugin_test\Plugin\plugin_test\fruit\Kale) must implement interface Drupal\plugin_test\Plugin\plugin_test\fruit\FruitInterface.'); + } DefaultFactory::getPluginClass('cherry', ['class' => $plugin_class, 'provider' => 'core'], FruitInterface::class); } @@ -136,11 +165,16 @@ public function testGetPluginClassWithInterfaceAndInvalidClassWithArrayPluginDef */ public function testGetPluginClassWithInterfaceAndInvalidClassWithObjectPluginDefinition() { $plugin_class = Kale::class; - $plugin_definition = $this->getMock(PluginDefinitionInterface::class); + $plugin_definition = $this->getMockBuilder(PluginDefinitionInterface::class)->getMock(); $plugin_definition->expects($this->atLeastOnce()) ->method('getClass') ->willReturn($plugin_class); - $this->setExpectedException(PluginException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginException::class); + } + else { + $this->setExpectedException(PluginException::class); + } DefaultFactory::getPluginClass('cherry', $plugin_definition, FruitInterface::class); } diff --git a/core/tests/Drupal/Tests/Component/Plugin/Discovery/DiscoveryTraitTest.php b/core/tests/Drupal/Tests/Component/Plugin/Discovery/DiscoveryTraitTest.php index c37a4b5a18d3..1a5c36e7b854 100644 --- a/core/tests/Drupal/Tests/Component/Plugin/Discovery/DiscoveryTraitTest.php +++ b/core/tests/Drupal/Tests/Component/Plugin/Discovery/DiscoveryTraitTest.php @@ -69,7 +69,12 @@ public function testDoGetDefinitionException($expected, $definitions, $plugin_id $method_ref = new \ReflectionMethod($trait, 'doGetDefinition'); $method_ref->setAccessible(TRUE); // Call doGetDefinition, with $exception_on_invalid always TRUE. - $this->setExpectedException(PluginNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginNotFoundException::class); + } + else { + $this->setExpectedException(PluginNotFoundException::class); + } $method_ref->invoke($trait, $definitions, $plugin_id, TRUE); } @@ -106,7 +111,12 @@ public function testGetDefinitionException($expected, $definitions, $plugin_id) ->method('getDefinitions') ->willReturn($definitions); // Call getDefinition(), with $exception_on_invalid always TRUE. - $this->setExpectedException(PluginNotFoundException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(PluginNotFoundException::class); + } + else { + $this->setExpectedException(PluginNotFoundException::class); + } $trait->getDefinition($plugin_id, TRUE); } diff --git a/core/tests/Drupal/Tests/Component/Plugin/Discovery/StaticDiscoveryDecoratorTest.php b/core/tests/Drupal/Tests/Component/Plugin/Discovery/StaticDiscoveryDecoratorTest.php index a44721bd9b4e..5b010930922a 100644 --- a/core/tests/Drupal/Tests/Component/Plugin/Discovery/StaticDiscoveryDecoratorTest.php +++ b/core/tests/Drupal/Tests/Component/Plugin/Discovery/StaticDiscoveryDecoratorTest.php @@ -100,7 +100,12 @@ public function testGetDefinition($expected, $has_register_definitions, $excepti $ref_decorated->setValue($mock_decorator, $mock_decorated); if ($exception_on_invalid) { - $this->setExpectedException('Drupal\Component\Plugin\Exception\PluginNotFoundException'); + if (method_exists($this, 'expectException')) { + $this->expectException('Drupal\Component\Plugin\Exception\PluginNotFoundException'); + } + else { + $this->setExpectedException('Drupal\Component\Plugin\Exception\PluginNotFoundException'); + } } // Exercise getDefinition(). It calls parent::getDefinition(). diff --git a/core/tests/Drupal/Tests/Component/Plugin/Factory/ReflectionFactoryTest.php b/core/tests/Drupal/Tests/Component/Plugin/Factory/ReflectionFactoryTest.php index 7e8fbefd1d1c..20d2f76edfc5 100644 --- a/core/tests/Drupal/Tests/Component/Plugin/Factory/ReflectionFactoryTest.php +++ b/core/tests/Drupal/Tests/Component/Plugin/Factory/ReflectionFactoryTest.php @@ -123,7 +123,12 @@ public function testGetInstanceArguments($expected, $reflector_name, $plugin_id, // us to use one data set for this test method as well as // testCreateInstance(). if ($plugin_id == 'arguments_no_constructor') { - $this->setExpectedException('\ReflectionException'); + if (method_exists($this, 'expectException')) { + $this->expectException('\ReflectionException'); + } + else { + $this->setExpectedException('\ReflectionException'); + } } // Finally invoke getInstanceArguments() on our mocked factory. diff --git a/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php b/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php index c2e0a0d888ee..5a71f4838101 100644 --- a/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php +++ b/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php @@ -87,7 +87,12 @@ public function testGetFileExtension() { * @covers ::errorHandler */ public function testError() { - $this->setExpectedException(InvalidDataTypeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidDataTypeException::class); + } + else { + $this->setExpectedException(InvalidDataTypeException::class); + } YamlPecl::decode('foo: [ads'); } diff --git a/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php b/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php index 86c818c18eaa..d857d097a525 100644 --- a/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php +++ b/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php @@ -59,7 +59,12 @@ public function testGetFileExtension() { * @covers ::decode */ public function testError() { - $this->setExpectedException(InvalidDataTypeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidDataTypeException::class); + } + else { + $this->setExpectedException(InvalidDataTypeException::class); + } YamlSymfony::decode('foo: [ads'); } @@ -69,7 +74,13 @@ public function testError() { * @covers ::encode */ public function testObjectSupportDisabled() { - $this->setExpectedException(InvalidDataTypeException::class, 'Object support when dumping a YAML file has been disabled.'); + if (method_exists($this, 'expectException')) { + $this->expectException(InvalidDataTypeException::class); + $this->expectExceptionMessage('Object support when dumping a YAML file has been disabled.'); + } + else { + $this->setExpectedException(InvalidDataTypeException::class, 'Object support when dumping a YAML file has been disabled.'); + } $object = new \stdClass(); $object->foo = 'bar'; YamlSymfony::encode([$object]); diff --git a/core/tests/Drupal/Tests/Component/Utility/ArgumentsResolverTest.php b/core/tests/Drupal/Tests/Component/Utility/ArgumentsResolverTest.php index f15f14b85a4c..9099d6fa582a 100644 --- a/core/tests/Drupal/Tests/Component/Utility/ArgumentsResolverTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/ArgumentsResolverTest.php @@ -96,9 +96,9 @@ public function testGetWildcardArgument() { * Tests getArgument() with a Route, Request, and Account object. */ public function testGetArgumentOrder() { - $a1 = $this->getMock('\Drupal\Tests\Component\Utility\Test1Interface'); - $a2 = $this->getMock('\Drupal\Tests\Component\Utility\TestClass'); - $a3 = $this->getMock('\Drupal\Tests\Component\Utility\Test2Interface'); + $a1 = $this->getMockBuilder('\Drupal\Tests\Component\Utility\Test1Interface')->getMock(); + $a2 = $this->getMockBuilder('\Drupal\Tests\Component\Utility\TestClass')->getMock(); + $a3 = $this->getMockBuilder('\Drupal\Tests\Component\Utility\Test2Interface')->getMock(); $objects = [ 't1' => $a1, @@ -123,12 +123,18 @@ public function testGetArgumentOrder() { * Without the typehint, the wildcard object will not be passed to the callable. */ public function testGetWildcardArgumentNoTypehint() { - $a = $this->getMock('\Drupal\Tests\Component\Utility\Test1Interface'); + $a = $this->getMockBuilder('\Drupal\Tests\Component\Utility\Test1Interface')->getMock(); $wildcards = [$a]; $resolver = new ArgumentsResolver([], [], $wildcards); $callable = function ($route) {}; - $this->setExpectedException(\RuntimeException::class, 'requires a value for the "$route" argument.'); + if (method_exists($this, 'expectException')) { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('requires a value for the "$route" argument.'); + } + else { + $this->setExpectedException(\RuntimeException::class, 'requires a value for the "$route" argument.'); + } $resolver->getArguments($callable); } @@ -156,7 +162,13 @@ public function testHandleNotUpcastedArgument() { $resolver = new ArgumentsResolver($scalars, $objects, []); $callable = function (\stdClass $foo) {}; - $this->setExpectedException(\RuntimeException::class, 'requires a value for the "$foo" argument.'); + if (method_exists($this, 'expectException')) { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('requires a value for the "$foo" argument.'); + } + else { + $this->setExpectedException(\RuntimeException::class, 'requires a value for the "$foo" argument.'); + } $resolver->getArguments($callable); } @@ -167,7 +179,13 @@ public function testHandleNotUpcastedArgument() { */ public function testHandleUnresolvedArgument($callable) { $resolver = new ArgumentsResolver([], [], []); - $this->setExpectedException(\RuntimeException::class, 'requires a value for the "$foo" argument.'); + if (method_exists($this, 'expectException')) { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('requires a value for the "$foo" argument.'); + } + else { + $this->setExpectedException(\RuntimeException::class, 'requires a value for the "$foo" argument.'); + } $resolver->getArguments($callable); } diff --git a/core/tests/Drupal/Tests/Component/Utility/ColorTest.php b/core/tests/Drupal/Tests/Component/Utility/ColorTest.php index cbb9d7e8eb91..aea1779841e6 100644 --- a/core/tests/Drupal/Tests/Component/Utility/ColorTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/ColorTest.php @@ -26,7 +26,12 @@ class ColorTest extends TestCase { */ public function testHexToRgb($value, $expected, $invalid = FALSE) { if ($invalid) { - $this->setExpectedException('InvalidArgumentException'); + if (method_exists($this, 'expectException')) { + $this->expectException('InvalidArgumentException'); + } + else { + $this->setExpectedException('InvalidArgumentException'); + } } $this->assertSame($expected, Color::hexToRgb($value)); } diff --git a/core/tests/Drupal/Tests/Component/Utility/CryptTest.php b/core/tests/Drupal/Tests/Component/Utility/CryptTest.php index c87628f75cda..80208ef294f5 100644 --- a/core/tests/Drupal/Tests/Component/Utility/CryptTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/CryptTest.php @@ -77,7 +77,12 @@ public function testHmacBase64($data, $key, $expected_hmac) { * Key to use in hashing process. */ public function testHmacBase64Invalid($data, $key) { - $this->setExpectedException(\InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException('InvalidArgumentException'); + } + else { + $this->setExpectedException('InvalidArgumentException'); + } Crypt::hmacBase64($data, $key); } diff --git a/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php index 1860e041050b..a8a8af0e6c35 100644 --- a/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php @@ -343,7 +343,12 @@ public function testTransformRootRelativeUrlsToAbsolute($html, $scheme_and_host, * @dataProvider providerTestTransformRootRelativeUrlsToAbsoluteAssertion */ public function testTransformRootRelativeUrlsToAbsoluteAssertion($scheme_and_host) { - $this->setExpectedException(\AssertionError::class); + if (method_exists($this, 'expectException')) { + $this->expectException(\AssertionError::class); + } + else { + $this->setExpectedException(\AssertionError::class); + } Html::transformRootRelativeUrlsToAbsolute('', $scheme_and_host); } diff --git a/core/tests/Drupal/Tests/Component/Utility/RandomTest.php b/core/tests/Drupal/Tests/Component/Utility/RandomTest.php index 64f0eaac1bd4..7523c8be0670 100644 --- a/core/tests/Drupal/Tests/Component/Utility/RandomTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/RandomTest.php @@ -62,7 +62,12 @@ public function testRandomNameException() { // There are fewer than 100 possibilities so an exception should occur to // prevent infinite loops. $random = new Random(); - $this->setExpectedException(\RuntimeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(\RuntimeException::class); + } + else { + $this->setExpectedException(\RuntimeException::class); + } for ($i = 0; $i <= 100; $i++) { $str = $random->name(1, TRUE); $names[$str] = TRUE; @@ -78,7 +83,12 @@ public function testRandomStringException() { // There are fewer than 100 possibilities so an exception should occur to // prevent infinite loops. $random = new Random(); - $this->setExpectedException(\RuntimeException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(\RuntimeException::class); + } + else { + $this->setExpectedException(\RuntimeException::class); + } for ($i = 0; $i <= 100; $i++) { $str = $random->string(1, TRUE); $names[$str] = TRUE; diff --git a/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php b/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php index 49d08333ef4c..ef46b7963af3 100644 --- a/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php @@ -17,7 +17,12 @@ class RectangleTest extends TestCase { * @covers ::rotate */ public function testWrongWidth() { - $this->setExpectedException(\InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(\InvalidArgumentException::class); + } + else { + $this->setExpectedException(\InvalidArgumentException::class); + } $rect = new Rectangle(-40, 20); } @@ -27,7 +32,12 @@ public function testWrongWidth() { * @covers ::rotate */ public function testWrongHeight() { - $this->setExpectedException(\InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(\InvalidArgumentException::class); + } + else { + $this->setExpectedException(\InvalidArgumentException::class); + } $rect = new Rectangle(40, 0); } diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php index 7f249d9bea14..f811c16c15d8 100644 --- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php @@ -37,7 +37,7 @@ protected function tearDown() { * @covers ::isSafe */ public function testIsSafe() { - $safe_string = $this->getMock('\Drupal\Component\Render\MarkupInterface'); + $safe_string = $this->getMockBuilder('\Drupal\Component\Render\MarkupInterface')->getMock(); $this->assertTrue(SafeMarkup::isSafe($safe_string)); $string_object = new SafeMarkupTestString('test'); $this->assertFalse(SafeMarkup::isSafe($string_object)); diff --git a/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php b/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php index 6bfc6cb0d2f7..ba1757ffc5ee 100644 --- a/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/UnicodeTest.php @@ -33,7 +33,12 @@ protected function setUp() { */ public function testStatus($value, $expected, $invalid = FALSE) { if ($invalid) { - $this->setExpectedException('InvalidArgumentException'); + if (method_exists($this, 'expectException')) { + $this->expectException('InvalidArgumentException'); + } + else { + $this->setExpectedException('InvalidArgumentException'); + } } Unicode::setStatus($value); $this->assertEquals($expected, Unicode::getStatus()); diff --git a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php index db78cd9458a5..d185219c9a2a 100644 --- a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php @@ -578,7 +578,12 @@ public function providerTestExternalIsLocal() { * @dataProvider providerTestExternalIsLocalInvalid */ public function testExternalIsLocalInvalid($url, $base_url) { - $this->setExpectedException(\InvalidArgumentException::class); + if (method_exists($this, 'expectException')) { + $this->expectException(\InvalidArgumentException::class); + } + else { + $this->setExpectedException(\InvalidArgumentException::class); + } UrlHelper::externalIsLocal($url, $base_url); } diff --git a/core/tests/Drupal/Tests/Core/Entity/Access/EntityFormDisplayAccessControlHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Access/EntityFormDisplayAccessControlHandlerTest.php index 1ee804d5398a..a92d76abb70d 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Access/EntityFormDisplayAccessControlHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Access/EntityFormDisplayAccessControlHandlerTest.php @@ -164,10 +164,6 @@ protected function setUp() { ->willReturnMap([ ['entity_display', $storage_access_control_handler], ]); - $entity_type_manager - ->expects($this->any()) - ->method('getFieldDefinitions') - ->willReturn([]); $entity_type_manager ->expects($this->any()) ->method('getDefinition') diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php index ad68e2df68e4..55928b98a6e2 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php @@ -123,9 +123,6 @@ public function testGetOperations() { $url = $this->getMockBuilder('\Drupal\Core\Url') ->disableOriginalConstructor() ->getMock(); - $url->expects($this->any()) - ->method('toArray') - ->will($this->returnValue([])); $url->expects($this->atLeastOnce()) ->method('mergeOptions') ->with(['query' => ['destination' => '/foo/bar']]); diff --git a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php index 4a2dac5653e0..846ce0a59f2f 100644 --- a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php @@ -361,7 +361,7 @@ public function testSaveConfigEntity() { $this->assertSame('foo', $entity->getOriginalId()); $expected = ['id' => 'foo']; - $entity->expects($this->once()) + $entity->expects($this->atLeastOnce()) ->method('toArray') ->will($this->returnValue($expected)); diff --git a/core/tests/Drupal/Tests/Core/Listeners/DrupalStandardsListenerDeprecationTest.php b/core/tests/Drupal/Tests/Core/Listeners/DrupalStandardsListenerDeprecationTest.php index 61b62118af14..ad3930440b89 100644 --- a/core/tests/Drupal/Tests/Core/Listeners/DrupalStandardsListenerDeprecationTest.php +++ b/core/tests/Drupal/Tests/Core/Listeners/DrupalStandardsListenerDeprecationTest.php @@ -21,6 +21,7 @@ * would trigger another deprecation error. * * @group Listeners + * @group legacy * * @coversDefaultClass \Drupal\deprecation_test\Deprecation\FixtureDeprecatedClass */ @@ -29,6 +30,8 @@ class DrupalStandardsListenerDeprecationTest extends UnitTestCase { /** * Exercise DrupalStandardsListener's coverage validation. * + * @expectedDeprecation Drupal\deprecation_test\Deprecation\FixtureDeprecatedClass is deprecated. + * * @covers ::testFunction */ public function testDeprecation() { diff --git a/core/tests/Drupal/Tests/Core/Logger/LoggerChannelTest.php b/core/tests/Drupal/Tests/Core/Logger/LoggerChannelTest.php index 2512602afaf2..0f934ebf8e67 100644 --- a/core/tests/Drupal/Tests/Core/Logger/LoggerChannelTest.php +++ b/core/tests/Drupal/Tests/Core/Logger/LoggerChannelTest.php @@ -102,12 +102,12 @@ public function testSortLoggers() { */ public function providerTestLog() { $account_mock = $this->getMock('Drupal\Core\Session\AccountInterface'); - $account_mock->expects($this->exactly(2)) + $account_mock->expects($this->any()) ->method('id') ->will($this->returnValue(1)); - $request_mock = $this->getMock('Symfony\Component\HttpFoundation\Request'); - $request_mock->expects($this->exactly(2)) + $request_mock = $this->getMock('Symfony\Component\HttpFoundation\Request', ['getClientIp']); + $request_mock->expects($this->any()) ->method('getClientIp') ->will($this->returnValue('127.0.0.1')); $request_mock->headers = $this->getMock('Symfony\Component\HttpFoundation\ParameterBag'); diff --git a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php index 929930a5216b..e6215caac834 100644 --- a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php +++ b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php @@ -76,12 +76,6 @@ protected function setUp() { $language_manager->expects($this->any()) ->method('getLanguageTypes') ->will($this->returnValue([LanguageInterface::TYPE_INTERFACE])); - $language_manager->expects($this->any()) - ->method('getNegotiationMethods') - ->will($this->returnValue($method_definitions)); - $language_manager->expects($this->any()) - ->method('getNegotiationMethodInstance') - ->will($this->returnValue($method_instance)); $method_instance->setLanguageManager($language_manager); $this->languageManager = $language_manager; diff --git a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php index 2190d5391878..7dead3d32e23 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php @@ -104,6 +104,7 @@ public function testSetContextValueCacheableDependency() { $container = new Container(); $cache_context_manager = $this->getMockBuilder('Drupal\Core\Cache\CacheContextsManager') ->disableOriginalConstructor() + ->setMethods(['validateTokens']) ->getMock(); $container->set('cache_contexts_manager', $cache_context_manager); $cache_context_manager->expects($this->any()) diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php index 1fca8179a832..a881e7c2ce72 100644 --- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -38,6 +38,7 @@ public function testMerge(BubbleableMetadata $a, CacheableMetadata $b, Bubbleabl if (!$b instanceof BubbleableMetadata) { $renderer = $this->getMockBuilder('Drupal\Core\Render\Renderer') ->disableOriginalConstructor() + ->setMethods(['mergeAttachments']) ->getMock(); $renderer->expects($this->never()) ->method('mergeAttachments'); diff --git a/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php index 5cf020a8df6b..ee14f005a6f8 100644 --- a/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php +++ b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php @@ -116,6 +116,31 @@ public function createMock($originalClassName) { } } + /** + * Compatibility layer for PHPUnit 6 to support PHPUnit 4 code. + * + * @param mixed $class + * The expected exception class. + * @param string $message + * The expected exception message. + * @param int $exception_code + * The expected exception code. + */ + public function setExpectedException($class, $message = '', $exception_code = NULL) { + if (method_exists($this, 'expectException')) { + $this->expectException($class); + if (!empty($message)) { + $this->expectExceptionMessage($message); + } + if ($exception_code !== NULL) { + $this->expectExceptionCode($exception_code); + } + } + else { + parent::setExpectedException($class, $message, $exception_code); + } + } + /** * Checks if the trait is used in a class that has a method. * diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index f78b69ff0498..50fef61ee685 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -8,6 +8,7 @@ */ use Drupal\Component\Assertion\Handle; +use PHPUnit\Runner\Version; /** * Finds all valid extension directories recursively within a given directory. @@ -166,3 +167,19 @@ function drupal_phpunit_populate_class_loader() { // make PHP 5 and 7 handle assertion failures the same way, but this call does // not turn runtime assertions on if they weren't on already. Handle::register(); + +// 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', '>=')) { + 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'); + class_alias('\PHPUnit\Framework\Error\Warning', '\PHPUnit_Framework_Error_Warning'); + class_alias('\PHPUnit\Framework\ExpectationFailedException', '\PHPUnit_Framework_ExpectationFailedException'); + class_alias('\PHPUnit\Framework\Exception', '\PHPUnit_Framework_Exception'); + class_alias('\PHPUnit\Framework\MockObject\Matcher\InvokedRecorder', '\PHPUnit_Framework_MockObject_Matcher_InvokedRecorder'); + class_alias('\PHPUnit\Framework\SkippedTestError', '\PHPUnit_Framework_SkippedTestError'); + class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase'); + class_alias('\PHPUnit\Util\Test', '\PHPUnit_Util_Test'); + class_alias('\PHPUnit\Util\XML', '\PHPUnit_Util_XML'); +} From a83b06a46b228eea09f19a1636b14609dd22652b Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Fri, 22 Dec 2017 13:15:21 +0000 Subject: [PATCH 057/232] Issue #2923015 by cburschka, amateescu, tstoeckler, alexpott, hchonov, mondrake, pfrenssen: [PHP 7.2] Incompatible method declarations --- core/lib/Drupal/Core/Entity/EntityDisplayBase.php | 4 ++-- core/lib/Drupal/Core/Field/FieldItemList.php | 12 ------------ .../src/Plugin/Menu/LocalTask/UnapprovedComments.php | 3 ++- .../comment/src/Tests/Views/CommentTestBase.php | 4 ++-- .../ConfigTranslationContextualLink.php | 3 ++- .../Menu/LocalTask/ConfigTranslationLocalTask.php | 3 ++- core/modules/datetime/src/DateTimeComputed.php | 2 +- .../src/Tests/Views/DateTimeHandlerTestBase.php | 4 ++-- core/modules/field/src/Tests/Views/FieldTestBase.php | 4 ++-- core/modules/field/src/Tests/Views/FieldUITest.php | 4 ++-- .../field/src/Tests/Views/HandlerFieldFieldTest.php | 4 ++-- .../src/Tests/Views/RelationshipUserFileDataTest.php | 4 ++-- core/modules/forum/src/Form/Overview.php | 3 ++- .../Tests/Views/RelationshipUserImageDataTest.php | 4 ++-- .../layout_builder/src/Form/UpdateBlockForm.php | 4 +++- .../link/src/Tests/Views/LinkViewsTokensTest.php | 4 ++-- .../src/Plugin/migrate/source/EmbeddedDataSource.php | 2 +- .../src/Plugin/migrate/source/EmptySource.php | 2 +- .../statistics/src/Tests/Views/IntegrationTest.php | 4 ++-- .../Menu/ContextualLink/TestContextualLink.php | 3 ++- .../src/Plugin/Menu/LocalAction/TestLocalAction.php | 3 ++- .../src/Plugin/Menu/LocalAction/TestLocalAction4.php | 3 ++- .../src/Plugin/Menu/LocalAction/TestLocalAction5.php | 3 ++- .../Menu/LocalAction/TestLocalActionWithConfig.php | 3 ++- .../Plugin/Menu/LocalTask/TestTasksSettingsSub1.php | 3 ++- .../tracker/src/Tests/Views/TrackerTestBase.php | 4 ++-- core/modules/views/src/Tests/FieldApiDataTest.php | 4 ++-- .../views/src/Tests/Plugin/DisplayFeedTest.php | 4 ++-- .../modules/views/src/Tests/Plugin/StyleOpmlTest.php | 4 ++-- core/modules/views/src/Tests/ViewAjaxTest.php | 4 ++-- .../views/src/Tests/Wizard/WizardTestBase.php | 4 ++-- core/modules/views_ui/src/Tests/UITestBase.php | 4 ++-- 32 files changed, 60 insertions(+), 60 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index 7c736e8adfa5..34ce858b13dc 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -250,7 +250,7 @@ public function id() { /** * {@inheritdoc} */ - public function preSave(EntityStorageInterface $storage, $update = TRUE) { + public function preSave(EntityStorageInterface $storage) { // Ensure that a region is set on each component. foreach ($this->getComponents() as $name => $component) { $this->handleHiddenType($name, $component); @@ -263,7 +263,7 @@ public function preSave(EntityStorageInterface $storage, $update = TRUE) { ksort($this->content); ksort($this->hidden); - parent::preSave($storage, $update); + parent::preSave($storage); } /** diff --git a/core/lib/Drupal/Core/Field/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php index 152005bcf34b..52bf9c9d23df 100644 --- a/core/lib/Drupal/Core/Field/FieldItemList.php +++ b/core/lib/Drupal/Core/Field/FieldItemList.php @@ -95,18 +95,6 @@ public function filterEmptyItems() { return $this; } - /** - * {@inheritdoc} - * @todo Revisit the need when all entity types are converted to NG entities. - */ - public function getValue($include_computed = FALSE) { - $values = []; - foreach ($this->list as $delta => $item) { - $values[$delta] = $item->getValue($include_computed); - } - return $values; - } - /** * {@inheritdoc} */ diff --git a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php index 89542d34ff3f..e946bfb9f8ac 100644 --- a/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php +++ b/core/modules/comment/src/Plugin/Menu/LocalTask/UnapprovedComments.php @@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; /** * Provides a local task that shows the amount of unapproved comments. @@ -53,7 +54,7 @@ public static function create(ContainerInterface $container, array $configuratio /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { return $this->t('Unapproved comments (@count)', ['@count' => $this->commentStorage->getUnapprovedCount()]); } diff --git a/core/modules/comment/src/Tests/Views/CommentTestBase.php b/core/modules/comment/src/Tests/Views/CommentTestBase.php index 3d10858351f3..c364e2631f52 100644 --- a/core/modules/comment/src/Tests/Views/CommentTestBase.php +++ b/core/modules/comment/src/Tests/Views/CommentTestBase.php @@ -63,8 +63,8 @@ abstract class CommentTestBase extends ViewTestBase { */ protected $comment; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); ViewTestData::createTestViews(get_class($this), ['comment_test_views']); diff --git a/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php b/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php index 63145a5d3b32..fff82f0c7299 100644 --- a/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php +++ b/core/modules/config_translation/src/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Menu\ContextualLinkDefault; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\HttpFoundation\Request; /** * Defines a contextual link plugin with a dynamic title. @@ -22,7 +23,7 @@ class ConfigTranslationContextualLink extends ContextualLinkDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { // Use the custom 'config_translation_plugin_id' plugin definition key to // retrieve the title. We need to retrieve a runtime title (as opposed to // storing the title on the plugin definition for the link) because it diff --git a/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php b/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php index 5a88d91d0abd..0aa84d224317 100644 --- a/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php +++ b/core/modules/config_translation/src/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Menu\LocalTaskDefault; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\HttpFoundation\Request; /** * Defines a local task plugin with a dynamic title. @@ -22,7 +23,7 @@ class ConfigTranslationLocalTask extends LocalTaskDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { // Take custom 'config_translation_plugin_id' plugin definition key to // retrieve title. We need to retrieve a runtime title (as opposed to // storing the title on the plugin definition for the link) because diff --git a/core/modules/datetime/src/DateTimeComputed.php b/core/modules/datetime/src/DateTimeComputed.php index 2208ab7cb770..73fb11786270 100644 --- a/core/modules/datetime/src/DateTimeComputed.php +++ b/core/modules/datetime/src/DateTimeComputed.php @@ -37,7 +37,7 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T /** * {@inheritdoc} */ - public function getValue($langcode = NULL) { + public function getValue() { if ($this->date !== NULL) { return $this->date; } diff --git a/core/modules/datetime/src/Tests/Views/DateTimeHandlerTestBase.php b/core/modules/datetime/src/Tests/Views/DateTimeHandlerTestBase.php index 0f068ab9ee0d..854352eb5bd2 100644 --- a/core/modules/datetime/src/Tests/Views/DateTimeHandlerTestBase.php +++ b/core/modules/datetime/src/Tests/Views/DateTimeHandlerTestBase.php @@ -43,8 +43,8 @@ abstract class DateTimeHandlerTestBase extends HandlerTestBase { /** * {@inheritdoc} */ - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); // Add a date field to page nodes. $node_type = NodeType::create([ diff --git a/core/modules/field/src/Tests/Views/FieldTestBase.php b/core/modules/field/src/Tests/Views/FieldTestBase.php index f04e19fb9bdf..c01e9604cf15 100644 --- a/core/modules/field/src/Tests/Views/FieldTestBase.php +++ b/core/modules/field/src/Tests/Views/FieldTestBase.php @@ -42,8 +42,8 @@ abstract class FieldTestBase extends ViewTestBase { */ public $fields; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); // Ensure the page node type exists. NodeType::create([ diff --git a/core/modules/field/src/Tests/Views/FieldUITest.php b/core/modules/field/src/Tests/Views/FieldUITest.php index 851b7bdff9a9..efe768505b67 100644 --- a/core/modules/field/src/Tests/Views/FieldUITest.php +++ b/core/modules/field/src/Tests/Views/FieldUITest.php @@ -38,8 +38,8 @@ class FieldUITest extends FieldTestBase { /** * {@inheritdoc} */ - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); $this->account = $this->drupalCreateUser(['administer views']); $this->drupalLogin($this->account); diff --git a/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php b/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php index dfbb89bd209d..da958e90f510 100644 --- a/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php +++ b/core/modules/field/src/Tests/Views/HandlerFieldFieldTest.php @@ -41,8 +41,8 @@ class HandlerFieldFieldTest extends FieldTestBase { /** * {@inheritdoc} */ - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); // Setup basic fields. $this->setUpFieldStorages(3); diff --git a/core/modules/file/src/Tests/Views/RelationshipUserFileDataTest.php b/core/modules/file/src/Tests/Views/RelationshipUserFileDataTest.php index 6a5f53b5d368..45e016d1698c 100644 --- a/core/modules/file/src/Tests/Views/RelationshipUserFileDataTest.php +++ b/core/modules/file/src/Tests/Views/RelationshipUserFileDataTest.php @@ -30,8 +30,8 @@ class RelationshipUserFileDataTest extends ViewTestBase { */ public static $testViews = ['test_file_user_file_data']; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); // Create the user profile field and instance. FieldStorageConfig::create([ diff --git a/core/modules/forum/src/Form/Overview.php b/core/modules/forum/src/Form/Overview.php index a51446b17311..efc926641818 100644 --- a/core/modules/forum/src/Form/Overview.php +++ b/core/modules/forum/src/Form/Overview.php @@ -8,6 +8,7 @@ use Drupal\Core\Url; use Drupal\taxonomy\Form\OverviewTerms; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\taxonomy\VocabularyInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -47,7 +48,7 @@ public function getFormId() { /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state) { + public function buildForm(array $form, FormStateInterface $form_state, VocabularyInterface $taxonomy_vocabulary = NULL) { $forum_config = $this->config('forum.settings'); $vid = $forum_config->get('vocabulary'); $vocabulary = $this->entityManager->getStorage('taxonomy_vocabulary')->load($vid); diff --git a/core/modules/image/src/Tests/Views/RelationshipUserImageDataTest.php b/core/modules/image/src/Tests/Views/RelationshipUserImageDataTest.php index 55cecc0fc09d..374981f63aa0 100644 --- a/core/modules/image/src/Tests/Views/RelationshipUserImageDataTest.php +++ b/core/modules/image/src/Tests/Views/RelationshipUserImageDataTest.php @@ -30,8 +30,8 @@ class RelationshipUserImageDataTest extends ViewTestBase { */ public static $testViews = ['test_image_user_image_data']; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); // Create the user profile field and instance. FieldStorageConfig::create([ diff --git a/core/modules/layout_builder/src/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php index 3cc36585a11a..443deedaeb82 100644 --- a/core/modules/layout_builder/src/Form/UpdateBlockForm.php +++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php @@ -35,11 +35,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, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $uuid = NULL) { + public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $uuid = NULL, array $configuration = []) { /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $entity->layout_builder__layout; $plugin = $field_list->getSection($delta)->getComponent($uuid)->getPlugin(); diff --git a/core/modules/link/src/Tests/Views/LinkViewsTokensTest.php b/core/modules/link/src/Tests/Views/LinkViewsTokensTest.php index 80913ece8dd6..968c02f8306a 100644 --- a/core/modules/link/src/Tests/Views/LinkViewsTokensTest.php +++ b/core/modules/link/src/Tests/Views/LinkViewsTokensTest.php @@ -38,8 +38,8 @@ class LinkViewsTokensTest extends ViewTestBase { /** * {@inheritdoc} */ - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); ViewTestData::createTestViews(get_class($this), ['link_test_views']); // Create Basic page node type. diff --git a/core/modules/migrate/src/Plugin/migrate/source/EmbeddedDataSource.php b/core/modules/migrate/src/Plugin/migrate/source/EmbeddedDataSource.php index 8cee6ddbe4d5..11f32cf563ea 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/EmbeddedDataSource.php +++ b/core/modules/migrate/src/Plugin/migrate/source/EmbeddedDataSource.php @@ -108,7 +108,7 @@ public function getIds() { /** * {@inheritdoc} */ - public function count() { + public function count($refresh = FALSE) { return count($this->dataRows); } diff --git a/core/modules/migrate/src/Plugin/migrate/source/EmptySource.php b/core/modules/migrate/src/Plugin/migrate/source/EmptySource.php index 5f44035e2545..09f8a95792b4 100644 --- a/core/modules/migrate/src/Plugin/migrate/source/EmptySource.php +++ b/core/modules/migrate/src/Plugin/migrate/source/EmptySource.php @@ -58,7 +58,7 @@ public function getIds() { /** * {@inheritdoc} */ - public function count() { + public function count($refresh = FALSE) { return 1; } diff --git a/core/modules/statistics/src/Tests/Views/IntegrationTest.php b/core/modules/statistics/src/Tests/Views/IntegrationTest.php index 18f55815879a..740ecf5e53f3 100644 --- a/core/modules/statistics/src/Tests/Views/IntegrationTest.php +++ b/core/modules/statistics/src/Tests/Views/IntegrationTest.php @@ -42,8 +42,8 @@ class IntegrationTest extends ViewTestBase { */ public static $testViews = ['test_statistics_integration']; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); ViewTestData::createTestViews(get_class($this), ['statistics_test_views']); diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php index 271922936e19..8257d32232fd 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/ContextualLink/TestContextualLink.php @@ -3,6 +3,7 @@ namespace Drupal\menu_test\Plugin\Menu\ContextualLink; use Drupal\Core\Menu\ContextualLinkDefault; +use Symfony\Component\HttpFoundation\Request; /** * Defines a contextual link plugin with a dynamic title from user input. @@ -12,7 +13,7 @@ class TestContextualLink extends ContextualLinkDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { return ""; } diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction.php index 4b9289eef123..13f2e5a0235c 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction.php @@ -3,6 +3,7 @@ namespace Drupal\menu_test\Plugin\Menu\LocalAction; use Drupal\Core\Menu\LocalActionDefault; +use Symfony\Component\HttpFoundation\Request; /** * Defines a test local action plugin class. @@ -12,7 +13,7 @@ class TestLocalAction extends LocalActionDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { return 'Title override'; } diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php index 1f8300a4ba47..eb8b1893c5d7 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction4.php @@ -4,6 +4,7 @@ use Drupal\Core\Menu\LocalActionDefault; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\HttpFoundation\Request; /** * Defines a local action plugin with a dynamic title. @@ -15,7 +16,7 @@ class TestLocalAction4 extends LocalActionDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { return $this->t('My @arg action', ['@arg' => 'dynamic-title']); } diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php index 0bd7fc38755a..e960e2661305 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalAction5.php @@ -3,6 +3,7 @@ namespace Drupal\menu_test\Plugin\Menu\LocalAction; use Drupal\Core\Menu\LocalActionDefault; +use Symfony\Component\HttpFoundation\Request; /** * Defines a local action plugin with a dynamic title from user input. @@ -12,7 +13,7 @@ class TestLocalAction5 extends LocalActionDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { return ""; } diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalActionWithConfig.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalActionWithConfig.php index 54a2f42246fb..4f56977e8690 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalActionWithConfig.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalActionWithConfig.php @@ -6,6 +6,7 @@ use Drupal\Core\Menu\LocalActionDefault; use Drupal\Core\Routing\RouteProviderInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; /** * Defines a test local action plugin class. @@ -20,7 +21,7 @@ class TestLocalActionWithConfig extends LocalActionDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { return $this->config->get('title'); } diff --git a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php index 7ba7548846c0..61ac9705a083 100644 --- a/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalTask/TestTasksSettingsSub1.php @@ -4,6 +4,7 @@ use Drupal\Core\Menu\LocalTaskDefault; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\HttpFoundation\Request; class TestTasksSettingsSub1 extends LocalTaskDefault { @@ -12,7 +13,7 @@ class TestTasksSettingsSub1 extends LocalTaskDefault { /** * {@inheritdoc} */ - public function getTitle() { + public function getTitle(Request $request = NULL) { return $this->t('Dynamic title for @class', ['@class' => 'TestTasksSettingsSub1']); } diff --git a/core/modules/tracker/src/Tests/Views/TrackerTestBase.php b/core/modules/tracker/src/Tests/Views/TrackerTestBase.php index 9746a22a51d1..4dfaab03bc4f 100644 --- a/core/modules/tracker/src/Tests/Views/TrackerTestBase.php +++ b/core/modules/tracker/src/Tests/Views/TrackerTestBase.php @@ -41,8 +41,8 @@ abstract class TrackerTestBase extends ViewTestBase { */ protected $comment; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); ViewTestData::createTestViews(get_class($this), ['tracker_test_views']); diff --git a/core/modules/views/src/Tests/FieldApiDataTest.php b/core/modules/views/src/Tests/FieldApiDataTest.php index f6d5159dd857..d2d035cfcc11 100644 --- a/core/modules/views/src/Tests/FieldApiDataTest.php +++ b/core/modules/views/src/Tests/FieldApiDataTest.php @@ -35,8 +35,8 @@ class FieldApiDataTest extends FieldTestBase { */ protected $translationNodes; - protected function setUp() { - parent::setUp(FALSE); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); $field_names = $this->setUpFieldStorages(4); diff --git a/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php b/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php index e8d254917653..04ba4cab993e 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php @@ -26,8 +26,8 @@ class DisplayFeedTest extends PluginTestBase { */ public static $modules = ['block', 'node', 'views']; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); $this->enableViewsTestModule(); diff --git a/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php b/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php index 4e84433dca17..66a8bc826be4 100644 --- a/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php +++ b/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php @@ -27,8 +27,8 @@ class StyleOpmlTest extends PluginTestBase { /** * {@inheritdoc} */ - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); $this->enableViewsTestModule(); diff --git a/core/modules/views/src/Tests/ViewAjaxTest.php b/core/modules/views/src/Tests/ViewAjaxTest.php index 5a966a8f76ba..820d9e2bc040 100644 --- a/core/modules/views/src/Tests/ViewAjaxTest.php +++ b/core/modules/views/src/Tests/ViewAjaxTest.php @@ -19,8 +19,8 @@ class ViewAjaxTest extends ViewTestBase { */ public static $testViews = ['test_ajax_view', 'test_view']; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); $this->enableViewsTestModule(); } diff --git a/core/modules/views/src/Tests/Wizard/WizardTestBase.php b/core/modules/views/src/Tests/Wizard/WizardTestBase.php index ba7509c8658d..29b96d38c198 100644 --- a/core/modules/views/src/Tests/Wizard/WizardTestBase.php +++ b/core/modules/views/src/Tests/Wizard/WizardTestBase.php @@ -21,8 +21,8 @@ abstract class WizardTestBase extends ViewTestBase { */ public static $modules = ['node', 'views_ui', 'block', 'rest']; - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); // Create and log in a user with administer views permission. $views_admin = $this->drupalCreateUser(['administer views', 'administer blocks', 'bypass node access', 'access user profiles', 'view all revisions']); diff --git a/core/modules/views_ui/src/Tests/UITestBase.php b/core/modules/views_ui/src/Tests/UITestBase.php index 83f2f85980c4..752be6e339b4 100644 --- a/core/modules/views_ui/src/Tests/UITestBase.php +++ b/core/modules/views_ui/src/Tests/UITestBase.php @@ -36,8 +36,8 @@ abstract class UITestBase extends ViewTestBase { /** * {@inheritdoc} */ - protected function setUp() { - parent::setUp(); + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); $this->enableViewsTestModule(); From 26235da61b9616053a7eaa49b909a05bc921d346 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Fri, 22 Dec 2017 14:36:08 +0000 Subject: [PATCH 058/232] Issue #2930788 by marcoscano, phenaproxima, Berdir, seanB: Do not show name by default in media displays --- core/modules/media/src/Entity/Media.php | 7 +- core/modules/media/templates/media.html.twig | 12 -- .../FunctionalJavascript/MediaDisplayTest.php | 170 ++++++++++++++++++ .../media/tests/src/Kernel/MediaTest.php | 7 +- ...entity_view_display.media.file.default.yml | 9 +- ...ntity_view_display.media.image.default.yml | 9 +- 6 files changed, 178 insertions(+), 36 deletions(-) create mode 100644 core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php index 49f7283f7e08..dc8e0dcf0fd5 100644 --- a/core/modules/media/src/Entity/Media.php +++ b/core/modules/media/src/Entity/Media.php @@ -404,12 +404,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'weight' => -5, ]) ->setDisplayConfigurable('form', TRUE) - ->setDisplayConfigurable('view', TRUE) - ->setDisplayOptions('view', [ - 'label' => 'hidden', - 'type' => 'string', - 'weight' => -5, - ]); + ->setDisplayConfigurable('view', TRUE); $fields['thumbnail'] = BaseFieldDefinition::create('image') ->setLabel(t('Thumbnail')) diff --git a/core/modules/media/templates/media.html.twig b/core/modules/media/templates/media.html.twig index 41731992ec0f..28c0a83ca64e 100644 --- a/core/modules/media/templates/media.html.twig +++ b/core/modules/media/templates/media.html.twig @@ -32,17 +32,5 @@ */ #} - {# - In the 'full' view mode the entity label is assumed to be displayed as the - page title, so we do not display it here. - #} - {{ title_prefix }} - {% if label and view_mode != 'full' %} - - {{ label }} - - {% endif %} - {{ title_suffix }} - {{ content }} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php new file mode 100644 index 000000000000..3bd1ccbc099f --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php @@ -0,0 +1,170 @@ +container->get('config.installer')->installOptionalConfig($storage, ''); + // Reset all the static caches and list caches. + $this->container->get('config.factory')->reset(); + } + + /** + * Test basic media display. + */ + public function testMediaDisplay() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $media_type = $this->createMediaType(); + + // Create a media item. + $media = Media::create([ + 'bundle' => $media_type->id(), + 'name' => 'Fantastic!', + ]); + $media->save(); + + $this->drupalGet('media/' . $media->id()); + // Verify the "name" field is really not present. + $assert_session->elementNotExists('css', '.field--name-name'); + + // Enable the field on the display and verify it becomes visible on the UI. + $this->drupalGet("/admin/structure/media/manage/{$media_type->id()}/display"); + $page->selectFieldOption('fields[name][region]', 'content'); + $assert_session->waitForElementVisible('css', '#edit-fields-name-settings-edit'); + $page->pressButton('Save'); + $this->drupalGet('media/' . $media->id()); + // Verify the name is present, and its text matches what is expected. + $assert_session->elementExists('css', '.field--name-name'); + $name_field = $page->find('css', '.field--name-name .field__item'); + $this->assertEquals($media->label(), $name_field->getText()); + + // In the standard profile, there are some pre-cooked types. Make sure the + // elements configured on their displays are the expected ones. + $this->drupalGet('media/add/image'); + $image_media_name = 'Fantastic image asset!'; + $page->fillField('name[0][value]', $image_media_name); + $page->attachFileToField('files[field_media_image_0]', \Drupal::root() . '/core/modules/media/tests/fixtures/example_1.jpeg'); + $result = $assert_session->waitForButton('Remove'); + $this->assertNotEmpty($result); + $page->fillField('field_media_image[0][alt]', 'Image Alt Text 1'); + $page->pressButton('Save'); + $image_media_id = $this->container->get('entity.query')->get('media') + ->sort('mid', 'DESC') + ->execute(); + $image_media_id = reset($image_media_id); + + // Here we expect to see only the image, nothing else. + // Assert only one element in the content region. + $this->assertEquals(1, count($page->findAll('css', '.media--type-image > div'))); + // Assert the image is present inside the media element, with "medium" + // image style. + $media_item = $assert_session->elementExists('css', '.media--type-image > div'); + $assert_session->elementExists('css', 'img.image-style-medium', $media_item); + + $test_filename = $this->randomMachineName() . '.txt'; + $test_filepath = 'public://' . $test_filename; + file_put_contents($test_filepath, $this->randomMachineName()); + $this->drupalGet("media/add/file"); + $page->fillField('name[0][value]', 'File media 1'); + $page->attachFileToField("files[field_media_file_0]", \Drupal::service('file_system')->realpath($test_filepath)); + $result = $assert_session->waitForButton('Remove'); + $this->assertNotEmpty($result); + $page->pressButton('Save'); + + // Here we expect to see only the linked filename. + // Assert only one element in the content region. + $this->assertEquals(1, count($page->findAll('css', 'article.media--type-file > div'))); + // Assert the file link is present, and its text matches the filename. + $assert_session->elementExists('css', 'article.media--type-file .field--name-field-media-file a'); + $link = $page->find('css', 'article.media--type-file .field--name-field-media-file a'); + $this->assertEquals($test_filename, $link->getText()); + + // Create a node type "page" to use as host entity. + $node_type = NodeType::create([ + 'type' => 'page', + 'name' => 'Page', + ]); + $node_type->save(); + + // Reference the created media using an entity_refernce field and make sure + // the output is what we expect. + $storage = FieldStorageConfig::create([ + 'entity_type' => 'node', + 'field_name' => 'field_related_media', + 'type' => 'entity_reference', + 'settings' => [ + 'target_type' => 'media', + ], + ]); + $storage->save(); + + FieldConfig::create([ + 'field_storage' => $storage, + 'entity_type' => 'node', + 'bundle' => $node_type->id(), + 'label' => 'Related media', + 'settings' => [ + 'handler_settings' => [ + 'target_bundles' => [ + 'image' => 'image', + ], + ], + ], + ])->save(); + + entity_get_display('node', $node_type->id(), 'default') + ->setComponent('field_related_media', [ + 'type' => 'entity_reference_entity_view', + 'label' => 'hidden', + 'settings' => [ + 'view_mode' => 'full', + ], + ])->save(); + + $node = Node::create([ + 'title' => 'Host node', + 'type' => $node_type->id(), + 'field_related_media' => [ + 'target_id' => $image_media_id, + ], + ]); + $node->save(); + + $this->drupalGet('/node/' . $node->id()); + // Media field is there. + $assert_session->elementExists('css', '.field--name-field-related-media'); + // Media name element is not there. + $assert_session->elementNotExists('css', '.field--name-name'); + $assert_session->pageTextNotContains($image_media_name); + // Only one element is present inside the media container. + $this->assertEquals(1, count($page->findAll('css', '.field--name-field-related-media article.media--type-image > div'))); + // Assert the image is present, with "medium" image style. + $assert_session->elementExists('css', '.field--name-field-related-media article.media--type-image img.image-style-medium'); + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaTest.php b/core/modules/media/tests/src/Kernel/MediaTest.php index d246fb669eb3..df48a9bf764d 100644 --- a/core/modules/media/tests/src/Kernel/MediaTest.php +++ b/core/modules/media/tests/src/Kernel/MediaTest.php @@ -21,14 +21,17 @@ public function testEntity() { } /** - * Ensure media name is configurable on manage display. + * Tests the Media "name" base field behavior. */ - public function testNameIsConfigurable() { + public function testNameBaseField() { /** @var \Drupal\Core\Field\BaseFieldDefinition[] $field_definitions */ $field_definitions = $this->container->get('entity_field.manager') ->getBaseFieldDefinitions('media'); + // Ensure media name is configurable on manage display. $this->assertTrue($field_definitions['name']->isDisplayConfigurable('view')); + // Ensure it is not visible by default. + $this->assertEquals($field_definitions['name']->getDisplayOptions('view'), ['region' => 'hidden']); } } diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml index f42bab7bb743..72b0bdc1846c 100644 --- a/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml +++ b/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml @@ -18,15 +18,8 @@ content: type: file_default weight: 1 region: content - name: - label: hidden - type: string - weight: 0 - region: content - settings: - link_to_entity: false - third_party_settings: { } hidden: created: true thumbnail: true uid: true + name: true diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml index c02d9084304a..eee9348a2399 100644 --- a/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml +++ b/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml @@ -21,15 +21,8 @@ content: type: image weight: 1 region: content - name: - label: hidden - type: string - weight: 0 - region: content - settings: - link_to_entity: false - third_party_settings: { } hidden: created: true thumbnail: true uid: true + name: true From 824fff57f3d5b77b7f2f1240b2f68a20a807b7bc Mon Sep 17 00:00:00 2001 From: webchick Date: Fri, 22 Dec 2017 11:01:50 -0800 Subject: [PATCH 059/232] Issue #2899708 by gaurav.kapoor, tedbow, droplet, Wim Leers: `quote` should be `blockquote` in off-canvas.base.css --- core/misc/dialog/off-canvas.base.css | 2 +- core/themes/stable/css/core/dialog/off-canvas.base.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/misc/dialog/off-canvas.base.css b/core/misc/dialog/off-canvas.base.css index a0cb45698386..4326f61eedcf 100644 --- a/core/misc/dialog/off-canvas.base.css +++ b/core/misc/dialog/off-canvas.base.css @@ -149,7 +149,7 @@ #drupal-off-canvas ol li { display: block; } -#drupal-off-canvas quote, +#drupal-off-canvas blockquote, #drupal-off-canvas code { margin: 20px 0; } 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 3c4964600300..4eded024b9bc 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.base.css +++ b/core/themes/stable/css/core/dialog/off-canvas.base.css @@ -149,7 +149,7 @@ #drupal-off-canvas ol li { display: block; } -#drupal-off-canvas quote, +#drupal-off-canvas blockquote, #drupal-off-canvas code { margin: 20px 0; } From abebbfb06dae1c7007aae8567356bef82bd227b2 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 23 Dec 2017 07:57:44 +1000 Subject: [PATCH 060/232] Issue #2845361 by claudiu.cristea, Munavijayalakshmi: Don't compute children and parents of a new term on TermForm --- core/modules/taxonomy/src/TermForm.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/core/modules/taxonomy/src/TermForm.php b/core/modules/taxonomy/src/TermForm.php index bb0b10e8998f..6de48e0af4b6 100644 --- a/core/modules/taxonomy/src/TermForm.php +++ b/core/modules/taxonomy/src/TermForm.php @@ -38,14 +38,17 @@ public function form(array $form, FormStateInterface $form_state) { // before loading the full vocabulary. Contrib modules can then intercept // before hook_form_alter to provide scalable alternatives. if (!$this->config('taxonomy.settings')->get('override_selector')) { - $parent = array_keys($taxonomy_storage->loadParents($term->id())); - $children = $taxonomy_storage->loadTree($vocabulary->id(), $term->id()); - - // A term can't be the child of itself, nor of its children. - foreach ($children as $child) { - $exclude[] = $child->tid; + $exclude = []; + if (!$term->isNew()) { + $parent = array_keys($taxonomy_storage->loadParents($term->id())); + $children = $taxonomy_storage->loadTree($vocabulary->id(), $term->id()); + + // A term can't be the child of itself, nor of its children. + foreach ($children as $child) { + $exclude[] = $child->tid; + } + $exclude[] = $term->id(); } - $exclude[] = $term->id(); $tree = $taxonomy_storage->loadTree($vocabulary->id()); $options = ['<' . $this->t('root') . '>']; From f9548751b86a7dfb5842a5eb99b37c0b42c1d45d Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 23 Dec 2017 07:59:09 +1000 Subject: [PATCH 061/232] Issue #2346893 by lauriii, idebr, slashrsm, RavindraSingh, Rade, Fabianx, alexpott, swentel, gauravjeet, darrenwh, deepak_zyxware, joelpittet, Wim Leers, Yogesh Pawar, Vj, ivan.chavarro, josephdpurcell, josmera01, rloos289, kattekrab, Tanvish Jha, csakiistvan, xjm, larowlan, akalata: Duplicate AJAX wrapper around a file field --- core/lib/Drupal/Core/Render/Renderer.php | 16 +++- .../file/src/Tests/FileFieldValidateTest.php | 21 +++++ .../src/Tests/ImageFieldValidateTest.php | 22 +++++ .../KernelTests/Core/Render/RenderTest.php | 19 +++- .../Core/Render/RendererBubblingTest.php | 36 ++++++++ .../Drupal/Tests/Core/Render/RendererTest.php | 91 +++++++++++++++++-- 6 files changed, 191 insertions(+), 14 deletions(-) diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 5038851edefd..00246943f547 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -509,11 +509,17 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // We store the resulting output in $elements['#markup'], to be consistent // with how render cached output gets stored. This ensures that placeholder // replacement logic gets the same data to work with, no matter if #cache is - // disabled, #cache is enabled, there is a cache hit or miss. - $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : ''; - $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : ''; - - $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); + // disabled, #cache is enabled, there is a cache hit or miss. If + // #render_children is set the #prefix and #suffix will have already been + // added. + if (isset($elements['#render_children'])) { + $elements['#markup'] = Markup::create($elements['#children']); + } + else { + $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : ''; + $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : ''; + $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); + } // We've rendered this element (and its subtree!), now update the context. $context->update($elements); diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/src/Tests/FileFieldValidateTest.php index 4698185c4f6a..be96f3c587ee 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/src/Tests/FileFieldValidateTest.php @@ -187,4 +187,25 @@ public function testFileRemoval() { $this->assertText('Article ' . $node->getTitle() . ' has been updated.'); } + /** + * Test the validation message is displayed only once for ajax uploads. + */ + public function testAJAXValidationMessage() { + $field_name = strtolower($this->randomMachineName()); + $this->createFileField($field_name, 'node', 'article'); + + $this->drupalGet('node/add/article'); + /** @var \Drupal\file\FileInterface $image_file */ + $image_file = $this->getTestFile('image'); + $edit = [ + 'files[' . $field_name . '_0]' => $this->container->get('file_system')->realpath($image_file->getFileUri()), + 'title[0][value]' => $this->randomMachineName(), + ]; + $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_0_upload_button'); + $elements = $this->xpath('//div[contains(@class, :class)]', [ + ':class' => 'messages--error', + ]); + $this->assertEqual(count($elements), 1, 'Ajax validation messages are displayed once.'); + } + } diff --git a/core/modules/image/src/Tests/ImageFieldValidateTest.php b/core/modules/image/src/Tests/ImageFieldValidateTest.php index f630a24f7cfb..98210d642199 100644 --- a/core/modules/image/src/Tests/ImageFieldValidateTest.php +++ b/core/modules/image/src/Tests/ImageFieldValidateTest.php @@ -161,4 +161,26 @@ protected function getFieldSettings($min_resolution, $max_resolution) { ]; } + /** + * Test the validation message is displayed only once for ajax uploads. + */ + public function testAJAXValidationMessage() { + $field_name = strtolower($this->randomMachineName()); + $this->createImageField($field_name, 'article', ['cardinality' => -1]); + + $this->drupalGet('node/add/article'); + /** @var \Drupal\file\FileInterface[] $text_files */ + $text_files = $this->drupalGetTestFiles('text'); + $text_file = reset($text_files); + $edit = [ + 'files[' . $field_name . '_0][]' => $this->container->get('file_system')->realpath($text_file->uri), + 'title[0][value]' => $this->randomMachineName(), + ]; + $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_0_upload_button'); + $elements = $this->xpath('//div[contains(@class, :class)]', [ + ':class' => 'messages--error', + ]); + $this->assertEqual(count($elements), 1, 'Ajax validation messages are displayed once.'); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php b/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php index a64b553a16dd..2acaefb873b6 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/RenderTest.php @@ -16,7 +16,7 @@ class RenderTest extends KernelTestBase { * * @var array */ - public static $modules = ['system', 'common_test']; + public static $modules = ['system', 'common_test', 'theme_test']; /** * Tests theme preprocess functions being able to attach assets. @@ -43,6 +43,23 @@ public function testDrupalRenderThemePreprocessAttached() { \Drupal::state()->set('theme_preprocess_attached_test', FALSE); } + /** + * Ensures that render array children are processed correctly. + */ + public function testRenderChildren() { + // Ensure that #prefix and #suffix is only being printed once since that is + // the behaviour the caller code expects. + $build = [ + '#type' => 'container', + '#theme' => 'theme_test_render_element_children', + '#prefix' => 'kangaroo', + '#suffix' => 'kitten', + ]; + $this->render($build); + $this->removeWhiteSpace(); + $this->assertNoRaw('
kangarookitten
'); + } + /** * Tests that we get an exception when we try to attach an illegal type. */ diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php index 573febcc2a4d..f55e34832e08 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php @@ -291,6 +291,42 @@ public function providerTestContextBubblingEdgeCases() { ]; $data[] = [$test_element, ['bar', 'foo'], $expected_cache_items]; + // Ensure that bubbleable metadata has been collected from children and set + // correctly to the main level of the render array. That ensures that correct + // bubbleable metadata exists if render array gets rendered multiple times. + $test_element = [ + '#cache' => [ + 'keys' => ['parent'], + 'tags' => ['yar', 'har'] + ], + '#markup' => 'parent', + 'child' => [ + '#render_children' => TRUE, + 'subchild' => [ + '#cache' => [ + 'contexts' => ['foo'], + 'tags' => ['fiddle', 'dee'], + ], + '#attached' => [ + 'library' => ['foo/bar'] + ], + '#markup' => '', + ] + ], + ]; + $expected_cache_items = [ + 'parent:foo' => [ + '#attached' => ['library' => ['foo/bar']], + '#cache' => [ + 'contexts' => ['foo'], + 'tags' => ['dee', 'fiddle', 'har', 'yar'], + 'max-age' => Cache::PERMANENT, + ], + '#markup' => 'parent', + ], + ]; + $data[] = [$test_element, ['foo'], $expected_cache_items]; + return $data; } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 52569eade9c5..eda1f9fc6057 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -403,6 +403,25 @@ public function providerTestRenderBasic() { }; $data[] = [$build, 'baz', $setup_code]; + // #theme is implemented but #render_children is TRUE. In this case the + // calling code is expecting only the children to be rendered. #prefix and + // #suffix should not be inherited for the children. + $build = [ + '#theme' => 'common_test_foo', + '#children' => '', + '#prefix' => 'kangaroo', + '#suffix' => 'unicorn', + '#render_children' => TRUE, + 'child' => [ + '#markup' => 'kitten', + ], + ]; + $setup_code = function () { + $this->themeManager->expects($this->never()) + ->method('render'); + }; + $data[] = [$build, 'kitten', $setup_code]; + return $data; } @@ -562,25 +581,81 @@ public function testRenderAccessCacheabilityDependencyInheritance() { } /** - * Tests that a first render returns the rendered output and a second doesn't. + * Tests rendering same render array twice. * - * (Because of the #printed property.) + * Tests that a first render returns the rendered output and a second doesn't + * because of the #printed property. Also tests that correct metadata has been + * set for re-rendering. * * @covers ::render * @covers ::doRender + * + * @dataProvider providerRenderTwice */ - public function testRenderTwice() { - $build = [ - '#markup' => 'test', - ]; - - $this->assertEquals('test', $this->renderer->renderRoot($build)); + public function testRenderTwice($build) { + $this->assertEquals('kittens', $this->renderer->renderRoot($build)); + $this->assertEquals('kittens', $build['#markup']); + $this->assertEquals(['kittens-147'], $build['#cache']['tags']); $this->assertTrue($build['#printed']); // We don't want to reprint already printed render arrays. $this->assertEquals('', $this->renderer->renderRoot($build)); } + /** + * Provides a list of render array iterations. + * + * @return array + */ + public function providerRenderTwice() { + return [ + [ + [ + '#markup' => 'kittens', + '#cache' => [ + 'tags' => ['kittens-147'] + ], + ], + ], + [ + [ + 'child' => [ + '#markup' => 'kittens', + '#cache' => [ + 'tags' => ['kittens-147'], + ], + ], + ], + ], + [ + [ + '#render_children' => TRUE, + 'child' => [ + '#markup' => 'kittens', + '#cache' => [ + 'tags' => ['kittens-147'], + ], + ], + ], + ], + ]; + } + + /** + * Ensures that #access is taken in account when rendering #render_children. + */ + public function testRenderChildrenAccess() { + $build = [ + '#access' => FALSE, + '#render_children' => TRUE, + 'child' => [ + '#markup' => 'kittens', + ], + ]; + + $this->assertEquals('', $this->renderer->renderRoot($build)); + } + /** * Provides a list of both booleans. * From e145f2a5438c567cae6f29cf2b93e226dd0b5f37 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 23 Dec 2017 08:58:54 +1000 Subject: [PATCH 062/232] Issue #2932551 by jeqq: Error when calling ModerationStateFieldItemList::updateModeratedEntity() if the entity doesn't have workflow --- .../Field/ModerationStateFieldItemList.php | 2 +- .../ModerationStateFieldItemListTest.php | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php index 4270a1fb539e..b5263639e2f7 100644 --- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php @@ -153,7 +153,7 @@ protected function updateModeratedEntity($moderation_state_id) { // Change the entity's default revision flag and the publishing status only // if the new workflow state is a valid one. - if ($workflow->getTypePlugin()->hasState($moderation_state_id)) { + if ($workflow && $workflow->getTypePlugin()->hasState($moderation_state_id)) { /** @var \Drupal\content_moderation\ContentModerationState $current_state */ $current_state = $workflow->getTypePlugin()->getState($moderation_state_id); diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php index a0d46da428c7..6281ab82bfa0 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php @@ -99,4 +99,29 @@ public function testModerationStateChanges() { $this->assertFalse($this->testNode->isDefaultRevision()); } + /** + * Test updating the state for an entity without a workflow. + */ + public function testEntityWithNoWorkflow() { + $node_type = NodeType::create([ + 'type' => 'example_no_workflow', + ]); + $node_type->save(); + $test_node = Node::create([ + 'type' => 'example_no_workflow', + 'title' => 'Test node with no workflow', + ]); + $test_node->save(); + + /** @var \Drupal\content_moderation\ModerationInformationInterface $content_moderation_info */ + $content_moderation_info = \Drupal::service('content_moderation.moderation_information'); + $workflow = $content_moderation_info->getWorkflowForEntity($test_node); + $this->assertNull($workflow); + + $this->assertTrue($test_node->isPublished()); + $test_node->moderation_state->setValue('draft'); + // The entity is still published because there is not a workflow. + $this->assertTrue($test_node->isPublished()); + } + } From f3b60edab6d22415a83d06d4be9fda1765563e68 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 23 Dec 2017 09:58:46 +1000 Subject: [PATCH 063/232] Issue #2932154 by jhedstrom: ModerationInformation::getLatestRevisionId returns access-specific results --- .../src/ModerationInformation.php | 6 ++ .../tests/src/Functional/NodeAccessTest.php | 35 +++++++- .../tests/src/Kernel/NodeAccessTest.php | 88 +++++++++++++++++++ .../node_access_test/node_access_test.module | 6 ++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 core/modules/content_moderation/tests/src/Kernel/NodeAccessTest.php diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php index 0fc9af4d2ccb..7e3e513307fe 100644 --- a/core/modules/content_moderation/src/ModerationInformation.php +++ b/core/modules/content_moderation/src/ModerationInformation.php @@ -87,6 +87,9 @@ public function getLatestRevisionId($entity_type_id, $entity_id) { $result = $storage->getQuery() ->latestRevision() ->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $entity_id) + // No access check is performed here since this is an API function and + // should return the same ID regardless of the current user. + ->accessCheck(FALSE) ->execute(); if ($result) { return key($result); @@ -102,6 +105,9 @@ public function getDefaultRevisionId($entity_type_id, $entity_id) { $result = $storage->getQuery() ->currentRevision() ->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $entity_id) + // No access check is performed here since this is an API function and + // should return the same ID regardless of the current user. + ->accessCheck(FALSE) ->execute(); if ($result) { return key($result); diff --git a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php index ea79c2d0558c..76e9d97608c4 100644 --- a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php +++ b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\content_moderation\Functional; +use Drupal\node\Entity\NodeType; + /** * Tests permission access control around nodes. * @@ -19,7 +21,7 @@ class NodeAccessTest extends ModerationStateTestBase { 'block', 'block_content', 'node', - 'node_access_test_empty', + 'node_access_test', ]; /** @@ -49,6 +51,9 @@ protected function setUp() { $this->createContentTypeFromUi('Moderated content', 'moderated_content', FALSE); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); + // Add the private field to the node type. + node_access_test_add_field(NodeType::load('moderated_content')); + // Rebuild permissions because hook_node_grants() is implemented by the // node_access_test_empty module. node_access_rebuild(); @@ -58,6 +63,10 @@ protected function setUp() { * Verifies that a non-admin user can still access the appropriate pages. */ public function testPageAccess() { + // Initially disable access grant records in + // node_access_test_node_access_records(). + \Drupal::state()->set('node_access_test.private', TRUE); + $this->drupalLogin($this->adminUser); // Access the node form before moderation is enabled, the publication state @@ -149,6 +158,30 @@ public function testPageAccess() { $this->assertResponse(403); $this->drupalGet($view_path); $this->assertResponse(200); + + // Now create a private node that the user is not granted access to by the + // node grants, but is granted access via hook_node_access(). + // @see node_access_test_node_access + $node = $this->createNode([ + 'type' => 'moderated_content', + 'private' => TRUE, + 'uid' => $this->adminUser->id(), + ]); + $user = $this->createUser([ + 'use editorial transition publish', + ]); + $this->drupalLogin($user); + + // Grant access to the node via node_access_test_node_access(). + \Drupal::state()->set('node_access_test.allow_uid', $user->id()); + + $this->drupalGet($node->toUrl()); + $this->assertResponse(200); + + // Verify the moderation form is in place by publishing the node. + $this->drupalPostForm(NULL, [], t('Apply')); + $node = \Drupal::entityTypeManager()->getStorage('node')->loadUnchanged($node->id()); + $this->assertEquals('published', $node->moderation_state->value); } } diff --git a/core/modules/content_moderation/tests/src/Kernel/NodeAccessTest.php b/core/modules/content_moderation/tests/src/Kernel/NodeAccessTest.php new file mode 100644 index 000000000000..6716ca36100c --- /dev/null +++ b/core/modules/content_moderation/tests/src/Kernel/NodeAccessTest.php @@ -0,0 +1,88 @@ +installEntitySchema('content_moderation_state'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('workflow'); + $this->installConfig(['content_moderation', 'filter']); + $this->installSchema('system', ['sequences']); + $this->installSchema('node', ['node_access']); + + // Add a moderated node type. + $node_type = NodeType::create([ + 'type' => 'page', + 'label' => 'Page', + ]); + $node_type->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->save(); + + $this->moderationInformation = \Drupal::service('content_moderation.moderation_information'); + } + + /** + * Tests for moderation information methods with node access. + */ + public function testModerationInformation() { + // Create an admin user. + $user = $this->createUser([], NULL, TRUE); + \Drupal::currentUser()->setAccount($user); + + // Create a node. + $node = $this->createNode(['type' => 'page']); + $this->assertEquals($node->getRevisionId(), $this->moderationInformation->getDefaultRevisionId('node', $node->id())); + $this->assertEquals($node->getRevisionId(), $this->moderationInformation->getLatestRevisionId('node', $node->id())); + + // Create a non-admin user. + $user = $this->createUser(); + \Drupal::currentUser()->setAccount($user); + $this->assertEquals($node->getRevisionId(), $this->moderationInformation->getDefaultRevisionId('node', $node->id())); + $this->assertEquals($node->getRevisionId(), $this->moderationInformation->getLatestRevisionId('node', $node->id())); + } + +} diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module index 40baeb53eded..5a9756b51adc 100644 --- a/core/modules/node/tests/modules/node_access_test/node_access_test.module +++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module @@ -152,6 +152,12 @@ function node_access_test_node_access(NodeInterface $node, $op, AccountInterface // Make all Catalan content secret. return AccessResult::forbidden()->setCacheMaxAge(0); } + + // Grant access if a specific user is specified. + if (\Drupal::state()->get('node_access_test.allow_uid') === $account->id()) { + return AccessResult::allowed(); + } + // No opinion. return AccessResult::neutral()->setCacheMaxAge(0); } From a351d726a6c031b2851c2b125bccb7302feddf46 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sun, 24 Dec 2017 10:03:14 +1000 Subject: [PATCH 064/232] Issue #2931598 by kim.pepper, markcarver, almaudoh, Sam152: Messenger methods drop repeat flag --- .../Drupal/Core/Messenger/LegacyMessenger.php | 11 +- core/lib/Drupal/Core/Messenger/Messenger.php | 6 +- .../Core/Messenger/MessengerLegacyTest.php | 108 ++++++++++ .../Core/Messenger/MessengerTest.php | 197 ++++++++++++------ 4 files changed, 257 insertions(+), 65 deletions(-) create mode 100644 core/tests/Drupal/KernelTests/Core/Messenger/MessengerLegacyTest.php diff --git a/core/lib/Drupal/Core/Messenger/LegacyMessenger.php b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php index bdc66ffe39bc..86034258315e 100644 --- a/core/lib/Drupal/Core/Messenger/LegacyMessenger.php +++ b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php @@ -34,7 +34,7 @@ class LegacyMessenger implements MessengerInterface { * {@inheritdoc} */ public function addError($message, $repeat = FALSE) { - return $this->addMessage($message, static::TYPE_ERROR); + return $this->addMessage($message, static::TYPE_ERROR, $repeat); } /** @@ -67,14 +67,14 @@ public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) * {@inheritdoc} */ public function addStatus($message, $repeat = FALSE) { - return $this->addMessage($message, static::TYPE_STATUS); + return $this->addMessage($message, static::TYPE_STATUS, $repeat); } /** * {@inheritdoc} */ public function addWarning($message, $repeat = FALSE) { - return $this->addMessage($message, static::TYPE_WARNING); + return $this->addMessage($message, static::TYPE_WARNING, $repeat); } /** @@ -100,13 +100,16 @@ protected function getMessengerService() { if (\Drupal::hasService('messenger')) { // Note: because the container has the potential to be rebuilt during // requests, this service cannot be directly stored on this class. + /** @var \Drupal\Core\Messenger\MessengerInterface $messenger */ $messenger = \Drupal::service('messenger'); // Transfer any messages into the service. if (isset($this->messages)) { foreach ($this->messages as $type => $messages) { foreach ($messages as $message) { - $messenger->addMessage($message, $type); + // Force repeat to TRUE since this is merging existing messages to + // the Messenger service and would have already checked this prior. + $messenger->addMessage($message, $type, TRUE); } } unset($this->messages); diff --git a/core/lib/Drupal/Core/Messenger/Messenger.php b/core/lib/Drupal/Core/Messenger/Messenger.php index b8b6c3ddf369..c0949438cc56 100644 --- a/core/lib/Drupal/Core/Messenger/Messenger.php +++ b/core/lib/Drupal/Core/Messenger/Messenger.php @@ -43,7 +43,7 @@ public function __construct(FlashBagInterface $flash_bag, KillSwitch $killSwitch * {@inheritdoc} */ public function addError($message, $repeat = FALSE) { - return $this->addMessage($message, static::TYPE_ERROR); + return $this->addMessage($message, static::TYPE_ERROR, $repeat); } /** @@ -70,14 +70,14 @@ public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) * {@inheritdoc} */ public function addStatus($message, $repeat = FALSE) { - return $this->addMessage($message, static::TYPE_STATUS); + return $this->addMessage($message, static::TYPE_STATUS, $repeat); } /** * {@inheritdoc} */ public function addWarning($message, $repeat = FALSE) { - return $this->addMessage($message, static::TYPE_WARNING); + return $this->addMessage($message, static::TYPE_WARNING, $repeat); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Messenger/MessengerLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerLegacyTest.php new file mode 100644 index 000000000000..29334647db17 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerLegacyTest.php @@ -0,0 +1,108 @@ +setAccessible(TRUE); + return $method->invoke($legacy_messenger); + } + + /** + * @covers \Drupal::messenger + * @covers ::getMessengerService + * @covers ::all + * @covers ::addMessage + * @covers ::addError + * @covers ::addStatus + * @covers ::addWarning + */ + public function testMessages() { + // Save the current container for later use. + $container = \Drupal::getContainer(); + + // Unset the container to mimic not having one. + \Drupal::unsetContainer(); + + /** @var \Drupal\Core\Messenger\LegacyMessenger $messenger */ + // Verify that the Messenger service doesn't exists. + $messenger = \Drupal::messenger(); + $this->assertNull($this->getMessengerService($messenger)); + + // Add messages. + $messenger->addMessage('Foobar', 'custom'); + $messenger->addMessage('Foobar', 'custom', TRUE); + $messenger->addError('Foo'); + $messenger->addError('Foo', TRUE); + + // Verify that retrieving another instance and adding more messages works. + $messenger = \Drupal::messenger(); + $messenger->addStatus('Bar'); + $messenger->addStatus('Bar', TRUE); + $messenger->addWarning('Fiz'); + $messenger->addWarning('Fiz', TRUE); + + // Restore the container. + \Drupal::setContainer($container); + + // Verify that the Messenger service exists. + $messenger = \Drupal::messenger(); + $this->assertInstanceOf(Messenger::class, $this->getMessengerService($messenger)); + + // Add more messages. + $messenger->addMessage('Platypus', 'custom'); + $messenger->addMessage('Platypus', 'custom', TRUE); + $messenger->addError('Rhinoceros'); + $messenger->addError('Rhinoceros', TRUE); + $messenger->addStatus('Giraffe'); + $messenger->addStatus('Giraffe', TRUE); + $messenger->addWarning('Cheetah'); + $messenger->addWarning('Cheetah', TRUE); + + // Verify all messages added via LegacyMessenger are accounted for. + $messages = $messenger->all(); + $this->assertContains('Foobar', $messages['custom']); + $this->assertContains('Foo', $messages[MessengerInterface::TYPE_ERROR]); + $this->assertContains('Bar', $messages[MessengerInterface::TYPE_STATUS]); + $this->assertContains('Fiz', $messages[MessengerInterface::TYPE_WARNING]); + + // Verify all messages added via Messenger service are accounted for. + $this->assertContains('Platypus', $messages['custom']); + $this->assertContains('Rhinoceros', $messages[MessengerInterface::TYPE_ERROR]); + $this->assertContains('Giraffe', $messages[MessengerInterface::TYPE_STATUS]); + $this->assertContains('Cheetah', $messages[MessengerInterface::TYPE_WARNING]); + + // Verify repeat counts. + $this->assertCount(4, $messages['custom']); + $this->assertCount(4, $messages[MessengerInterface::TYPE_STATUS]); + $this->assertCount(4, $messages[MessengerInterface::TYPE_WARNING]); + $this->assertCount(4, $messages[MessengerInterface::TYPE_ERROR]); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php index 362069756eae..32c4743d93d4 100644 --- a/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php +++ b/core/tests/Drupal/KernelTests/Core/Messenger/MessengerTest.php @@ -2,83 +2,164 @@ namespace Drupal\KernelTests\Core\Messenger; -use Drupal\Core\Messenger\LegacyMessenger; -use Drupal\Core\Messenger\Messenger; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\KernelTests\KernelTestBase; /** * @group Messenger - * @coversDefaultClass \Drupal\Core\Messenger\LegacyMessenger + * @coversDefaultClass \Drupal\Core\Messenger\Messenger */ class MessengerTest extends KernelTestBase { /** - * Retrieves the Messenger service from LegacyMessenger. + * The messenger under test. * - * @param \Drupal\Core\Messenger\LegacyMessenger $legacy_messenger - * - * @return \Drupal\Core\Messenger\MessengerInterface|null + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->messenger = \Drupal::service('messenger'); + } + + /** + * @covers ::addStatus + * @covers ::deleteByType + * @covers ::messagesByType */ - protected function getMessengerService(LegacyMessenger $legacy_messenger) { - $method = new \ReflectionMethod($legacy_messenger, 'getMessengerService'); - $method->setAccessible(TRUE); - return $method->invoke($legacy_messenger); + public function testRemoveSingleMessage() { + + // Set two messages. + $this->messenger->addStatus('First message (removed).'); + $this->messenger->addStatus(t('Second message with markup! (not removed).')); + $messages = $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS); + // Remove the first. + unset($messages[0]); + + // Re-add the second. + foreach ($messages as $message) { + $this->messenger->addStatus($message); + } + + // Check we only have the second one. + $this->assertCount(1, $this->messenger->messagesByType(MessengerInterface::TYPE_STATUS)); + $this->assertContains('Second message with markup! (not removed).', $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS)); + } /** - * @covers \Drupal::messenger - * @covers ::getMessengerService + * Tests we don't add duplicates. + * * @covers ::all - * @covers ::addMessage + * @covers ::addStatus + * @covers ::addWarning * @covers ::addError + * @covers ::deleteByType + * @covers ::deleteAll + */ + public function testAddNoDuplicates() { + + $this->messenger->addStatus('Non Duplicated status message'); + $this->messenger->addStatus('Non Duplicated status message'); + + $this->assertCount(1, $this->messenger->messagesByType(MessengerInterface::TYPE_STATUS)); + + $this->messenger->addWarning('Non Duplicated warning message'); + $this->messenger->addWarning('Non Duplicated warning message'); + + $this->assertCount(1, $this->messenger->messagesByType(MessengerInterface::TYPE_WARNING)); + + $this->messenger->addError('Non Duplicated error message'); + $this->messenger->addError('Non Duplicated error message'); + + $messages = $this->messenger->messagesByType(MessengerInterface::TYPE_ERROR); + $this->assertCount(1, $messages); + + // Check getting all messages. + $messages = $this->messenger->all(); + $this->assertCount(3, $messages); + $this->assertArrayHasKey(MessengerInterface::TYPE_STATUS, $messages); + $this->assertArrayHasKey(MessengerInterface::TYPE_WARNING, $messages); + $this->assertArrayHasKey(MessengerInterface::TYPE_ERROR, $messages); + + // Check deletion. + $this->messenger->deleteAll(); + $this->assertCount(0, $this->messenger->messagesByType(MessengerInterface::TYPE_STATUS)); + $this->assertCount(0, $this->messenger->messagesByType(MessengerInterface::TYPE_WARNING)); + $this->assertCount(0, $this->messenger->messagesByType(MessengerInterface::TYPE_ERROR)); + + } + + /** + * Tests we do add duplicates with repeat flag. + * * @covers ::addStatus * @covers ::addWarning + * @covers ::addError + * @covers ::deleteByType */ - public function testMessages() { - // Save the current container for later use. - $container = \Drupal::getContainer(); - - // Unset the container to mimic not having one. - \Drupal::unsetContainer(); - - /** @var \Drupal\Core\Messenger\LegacyMessenger $messenger */ - // Verify that the Messenger service doesn't exists. - $messenger = \Drupal::messenger(); - $this->assertNull($this->getMessengerService($messenger)); - - // Add messages. - $messenger->addMessage('Foobar'); - $messenger->addError('Foo'); - - // Verify that retrieving another instance and adding more messages works. - $messenger = \Drupal::messenger(); - $messenger->addStatus('Bar'); - $messenger->addWarning('Fiz'); - - // Restore the container. - \Drupal::setContainer($container); - - // Verify that the Messenger service exists. - $messenger = \Drupal::messenger(); - $this->assertInstanceOf(Messenger::class, $this->getMessengerService($messenger)); - - // Add more messages. - $messenger->addMessage('Platypus'); - $messenger->addError('Rhinoceros'); - $messenger->addStatus('Giraffe'); - $messenger->addWarning('Cheetah'); - - // Verify that all the messages are present and accounted for. - $messages = $messenger->all(); - $this->assertContains('Foobar', $messages[MessengerInterface::TYPE_STATUS]); - $this->assertContains('Foo', $messages[MessengerInterface::TYPE_ERROR]); - $this->assertContains('Bar', $messages[MessengerInterface::TYPE_STATUS]); - $this->assertContains('Fiz', $messages[MessengerInterface::TYPE_WARNING]); - $this->assertContains('Platypus', $messages[MessengerInterface::TYPE_STATUS]); - $this->assertContains('Rhinoceros', $messages[MessengerInterface::TYPE_ERROR]); - $this->assertContains('Giraffe', $messages[MessengerInterface::TYPE_STATUS]); - $this->assertContains('Cheetah', $messages[MessengerInterface::TYPE_WARNING]); + public function testAddWithDuplicates() { + + $this->messenger->addStatus('Duplicated status message', TRUE); + $this->messenger->addStatus('Duplicated status message', TRUE); + + $this->assertCount(2, $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS)); + + $this->messenger->addWarning('Duplicated warning message', TRUE); + $this->messenger->addWarning('Duplicated warning message', TRUE); + + $this->assertCount(2, $this->messenger->deleteByType(MessengerInterface::TYPE_WARNING)); + + $this->messenger->addError('Duplicated error message', TRUE); + $this->messenger->addError('Duplicated error message', TRUE); + + $this->assertCount(2, $this->messenger->deleteByType(MessengerInterface::TYPE_ERROR)); + + } + + /** + * Test adding markup. + * + * @covers ::addStatus + * @covers ::deleteByType + * @covers ::messagesByType + */ + public function testAddMarkup() { + + // Add a Markup message. + $this->messenger->addStatus(Markup::create('Markup with markup!')); + // Test duplicate Markup messages. + $this->messenger->addStatus(Markup::create('Markup with markup!')); + + $this->assertCount(1, $this->messenger->messagesByType(MessengerInterface::TYPE_STATUS)); + + // Ensure that multiple Markup messages work. + $this->messenger->addStatus(Markup::create('Markup2 with markup!')); + + $this->assertCount(2, $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS)); + + // Test mixing of types. + $this->messenger->addStatus(Markup::create('Non duplicate Markup / string.')); + $this->messenger->addStatus('Non duplicate Markup / string.'); + $this->messenger->addStatus(Markup::create('Duplicate Markup / string.'), TRUE); + $this->messenger->addStatus('Duplicate Markup / string.', TRUE); + + $this->assertCount(3, $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS)); + + $this->messenger->deleteAll(); + + // Check translatable string is converted to Markup. + $this->messenger->addStatus(new TranslatableMarkup('Translatable message')); + $messages = $this->messenger->deleteByType(MessengerInterface::TYPE_STATUS); + + $this->assertInstanceOf(Markup::class, $messages[0]); + } } From 8946662d754d59dd8feffb8f2ccad139020df5be Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Wed, 27 Dec 2017 18:01:45 +1000 Subject: [PATCH 065/232] Issue #2928450 by tim.plunkett: Remove dead code in the Layout Builder following Section refactoring --- .../layout_builder/layout_builder.module | 23 +--- .../layout_builder.services.yml | 5 - .../src/Access/LayoutSectionAccessCheck.php | 18 --- .../Controller/LayoutBuilderController.php | 38 +----- .../src/Form/ConfigureSectionForm.php | 14 +- .../src/LayoutSectionBuilder.php | 121 ------------------ .../FieldFormatter/LayoutSectionFormatter.php | 54 +------- .../Field/FieldType/LayoutSectionItem.php | 1 - ...outBuilderFieldLayoutCompatibilityTest.php | 20 +-- ...nBuilderTest.php => SectionRenderTest.php} | 68 ++++------ 10 files changed, 38 insertions(+), 324 deletions(-) delete mode 100644 core/modules/layout_builder/src/LayoutSectionBuilder.php rename core/modules/layout_builder/tests/src/Unit/{LayoutSectionBuilderTest.php => SectionRenderTest.php} (75%) diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 2192168cb0cd..0ba9a21f6b6b 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -41,7 +41,7 @@ function layout_builder_entity_type_alter(array &$entity_types) { * 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_display_alter(). + * in layout_builder_entity_view_alter(). * * @internal */ @@ -161,27 +161,16 @@ function layout_builder_add_layout_section_field($entity_type_id, $bundle, $fiel return $field; } -/** - * Implements hook_entity_view_display_alter(). - */ -function layout_builder_entity_view_display_alter(EntityViewDisplayInterface $display, array $context) { - if ($display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE)) { - // Force the layout to render with no label. - $display->setComponent('layout_builder__layout', [ - 'label' => 'hidden', - 'region' => '__layout_builder', - ]); - } - else { - $display->removeComponent('layout_builder__layout'); - } -} - /** * Implements hook_entity_view_alter(). */ 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()) { + $sections = $entity->layout_builder__layout->getSections(); + foreach ($sections as $delta => $section) { + $build['_layout_builder'][$delta] = $section->toRenderArray(); + } + // 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']); diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 518d9ee9421f..b71f6b1887b7 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -1,13 +1,9 @@ services: - layout_builder.builder: - class: Drupal\layout_builder\LayoutSectionBuilder - arguments: ['@current_user', '@plugin.manager.core.layout', '@plugin.manager.block', '@context.handler', '@context.repository'] layout_builder.tempstore_repository: class: Drupal\layout_builder\LayoutTempstoreRepository arguments: ['@user.shared_tempstore', '@entity_type.manager'] access_check.entity.layout: class: Drupal\layout_builder\Access\LayoutSectionAccessCheck - arguments: ['@entity_type.manager'] tags: - { name: access_check, applies_to: _has_layout_section } layout_builder.routes: @@ -15,7 +11,6 @@ services: arguments: ['@entity_type.manager', '@entity_field.manager'] layout_builder.route_enhancer: class: Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer - arguments: ['@entity_type.manager'] tags: - { name: route_enhancer } layout_builder.param_converter: diff --git a/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php b/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php index e13a1492386a..231a70fac5b5 100644 --- a/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php +++ b/core/modules/layout_builder/src/Access/LayoutSectionAccessCheck.php @@ -3,7 +3,6 @@ namespace Drupal\layout_builder\Access; use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Routing\RouteMatchInterface; @@ -16,23 +15,6 @@ */ class LayoutSectionAccessCheck implements AccessInterface { - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a new LayoutSectionAccessCheck. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { - $this->entityTypeManager = $entity_type_manager; - } - /** * Checks routing access to layout for the entity. * diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php index 959ce1d5b414..9e09a61c6d52 100644 --- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php @@ -2,14 +2,11 @@ namespace Drupal\layout_builder\Controller; -use Drupal\Core\Block\BlockManagerInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; -use Drupal\layout_builder\LayoutSectionBuilder; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -24,27 +21,6 @@ class LayoutBuilderController implements ContainerInjectionInterface { use StringTranslationTrait; - /** - * The layout builder. - * - * @var \Drupal\layout_builder\LayoutSectionBuilder - */ - protected $builder; - - /** - * The layout manager. - * - * @var \Drupal\Core\Layout\LayoutPluginManagerInterface - */ - protected $layoutManager; - - /** - * The block manager. - * - * @var \Drupal\Core\Block\BlockManagerInterface - */ - protected $blockManager; - /** * The layout tempstore repository. * @@ -55,19 +31,10 @@ class LayoutBuilderController implements ContainerInjectionInterface { /** * LayoutBuilderController constructor. * - * @param \Drupal\layout_builder\LayoutSectionBuilder $builder - * The layout section builder. - * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager - * The layout manager. - * @param \Drupal\Core\Block\BlockManagerInterface $block_manager - * The block manager. * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository * The layout tempstore repository. */ - public function __construct(LayoutSectionBuilder $builder, LayoutPluginManagerInterface $layout_manager, BlockManagerInterface $block_manager, LayoutTempstoreRepositoryInterface $layout_tempstore_repository) { - $this->builder = $builder; - $this->layoutManager = $layout_manager; - $this->blockManager = $block_manager; + public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) { $this->layoutTempstoreRepository = $layout_tempstore_repository; } @@ -76,9 +43,6 @@ public function __construct(LayoutSectionBuilder $builder, LayoutPluginManagerIn */ public static function create(ContainerInterface $container) { return new static( - $container->get('layout_builder.builder'), - $container->get('plugin.manager.core.layout'), - $container->get('plugin.manager.block'), $container->get('layout_builder.tempstore_repository') ); } diff --git a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php index 6993d218be50..0d43c3d23e32 100644 --- a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php @@ -8,7 +8,6 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\SubformState; use Drupal\Core\Layout\LayoutInterface; -use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\Core\Plugin\PluginFormFactoryInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Plugin\PluginWithFormsInterface; @@ -41,13 +40,6 @@ class ConfigureSectionForm extends FormBase { */ protected $layout; - /** - * The layout manager. - * - * @var \Drupal\Core\Layout\LayoutPluginManagerInterface - */ - protected $layoutManager; - /** * The plugin form manager. * @@ -81,16 +73,13 @@ class ConfigureSectionForm extends FormBase { * * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository * The layout tempstore repository. - * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager - * The layout manager. * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver * The class resolver. * @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager * The plugin form manager. */ - public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, LayoutPluginManagerInterface $layout_manager, ClassResolverInterface $class_resolver, PluginFormFactoryInterface $plugin_form_manager) { + public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver, PluginFormFactoryInterface $plugin_form_manager) { $this->layoutTempstoreRepository = $layout_tempstore_repository; - $this->layoutManager = $layout_manager; $this->classResolver = $class_resolver; $this->pluginFormFactory = $plugin_form_manager; } @@ -101,7 +90,6 @@ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore public static function create(ContainerInterface $container) { return new static( $container->get('layout_builder.tempstore_repository'), - $container->get('plugin.manager.core.layout'), $container->get('class_resolver'), $container->get('plugin_form.factory') ); diff --git a/core/modules/layout_builder/src/LayoutSectionBuilder.php b/core/modules/layout_builder/src/LayoutSectionBuilder.php deleted file mode 100644 index 525849d9cef3..000000000000 --- a/core/modules/layout_builder/src/LayoutSectionBuilder.php +++ /dev/null @@ -1,121 +0,0 @@ -account = $account; - $this->layoutPluginManager = $layoutPluginManager; - $this->blockManager = $blockManager; - $this->contextHandler = $context_handler; - $this->contextRepository = $context_repository; - } - - /** - * Builds the render array for the layout section. - * - * @param \Drupal\Core\Layout\LayoutInterface $layout - * The ID of the layout. - * @param \Drupal\layout_builder\SectionComponent[] $components - * An array of components. - * - * @return array - * The render array for a given section. - */ - public function buildSectionFromLayout(LayoutInterface $layout, array $components) { - $regions = []; - foreach ($components as $component) { - if ($output = $component->toRenderArray()) { - $regions[$component->getRegion()][$component->getUuid()] = $output; - } - } - - return $layout->build($regions); - } - - /** - * Builds the render array for the layout section. - * - * @param string $layout_id - * The ID of the layout. - * @param array $layout_settings - * The configuration for the layout. - * @param \Drupal\layout_builder\SectionComponent[] $components - * An array of components. - * - * @return array - * The render array for a given section. - */ - public function buildSection($layout_id, array $layout_settings, array $components) { - $layout = $this->layoutPluginManager->createInstance($layout_id, $layout_settings); - return $this->buildSectionFromLayout($layout, $components); - } - -} diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php index c321585fdf55..c579b3e4fc51 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php @@ -2,12 +2,8 @@ namespace Drupal\layout_builder\Plugin\Field\FieldFormatter; -use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FormatterBase; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\layout_builder\LayoutSectionBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of the 'layout_section' formatter. @@ -22,55 +18,7 @@ * } * ) */ -class LayoutSectionFormatter extends FormatterBase implements ContainerFactoryPluginInterface { - - /** - * The layout section builder. - * - * @var \Drupal\layout_builder\LayoutSectionBuilder - */ - protected $builder; - - /** - * Constructs a LayoutSectionFormatter object. - * - * @param \Drupal\layout_builder\LayoutSectionBuilder $builder - * The layout section builder. - * @param string $plugin_id - * The plugin ID for the formatter. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The definition of the field to which the formatter is associated. - * @param array $settings - * The formatter settings. - * @param string $label - * The formatter label display setting. - * @param string $view_mode - * The view mode. - * @param array $third_party_settings - * Any third party settings. - */ - public function __construct(LayoutSectionBuilder $builder, $plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings) { - $this->builder = $builder; - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $container->get('layout_builder.builder'), - $plugin_id, - $plugin_definition, - $configuration['field_definition'], - $configuration['settings'], - $configuration['label'], - $configuration['view_mode'], - $configuration['third_party_settings'] - ); - } +class LayoutSectionFormatter extends FormatterBase { /** * {@inheritdoc} 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 2001d1b5ff19..a8d5c7074bd4 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php @@ -18,7 +18,6 @@ * id = "layout_section", * label = @Translation("Layout Section"), * description = @Translation("Layout Section"), - * default_formatter = "layout_section", * list_class = "\Drupal\layout_builder\Field\LayoutSectionItemList", * no_ui = TRUE, * cardinality = \Drupal\Core\Field\FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php index c1b340dea411..1ea23218ccc2 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php @@ -4,18 +4,17 @@ use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Entity\EntityInterface; -use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; -use Drupal\KernelTests\KernelTestBase; use Drupal\layout_builder\Section; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; /** * Ensures that Layout Builder and Field Layout are compatible with each other. * * @group layout_builder */ -class LayoutBuilderFieldLayoutCompatibilityTest extends KernelTestBase { +class LayoutBuilderFieldLayoutCompatibilityTest extends EntityKernelTestBase { /** * {@inheritdoc} @@ -23,12 +22,6 @@ class LayoutBuilderFieldLayoutCompatibilityTest extends KernelTestBase { public static $modules = [ 'layout_discovery', 'field_layout', - 'user', - 'field', - 'entity_test', - 'system', - 'text', - 'filter', ]; /** @@ -45,9 +38,7 @@ protected function setUp() { parent::setUp(); $this->installEntitySchema('entity_test_base_field_display'); - $this->installEntitySchema('user'); - $this->installSchema('system', ['sequences', 'key_value']); - $this->installConfig(['field', 'filter', 'user', 'system']); + $this->installConfig(['filter']); \Drupal::service('theme_handler')->install(['classy']); $this->config('system.theme')->set('default', 'classy')->save(); @@ -100,9 +91,10 @@ public function testCompatibility() { // Install the Layout Builder, configure it for this entity display, and // reload the entity. - $this->enableModules(['layout_builder']); + $this->installModule('layout_builder'); + $this->display = $this->reloadEntity($this->display); $this->display->setThirdPartySetting('layout_builder', 'allow_custom', TRUE)->save(); - $entity = EntityTestBaseFieldDisplay::load($entity->id()); + $entity = $this->reloadEntity($entity); // Without using Layout Builder for an override, the result has not changed. $new_markup = $this->renderEntity($entity); diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php similarity index 75% rename from core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php rename to core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php index 28f22557e75b..634030e70bf6 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php @@ -15,16 +15,16 @@ use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\layout_builder\LayoutSectionBuilder; +use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; /** - * @coversDefaultClass \Drupal\layout_builder\LayoutSectionBuilder + * @coversDefaultClass \Drupal\layout_builder\Section * @group layout_builder */ -class LayoutSectionBuilderTest extends UnitTestCase { +class SectionRenderTest extends UnitTestCase { /** * The current user. @@ -33,13 +33,6 @@ class LayoutSectionBuilderTest extends UnitTestCase { */ protected $account; - /** - * The layout plugin manager. - * - * @var \Drupal\Core\Layout\LayoutPluginManagerInterface - */ - protected $layoutPluginManager; - /** * The block plugin manager. * @@ -61,20 +54,6 @@ class LayoutSectionBuilderTest extends UnitTestCase { */ protected $contextRepository; - /** - * The object under test. - * - * @var \Drupal\layout_builder\LayoutSectionBuilder - */ - protected $layoutSectionBuilder; - - /** - * The layout plugin. - * - * @var \Drupal\Core\Layout\LayoutInterface - */ - protected $layout; - /** * {@inheritdoc} */ @@ -82,29 +61,29 @@ protected function setUp() { parent::setUp(); $this->account = $this->prophesize(AccountInterface::class); - $this->layoutPluginManager = $this->prophesize(LayoutPluginManagerInterface::class); + $layout_plugin_manager = $this->prophesize(LayoutPluginManagerInterface::class); $this->blockManager = $this->prophesize(BlockManagerInterface::class); $this->contextHandler = $this->prophesize(ContextHandlerInterface::class); $this->contextRepository = $this->prophesize(ContextRepositoryInterface::class); - $this->layoutSectionBuilder = new LayoutSectionBuilder($this->account->reveal(), $this->layoutPluginManager->reveal(), $this->blockManager->reveal(), $this->contextHandler->reveal(), $this->contextRepository->reveal()); - $this->layout = $this->prophesize(LayoutInterface::class); - $this->layout->getPluginDefinition()->willReturn(new LayoutDefinition([])); - $this->layout->build(Argument::type('array'))->willReturnArgument(0); - $this->layoutPluginManager->createInstance('layout_onecol', [])->willReturn($this->layout->reveal()); + $layout = $this->prophesize(LayoutInterface::class); + $layout->getPluginDefinition()->willReturn(new LayoutDefinition([])); + $layout->build(Argument::type('array'))->willReturnArgument(0); + $layout_plugin_manager->createInstance('layout_onecol', [])->willReturn($layout->reveal()); $container = new ContainerBuilder(); $container->set('current_user', $this->account->reveal()); $container->set('plugin.manager.block', $this->blockManager->reveal()); + $container->set('plugin.manager.core.layout', $layout_plugin_manager->reveal()); $container->set('context.handler', $this->contextHandler->reveal()); $container->set('context.repository', $this->contextRepository->reveal()); \Drupal::setContainer($container); } /** - * @covers ::buildSection + * @covers ::toRenderArray */ - public function testBuildSection() { + public function testToRenderArray() { $block_content = ['#markup' => 'The block content.']; $render_array = [ '#theme' => 'block', @@ -143,15 +122,14 @@ public function testBuildSection() { 'some_uuid' => $render_array, ], ]; - $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); + $result = (new Section('layout_onecol', [], $section))->toRenderArray(); $this->assertEquals($expected, $result); } /** - * @covers ::buildSection + * @covers ::toRenderArray */ - public function testBuildSectionAccessDenied() { - + public function testToRenderArrayAccessDenied() { $block = $this->prophesize(BlockPluginInterface::class); $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); @@ -173,22 +151,22 @@ public function testBuildSectionAccessDenied() { ], ], ]; - $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); + $result = (new Section('layout_onecol', [], $section))->toRenderArray(); $this->assertEquals($expected, $result); } /** - * @covers ::buildSection + * @covers ::toRenderArray */ - public function testBuildSectionEmpty() { + public function testToRenderArrayEmpty() { $section = []; $expected = []; - $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); + $result = (new Section('layout_onecol', [], $section))->toRenderArray(); $this->assertEquals($expected, $result); } /** - * @covers ::buildSection + * @covers ::toRenderArray */ public function testContextAwareBlock() { $render_array = [ @@ -232,16 +210,16 @@ public function testContextAwareBlock() { 'some_uuid' => $render_array, ], ]; - $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); + $result = (new Section('layout_onecol', [], $section))->toRenderArray(); $this->assertEquals($expected, $result); } /** - * @covers ::buildSection + * @covers ::toRenderArray */ - public function testBuildSectionMissingPluginId() { + public function testToRenderArrayMissingPluginId() { $this->setExpectedException(PluginException::class, 'No plugin ID specified for component with "some_uuid" UUID'); - $this->layoutSectionBuilder->buildSection('layout_onecol', [], [new SectionComponent('some_uuid', 'content')]); + (new Section('layout_onecol', [], [new SectionComponent('some_uuid', 'content')]))->toRenderArray(); } } From 984a268454fabfe0a84a25cf3c4db24459d51a8d Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Wed, 27 Dec 2017 18:05:16 +1000 Subject: [PATCH 066/232] Issue #2931264 by markcarver, claudiu.cristea: Remove static \Drupal::$legacyMessenger property --- core/lib/Drupal.php | 21 +--------- .../Drupal/Core/Messenger/LegacyMessenger.php | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 8b98ccd1dc71..1697ff62a240 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -101,22 +101,6 @@ class Drupal { */ protected static $container; - /** - * The LegacyMessenger instance. - * - * Note: this is merely used to ensure that the instance survives when - * \Drupal::messenger() is invoked. It is required to ensure that messages - * are properly transferred to the Messenger service once the container has - * been initialized. Do not store the Messenger service here. - * - * @todo Remove once LegacyMessenger has been removed before 9.0.0. - * - * @see https://www.drupal.org/node/2928994 - * - * @var \Drupal\Core\Messenger\LegacyMessenger|null - */ - protected static $legacyMessenger; - /** * Sets a new global container. * @@ -783,10 +767,7 @@ public static function time() { public static function messenger() { // @todo Replace with service once LegacyMessenger is removed in 9.0.0. // @see https://www.drupal.org/node/2928994 - if (!isset(static::$legacyMessenger)) { - static::$legacyMessenger = new LegacyMessenger(); - } - return static::$legacyMessenger; + return new LegacyMessenger(); } } diff --git a/core/lib/Drupal/Core/Messenger/LegacyMessenger.php b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php index 86034258315e..ff323d20f94d 100644 --- a/core/lib/Drupal/Core/Messenger/LegacyMessenger.php +++ b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php @@ -26,9 +26,13 @@ class LegacyMessenger implements MessengerInterface { /** * The messages. * + * Note: this property must remain static because it must behave in a + * persistent manner, similar to $_SESSION['messages']. Creating a new class + * each time would destroy any previously set messages. + * * @var array */ - protected $messages; + protected static $messages; /** * {@inheritdoc} @@ -46,8 +50,8 @@ public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) return $messenger->addMessage($message, $type, $repeat); } - if (!isset($this->messages[$type])) { - $this->messages[$type] = []; + if (!isset(static::$messages[$type])) { + static::$messages[$type] = []; } if (!($message instanceof Markup) && $message instanceof MarkupInterface) { @@ -56,8 +60,8 @@ public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) // Do not use strict type checking so that equivalent string and // MarkupInterface objects are detected. - if ($repeat || !in_array($message, $this->messages[$type])) { - $this->messages[$type][] = $message; + if ($repeat || !in_array($message, static::$messages[$type])) { + static::$messages[$type][] = $message; } return $this; @@ -86,7 +90,7 @@ public function all() { return $messenger->all(); } - return $this->messages; + return static::$messages; } /** @@ -104,15 +108,15 @@ protected function getMessengerService() { $messenger = \Drupal::service('messenger'); // Transfer any messages into the service. - if (isset($this->messages)) { - foreach ($this->messages as $type => $messages) { + if (isset(static::$messages)) { + foreach (static::$messages as $type => $messages) { foreach ($messages as $message) { // Force repeat to TRUE since this is merging existing messages to // the Messenger service and would have already checked this prior. $messenger->addMessage($message, $type, TRUE); } } - unset($this->messages); + static::$messages = NULL; } return $messenger; @@ -128,18 +132,18 @@ protected function getMessengerService() { // reasonable to assume that if the container becomes available in a // subsequent request, a new instance of this class will be created and // this code will never be reached. This is merely for BC purposes. - if (!isset($this->messages)) { + if (!isset(static::$messages)) { // A "session" was already created, perhaps to simply allow usage of // the previous method core used to store messages, use it. if (isset($_SESSION)) { if (!isset($_SESSION['messages'])) { $_SESSION['messages'] = []; } - $this->messages = &$_SESSION['messages']; + static::$messages = &$_SESSION['messages']; } // Otherwise, just set an empty array. else { - $this->messages = []; + static::$messages = []; } } } @@ -153,7 +157,7 @@ public function messagesByType($type) { return $messenger->messagesByType($type); } - return $this->messages[$type]; + return static::$messages[$type]; } /** @@ -165,8 +169,8 @@ public function deleteAll() { return $messenger->deleteAll(); } - $messages = $this->messages; - unset($this->messages); + $messages = static::$messages; + static::$messages = NULL; return $messages; } @@ -179,8 +183,8 @@ public function deleteByType($type) { return $messenger->messagesByType($type); } - $messages = $this->messages[$type]; - unset($this->messages[$type]); + $messages = static::$messages[$type]; + unset(static::$messages[$type]); return $messages; } From 6585630775fd1d1f276b7a3747eac405554ff8df Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Wed, 27 Dec 2017 18:07:19 +1000 Subject: [PATCH 067/232] Issue #2931709 by marcoscano, claudiu.cristea: Wrong constant name in \Drupal\image\Plugin\Field\FieldType\ImageItem::generateSampleValue() --- core/modules/image/src/Plugin/Field/FieldType/ImageItem.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php index 14335d8ecec2..67a15a7d0084 100644 --- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php +++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php @@ -344,7 +344,7 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin if (!isset($images[$extension][$min_resolution][$max_resolution]) || count($images[$extension][$min_resolution][$max_resolution]) <= 5) { $tmp_file = drupal_tempnam('temporary://', 'generateImage_'); $destination = $tmp_file . '.' . $extension; - file_unmanaged_move($tmp_file, $destination, FILE_CREATE_DIRECTORY); + file_unmanaged_move($tmp_file, $destination); if ($path = $random->image(\Drupal::service('file_system')->realpath($destination), $min_resolution, $max_resolution)) { $image = File::create(); $image->setFileUri($path); @@ -354,7 +354,7 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin $destination_dir = static::doGetUploadLocation($settings); file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY); $destination = $destination_dir . '/' . basename($path); - $file = file_move($image, $destination, FILE_CREATE_DIRECTORY); + $file = file_move($image, $destination); $images[$extension][$min_resolution][$max_resolution][$file->id()] = $file; } else { From 1872e83be50457bf4c39a4b157a5c90aef756445 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Wed, 27 Dec 2017 18:19:18 +1000 Subject: [PATCH 068/232] Issue #2931294 by claudiu.cristea, Wim Leers: Timestamp field type misses schema for value --- core/config/schema/core.data_types.schema.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 3b5bbe41906a..8e98d7c2fdb4 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -743,6 +743,16 @@ field.value.float: type: float label: 'Value' +# Schema for the configuration of the Timestamp field type. + +field.value.timestamp: + type: mapping + label: 'Timestamp value' + mapping: + value: + type: timestamp + label: 'Value' + # Text with a text format. text_format: type: mapping From 384a35caf64ffcd872167b7aba5e73c5c1c43a1b Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sun, 31 Dec 2017 08:13:45 +1000 Subject: [PATCH 069/232] Issue #2779921 by kiamlaluno, alexpott: hook_field_widget_form_alter() still reference a hook that is not used anymore --- core/modules/field/field.api.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index b0899ea02b07..42bdd1e7125b 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -138,7 +138,8 @@ function hook_field_widget_info_alter(array &$info) { * Alter forms for field widgets provided by other modules. * * @param $element - * The field widget form element as constructed by hook_field_widget_form(). + * The field widget form element as constructed by + * \Drupal\Core\Field\WidgetBaseInterface::form(). * @param $form_state * The current state of the form. * @param $context @@ -152,6 +153,7 @@ function hook_field_widget_info_alter(array &$info) { * - 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::formSingleElement() * @see hook_field_widget_WIDGET_TYPE_form_alter() */ @@ -172,13 +174,15 @@ function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInte * checking the widget type. * * @param $element - * The field widget form element as constructed by hook_field_widget_form(). + * The field widget form element as constructed by + * \Drupal\Core\Field\WidgetBaseInterface::form(). * @param $form_state * The current state of the form. * @param $context * An associative array. See hook_field_widget_form_alter() for the structure * and content of the array. * + * @see \Drupal\Core\Field\WidgetBaseInterface::form() * @see \Drupal\Core\Field\WidgetBase::formSingleElement() * @see hook_field_widget_form_alter() */ From ed45b4d5dc0524b7120eb7cb8acd13c3b0f8ff74 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sun, 31 Dec 2017 08:21:22 +1000 Subject: [PATCH 070/232] Issue #2840257 by kiamlaluno: The documentation makes reference to a function that doesn't exist --- core/modules/toolbar/toolbar.api.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/modules/toolbar/toolbar.api.php b/core/modules/toolbar/toolbar.api.php index 548b488992a4..eb087d36e28a 100644 --- a/core/modules/toolbar/toolbar.api.php +++ b/core/modules/toolbar/toolbar.api.php @@ -152,10 +152,10 @@ function hook_toolbar() { /** * Alter the toolbar menu after hook_toolbar() is invoked. * - * This hook is invoked by toolbar_view() immediately after hook_toolbar(). The - * toolbar definitions are passed in by reference. Each element of the $items - * array is one item returned by a module from hook_toolbar(). Additional items - * may be added, or existing items altered. + * This hook is invoked by Toolbar::preRenderToolbar() immediately after + * hook_toolbar(). The toolbar definitions are passed in by reference. Each + * element of the $items array is one item returned by a module from + * hook_toolbar(). Additional items may be added, or existing items altered. * * @param $items * Associative array of toolbar menu definitions returned from hook_toolbar(). From d372f6d8eee746ccab8089fbe00357ea270b4211 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sun, 31 Dec 2017 08:25:45 +1000 Subject: [PATCH 071/232] Issue #2323459 by harsha012, jhodgdon, joachim: Change wording of annotation keys to properties --- core/lib/Drupal/Core/Entity/entity.api.php | 54 +++++++++++----------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index 0af0b532328b..6b4029c71b08 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -331,11 +331,11 @@ * out-of-the-box support for Entity API's revisioning and publishing * features, which will allow your entity type to be used with Drupal's * editorial workflow provided by the Content Moderation module. - * - The 'id' annotation gives the entity type ID, and the 'label' annotation - * gives the human-readable name of the entity type. If you are defining a - * content entity type that uses bundles, the 'bundle_label' annotation gives - * the human-readable name to use for a bundle of this entity type (for - * example, "Content type" for the Node entity). + * - In the annotation, the 'id' property gives the entity type ID, and the + * 'label' property gives the human-readable name of the entity type. If you + * are defining a content entity type that uses bundles, the 'bundle_label' + * property gives the human-readable name to use for a bundle of this entity + * type (for example, "Content type" for the Node entity). * - The annotation will refer to several handler classes, which you will also * need to define: * - list_builder: Define a class that extends @@ -354,16 +354,17 @@ * \Drupal\Core\Entity\EntityViewBuilderInterface (usually extending * \Drupal\Core\Entity\EntityViewBuilder), to display a single entity. * - translation: For translatable content entities (if the 'translatable' - * annotation has value TRUE), define a class that extends + * annotation property has value TRUE), define a class that extends * \Drupal\content_translation\ContentTranslationHandler, to translate * the content. Configuration translation is handled automatically by the * Configuration Translation module, without the need of a handler class. * - access: If your configuration entity has complex permissions, you might * need an access control handling, implementing - * \Drupal\Core\Entity\EntityAccessControlHandlerInterface, but most entities - * can just use the 'admin_permission' annotation instead. Note that if you - * are creating your own access control handler, you should override the - * checkAccess() and checkCreateAccess() methods, not access(). + * \Drupal\Core\Entity\EntityAccessControlHandlerInterface, but most + * entities can just use the 'admin_permission' annotation property + * instead. Note that if you are creating your own access control handler, + * you should override the checkAccess() and checkCreateAccess() methods, + * not access(). * - storage: A class implementing * \Drupal\Core\Entity\EntityStorageInterface. If not specified, content * entities will use \Drupal\Core\Entity\Sql\SqlContentEntityStorage, and @@ -396,25 +397,26 @@ * - delete-form: Confirmation form to delete the entity. * - edit-form: Editing form. * - Other link types specific to your entity type can also be defined. - * - If your content entity is fieldable, provide 'field_ui_base_route' - * annotation, giving the name of the route that the Manage Fields, Manage - * Display, and Manage Form Display pages from the Field UI module will be - * attached to. This is usually the bundle settings edit page, or an entity - * type settings page if there are no bundles. + * - If your content entity is fieldable, provide the 'field_ui_base_route' + * annotation property, giving the name of the route that the Manage Fields, + * Manage Display, and Manage Form Display pages from the Field UI module + * will be attached to. This is usually the bundle settings edit page, or an + * entity type settings page if there are no bundles. * - If your content entity has bundles, you will also need to define a second * plugin to handle the bundles. This plugin is itself a configuration entity * type, so follow the steps here to define it. The machine name ('id' - * annotation) of this configuration entity class goes into the - * 'bundle_entity_type' annotation on the entity type class. For example, for - * the Node entity, the bundle class is \Drupal\node\Entity\NodeType, whose - * machine name is 'node_type'. This is the annotation value for - * 'bundle_entity_type' on the \Drupal\node\Entity\Node class. Also, the - * bundle config entity type annotation must have a 'bundle_of' entry, + * annotation property) of this configuration entity class goes into the + * 'bundle_entity_type' annotation property on the entity type class. For + * example, for the Node entity, the bundle class is + * \Drupal\node\Entity\NodeType, whose machine name is 'node_type'. This is + * the annotation property 'bundle_entity_type' on the + * \Drupal\node\Entity\Node class. Also, the + * bundle config entity type annotation must have a 'bundle_of' property, * giving the machine name of the entity type it is acting as a bundle for. * These machine names are considered permanent, they may not be renamed. - * - Additional annotations can be seen on entity class examples such as - * \Drupal\node\Entity\Node (content) and \Drupal\user\Entity\Role - * (configuration). These annotations are documented on + * - Additional annotation properties can be seen on entity class examples such + * as \Drupal\node\Entity\Node (content) and \Drupal\user\Entity\Role + * (configuration). These annotation properties are documented on * \Drupal\Core\Entity\EntityType. * * @section sec_routes Entity routes @@ -500,8 +502,8 @@ * $storage = $container->get('entity.manager')->getStorage('your_entity_type'); * @endcode * Here, 'your_entity_type' is the machine name of your entity type ('id' - * annotation on the entity class), and note that you should use dependency - * injection to retrieve this object if possible. See the + * annotation property on the entity class), and note that you should use + * dependency injection to retrieve this object if possible. See the * @link container Services and Dependency Injection topic @endlink for more * about how to properly retrieve services. * From efab31b7f212ab9ec6bc7c60f9a19527453ca37a Mon Sep 17 00:00:00 2001 From: Francesco Placella Date: Sun, 31 Dec 2017 17:31:41 +0100 Subject: [PATCH 072/232] Issue #2862894 by fgm, Wim Leers, bkosborne, borisson_, catch: Docs for Internal Page Cache incorrectly state that it respects the maximum age performance setting --- core/modules/page_cache/page_cache.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/page_cache/page_cache.module b/core/modules/page_cache/page_cache.module index ff7286199d7e..2879b08e0c7c 100644 --- a/core/modules/page_cache/page_cache.module +++ b/core/modules/page_cache/page_cache.module @@ -23,7 +23,7 @@ function page_cache_help($route_name, RouteMatchInterface $route_match) { $output .= '
' . t('Pages are usually identical for all anonymous users, while they can be personalized for each authenticated user. This is why entire pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '
'; $output .= '
' . t('To speed up your site for authenticated users, see the Dynamic Page Cache module.', [':dynamic_page_cache-help' => (\Drupal::moduleHandler()->moduleExists('dynamic_page_cache')) ? Url::fromRoute('help.page', ['name' => 'dynamic_page_cache'])->toString() : '#']) . '

'; $output .= '
' . t('Configuring the internal page cache') . '
'; - $output .= '
' . t('On the Performance page, you can configure how long browsers and proxies may cache pages; that setting is also respected by the Internal Page Cache module. There is no other configuration.', [':cache-settings' => \Drupal::url('system.performance_settings')]) . '
'; + $output .= '
' . t('On the Performance page, you can configure how long browsers and proxies may cache pages based on the Cache-Control header; this setting is ignored by the Internal Page Cache module, which caches pages permanently until invalidation, unless they carry an Expires header. There is no other configuration.', [':cache-settings' => \Drupal::url('system.performance_settings')]) . '
'; $output .= ''; return $output; From 9ba7824211cc1bc2659cd9d2de815ee9aa62287f Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Mon, 1 Jan 2018 16:16:51 +1000 Subject: [PATCH 073/232] Issue #2626924 by Wim Leers, tedbow, dawehner, frob, martin107, damiankloip, dagmar, almaudoh, Berdir, larowlan, amateescu: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata --- .../filter/src/Element/ProcessedText.php | 8 +- .../filter/src/FilterProcessResult.php | 2 +- .../hal/tests/src/Kernel/NormalizeTest.php | 25 ++- .../BlockContentResourceTestBase.php | 16 ++ .../Comment/CommentResourceTestBase.php | 16 ++ .../EntityTestTextItemNormalizerTest.php | 195 ++++++++++++++++++ .../Term/TermResourceTestBase.php | 19 +- .../src/Normalizer/TypedDataNormalizer.php | 7 +- .../src/Kernel/EntitySerializationTest.php | 33 ++- .../Plugin/Field/FieldType/TextItemBase.php | 3 +- core/modules/text/src/TextProcessed.php | 59 +++++- 11 files changed, 366 insertions(+), 17 deletions(-) create mode 100644 core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php index 5c7a21234ecc..1a3392ba42ed 100644 --- a/core/modules/filter/src/Element/ProcessedText.php +++ b/core/modules/filter/src/Element/ProcessedText.php @@ -3,6 +3,7 @@ namespace Drupal\filter\Element; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element\RenderElement; use Drupal\filter\Entity\FilterFormat; @@ -69,7 +70,12 @@ public static function preRenderText($element) { $langcode = $element['#langcode']; if (!isset($format_id)) { - $format_id = static::configFactory()->get('filter.settings')->get('fallback_format'); + $filter_settings = static::configFactory()->get('filter.settings'); + $format_id = $filter_settings->get('fallback_format'); + // Ensure 'filter.settings' config's cacheability is respected. + CacheableMetadata::createFromRenderArray($element) + ->addCacheableDependency($filter_settings) + ->applyTo($element); } /** @var \Drupal\filter\Entity\FilterFormat $format **/ $format = FilterFormat::load($format_id); diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php index 3fa592b6277e..2a8a71bd1f18 100644 --- a/core/modules/filter/src/FilterProcessResult.php +++ b/core/modules/filter/src/FilterProcessResult.php @@ -78,7 +78,7 @@ class FilterProcessResult extends BubbleableMetadata { * @param string $processed_text * The text as processed by a text filter. */ - public function __construct($processed_text) { + public function __construct($processed_text = '') { $this->processedText = $processed_text; } diff --git a/core/modules/hal/tests/src/Kernel/NormalizeTest.php b/core/modules/hal/tests/src/Kernel/NormalizeTest.php index d837b79a02ee..ffea170ba201 100644 --- a/core/modules/hal/tests/src/Kernel/NormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Url; use Drupal\entity_test\Entity\EntityTest; +use Drupal\filter\Entity\FilterFormat; /** * Tests HAL normalization edge cases for EntityResource. @@ -19,6 +20,27 @@ class NormalizeTest extends NormalizerTestBase { protected function setUp() { parent::setUp(); + FilterFormat::create([ + 'format' => 'my_text_format', + 'name' => 'My Text Format', + 'filters' => [ + 'filter_html' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [ + 'allowed_html' => '

', + ], + ], + 'filter_autop' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [], + ], + ], + ])->save(); + \Drupal::service('router.builder')->rebuild(); } @@ -37,7 +59,7 @@ public function testNormalize() { 'name' => $this->randomMachineName(), 'field_test_text' => [ 'value' => $this->randomMachineName(), - 'format' => 'full_html', + 'format' => 'my_text_format', ], 'field_test_entity_reference' => [ 'target_id' => $target_entity_de->id(), @@ -152,6 +174,7 @@ public function testNormalize() { [ 'value' => $values['field_test_text']['value'], 'format' => $values['field_test_text']['format'], + 'processed' => "

{$values['field_test_text']['value']}

", ], ], ]; diff --git a/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php index 5d7329feb96a..576681794041 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php @@ -4,6 +4,7 @@ use Drupal\block_content\Entity\BlockContent; use Drupal\block_content\Entity\BlockContentType; +use Drupal\Core\Cache\Cache; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; @@ -131,6 +132,7 @@ protected function getExpectedNormalizedEntity() { 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', 'format' => 'plain_text', 'summary' => NULL, + 'processed' => "

The name "llama" was adopted by European settlers from native Peruvians.

\n", ], ], 'status' => [ @@ -180,4 +182,18 @@ protected function getExpectedUnauthorizedAccessCacheability() { ->addCacheTags(['block_content:1']); } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']); + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php index 60b5930e1efd..d3098545cdfe 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php @@ -5,6 +5,7 @@ use Drupal\comment\Entity\Comment; use Drupal\comment\Entity\CommentType; use Drupal\comment\Tests\CommentTestTrait; +use Drupal\Core\Cache\Cache; use Drupal\entity_test\Entity\EntityTest; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; @@ -197,6 +198,7 @@ protected function getExpectedNormalizedEntity() { [ 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', 'format' => 'plain_text', + 'processed' => '

The name "llama" was adopted by European settlers from native Peruvians.

' . "\n", ], ], ]; @@ -248,6 +250,20 @@ protected function getNormalizedPatchEntity() { return array_diff_key($this->getNormalizedPostEntity(), ['entity_type' => TRUE, 'entity_id' => TRUE, 'field_name' => TRUE]); } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['languages:language_interface', 'theme'], parent::getExpectedCacheContexts()); + } + /** * Tests POSTing a comment without critical base fields. * diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php new file mode 100644 index 000000000000..cd8654df4d81 --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php @@ -0,0 +1,195 @@ +grantPermissionsToTestedRole(['use text format my_text_format']); + } + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + $expected = parent::getExpectedNormalizedEntity(); + $expected['field_test_text'] = [ + [ + 'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.', + 'format' => 'my_text_format', + 'processed' => '

Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.

' . "\n" . '

This is a dynamic llama.

', + ], + ]; + return $expected; + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $entity = parent::createEntity(); + if (!FilterFormat::load('my_text_format')) { + FilterFormat::create([ + 'format' => 'my_text_format', + 'name' => 'My Text Format', + 'filters' => [ + 'filter_test_assets' => [ + 'weight' => -1, + 'status' => TRUE, + ], + 'filter_test_cache_tags' => [ + 'weight' => 0, + 'status' => TRUE, + ], + 'filter_test_cache_contexts' => [ + 'weight' => 0, + 'status' => TRUE, + ], + 'filter_test_cache_merge' => [ + 'weight' => 0, + 'status' => TRUE, + ], + 'filter_test_placeholders' => [ + 'weight' => 1, + 'status' => TRUE, + ], + 'filter_autop' => [ + 'status' => TRUE, + ], + ], + ])->save(); + } + $entity->field_test_text = [ + 'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.', + 'format' => 'my_text_format', + ]; + $entity->save(); + return $entity; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + $post_entity = parent::getNormalizedPostEntity(); + $post_entity['field_test_text'] = [ + [ + 'value' => 'Llamas are awesome.', + 'format' => 'my_text_format', + ], + ]; + return $post_entity; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags([ + // The cache tag set by the processed_text element itself. + 'config:filter.format.my_text_format', + // The cache tags set by the filter_test_cache_tags filter. + 'foo:bar', + 'foo:baz', + // The cache tags set by the filter_test_cache_merge filter. + 'merge:tag' + ], parent::getExpectedCacheTags()); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts([ + // The cache context set by the filter_test_cache_contexts filter. + 'languages:' . LanguageInterface::TYPE_CONTENT, + // The default cache contexts for Renderer. + 'languages:' . LanguageInterface::TYPE_INTERFACE, + 'theme', + // The cache tags set by the filter_test_cache_merge filter. + 'user.permissions', + ], parent::getExpectedCacheContexts()); + } + + /** + * Tests GETting an entity with the test text field set to a specific format. + * + * @dataProvider providerTestGetWithFormat + */ + public function testGetWithFormat($text_format_id, array $expected_cache_tags) { + FilterFormat::create([ + 'name' => 'Pablo Piccasso', + 'format' => 'pablo', + 'langcode' => 'es', + 'filters' => [], + ])->save(); + + // Set TextItemBase field's value for testing, using the given text format. + $value = [ + 'value' => $this->randomString(), + ]; + if ($text_format_id !== FALSE) { + $value['format'] = $text_format_id; + } + $this->entity->set('field_test_text', $value)->save(); + + $this->initAuthentication(); + $url = $this->getEntityResourceUrl(); + $url->setOption('query', ['_format' => static::$format]); + $request_options = $this->getAuthenticationRequestOptions('GET'); + $this->provisionEntityResource(); + $this->setUpAuthorization('GET'); + $response = $this->request('GET', $url, $request_options); + $expected_cache_tags = Cache::mergeTags($expected_cache_tags, parent::getExpectedCacheTags()); + $this->assertSame($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0])); + } + + public function providerTestGetWithFormat() { + return [ + 'format specified (different from fallback format)' => [ + 'pablo', + ['config:filter.format.pablo'], + ], + 'format specified (happens to be the same as fallback format)' => [ + 'plain_text', + ['config:filter.format.plain_text'], + ], + 'no format specified: fallback format used automatically' => [ + FALSE, + ['config:filter.format.plain_text', 'config:filter.settings'], + ], + ]; + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index e0a15813a82f..713c1e5c1bf0 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Term; +use Drupal\Core\Cache\Cache; use Drupal\taxonomy\Entity\Term; use Drupal\taxonomy\Entity\Vocabulary; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; @@ -79,6 +80,7 @@ protected function createEntity() { // Create a "Llama" taxonomy term. $term = Term::create(['vid' => $vocabulary->id()]) ->setName('Llama') + ->setDescription("It is a little known fact that llamas cannot count higher than seven.") ->setChangedTime(123456789) ->set('path', '/llama'); $term->save(); @@ -109,8 +111,9 @@ protected function getExpectedNormalizedEntity() { ], 'description' => [ [ - 'value' => NULL, + 'value' => 'It is a little known fact that llamas cannot count higher than seven.', 'format' => NULL, + 'processed' => "

It is a little known fact that llamas cannot count higher than seven.

\n", ], ], 'parent' => [], @@ -230,4 +233,18 @@ public function testPatchPath() { $this->assertSame($normalization['path'], $updated_normalization['path']); } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text', 'config:filter.settings']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']); + } + } diff --git a/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php index 958b987dc7da..690fd8db252e 100644 --- a/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php +++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php @@ -19,7 +19,12 @@ class TypedDataNormalizer extends NormalizerBase { */ public function normalize($object, $format = NULL, array $context = []) { $this->addCacheableDependency($context, $object); - return $object->getValue(); + $value = $object->getValue(); + // Support for stringable value objects: avoid numerous custom normalizers. + if (is_object($value) && method_exists($value, '__toString')) { + $value = (string) $value; + } + return $value; } } diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php index 5b71b93b7a14..5df9d75db0cc 100644 --- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php @@ -2,8 +2,10 @@ namespace Drupal\Tests\serialization\Kernel; +use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\SafeMarkup; use Drupal\entity_test\Entity\EntityTestMulRev; +use Drupal\filter\Entity\FilterFormat; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; /** @@ -63,6 +65,27 @@ protected function setUp() { // User create needs sequence table. $this->installSchema('system', ['sequences']); + FilterFormat::create([ + 'format' => 'my_text_format', + 'name' => 'My Text Format', + 'filters' => [ + 'filter_html' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [ + 'allowed_html' => '

', + ], + ], + 'filter_autop' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [], + ], + ], + ])->save(); + // Create a test user to use as the entity owner. $this->user = \Drupal::entityManager()->getStorage('user')->create([ 'name' => 'serialization_test_user', @@ -72,12 +95,13 @@ protected function setUp() { $this->user->save(); // Create a test entity to serialize. + $test_text_value = $this->randomMachineName(); $this->values = [ 'name' => $this->randomMachineName(), 'user_id' => $this->user->id(), 'field_test_text' => [ - 'value' => $this->randomMachineName(), - 'format' => 'full_html', + 'value' => $test_text_value, + 'format' => 'my_text_format', ], ]; $this->entity = EntityTestMulRev::create($this->values); @@ -134,6 +158,7 @@ public function testNormalize() { [ 'value' => $this->values['field_test_text']['value'], 'format' => $this->values['field_test_text']['format'], + 'processed' => "

{$this->values['field_test_text']['value']}

", ], ], ]; @@ -174,7 +199,7 @@ public function testSerialize() { // JsonEncoder. The output of ComplexDataNormalizer::normalize() is tested // elsewhere, so we can just assume that it works properly here. $normalized = $this->serializer->normalize($this->entity, 'json'); - $expected = json_encode($normalized); + $expected = Json::encode($normalized); // Test 'json'. $actual = $this->serializer->serialize($this->entity, 'json'); $this->assertIdentical($actual, $expected, 'Entity serializes to JSON when "json" is requested.'); @@ -202,7 +227,7 @@ public function testSerialize() { 'default_langcode' => '1', 'revision_translation_affected' => '1', 'non_rev_field' => '', - 'field_test_text' => '' . $this->values['field_test_text']['value'] . '' . $this->values['field_test_text']['format'] . '', + 'field_test_text' => '' . $this->values['field_test_text']['value'] . '' . $this->values['field_test_text']['format'] . '' . $this->values['field_test_text']['value'] . '

]]>
', ]; // Sort it in the same order as normalised. $expected = array_merge($normalized, $expected); diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php index 6dd433929922..e70fadacb1a0 100644 --- a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php @@ -29,7 +29,8 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel ->setDescription(t('The text with the text format applied.')) ->setComputed(TRUE) ->setClass('\Drupal\text\TextProcessed') - ->setSetting('text source', 'value'); + ->setSetting('text source', 'value') + ->setInternal(FALSE); return $properties; } diff --git a/core/modules/text/src/TextProcessed.php b/core/modules/text/src/TextProcessed.php index 61bef37dd56e..a0455d74bdb0 100644 --- a/core/modules/text/src/TextProcessed.php +++ b/core/modules/text/src/TextProcessed.php @@ -2,9 +2,12 @@ namespace Drupal\text; +use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\TypedData\DataDefinitionInterface; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedData; +use Drupal\filter\FilterProcessResult; +use Drupal\filter\Render\FilteredMarkup; /** * A computed property for processing text with a format. @@ -12,12 +15,12 @@ * Required settings (below the definition's 'settings' key) are: * - text source: The text property containing the to be processed text. */ -class TextProcessed extends TypedData { +class TextProcessed extends TypedData implements CacheableDependencyInterface { /** * Cached processed text. * - * @var string|null + * @var \Drupal\filter\FilterProcessResult|null */ protected $processed = NULL; @@ -37,20 +40,29 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T */ public function getValue() { if ($this->processed !== NULL) { - return $this->processed; + return FilteredMarkup::create($this->processed->getProcessedText()); } $item = $this->getParent(); $text = $item->{($this->definition->getSetting('text source'))}; - // Avoid running check_markup() on empty strings. + // Avoid doing unnecessary work on empty strings. if (!isset($text) || $text === '') { - $this->processed = ''; + $this->processed = new FilterProcessResult(''); } else { - $this->processed = check_markup($text, $item->format, $item->getLangcode()); + $build = [ + '#type' => 'processed_text', + '#text' => $text, + '#format' => $item->format, + '#filter_types_to_skip' => [], + '#langcode' => $item->getLangcode(), + ]; + // Capture the cacheability metadata associated with the processed text. + $processed_text = $this->getRenderer()->renderPlain($build); + $this->processed = FilterProcessResult::createFromRenderArray($build)->setProcessedText((string) $processed_text); } - return $this->processed; + return FilteredMarkup::create($this->processed->getProcessedText()); } /** @@ -64,4 +76,37 @@ public function setValue($value, $notify = TRUE) { } } + /** + * {@inheritdoc} + */ + public function getCacheTags() { + $this->getValue(); + return $this->processed->getCacheTags(); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $this->getValue(); + return $this->processed->getCacheContexts(); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + $this->getValue(); + return $this->processed->getCacheMaxAge(); + } + + /** + * Returns the renderer service. + * + * @return \Drupal\Core\Render\RendererInterface + */ + protected function getRenderer() { + return \Drupal::service('renderer'); + } + } From 4cd7b4950cfd9432181cc9ca8d137e933c29c3ee Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Tue, 2 Jan 2018 06:53:06 +1000 Subject: [PATCH 074/232] Issue #2933125 by Tessa Bakker: Case mismatch in ExportForm.php --- core/modules/locale/src/Form/ExportForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/locale/src/Form/ExportForm.php b/core/modules/locale/src/Form/ExportForm.php index fc5b05d72aa7..89f3fbb69dbe 100644 --- a/core/modules/locale/src/Form/ExportForm.php +++ b/core/modules/locale/src/Form/ExportForm.php @@ -167,7 +167,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $header->setLanguageName($language_name); $writer = new PoStreamWriter(); - $writer->setUri($uri); + $writer->setURI($uri); $writer->setHeader($header); $writer->open(); From 0c20200d124e0f13d33422ec2d3008206f36e265 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Tue, 2 Jan 2018 12:52:15 +0000 Subject: [PATCH 075/232] =?UTF-8?q?Issue=20#2837022=20by=20hchonov,=20xjm,?= =?UTF-8?q?=20vlad.dancer,=20plach,=20matsbla,=20G=C3=A1bor=20Hojtsy:=20Co?= =?UTF-8?q?ncurrently=20editing=20two=20translations=20of=20a=20node=20may?= =?UTF-8?q?=20result=20in=20data=20loss=20for=20non-translatable=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Entity/EntityChangedInterface.php | 5 ++ .../EntityChangedConstraintValidator.php | 21 ++++++-- .../Core/Entity/EntityValidationTest.php | 52 ++++++++++++++++++- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php index 747571799cdb..2c92b0cf431d 100644 --- a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php @@ -37,6 +37,11 @@ public function setChangedTime($timestamp); /** * Gets the timestamp of the last entity change across all translations. * + * This method will return the highest timestamp across all translations. To + * check that no translation is older than in another version of the entity + * (e.g. to avoid overwriting newer translations with old data), compare each + * translation to the other version individually. + * * @return int * The timestamp of the last entity save operation across all * translations. diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php index b1fee2a29452..28d81ba6e4de 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php @@ -18,10 +18,23 @@ public function validate($entity, Constraint $constraint) { /** @var \Drupal\Core\Entity\EntityInterface $entity */ if (!$entity->isNew()) { $saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id()); - // A change to any other translation must add a violation to the current - // translation because there might be untranslatable shared fields. - if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) { - $this->context->addViolation($constraint->message); + // Ensure that all the entity translations are the same as or newer + // than their current version in the storage in order to avoid + // reverting other changes. In fact the entity object that is being + // saved might contain an older entity translation when different + // translations are being concurrently edited. + if ($saved_entity) { + $common_translation_languages = array_intersect_key($entity->getTranslationLanguages(), $saved_entity->getTranslationLanguages()); + foreach (array_keys($common_translation_languages) as $langcode) { + // Merely comparing the latest changed timestamps across all + // translations is not sufficient since other translations may have + // been edited and saved in the meanwhile. Therefore, compare the + // changed timestamps of each entity translation individually. + if ($saved_entity->getTranslation($langcode)->getChangedTime() > $entity->getTranslation($langcode)->getChangedTime()) { + $this->context->addViolation($constraint->message); + break; + } + } } } } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php index c6461943ec37..2a6dfed6c2d3 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityValidationTest.php @@ -3,6 +3,7 @@ namespace Drupal\KernelTests\Core\Entity; use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase; +use Drupal\language\Entity\ConfigurableLanguage; /** * Tests the Entity Validation API. @@ -16,7 +17,7 @@ class EntityValidationTest extends EntityKernelTestBase { * * @var array */ - public static $modules = ['filter', 'text']; + public static $modules = ['filter', 'text', 'language']; /** * @var string @@ -39,6 +40,10 @@ class EntityValidationTest extends EntityKernelTestBase { protected function setUp() { parent::setUp(); + // Enable an additional language. + ConfigurableLanguage::createFromLangcode('de') + ->save(); + // Create the test field. module_load_install('entity_test'); entity_test_install(); @@ -200,4 +205,49 @@ public function testCompositeConstraintValidation() { $this->assertEqual($constraint->coversFields(), ['name', 'type'], 'Information about covered fields can be retrieved.'); } + /** + * Tests the EntityChangedConstraintValidator with multiple translations. + */ + public function testEntityChangedConstraintOnConcurrentMultilingualEditing() { + $this->installEntitySchema('entity_test_mulrev_changed'); + $storage = \Drupal::entityTypeManager() + ->getStorage('entity_test_mulrev_changed'); + + // Create a test entity. + $entity = $this->createTestEntity('entity_test_mulrev_changed'); + $entity->save(); + + $entity->setChangedTime($entity->getChangedTime() - 1); + $violations = $entity->validate(); + $this->assertEquals(1, $violations->count()); + $this->assertEqual($violations[0]->getMessage(), 'The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.'); + + $entity = $storage->loadUnchanged($entity->id()); + $translation = $entity->addTranslation('de'); + $entity->save(); + + // Ensure that the new translation has a newer changed timestamp than the + // default translation. + $this->assertGreaterThan($entity->getChangedTime(), $translation->getChangedTime()); + + // Simulate concurrent form editing by saving the entity with an altered + // non-translatable field in order for the changed timestamp to be updated + // across all entity translations. + $original_entity_time = $entity->getChangedTime(); + $entity->set('not_translatable', $this->randomString()); + $entity->save(); + // Simulate form submission of an uncached form by setting the previous + // timestamp of an entity translation on the saved entity object. This + // happens in the entity form API where we put the changed timestamp of + // the entity in a form hidden value and then set it on the entity which on + // form submit is loaded from the storage if the form is not yet cached. + $entity->setChangedTime($original_entity_time); + // Setting the changed timestamp from the user input on the entity loaded + // from the storage is used as a prevention from saving a form built with a + // previous version of the entity and thus reverting changes by other users. + $violations = $entity->validate(); + $this->assertEquals(1, $violations->count()); + $this->assertEqual($violations[0]->getMessage(), 'The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.'); + } + } From 0dc30938c7de123ddd70b734fd50421d104ca508 Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Tue, 2 Jan 2018 11:57:52 -0800 Subject: [PATCH 076/232] Issue #2824851 by Wim Leers, arshadcn, amateescu, effulgentsia, tedbow, timmillwood, cburschka, tstoeckler, Berdir, xjm, catch: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior --- .../Comment/CommentHalJsonAnonTest.php | 2 +- .../Comment/CommentHalJsonTestBase.php | 19 --- .../HalEntityNormalizationTrait.php | 18 --- .../Node/NodeHalJsonAnonTest.php | 13 --- .../Plugin/rest/resource/EntityResource.php | 80 ++++++++----- .../EntityResource/EntityResourceTestBase.php | 110 +++++++++++++----- .../Node/NodeResourceTestBase.php | 19 +-- .../Term/TermResourceTestBase.php | 9 -- 8 files changed, 138 insertions(+), 132 deletions(-) diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php index 3edd9b16098a..9b0cee24a0ec 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php @@ -25,11 +25,11 @@ class CommentHalJsonAnonTest extends CommentHalJsonTestBase { * @see ::setUpAuthorization */ protected static $patchProtectedFieldNames = [ + 'entity_id', 'changed', 'thread', 'entity_type', 'field_name', - 'entity_id', ]; } diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php index 1939e04364bf..3deb0eaba50b 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php @@ -26,25 +26,6 @@ abstract class CommentHalJsonTestBase extends CommentResourceTestBase { */ protected static $mimeType = 'application/hal+json'; - /** - * {@inheritdoc} - * - * The HAL+JSON format causes different PATCH-protected fields. For some - * reason, the 'pid' and 'homepage' fields are NOT PATCH-protected, even - * though they are for non-HAL+JSON serializations. - * - * @todo fix in https://www.drupal.org/node/2824271 - */ - protected static $patchProtectedFieldNames = [ - 'status', - 'created', - 'changed', - 'thread', - 'entity_type', - 'field_name', - 'entity_id', - 'uid', - ]; /** * {@inheritdoc} diff --git a/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php b/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php index 1fc23291b577..71c455065c3c 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php @@ -70,24 +70,6 @@ protected function applyHalFieldNormalization(array $normalization) { return $normalization; } - /** - * {@inheritdoc} - */ - protected function removeFieldsFromNormalization(array $normalization, $field_names) { - $normalization = parent::removeFieldsFromNormalization($normalization, $field_names); - foreach ($field_names as $field_name) { - $relation_url = Url::fromUri('base:rest/relation/' . static::$entityTypeId . '/' . $this->entity->bundle() . '/' . $field_name) - ->setAbsolute(TRUE) - ->toString(); - $normalization['_links'] = array_diff_key($normalization['_links'], [$relation_url => TRUE]); - if (isset($normalization['_embedded'])) { - $normalization['_embedded'] = array_diff_key($normalization['_embedded'], [$relation_url => TRUE]); - } - } - - return array_diff_key($normalization, array_flip($field_names)); - } - /** * {@inheritdoc} */ diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php index e218a73a4201..2de6539fe420 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php @@ -30,19 +30,6 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase { */ protected static $mimeType = 'application/hal+json'; - /** - * {@inheritdoc} - */ - protected static $patchProtectedFieldNames = [ - 'revision_timestamp', - 'created', - 'changed', - 'promote', - 'sticky', - 'path', - 'revision_uid', - ]; - /** * {@inheritdoc} */ diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 15396fd807cf..4b6febcbe84c 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -11,6 +11,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; @@ -226,38 +227,18 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update')); } - // Overwrite the received properties. - $entity_keys = $entity->getEntityType()->getKeys(); + // Overwrite the received fields. foreach ($entity->_restSubmittedFields as $field_name) { $field = $entity->get($field_name); - - // Entity key fields need special treatment: together they uniquely - // identify the entity. Therefore it does not make sense to modify any of - // them. However, rather than throwing an error, we just ignore them as - // long as their specified values match their current values. - if (in_array($field_name, $entity_keys, TRUE)) { - // @todo Work around the wrong assumption that entity keys need special - // treatment, when only read-only fields need it. - // This will be fixed in https://www.drupal.org/node/2824851. - if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); - } - - // Unchanged values for entity keys don't need access checking. - if ($original_entity->get($field_name)->equals($field)) { - continue; - } - // It is not possible to set the language to NULL as it is automatically - // re-initialized. As it must not be empty, skip it if it is. - elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) { - continue; - } + // It is not possible to set the language to NULL as it is automatically + // re-initialized. As it must not be empty, skip it if it is. + // @todo Remove in https://www.drupal.org/project/drupal/issues/2933408. + if ($entity->getEntityType()->hasKey('langcode') && $field_name === $entity->getEntityType()->getKey('langcode') && $field->isEmpty()) { + continue; } - - if (!$original_entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + if ($this->checkPatchFieldAccess($original_entity->get($field_name), $field)) { + $original_entity->set($field_name, $field->getValue()); } - $original_entity->set($field_name, $field->getValue()); } // Validate the received data before saving. @@ -274,6 +255,49 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity } } + /** + * Checks whether the given field should be PATCHed. + * + * @param \Drupal\Core\Field\FieldItemListInterface $original_field + * The original (stored) value for the field. + * @param \Drupal\Core\Field\FieldItemListInterface $received_field + * The received value for the field. + * + * @return bool + * Whether the field should be PATCHed or not. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown when the user sending the request is not allowed to update the + * field. Only thrown when the user could not abuse this information to + * determine the stored value. + * + * @internal + */ + protected function checkPatchFieldAccess(FieldItemListInterface $original_field, FieldItemListInterface $received_field) { + // If the user is allowed to edit the field, it is always safe to set the + // received value. We may be setting an unchanged value, but that is ok. + if ($original_field->access('edit')) { + return TRUE; + } + + // The user might not have access to edit the field, but still needs to + // submit the current field value as part of the PATCH request. For + // example, the entity keys required by denormalizers. Therefore, if the + // received value equals the stored value, return FALSE without throwing an + // exception. But only for fields that the user has access to view, because + // the user has no legitimate way of knowing the current value of fields + // that they are not allowed to view, and we must not make the presence or + // absence of a 403 response a way to find that out. + if ($original_field->equals($received_field) && $original_field->access('view')) { + return FALSE; + } + + // It's helpful and safe to let the user know when they are not allowed to + // update a field. + $field_name = $received_field->getName(); + throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + } + /** * Responds to entity DELETE requests. * diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 6d0d0bd18070..4c7947df2d39 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -3,15 +3,20 @@ namespace Drupal\Tests\rest\Functional\EntityResource; use Drupal\Component\Utility\NestedArray; +use Drupal\Component\Utility\Random; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityNullStorage; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem; +use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Url; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\path\Plugin\Field\FieldType\PathItem; use Drupal\rest\ResourceResponseInterface; use Drupal\Tests\rest\Functional\ResourceTestBase; use GuzzleHttp\RequestOptions; @@ -906,6 +911,10 @@ public function testPatch() { $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity(), static::$format); $parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPatchEntity()), static::$format); $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format); + // The 'field_rest_test' field does not allow 'view' access, so does not end + // up in the normalization. Even when we explicitly add it the normalization + // that we send in the body of a PATCH request, it is considered invalid. + $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPatchEntity() + ['field_rest_test' => $this->entity->get('field_rest_test')->getValue()], static::$format); // The URL and Guzzle request options that will be used in this test. The // request options will be modified/expanded throughout this test: @@ -997,22 +1006,31 @@ public function testPatch() { $response = $this->request('PATCH', $url, $request_options); $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response); - // DX: 403 when sending PATCH request with read-only fields. - // First send all fields (the "maximum normalization"). Assert the expected - // error message for the first PATCH-protected field. Remove that field from - // the normalization, send another request, assert the next PATCH-protected - // field error message. And so on. - $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format); + $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3; + + // DX: 403 when entity contains field without 'edit' nor 'view' access, even + // when the value for that field matches the current value. This is allowed + // in principle, but leads to information disclosure. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response); + + // DX: 403 when sending PATCH request with updated read-only fields. + list($modified_entity, $original_values) = static::getModifiedEntityForPatchTesting($this->entity); + // Send PATCH request by serializing the modified entity, assert the error + // response, change the modified entity field that caused the error response + // back to its original value, repeat. for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) { - $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i)); - $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format); + $patch_protected_field_name = static::$patchProtectedFieldNames[$i]; + $request_options[RequestOptions::BODY] = $this->serializer->serialize($modified_entity, static::$format); $response = $this->request('PATCH', $url, $request_options); - $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response); + $this->assertResourceErrorResponse(403, "Access denied on updating field '" . $patch_protected_field_name . "'.", $response); + $modified_entity->get($patch_protected_field_name)->setValue($original_values[$patch_protected_field_name]); } - // 200 for well-formed request that sends the maximum number of fields. - $max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames); - $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format); + // 200 for well-formed PATCH request that sends all fields (even including + // read-only ones, but with unchanged values). + $valid_request_body = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format); + $request_options[RequestOptions::BODY] = $this->serializer->serialize($valid_request_body, static::$format); $response = $this->request('PATCH', $url, $request_options); $this->assertResourceResponse(200, FALSE, $response); @@ -1235,37 +1253,71 @@ protected function getEntityResourcePostUrl() { } /** - * Makes the given entity normalization invalid. + * Clones the given entity and modifies all PATCH-protected fields. * - * @param array $normalization - * An entity normalization. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being tested and to modify. * * @return array - * The updated entity normalization, now invalid. + * Contains two items: + * 1. The modified entity object. + * 2. The original field values, keyed by field name. + * + * @internal */ - protected function makeNormalizationInvalid(array $normalization) { - // Add a second label to this entity to make it invalid. - $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $normalization[$label_field][1]['value'] = 'Second Title'; + protected static function getModifiedEntityForPatchTesting(EntityInterface $entity) { + $modified_entity = clone $entity; + $original_values = []; + foreach (static::$patchProtectedFieldNames as $field_name) { + $field = $modified_entity->get($field_name); + $original_values[$field_name] = $field->getValue(); + switch ($field->getItemDefinition()->getClass()) { + case EntityReferenceItem::class: + // EntityReferenceItem::generateSampleValue() picks one of the last 50 + // entities of the supported type & bundle. We don't care if the value + // is valid, we only care that it's different. + $field->setValue(['target_id' => 99999]); + break; + case BooleanItem::class: + // BooleanItem::generateSampleValue() picks either 0 or 1. So a 50% + // chance of not picking a different value. + $field->value = ((int) $field->value) === 1 ? '0' : '1'; + break; + case PathItem::class: + // 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); + break; + default: + $original_field = clone $field; + while ($field->equals($original_field)) { + $field->generateSampleItems(); + } + break; + } + } - return $normalization; + return [$modified_entity, $original_values]; } /** - * Removes fields from a normalization. + * Makes the given entity normalization invalid. * * @param array $normalization * An entity normalization. - * @param string[] $field_names - * The field names to remove from the entity normalization. * * @return array - * The updated entity normalization. - * - * @see ::testPatch + * The updated entity normalization, now invalid. */ - protected function removeFieldsFromNormalization(array $normalization, $field_names) { - return array_diff_key($normalization, array_flip($field_names)); + protected function makeNormalizationInvalid(array $normalization) { + // Add a second label to this entity to make it invalid. + $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; + $normalization[$label_field][1]['value'] = 'Second Title'; + + return $normalization; } /** diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php index 492ff642e42e..bf0ba7a59194 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php @@ -235,20 +235,6 @@ public function testPatchPath() { $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET')); $normalization = $this->serializer->decode((string) $response->getBody(), static::$format); - // @todo In https://www.drupal.org/node/2824851, we will be able to stop - // unsetting these fields from the normalization, because - // EntityResource::patch() will ignore any fields that are sent that - // match the current value (and obviously we're sending the current - // value). - $normalization = $this->removeFieldsFromNormalization($normalization, [ - 'revision_timestamp', - 'revision_uid', - 'created', - 'changed', - 'promote', - 'sticky', - ]); - // Change node's path alias. $normalization['path'][0]['alias'] .= 's-rule-the-world'; @@ -258,8 +244,11 @@ public function testPatchPath() { $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH')); $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); - // PATCH request: 403 when creating URL aliases unauthorized. + // PATCH request: 403 when creating URL aliases unauthorized. Before + // asserting the 403 response, assert that the stored path alias remains + // unchanged. $response = $this->request('PATCH', $url, $request_options); + $this->assertSame('/llama', $this->entityStorage->loadUnchanged($this->entity->id())->get('path')->alias); $this->assertResourceErrorResponse(403, "Access denied on updating field 'path'.", $response); // Grant permission to create URL aliases. diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index 713c1e5c1bf0..85a4b1a1077f 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -208,15 +208,6 @@ public function testPatchPath() { $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET')); $normalization = $this->serializer->decode((string) $response->getBody(), static::$format); - // @todo In https://www.drupal.org/node/2824851, we will be able to stop - // unsetting these fields from the normalization, because - // EntityResource::patch() will ignore any fields that are sent that - // match the current value (and obviously we're sending the current - // value). - $normalization = $this->removeFieldsFromNormalization($normalization, [ - 'changed', - ]); - // Change term's path alias. $normalization['path'][0]['alias'] .= 's-rule-the-world'; From 7f00c34194bf3c71a4de22823960a5d20f55ed5a Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Tue, 2 Jan 2018 13:23:23 -0800 Subject: [PATCH 077/232] =?UTF-8?q?Issue=20#2926483=20by=20plach,=20amatee?= =?UTF-8?q?scu,=20effulgentsia,=20xjm,=20Sam152,=20timmillwood,=20jibran,?= =?UTF-8?q?=20larowlan,=20G=C3=A1bor=20Hojtsy:=20Add=20API=20methods=20for?= =?UTF-8?q?=20determining=20whether=20an=20entity=20object=20is=20the=20la?= =?UTF-8?q?test=20(translation-affecting)=20revision?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Drupal/Core/Entity/ContentEntityBase.php | 20 ++++ .../Core/Entity/ContentEntityStorageBase.php | 41 ++++++++ .../KeyValueContentEntityStorage.php | 14 +++ .../Core/Entity/RevisionableInterface.php | 8 ++ .../Entity/RevisionableStorageInterface.php | 11 +++ .../TranslatableRevisionableInterface.php | 9 ++ ...anslatableRevisionableStorageInterface.php | 15 +++ core/lib/Drupal/Core/Entity/entity.api.php | 33 +++++++ .../Core/ParamConverter/EntityConverter.php | 36 +------ core/modules/quickedit/quickedit.module | 27 ++---- ...visionTest.php => EntityRevisionsTest.php} | 96 ++++++++++++++++++- .../EntityConverterLatestRevisionTest.php | 28 ++++++ 12 files changed, 289 insertions(+), 49 deletions(-) rename core/tests/Drupal/KernelTests/Core/Entity/{EntityLoadedRevisionTest.php => EntityRevisionsTest.php} (60%) diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 3effe8b3c259..6094bdfefddb 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -331,6 +331,26 @@ public function isDefaultRevision($new_value = NULL) { return $this->isNew() || $return; } + /** + * {@inheritdoc} + */ + public function isLatestRevision() { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId()); + + return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id()); + } + + /** + * {@inheritdoc} + */ + public function isLatestTranslationAffectedRevision() { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId()); + + return $this->getLoadedRevisionId() == $storage->getLatestTranslationAffectedRevisionId($this->id(), $this->language()->getId()); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index a3bb20508f90..7f36c11a67b9 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -166,6 +166,47 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr return $translation; } + /** + * {@inheritdoc} + */ + public function getLatestRevisionId($entity_id) { + if (!$this->entityType->isRevisionable()) { + return NULL; + } + + $result = $this->getQuery() + ->latestRevision() + ->condition($this->entityType->getKey('id'), $entity_id) + ->accessCheck(FALSE) + ->execute(); + + return key($result); + } + + /** + * {@inheritdoc} + */ + public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) { + if (!$this->entityType->isRevisionable()) { + return NULL; + } + + if (!$this->entityType->isTranslatable()) { + return $this->getLatestRevisionId($entity_id); + } + + $result = $this->getQuery() + ->allRevisions() + ->condition($this->entityType->getKey('id'), $entity_id) + ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode) + ->range(0, 1) + ->sort($this->entityType->getKey('revision'), 'DESC') + ->accessCheck(FALSE) + ->execute(); + + return key($result); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php index 2c57405c19f8..b0725251e526 100644 --- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php @@ -30,4 +30,18 @@ public function loadMultipleRevisions(array $revision_ids) { return []; } + /** + * {@inheritdoc} + */ + public function getLatestRevisionId($entity_id) { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) { + return NULL; + } + } diff --git a/core/lib/Drupal/Core/Entity/RevisionableInterface.php b/core/lib/Drupal/Core/Entity/RevisionableInterface.php index 14690e3c54a6..e25bc254430c 100644 --- a/core/lib/Drupal/Core/Entity/RevisionableInterface.php +++ b/core/lib/Drupal/Core/Entity/RevisionableInterface.php @@ -51,6 +51,14 @@ public function getRevisionId(); */ public function isDefaultRevision($new_value = NULL); + /** + * Checks if this entity is the latest revision. + * + * @return bool + * TRUE if the entity is the latest revision, FALSE otherwise. + */ + public function isLatestRevision(); + /** * Acts on a revision before it gets saved. * diff --git a/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php index 871fcc5435f3..f92d419aa543 100644 --- a/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/RevisionableStorageInterface.php @@ -40,4 +40,15 @@ public function loadMultipleRevisions(array $revision_ids); */ public function deleteRevision($revision_id); + /** + * Returns the latest revision identifier for an entity. + * + * @param int|string $entity_id + * The entity identifier. + * + * @return int|string|null + * The latest revision identifier or NULL if no revision could be found. + */ + public function getLatestRevisionId($entity_id); + } diff --git a/core/lib/Drupal/Core/Entity/TranslatableRevisionableInterface.php b/core/lib/Drupal/Core/Entity/TranslatableRevisionableInterface.php index ac1f2e9001ae..e1bc1e375c8f 100644 --- a/core/lib/Drupal/Core/Entity/TranslatableRevisionableInterface.php +++ b/core/lib/Drupal/Core/Entity/TranslatableRevisionableInterface.php @@ -7,6 +7,15 @@ */ interface TranslatableRevisionableInterface extends TranslatableInterface, RevisionableInterface { + /** + * Checks whether this is the latest revision affecting this translation. + * + * @return bool + * TRUE if this revision is the latest one affecting the active translation, + * FALSE otherwise. + */ + public function isLatestTranslationAffectedRevision(); + /** * Marks the current revision translation as affected. * diff --git a/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php b/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php index eded8ed37c98..1a6b73784e14 100644 --- a/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php @@ -6,4 +6,19 @@ * A storage that supports translatable and revisionable entity types. */ interface TranslatableRevisionableStorageInterface extends TranslatableStorageInterface, RevisionableStorageInterface { + + /** + * Returns the latest revision affecting the specified translation. + * + * @param int|string $entity_id + * The entity identifier. + * @param string $langcode + * The language code of the translation. + * + * @return int|string|null + * A revision ID or NULL if no revision affecting the specified translation + * could be found. + */ + public function getLatestTranslationAffectedRevisionId($entity_id, $langcode); + } diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index 6b4029c71b08..2b893fd7b3e0 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -168,6 +168,35 @@ * @endcode * This involves the same hooks and operations as regular entity loading. * + * The "latest revision" of an entity is the most recently created one, + * regardless of it being default or pending. If the entity is translatable, + * revision translations are not taken into account either. In other words, any + * time a new revision is created, that becomes the latest revision for the + * entity overall, regardless of the affected translations. To load the latest + * revision of an entity: + * @code + * $revision_id = $storage->getLatestRevisionId($entity_id); + * $entity = $storage->loadRevision($revision_id); + * @endcode + * As usual, if the entity is translatable, this code instantiates into $entity + * the default translation of the revision, even if the latest revision contains + * only changes to a different translation: + * @code + * $is_default = $entity->isDefaultTranslation(); // returns TRUE + * @endcode + * + * The "latest translation-affected revision" is the most recently created one + * that affects the specified translation. For example, when a new revision + * introducing some changes to an English translation is saved, that becomes the + * new "latest revision". However, if an existing Italian translation was not + * affected by those changes, then the "latest translation-affected revision" + * for Italian remains what it was. To load the Italian translation at its + * latest translation-affected revision: + * @code + * $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity_id, 'it'); + * $it_translation = $storage->loadRevision($revision_id)->getTranslation('it'); + * @endcode + * * @section save Save operations * To update an existing entity, you will need to load it, change properties, * and then save; as described above, when creating a new entity, you will also @@ -198,6 +227,10 @@ * - Comment: hook_comment_publish() and hook_comment_unpublish() as * appropriate. * + * Note that all translations available for the entity are stored during a save + * operation. When saving a new revision, a copy of every translation is stored, + * regardless of it being affected by the revision. + * * @section edit Editing operations * When an entity's add/edit form is used to add or edit an entity, there * are several hooks that are invoked: diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index 1f6b42b6096f..f76b327a86a3 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -5,8 +5,6 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\TypedData\TranslatableInterface; use Symfony\Component\Routing\Route; @@ -71,8 +69,11 @@ public function convert($value, $definition, $name, array $defaults) { // If the entity type is revisionable and the parameter has the // "load_latest_revision" flag, load the latest revision. if ($entity instanceof ContentEntityInterface && !empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) { - $latest_revision_id = $this->getLatestRevisionId($storage, $entity_definition, $value); - if ($entity->getLoadedRevisionId() !== $latest_revision_id) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $latest_revision_id = $storage->getLatestRevisionId($value); + // We explicitly perform a loose equality check, since a revision ID may + // be returned as an integer or a string. + if ($entity->getLoadedRevisionId() != $latest_revision_id) { $entity = $storage->loadRevision($latest_revision_id); } } @@ -86,33 +87,6 @@ public function convert($value, $definition, $name, array $defaults) { return $entity; } - /** - * Get the latest revision ID. - * - * @param \Drupal\Core\Entity\EntityStorageInterface $storage - * The entity storage. - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_definition - * The entity definition. - * @param mixed $value - * The raw value. - * - * @return int - * The latest revision ID for a given entity. - */ - protected function getLatestRevisionId(EntityStorageInterface $storage, EntityTypeInterface $entity_definition, $value) { - // @todo, replace this query with a standardized way of getting the - // latest revision in https://www.drupal.org/node/2784201. - $result = $storage - ->getQuery() - ->latestRevision() - ->condition($entity_definition->getKey('id'), $value) - // The entity converter is not concerned with access checking, skip the - // access check when looking up the latest revision. - ->accessCheck(FALSE) - ->execute(); - return key($result); - } - /** * {@inheritdoc} */ diff --git a/core/modules/quickedit/quickedit.module b/core/modules/quickedit/quickedit.module index ad08fc1e0df6..1794f66f902f 100644 --- a/core/modules/quickedit/quickedit.module +++ b/core/modules/quickedit/quickedit.module @@ -130,10 +130,10 @@ function quickedit_preprocess_page_title(&$variables) { function quickedit_preprocess_field(&$variables) { $variables['#cache']['contexts'][] = 'user.permissions'; $element = $variables['element']; - /** @var $entity \Drupal\Core\Entity\EntityInterface */ + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $element['#object']; - if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !_quickedit_entity_is_latest_revision($entity)) { + if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !$entity->isLatestRevision()) { return; } @@ -157,8 +157,9 @@ function quickedit_preprocess_field(&$variables) { * Implements hook_entity_view_alter(). */ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $build['#cache']['contexts'][] = 'user.permissions'; - if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !_quickedit_entity_is_latest_revision($entity)) { + if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !$entity->isLatestRevision()) { return; } @@ -174,22 +175,14 @@ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityVie * @return bool * TRUE if the loaded entity is the latest revision, FALSE otherwise. * - * @todo Remove this method once better support for pending revisions is added - * to core https://www.drupal.org/node/2784201. + * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use + * \Drupal\Core\Entity\RevisionableInterface::isLatestRevision() instead. + * As internal API, _quickedit_entity_is_latest_revision() may also be removed + * in a minor release. * * @internal */ function _quickedit_entity_is_latest_revision(ContentEntityInterface $entity) { - if (!$entity->getEntityType()->isRevisionable() || $entity->isNew()) { - return TRUE; - } - - $latest_revision = \Drupal::entityTypeManager() - ->getStorage($entity->getEntityTypeId()) - ->getQuery() - ->latestRevision() - ->condition($entity->getEntityType()->getKey('id'), $entity->id()) - ->execute(); - - return !empty($latest_revision) && $entity->getLoadedRevisionId() == key($latest_revision) ? TRUE : FALSE; + @trigger_error('_quickedit_entity_is_latest_revision() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\RevisionableInterface::isLatestRevision() instead. As internal API, _quickedit_entity_is_latest_revision() may also be removed in a minor release.', E_USER_DEPRECATED); + return $entity->isLatestRevision(); } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionsTest.php similarity index 60% rename from core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php rename to core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionsTest.php index a3b99fe0c95e..e5407fcc968e 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionsTest.php @@ -8,9 +8,11 @@ /** * Tests the loaded Revision of an entity. * + * @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase + * * @group entity */ -class EntityLoadedRevisionTest extends EntityKernelTestBase { +class EntityRevisionsTest extends EntityKernelTestBase { /** * Modules to enable. @@ -164,7 +166,99 @@ public function testSaveInHookEntityInsert() { $loadedRevisionId = \Drupal::state()->get('entity_test.loadedRevisionId'); $this->assertEquals($entity->getLoadedRevisionId(), $loadedRevisionId); $this->assertEquals($entity->getRevisionId(), $entity->getLoadedRevisionId()); + } + + /** + * Tests that latest revisions are working as expected. + * + * @covers ::isLatestRevision + */ + public function testIsLatestRevision() { + // Create a basic EntityTestMulRev entity and save it. + $entity = EntityTestMulRev::create(); + $entity->save(); + $this->assertTrue($entity->isLatestRevision()); + + // Load the created entity and create a new pending revision. + $pending_revision = EntityTestMulRev::load($entity->id()); + $pending_revision->setNewRevision(TRUE); + $pending_revision->isDefaultRevision(FALSE); + + // The pending revision should still be marked as the latest one before it + // is saved. + $this->assertTrue($pending_revision->isLatestRevision()); + $pending_revision->save(); + $this->assertTrue($pending_revision->isLatestRevision()); + + // Load the default revision and check that it is not marked as the latest + // revision. + $default_revision = EntityTestMulRev::load($entity->id()); + $this->assertFalse($default_revision->isLatestRevision()); + } + + /** + * Tests that latest affected revisions are working as expected. + * + * The latest revision affecting a particular translation behaves as the + * latest revision for monolingual entities. + * + * @covers ::isLatestTranslationAffectedRevision + * @covers \Drupal\Core\Entity\ContentEntityStorageBase::getLatestRevisionId + * @covers \Drupal\Core\Entity\ContentEntityStorageBase::getLatestTranslationAffectedRevisionId + */ + public function testIsLatestAffectedRevisionTranslation() { + ConfigurableLanguage::createFromLangcode('it')->save(); + + // Create a basic EntityTestMulRev entity and save it. + $entity = EntityTestMulRev::create(); + $entity->setName($this->randomString()); + $entity->save(); + $this->assertTrue($entity->isLatestTranslationAffectedRevision()); + + // Load the created entity and create a new pending revision. + $pending_revision = EntityTestMulRev::load($entity->id()); + $pending_revision->setName($this->randomString()); + $pending_revision->setNewRevision(TRUE); + $pending_revision->isDefaultRevision(FALSE); + + // Check that no revision affecting Italian is available, given that no + // Italian translation has been created yet. + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage($entity->getEntityTypeId()); + $this->assertNull($storage->getLatestTranslationAffectedRevisionId($entity->id(), 'it')); + $this->assertEquals($pending_revision->getLoadedRevisionId(), $storage->getLatestRevisionId($entity->id())); + + // The pending revision should still be marked as the latest affected one + // before it is saved. + $this->assertTrue($pending_revision->isLatestTranslationAffectedRevision()); + $pending_revision->save(); + $this->assertTrue($pending_revision->isLatestTranslationAffectedRevision()); + + // Load the default revision and check that it is not marked as the latest + // (translation-affected) revision. + $default_revision = EntityTestMulRev::load($entity->id()); + $this->assertFalse($default_revision->isLatestRevision()); + $this->assertFalse($default_revision->isLatestTranslationAffectedRevision()); + // Add a translation in a new pending revision and verify that both the + // English and Italian revision translations are the latest affected + // revisions for their respective languages, while the English revision is + // not the latest revision. + /** @var \Drupal\entity_test\Entity\EntityTestMulRev $en_revision */ + $en_revision = clone $pending_revision; + /** @var \Drupal\entity_test\Entity\EntityTestMulRev $it_revision */ + $it_revision = $pending_revision->addTranslation('it'); + $it_revision->setName($this->randomString()); + $it_revision->setNewRevision(TRUE); + $it_revision->isDefaultRevision(FALSE); + // @todo Remove this once the "original" property works with revisions. See + // https://www.drupal.org/project/drupal/issues/2859042. + $it_revision->original = $storage->loadRevision($it_revision->getLoadedRevisionId()); + $it_revision->save(); + $this->assertTrue($it_revision->isLatestRevision()); + $this->assertTrue($it_revision->isLatestTranslationAffectedRevision()); + $this->assertFalse($en_revision->isLatestRevision()); + $this->assertTrue($en_revision->isLatestTranslationAffectedRevision()); } } diff --git a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php index 34b53cb97ea1..d2fd2f524132 100644 --- a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php +++ b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php @@ -122,4 +122,32 @@ public function testWithTranslatedPendingRevision() { $this->assertEquals($translated_entity->getLoadedRevisionId(), $converted->getLoadedRevisionId()); } + /** + * Tests that pending revisions are loaded only when needed. + */ + public function testOptimizedConvert() { + $entity = EntityTestMulRev::create(); + $entity->save(); + + // Populate static cache for the current entity. + $entity = EntityTestMulRev::load($entity->id()); + + // Delete the base table entry for the current entity, however, since the + // storage will query the revision table to get the latest revision, the + // logic handling pending revisions will work correctly anyway. + /** @var \Drupal\Core\Database\Connection $database */ + $database = $this->container->get('database'); + $database->delete('entity_test_mulrev') + ->condition('id', $entity->id()) + ->execute(); + + // If optimization works, converting a default revision should not trigger + // a storage load, thus making the following assertion pass. + $converted = $this->converter->convert(1, [ + 'load_latest_revision' => TRUE, + 'type' => 'entity:entity_test_mulrev', + ], 'foo', []); + $this->assertEquals($entity->getLoadedRevisionId(), $converted->getLoadedRevisionId()); + } + } From 0d2a45adc2dd3c84ccd893875edf2820a4d5e824 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 3 Jan 2018 15:47:09 +0000 Subject: [PATCH 078/232] Issue #2930715 by alexpott, dawehner: Recursive rebuild caused by installing admin_toolbar_tools module --- core/core.services.yml | 2 + .../Core/Routing/RouteProviderLazyBuilder.php | 40 ++++++++++++++++++- .../lazy_route_provider_install_test.module | 21 ++++++++++ .../Routing/LazyRouteProviderInstallTest.php | 5 +++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 core/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module diff --git a/core/core.services.yml b/core/core.services.yml index 49b27089b52d..737009bac531 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -803,6 +803,8 @@ services: router.route_provider.lazy_builder: class: Drupal\Core\Routing\RouteProviderLazyBuilder arguments: ['@router.route_provider', '@router.builder'] + tags: + - { name: event_subscriber } router.route_preloader: class: Drupal\Core\Routing\RoutePreloader arguments: ['@router.route_provider', '@state', '@cache.bootstrap'] diff --git a/core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php b/core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php index 8c59900c82ff..a2cb8c578513 100644 --- a/core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php +++ b/core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php @@ -3,12 +3,13 @@ namespace Drupal\Core\Routing; use Symfony\Cmf\Component\Routing\PagedRouteProviderInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; /** * A Route Provider front-end for all Drupal-stored routes. */ -class RouteProviderLazyBuilder implements PreloadableRouteProviderInterface, PagedRouteProviderInterface { +class RouteProviderLazyBuilder implements PreloadableRouteProviderInterface, PagedRouteProviderInterface, EventSubscriberInterface { /** * The route provider service. @@ -31,6 +32,18 @@ class RouteProviderLazyBuilder implements PreloadableRouteProviderInterface, Pag */ protected $rebuilt = FALSE; + /** + * Flag to determine if router is currently being rebuilt. + * + * Used to prevent recursive router rebuilds during module installation. + * Recursive rebuilds can occur when route information is required by alter + * hooks that are triggered during a rebuild, for example, + * hook_menu_links_discovered_alter(). + * + * @var bool + */ + protected $rebuilding = FALSE; + /** * RouteProviderLazyBuilder constructor. * @@ -51,7 +64,7 @@ public function __construct(RouteProviderInterface $route_provider, RouteBuilder * The route provider service. */ protected function getRouteProvider() { - if (!$this->rebuilt) { + if (!$this->rebuilt && !$this->rebuilding) { $this->routeBuilder->rebuild(); $this->rebuilt = TRUE; } @@ -132,4 +145,27 @@ public function hasRebuilt() { return $this->rebuilt; } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[RoutingEvents::DYNAMIC][] = ['routerRebuilding', 3000]; + $events[RoutingEvents::FINISHED][] = ['routerRebuildFinished', -3000]; + return $events; + } + + /** + * Sets the router rebuilding flag to TRUE. + */ + public function routerRebuilding() { + $this->rebuilding = TRUE; + } + + /** + * Sets the router rebuilding flag to FALSE. + */ + public function routerRebuildFinished() { + $this->rebuilding = FALSE; + } + } diff --git a/core/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module b/core/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module new file mode 100644 index 000000000000..7989d765e4e6 --- /dev/null +++ b/core/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module @@ -0,0 +1,21 @@ +get(__FUNCTION__, 'success'); + try { + // Ensure that calling this does not cause a recursive rebuild. + \Drupal::service('router.route_provider')->getAllRoutes(); + } + catch (\RuntimeException $e) { + $message = 'failed'; + } + \Drupal::state()->set(__FUNCTION__, $message); +} diff --git a/core/tests/Drupal/FunctionalTests/Routing/LazyRouteProviderInstallTest.php b/core/tests/Drupal/FunctionalTests/Routing/LazyRouteProviderInstallTest.php index bc01dc288a51..cec35a07acc0 100644 --- a/core/tests/Drupal/FunctionalTests/Routing/LazyRouteProviderInstallTest.php +++ b/core/tests/Drupal/FunctionalTests/Routing/LazyRouteProviderInstallTest.php @@ -23,6 +23,11 @@ public function testInstallation() { // we cannot use ::assertEquals(). $this->assertStringEndsWith('/admin', \Drupal::state()->get('Drupal\lazy_route_provider_install_test\PluginManager')); $this->assertStringEndsWith('/router_test/test1', \Drupal::state()->get('router_test_install')); + // If there is an exception thrown in rebuilding a route then the state + // 'lazy_route_provider_install_test_menu_links_discovered_alter' will be + // set. + // @see lazy_route_provider_install_test_menu_links_discovered_alter(). + $this->assertEquals('success', \Drupal::state()->get('lazy_route_provider_install_test_menu_links_discovered_alter', NULL)); } } From c2d7454b549f19268302bb5017d59465c063bd8e Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 3 Jan 2018 15:49:54 +0000 Subject: [PATCH 079/232] Issue #2932865 by kiamlaluno, joachim: Incorrect description for ExtensionDiscovery::scanDirectory() --- core/lib/Drupal/Core/Extension/ExtensionDiscovery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index b74fc04f1db2..17c0e5b4a311 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -381,7 +381,7 @@ protected function process(array $all_files) { } /** - * Recursively scans a base directory for the requested extension type. + * Recursively scans a base directory for the extensions it contains. * * @param string $dir * A relative base directory path to scan, without trailing slash. From 3f57ebc31b9e73a2cbf54cabf6ea8408e05b0d36 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 3 Jan 2018 15:52:22 +0000 Subject: [PATCH 080/232] Issue #2862743 by masipila: Add documentation to DestinationBase destination plugin --- .../src/Plugin/migrate/destination/DestinationBase.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php b/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php index d6c3088817ae..21a1869b1f90 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php @@ -12,7 +12,12 @@ /** * Base class for migrate destination classes. * - * @see \Drupal\migrate\Plugin\MigrateDestinationInterface + * Migrate destination plugins perfom the import operation of the migration. + * Destination plugins extend this abstract base class. A destination plugin + * must implement at least fields(), getIds() and import() methods. Destination + * plugins can also support rollback operations. For more + * information, refer to \Drupal\migrate\Plugin\MigrateDestinationInterface. + * * @see \Drupal\migrate\Plugin\MigrateDestinationPluginManager * @see \Drupal\migrate\Annotation\MigrateDestination * @see plugin_api From c5fba61258904541e78a1eefdb9b2f9b344edc84 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 3 Jan 2018 17:14:42 +0000 Subject: [PATCH 081/232] Issue #2894068 by Jo Fitzgerald, davidsickmiller, alexpott, heddn, Yogesh Pawar, quietone, xjm: datetime_type is not set correctly when migrating datetime fields from D7 --- .../migrate/process/d7/FieldSettings.php | 10 + .../Kernel/Migrate/d7/MigrateFieldTest.php | 14 + .../migrate_drupal/tests/fixtures/drupal7.php | 386 ++++++++++++++++++ .../src/Functional/d7/MigrateUpgrade7Test.php | 4 +- 4 files changed, 412 insertions(+), 2 deletions(-) diff --git a/core/modules/field/src/Plugin/migrate/process/d7/FieldSettings.php b/core/modules/field/src/Plugin/migrate/process/d7/FieldSettings.php index dc629a448289..63f3f27255fc 100644 --- a/core/modules/field/src/Plugin/migrate/process/d7/FieldSettings.php +++ b/core/modules/field/src/Plugin/migrate/process/d7/FieldSettings.php @@ -26,6 +26,16 @@ public function transform($value, MigrateExecutableInterface $migrate_executable } break; + case 'date': + case 'datetime': + case 'datestamp': + if ($value['granularity']['hour'] === 0 + && $value['granularity']['minute'] === 0 + && $value['granularity']['second'] === 0) { + $value['datetime_type'] = 'date'; + } + break; + case 'taxonomy_term_reference': $value['target_type'] = 'taxonomy_term'; break; diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php index 272af0ec6850..6531fe4d40a1 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php @@ -101,6 +101,8 @@ public function testFields() { $this->assertEntity('node.field_node_entityreference', 'entity_reference', TRUE, -1); $this->assertEntity('node.field_user_entityreference', 'entity_reference', TRUE, 1); $this->assertEntity('node.field_term_entityreference', 'entity_reference', TRUE, -1); + $this->assertEntity('node.field_date_without_time', 'datetime', TRUE, 1); + $this->assertEntity('node.field_datetime_without_time', 'datetime', TRUE, 1); // Assert that the taxonomy term reference fields are referencing the // correct entity type. @@ -117,6 +119,18 @@ public function testFields() { $this->assertEquals('user', $field->getSetting('target_type')); $field = FieldStorageConfig::load('node.field_term_entityreference'); $this->assertEquals('taxonomy_term', $field->getSetting('target_type')); + + // Make sure that datetime fields get the right datetime_type setting + $field = FieldStorageConfig::load('node.field_date'); + $this->assertEquals('datetime', $field->getSetting('datetime_type')); + $field = FieldStorageConfig::load('node.field_date_without_time'); + $this->assertEquals('date', $field->getSetting('datetime_type')); + $field = FieldStorageConfig::load('node.field_datetime_without_time'); + $this->assertEquals('date', $field->getSetting('datetime_type')); + // Except for field_date_with_end_time which is a timestamp and so does not + // have a datetime_type setting. + $field = FieldStorageConfig::load('node.field_date_with_end_time'); + $this->assertNull($field->getSetting('datetime_type')); } /** diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal7.php b/core/modules/migrate_drupal/tests/fixtures/drupal7.php index da9d46748ba1..a7117303e72a 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal7.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal7.php @@ -3576,6 +3576,36 @@ 'translatable' => '0', 'deleted' => '0', )) +->values(array( + 'id' => '35', + 'field_name' => 'field_datetime_without_time', + 'type' => 'datetime', + 'module' => 'date', + 'active' => '1', + 'storage_type' => 'field_sql_storage', + 'storage_module' => 'field_sql_storage', + 'storage_active' => '1', + 'locked' => '0', + 'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:6:{s:11:"granularity";a:6:{s:5:"month";s:5:"month";s:3:"day";s:3:"day";s:4:"hour";i:0;s:6:"minute";i:0;s:4:"year";s:4:"year";s:6:"second";i:0;}s:11:"tz_handling";s:4:"site";s:11:"timezone_db";s:3:"UTC";s:13:"cache_enabled";i:0;s:11:"cache_count";s:1:"4";s:6:"todate";s:0:"";}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:38:"field_data_field_datetime_without_time";a:1:{s:5:"value";s:33:"field_datetime_without_time_value";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:42:"field_revision_field_datetime_without_time";a:1:{s:5:"value";s:33:"field_datetime_without_time_value";}}}}}s:12:"foreign keys";a:0:{}s:7:"indexes";a:0:{}s:2:"id";s:1:"9";}', + 'cardinality' => '1', + 'translatable' => '0', + 'deleted' => '0', +)) +->values(array( + 'id' => '36', + 'field_name' => 'field_date_without_time', + 'type' => 'date', + 'module' => 'date', + 'active' => '1', + 'storage_type' => 'field_sql_storage', + 'storage_module' => 'field_sql_storage', + 'storage_active' => '1', + 'locked' => '0', + 'data' => 'a:7:{s:12:"translatable";s:1:"0";s:12:"entity_types";a:0:{}s:8:"settings";a:6:{s:11:"granularity";a:6:{s:5:"month";s:5:"month";s:3:"day";s:3:"day";s:4:"hour";i:0;s:6:"minute";i:0;s:4:"year";s:4:"year";s:6:"second";i:0;}s:11:"tz_handling";s:4:"site";s:11:"timezone_db";s:3:"UTC";s:13:"cache_enabled";i:0;s:11:"cache_count";s:1:"4";s:6:"todate";s:0:"";}s:7:"storage";a:5:{s:4:"type";s:17:"field_sql_storage";s:8:"settings";a:0:{}s:6:"module";s:17:"field_sql_storage";s:6:"active";s:1:"1";s:7:"details";a:1:{s:3:"sql";a:2:{s:18:"FIELD_LOAD_CURRENT";a:1:{s:34:"field_data_field_date_without_time";a:1:{s:5:"value";s:29:"field_date_without_time_value";}}s:19:"FIELD_LOAD_REVISION";a:1:{s:38:"field_revision_field_date_without_time";a:1:{s:5:"value";s:29:"field_date_without_time_value";}}}}}s:12:"foreign keys";a:0:{}s:7:"indexes";a:0:{}s:2:"id";s:1:"9";}', + 'cardinality' => '1', + 'translatable' => '0', + 'deleted' => '0', +)) ->execute(); $connection->schema()->createTable('field_config_instance', array( @@ -4158,6 +4188,24 @@ 'data' => 'a:7:{s:5:"label";s:31:"Text summary plain and filtered";s:6:"widget";a:5:{s:6:"weight";s:2:"14";s:4:"type";s:26:"text_textarea_with_summary";s:6:"module";s:4:"text";s:6:"active";i:1;s:8:"settings";a:2:{s:4:"rows";s:2:"20";s:12:"summary_rows";i:5;}}s:8:"settings";a:3:{s:15:"text_processing";s:1:"1";s:15:"display_summary";i:0;s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"text_default";s:8:"settings";a:0:{}s:6:"module";s:4:"text";s:6:"weight";i:9;}}s:8:"required";i:0;s:11:"description";s:0:"";s:13:"default_value";N;}', 'deleted' => '0', )) +->values(array( + 'id' => '61', + 'field_id' => '35', + 'field_name' => 'field_datetime_without_time', + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'data' => 'a:6:{s:5:"label";s:21:"Datetime without time";s:6:"widget";a:5:{s:6:"weight";s:1:"2";s:4:"type";s:11:"date_select";s:6:"module";s:4:"date";s:6:"active";i:1;s:8:"settings";a:6:{s:12:"input_format";s:13:"m/d/Y - H:i:s";s:19:"input_format_custom";s:0:"";s:10:"year_range";s:5:"-3:+3";s:9:"increment";s:2:"15";s:14:"label_position";s:5:"above";s:10:"text_parts";a:0:{}}}s:8:"settings";a:5:{s:13:"default_value";s:3:"now";s:18:"default_value_code";s:0:"";s:14:"default_value2";s:4:"same";s:19:"default_value_code2";s:0:"";s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"date_default";s:6:"weight";s:1:"3";s:8:"settings";a:5:{s:11:"format_type";s:4:"long";s:15:"multiple_number";s:0:"";s:13:"multiple_from";s:0:"";s:11:"multiple_to";s:0:"";s:6:"fromto";s:4:"both";}s:6:"module";s:4:"date";}}s:8:"required";i:0;s:11:"description";s:0:"";}', + 'deleted' => '0', +)) +->values(array( + 'id' => '62', + 'field_id' => '36', + 'field_name' => 'field_date_without_time', + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'data' => 'a:6:{s:5:"label";s:17:"Date without time";s:6:"widget";a:5:{s:6:"weight";s:1:"2";s:4:"type";s:11:"date_select";s:6:"module";s:4:"date";s:6:"active";i:1;s:8:"settings";a:6:{s:12:"input_format";s:13:"m/d/Y - H:i:s";s:19:"input_format_custom";s:0:"";s:10:"year_range";s:5:"-3:+3";s:9:"increment";s:2:"15";s:14:"label_position";s:5:"above";s:10:"text_parts";a:0:{}}}s:8:"settings";a:5:{s:13:"default_value";s:3:"now";s:18:"default_value_code";s:0:"";s:14:"default_value2";s:4:"same";s:19:"default_value_code2";s:0:"";s:18:"user_register_form";b:0;}s:7:"display";a:1:{s:7:"default";a:5:{s:5:"label";s:5:"above";s:4:"type";s:12:"date_default";s:6:"weight";s:1:"3";s:8:"settings";a:5:{s:11:"format_type";s:4:"long";s:15:"multiple_number";s:0:"";s:13:"multiple_from";s:0:"";s:11:"multiple_to";s:0:"";s:6:"fromto";s:4:"both";}s:6:"module";s:4:"date";}}s:8:"required";i:0;s:11:"description";s:0:"";}', + 'deleted' => '0', +)) ->execute(); $connection->schema()->createTable('field_data_body', array( @@ -4615,6 +4663,174 @@ )) ->execute(); +$connection->schema()->createTable('field_data_field_datetime_without_time', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_datetime_without_time_value' => array( + 'mysql_type' => 'datetime', + 'pgsql_type' => 'timestamp without time zone', + 'sqlite_type' => 'varchar', + 'sqlsrv_type' => 'smalldatetime', + 'not null' => FALSE, + ), + ), + 'primary key' => array( + 'entity_type', + 'deleted', + 'entity_id', + 'language', + 'delta', + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_data_field_datetime_without_time') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_datetime_without_time_value', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_datetime_without_time_value' => '2015-01-20 00:00:00', +)) +->execute(); + +$connection->schema()->createTable('field_data_field_date_without_time', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'entity_id' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'not null' => FALSE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_date_without_time_value' => array( + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => '100', + ), + ), + 'primary key' => array( + 'entity_type', + 'deleted', + 'entity_id', + 'language', + 'delta', + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_data_field_date_without_time') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_date_without_time_value', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_date_without_time_value' => '2015-01-20T00:00:00', +)) +->execute(); + $connection->schema()->createTable('field_data_field_email', array( 'fields' => array( 'entity_type' => array( @@ -7801,6 +8017,176 @@ )) ->execute(); +$connection->schema()->createTable('field_revision_field_datetime_without_time', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + '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, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_datetime_without_time_value' => array( + 'mysql_type' => 'datetime', + 'pgsql_type' => 'timestamp without time zone', + 'sqlite_type' => 'varchar', + 'sqlsrv_type' => 'smalldatetime', + 'not null' => FALSE, + ), + ), + 'primary key' => array( + 'entity_type', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_revision_field_datetime_without_time') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_datetime_without_time_value', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_datetime_without_time_value' => '2015-01-20 00:00:00', +)) +->execute(); + +$connection->schema()->createTable('field_revision_field_date_without_time', array( + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'bundle' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'deleted' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + '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, + ), + 'language' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ), + 'delta' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ), + 'field_date_without_time_value' => array( + 'type' => 'varchar', + 'not null' => FALSE, + 'length' => '100', + ), + ), + 'primary key' => array( + 'entity_type', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('field_revision_field_date_without_time') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_date_without_time_value', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_date_without_time_value' => '2015-01-20T00:00:00', +)) +->execute(); + $connection->schema()->createTable('field_revision_field_email', array( 'fields' => array( 'entity_type' => array( 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 608967c75411..21210dde7653 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 @@ -52,8 +52,8 @@ protected function getEntityCounts() { 'configurable_language' => 4, 'contact_form' => 3, 'editor' => 2, - 'field_config' => 61, - 'field_storage_config' => 44, + 'field_config' => 63, + 'field_storage_config' => 46, 'file' => 3, 'filter_format' => 7, 'image_style' => 6, From 5485720ee8b53acb7b666433667a6f83b192a80e Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Wed, 3 Jan 2018 17:42:51 +0000 Subject: [PATCH 082/232] Issue #2866779 by Mile23, dawehner: Add a way to trigger_error() for deprecated hooks --- .../Drupal/Core/Extension/ModuleHandler.php | 60 +++++++++++++ .../Core/Extension/ModuleHandlerInterface.php | 89 +++++++++++++++++++ .../deprecation_test/deprecation_test.module | 14 +++ .../ModuleHandlerDeprecatedHookTest.php | 61 +++++++++++++ ...HandlerDeprecatedHookUnimplementedTest.php | 33 +++++++ 5 files changed, 257 insertions(+) create mode 100644 core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookTest.php create mode 100644 core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookUnimplementedTest.php diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index d8d36088accd..ad539fd7dfba 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -411,6 +411,44 @@ public function invokeAll($hook, array $args = []) { return $return; } + /** + * {@inheritdoc} + */ + public function invokeDeprecated($description, $module, $hook, array $args = array()) { + $result = $this->invoke($module, $hook, $args); + $this->triggerDeprecationError($description, $hook); + return $result; + } + + /** + * {@inheritdoc} + */ + public function invokeAllDeprecated($description, $hook, array $args = array()) { + $result = $this->invokeAll($hook, $args); + $this->triggerDeprecationError($description, $hook); + return $result; + } + + + /** + * Triggers an E_USER_DEPRECATED error if any module implements the hook. + * + * @param string $description + * Helpful text describing what to do instead of implementing this hook. + * @param string $hook + * The name of the hook. + */ + private function triggerDeprecationError($description, $hook) { + $modules = array_keys($this->getImplementationInfo($hook)); + if (!empty($modules)) { + $message = 'The deprecated hook hook_' . $hook . '() is implemented in these functions: '; + $implementations = array_map(function ($module) use ($hook) { + return $module . '_' . $hook . '()'; + }, $modules); + @trigger_error($message . implode(', ', $implementations) . '. ' . $description, E_USER_DEPRECATED); + } + } + /** * {@inheritdoc} */ @@ -502,6 +540,28 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) { } } + /** + * {@inheritdoc} + */ + public function alterDeprecated($description, $type, &$data, &$context1 = NULL, &$context2 = NULL) { + // Invoke the alter hook. This has the side effect of populating + // $this->alterFunctions. + $this->alter($type, $data, $context1, $context2); + // The $type parameter can be an array. alter() will deal with this + // internally, but we have to extract the proper $cid in order to discover + // implementations. + $cid = $type; + if (is_array($type)) { + $cid = implode(',', $type); + $extra_types = $type; + $type = array_shift($extra_types); + } + if (!empty($this->alterFunctions[$cid])) { + $message = 'The deprecated alter hook hook_' . $type . '_alter() is implemented in these functions: ' . implode(', ', $this->alterFunctions[$cid]) . '.'; + @trigger_error($message . ' ' . $description, E_USER_DEPRECATED); + } + } + /** * Provides information about modules' implementations of a hook. * diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index 03d3f66c7ba2..f1097e388104 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -238,6 +238,59 @@ public function invoke($module, $hook, array $args = []); */ public function invokeAll($hook, array $args = []); + /** + * Invokes a deprecated hook in a particular module. + * + * Invoking a deprecated hook adds the behavior of triggering an + * E_USER_DEPRECATED error if any implementations are found. + * + * API maintainers should use this method instead of invoke() when their hook + * is deprecated. This method does not detect when a hook is deprecated. + * + * @param string $description + * Helpful text describing what to do instead of implementing this hook. + * @param string $module + * The name of the module (without the .module extension). + * @param string $hook + * The name of the hook to invoke. + * @param array $args + * Arguments to pass to the hook implementation. + * + * @return mixed + * The return value of the hook implementation. + * + * @see \Drupal\Core\Extension\ModuleHandlerInterface::invoke() + * @see https://www.drupal.org/core/deprecation#how-hook + */ + public function invokeDeprecated($description, $module, $hook, array $args = []); + + /** + * Invokes a deprecated hook in all enabled modules that implement it. + * + * Invoking a deprecated hook adds the behavior of triggering an + * E_USER_DEPRECATED error if any implementations are found. + * + * API maintainers should use this method instead of invokeAll() when their + * hook is deprecated. This method does not detect when a hook is deprecated. + * + * @param string $description + * Helpful text describing what to do instead of implementing this hook. + * @param string $hook + * The name of the hook to invoke. + * @param array $args + * Arguments to pass to the hook. + * + * @return array + * An array of return values of the hook implementations. If modules return + * arrays from their implementations, those are merged into one array + * recursively. Note: integer keys in arrays will be lost, as the merge is + * done using array_merge_recursive(). + * + * @see \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll() + * @see https://www.drupal.org/core/deprecation#how-hook + */ + public function invokeAllDeprecated($description, $hook, array $args = []); + /** * Passes alterable variables to specific hook_TYPE_alter() implementations. * @@ -289,6 +342,42 @@ public function invokeAll($hook, array $args = []); */ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL); + /** + * Passes alterable variables to deprecated hook_TYPE_alter() implementations. + * + * This method triggers an E_USER_DEPRECATED error if any implementations of + * the alter hook are found. It is otherwise identical to alter(). + * + * See the documentation for alter() for more details. + * + * @param string $description + * Helpful text describing what to do instead of implementing this alter + * hook. + * @param string|array $type + * A string describing the type of the alterable $data. 'form', 'links', + * 'node_content', and so on are several examples. Alternatively can be an + * array, in which case hook_TYPE_alter() is invoked for each value in the + * array, ordered first by module, and then for each module, in the order of + * values in $type. For example, when Form API is using $this->alter() to + * execute both hook_form_alter() and hook_form_FORM_ID_alter() + * implementations, it passes array('form', 'form_' . $form_id) for $type. + * @param mixed $data + * The variable that will be passed to hook_TYPE_alter() implementations to be + * altered. The type of this variable depends on the value of the $type + * argument. For example, when altering a 'form', $data will be a structured + * array. When altering a 'profile', $data will be an object. + * @param mixed $context1 + * (optional) An additional variable that is passed by reference. + * @param mixed $context2 + * (optional) An additional variable that is passed by reference. If more + * context needs to be provided to implementations, then this should be an + * associative array as described above. + * + * @see \Drupal\Core\Extension\ModuleHandlerInterface::alter() + * @see https://www.drupal.org/core/deprecation#how-hook + */ + public function alterDeprecated($description, $type, &$data, &$context1 = NULL, &$context2 = NULL); + /** * Returns an array of directories for all enabled modules. Useful for * tasks such as finding a file that exists in all module directories. diff --git a/core/modules/system/tests/modules/deprecation_test/deprecation_test.module b/core/modules/system/tests/modules/deprecation_test/deprecation_test.module index e255021d6548..69217db45e46 100644 --- a/core/modules/system/tests/modules/deprecation_test/deprecation_test.module +++ b/core/modules/system/tests/modules/deprecation_test/deprecation_test.module @@ -18,3 +18,17 @@ function deprecation_test_function() { @trigger_error('This is the deprecation message for deprecation_test_function().', E_USER_DEPRECATED); return 'known_return_value'; } + +/** + * Implements hook_deprecated_hook(). + */ +function deprecation_test_deprecated_hook($arg) { + return $arg; +} + +/** + * Implements hook_deprecated_alter_alter(). + */ +function deprecation_test_deprecated_alter_alter(&$data, $context1, $context2) { + $data = [$context1, $context2]; +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookTest.php new file mode 100644 index 000000000000..18245634acbf --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookTest.php @@ -0,0 +1,61 @@ +container->get('module_handler'); + $arg = 'an_arg'; + $this->assertEqual( + $arg, + $module_handler->invokeDeprecated('Use something else.', 'deprecation_test', 'deprecated_hook', [$arg]) + ); + } + + /** + * @covers ::invokeAllDeprecated + * @expectedDeprecation The deprecated hook hook_deprecated_hook() is implemented in these functions: deprecation_test_deprecated_hook(). Use something else. + */ + public function testInvokeAllDeprecated() { + /* @var $module_handler \Drupal\Core\Extension\ModuleHandlerInterface */ + $module_handler = $this->container->get('module_handler'); + $arg = 'an_arg'; + $this->assertEqual( + [$arg], + $module_handler->invokeAllDeprecated('Use something else.', 'deprecated_hook', [$arg]) + ); + } + + /** + * @covers ::alterDeprecated + * @expectedDeprecation The deprecated alter hook hook_deprecated_alter_alter() is implemented in these functions: deprecation_test_deprecated_alter_alter. Alter something else. + */ + public function testAlterDeprecated() { + /* @var $module_handler \Drupal\Core\Extension\ModuleHandlerInterface */ + $module_handler = $this->container->get('module_handler'); + $data = []; + $context1 = 'test1'; + $context2 = 'test2'; + $module_handler->alterDeprecated('Alter something else.', 'deprecated_alter', $data, $context1, $context2); + $this->assertEqual([$context1, $context2], $data); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookUnimplementedTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookUnimplementedTest.php new file mode 100644 index 000000000000..1d4e833e826b --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerDeprecatedHookUnimplementedTest.php @@ -0,0 +1,33 @@ +container->get('module_handler'); + + $module_handler->invokeDeprecated('Use something else.', 'deprecation_test', $unimplemented_hook_name); + $module_handler->invokeAllDeprecated('Use something else.', $unimplemented_hook_name); + $data = []; + $module_handler->alterDeprecated('Alter something else.', $unimplemented_hook_name, $data); + } + +} From 35de151d0ef048ed588dacf0c8bf022ee208abcd Mon Sep 17 00:00:00 2001 From: webchick Date: Wed, 3 Jan 2018 10:34:30 -0800 Subject: [PATCH 083/232] Issue #2924351 by drpal, tedbow, xjm, dawehner: Fix coding standards issues with existing settings tray JavaScript --- .../settings_tray/js/settings_tray.es6.js | 25 +++++++++++-------- .../modules/settings_tray/js/settings_tray.js | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/core/modules/settings_tray/js/settings_tray.es6.js b/core/modules/settings_tray/js/settings_tray.es6.js index a680115490a6..6487690c7bc9 100644 --- a/core/modules/settings_tray/js/settings_tray.es6.js +++ b/core/modules/settings_tray/js/settings_tray.es6.js @@ -5,7 +5,7 @@ * @private */ -(function ($, Drupal) { +(($, Drupal) => { const blockConfigureSelector = '[data-settings-tray-edit]'; const toggleEditSelector = '[data-drupal-settingstray="toggle"]'; const itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, [data-drupal-settingstray="editable"] a, [data-drupal-settingstray="editable"] button'; @@ -81,9 +81,10 @@ if ($editables.length) { // Use event capture to prevent clicks on links. document.querySelector('[data-off-canvas-main-canvas]').addEventListener('click', preventClick, true); - - // When a click occurs try and find the settings-tray edit link - // and click it. + /** + * When a click occurs try and find the settings-tray edit link + * and click it. + */ $editables .not(contextualItemsSelector) .on('click.settingstray', (e) => { @@ -154,14 +155,18 @@ function prepareAjaxLinks() { // Find all Ajax instances that use the 'off_canvas' renderer. Drupal.ajax.instances - // If there is an element and the renderer is 'off_canvas' then we want - // to add our changes. + /** + * If there is an element and the renderer is 'off_canvas' then we want + * to add our changes. + */ .filter(instance => instance && $(instance.element).attr('data-dialog-renderer') === 'off_canvas') - // Loop through all Ajax instances that use the 'off_canvas' renderer to - // set active editable ID. + /** + * Loop through all Ajax instances that use the 'off_canvas' renderer to + * set active editable ID. + */ .forEach((instance) => { // Check to make sure existing dialogOptions aren't overridden. - if (!('dialogOptions' in instance.options.data)) { + if (!instance.options.data.hasOwnProperty('dialogOptions')) { instance.options.data.dialogOptions = {}; } instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(instance.element).parents('.settings-tray-editable').attr('id'); @@ -253,4 +258,4 @@ } }, }); -}(jQuery, Drupal)); +})(jQuery, Drupal); diff --git a/core/modules/settings_tray/js/settings_tray.js b/core/modules/settings_tray/js/settings_tray.js index 78c5b603826a..7a83e156ca5e 100644 --- a/core/modules/settings_tray/js/settings_tray.js +++ b/core/modules/settings_tray/js/settings_tray.js @@ -97,7 +97,7 @@ Drupal.ajax.instances.filter(function (instance) { return instance && $(instance.element).attr('data-dialog-renderer') === 'off_canvas'; }).forEach(function (instance) { - if (!('dialogOptions' in instance.options.data)) { + if (!instance.options.data.hasOwnProperty('dialogOptions')) { instance.options.data.dialogOptions = {}; } instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(instance.element).parents('.settings-tray-editable').attr('id'); From f9445b466bd19c6bf4b017ac6582fb0c85794117 Mon Sep 17 00:00:00 2001 From: webchick Date: Wed, 3 Jan 2018 12:29:21 -0800 Subject: [PATCH 084/232] Issue #2919147 by tedbow, tim.plunkett: When edit mode is enabled new page loads will not have Contextual tabbing constrained --- .../js/toolbar/views/AuralView.es6.js | 1 + .../contextual/js/toolbar/views/AuralView.js | 1 + .../src/FunctionalJavascript/EditModeTest.php | 72 ++++++++++++++----- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/core/modules/contextual/js/toolbar/views/AuralView.es6.js b/core/modules/contextual/js/toolbar/views/AuralView.es6.js index f243e4bd34d0..757c8868d171 100644 --- a/core/modules/contextual/js/toolbar/views/AuralView.es6.js +++ b/core/modules/contextual/js/toolbar/views/AuralView.es6.js @@ -30,6 +30,7 @@ this.listenTo(this.model, 'change:isViewing', this.manageTabbing); $(document).on('keyup', _.bind(this.onKeypress, this)); + this.manageTabbing(); }, /** diff --git a/core/modules/contextual/js/toolbar/views/AuralView.js b/core/modules/contextual/js/toolbar/views/AuralView.js index 7bf7e4076529..d3af87bc8f28 100644 --- a/core/modules/contextual/js/toolbar/views/AuralView.js +++ b/core/modules/contextual/js/toolbar/views/AuralView.js @@ -16,6 +16,7 @@ this.listenTo(this.model, 'change:isViewing', this.manageTabbing); $(document).on('keyup', _.bind(this.onKeypress, this)); + this.manageTabbing(); }, render: function render() { this.$el.find('button').attr('aria-pressed', !this.model.get('isViewing')); diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php index dc978aad1dc1..a61ea8a3d788 100644 --- a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php +++ b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php @@ -44,27 +44,45 @@ protected function setUp() { } /** - * Tests that Drupal.announce messages appear. + * Tests enabling and disabling edit mode. */ - public function testAnnounceEditMode() { + public function testEditModeEnableDisalbe() { $web_assert = $this->assertSession(); - $this->drupalGet('user'); - - // After the page loaded we need to additionally wait until the settings - // tray Ajax activity is done. - $web_assert->assertWaitOnAjaxRequest(); - - // Enable edit mode. - $this->pressToolbarEditButton(); - $this->assertAnnounceEditMode(); - // Disable edit mode. - $this->pressToolbarEditButton(); - $this->assertAnnounceLeaveEditMode(); - // Enable edit mode again. - $this->pressToolbarEditButton(); - // Finally assert that the 'edit mode enabled' announcement is still correct - // after toggling the edit mode at least once. - $this->assertAnnounceEditMode(); + $page = $this->getSession()->getPage(); + // Get the page twice to ensure edit mode remains enabled after a new page + // request. + for ($page_get_count = 0; $page_get_count < 2; $page_get_count++) { + $this->drupalGet('user'); + $expected_restricted_tab_count = 1 + count($page->findAll('css', '[data-contextual-id]')); + + // After the page loaded we need to additionally wait until the settings + // tray Ajax activity is done. + $web_assert->assertWaitOnAjaxRequest(); + + if ($page_get_count == 0) { + $unrestricted_tab_count = $this->getTabbableElementsCount(); + $this->assertGreaterThan($expected_restricted_tab_count, $unrestricted_tab_count); + + // Enable edit mode. + // After the first page load the page will be in edit mode when loaded. + $this->pressToolbarEditButton(); + } + + $this->assertAnnounceEditMode(); + $this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount()); + + // Disable edit mode. + $this->pressToolbarEditButton(); + $this->assertAnnounceLeaveEditMode(); + $this->assertSame($unrestricted_tab_count, $this->getTabbableElementsCount()); + // Enable edit mode again. + $this->pressToolbarEditButton(); + // Finally assert that the 'edit mode enabled' announcement is still + // correct after toggling the edit mode at least once. + $this->assertAnnounceEditMode(); + $this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount()); + } + } /** @@ -100,4 +118,20 @@ protected function assertAnnounceLeaveEditMode() { $web_assert->elementNotContains('css', static::ANNOUNCE_SELECTOR, 'Tabbing is constrained to a set of'); } + /** + * Gets the number of elements that are tabbable. + * + * @return int + * The number of tabbable elements. + */ + protected function getTabbableElementsCount() { + // Mark all tabbable elements. + $this->getSession()->executeScript("jQuery(':tabbable').attr('data-marked', '');"); + // Count all marked elements. + $count = count($this->getSession()->getPage()->findAll('css', "[data-marked]")); + // Remove set attributes. + $this->getSession()->executeScript("jQuery('[data-marked]').removeAttr('data-marked');"); + return $count; + } + } From 354462c6f6fb7407d1ef319d49d9fba72e5e0e5d Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Thu, 4 Jan 2018 07:07:40 +1000 Subject: [PATCH 085/232] Issue #2932774 by harsha012, gaurav.kapoor, neelam.chaudhary, dpi: Update docs for getEmail, can return null --- core/lib/Drupal/Core/Session/AccountInterface.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/lib/Drupal/Core/Session/AccountInterface.php b/core/lib/Drupal/Core/Session/AccountInterface.php index 1cf82dcb63ca..ba3ed9378cef 100644 --- a/core/lib/Drupal/Core/Session/AccountInterface.php +++ b/core/lib/Drupal/Core/Session/AccountInterface.php @@ -148,8 +148,9 @@ public function getDisplayName(); /** * Returns the email address of this account. * - * @return string - * The email address. + * @return string|null + * The email address, or NULL if the account is anonymous or the user does + * not have an email address. */ public function getEmail(); From 72c76b73c5c9a7ae42f288e86191464fe4a6ee39 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Thu, 4 Jan 2018 07:14:47 +1000 Subject: [PATCH 086/232] Issue #2359389 by harsha012, hgoto, jeqq, fago, sureshcj, larowlan: Call to a member function filters() on a non-object in core/modules/text/text.module on line 83 --- core/modules/text/tests/src/Kernel/TextSummaryTest.php | 9 +++++++++ core/modules/text/text.module | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/modules/text/tests/src/Kernel/TextSummaryTest.php b/core/modules/text/tests/src/Kernel/TextSummaryTest.php index dab6ed5de51e..2eeaf2de0be6 100644 --- a/core/modules/text/tests/src/Kernel/TextSummaryTest.php +++ b/core/modules/text/tests/src/Kernel/TextSummaryTest.php @@ -207,6 +207,15 @@ public function testLength() { $this->assertTextSummary($text, "

\nHi\n

\n

\nfolks\n
\n!\n

", $format, $i++); } + /** + * Test text_summary() returns an empty string without any error when called + * with an invalid format. + */ + public function testInvalidFilterFormat() { + + $this->assertTextSummary($this->randomString(100), '', 'non_existent_format'); + } + /** * Calls text_summary() and asserts that the expected teaser is returned. */ diff --git a/core/modules/text/text.module b/core/modules/text/text.module index 1932e6164966..b88fdc1fd74c 100644 --- a/core/modules/text/text.module +++ b/core/modules/text/text.module @@ -81,11 +81,11 @@ function text_summary($text, $format = NULL, $size = NULL) { // Retrieve the filters of the specified text format, if any. if (isset($format)) { - $filters = FilterFormat::load($format)->filters(); + $filter_format = FilterFormat::load($format); // If the specified format does not exist, return nothing. $text is already // filtered text, but the remainder of this function will not be able to // ensure a sane and secure summary. - if (!$filters) { + if (!$filter_format || !($filters = $filter_format->filters())) { return ''; } } From cf805af249c160e5d8da632c72ee4d12719ff8f5 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Thu, 4 Jan 2018 07:29:24 +1000 Subject: [PATCH 087/232] Issue #2928699 by marcoscano, phenaproxima, amateescu, seanB, yoroy: Add an alter hook for the pre-configured field UI options and implement it in the Media module --- .../Core/Field/FieldTypePluginManager.php | 15 +++++++- .../Field/FieldTypePluginManagerInterface.php | 20 +++++++++++ .../PreconfiguredFieldUiOptionsInterface.php | 6 ++++ core/modules/field/field.api.php | 27 ++++++++++++++ .../modules/field_test/field_test.module | 11 ++++++ .../field_ui/src/Form/FieldStorageAddForm.php | 36 ++++++++++++++----- .../field_ui/src/Tests/ManageFieldsTest.php | 1 + core/modules/media/media.module | 19 ++++++++++ .../Functional/MediaFunctionalTestTrait.php | 1 + .../src/Functional/MediaUiFunctionalTest.php | 17 +++++++++ 10 files changed, 144 insertions(+), 9 deletions(-) diff --git a/core/lib/Drupal/Core/Field/FieldTypePluginManager.php b/core/lib/Drupal/Core/Field/FieldTypePluginManager.php index 905cdb4f1bb1..565fa4e6f749 100644 --- a/core/lib/Drupal/Core/Field/FieldTypePluginManager.php +++ b/core/lib/Drupal/Core/Field/FieldTypePluginManager.php @@ -135,7 +135,7 @@ public function getUiDefinitions() { // Add preconfigured definitions. foreach ($definitions as $id => $definition) { if (is_subclass_of($definition['class'], '\Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface')) { - foreach ($definition['class']::getPreconfiguredOptions() as $key => $option) { + foreach ($this->getPreconfiguredOptions($definition['id']) as $key => $option) { $definitions['field_ui:' . $id . ':' . $key] = [ 'label' => $option['label'], ] + $definition; @@ -150,6 +150,19 @@ public function getUiDefinitions() { return $definitions; } + /** + * {@inheritdoc} + */ + public function getPreconfiguredOptions($field_type) { + $options = []; + $class = $this->getPluginClass($field_type); + if (is_subclass_of($class, '\Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface')) { + $options = $class::getPreconfiguredOptions(); + $this->moduleHandler->alter('field_ui_preconfigured_options', $options, $field_type); + } + return $options; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Field/FieldTypePluginManagerInterface.php b/core/lib/Drupal/Core/Field/FieldTypePluginManagerInterface.php index 3a5c7b546cdb..d937aabd446d 100644 --- a/core/lib/Drupal/Core/Field/FieldTypePluginManagerInterface.php +++ b/core/lib/Drupal/Core/Field/FieldTypePluginManagerInterface.php @@ -84,6 +84,26 @@ public function getDefaultStorageSettings($type); */ public function getUiDefinitions(); + /** + * Returns preconfigured field options for a field type. + * + * This is a wrapper around + * \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface::getPreconfiguredOptions() + * allowing modules to alter the result of this method by implementing + * hook_field_ui_preconfigured_options_alter(). + * + * @param string $field_type + * The field type plugin ID. + * + * @return array + * A multi-dimensional array as returned from + * \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface::getPreconfiguredOptions(). + * + * @see \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface::getPreconfiguredOptions() + * @see hook_field_ui_preconfigured_options_alter() + */ + public function getPreconfiguredOptions($field_type); + /** * Returns the PHP class that implements the field type plugin. * diff --git a/core/lib/Drupal/Core/Field/PreconfiguredFieldUiOptionsInterface.php b/core/lib/Drupal/Core/Field/PreconfiguredFieldUiOptionsInterface.php index 9130e8c017cc..684168aacb8a 100644 --- a/core/lib/Drupal/Core/Field/PreconfiguredFieldUiOptionsInterface.php +++ b/core/lib/Drupal/Core/Field/PreconfiguredFieldUiOptionsInterface.php @@ -16,6 +16,11 @@ interface PreconfiguredFieldUiOptionsInterface { /** * Returns preconfigured field options for a field type. * + * Note that if you want to give modules an opportunity to alter the result + * of this method, you should call + * \Drupal\Core\Field\FieldTypePluginManagerInterface::getPreconfiguredOptions() + * instead. + * * @return mixed[][] * A multi-dimensional array with string keys and the following structure: * - label: The label to show in the field type selection list. @@ -35,6 +40,7 @@ interface PreconfiguredFieldUiOptionsInterface { * @see \Drupal\field\Entity\FieldStorageConfig * @see \Drupal\field\Entity\FieldConfig * @see \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent() + * @see \Drupal\Core\Field\FieldTypePluginManagerInterface::getPreconfiguredOptions() */ public static function getPreconfiguredOptions(); diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 42bdd1e7125b..130f92318036 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -58,6 +58,33 @@ function hook_field_info_alter(&$info) { } } +/** + * Perform alterations on preconfigured field options. + * + * @param array $options + * Array of options as returned from + * \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface::getPreconfiguredOptions(). + * @param string $field_type + * The field type plugin ID. + * + * @see \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface::getPreconfiguredOptions() + */ +function hook_field_ui_preconfigured_options_alter(array &$options, $field_type) { + // If the field is not an "entity_reference"-based field, bail out. + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $class = $field_type_manager->getPluginClass($field_type); + if (!is_a($class, 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', TRUE)) { + return; + } + + // Set the default formatter for media in entity reference fields to be the + // "Rendered entity" formatter. + if (!empty($options['media'])) { + $options['media']['entity_view_display']['type'] = 'entity_reference_entity_view'; + } +} + /** * Forbid a field storage update from occurring. * 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 cc1f9fa9054a..9121e942eae6 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -171,3 +171,14 @@ function field_test_entity_bundle_field_info_alter(&$fields, EntityTypeInterface ]); } } + +/** + * Implements hook_field_ui_preconfigured_options_alter(). + */ +function field_test_field_ui_preconfigured_options_alter(array &$options, $field_type) { + if ($field_type === 'test_field_with_preconfigured_options') { + $options['custom_options']['entity_view_display']['settings'] = [ + 'test_formatter_setting_multiple' => 'altered dummy test string', + ]; + } +} diff --git a/core/modules/field_ui/src/Form/FieldStorageAddForm.php b/core/modules/field_ui/src/Form/FieldStorageAddForm.php index 97e3ce7d102e..584cc9684674 100644 --- a/core/modules/field_ui/src/Form/FieldStorageAddForm.php +++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php @@ -312,14 +312,16 @@ public function submitForm(array &$form, FormStateInterface $form_state) { 'translatable' => FALSE, ]; $widget_id = $formatter_id = NULL; + $widget_settings = $formatter_settings = []; // Check if we're dealing with a preconfigured field. if (strpos($field_storage_values['type'], 'field_ui:') !== FALSE) { list(, $field_type, $option_key) = explode(':', $field_storage_values['type'], 3); $field_storage_values['type'] = $field_type; - $field_type_class = $this->fieldTypePluginManager->getDefinition($field_type)['class']; - $field_options = $field_type_class::getPreconfiguredOptions()[$option_key]; + $field_definition = $this->fieldTypePluginManager->getDefinition($field_type); + $options = $this->fieldTypePluginManager->getPreconfiguredOptions($field_definition['id']); + $field_options = $options[$option_key]; // Merge in preconfigured field storage options. if (isset($field_options['field_storage_config'])) { @@ -340,7 +342,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } $widget_id = isset($field_options['entity_form_display']['type']) ? $field_options['entity_form_display']['type'] : NULL; + $widget_settings = isset($field_options['entity_form_display']['settings']) ? $field_options['entity_form_display']['settings'] : []; $formatter_id = isset($field_options['entity_view_display']['type']) ? $field_options['entity_view_display']['type'] : NULL; + $formatter_settings = isset($field_options['entity_view_display']['settings']) ? $field_options['entity_view_display']['settings'] : []; } // Create the field storage and field. @@ -349,8 +353,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $field = $this->entityManager->getStorage('field_config')->create($field_values); $field->save(); - $this->configureEntityFormDisplay($values['field_name'], $widget_id); - $this->configureEntityViewDisplay($values['field_name'], $formatter_id); + $this->configureEntityFormDisplay($values['field_name'], $widget_id, $widget_settings); + $this->configureEntityViewDisplay($values['field_name'], $formatter_id, $formatter_settings); // Always show the field settings step, as the cardinality needs to be // configured for new fields. @@ -418,12 +422,20 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * The field name. * @param string|null $widget_id * (optional) The plugin ID of the widget. Defaults to NULL. + * @param array $widget_settings + * (optional) An array of widget settings. Defaults to an empty array. */ - protected function configureEntityFormDisplay($field_name, $widget_id = NULL) { + protected function configureEntityFormDisplay($field_name, $widget_id = NULL, array $widget_settings = []) { + $options = []; + if ($widget_id) { + $options['type'] = $widget_id; + if (!empty($widget_settings)) { + $options['settings'] = $widget_settings; + } + } // Make sure the field is displayed in the 'default' form mode (using // default widget and settings). It stays hidden for other form modes // until it is explicitly configured. - $options = $widget_id ? ['type' => $widget_id] : []; entity_get_form_display($this->entityTypeId, $this->bundle, 'default') ->setComponent($field_name, $options) ->save(); @@ -436,12 +448,20 @@ protected function configureEntityFormDisplay($field_name, $widget_id = NULL) { * The field name. * @param string|null $formatter_id * (optional) The plugin ID of the formatter. Defaults to NULL. + * @param array $formatter_settings + * (optional) An array of formatter settings. Defaults to an empty array. */ - protected function configureEntityViewDisplay($field_name, $formatter_id = NULL) { + protected function configureEntityViewDisplay($field_name, $formatter_id = NULL, array $formatter_settings = []) { + $options = []; + if ($formatter_id) { + $options['type'] = $formatter_id; + if (!empty($formatter_settings)) { + $options['settings'] = $formatter_settings; + } + } // Make sure the field is displayed in the 'default' view mode (using // default formatter and settings). It stays hidden for other view // modes until it is explicitly configured. - $options = $formatter_id ? ['type' => $formatter_id] : []; entity_get_display($this->entityTypeId, $this->bundle, 'default') ->setComponent($field_name, $options) ->save(); diff --git a/core/modules/field_ui/src/Tests/ManageFieldsTest.php b/core/modules/field_ui/src/Tests/ManageFieldsTest.php index d08ab03ba4cd..112e3923c497 100644 --- a/core/modules/field_ui/src/Tests/ManageFieldsTest.php +++ b/core/modules/field_ui/src/Tests/ManageFieldsTest.php @@ -750,6 +750,7 @@ public function testPreconfiguredFields() { $this->assertEqual($form_display->getComponent('field_test_custom_options')['type'], 'test_field_widget_multiple'); $view_display = entity_get_display('node', 'article', 'default'); $this->assertEqual($view_display->getComponent('field_test_custom_options')['type'], 'field_test_multiple'); + $this->assertEqual($view_display->getComponent('field_test_custom_options')['settings']['test_formatter_setting_multiple'], 'altered dummy test string'); } /** diff --git a/core/modules/media/media.module b/core/modules/media/media.module index dc2d898e3c8e..5df059c284b8 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -96,3 +96,22 @@ function template_preprocess_media(array &$variables) { $variables['content'][$key] = $variables['elements'][$key]; } } + +/** + * Implements hook_field_ui_preconfigured_options_alter(). + */ +function media_field_ui_preconfigured_options_alter(array &$options, $field_type) { + // If the field is not an "entity_reference"-based field, bail out. + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $class = $field_type_manager->getPluginClass($field_type); + if (!is_a($class, 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', TRUE)) { + return; + } + + // Set the default formatter for media in entity reference fields to be the + // "Rendered entity" formatter. + if (!empty($options['media'])) { + $options['media']['entity_view_display']['type'] = 'entity_reference_entity_view'; + } +} diff --git a/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php index d56e1565f758..4fa7b54280f7 100644 --- a/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php @@ -32,6 +32,7 @@ trait MediaFunctionalTestTrait { 'administer content types', 'administer node fields', 'administer node form display', + 'administer node display', 'bypass node access', ]; diff --git a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php index 4aa3f001f6d4..8a0a82fffb0f 100644 --- a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php +++ b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php @@ -175,4 +175,21 @@ public function testMediaWithMultipleMediaTypes() { $assert_session->pageTextContains($second_media_item->getName()); } + /** + * Test that media in ER fields use the Rendered Entity formatter by default. + */ + public function testRenderedEntityReferencedMedia() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + $this->drupalGet('/admin/structure/types/manage/page/fields/add-field'); + $page->selectFieldOption('new_storage_type', 'field_ui:entity_reference:media'); + $page->fillField('label', 'Foo field'); + $page->fillField('field_name', 'foo_field'); + $page->pressButton('Save and continue'); + $this->drupalGet('/admin/structure/types/manage/page/display'); + $assert_session->fieldValueEquals('fields[field_foo_field][type]', 'entity_reference_entity_view'); + } + } From 9d552cad6cfbe8383b96b74ffc6176503a7b0c2e Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Thu, 4 Jan 2018 07:37:32 +1000 Subject: [PATCH 088/232] Issue #2926309 by vaplas, alexpott, mpdonadio, tacituseu, Mixologic: Random fail due to APCu not being able to allocate memory --- core/lib/Drupal/Core/Site/Settings.php | 6 ++++-- .../Core/Test/FunctionalTestSetupTrait.php | 16 ++++++++++++++++ .../src/Functional/Module/ClassLoaderTest.php | 5 +++++ .../Drupal/Tests/Core/Site/SettingsTest.php | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/lib/Drupal/Core/Site/Settings.php b/core/lib/Drupal/Core/Site/Settings.php index a1a6c38893ea..bdd2609a7c02 100644 --- a/core/lib/Drupal/Core/Site/Settings.php +++ b/core/lib/Drupal/Core/Site/Settings.php @@ -156,8 +156,8 @@ public static function getHashSalt() { * cache. By default, this method will produce a unique prefix per site using * the hash salt. If the setting 'apcu_ensure_unique_prefix' is set to FALSE * then if the caller does not provide a $site_path only the Drupal root will - * be used. This allows WebTestBase to use the same prefix ensuring that the - * number of APCu items created during a full test run is kept to a minimum. + * be used. This allows tests to use the same prefix ensuring that the number + * of APCu items created during a full test run is kept to a minimum. * Additionally, if a multi site implementation does not use site specific * module directories setting apcu_ensure_unique_prefix would allow the sites * to share APCu cache items. @@ -168,6 +168,8 @@ public static function getHashSalt() { * * @return string * The prefix for APCu user cache keys. + * + * @see https://www.drupal.org/project/drupal/issues/2926309 */ public static function getApcuPrefix($identifier, $root, $site_path = '') { if (static::get('apcu_ensure_unique_prefix', TRUE)) { diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php index f9f97ecaa8c3..3165815b1755 100644 --- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php @@ -41,6 +41,18 @@ trait FunctionalTestSetupTrait { */ protected $configDirectories = []; + /** + * The flag to set 'apcu_ensure_unique_prefix' setting. + * + * Wide use of a unique prefix can lead to problems with memory, if tests are + * run with a concurrency higher than 1. Therefore, FALSE by default. + * + * @var bool + * + * @see \Drupal\Core\Site\Settings::getApcuPrefix(). + */ + protected $apcuEnsureUniquePrefix = FALSE; + /** * Prepares site settings and services before installation. */ @@ -83,6 +95,10 @@ protected function prepareSettings() { 'value' => $this->originalProfile, 'required' => TRUE, ]; + $settings['settings']['apcu_ensure_unique_prefix'] = (object) [ + 'value' => $this->apcuEnsureUniquePrefix, + 'required' => TRUE, + ]; $this->writeSettings($settings); // Allow for test-specific overrides. $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php'; diff --git a/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php b/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php index 93c0bf3f422a..335a03a329b0 100644 --- a/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php +++ b/core/modules/system/tests/src/Functional/Module/ClassLoaderTest.php @@ -18,6 +18,11 @@ class ClassLoaderTest extends BrowserTestBase { */ protected $expected = 'Drupal\\module_autoload_test\\SomeClass::testMethod() was invoked.'; + /** + * {@inheritdoc} + */ + protected $apcuEnsureUniquePrefix = TRUE; + /** * Tests that module-provided classes can be loaded when a module is enabled. * diff --git a/core/tests/Drupal/Tests/Core/Site/SettingsTest.php b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php index 5e4f1d084c15..f72e4ce9f0e7 100644 --- a/core/tests/Drupal/Tests/Core/Site/SettingsTest.php +++ b/core/tests/Drupal/Tests/Core/Site/SettingsTest.php @@ -117,7 +117,7 @@ public function testSerialize() { * @covers ::getApcuPrefix */ public function testGetApcuPrefix() { - $settings = new Settings(['hash_salt' => 123]); + $settings = new Settings(['hash_salt' => 123, 'apcu_ensure_unique_prefix' => TRUE]); $this->assertNotEquals($settings::getApcuPrefix('cache_test', '/test/a'), $settings::getApcuPrefix('cache_test', '/test/b')); $settings = new Settings(['hash_salt' => 123, 'apcu_ensure_unique_prefix' => FALSE]); From f1e33ca3d84ac2251aed144c2587ed9eb2e578fb Mon Sep 17 00:00:00 2001 From: webchick Date: Wed, 3 Jan 2018 15:25:46 -0800 Subject: [PATCH 089/232] Issue #2925064 by drpal, dawehner, droplet, xjm, webchick, justafish: [1/2] JS codestyle: no-restricted-syntax --- core/.eslintrc.passing.json | 1 - core/misc/ajax.es6.js | 12 +- core/misc/ajax.js | 18 +-- core/misc/drupal.es6.js | 55 ++++---- core/misc/drupal.js | 45 +++---- core/misc/states.es6.js | 38 +++--- core/misc/states.js | 45 +++---- core/misc/tabledrag.es6.js | 118 ++++++++---------- core/misc/tabledrag.js | 102 +++++++-------- core/modules/ckeditor/js/ckeditor.es6.js | 8 +- core/modules/ckeditor/js/ckeditor.js | 8 +- .../js/plugins/drupallink/plugin.es6.js | 40 +++--- .../ckeditor/js/plugins/drupallink/plugin.js | 36 +++--- .../ckeditor/js/views/ControllerView.es6.js | 46 +++---- .../ckeditor/js/views/ControllerView.js | 46 +++---- core/modules/color/color.es6.js | 48 ++++--- core/modules/color/color.js | 34 +++-- core/modules/color/preview.es6.js | 36 +++--- core/modules/color/preview.js | 34 +++-- .../comment/js/node-new-comments-link.es6.js | 6 +- .../comment/js/node-new-comments-link.js | 6 +- .../content_translation.admin.es6.js | 41 +++--- .../content_translation.admin.js | 37 +++--- core/modules/editor/js/editor.admin.es6.js | 17 ++- core/modules/editor/js/editor.admin.js | 13 +- core/modules/field_ui/field_ui.es6.js | 11 +- core/modules/field_ui/field_ui.js | 11 +- .../filter/filter.filter_html.admin.es6.js | 70 +++++------ .../filter/filter.filter_html.admin.js | 53 ++++---- core/modules/history/js/history.es6.js | 8 +- core/modules/history/js/history.js | 8 +- core/modules/menu_ui/menu_ui.admin.es6.js | 14 +-- core/modules/menu_ui/menu_ui.admin.js | 10 +- core/modules/quickedit/js/theme.es6.js | 8 +- core/modules/quickedit/js/theme.js | 15 ++- core/modules/quickedit/js/util.es6.js | 6 +- core/modules/quickedit/js/util.js | 10 +- core/modules/system/js/system.es6.js | 6 +- core/modules/system/js/system.js | 6 +- core/modules/toolbar/js/toolbar.es6.js | 24 ++-- core/modules/toolbar/js/toolbar.js | 16 ++- .../toolbar/js/views/MenuVisualView.es6.js | 14 +-- .../toolbar/js/views/MenuVisualView.js | 10 +- .../modules/tracker/js/tracker-history.es6.js | 6 +- core/modules/tracker/js/tracker-history.js | 6 +- core/modules/views/js/ajax_view.es6.js | 8 +- core/modules/views/js/ajax_view.js | 8 +- 47 files changed, 537 insertions(+), 681 deletions(-) diff --git a/core/.eslintrc.passing.json b/core/.eslintrc.passing.json index 7ed6508febdb..3f42d326c6c0 100644 --- a/core/.eslintrc.passing.json +++ b/core/.eslintrc.passing.json @@ -3,7 +3,6 @@ "rules": { "no-use-before-define": "off", "no-shadow": "off", - "no-restricted-syntax": "off", "no-new": "off", "no-continue": "off", "new-cap": "off", diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js index fa4e3a55f042..c3633b463663 100644 --- a/core/misc/ajax.es6.js +++ b/core/misc/ajax.es6.js @@ -40,11 +40,7 @@ } // Load all Ajax behaviors specified in the settings. - for (const base in settings.ajax) { - if (settings.ajax.hasOwnProperty(base)) { - loadAjaxBehavior(base); - } - } + Object.keys(settings.ajax || {}).forEach(base => loadAjaxBehavior(base)); Drupal.ajax.bindAjaxLinks(document.body); @@ -877,14 +873,14 @@ // Track if any command is altering the focus so we can avoid changing the // focus set by the Ajax command. let focusChanged = false; - for (const i in response) { - if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) { + Object.keys(response || {}).forEach((i) => { + if (response[i].command && this.commands[response[i].command]) { this.commands[response[i].command](this, response[i], status); if (response[i].command === 'invoke' && response[i].method === 'focus') { focusChanged = true; } } - } + }); // If the focus hasn't be changed by the ajax commands, try to refocus the // triggering element or one of its parents if that element does not exist diff --git a/core/misc/ajax.js b/core/misc/ajax.js index 8ec175fee17c..58759ac7a09b 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -21,11 +21,9 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr }); } - for (var base in settings.ajax) { - if (settings.ajax.hasOwnProperty(base)) { - loadAjaxBehavior(base); - } - } + Object.keys(settings.ajax || {}).forEach(function (base) { + return loadAjaxBehavior(base); + }); Drupal.ajax.bindAjaxLinks(document.body); @@ -397,6 +395,8 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr }; Drupal.Ajax.prototype.success = function (response, status) { + var _this = this; + if (this.progress.element) { $(this.progress.element).remove(); } @@ -408,14 +408,14 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr var elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray(); var focusChanged = false; - for (var i in response) { - if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) { - this.commands[response[i].command](this, response[i], status); + Object.keys(response || {}).forEach(function (i) { + if (response[i].command && _this.commands[response[i].command]) { + _this.commands[response[i].command](_this, response[i], status); if (response[i].command === 'invoke' && response[i].method === 'focus') { focusChanged = true; } } - } + }); if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) { var target = false; diff --git a/core/misc/drupal.es6.js b/core/misc/drupal.es6.js index 43a78f9f71f9..48f0510346d0 100644 --- a/core/misc/drupal.es6.js +++ b/core/misc/drupal.es6.js @@ -152,8 +152,8 @@ window.Drupal = { behaviors: {}, locale: {} }; settings = settings || drupalSettings; const behaviors = Drupal.behaviors; // Execute all of them. - for (const i in behaviors) { - if (behaviors.hasOwnProperty(i) && typeof behaviors[i].attach === 'function') { + Object.keys(behaviors || {}).forEach((i) => { + if (typeof behaviors[i].attach === 'function') { // Don't stop the execution of behaviors in case of an error. try { behaviors[i].attach(context, settings); @@ -162,7 +162,7 @@ window.Drupal = { behaviors: {}, locale: {} }; Drupal.throwError(e); } } - } + }); }; /** @@ -212,8 +212,8 @@ window.Drupal = { behaviors: {}, locale: {} }; trigger = trigger || 'unload'; const behaviors = Drupal.behaviors; // Execute all of them. - for (const i in behaviors) { - if (behaviors.hasOwnProperty(i) && typeof behaviors[i].detach === 'function') { + Object.keys(behaviors || {}).forEach((i) => { + if (typeof behaviors[i].detach === 'function') { // Don't stop the execution of behaviors in case of an error. try { behaviors[i].detach(context, settings, trigger); @@ -222,7 +222,7 @@ window.Drupal = { behaviors: {}, locale: {} }; Drupal.throwError(e); } } - } + }); }; /** @@ -269,26 +269,24 @@ window.Drupal = { behaviors: {}, locale: {} }; // Keep args intact. const processedArgs = {}; // Transform arguments before inserting them. - for (const key in args) { - if (args.hasOwnProperty(key)) { - switch (key.charAt(0)) { - // Escaped only. - case '@': - processedArgs[key] = Drupal.checkPlain(args[key]); - break; - - // Pass-through. - case '!': - processedArgs[key] = args[key]; - break; - - // Escaped and placeholder. - default: - processedArgs[key] = Drupal.theme('placeholder', args[key]); - break; - } + Object.keys(args || {}).forEach((key) => { + switch (key.charAt(0)) { + // Escaped only. + case '@': + processedArgs[key] = Drupal.checkPlain(args[key]); + break; + + // Pass-through. + case '!': + processedArgs[key] = args[key]; + break; + + // Escaped and placeholder. + default: + processedArgs[key] = Drupal.theme('placeholder', args[key]); + break; } - } + }); return Drupal.stringReplace(str, processedArgs, null); }; @@ -316,12 +314,7 @@ window.Drupal = { behaviors: {}, locale: {} }; // If the array of keys is not passed then collect the keys from the args. if (!Array.isArray(keys)) { - keys = []; - for (const k in args) { - if (args.hasOwnProperty(k)) { - keys.push(k); - } - } + keys = Object.keys(args || {}); // Order the keys by the character length. The shortest one is the first. keys.sort((a, b) => a.length - b.length); diff --git a/core/misc/drupal.js b/core/misc/drupal.js index 0ede48da607b..e080caae65af 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -19,15 +19,15 @@ window.Drupal = { behaviors: {}, locale: {} }; settings = settings || drupalSettings; var behaviors = Drupal.behaviors; - for (var i in behaviors) { - if (behaviors.hasOwnProperty(i) && typeof behaviors[i].attach === 'function') { + Object.keys(behaviors || {}).forEach(function (i) { + if (typeof behaviors[i].attach === 'function') { try { behaviors[i].attach(context, settings); } catch (e) { Drupal.throwError(e); } } - } + }); }; Drupal.detachBehaviors = function (context, settings, trigger) { @@ -36,15 +36,15 @@ window.Drupal = { behaviors: {}, locale: {} }; trigger = trigger || 'unload'; var behaviors = Drupal.behaviors; - for (var i in behaviors) { - if (behaviors.hasOwnProperty(i) && typeof behaviors[i].detach === 'function') { + Object.keys(behaviors || {}).forEach(function (i) { + if (typeof behaviors[i].detach === 'function') { try { behaviors[i].detach(context, settings, trigger); } catch (e) { Drupal.throwError(e); } } - } + }); }; Drupal.checkPlain = function (str) { @@ -55,23 +55,21 @@ window.Drupal = { behaviors: {}, locale: {} }; Drupal.formatString = function (str, args) { var processedArgs = {}; - for (var key in args) { - if (args.hasOwnProperty(key)) { - switch (key.charAt(0)) { - case '@': - processedArgs[key] = Drupal.checkPlain(args[key]); - break; + Object.keys(args || {}).forEach(function (key) { + switch (key.charAt(0)) { + case '@': + processedArgs[key] = Drupal.checkPlain(args[key]); + break; - case '!': - processedArgs[key] = args[key]; - break; + case '!': + processedArgs[key] = args[key]; + break; - default: - processedArgs[key] = Drupal.theme('placeholder', args[key]); - break; - } + default: + processedArgs[key] = Drupal.theme('placeholder', args[key]); + break; } - } + }); return Drupal.stringReplace(str, processedArgs, null); }; @@ -82,12 +80,7 @@ window.Drupal = { behaviors: {}, locale: {} }; } if (!Array.isArray(keys)) { - keys = []; - for (var k in args) { - if (args.hasOwnProperty(k)) { - keys.push(k); - } - } + keys = Object.keys(args || {}); keys.sort(function (a, b) { return a.length - b.length; diff --git a/core/misc/states.es6.js b/core/misc/states.es6.js index ec7a4e7e3a84..b47b90de57e4 100644 --- a/core/misc/states.es6.js +++ b/core/misc/states.es6.js @@ -33,20 +33,16 @@ Drupal.behaviors.states = { attach(context, settings) { const $states = $(context).find('[data-drupal-states]'); - let config; - let state; const il = $states.length; for (let i = 0; i < il; i++) { - config = JSON.parse($states[i].getAttribute('data-drupal-states')); - for (state in config) { - if (config.hasOwnProperty(state)) { - new states.Dependent({ - element: $($states[i]), - state: states.State.sanitize(state), - constraints: config[state], - }); - } - } + const config = JSON.parse($states[i].getAttribute('data-drupal-states')); + Object.keys(config || {}).forEach((state) => { + new states.Dependent({ + element: $($states[i]), + state: states.State.sanitize(state), + constraints: config[state], + }); + }); } // Execute all postponed functions now. @@ -76,11 +72,9 @@ $.extend(this, { values: {}, oldValue: null }, args); this.dependees = this.getDependees(); - for (const selector in this.dependees) { - if (this.dependees.hasOwnProperty(selector)) { - this.initializeDependee(selector, this.dependees[selector]); - } - } + Object.keys(this.dependees || {}).forEach((selector) => { + this.initializeDependee(selector, this.dependees[selector]); + }); }; /** @@ -136,6 +130,7 @@ // Cache for the states of this dependee. this.values[selector] = {}; + // eslint-disable-next-line no-restricted-syntax for (const i in dependeeStates) { if (dependeeStates.hasOwnProperty(i)) { state = dependeeStates[i]; @@ -267,6 +262,7 @@ // bogus, we don't want to end up with an infinite loop. else if ($.isPlainObject(constraints)) { // This constraint is an object (AND). + // eslint-disable-next-line no-restricted-syntax for (const n in constraints) { if (constraints.hasOwnProperty(n)) { result = ternary(result, this.checkConstraints(constraints[n], selector, n)); @@ -391,11 +387,9 @@ trigger.call(window, this.element); } else { - for (const event in trigger) { - if (trigger.hasOwnProperty(event)) { - this.defaultTrigger(event, trigger[event]); - } - } + Object.keys(trigger || {}).forEach((event) => { + this.defaultTrigger(event, trigger[event]); + }); } // Mark this trigger as initialized for this element. diff --git a/core/misc/states.js b/core/misc/states.js index 2b9d3b886a67..4fd2052a4398 100644 --- a/core/misc/states.js +++ b/core/misc/states.js @@ -15,20 +15,21 @@ Drupal.behaviors.states = { attach: function attach(context, settings) { var $states = $(context).find('[data-drupal-states]'); - var config = void 0; - var state = void 0; var il = $states.length; + + var _loop = function _loop(i) { + var config = JSON.parse($states[i].getAttribute('data-drupal-states')); + Object.keys(config || {}).forEach(function (state) { + new states.Dependent({ + element: $($states[i]), + state: states.State.sanitize(state), + constraints: config[state] + }); + }); + }; + for (var i = 0; i < il; i++) { - config = JSON.parse($states[i].getAttribute('data-drupal-states')); - for (state in config) { - if (config.hasOwnProperty(state)) { - new states.Dependent({ - element: $($states[i]), - state: states.State.sanitize(state), - constraints: config[state] - }); - } - } + _loop(i); } while (states.postponed.length) { @@ -38,14 +39,14 @@ }; states.Dependent = function (args) { + var _this = this; + $.extend(this, { values: {}, oldValue: null }, args); this.dependees = this.getDependees(); - for (var selector in this.dependees) { - if (this.dependees.hasOwnProperty(selector)) { - this.initializeDependee(selector, this.dependees[selector]); - } - } + Object.keys(this.dependees || {}).forEach(function (selector) { + _this.initializeDependee(selector, _this.dependees[selector]); + }); }; states.Dependent.comparisons = { @@ -187,16 +188,16 @@ states.Trigger.prototype = { initialize: function initialize() { + var _this2 = this; + var trigger = states.Trigger.states[this.state]; if (typeof trigger === 'function') { trigger.call(window, this.element); } else { - for (var event in trigger) { - if (trigger.hasOwnProperty(event)) { - this.defaultTrigger(event, trigger[event]); - } - } + Object.keys(trigger || {}).forEach(function (event) { + _this2.defaultTrigger(event, trigger[event]); + }); } this.element.data('trigger:' + this.state, true); diff --git a/core/misc/tabledrag.es6.js b/core/misc/tabledrag.es6.js index abbe193862b6..0a69deb2ca78 100644 --- a/core/misc/tabledrag.es6.js +++ b/core/misc/tabledrag.es6.js @@ -41,11 +41,9 @@ } } - for (const base in settings.tableDrag) { - if (settings.tableDrag.hasOwnProperty(base)) { - initTableDrag($(context).find(`#${base}`).once('tabledrag'), base); - } - } + Object.keys(settings.tableDrag || {}).forEach((base) => { + initTableDrag($(context).find(`#${base}`).once('tabledrag'), base); + }); }, }; @@ -172,20 +170,16 @@ * @type {bool} */ this.indentEnabled = false; - for (const group in tableSettings) { - if (tableSettings.hasOwnProperty(group)) { - for (const n in tableSettings[group]) { - if (tableSettings[group].hasOwnProperty(n)) { - if (tableSettings[group][n].relationship === 'parent') { - this.indentEnabled = true; - } - if (tableSettings[group][n].limit > 0) { - this.maxDepth = tableSettings[group][n].limit; - } - } + Object.keys(tableSettings || {}).forEach((group) => { + Object.keys(tableSettings[group] || {}).forEach((n) => { + if (tableSettings[group][n].relationship === 'parent') { + this.indentEnabled = true; } - } - } + if (tableSettings[group][n].limit > 0) { + this.maxDepth = tableSettings[group][n].limit; + } + }); + }); if (this.indentEnabled) { /** * Total width of indents, set in makeDraggable. @@ -264,30 +258,29 @@ let hidden; let cell; let columnIndex; - for (const group in this.tableSettings) { - if (this.tableSettings.hasOwnProperty(group)) { - // Find the first field in this group. - for (const d in this.tableSettings[group]) { - if (this.tableSettings[group].hasOwnProperty(d)) { - const field = $table.find(`.${this.tableSettings[group][d].target}`).eq(0); - if (field.length && this.tableSettings[group][d].hidden) { - hidden = this.tableSettings[group][d].hidden; - cell = field.closest('td'); - break; - } + Object.keys(this.tableSettings || {}).forEach((group) => { + // Find the first field in this group. + // eslint-disable-next-line no-restricted-syntax + for (const d in this.tableSettings[group]) { + if (this.tableSettings[group].hasOwnProperty(d)) { + const field = $table.find(`.${this.tableSettings[group][d].target}`).eq(0); + if (field.length && this.tableSettings[group][d].hidden) { + hidden = this.tableSettings[group][d].hidden; + cell = field.closest('td'); + break; } } + } - // Mark the column containing this field so it can be hidden. - if (hidden && cell[0]) { - // Add 1 to our indexes. The nth-child selector is 1 based, not 0 - // based. Match immediate children of the parent element to allow - // nesting. - columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1; - $table.find('> thead > tr, > tbody > tr, > tr').each(this.addColspanClass(columnIndex)); - } + // Mark the column containing this field so it can be hidden. + if (hidden && cell[0]) { + // Add 1 to our indexes. The nth-child selector is 1 based, not 0 + // based. Match immediate children of the parent element to allow + // nesting. + columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1; + $table.find('> thead > tr, > tbody > tr, > tr').each(this.addColspanClass(columnIndex)); } - } + }); this.displayColumns(showWeight); }; @@ -419,12 +412,14 @@ Drupal.tableDrag.prototype.rowSettings = function (group, row) { const field = $(row).find(`.${group}`); const tableSettingsGroup = this.tableSettings[group]; + // eslint-disable-next-line no-restricted-syntax for (const delta in tableSettingsGroup) { if (tableSettingsGroup.hasOwnProperty(delta)) { const targetClass = tableSettingsGroup[delta].target; if (field.is(`.${targetClass}`)) { // Return a copy of the row settings. const rowSettings = {}; + // eslint-disable-next-line no-restricted-syntax for (const n in tableSettingsGroup[delta]) { if (tableSettingsGroup[delta].hasOwnProperty(n)) { rowSettings[n] = tableSettingsGroup[delta][n]; @@ -774,18 +769,14 @@ // If a setting exists for affecting the entire group, update all the // fields in the entire dragged group. - for (const group in self.tableSettings) { - if (self.tableSettings.hasOwnProperty(group)) { - const rowSettings = self.rowSettings(group, droppedRow); - if (rowSettings.relationship === 'group') { - for (const n in self.rowObject.children) { - if (self.rowObject.children.hasOwnProperty(n)) { - self.updateField(self.rowObject.children[n], group); - } - } - } + Object.keys(self.tableSettings || {}).forEach((group) => { + const rowSettings = self.rowSettings(group, droppedRow); + if (rowSettings.relationship === 'group') { + Object.keys(self.rowObject.children || {}).forEach((n) => { + self.updateField(self.rowObject.children[n], group); + }); } - } + }); self.rowObject.markChanged(); if (self.changed === false) { @@ -888,6 +879,7 @@ if ((y > (rowY - rowHeight)) && (y < (rowY + rowHeight))) { if (this.indentEnabled) { // Check that this row is not a child of the row being dragged. + // eslint-disable-next-line no-restricted-syntax for (n in this.rowObject.group) { if (this.rowObject.group[n] === row) { return null; @@ -924,13 +916,11 @@ * DOM object for the row that was just dropped. */ Drupal.tableDrag.prototype.updateFields = function (changedRow) { - for (const group in this.tableSettings) { - if (this.tableSettings.hasOwnProperty(group)) { - // Each group may have a different setting for relationship, so we find - // the source rows for each separately. - this.updateField(changedRow, group); - } - } + Object.keys(this.tableSettings || {}).forEach((group) => { + // Each group may have a different setting for relationship, so we find + // the source rows for each separately. + this.updateField(changedRow, group); + }); }; /** @@ -1486,15 +1476,13 @@ * Remove indentation helper classes from the current row group. */ Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () { - for (const n in this.children) { - if (this.children.hasOwnProperty(n)) { - $(this.children[n]).find('.js-indentation') - .removeClass('tree-child') - .removeClass('tree-child-first') - .removeClass('tree-child-last') - .removeClass('tree-child-horizontal'); - } - } + Object.keys(this.children || {}).forEach((n) => { + $(this.children[n]).find('.js-indentation') + .removeClass('tree-child') + .removeClass('tree-child-first') + .removeClass('tree-child-last') + .removeClass('tree-child-horizontal'); + }); }; /** diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index 859b87e013a2..06dccfab060a 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -16,15 +16,15 @@ } } - for (var base in settings.tableDrag) { - if (settings.tableDrag.hasOwnProperty(base)) { - initTableDrag($(context).find('#' + base).once('tabledrag'), base); - } - } + Object.keys(settings.tableDrag || {}).forEach(function (base) { + initTableDrag($(context).find('#' + base).once('tabledrag'), base); + }); } }; Drupal.tableDrag = function (table, tableSettings) { + var _this = this; + var self = this; var $table = $(table); @@ -59,20 +59,16 @@ this.windowHeight = 0; this.indentEnabled = false; - for (var group in tableSettings) { - if (tableSettings.hasOwnProperty(group)) { - for (var n in tableSettings[group]) { - if (tableSettings[group].hasOwnProperty(n)) { - if (tableSettings[group][n].relationship === 'parent') { - this.indentEnabled = true; - } - if (tableSettings[group][n].limit > 0) { - this.maxDepth = tableSettings[group][n].limit; - } - } + Object.keys(tableSettings || {}).forEach(function (group) { + Object.keys(tableSettings[group] || {}).forEach(function (n) { + if (tableSettings[group][n].relationship === 'parent') { + _this.indentEnabled = true; } - } - } + if (tableSettings[group][n].limit > 0) { + _this.maxDepth = tableSettings[group][n].limit; + } + }); + }); if (this.indentEnabled) { this.indentCount = 1; @@ -118,29 +114,29 @@ }; Drupal.tableDrag.prototype.initColumns = function () { + var _this2 = this; + var $table = this.$table; var hidden = void 0; var cell = void 0; var columnIndex = void 0; - for (var group in this.tableSettings) { - if (this.tableSettings.hasOwnProperty(group)) { - for (var d in this.tableSettings[group]) { - if (this.tableSettings[group].hasOwnProperty(d)) { - var field = $table.find('.' + this.tableSettings[group][d].target).eq(0); - if (field.length && this.tableSettings[group][d].hidden) { - hidden = this.tableSettings[group][d].hidden; - cell = field.closest('td'); - break; - } + Object.keys(this.tableSettings || {}).forEach(function (group) { + for (var d in _this2.tableSettings[group]) { + if (_this2.tableSettings[group].hasOwnProperty(d)) { + var field = $table.find('.' + _this2.tableSettings[group][d].target).eq(0); + if (field.length && _this2.tableSettings[group][d].hidden) { + hidden = _this2.tableSettings[group][d].hidden; + cell = field.closest('td'); + break; } } + } - if (hidden && cell[0]) { - columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1; - $table.find('> thead > tr, > tbody > tr, > tr').each(this.addColspanClass(columnIndex)); - } + if (hidden && cell[0]) { + columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1; + $table.find('> thead > tr, > tbody > tr, > tr').each(_this2.addColspanClass(columnIndex)); } - } + }); this.displayColumns(showWeight); }; @@ -217,11 +213,13 @@ Drupal.tableDrag.prototype.rowSettings = function (group, row) { var field = $(row).find('.' + group); var tableSettingsGroup = this.tableSettings[group]; + for (var delta in tableSettingsGroup) { if (tableSettingsGroup.hasOwnProperty(delta)) { var targetClass = tableSettingsGroup[delta].target; if (field.is('.' + targetClass)) { var rowSettings = {}; + for (var n in tableSettingsGroup[delta]) { if (tableSettingsGroup[delta].hasOwnProperty(n)) { rowSettings[n] = tableSettingsGroup[delta][n]; @@ -482,18 +480,14 @@ if (self.rowObject.changed === true) { self.updateFields(droppedRow); - for (var group in self.tableSettings) { - if (self.tableSettings.hasOwnProperty(group)) { - var rowSettings = self.rowSettings(group, droppedRow); - if (rowSettings.relationship === 'group') { - for (var n in self.rowObject.children) { - if (self.rowObject.children.hasOwnProperty(n)) { - self.updateField(self.rowObject.children[n], group); - } - } - } + Object.keys(self.tableSettings || {}).forEach(function (group) { + var rowSettings = self.rowSettings(group, droppedRow); + if (rowSettings.relationship === 'group') { + Object.keys(self.rowObject.children || {}).forEach(function (n) { + self.updateField(self.rowObject.children[n], group); + }); } - } + }); self.rowObject.markChanged(); if (self.changed === false) { @@ -577,11 +571,11 @@ }; Drupal.tableDrag.prototype.updateFields = function (changedRow) { - for (var group in this.tableSettings) { - if (this.tableSettings.hasOwnProperty(group)) { - this.updateField(changedRow, group); - } - } + var _this3 = this; + + Object.keys(this.tableSettings || {}).forEach(function (group) { + _this3.updateField(changedRow, group); + }); }; Drupal.tableDrag.prototype.updateField = function (changedRow, group) { @@ -932,11 +926,11 @@ }; Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () { - for (var n in this.children) { - if (this.children.hasOwnProperty(n)) { - $(this.children[n]).find('.js-indentation').removeClass('tree-child').removeClass('tree-child-first').removeClass('tree-child-last').removeClass('tree-child-horizontal'); - } - } + var _this4 = this; + + Object.keys(this.children || {}).forEach(function (n) { + $(_this4.children[n]).find('.js-indentation').removeClass('tree-child').removeClass('tree-child-first').removeClass('tree-child-last').removeClass('tree-child-horizontal'); + }); }; Drupal.tableDrag.prototype.row.prototype.markChanged = function () { diff --git a/core/modules/ckeditor/js/ckeditor.es6.js b/core/modules/ckeditor/js/ckeditor.es6.js index 21f6e4bb451d..a6429e0b5ecf 100644 --- a/core/modules/ckeditor/js/ckeditor.es6.js +++ b/core/modules/ckeditor/js/ckeditor.es6.js @@ -182,11 +182,9 @@ const externalPlugins = format.editorSettings.drupalExternalPlugins; // Register and load additional CKEditor plugins as necessary. if (externalPlugins) { - for (const pluginName in externalPlugins) { - if (externalPlugins.hasOwnProperty(pluginName)) { - CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); - } - } + Object.keys(externalPlugins || {}).forEach((pluginName) => { + CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); + }); delete format.editorSettings.drupalExternalPlugins; } }, diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js index 17477e559e17..becf7a19e791 100644 --- a/core/modules/ckeditor/js/ckeditor.js +++ b/core/modules/ckeditor/js/ckeditor.js @@ -102,11 +102,9 @@ var externalPlugins = format.editorSettings.drupalExternalPlugins; if (externalPlugins) { - for (var pluginName in externalPlugins) { - if (externalPlugins.hasOwnProperty(pluginName)) { - CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); - } - } + Object.keys(externalPlugins || {}).forEach(function (pluginName) { + CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); + }); delete format.editorSettings.drupalExternalPlugins; } } diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.es6.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.es6.js index 7444fcc24382..8dddd2e18440 100644 --- a/core/modules/ckeditor/js/plugins/drupallink/plugin.es6.js +++ b/core/modules/ckeditor/js/plugins/drupallink/plugin.es6.js @@ -34,11 +34,9 @@ function getAttributes(editor, data) { const set = {}; - for (const attributeName in data) { - if (data.hasOwnProperty(attributeName)) { - set[attributeName] = data[attributeName]; - } - } + Object.keys(data || {}).forEach((attributeName) => { + set[attributeName] = data[attributeName]; + }); // CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute // to work around browser quirks. We need to update it. @@ -46,11 +44,9 @@ // Remove all attributes which are not currently set. const removed = {}; - for (const s in set) { - if (set.hasOwnProperty(s)) { - delete removed[s]; - } - } + Object.keys(set).forEach((s) => { + delete removed[s]; + }); return { set, @@ -133,20 +129,18 @@ } // Update the link properties. else if (linkElement) { - for (const attrName in returnValues.attributes) { - if (returnValues.attributes.hasOwnProperty(attrName)) { - // Update the property if a value is specified. - if (returnValues.attributes[attrName].length > 0) { - const value = returnValues.attributes[attrName]; - linkElement.data(`cke-saved-${attrName}`, value); - linkElement.setAttribute(attrName, value); - } - // Delete the property if set to an empty string. - else { - linkElement.removeAttribute(attrName); - } + Object.keys(returnValues.attributes || {}).forEach((attrName) => { + // Update the property if a value is specified. + if (returnValues.attributes[attrName].length > 0) { + const value = returnValues.attributes[attrName]; + linkElement.data(`cke-saved-${attrName}`, value); + linkElement.setAttribute(attrName, value); } - } + // Delete the property if set to an empty string. + else { + linkElement.removeAttribute(attrName); + } + }); } // Save snapshot for undo support. diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.js index 1f7f8c46681d..d53f49a345ec 100644 --- a/core/modules/ckeditor/js/plugins/drupallink/plugin.js +++ b/core/modules/ckeditor/js/plugins/drupallink/plugin.js @@ -32,20 +32,16 @@ function getAttributes(editor, data) { var set = {}; - for (var attributeName in data) { - if (data.hasOwnProperty(attributeName)) { - set[attributeName] = data[attributeName]; - } - } + Object.keys(data || {}).forEach(function (attributeName) { + set[attributeName] = data[attributeName]; + }); set['data-cke-saved-href'] = set.href; var removed = {}; - for (var s in set) { - if (set.hasOwnProperty(s)) { - delete removed[s]; - } - } + Object.keys(set).forEach(function (s) { + delete removed[s]; + }); return { set: set, @@ -113,17 +109,15 @@ linkElement = getSelectedLink(editor); } else if (linkElement) { - for (var attrName in returnValues.attributes) { - if (returnValues.attributes.hasOwnProperty(attrName)) { - if (returnValues.attributes[attrName].length > 0) { - var value = returnValues.attributes[attrName]; - linkElement.data('cke-saved-' + attrName, value); - linkElement.setAttribute(attrName, value); - } else { - linkElement.removeAttribute(attrName); - } - } - } + Object.keys(returnValues.attributes || {}).forEach(function (attrName) { + if (returnValues.attributes[attrName].length > 0) { + var value = returnValues.attributes[attrName]; + linkElement.data('cke-saved-' + attrName, value); + linkElement.setAttribute(attrName, value); + } else { + linkElement.removeAttribute(attrName); + } + }); } editor.fire('saveSnapshot'); diff --git a/core/modules/ckeditor/js/views/ControllerView.es6.js b/core/modules/ckeditor/js/views/ControllerView.es6.js index 869a004b960c..8eaeec047984 100644 --- a/core/modules/ckeditor/js/views/ControllerView.es6.js +++ b/core/modules/ckeditor/js/views/ControllerView.es6.js @@ -150,11 +150,9 @@ const hiddenEditorConfig = this.model.get('hiddenEditorConfig'); if (hiddenEditorConfig.drupalExternalPlugins) { const externalPlugins = hiddenEditorConfig.drupalExternalPlugins; - for (const pluginName in externalPlugins) { - if (externalPlugins.hasOwnProperty(pluginName)) { - CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); - } - } + Object.keys(externalPlugins || {}).forEach((pluginName) => { + CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); + }); } CKEDITOR.inline($(`#${hiddenCKEditorID}`).get(0), CKEditorConfig); @@ -181,17 +179,15 @@ // @see getFeatureForButton() const features = {}; const buttonsToFeatures = {}; - for (const featureName in CKEFeatureRulesMap) { - if (CKEFeatureRulesMap.hasOwnProperty(featureName)) { - const feature = new Drupal.EditorFeature(featureName); - convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]); - features[featureName] = feature; - const command = e.editor.getCommand(featureName); - if (command) { - buttonsToFeatures[command.uiItems[0].name] = featureName; - } + Object.keys(CKEFeatureRulesMap).forEach((featureName) => { + const feature = new Drupal.EditorFeature(featureName); + convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]); + features[featureName] = feature; + const command = e.editor.getCommand(featureName); + if (command) { + buttonsToFeatures[command.uiItems[0].name] = featureName; } - } + }); callback(features, buttonsToFeatures); } @@ -326,25 +322,21 @@ // changed, rebuild the CKEditor features metadata. .on('CKEditorPluginSettingsChanged.ckeditorAdmin', (event, settingsChanges) => { // Update hidden CKEditor configuration. - for (const key in settingsChanges) { - if (settingsChanges.hasOwnProperty(key)) { - hiddenEditorConfig[key] = settingsChanges[key]; - } - } + Object.keys(settingsChanges || {}).forEach((key) => { + hiddenEditorConfig[key] = settingsChanges[key]; + }); // Retrieve features for the updated hidden CKEditor configuration. getCKEditorFeatures(hiddenEditorConfig, (features) => { // Trigger a standardized text editor configuration event for each // feature that was modified by the configuration changes. const featuresMetadata = view.model.get('featuresMetadata'); - for (const name in features) { - if (features.hasOwnProperty(name)) { - const feature = features[name]; - if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) { - Drupal.editorConfiguration.modifiedFeature(feature); - } + Object.keys(features || {}).forEach((name) => { + const feature = features[name]; + if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) { + Drupal.editorConfiguration.modifiedFeature(feature); } - } + }); // Update the CKEditor features metadata. view.model.set('featuresMetadata', features); }); diff --git a/core/modules/ckeditor/js/views/ControllerView.js b/core/modules/ckeditor/js/views/ControllerView.js index 4a19c29dfee7..ca3c83037070 100644 --- a/core/modules/ckeditor/js/views/ControllerView.js +++ b/core/modules/ckeditor/js/views/ControllerView.js @@ -91,11 +91,9 @@ var hiddenEditorConfig = this.model.get('hiddenEditorConfig'); if (hiddenEditorConfig.drupalExternalPlugins) { var externalPlugins = hiddenEditorConfig.drupalExternalPlugins; - for (var pluginName in externalPlugins) { - if (externalPlugins.hasOwnProperty(pluginName)) { - CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); - } - } + Object.keys(externalPlugins || {}).forEach(function (pluginName) { + CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); + }); } CKEDITOR.inline($('#' + hiddenCKEditorID).get(0), CKEditorConfig); @@ -116,17 +114,15 @@ var features = {}; var buttonsToFeatures = {}; - for (var featureName in CKEFeatureRulesMap) { - if (CKEFeatureRulesMap.hasOwnProperty(featureName)) { - var feature = new Drupal.EditorFeature(featureName); - convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]); - features[featureName] = feature; - var command = e.editor.getCommand(featureName); - if (command) { - buttonsToFeatures[command.uiItems[0].name] = featureName; - } + Object.keys(CKEFeatureRulesMap).forEach(function (featureName) { + var feature = new Drupal.EditorFeature(featureName); + convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]); + features[featureName] = feature; + var command = e.editor.getCommand(featureName); + if (command) { + buttonsToFeatures[command.uiItems[0].name] = featureName; } - } + }); callback(features, buttonsToFeatures); } @@ -200,22 +196,18 @@ var configEvent = action === 'added' ? 'addedFeature' : 'removedFeature'; Drupal.editorConfiguration[configEvent](feature); }).on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (event, settingsChanges) { - for (var key in settingsChanges) { - if (settingsChanges.hasOwnProperty(key)) { - hiddenEditorConfig[key] = settingsChanges[key]; - } - } + Object.keys(settingsChanges || {}).forEach(function (key) { + hiddenEditorConfig[key] = settingsChanges[key]; + }); getCKEditorFeatures(hiddenEditorConfig, function (features) { var featuresMetadata = view.model.get('featuresMetadata'); - for (var name in features) { - if (features.hasOwnProperty(name)) { - var feature = features[name]; - if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) { - Drupal.editorConfiguration.modifiedFeature(feature); - } + Object.keys(features || {}).forEach(function (name) { + var feature = features[name]; + if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) { + Drupal.editorConfiguration.modifiedFeature(feature); } - } + }); view.model.set('featuresMetadata', features); }); diff --git a/core/modules/color/color.es6.js b/core/modules/color/color.es6.js index 109bfa1a2cae..c4f923a02d4f 100644 --- a/core/modules/color/color.es6.js +++ b/core/modules/color/color.es6.js @@ -33,34 +33,30 @@ // Decode reference colors to HSL. const reference = settings.color.reference; - for (i in reference) { - if (reference.hasOwnProperty(i)) { - reference[i] = farb.RGBToHSL(farb.unpack(reference[i])); - } - } + Object.keys(reference || {}).forEach((color) => { + reference[color] = farb.RGBToHSL(farb.unpack(reference[color])); + }); // Build a preview. const height = []; const width = []; // Loop through all defined gradients. - for (i in settings.gradients) { - if (settings.gradients.hasOwnProperty(i)) { - // Add element to display the gradient. - $('.color-preview').once('color').append(`
`); - const gradient = $(`.color-preview #gradient-${i}`); - // Add height of current gradient to the list (divided by 10). - height.push(parseInt(gradient.css('height'), 10) / 10); - // Add width of current gradient to the list (divided by 10). - width.push(parseInt(gradient.css('width'), 10) / 10); - // Add rows (or columns for horizontal gradients). - // Each gradient line should have a height (or width for horizontal - // gradients) of 10px (because we divided the height/width by 10 - // above). - for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) { - gradient.append('
'); - } + Object.keys(settings.gradients || {}).forEach((i) => { + // Add element to display the gradient. + $('.color-preview').once('color').append(`
`); + const gradient = $(`.color-preview #gradient-${i}`); + // Add height of current gradient to the list (divided by 10). + height.push(parseInt(gradient.css('height'), 10) / 10); + // Add width of current gradient to the list (divided by 10). + width.push(parseInt(gradient.css('width'), 10) / 10); + // Add rows (or columns for horizontal gradients). + // Each gradient line should have a height (or width for horizontal + // gradients) of 10px (because we divided the height/width by 10 + // above). + for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) { + gradient.append('
'); } - } + }); // Set up colorScheme selector. form.find('#edit-scheme').on('change', function () { @@ -69,11 +65,9 @@ if (colorScheme !== '' && schemes[colorScheme]) { // Get colors of active scheme. colors = schemes[colorScheme]; - for (const fieldName in colors) { - if (colors.hasOwnProperty(fieldName)) { - callback($(`#edit-palette-${fieldName}`), colors[fieldName], false, true); - } - } + Object.keys(colors || {}).forEach((fieldName) => { + callback($(`#edit-palette-${fieldName}`), colors[fieldName], false, true); + }); preview(); } }); diff --git a/core/modules/color/color.js b/core/modules/color/color.js index ee91e56274d1..5cc0b9be12d9 100644 --- a/core/modules/color/color.js +++ b/core/modules/color/color.js @@ -25,40 +25,34 @@ var farb = $.farbtastic('.color-placeholder'); var reference = settings.color.reference; - for (i in reference) { - if (reference.hasOwnProperty(i)) { - reference[i] = farb.RGBToHSL(farb.unpack(reference[i])); - } - } + Object.keys(reference || {}).forEach(function (color) { + reference[color] = farb.RGBToHSL(farb.unpack(reference[color])); + }); var height = []; var width = []; - for (i in settings.gradients) { - if (settings.gradients.hasOwnProperty(i)) { - $('.color-preview').once('color').append('
'); - var gradient = $('.color-preview #gradient-' + i); + Object.keys(settings.gradients || {}).forEach(function (i) { + $('.color-preview').once('color').append('
'); + var gradient = $('.color-preview #gradient-' + i); - height.push(parseInt(gradient.css('height'), 10) / 10); + height.push(parseInt(gradient.css('height'), 10) / 10); - width.push(parseInt(gradient.css('width'), 10) / 10); + width.push(parseInt(gradient.css('width'), 10) / 10); - for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) { - gradient.append('
'); - } + for (j = 0; j < (settings.gradients[i].direction === 'vertical' ? height[i] : width[i]); ++j) { + gradient.append('
'); } - } + }); form.find('#edit-scheme').on('change', function () { var schemes = settings.color.schemes; var colorScheme = this.options[this.selectedIndex].value; if (colorScheme !== '' && schemes[colorScheme]) { colors = schemes[colorScheme]; - for (var fieldName in colors) { - if (colors.hasOwnProperty(fieldName)) { - callback($('#edit-palette-' + fieldName), colors[fieldName], false, true); - } - } + Object.keys(colors || {}).forEach(function (fieldName) { + callback($('#edit-palette-' + fieldName), colors[fieldName], false, true); + }); preview(); } }); diff --git a/core/modules/color/preview.es6.js b/core/modules/color/preview.es6.js index f5988b382dbe..cfc5e76b78d2 100644 --- a/core/modules/color/preview.es6.js +++ b/core/modules/color/preview.es6.js @@ -38,34 +38,28 @@ form.find('#text a, #text h2').css('color', form.find('.color-palette input[name="palette[link]"]').val()); function gradientLineColor(i, element) { - for (const k in accum) { - if (accum.hasOwnProperty(k)) { - accum[k] += delta[k]; - } - } + Object.keys(accum || {}).forEach((k) => { + accum[k] += delta[k]; + }); element.style.backgroundColor = farb.pack(accum); } // Set up gradients if there are some. let colorStart; let colorEnd; - for (const i in settings.gradients) { - if (settings.gradients.hasOwnProperty(i)) { - colorStart = farb.unpack(form.find(`.color-palette input[name="palette[${settings.gradients[i].colors[0]}]"]`).val()); - colorEnd = farb.unpack(form.find(`.color-palette input[name="palette[${settings.gradients[i].colors[1]}]"]`).val()); - if (colorStart && colorEnd) { - delta = []; - for (const j in colorStart) { - if (colorStart.hasOwnProperty(j)) { - delta[j] = (colorEnd[j] - colorStart[j]) / (settings.gradients[i].vertical ? height[i] : width[i]); - } - } - accum = colorStart; - // Render gradient lines. - form.find(`#gradient-${i} > div`).each(gradientLineColor); - } + Object.keys(settings.gradients || {}).forEach((i) => { + colorStart = farb.unpack(form.find(`.color-palette input[name="palette[${settings.gradients[i].colors[0]}]"]`).val()); + colorEnd = farb.unpack(form.find(`.color-palette input[name="palette[${settings.gradients[i].colors[1]}]"]`).val()); + if (colorStart && colorEnd) { + delta = []; + Object.keys(colorStart || {}).forEach((colorStartKey) => { + delta[colorStartKey] = (colorEnd[colorStartKey] - colorStart[colorStartKey]) / (settings.gradients[i].vertical ? height[i] : width[i]); + }); + accum = colorStart; + // Render gradient lines. + form.find(`#gradient-${i} > div`).each(gradientLineColor); } - } + }); }, }; }(jQuery, Drupal)); diff --git a/core/modules/color/preview.js b/core/modules/color/preview.js index 6ca5e32891c3..557e7dbdce3b 100644 --- a/core/modules/color/preview.js +++ b/core/modules/color/preview.js @@ -17,33 +17,27 @@ form.find('#text a, #text h2').css('color', form.find('.color-palette input[name="palette[link]"]').val()); function gradientLineColor(i, element) { - for (var k in accum) { - if (accum.hasOwnProperty(k)) { - accum[k] += delta[k]; - } - } + Object.keys(accum || {}).forEach(function (k) { + accum[k] += delta[k]; + }); element.style.backgroundColor = farb.pack(accum); } var colorStart = void 0; var colorEnd = void 0; - for (var i in settings.gradients) { - if (settings.gradients.hasOwnProperty(i)) { - colorStart = farb.unpack(form.find('.color-palette input[name="palette[' + settings.gradients[i].colors[0] + ']"]').val()); - colorEnd = farb.unpack(form.find('.color-palette input[name="palette[' + settings.gradients[i].colors[1] + ']"]').val()); - if (colorStart && colorEnd) { - delta = []; - for (var j in colorStart) { - if (colorStart.hasOwnProperty(j)) { - delta[j] = (colorEnd[j] - colorStart[j]) / (settings.gradients[i].vertical ? height[i] : width[i]); - } - } - accum = colorStart; + Object.keys(settings.gradients || {}).forEach(function (i) { + colorStart = farb.unpack(form.find('.color-palette input[name="palette[' + settings.gradients[i].colors[0] + ']"]').val()); + colorEnd = farb.unpack(form.find('.color-palette input[name="palette[' + settings.gradients[i].colors[1] + ']"]').val()); + if (colorStart && colorEnd) { + delta = []; + Object.keys(colorStart || {}).forEach(function (colorStartKey) { + delta[colorStartKey] = (colorEnd[colorStartKey] - colorStart[colorStartKey]) / (settings.gradients[i].vertical ? height[i] : width[i]); + }); + accum = colorStart; - form.find('#gradient-' + i + ' > div').each(gradientLineColor); - } + form.find('#gradient-' + i + ' > div').each(gradientLineColor); } - } + }); } }; })(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/comment/js/node-new-comments-link.es6.js b/core/modules/comment/js/node-new-comments-link.es6.js index e32c38cde0c6..a2bdc35b9d72 100644 --- a/core/modules/comment/js/node-new-comments-link.es6.js +++ b/core/modules/comment/js/node-new-comments-link.es6.js @@ -149,15 +149,15 @@ * Data about new comment links indexed by nodeID. */ function render(results) { - for (const nodeID in results) { - if (results.hasOwnProperty(nodeID) && $placeholdersToUpdate.hasOwnProperty(nodeID)) { + Object.keys(results || {}).forEach((nodeID) => { + if ($placeholdersToUpdate.hasOwnProperty(nodeID)) { $placeholdersToUpdate[nodeID] .attr('href', results[nodeID].first_new_comment_link) .text(Drupal.formatPlural(results[nodeID].new_comment_count, '1 new comment', '@count new comments')) .removeClass('hidden'); show($placeholdersToUpdate[nodeID]); } - } + }); } if (drupalSettings.comment && drupalSettings.comment.newCommentsLinks) { diff --git a/core/modules/comment/js/node-new-comments-link.js b/core/modules/comment/js/node-new-comments-link.js index 37fdb58c5e17..1396018a5618 100644 --- a/core/modules/comment/js/node-new-comments-link.js +++ b/core/modules/comment/js/node-new-comments-link.js @@ -70,12 +70,12 @@ } function render(results) { - for (var nodeID in results) { - if (results.hasOwnProperty(nodeID) && $placeholdersToUpdate.hasOwnProperty(nodeID)) { + Object.keys(results || {}).forEach(function (nodeID) { + if ($placeholdersToUpdate.hasOwnProperty(nodeID)) { $placeholdersToUpdate[nodeID].attr('href', results[nodeID].first_new_comment_link).text(Drupal.formatPlural(results[nodeID].new_comment_count, '1 new comment', '@count new comments')).removeClass('hidden'); show($placeholdersToUpdate[nodeID]); } - } + }); } if (drupalSettings.comment && drupalSettings.comment.newCommentsLinks) { diff --git a/core/modules/content_translation/content_translation.admin.es6.js b/core/modules/content_translation/content_translation.admin.es6.js index 6c5d0d9945f4..53ace084db03 100644 --- a/core/modules/content_translation/content_translation.admin.es6.js +++ b/core/modules/content_translation/content_translation.admin.es6.js @@ -17,7 +17,6 @@ const $context = $(context); const options = drupalSettings.contentTranslationDependentOptions; let $fields; - let dependentColumns; function fieldsChangeHandler($fields, dependentColumns) { return function (e) { @@ -29,15 +28,13 @@ // that name and copy over the input values that require all columns to be // translatable. if (options && options.dependent_selectors) { - for (const field in options.dependent_selectors) { - if (options.dependent_selectors.hasOwnProperty(field)) { - $fields = $context.find(`input[name^="${field}"]`); - dependentColumns = options.dependent_selectors[field]; + Object.keys(options.dependent_selectors).forEach((field) => { + $fields = $context.find(`input[name^="${field}"]`); + const dependentColumns = options.dependent_selectors[field]; - $fields.on('change', fieldsChangeHandler($fields, dependentColumns)); - Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns); - } - } + $fields.on('change', fieldsChangeHandler($fields, dependentColumns)); + Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns); + }); } }, check($fields, dependentColumns, $changed) { @@ -50,23 +47,21 @@ // A field that has many different translatable parts can also define one // or more columns that require all columns to be translatable. - for (const index in dependentColumns) { - if (dependentColumns.hasOwnProperty(index)) { - column = dependentColumns[index]; + Object.keys(dependentColumns || {}).forEach((index) => { + column = dependentColumns[index]; - if (!$changed) { - $element = $fields.filter(filterFieldsList); - } + if (!$changed) { + $element = $fields.filter(filterFieldsList); + } - if ($element.is(`input[value="${column}"]:checked`)) { - $fields.prop('checked', true) - .not($element).prop('disabled', true); - } - else { - $fields.prop('disabled', false); - } + if ($element.is(`input[value="${column}"]:checked`)) { + $fields.prop('checked', true) + .not($element).prop('disabled', true); } - } + else { + $fields.prop('disabled', false); + } + }); }, }; diff --git a/core/modules/content_translation/content_translation.admin.js b/core/modules/content_translation/content_translation.admin.js index 5066438930ef..33e3fbd45be2 100644 --- a/core/modules/content_translation/content_translation.admin.js +++ b/core/modules/content_translation/content_translation.admin.js @@ -11,7 +11,6 @@ var $context = $(context); var options = drupalSettings.contentTranslationDependentOptions; var $fields = void 0; - var dependentColumns = void 0; function fieldsChangeHandler($fields, dependentColumns) { return function (e) { @@ -20,15 +19,13 @@ } if (options && options.dependent_selectors) { - for (var field in options.dependent_selectors) { - if (options.dependent_selectors.hasOwnProperty(field)) { - $fields = $context.find('input[name^="' + field + '"]'); - dependentColumns = options.dependent_selectors[field]; + Object.keys(options.dependent_selectors).forEach(function (field) { + $fields = $context.find('input[name^="' + field + '"]'); + var dependentColumns = options.dependent_selectors[field]; - $fields.on('change', fieldsChangeHandler($fields, dependentColumns)); - Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns); - } - } + $fields.on('change', fieldsChangeHandler($fields, dependentColumns)); + Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns); + }); } }, check: function check($fields, dependentColumns, $changed) { @@ -39,21 +36,19 @@ return $(field).val() === column; } - for (var index in dependentColumns) { - if (dependentColumns.hasOwnProperty(index)) { - column = dependentColumns[index]; + Object.keys(dependentColumns || {}).forEach(function (index) { + column = dependentColumns[index]; - if (!$changed) { - $element = $fields.filter(filterFieldsList); - } + if (!$changed) { + $element = $fields.filter(filterFieldsList); + } - if ($element.is('input[value="' + column + '"]:checked')) { - $fields.prop('checked', true).not($element).prop('disabled', true); - } else { - $fields.prop('disabled', false); - } + if ($element.is('input[value="' + column + '"]:checked')) { + $fields.prop('checked', true).not($element).prop('disabled', true); + } else { + $fields.prop('disabled', false); } - } + }); } }; diff --git a/core/modules/editor/js/editor.admin.es6.js b/core/modules/editor/js/editor.admin.es6.js index b02fe8bc447a..a9c040c72ed9 100644 --- a/core/modules/editor/js/editor.admin.es6.js +++ b/core/modules/editor/js/editor.admin.es6.js @@ -567,6 +567,7 @@ // If any filter's current status forbids the editor feature, return // false. Drupal.filterConfiguration.update(); + // eslint-disable-next-line no-restricted-syntax for (const filterID in Drupal.filterConfiguration.statuses) { if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) { const filterStatus = Drupal.filterConfiguration.statuses[filterID]; @@ -879,17 +880,15 @@ * up-to-date. */ update() { - for (const filterID in Drupal.filterConfiguration.statuses) { - if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) { - // Update status. - Drupal.filterConfiguration.statuses[filterID].active = $(`[name="filters[${filterID}][status]"]`).is(':checked'); + Object.keys(Drupal.filterConfiguration.statuses || {}).forEach((filterID) => { + // Update status. + Drupal.filterConfiguration.statuses[filterID].active = $(`[name="filters[${filterID}][status]"]`).is(':checked'); - // Update current rules. - if (Drupal.filterConfiguration.liveSettingParsers[filterID]) { - Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules(); - } + // Update current rules. + if (Drupal.filterConfiguration.liveSettingParsers[filterID]) { + Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules(); } - } + }); }, }; diff --git a/core/modules/editor/js/editor.admin.js b/core/modules/editor/js/editor.admin.js index 377aef23244c..9cec9ada6aec 100644 --- a/core/modules/editor/js/editor.admin.js +++ b/core/modules/editor/js/editor.admin.js @@ -256,6 +256,7 @@ } Drupal.filterConfiguration.update(); + for (var filterID in Drupal.filterConfiguration.statuses) { if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) { var filterStatus = Drupal.filterConfiguration.statuses[filterID]; @@ -331,15 +332,13 @@ liveSettingParsers: {}, update: function update() { - for (var filterID in Drupal.filterConfiguration.statuses) { - if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) { - Drupal.filterConfiguration.statuses[filterID].active = $('[name="filters[' + filterID + '][status]"]').is(':checked'); + Object.keys(Drupal.filterConfiguration.statuses || {}).forEach(function (filterID) { + Drupal.filterConfiguration.statuses[filterID].active = $('[name="filters[' + filterID + '][status]"]').is(':checked'); - if (Drupal.filterConfiguration.liveSettingParsers[filterID]) { - Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules(); - } + if (Drupal.filterConfiguration.liveSettingParsers[filterID]) { + Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules(); } - } + }); } }; diff --git a/core/modules/field_ui/field_ui.es6.js b/core/modules/field_ui/field_ui.es6.js index e8f9f9aba529..965a2e4c36b0 100644 --- a/core/modules/field_ui/field_ui.es6.js +++ b/core/modules/field_ui/field_ui.es6.js @@ -212,13 +212,10 @@ // Separate keys and values. const rowNames = []; const ajaxElements = []; - let rowName; - for (rowName in rows) { - if (rows.hasOwnProperty(rowName)) { - rowNames.push(rowName); - ajaxElements.push(rows[rowName]); - } - } + Object.keys(rows || {}).forEach((rowName) => { + rowNames.push(rowName); + ajaxElements.push(rows[rowName]); + }); if (rowNames.length) { // Add a throbber next each of the ajaxElements. diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js index f8bf3e560649..d0e8a6d7126f 100644 --- a/core/modules/field_ui/field_ui.js +++ b/core/modules/field_ui/field_ui.js @@ -120,13 +120,10 @@ AJAXRefreshRows: function AJAXRefreshRows(rows) { var rowNames = []; var ajaxElements = []; - var rowName = void 0; - for (rowName in rows) { - if (rows.hasOwnProperty(rowName)) { - rowNames.push(rowName); - ajaxElements.push(rows[rowName]); - } - } + Object.keys(rows || {}).forEach(function (rowName) { + rowNames.push(rowName); + ajaxElements.push(rows[rowName]); + }); if (rowNames.length) { $(ajaxElements).after('
 
'); diff --git a/core/modules/filter/filter.filter_html.admin.es6.js b/core/modules/filter/filter.filter_html.admin.es6.js index c5809c09e8b2..548088397c64 100644 --- a/core/modules/filter/filter.filter_html.admin.es6.js +++ b/core/modules/filter/filter.filter_html.admin.es6.js @@ -135,46 +135,44 @@ * A list of new allowed tags. */ _calculateAutoAllowedTags(userAllowedTags, newFeatures) { - let featureName; - let feature; - let featureRule; - let filterRule; - let tag; const editorRequiredTags = {}; + // Map the newly added Text Editor features to Drupal.FilterHtmlRule // objects (to allow comparing userTags with autoTags). - for (featureName in newFeatures) { - if (newFeatures.hasOwnProperty(featureName)) { - feature = newFeatures[featureName]; - for (let f = 0; f < feature.length; f++) { - featureRule = feature[f]; - for (let t = 0; t < featureRule.required.tags.length; t++) { - tag = featureRule.required.tags[t]; - if (!_.has(editorRequiredTags, tag)) { - filterRule = new Drupal.FilterHTMLRule(); - filterRule.restrictedTags.tags = [tag]; - // @todo Neither Drupal.FilterHtmlRule nor - // Drupal.EditorFeatureHTMLRule allow for generic attribute - // value restrictions, only for the "class" and "style" - // attribute's values to be restricted. The filter_html filter - // always disallows the "style" attribute, so we only need to - // support "class" attribute value restrictions. Fix once - // https://www.drupal.org/node/2567801 lands. - filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0); - filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0); - editorRequiredTags[tag] = filterRule; - } - // The tag is already allowed, add any additionally allowed - // attributes. - else { - filterRule = editorRequiredTags[tag]; - filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes); - filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes); - } + Object.keys(newFeatures || {}).forEach((featureName) => { + const feature = newFeatures[featureName]; + let featureRule; + let filterRule; + let tag; + + for (let f = 0; f < feature.length; f++) { + featureRule = feature[f]; + for (let t = 0; t < featureRule.required.tags.length; t++) { + tag = featureRule.required.tags[t]; + if (!_.has(editorRequiredTags, tag)) { + filterRule = new Drupal.FilterHTMLRule(); + filterRule.restrictedTags.tags = [tag]; + // @todo Neither Drupal.FilterHtmlRule nor + // Drupal.EditorFeatureHTMLRule allow for generic attribute + // value restrictions, only for the "class" and "style" + // attribute's values to be restricted. The filter_html filter + // always disallows the "style" attribute, so we only need to + // support "class" attribute value restrictions. Fix once + // https://www.drupal.org/node/2567801 lands. + filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0); + filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0); + editorRequiredTags[tag] = filterRule; + } + // The tag is already allowed, add any additionally allowed + // attributes. + else { + filterRule = editorRequiredTags[tag]; + filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes); + filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes); } } } - } + }); // Now compare userAllowedTags with editorRequiredTags, and build // autoAllowedTags, which contains: @@ -183,7 +181,7 @@ // - any tags in editorRequiredTags that already exists in userAllowedTags // but does not allow all attributes or attribute values const autoAllowedTags = {}; - for (tag in editorRequiredTags) { + Object.keys(editorRequiredTags).forEach((tag) => { // If userAllowedTags does not contain a rule for this editor-required // tag, then add it to the list of automatically allowed tags. if (!_.has(userAllowedTags, tag)) { @@ -209,7 +207,7 @@ autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(allowedClasses, requiredClasses); } } - } + }); return autoAllowedTags; }, diff --git a/core/modules/filter/filter.filter_html.admin.js b/core/modules/filter/filter.filter_html.admin.js index 773f35a9d840..5f4c6ef9fad3 100644 --- a/core/modules/filter/filter.filter_html.admin.js +++ b/core/modules/filter/filter.filter_html.admin.js @@ -74,39 +74,36 @@ } }, _calculateAutoAllowedTags: function _calculateAutoAllowedTags(userAllowedTags, newFeatures) { - var featureName = void 0; - var feature = void 0; - var featureRule = void 0; - var filterRule = void 0; - var tag = void 0; var editorRequiredTags = {}; - for (featureName in newFeatures) { - if (newFeatures.hasOwnProperty(featureName)) { - feature = newFeatures[featureName]; - for (var f = 0; f < feature.length; f++) { - featureRule = feature[f]; - for (var t = 0; t < featureRule.required.tags.length; t++) { - tag = featureRule.required.tags[t]; - if (!_.has(editorRequiredTags, tag)) { - filterRule = new Drupal.FilterHTMLRule(); - filterRule.restrictedTags.tags = [tag]; - - filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0); - filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0); - editorRequiredTags[tag] = filterRule; - } else { - filterRule = editorRequiredTags[tag]; - filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes); - filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes); - } - } + Object.keys(newFeatures || {}).forEach(function (featureName) { + var feature = newFeatures[featureName]; + var featureRule = void 0; + var filterRule = void 0; + var tag = void 0; + + for (var f = 0; f < feature.length; f++) { + featureRule = feature[f]; + for (var t = 0; t < featureRule.required.tags.length; t++) { + tag = featureRule.required.tags[t]; + if (!_.has(editorRequiredTags, tag)) { + filterRule = new Drupal.FilterHTMLRule(); + filterRule.restrictedTags.tags = [tag]; + + filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(0); + filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(0); + editorRequiredTags[tag] = filterRule; + } else { + filterRule = editorRequiredTags[tag]; + filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes); + filterRule.restrictedTags.allowed.classes = _.union(filterRule.restrictedTags.allowed.classes, featureRule.required.classes); + } } } - } + }); var autoAllowedTags = {}; - for (tag in editorRequiredTags) { + Object.keys(editorRequiredTags).forEach(function (tag) { if (!_.has(userAllowedTags, tag)) { autoAllowedTags[tag] = editorRequiredTags[tag]; } else { @@ -126,7 +123,7 @@ autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(allowedClasses, requiredClasses); } } - } + }); return autoAllowedTags; }, diff --git a/core/modules/history/js/history.es6.js b/core/modules/history/js/history.es6.js index c34a4adcee8b..dba78b12df3e 100644 --- a/core/modules/history/js/history.es6.js +++ b/core/modules/history/js/history.es6.js @@ -45,11 +45,9 @@ data: { 'node_ids[]': nodeIDs }, dataType: 'json', success(results) { - for (const nodeID in results) { - if (results.hasOwnProperty(nodeID)) { - storage.setItem(`Drupal.history.${currentUserID}.${nodeID}`, results[nodeID]); - } - } + Object.keys(results || {}).forEach((nodeID) => { + storage.setItem(`Drupal.history.${currentUserID}.${nodeID}`, results[nodeID]); + }); callback(); }, }); diff --git a/core/modules/history/js/history.js b/core/modules/history/js/history.js index 6dbd15cb99bf..b9cb9fc5fb00 100644 --- a/core/modules/history/js/history.js +++ b/core/modules/history/js/history.js @@ -29,11 +29,9 @@ data: { 'node_ids[]': nodeIDs }, dataType: 'json', success: function success(results) { - for (var nodeID in results) { - if (results.hasOwnProperty(nodeID)) { - storage.setItem('Drupal.history.' + currentUserID + '.' + nodeID, results[nodeID]); - } - } + Object.keys(results || {}).forEach(function (nodeID) { + storage.setItem('Drupal.history.' + currentUserID + '.' + nodeID, results[nodeID]); + }); callback(); } }); diff --git a/core/modules/menu_ui/menu_ui.admin.es6.js b/core/modules/menu_ui/menu_ui.admin.es6.js index fabcec048d0c..3144d13dc1b3 100644 --- a/core/modules/menu_ui/menu_ui.admin.es6.js +++ b/core/modules/menu_ui/menu_ui.admin.es6.js @@ -47,14 +47,12 @@ $select.children().remove(); // Add new options to dropdown. Keep a count of options for testing later. let totalOptions = 0; - for (const machineName in options) { - if (options.hasOwnProperty(machineName)) { - $select.append( - $(``).val(machineName).text(options[machineName]), - ); - totalOptions++; - } - } + Object.keys(options || {}).forEach((machineName) => { + $select.append( + $(``).val(machineName).text(options[machineName]), + ); + totalOptions++; + }); // Hide the parent options if there are no options for it. $select.closest('div').toggle(totalOptions > 0).attr('hidden', totalOptions === 0); diff --git a/core/modules/menu_ui/menu_ui.admin.js b/core/modules/menu_ui/menu_ui.admin.js index 324b567a07e5..03e0e22ae73d 100644 --- a/core/modules/menu_ui/menu_ui.admin.js +++ b/core/modules/menu_ui/menu_ui.admin.js @@ -38,12 +38,10 @@ $select.children().remove(); var totalOptions = 0; - for (var machineName in options) { - if (options.hasOwnProperty(machineName)) { - $select.append($('').val(machineName).text(options[machineName])); - totalOptions++; - } - } + Object.keys(options || {}).forEach(function (machineName) { + $select.append($('').val(machineName).text(options[machineName])); + totalOptions++; + }); $select.closest('div').toggle(totalOptions > 0).attr('hidden', totalOptions === 0); } diff --git a/core/modules/quickedit/js/theme.es6.js b/core/modules/quickedit/js/theme.es6.js index b2fa17c901d1..b9355b13aa8a 100644 --- a/core/modules/quickedit/js/theme.es6.js +++ b/core/modules/quickedit/js/theme.es6.js @@ -144,11 +144,9 @@ // Attributes. const attributes = []; const attrMap = settings.buttons[i].attributes || {}; - for (const attr in attrMap) { - if (attrMap.hasOwnProperty(attr)) { - attributes.push(attr + ((attrMap[attr]) ? `="${attrMap[attr]}"` : '')); - } - } + Object.keys(attrMap).forEach((attr) => { + attributes.push(attr + ((attrMap[attr]) ? `="${attrMap[attr]}"` : '')); + }); html += ``; } return html; diff --git a/core/modules/quickedit/js/theme.js b/core/modules/quickedit/js/theme.js index c1d1cfadfd04..8b12cd7651d1 100644 --- a/core/modules/quickedit/js/theme.js +++ b/core/modules/quickedit/js/theme.js @@ -53,7 +53,8 @@ Drupal.theme.quickeditButtons = function (settings) { var html = ''; - for (var i = 0; i < settings.buttons.length; i++) { + + var _loop = function _loop(i) { var button = settings.buttons[i]; if (!button.hasOwnProperty('type')) { button.type = 'button'; @@ -61,12 +62,14 @@ var attributes = []; var attrMap = settings.buttons[i].attributes || {}; - for (var attr in attrMap) { - if (attrMap.hasOwnProperty(attr)) { - attributes.push(attr + (attrMap[attr] ? '="' + attrMap[attr] + '"' : '')); - } - } + Object.keys(attrMap).forEach(function (attr) { + attributes.push(attr + (attrMap[attr] ? '="' + attrMap[attr] + '"' : '')); + }); html += ''; + }; + + for (var i = 0; i < settings.buttons.length; i++) { + _loop(i); } return html; }; diff --git a/core/modules/quickedit/js/util.es6.js b/core/modules/quickedit/js/util.es6.js index f8873e2e3513..192cea6537b2 100644 --- a/core/modules/quickedit/js/util.es6.js +++ b/core/modules/quickedit/js/util.es6.js @@ -181,11 +181,11 @@ * The HTTP status code. */ success(response, status) { - for (const i in response) { - if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) { + Object.keys(response || {}).forEach((i) => { + if (response[i].command && this.commands[response[i].command]) { this.commands[response[i].command](this, response[i], status); } - } + }); }, base: $submit.attr('id'), element: $submit[0], diff --git a/core/modules/quickedit/js/util.js b/core/modules/quickedit/js/util.js index 16b524a9f310..a9c09728619e 100644 --- a/core/modules/quickedit/js/util.js +++ b/core/modules/quickedit/js/util.js @@ -85,11 +85,13 @@ }, success: function success(response, status) { - for (var i in response) { - if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) { - this.commands[response[i].command](this, response[i], status); + var _this = this; + + Object.keys(response || {}).forEach(function (i) { + if (response[i].command && _this.commands[response[i].command]) { + _this.commands[response[i].command](_this, response[i], status); } - } + }); }, base: $submit.attr('id'), diff --git a/core/modules/system/js/system.es6.js b/core/modules/system/js/system.es6.js index 18153b09ec73..51007662e3c3 100644 --- a/core/modules/system/js/system.es6.js +++ b/core/modules/system/js/system.es6.js @@ -23,11 +23,7 @@ attach(context) { // List of fields IDs on which to bind the event listener. // Create an array of IDs to use with jQuery. - for (const sourceId in drupalSettings.copyFieldValue) { - if (drupalSettings.copyFieldValue.hasOwnProperty(sourceId)) { - ids.push(sourceId); - } - } + Object.keys(drupalSettings.copyFieldValue || {}).forEach(ids.push); 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 d62cc0744630..4bcfe348ed7f 100644 --- a/core/modules/system/js/system.js +++ b/core/modules/system/js/system.js @@ -10,11 +10,7 @@ Drupal.behaviors.copyFieldValue = { attach: function attach(context) { - for (var sourceId in drupalSettings.copyFieldValue) { - if (drupalSettings.copyFieldValue.hasOwnProperty(sourceId)) { - ids.push(sourceId); - } - } + Object.keys(drupalSettings.copyFieldValue || {}).forEach(ids.push); if (ids.length) { $('body').once('copy-field-values').on('value:copy', this.valueTargetCopyHandler); diff --git a/core/modules/toolbar/js/toolbar.es6.js b/core/modules/toolbar/js/toolbar.es6.js index f715fc7fc96e..4149d27da7c7 100644 --- a/core/modules/toolbar/js/toolbar.es6.js +++ b/core/modules/toolbar/js/toolbar.es6.js @@ -56,19 +56,17 @@ // Attach a listener to the configured media query breakpoints. // Executes it before Drupal.toolbar.views to avoid extra rendering. - for (const label in options.breakpoints) { - if (options.breakpoints.hasOwnProperty(label)) { - const mq = options.breakpoints[label]; - const mql = window.matchMedia(mq); - Drupal.toolbar.mql[label] = mql; - // Curry the model and the label of the media query breakpoint to - // the mediaQueryChangeHandler function. - mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label)); - // Fire the mediaQueryChangeHandler for each configured breakpoint - // so that they process once. - Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql); - } - } + Object.keys(options.breakpoints).forEach((label) => { + const mq = options.breakpoints[label]; + const mql = window.matchMedia(mq); + Drupal.toolbar.mql[label] = mql; + // Curry the model and the label of the media query breakpoint to + // the mediaQueryChangeHandler function. + mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label)); + // Fire the mediaQueryChangeHandler for each configured breakpoint + // so that they process once. + Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql); + }); Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView({ el: this, diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js index 547bcd9bac7a..7246420f8888 100644 --- a/core/modules/toolbar/js/toolbar.js +++ b/core/modules/toolbar/js/toolbar.js @@ -34,17 +34,15 @@ Drupal.toolbar.models.toolbarModel = model; - for (var label in options.breakpoints) { - if (options.breakpoints.hasOwnProperty(label)) { - var mq = options.breakpoints[label]; - var mql = window.matchMedia(mq); - Drupal.toolbar.mql[label] = mql; + Object.keys(options.breakpoints).forEach(function (label) { + var mq = options.breakpoints[label]; + var mql = window.matchMedia(mq); + Drupal.toolbar.mql[label] = mql; - mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label)); + mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label)); - Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql); - } - } + Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql); + }); Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView({ el: this, diff --git a/core/modules/toolbar/js/views/MenuVisualView.es6.js b/core/modules/toolbar/js/views/MenuVisualView.es6.js index e956112aafb2..108f65c4261a 100644 --- a/core/modules/toolbar/js/views/MenuVisualView.es6.js +++ b/core/modules/toolbar/js/views/MenuVisualView.es6.js @@ -23,14 +23,12 @@ render() { const subtrees = this.model.get('subtrees'); // Add subtrees. - for (const id in subtrees) { - if (subtrees.hasOwnProperty(id)) { - this.$el - .find(`#toolbar-link-${id}`) - .once('toolbar-subtrees') - .after(subtrees[id]); - } - } + Object.keys(subtrees || {}).forEach((id) => { + this.$el + .find(`#toolbar-link-${id}`) + .once('toolbar-subtrees') + .after(subtrees[id]); + }); // Render the main menu as a nested, collapsible accordion. if ('drupalToolbarMenu' in $.fn) { this.$el diff --git a/core/modules/toolbar/js/views/MenuVisualView.js b/core/modules/toolbar/js/views/MenuVisualView.js index a9cd1a30f6a4..8156a8b11d9e 100644 --- a/core/modules/toolbar/js/views/MenuVisualView.js +++ b/core/modules/toolbar/js/views/MenuVisualView.js @@ -11,13 +11,13 @@ this.listenTo(this.model, 'change:subtrees', this.render); }, render: function render() { + var _this = this; + var subtrees = this.model.get('subtrees'); - for (var id in subtrees) { - if (subtrees.hasOwnProperty(id)) { - this.$el.find('#toolbar-link-' + id).once('toolbar-subtrees').after(subtrees[id]); - } - } + Object.keys(subtrees || {}).forEach(function (id) { + _this.$el.find('#toolbar-link-' + id).once('toolbar-subtrees').after(subtrees[id]); + }); if ('drupalToolbarMenu' in $.fn) { this.$el.children('.toolbar-menu').drupalToolbarMenu(); diff --git a/core/modules/tracker/js/tracker-history.es6.js b/core/modules/tracker/js/tracker-history.es6.js index 5df3494a9cd2..13090008d65e 100644 --- a/core/modules/tracker/js/tracker-history.es6.js +++ b/core/modules/tracker/js/tracker-history.es6.js @@ -103,13 +103,13 @@ data: { 'node_ids[]': nodeIDs }, dataType: 'json', success(results) { - for (const nodeID in results) { - if (results.hasOwnProperty(nodeID) && placeholdersToUpdate.hasOwnProperty(nodeID)) { + Object.keys(results || {}).forEach((nodeID) => { + if (placeholdersToUpdate.hasOwnProperty(nodeID)) { const url = results[nodeID].first_new_comment_link; const text = Drupal.formatPlural(results[nodeID].new_comment_count, '1 new', '@count new'); $(placeholdersToUpdate[nodeID]).append(`
${text}`); } - } + }); }, }); } diff --git a/core/modules/tracker/js/tracker-history.js b/core/modules/tracker/js/tracker-history.js index c60f50d9be6a..a147eb8a6364 100644 --- a/core/modules/tracker/js/tracker-history.js +++ b/core/modules/tracker/js/tracker-history.js @@ -87,13 +87,13 @@ data: { 'node_ids[]': nodeIDs }, dataType: 'json', success: function success(results) { - for (var nodeID in results) { - if (results.hasOwnProperty(nodeID) && placeholdersToUpdate.hasOwnProperty(nodeID)) { + Object.keys(results || {}).forEach(function (nodeID) { + if (placeholdersToUpdate.hasOwnProperty(nodeID)) { var url = results[nodeID].first_new_comment_link; var text = Drupal.formatPlural(results[nodeID].new_comment_count, '1 new', '@count new'); $(placeholdersToUpdate[nodeID]).append('
' + text + ''); } - } + }); } }); } diff --git a/core/modules/views/js/ajax_view.es6.js b/core/modules/views/js/ajax_view.es6.js index eb06da0ec1b2..0c8535890b5a 100644 --- a/core/modules/views/js/ajax_view.es6.js +++ b/core/modules/views/js/ajax_view.es6.js @@ -16,11 +16,9 @@ Drupal.behaviors.ViewsAjaxView.attach = function () { if (drupalSettings && drupalSettings.views && drupalSettings.views.ajaxViews) { const ajaxViews = drupalSettings.views.ajaxViews; - for (const i in ajaxViews) { - if (ajaxViews.hasOwnProperty(i)) { - Drupal.views.instances[i] = new Drupal.views.ajaxView(ajaxViews[i]); - } - } + Object.keys(ajaxViews || {}).forEach((i) => { + Drupal.views.instances[i] = new Drupal.views.ajaxView(ajaxViews[i]); + }); } }; diff --git a/core/modules/views/js/ajax_view.js b/core/modules/views/js/ajax_view.js index 175126b83c73..a10eb837eb97 100644 --- a/core/modules/views/js/ajax_view.js +++ b/core/modules/views/js/ajax_view.js @@ -10,11 +10,9 @@ Drupal.behaviors.ViewsAjaxView.attach = function () { if (drupalSettings && drupalSettings.views && drupalSettings.views.ajaxViews) { var ajaxViews = drupalSettings.views.ajaxViews; - for (var i in ajaxViews) { - if (ajaxViews.hasOwnProperty(i)) { - Drupal.views.instances[i] = new Drupal.views.ajaxView(ajaxViews[i]); - } - } + Object.keys(ajaxViews || {}).forEach(function (i) { + Drupal.views.instances[i] = new Drupal.views.ajaxView(ajaxViews[i]); + }); } }; From 44805b25e640a80a1ceaeab92aaa1103bd69e9cf Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole Date: Thu, 4 Jan 2018 11:36:55 +0000 Subject: [PATCH 090/232] Issue #2933769 by masipila: Merge handbook documentation to API: substr process plugin --- .../src/Plugin/migrate/process/Substr.php | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/core/modules/migrate/src/Plugin/migrate/process/Substr.php b/core/modules/migrate/src/Plugin/migrate/process/Substr.php index 1f852a9238b6..f36da0acd597 100644 --- a/core/modules/migrate/src/Plugin/migrate/process/Substr.php +++ b/core/modules/migrate/src/Plugin/migrate/process/Substr.php @@ -12,19 +12,19 @@ * Returns a substring of the input value. * * The substr process plugin returns the portion of the input value specified by - * the start and length parameters. This is a wrapper around the PHP substr() - * function. + * the start and length parameters. This is a wrapper around + * \Drupal\Component\Utility\Unicode::substr(). * * Available configuration keys: * - start: (optional) The returned string will start this many characters after - * the beginning of the string. Defaults to NULL. + * the beginning of the string, defaults to 0. * - length: (optional) The maximum number of characters in the returned - * string. Defaults to NULL. + * string, defaults to NULL. * - * If start is NULL and length is an integer, the start position is the + * If start is 0 and length is an integer, the start position is the * beginning of the string. If start is an integer and length is NULL, the * substring starting from the start position until the end of the string will - * be returned. If both start and length are NULL the entire string is returned. + * be returned. If start is 0 and length is NULL the entire string is returned. * * Example: * @@ -36,16 +36,30 @@ * start: 6 * length: 10 * @endcode - * * If some_text_field was 'Marie Skłodowska Curie' then * $destination['new_text_field'] would be 'Skłodowska'. * * The PHP equivalent of this is: - * * @code * $destination['new_text_field'] = substr($source['some_text_field'], 6, 10); * @endcode * + * The substr plugin requires that the source value is not empty. If empty + * values are expected, combine skip_on_empty process plugin to the pipeline: + * @code + * process: + * new_text_field: + * - + * plugin: skip_on_empty + * method: process + * source: some_text_field + * - + * plugin: substr + * source: some_text_field + * start: 6 + * length: 10 + * @endcode + * * @see \Drupal\migrate\Plugin\MigrateProcessInterface * * @MigrateProcessPlugin( From 03e3d7a3a07be4d4e42dfb1fb2f2d9445f68b79d Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Thu, 4 Jan 2018 11:42:56 +0000 Subject: [PATCH 091/232] Issue #2377747 by mondrake, eiriksm, rpayanm, adci_contributor, mgifford, oakulm, Truptti, chetan2111, joyceg, alexpott, xjm, yoroy, catch: Incorrect node create validation error when an invalid image is attached to a field --- core/modules/file/file.module | 2 +- .../Plugin/Field/FieldWidget/ImageWidget.php | 3 + .../src/Tests/ImageFieldValidateTest.php | 55 +++++++++++++++++++ .../simpletest/files/invalid-img-test.png | 1 + .../files/invalid-img-zero-size.png | 0 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 core/modules/simpletest/files/invalid-img-test.png create mode 100644 core/modules/simpletest/files/invalid-img-zero-size.png diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 013201119a71..be1e136958fc 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -406,7 +406,7 @@ function file_validate_is_image(FileInterface $file) { $image = $image_factory->get($file->getFileUri()); if (!$image->isValid()) { $supported_extensions = $image_factory->getSupportedExtensions(); - $errors[] = t('Image type not supported. Allowed types: %types', ['%types' => implode(' ', $supported_extensions)]); + $errors[] = t('The image file is invalid or the image type is not allowed. Allowed types: %types', ['%types' => implode(', ', $supported_extensions)]); } return $errors; diff --git a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php index 735cfceebc44..e66d590ed825 100644 --- a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php +++ b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php @@ -112,6 +112,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $field_settings = $this->getFieldSettings(); + // Add image validation. + $element['#upload_validators']['file_validate_is_image'] = []; + // Add upload resolution validation. if ($field_settings['max_resolution'] || $field_settings['min_resolution']) { $element['#upload_validators']['file_validate_image_resolution'] = [$field_settings['max_resolution'], $field_settings['min_resolution']]; diff --git a/core/modules/image/src/Tests/ImageFieldValidateTest.php b/core/modules/image/src/Tests/ImageFieldValidateTest.php index 98210d642199..ba1a766509d9 100644 --- a/core/modules/image/src/Tests/ImageFieldValidateTest.php +++ b/core/modules/image/src/Tests/ImageFieldValidateTest.php @@ -8,6 +8,61 @@ * @group image */ class ImageFieldValidateTest extends ImageFieldTestBase { + + /** + * Test image validity. + */ + public function testValid() { + $file_system = $this->container->get('file_system'); + $image_files = $this->drupalGetTestFiles('image'); + + $field_name = strtolower($this->randomMachineName()); + $this->createImageField($field_name, 'article', [], ['file_directory' => 'test-upload']); + $expected_path = 'public://test-upload'; + + // Create alt text for the image. + $alt = $this->randomMachineName(); + + // Create a node with a valid image. + $node = $this->uploadNodeImage($image_files[0], $field_name, 'article', $alt); + $this->assertTrue(file_exists($expected_path . '/' . $image_files[0]->filename)); + + // Remove the image. + $this->drupalPostForm('node/' . $node . '/edit', [], t('Remove')); + $this->drupalPostForm(NULL, [], t('Save')); + + // Get invalid image test files from simpletest. + $files = file_scan_directory(drupal_get_path('module', 'simpletest') . '/files', '/invalid-img-.*/'); + $invalid_image_files = []; + foreach ($files as $file) { + $invalid_image_files[$file->filename] = $file; + } + + // Try uploading a zero-byte image. + $zero_size_image = $invalid_image_files['invalid-img-zero-size.png']; + $edit = [ + 'files[' . $field_name . '_0]' => $file_system->realpath($zero_size_image->uri), + ]; + $this->drupalPostForm('node/' . $node . '/edit', $edit, t('Upload')); + $this->assertFalse(file_exists($expected_path . '/' . $zero_size_image->filename)); + + // Try uploading an invalid image. + $invalid_image = $invalid_image_files['invalid-img-test.png']; + $edit = [ + 'files[' . $field_name . '_0]' => $file_system->realpath($invalid_image->uri), + ]; + $this->drupalPostForm('node/' . $node . '/edit', $edit, t('Upload')); + $this->assertFalse(file_exists($expected_path . '/' . $invalid_image->filename)); + + // Upload a valid image again. + $valid_image = $image_files[0]; + $edit = [ + 'files[' . $field_name . '_0]' => $file_system->realpath($valid_image->uri), + ]; + $this->drupalPostForm('node/' . $node . '/edit', $edit, t('Upload')); + $this->assertTrue(file_exists($expected_path . '/' . $valid_image->filename)); + } + /** * Test min/max resolution settings. */ diff --git a/core/modules/simpletest/files/invalid-img-test.png b/core/modules/simpletest/files/invalid-img-test.png new file mode 100644 index 000000000000..8175b448b9e0 --- /dev/null +++ b/core/modules/simpletest/files/invalid-img-test.png @@ -0,0 +1 @@ +invalid image file diff --git a/core/modules/simpletest/files/invalid-img-zero-size.png b/core/modules/simpletest/files/invalid-img-zero-size.png new file mode 100644 index 000000000000..e69de29bb2d1 From 81b7676391f014e9d1cb02b9e7209218fedf50e7 Mon Sep 17 00:00:00 2001 From: xjm Date: Thu, 4 Jan 2018 14:47:20 -0600 Subject: [PATCH 092/232] Issue #2915759 by samuel.mortenson, tedbow, xjm, yoroy: :focus is is hard to see for links in the off-canvas dialog --- core/misc/dialog/off-canvas.base.css | 3 +-- core/misc/dialog/off-canvas.details.css | 1 - core/themes/stable/css/core/dialog/off-canvas.base.css | 3 +-- core/themes/stable/css/core/dialog/off-canvas.details.css | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core/misc/dialog/off-canvas.base.css b/core/misc/dialog/off-canvas.base.css index 4326f61eedcf..5d508120ceb5 100644 --- a/core/misc/dialog/off-canvas.base.css +++ b/core/misc/dialog/off-canvas.base.css @@ -27,8 +27,7 @@ #drupal-off-canvas .link:focus, #drupal-off-canvas a:hover, #drupal-off-canvas .link:hover { - outline: none; - color: #46a0f5; + text-decoration: underline; } #drupal-off-canvas hr { height: 1px; diff --git a/core/misc/dialog/off-canvas.details.css b/core/misc/dialog/off-canvas.details.css index fcd526f0b078..dcaea5ec10d2 100644 --- a/core/misc/dialog/off-canvas.details.css +++ b/core/misc/dialog/off-canvas.details.css @@ -40,7 +40,6 @@ #drupal-off-canvas summary:hover, #drupal-off-canvas summary:focus { background-color: #222; - outline: none; } #drupal-off-canvas details[open] { padding-bottom: 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 4eded024b9bc..aaa2abe3545a 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.base.css +++ b/core/themes/stable/css/core/dialog/off-canvas.base.css @@ -27,8 +27,7 @@ #drupal-off-canvas .link:focus, #drupal-off-canvas a:hover, #drupal-off-canvas .link:hover { - outline: none; - color: #46a0f5; + text-decoration: underline; } #drupal-off-canvas hr { height: 1px; 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 fcd526f0b078..dcaea5ec10d2 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.details.css +++ b/core/themes/stable/css/core/dialog/off-canvas.details.css @@ -40,7 +40,6 @@ #drupal-off-canvas summary:hover, #drupal-off-canvas summary:focus { background-color: #222; - outline: none; } #drupal-off-canvas details[open] { padding-bottom: 10px; From b66af73c82ead96e1a18ae033b0408b606633856 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 5 Jan 2018 07:42:17 +1000 Subject: [PATCH 093/232] Issue #2918500 by tim.plunkett, EclipseGc, tedbow, larowlan, jibran, Wim Leers, phenaproxima, amateescu, borisson_, samuel.mortenson, gaurav.kapoor, KarlShea, hctom, mroycroft, neerajsingh, DamienMcKenna, dsnopek, Xano, TravisCarden, Tim Bozeman: Create a block which can render entity fields --- core/config/schema/core.entity.schema.yml | 58 +-- .../src/Controller/BlockLibraryController.php | 4 + .../src/Plugin/Block/FieldBlock.php | 363 ++++++++++++++++++ .../Plugin/Derivative/FieldBlockDeriver.php | 169 ++++++++ .../FunctionalJavascript/FieldBlockTest.php | 121 ++++++ .../tests/src/Kernel/FieldBlockTest.php | 199 ++++++++++ core/modules/system/system.post_update.php | 7 + 7 files changed, 898 insertions(+), 23 deletions(-) create mode 100644 core/modules/layout_builder/src/Plugin/Block/FieldBlock.php create mode 100644 core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php create mode 100644 core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php create mode 100644 core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index df2a2fd02d97..f58db9ef9197 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -55,29 +55,7 @@ core.entity_view_display.*.*.*: type: sequence label: 'Field formatters' sequence: - type: mapping - label: 'Field formatter' - mapping: - type: - type: string - label: 'Format type machine name' - weight: - type: integer - label: 'Weight' - region: - type: string - label: 'Region' - label: - type: string - label: 'Label setting machine name' - settings: - type: field.formatter.settings.[%parent.type] - label: 'Settings' - third_party_settings: - type: sequence - label: 'Third party settings' - sequence: - type: field.formatter.third_party.[%key] + type: field_formatter.entity_view_display hidden: type: sequence label: 'Field display setting' @@ -85,6 +63,35 @@ core.entity_view_display.*.*.*: type: boolean label: 'Value' +field_formatter: + type: mapping + label: 'Field formatter' + mapping: + type: + type: string + label: 'Format type machine name' + label: + type: string + label: 'Label setting machine name' + settings: + type: field.formatter.settings.[%parent.type] + label: 'Settings' + third_party_settings: + type: sequence + label: 'Third party settings' + sequence: + type: field.formatter.third_party.[%key] + +field_formatter.entity_view_display: + type: field_formatter + mapping: + weight: + type: integer + label: 'Weight' + region: + type: string + label: 'Region' + # Overview configuration information for form mode displays. core.entity_form_display.*.*.*: type: config_entity @@ -362,3 +369,8 @@ field.formatter.settings.entity_reference_label: type: boolean label: 'Link label to the referenced entity' +block.settings.field_block:*:*: + type: block_settings + mapping: + formatter: + type: field_formatter diff --git a/core/modules/block/src/Controller/BlockLibraryController.php b/core/modules/block/src/Controller/BlockLibraryController.php index 79d6eff8cd02..959c642bbaf4 100644 --- a/core/modules/block/src/Controller/BlockLibraryController.php +++ b/core/modules/block/src/Controller/BlockLibraryController.php @@ -105,6 +105,10 @@ public function listBlocks(Request $request, $theme) { $definitions = $this->blockManager->getDefinitionsForContexts($this->contextRepository->getAvailableContexts()); // Order by category, and then by admin label. $definitions = $this->blockManager->getSortedDefinitions($definitions); + // Filter out definitions that are not intended to be placed by the UI. + $definitions = array_filter($definitions, function (array $definition) { + return empty($definition['_block_ui_hidden']); + }); $region = $request->query->get('region'); $weight = $request->query->get('weight'); diff --git a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php new file mode 100644 index 000000000000..11b3e4588c08 --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php @@ -0,0 +1,363 @@ +entityFieldManager = $entity_field_manager; + $this->formatterManager = $formatter_manager; + $this->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); + $this->entityTypeId = $entity_type_id; + $this->fieldName = $field_name; + + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_field.manager'), + $container->get('plugin.manager.field.formatter'), + $container->get('module_handler') + ); + } + + /** + * Gets the entity that has the field. + * + * @return \Drupal\Core\Entity\FieldableEntityInterface + * The entity. + */ + protected function getEntity() { + return $this->getContextValue('entity'); + } + + /** + * {@inheritdoc} + */ + public function build() { + $display_settings = $this->getConfiguration()['formatter']; + $build = $this->getEntity()->get($this->fieldName)->view($display_settings); + CacheableMetadata::createFromObject($this)->applyTo($build); + return $build; + } + + /** + * {@inheritdoc} + */ + protected function blockAccess(AccountInterface $account) { + $entity = $this->getEntity(); + + // First consult the entity. + $access = $entity->access('view', $account, TRUE); + if (!$access->isAllowed()) { + return $access; + } + + // Check that the entity in question has this field. + if (!$entity instanceof FieldableEntityInterface || !$entity->hasField($this->fieldName)) { + return $access->andIf(AccessResult::forbidden()); + } + + // Check field access. + $field = $entity->get($this->fieldName); + $access = $access->andIf($field->access('view', $account, TRUE)); + if (!$access->isAllowed()) { + return $access; + } + + // Check to see if the field has any values. + if ($field->isEmpty()) { + return $access->andIf(AccessResult::forbidden()); + } + return $access; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'formatter' => [ + 'label' => 'above', + 'type' => $this->pluginDefinition['default_formatter'], + 'settings' => [], + 'third_party_settings' => [], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $config = $this->getConfiguration(); + + $form['formatter'] = [ + '#tree' => TRUE, + '#process' => [ + [$this, 'formatterSettingsProcessCallback'], + ], + ]; + $form['formatter']['label'] = [ + '#type' => 'select', + '#title' => $this->t('Label'), + // @todo This is directly copied from + // \Drupal\field_ui\Form\EntityViewDisplayEditForm::getFieldLabelOptions(), + // resolve this in https://www.drupal.org/project/drupal/issues/2933924. + '#options' => [ + 'above' => $this->t('Above'), + 'inline' => $this->t('Inline'), + 'hidden' => '- ' . $this->t('Hidden') . ' -', + 'visually_hidden' => '- ' . $this->t('Visually Hidden') . ' -', + ], + '#default_value' => $config['formatter']['label'], + ]; + + $form['formatter']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Formatter'), + '#options' => $this->getApplicablePluginOptions($this->getFieldDefinition()), + '#required' => TRUE, + '#default_value' => $config['formatter']['type'], + '#ajax' => [ + 'callback' => [static::class, 'formatterSettingsAjaxCallback'], + 'wrapper' => 'formatter-settings-wrapper', + ], + ]; + + // Add the formatter settings to the form via AJAX. + $form['formatter']['settings_wrapper'] = [ + '#prefix' => '
', + '#suffix' => '
', + ]; + + return $form; + } + + /** + * Render API callback: builds the formatter settings elements. + */ + public function formatterSettingsProcessCallback(array &$element, FormStateInterface $form_state, array &$complete_form) { + if ($formatter = $this->getFormatter($element['#parents'], $form_state)) { + $element['settings_wrapper']['settings'] = $formatter->settingsForm($complete_form, $form_state); + $element['settings_wrapper']['settings']['#parents'] = array_merge($element['#parents'], ['settings']); + $element['settings_wrapper']['third_party_settings'] = $this->thirdPartySettingsForm($formatter, $this->getFieldDefinition(), $complete_form, $form_state); + $element['settings_wrapper']['third_party_settings']['#parents'] = array_merge($element['#parents'], ['third_party_settings']); + + // Store the array parents for our element so that we can retrieve the + // formatter settings in our AJAX callback. + $form_state->set('field_block_array_parents', $element['#array_parents']); + } + return $element; + } + + /** + * Adds the formatter third party settings forms. + * + * @param \Drupal\Core\Field\FormatterInterface $plugin + * The formatter. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field definition. + * @param array $form + * The (entire) configuration form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The formatter third party settings form. + */ + protected function thirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) { + $settings_form = []; + // Invoke hook_field_formatter_third_party_settings_form(), keying resulting + // subforms by module name. + foreach ($this->moduleHandler->getImplementations('field_formatter_third_party_settings_form') as $module) { + $settings_form[$module] = $this->moduleHandler->invoke($module, 'field_formatter_third_party_settings_form', [ + $plugin, + $field_definition, + EntityDisplayBase::CUSTOM_MODE, + $form, + $form_state, + ]); + } + return $settings_form; + } + + /** + * Render API callback: gets the layout settings elements. + */ + public static function formatterSettingsAjaxCallback(array $form, FormStateInterface $form_state) { + $formatter_array_parents = $form_state->get('field_block_array_parents'); + return NestedArray::getValue($form, array_merge($formatter_array_parents, ['settings_wrapper'])); + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['formatter'] = $form_state->getValue('formatter'); + } + + /** + * Gets the field definition. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface + * The field definition. + */ + protected function getFieldDefinition() { + if (empty($this->fieldDefinition)) { + $bundle = reset($this->getPluginDefinition()['bundles']); + $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $bundle); + $this->fieldDefinition = $field_definitions[$this->fieldName]; + } + return $this->fieldDefinition; + } + + /** + * Returns an array of applicable formatter options for a field. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field definition. + * + * @return array + * An array of applicable formatter options. + * + * @see \Drupal\field_ui\Form\EntityDisplayFormBase::getApplicablePluginOptions() + */ + protected function getApplicablePluginOptions(FieldDefinitionInterface $field_definition) { + $options = $this->formatterManager->getOptions($field_definition->getType()); + $applicable_options = []; + foreach ($options as $option => $label) { + $plugin_class = DefaultFactory::getPluginClass($option, $this->formatterManager->getDefinition($option)); + if ($plugin_class::isApplicable($field_definition)) { + $applicable_options[$option] = $label; + } + } + return $applicable_options; + } + + /** + * Gets the formatter object. + * + * @param array $parents + * The #parents of the element representing the formatter. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Field\FormatterInterface + * The formatter object. + */ + protected function getFormatter(array $parents, FormStateInterface $form_state) { + // Use the processed values, if available. + $configuration = NestedArray::getValue($form_state->getValues(), $parents); + if (!$configuration) { + // Next check the raw user input. + $configuration = NestedArray::getValue($form_state->getUserInput(), $parents); + if (!$configuration) { + // If no user input exists, use the default values. + $configuration = $this->getConfiguration()['formatter']; + } + } + + return $this->formatterManager->getInstance([ + 'configuration' => $configuration, + 'field_definition' => $this->getFieldDefinition(), + 'view_mode' => EntityDisplayBase::CUSTOM_MODE, + 'prepare' => TRUE, + ]); + } + +} diff --git a/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php new file mode 100644 index 000000000000..6a39f4a17c4c --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php @@ -0,0 +1,169 @@ +entityTypeRepository = $entity_type_repository; + $this->entityFieldManager = $entity_field_manager; + $this->fieldTypeManager = $field_type_manager; + $this->formatterManager = $formatter_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.repository'), + $container->get('entity_field.manager'), + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.formatter') + ); + } + + /** + * {@inheritdoc} + */ + 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]; + + // Skip fields without any formatters. + $options = $this->formatterManager->getOptions($field_storage_definition->getType()); + 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']; + } + + // 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; + } + $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']); + + // 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['_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/tests/src/FunctionalJavascript/FieldBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php new file mode 100644 index 000000000000..e7ae850d42bb --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php @@ -0,0 +1,121 @@ + 'field_date', + 'entity_type' => 'user', + 'type' => 'datetime', + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'user', + 'label' => 'Date field', + ]); + $field->save(); + + $user = $this->drupalCreateUser([ + 'administer blocks', + 'access administration pages', + ]); + $user->field_date = '1978-11-19T05:00:00'; + $user->save(); + $this->drupalLogin($user); + } + + /** + * Tests configuring a field block for a user field. + */ + public function testFieldBlock() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + // Assert that the field value is not displayed. + $this->drupalGet('admin'); + $assert_session->pageTextNotContains('Sunday, November 19, 1978 - 16:00'); + + $this->drupalGet('admin/structure/block'); + $this->clickLink('Place block'); + $assert_session->assertWaitOnAjaxRequest(); + + // Ensure that fields without any formatters are not available. + $assert_session->pageTextNotContains('Password'); + // Ensure that non-display-configurable fields are not available. + $assert_session->pageTextNotContains('Initial email'); + + $assert_session->pageTextContains('Date field'); + $block_url = 'admin/structure/block/add/field_block%3Auser%3Afield_date/classy'; + $assert_session->linkByHrefExists($block_url); + + $this->drupalGet($block_url); + $page->fillField('region', 'content'); + + // Assert the default formatter configuration. + $assert_session->fieldValueEquals('settings[formatter][type]', 'datetime_default'); + $assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium'); + + // Change the formatter. + $page->selectFieldOption('settings[formatter][type]', 'datetime_time_ago'); + $assert_session->assertWaitOnAjaxRequest(); + // Changing the formatter removes the old settings and introduces new ones. + $assert_session->fieldNotExists('settings[formatter][settings][format_type]'); + $assert_session->fieldExists('settings[formatter][settings][granularity]'); + $page->pressButton('Save block'); + $assert_session->pageTextContains('The block configuration has been saved.'); + + // Configure the block and change the formatter again. + $this->clickLink('Configure'); + $page->selectFieldOption('settings[formatter][type]', 'datetime_default'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium'); + $page->selectFieldOption('settings[formatter][settings][format_type]', 'long'); + + $page->pressButton('Save block'); + $assert_session->pageTextContains('The block configuration has been saved.'); + + // Assert that the field value is updated. + $this->clickLink('Configure'); + $assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'long'); + + // Assert that the field block is configured as expected. + $expected = [ + 'label' => 'above', + 'type' => 'datetime_default', + 'settings' => [ + 'format_type' => 'long', + 'timezone_override' => '', + ], + 'third_party_settings' => [], + ]; + $config = $this->container->get('config.factory')->get('block.block.datefield'); + $this->assertEquals($expected, $config->get('settings.formatter')); + + // Assert that the block is displaying the user field. + $this->drupalGet('admin'); + $assert_session->pageTextContains('Sunday, November 19, 1978 - 16:00'); + } + +} diff --git a/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php new file mode 100644 index 000000000000..a118cf5bef8a --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php @@ -0,0 +1,199 @@ +prophesize(FieldableEntityInterface::class); + $block = $this->getTestBlock($entity); + + $account = $this->prophesize(AccountInterface::class); + $entity->access('view', $account->reveal(), TRUE)->willReturn($entity_access); + $entity->hasField()->shouldNotBeCalled(); + + $access = $block->access($account->reveal(), TRUE); + $this->assertSame($expected, $access->isAllowed()); + } + + /** + * Provides test data for ::testBlockAccessEntityNotAllowed(). + */ + public function providerTestBlockAccessNotAllowed() { + $data = []; + $data['entity_forbidden'] = [ + FALSE, + AccessResult::forbidden(), + ]; + $data['entity_neutral'] = [ + FALSE, + AccessResult::neutral(), + ]; + return $data; + } + + /** + * Tests unfieldable entity. + * + * @covers ::blockAccess + */ + public function testBlockAccessEntityAllowedNotFieldable() { + $entity = $this->prophesize(EntityInterface::class); + $block = $this->getTestBlock($entity); + + $account = $this->prophesize(AccountInterface::class); + $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed()); + + $access = $block->access($account->reveal(), TRUE); + $this->assertSame(FALSE, $access->isAllowed()); + } + + /** + * Tests fieldable entity without a particular field. + * + * @covers ::blockAccess + */ + public function testBlockAccessEntityAllowedNoField() { + $entity = $this->prophesize(FieldableEntityInterface::class); + $block = $this->getTestBlock($entity); + + $account = $this->prophesize(AccountInterface::class); + $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed()); + $entity->hasField('the_field_name')->willReturn(FALSE); + $entity->get('the_field_name')->shouldNotBeCalled(); + + $access = $block->access($account->reveal(), TRUE); + $this->assertSame(FALSE, $access->isAllowed()); + } + + /** + * Tests field access. + * + * @covers ::blockAccess + * @dataProvider providerTestBlockAccessNotAllowed + */ + public function testBlockAccessEntityAllowedFieldNotAllowed($expected, $field_access) { + $entity = $this->prophesize(FieldableEntityInterface::class); + $block = $this->getTestBlock($entity); + + $account = $this->prophesize(AccountInterface::class); + $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed()); + $entity->hasField('the_field_name')->willReturn(TRUE); + $field = $this->prophesize(FieldItemListInterface::class); + $entity->get('the_field_name')->willReturn($field->reveal()); + + $field->access('view', $account->reveal(), TRUE)->willReturn($field_access); + $field->isEmpty()->shouldNotBeCalled(); + + $access = $block->access($account->reveal(), TRUE); + $this->assertSame($expected, $access->isAllowed()); + } + + /** + * Tests populated vs empty build. + * + * @covers ::blockAccess + * @covers ::build + * @dataProvider providerTestBlockAccessEntityAllowedFieldHasValue + */ + public function testBlockAccessEntityAllowedFieldHasValue($expected, $is_empty) { + $entity = $this->prophesize(FieldableEntityInterface::class); + $block = $this->getTestBlock($entity); + + $account = $this->prophesize(AccountInterface::class); + $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed()); + $entity->hasField('the_field_name')->willReturn(TRUE); + $field = $this->prophesize(FieldItemListInterface::class); + $entity->get('the_field_name')->willReturn($field->reveal()); + + $field->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed()); + $field->isEmpty()->willReturn($is_empty)->shouldBeCalled(); + + $access = $block->access($account->reveal(), TRUE); + $this->assertSame($expected, $access->isAllowed()); + } + + /** + * Provides test data for ::testBlockAccessEntityAllowedFieldHasValue(). + */ + public function providerTestBlockAccessEntityAllowedFieldHasValue() { + $data = []; + $data['empty'] = [ + FALSE, + TRUE, + ]; + $data['populated'] = [ + TRUE, + FALSE, + ]; + return $data; + } + + /** + * Instantiates a block for testing. + * + * @param \Prophecy\Prophecy\ProphecyInterface $entity_prophecy + * An entity prophecy for use as an entity context value. + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * + * @return \Drupal\layout_builder\Plugin\Block\FieldBlock + * The block to test. + */ + protected function getTestBlock(ProphecyInterface $entity_prophecy, array $configuration = [], array $plugin_definition = []) { + $entity_prophecy->getCacheContexts()->willReturn([]); + $entity_prophecy->getCacheTags()->willReturn([]); + $entity_prophecy->getCacheMaxAge()->willReturn(0); + + $plugin_definition += [ + 'provider' => 'test', + 'default_formatter' => '', + 'category' => 'Test', + 'admin_label' => 'Test Block', + 'bundles' => ['entity_test'], + 'context' => [ + 'entity' => new ContextDefinition('entity:entity_test', 'Test', TRUE), + ], + ]; + $entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class); + $formatter_manager = $this->prophesize(FormatterPluginManager::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + $block = new FieldBlock( + $configuration, + 'field_block:entity_test:the_field_name', + $plugin_definition, + $entity_field_manager->reveal(), + $formatter_manager->reveal(), + $module_handler->reveal() + ); + $block->setContextValue('entity', $entity_prophecy->reveal()); + return $block; + } + +} diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index 36039dff0d3b..eb11a19f93fc 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -81,3 +81,10 @@ function system_post_update_classy_message_library() { function system_post_update_field_type_plugins() { // Empty post-update hook. } + +/** + * Clear caches due to schema changes in core.entity.schema.yml. + */ +function system_post_update_field_formatter_entity_schema() { + // Empty post-update hook. +} From 32cc90b3c1fb7139401870cfcf81100670d9c013 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 5 Jan 2018 08:09:08 +1000 Subject: [PATCH 094/232] Issue #2933424 by David_Rothstein, harsha012, jhodgdon, vaplas: English-specific links to php.net shouldn't be used in non-translatable strings --- core/core.api.php | 2 +- core/install.php | 2 +- core/lib/Drupal/Component/Datetime/DateTimePlus.php | 2 +- core/lib/Drupal/Component/Serialization/YamlPecl.php | 2 +- core/lib/Drupal/Core/Datetime/DrupalDateTime.php | 2 +- core/lib/Drupal/Core/Test/TestStatus.php | 2 +- core/modules/datetime/src/DateTimeComputed.php | 2 +- .../src/Normalizer/TimeStampItemNormalizerTrait.php | 2 +- core/scripts/run-tests.sh | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) mode change 100755 => 100644 core/scripts/run-tests.sh diff --git a/core/core.api.php b/core/core.api.php index 0be1870b177e..3b0765104ba5 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -1584,7 +1584,7 @@ * Introduction to namespaces * * PHP classes, interfaces, and traits in Drupal are - * @link http://php.net/manual/en/language.namespaces.rationale.php namespaced. @endlink + * @link http://php.net/manual/language.namespaces.rationale.php namespaced. @endlink * See the * @link oo_conventions Objected-oriented programming conventions @endlink * for more information. diff --git a/core/install.php b/core/install.php index 6bdd6f8c3156..c88ffeedaacd 100644 --- a/core/install.php +++ b/core/install.php @@ -35,7 +35,7 @@ // If OPCache is in use, ensure opcache.save_comments is enabled. if (OpCodeCache::isEnabled() && !ini_get('opcache.save_comments')) { - print 'Systems with OPcache installed must have opcache.save_comments enabled.'; + print 'Systems with OPcache installed must have opcache.save_comments enabled.'; exit(); } diff --git a/core/lib/Drupal/Component/Datetime/DateTimePlus.php b/core/lib/Drupal/Component/Datetime/DateTimePlus.php index d0e4faf12681..11611686728b 100644 --- a/core/lib/Drupal/Component/Datetime/DateTimePlus.php +++ b/core/lib/Drupal/Component/Datetime/DateTimePlus.php @@ -280,7 +280,7 @@ public static function createFromFormat($format, $time, $timezone = NULL, $setti * parameter and the current timezone are ignored when the $time parameter * either is a UNIX timestamp (e.g. @946684800) or specifies a timezone * (e.g. 2010-01-28T15:00:00+02:00). - * @see http://php.net/manual/en/datetime.construct.php + * @see http://php.net/manual/datetime.construct.php * @param array $settings * (optional) Keyed array of settings. Defaults to empty array. * - langcode: (optional) String two letter language code used to control diff --git a/core/lib/Drupal/Component/Serialization/YamlPecl.php b/core/lib/Drupal/Component/Serialization/YamlPecl.php index a01f980fd78f..8200f49ff03e 100644 --- a/core/lib/Drupal/Component/Serialization/YamlPecl.php +++ b/core/lib/Drupal/Component/Serialization/YamlPecl.php @@ -47,7 +47,7 @@ public static function decode($raw) { // and then restore it after decoding has occurred. This allows us to turn // parsing errors into a throwable exception. // @see Drupal\Component\Serialization\Exception\InvalidDataTypeException - // @see http://php.net/manual/en/class.errorexception.php + // @see http://php.net/manual/class.errorexception.php set_error_handler([__CLASS__, 'errorHandler']); $ndocs = 0; $data = yaml_parse($raw, 0, $ndocs, [ diff --git a/core/lib/Drupal/Core/Datetime/DrupalDateTime.php b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php index bb7b821bc30d..f921b45b464f 100644 --- a/core/lib/Drupal/Core/Datetime/DrupalDateTime.php +++ b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php @@ -38,7 +38,7 @@ class DrupalDateTime extends DateTimePlus { * timezone are ignored when the $time parameter either is a UNIX timestamp * (e.g. @946684800) or specifies a timezone * (e.g. 2010-01-28T15:00:00+02:00). - * @see http://php.net/manual/en/datetime.construct.php + * @see http://php.net/manual/datetime.construct.php * @param array $settings * - validate_format: (optional) Boolean choice to validate the * created date using the input format. The format used in diff --git a/core/lib/Drupal/Core/Test/TestStatus.php b/core/lib/Drupal/Core/Test/TestStatus.php index b70dcdd1d8ad..79459e4eb06f 100644 --- a/core/lib/Drupal/Core/Test/TestStatus.php +++ b/core/lib/Drupal/Core/Test/TestStatus.php @@ -35,7 +35,7 @@ class TestStatus { * the returned value could be as high as 127. Since that's the case, this * constant should be used for range comparisons, and not just for equality. * - * @see http://php.net/manual/en/pcntl.constants.php + * @see http://php.net/manual/pcntl.constants.php */ const SYSTEM = 3; diff --git a/core/modules/datetime/src/DateTimeComputed.php b/core/modules/datetime/src/DateTimeComputed.php index 73fb11786270..16f664be5283 100644 --- a/core/modules/datetime/src/DateTimeComputed.php +++ b/core/modules/datetime/src/DateTimeComputed.php @@ -58,7 +58,7 @@ public function getValue() { // that the local date portion is the same, across nearly all time // zones. // @see \Drupal\Component\Datetime\DateTimePlus::setDefaultDateTime() - // @see http://php.net/manual/en/datetime.createfromformat.php + // @see http://php.net/manual/datetime.createfromformat.php if ($datetime_type === DateTimeItem::DATETIME_TYPE_DATE) { $this->date->setDefaultDateTime(); } diff --git a/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php index 78f6030e5710..1ad0d8e66be7 100644 --- a/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php +++ b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php @@ -20,7 +20,7 @@ trait TimeStampItemNormalizerTrait { * * @var string[] * - * @see http://php.net/manual/en/datetime.createfromformat.php + * @see http://php.net/manual/datetime.createfromformat.php */ protected $allowedFormats = [ 'UNIX timestamp' => 'U', diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh old mode 100755 new mode 100644 index 407d87a34f6c..ae33bb5410f7 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -1424,7 +1424,7 @@ function simpletest_script_color_code($status) { * string in $array would be identical to $string by changing 1/4 or fewer of * its characters. * - * @see http://php.net/manual/en/function.levenshtein.php + * @see http://php.net/manual/function.levenshtein.php */ function simpletest_script_print_alternatives($string, $array, $degree = 4) { $alternatives = array(); From 1911dc40560509a367a0a58a028ecfb9d9542cc0 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 5 Jan 2018 08:12:40 +1000 Subject: [PATCH 095/232] Issue #2862741 by masipila: Add documentation to ComponentEntityDisplayBase destination plugin --- .../migrate/destination/ComponentEntityDisplayBase.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/modules/migrate/src/Plugin/migrate/destination/ComponentEntityDisplayBase.php b/core/modules/migrate/src/Plugin/migrate/destination/ComponentEntityDisplayBase.php index eaf3b54da6f9..1c94214fd5a7 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/ComponentEntityDisplayBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/ComponentEntityDisplayBase.php @@ -6,7 +6,14 @@ use Drupal\migrate\Row; /** - * Defines the base abstract class for component entity display. + * Provides a destination plugin for migrating entity display components. + * + * Display modes provide different presentations for viewing ('view modes') or + * editing ('form modes') content. This destination plugin is an abstract base + * class for migrating fields and other components into view and form modes. + * + * @see \Drupal\migrate\Plugin\migrate\destination\PerComponentEntityDisplay + * @see \Drupal\migrate\Plugin\migrate\destination\PerComponentEntityFormDisplay */ abstract class ComponentEntityDisplayBase extends DestinationBase { From be569c75d76d53825a3e804c11b8d42c44d677cc Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 5 Jan 2018 08:29:33 +1000 Subject: [PATCH 096/232] Issue #2859381 by Manuel Garcia, Sam152, mstef, jhedstrom: Broken/missing handler for Moderation state field --- .../content_moderation/src/ViewsData.php | 12 +- ...st_content_moderation_field_state_test.yml | 205 ++++++++++++++++++ .../src/Kernel/ViewsDataIntegrationTest.php | 23 ++ 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_field_state_test.yml diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php index 57d6d7616074..9ade858c380b 100644 --- a/core/modules/content_moderation/src/ViewsData.php +++ b/core/modules/content_moderation/src/ViewsData.php @@ -78,7 +78,11 @@ public function getViewsData() { ], ], ], - 'field' => ['default_formatter' => 'content_moderation_state'], + 'field' => [ + 'id' => 'field', + 'default_formatter' => 'content_moderation_state', + 'field_name' => 'moderation_state', + ], ]; $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); @@ -97,7 +101,11 @@ public function getViewsData() { ], ], ], - 'field' => ['default_formatter' => 'content_moderation_state'], + 'field' => [ + 'id' => 'field', + 'default_formatter' => 'content_moderation_state', + 'field_name' => 'moderation_state', + ], ]; } diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_field_state_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_field_state_test.yml new file mode 100644 index 000000000000..c0f511c66dd1 --- /dev/null +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_field_state_test.yml @@ -0,0 +1,205 @@ +langcode: en +status: true +dependencies: + module: + - content_moderation + - node + - user +id: test_content_moderation_field_state_test +label: test_content_moderation_field_state_test +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: some + options: + items_per_page: 10 + offset: 0 + style: + type: default + row: + type: fields + options: + default_field_elements: true + inline: { } + separator: '' + hide_empty: false + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + 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 + moderation_state: + id: moderation_state + table: node_field_data + field: moderation_state + 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 + click_sort_column: value + type: content_moderation_state + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + plugin_id: field + filters: { } + sorts: { } + title: test_content_moderation_field_state_test + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test-content-moderation-field-state-test + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php index 125d68fbd2b0..a1b396fb453d 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php @@ -124,4 +124,27 @@ public function testContentModerationStateBaseJoin() { $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1', 'moderation_state_2' => 'moderation_state_2']); } + /** + * Tests the content moderation state views field. + */ + public function testContentModerationStateField() { + $node = Node::create([ + 'type' => 'page', + 'title' => 'Test title', + ]); + $node->moderation_state->value = 'published'; + $node->save(); + + $view = Views::getView('test_content_moderation_field_state_test'); + $view->execute(); + + $expected_result = [ + [ + 'title' => 'Test title', + 'moderation_state' => 'published', + ], + ]; + $this->assertIdenticalResultset($view, $expected_result, ['title' => 'title', 'moderation_state' => 'moderation_state']); + } + } From 2971ea9d10bf0288e5b94e62296efb8913a79b11 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 5 Jan 2018 17:11:04 +1000 Subject: [PATCH 097/232] Issue #2934520 by tstoeckler: Avoid information disclosure by timing attack in EntityResource::patch() --- core/modules/rest/src/Plugin/rest/resource/EntityResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 4b6febcbe84c..8b1c2fa979f7 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -288,7 +288,7 @@ protected function checkPatchFieldAccess(FieldItemListInterface $original_field, // the user has no legitimate way of knowing the current value of fields // that they are not allowed to view, and we must not make the presence or // absence of a 403 response a way to find that out. - if ($original_field->equals($received_field) && $original_field->access('view')) { + if ($original_field->access('view') && $original_field->equals($received_field)) { return FALSE; } From 3b06c57e4f04e56e9e2da7d02c1befd4bf5f98df Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Fri, 5 Jan 2018 09:34:59 +0000 Subject: [PATCH 098/232] Issue #2866812 by idebr: Update stylelint rule function-name-case to be consistent with Drupal's CSS standards --- core/.stylelintrc.json | 1 - core/themes/seven/css/components/jquery.ui/theme.css | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/.stylelintrc.json b/core/.stylelintrc.json index 832748d391a2..e3a52b04849b 100644 --- a/core/.stylelintrc.json +++ b/core/.stylelintrc.json @@ -16,7 +16,6 @@ "declaration-colon-space-after": null, "function-comma-space-after": null, "function-linear-gradient-no-nonstandard-direction": null, - "function-name-case": null, "function-whitespace-after": null, "length-zero-no-unit": null, "no-empty-source": null, diff --git a/core/themes/seven/css/components/jquery.ui/theme.css b/core/themes/seven/css/components/jquery.ui/theme.css index dd81ec26a5dc..63d23efc78ae 100644 --- a/core/themes/seven/css/components/jquery.ui/theme.css +++ b/core/themes/seven/css/components/jquery.ui/theme.css @@ -43,12 +43,12 @@ .ui-state-disabled, .ui-widget-content .ui-state-disabled { opacity: .35; - filter: Alpha(Opacity=35); + filter: alpha(Opacity=35); } .ui-priority-secondary, .ui-widget-content .ui-priority-secondary { opacity: .7; - filter: Alpha(Opacity=70); + filter: alpha(Opacity=70); } /** @@ -332,7 +332,7 @@ .ui-widget-overlay { background: #000; opacity: .70; - filter: Alpha(Opacity=70); + filter: alpha(Opacity=70); } /** From d540d6426f1583ef9679f9577899f315760be27c Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Fri, 5 Jan 2018 09:37:45 +0000 Subject: [PATCH 099/232] Issue #2866799 by cwells: Update stylelint rule at-rule-empty-line-before to be consistent with Drupal's CSS standards --- core/.stylelintrc.json | 1 - core/modules/color/css/color.admin.css | 1 + core/modules/locale/css/locale.admin.css | 1 + core/modules/toolbar/css/toolbar.module.css | 3 +++ core/modules/tour/css/tour.module.css | 1 + core/modules/views_ui/css/views_ui.admin.theme.css | 1 + core/themes/bartik/css/components/featured-bottom.css | 2 ++ core/themes/bartik/css/components/field.css | 1 + core/themes/bartik/css/components/header.css | 3 +++ core/themes/bartik/css/components/sidebar.css | 1 + core/themes/bartik/css/components/site-branding.css | 3 +++ core/themes/bartik/css/components/site-footer.css | 3 +++ core/themes/bartik/css/components/table.css | 1 + core/themes/bartik/css/components/tabs.css | 2 ++ core/themes/bartik/css/layout.css | 1 + core/themes/bartik/css/maintenance-page.css | 2 ++ core/themes/classy/css/components/dialog.css | 1 + core/themes/classy/css/components/progress.css | 2 ++ core/themes/seven/css/components/dialog.css | 1 + core/themes/seven/css/components/tables.css | 1 + core/themes/seven/css/theme/maintenance-page.css | 1 + core/themes/stable/css/color/color.admin.css | 1 + core/themes/stable/css/locale/locale.admin.css | 1 + core/themes/stable/css/toolbar/toolbar.module.css | 3 +++ core/themes/stable/css/tour/tour.module.css | 1 + core/themes/stable/css/views_ui/views_ui.admin.theme.css | 1 + 26 files changed, 39 insertions(+), 1 deletion(-) diff --git a/core/.stylelintrc.json b/core/.stylelintrc.json index e3a52b04849b..c23bd89dea2c 100644 --- a/core/.stylelintrc.json +++ b/core/.stylelintrc.json @@ -4,7 +4,6 @@ "stylelint-no-browser-hacks/lib" ], "rules": { - "at-rule-empty-line-before": null, "block-no-empty": null, "color-hex-case": null, "color-hex-length": null, diff --git a/core/modules/color/css/color.admin.css b/core/modules/color/css/color.admin.css index 69311c2a885c..c7c1fd78198b 100644 --- a/core/modules/color/css/color.admin.css +++ b/core/modules/color/css/color.admin.css @@ -143,6 +143,7 @@ button.is-unlocked, .js[dir="rtl"] .color-preview { float: right; } + @media screen and (max-width: 30em) { /* 480px */ .color-form .color-preview-sidebar, .color-form .color-preview-content { diff --git a/core/modules/locale/css/locale.admin.css b/core/modules/locale/css/locale.admin.css index aa83c8bfb012..131540464f4e 100644 --- a/core/modules/locale/css/locale.admin.css +++ b/core/modules/locale/css/locale.admin.css @@ -125,6 +125,7 @@ margin: 0 0 0.25em 1.5em; padding: 0; } + @media screen and (max-width: 40em) { #locale-translation-status-form th.title { width: 20%; diff --git a/core/modules/toolbar/css/toolbar.module.css b/core/modules/toolbar/css/toolbar.module.css index 14d0cf0e1211..d7bfc904c5cf 100644 --- a/core/modules/toolbar/css/toolbar.module.css +++ b/core/modules/toolbar/css/toolbar.module.css @@ -16,6 +16,7 @@ padding: 0; vertical-align: baseline; } + @media print { #toolbar-administration { display: none; @@ -238,6 +239,7 @@ body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-left: 240px; /* LTR */ margin-left: 15rem; /* LTR */ } + @media print { body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-left: 0; @@ -249,6 +251,7 @@ body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-right: 240px; margin-right: 15rem; } + @media print { [dir="rtl"] body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-right: 0; diff --git a/core/modules/tour/css/tour.module.css b/core/modules/tour/css/tour.module.css index 18d8aa04cad4..035dfd0c3c89 100644 --- a/core/modules/tour/css/tour.module.css +++ b/core/modules/tour/css/tour.module.css @@ -32,6 +32,7 @@ top: 0; left: 0; } + @media only screen and (max-width: 767px) { .joyride-tip-guide { width: 85%; 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 30616f05ccf3..56223069cef5 100644 --- a/core/modules/views_ui/css/views_ui.admin.theme.css +++ b/core/modules/views_ui/css/views_ui.admin.theme.css @@ -483,6 +483,7 @@ td.group-title { .view-preview-form .form-actions { vertical-align: top; } + @media screen and (min-width: 45em) { /* 720px */ .view-preview-form .form-type-textfield .description { white-space: nowrap; diff --git a/core/themes/bartik/css/components/featured-bottom.css b/core/themes/bartik/css/components/featured-bottom.css index b9bb6b40e0b6..56e5a513c77b 100644 --- a/core/themes/bartik/css/components/featured-bottom.css +++ b/core/themes/bartik/css/components/featured-bottom.css @@ -10,6 +10,7 @@ .featured-bottom .region { padding: 0 20px; } + @media all and (min-width: 560px) { .featured-bottom .region { float: left; /* LTR */ @@ -22,6 +23,7 @@ float: right; } } + @media all and (min-width: 851px) { .featured-bottom .region { padding: 0 20px; diff --git a/core/themes/bartik/css/components/field.css b/core/themes/bartik/css/components/field.css index a2a16e0a1fa1..a358e1af2f39 100644 --- a/core/themes/bartik/css/components/field.css +++ b/core/themes/bartik/css/components/field.css @@ -42,6 +42,7 @@ padding: 0 0 0 1em; float: right; } + @media all and (min-width: 560px) { .node .field--type-image { float: left; /* LTR */ diff --git a/core/themes/bartik/css/components/header.css b/core/themes/bartik/css/components/header.css index 52ab1c6239f5..89701962e4eb 100644 --- a/core/themes/bartik/css/components/header.css +++ b/core/themes/bartik/css/components/header.css @@ -11,6 +11,7 @@ .region-header .site-branding { margin-top: 0.429em; } + @media all and (min-width: 461px) { .region-header .block { float: right; /* LTR */ @@ -27,6 +28,7 @@ float: right; } } + @media screen and (max-width: 460px) { .region-header { padding-bottom: 0.357em; @@ -38,6 +40,7 @@ margin: 0 0 1em; clear: right; } + @media all and (min-width: 901px) { .region-header .block:not(.site-branding) { margin: 1.167em 0 1em; diff --git a/core/themes/bartik/css/components/sidebar.css b/core/themes/bartik/css/components/sidebar.css index 8b56352456cd..e59aa3090028 100644 --- a/core/themes/bartik/css/components/sidebar.css +++ b/core/themes/bartik/css/components/sidebar.css @@ -13,6 +13,7 @@ width: 100%; } } + @media all and (min-width: 851px) { .layout-one-sidebar .sidebar { width: 25%; diff --git a/core/themes/bartik/css/components/site-branding.css b/core/themes/bartik/css/components/site-branding.css index fda5d9e6ed06..4d0b389e2eb6 100644 --- a/core/themes/bartik/css/components/site-branding.css +++ b/core/themes/bartik/css/components/site-branding.css @@ -16,11 +16,13 @@ display: inline-block; vertical-align: top; } + @media all and (min-width: 461px) { .site-branding__text { margin-bottom: 1.857em; } } + @media all and (min-width: 901px) { .site-branding__text { padding: 1.286em 0 0; @@ -31,6 +33,7 @@ color: #686868; line-height: 1; } + @media all and (min-width: 901px) { .site-branding__name { font-size: 1.821em; diff --git a/core/themes/bartik/css/components/site-footer.css b/core/themes/bartik/css/components/site-footer.css index d99459f62fd8..1b7d67c0c1e2 100644 --- a/core/themes/bartik/css/components/site-footer.css +++ b/core/themes/bartik/css/components/site-footer.css @@ -10,6 +10,7 @@ .site-footer .layout-container { padding: 0 15px; } + @media all and (min-width: 560px) { .site-footer__top .region { float: left; /* LTR */ @@ -20,6 +21,7 @@ float: right; } } + @media all and (min-width: 560px) and (max-width: 850px) { .site-footer .region { box-sizing: border-box; @@ -42,6 +44,7 @@ clear: both; } } + @media all and (min-width: 851px) { .site-footer__top .region { width: 24%; diff --git a/core/themes/bartik/css/components/table.css b/core/themes/bartik/css/components/table.css index ac8898cd2590..ea8498ac89b4 100644 --- a/core/themes/bartik/css/components/table.css +++ b/core/themes/bartik/css/components/table.css @@ -68,6 +68,7 @@ tr th { display: none; } } + @media screen and (max-width: 60em) { /* 920px */ th.priority-low, td.priority-low { diff --git a/core/themes/bartik/css/components/tabs.css b/core/themes/bartik/css/components/tabs.css index 6f232fee483e..499b99eef656 100644 --- a/core/themes/bartik/css/components/tabs.css +++ b/core/themes/bartik/css/components/tabs.css @@ -24,6 +24,7 @@ div.tabs { background-color: #ffffff; border: 1px solid #bbb; } + @media screen and (max-width: 37.5em) { /* 600px */ .tabs ul.primary { border-bottom: 1px solid #bbb; @@ -39,6 +40,7 @@ div.tabs { border-bottom: none; } } + @media screen and (min-width: 37.5em) { /* 600px */ .tabs ul.primary { border-collapse: collapse; diff --git a/core/themes/bartik/css/layout.css b/core/themes/bartik/css/layout.css index fe455835d1a6..ab85ca82701b 100644 --- a/core/themes/bartik/css/layout.css +++ b/core/themes/bartik/css/layout.css @@ -12,6 +12,7 @@ margin-right: auto; box-sizing: border-box; } + @media all and (min-width: 851px) { .layout-container { max-width: 1290px; diff --git a/core/themes/bartik/css/maintenance-page.css b/core/themes/bartik/css/maintenance-page.css index eb1a3d0c83ab..c5f513844de7 100644 --- a/core/themes/bartik/css/maintenance-page.css +++ b/core/themes/bartik/css/maintenance-page.css @@ -53,6 +53,7 @@ body.maintenance-page { line-height: 1em; margin-top: 0; } + @media all and (min-width: 800px) { .maintenance-page #page-wrapper { width: 800px; @@ -62,6 +63,7 @@ body.maintenance-page { width: 700px; } } + @media all and (min-width: 600px) { /* @TODO find the proper breakpoint */ .maintenance-page #page { margin: 20px 40px 40px; diff --git a/core/themes/classy/css/components/dialog.css b/core/themes/classy/css/components/dialog.css index 179310836179..ed9fbb030e80 100644 --- a/core/themes/classy/css/components/dialog.css +++ b/core/themes/classy/css/components/dialog.css @@ -12,6 +12,7 @@ border: solid 1px #ccc; padding: 0; } + @media all and (max-width: 48em) { /* 768px */ .ui-dialog { width: 92% !important; diff --git a/core/themes/classy/css/components/progress.css b/core/themes/classy/css/components/progress.css index 60bc123c96bd..39a055dd1abb 100644 --- a/core/themes/classy/css/components/progress.css +++ b/core/themes/classy/css/components/progress.css @@ -51,10 +51,12 @@ 0% { background-position: 0 0, 0 0; } 100% { background-position: 0 0, -80px 0; } } + @-ms-keyframes animate-stripes { 0% { background-position: 0 0, 0 0; } 100% { background-position: 0 0, -80px 0; } } + @keyframes animate-stripes { 0% { background-position: 0 0, 0 0; } 100% { background-position: 0 0, -80px 0; } diff --git a/core/themes/seven/css/components/dialog.css b/core/themes/seven/css/components/dialog.css index 6d018208d176..b4459d1a6ea8 100644 --- a/core/themes/seven/css/components/dialog.css +++ b/core/themes/seven/css/components/dialog.css @@ -9,6 +9,7 @@ z-index: 1260; padding: 0; } + @media all and (max-width: 48em) { /* 768px */ .ui-dialog { min-width: 92%; diff --git a/core/themes/seven/css/components/tables.css b/core/themes/seven/css/components/tables.css index 5464456edd60..e51ac9f1971e 100644 --- a/core/themes/seven/css/components/tables.css +++ b/core/themes/seven/css/components/tables.css @@ -143,6 +143,7 @@ th.select-all { display: none; } } + @media screen and (max-width: 60em) { /* 920px */ th.priority-low, td.priority-low { diff --git a/core/themes/seven/css/theme/maintenance-page.css b/core/themes/seven/css/theme/maintenance-page.css index e2875e3c1d41..9b96894907b7 100644 --- a/core/themes/seven/css/theme/maintenance-page.css +++ b/core/themes/seven/css/theme/maintenance-page.css @@ -110,6 +110,7 @@ display: table; clear: both; } + @media all and (max-width: 48em) { /* 768px */ .layout-container { margin: 1.25em; diff --git a/core/themes/stable/css/color/color.admin.css b/core/themes/stable/css/color/color.admin.css index 2efe6fd4f868..f9ccd9abe391 100644 --- a/core/themes/stable/css/color/color.admin.css +++ b/core/themes/stable/css/color/color.admin.css @@ -143,6 +143,7 @@ button.is-unlocked, .js[dir="rtl"] .color-preview { float: right; } + @media screen and (max-width: 30em) { /* 480px */ .color-form .color-preview-sidebar, .color-form .color-preview-content { diff --git a/core/themes/stable/css/locale/locale.admin.css b/core/themes/stable/css/locale/locale.admin.css index d55b4297aa02..985a1ade3b1c 100644 --- a/core/themes/stable/css/locale/locale.admin.css +++ b/core/themes/stable/css/locale/locale.admin.css @@ -125,6 +125,7 @@ margin: 0 0 0.25em 1.5em; padding: 0; } + @media screen and (max-width: 40em) { #locale-translation-status-form th.title { width: 20%; diff --git a/core/themes/stable/css/toolbar/toolbar.module.css b/core/themes/stable/css/toolbar/toolbar.module.css index f50d4cb4221b..8c9466720aa0 100644 --- a/core/themes/stable/css/toolbar/toolbar.module.css +++ b/core/themes/stable/css/toolbar/toolbar.module.css @@ -16,6 +16,7 @@ padding: 0; vertical-align: baseline; } + @media print { #toolbar-administration { display: none; @@ -238,6 +239,7 @@ body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-left: 240px; /* LTR */ margin-left: 15rem; /* LTR */ } + @media print { body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-left: 0; @@ -249,6 +251,7 @@ body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-right: 240px; margin-right: 15rem; } + @media print { [dir="rtl"] body.toolbar-tray-open.toolbar-vertical.toolbar-fixed { margin-right: 0; diff --git a/core/themes/stable/css/tour/tour.module.css b/core/themes/stable/css/tour/tour.module.css index 18d8aa04cad4..035dfd0c3c89 100644 --- a/core/themes/stable/css/tour/tour.module.css +++ b/core/themes/stable/css/tour/tour.module.css @@ -32,6 +32,7 @@ top: 0; left: 0; } + @media only screen and (max-width: 767px) { .joyride-tip-guide { width: 85%; 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 b843586ad4f4..80c5743a6702 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 @@ -483,6 +483,7 @@ td.group-title { .view-preview-form .form-actions { vertical-align: top; } + @media screen and (min-width: 45em) { /* 720px */ .view-preview-form .form-type-textfield .description { white-space: nowrap; From c5654de7aaf0c66b3b14ed0f4f472e61d2ea6103 Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Fri, 5 Jan 2018 09:45:13 +0000 Subject: [PATCH 100/232] Issue #2866809 by BrightBold: Update stylelint rule declaration-colon-space-after to be consistent with Drupal's CSS standards --- core/.stylelintrc.json | 1 - core/modules/ckeditor/css/ckeditor.admin.css | 6 +++--- core/modules/contextual/css/contextual.toolbar.css | 4 ++-- core/modules/locale/css/locale.admin.css | 2 +- core/modules/node/css/node.module.css | 4 ++-- core/modules/quickedit/css/quickedit.theme.css | 4 ++-- core/modules/views_ui/css/views_ui.admin.theme.css | 2 +- core/themes/bartik/css/components/primary-menu.css | 2 +- core/themes/bartik/css/components/secondary-menu.css | 2 +- core/themes/classy/css/components/form.css | 2 +- core/themes/classy/css/components/progress.css | 2 +- core/themes/seven/css/components/admin-list.css | 2 +- core/themes/seven/css/components/buttons.css | 12 ++++++------ core/themes/seven/css/components/quickedit.css | 6 +++--- core/themes/seven/css/components/tabs.css | 2 +- core/themes/stable/css/ckeditor/ckeditor.admin.css | 6 +++--- .../stable/css/contextual/contextual.toolbar.css | 4 ++-- core/themes/stable/css/locale/locale.admin.css | 2 +- core/themes/stable/css/node/node.module.css | 4 ++-- core/themes/stable/css/quickedit/quickedit.theme.css | 4 ++-- .../stable/css/views_ui/views_ui.admin.theme.css | 2 +- 21 files changed, 37 insertions(+), 38 deletions(-) diff --git a/core/.stylelintrc.json b/core/.stylelintrc.json index c23bd89dea2c..28d0e93a8577 100644 --- a/core/.stylelintrc.json +++ b/core/.stylelintrc.json @@ -12,7 +12,6 @@ "declaration-block-no-redundant-longhand-properties": null, "declaration-block-no-shorthand-property-overrides": null, "declaration-block-trailing-semicolon": null, - "declaration-colon-space-after": null, "function-comma-space-after": null, "function-linear-gradient-no-nonstandard-direction": null, "function-whitespace-after": null, diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css index cfbeae061b44..b25f3b9a5501 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -16,9 +16,9 @@ margin: 5px 0; /* Disallow any user selections in the drag-and-drop toolbar config UI. */ -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .ckeditor-toolbar-active { margin-top: 0.25em; diff --git a/core/modules/contextual/css/contextual.toolbar.css b/core/modules/contextual/css/contextual.toolbar.css index dd573a7a303a..7a493ab10329 100644 --- a/core/modules/contextual/css/contextual.toolbar.css +++ b/core/modules/contextual/css/contextual.toolbar.css @@ -14,8 +14,8 @@ margin: 0; } .toolbar .toolbar-bar .contextual-toolbar-tab .toolbar-item.is-active { - background-image:-webkit-linear-gradient(rgb(78,159,234) 0%, rgb(69,132,221) 100%); - background-image:linear-gradient(rgb(78,159,234) 0%,rgb(69,132,221) 100%); + background-image: -webkit-linear-gradient(rgb(78,159,234) 0%, rgb(69,132,221) 100%); + background-image: linear-gradient(rgb(78,159,234) 0%, rgb(69,132,221) 100%); } /* @todo get rid of this declaration by making toolbar.module's CSS less specific */ diff --git a/core/modules/locale/css/locale.admin.css b/core/modules/locale/css/locale.admin.css index 131540464f4e..e15ccf925796 100644 --- a/core/modules/locale/css/locale.admin.css +++ b/core/modules/locale/css/locale.admin.css @@ -49,7 +49,7 @@ } .locale-translate-filter-form .form-wrapper { - margin-bottom:0; + margin-bottom: 0; } .locale-translate-edit-form table.changed { diff --git a/core/modules/node/css/node.module.css b/core/modules/node/css/node.module.css index 397adb5e85e8..d95060b8db09 100644 --- a/core/modules/node/css/node.module.css +++ b/core/modules/node/css/node.module.css @@ -9,7 +9,7 @@ /* Narrow screens */ .layout-region { - box-sizing: border-box; + box-sizing: border-box; } /* Wide screens */ @@ -51,7 +51,7 @@ .layout-region-node-secondary .form-number, .layout-region-node-secondary .form-color, .layout-region-node-secondary textarea { - box-sizing: border-box; + box-sizing: border-box; width: 100%; max-width: 100%; } diff --git a/core/modules/quickedit/css/quickedit.theme.css b/core/modules/quickedit/css/quickedit.theme.css index 120573a57b41..7ca1c7c59553 100644 --- a/core/modules/quickedit/css/quickedit.theme.css +++ b/core/modules/quickedit/css/quickedit.theme.css @@ -107,7 +107,7 @@ } .quickedit-toolbar-container > .quickedit-toolbar-content { background-image: -webkit-linear-gradient(top, #fff, #e4e4e4); - background-image: linear-gradient(to bottom, #fff, #e4e4e4); + background-image: linear-gradient(to bottom, #fff, #e4e4e4); box-sizing: border-box; color: black; padding: 0.1667em; @@ -237,7 +237,7 @@ color: white; background-color: #50a0e9; background-image: -webkit-linear-gradient(top, #50a0e9, #4481dc); - background-image: linear-gradient(to bottom, #50a0e9, #4481dc); + background-image: linear-gradient(to bottom, #50a0e9, #4481dc); border: 1px solid transparent; } .quickedit-button.action-save:hover, 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 56223069cef5..2c72e1b6a98f 100644 --- a/core/modules/views_ui/css/views_ui.admin.theme.css +++ b/core/modules/views_ui/css/views_ui.admin.theme.css @@ -575,7 +575,7 @@ td.group-title { border-top: none; } .views-filterable-options { - border-top: 1px solid #ccc; + border-top: 1px solid #ccc; } .filterable-option .form-item { margin-bottom: 0; diff --git a/core/themes/bartik/css/components/primary-menu.css b/core/themes/bartik/css/components/primary-menu.css index 1132ae2c2e0d..72404ccc08f6 100644 --- a/core/themes/bartik/css/components/primary-menu.css +++ b/core/themes/bartik/css/components/primary-menu.css @@ -78,7 +78,7 @@ body:not(:target) .region-primary-menu .menu-toggle { z-index: 1000; } body:not(:target) .region-primary-menu .menu-toggle:after { - content:""; + content: ""; background: url(../../../../misc/icons/ffffff/hamburger.svg) no-repeat; background-size: contain; width: 22px; diff --git a/core/themes/bartik/css/components/secondary-menu.css b/core/themes/bartik/css/components/secondary-menu.css index c3164dbd0635..c099dc73ed67 100644 --- a/core/themes/bartik/css/components/secondary-menu.css +++ b/core/themes/bartik/css/components/secondary-menu.css @@ -18,7 +18,7 @@ } .region-secondary-menu .menu a { display: inline-block; - padding: 0.8em; + padding: 0.8em; } .region-secondary-menu .menu a:hover, .region-secondary-menu .menu a:focus { diff --git a/core/themes/classy/css/components/form.css b/core/themes/classy/css/components/form.css index 97b94fe8c84d..aaf702272c3a 100644 --- a/core/themes/classy/css/components/form.css +++ b/core/themes/classy/css/components/form.css @@ -47,7 +47,7 @@ label.option { } .form-composite > legend, .label { - display:inline; + display: inline; font-size: inherit; font-weight: bold; margin: 0; diff --git a/core/themes/classy/css/components/progress.css b/core/themes/classy/css/components/progress.css index 39a055dd1abb..09aab9071d85 100644 --- a/core/themes/classy/css/components/progress.css +++ b/core/themes/classy/css/components/progress.css @@ -10,7 +10,7 @@ border-radius: 10em; background-color: #f2f1eb; background-image: -webkit-linear-gradient(#e7e7df, #f0f0f0); - background-image: linear-gradient(#e7e7df, #f0f0f0); + background-image: linear-gradient(#e7e7df, #f0f0f0); box-shadow: inset 0 1px 3px hsla(0, 0%, 0%, 0.16); } .progress__bar { diff --git a/core/themes/seven/css/components/admin-list.css b/core/themes/seven/css/components/admin-list.css index 5d0b63d8320e..f535e9980529 100644 --- a/core/themes/seven/css/components/admin-list.css +++ b/core/themes/seven/css/components/admin-list.css @@ -28,7 +28,7 @@ ul.admin-list { padding-left: 15px; } .admin-list.compact li a { - background-image: none; + background-image: none; padding: 2px 0; } .admin-list li a:hover, diff --git a/core/themes/seven/css/components/buttons.css b/core/themes/seven/css/components/buttons.css index 479ca9cfcb38..6d0d50fbc94f 100644 --- a/core/themes/seven/css/components/buttons.css +++ b/core/themes/seven/css/components/buttons.css @@ -35,7 +35,7 @@ border-radius: 20em; background-color: #f2f1eb; background-image: -webkit-linear-gradient(top, #f6f6f3, #e7e7df); - background-image: linear-gradient(to bottom, #f6f6f3, #e7e7df); + background-image: linear-gradient(to bottom, #f6f6f3, #e7e7df); color: #333; text-decoration: none; text-shadow: 0 1px hsla(0, 0%, 100%, 0.6); @@ -50,7 +50,7 @@ .button:focus { background-color: #f9f8f6; background-image: -webkit-linear-gradient(top, #fcfcfa, #e9e9dd); - background-image: linear-gradient(to bottom, #fcfcfa, #e9e9dd); + background-image: linear-gradient(to bottom, #fcfcfa, #e9e9dd); color: #1a1a1a; text-decoration: none; outline: none; @@ -69,7 +69,7 @@ border: 1px solid #a6a6a6; background-color: #dfdfd9; background-image: -webkit-linear-gradient(top, #f6f6f3, #e7e7df); - background-image: linear-gradient(to bottom, #f6f6f3, #e7e7df); + background-image: linear-gradient(to bottom, #f6f6f3, #e7e7df); box-shadow: inset 0 1px 3px hsla(0, 0%, 0%, 0.2); -webkit-transition: none; transition: none; @@ -79,7 +79,7 @@ border-color: #1e5c90; background-color: #0071b8; background-image: -webkit-linear-gradient(top, #007bc6, #0071b8); - background-image: linear-gradient(to bottom, #007bc6, #0071b8); + background-image: linear-gradient(to bottom, #007bc6, #0071b8); color: #fff; text-shadow: 0 1px hsla(0, 0%, 0%, 0.5); font-weight: 700; @@ -89,7 +89,7 @@ .button--primary:focus { background-color: #2369a6; background-image: -webkit-linear-gradient(top, #0c97ed, #1f86c7); - background-image: linear-gradient(to bottom, #0c97ed, #1f86c7); + background-image: linear-gradient(to bottom, #0c97ed, #1f86c7); border-color: #1e5c90; color: #fff; } @@ -101,7 +101,7 @@ } .button--primary:active { background-image: -webkit-linear-gradient(top, #08639b, #0071b8); - background-image: linear-gradient(to bottom, #08639b, #0071b8); + background-image: linear-gradient(to bottom, #08639b, #0071b8); border-color: #144b78; box-shadow: inset 0 1px 3px hsla(0, 0%, 0%, 0.2); } diff --git a/core/themes/seven/css/components/quickedit.css b/core/themes/seven/css/components/quickedit.css index 5949c08f4a8e..454863b84d2e 100644 --- a/core/themes/seven/css/components/quickedit.css +++ b/core/themes/seven/css/components/quickedit.css @@ -23,7 +23,7 @@ .quickedit-button.action-saving { border-color: #1e5c90; background-image: -webkit-linear-gradient(top, #007bc6, #0071b8); - background-image: linear-gradient(to bottom, #007bc6, #0071b8); + background-image: linear-gradient(to bottom, #007bc6, #0071b8); color: #fff; text-shadow: 0 1px hsla(0, 0%, 0%, 0.5); font-weight: 700; @@ -36,7 +36,7 @@ .quickedit-button.action-saving:focus { background-color: #2369a6; background-image: -webkit-linear-gradient(top, #0c97ed, #1f86c7); - background-image: linear-gradient(to bottom, #0c97ed, #1f86c7); + background-image: linear-gradient(to bottom, #0c97ed, #1f86c7); border-color: #1e5c90; color: #fff; } @@ -49,7 +49,7 @@ .quickedit-button.action-save:active, .quickedit-button.action-saving:active { background-image: -webkit-linear-gradient(top, #08639b, #0071b8); - background-image: linear-gradient(to bottom, #08639b, #0071b8); + background-image: linear-gradient(to bottom, #08639b, #0071b8); border-color: #144b78; box-shadow: inset 0 1px 3px hsla(0, 0%, 0%, 0.2); } diff --git a/core/themes/seven/css/components/tabs.css b/core/themes/seven/css/components/tabs.css index 5aab30b8499c..e9aa26a92635 100644 --- a/core/themes/seven/css/components/tabs.css +++ b/core/themes/seven/css/components/tabs.css @@ -155,7 +155,7 @@ li.tabs__tab a { } .tabs.is-open { max-height: 999em; - padding-bottom:16px; + padding-bottom: 16px; padding-bottom: 1rem; } .is-collapse-enabled .tabs__tab.is-active { diff --git a/core/themes/stable/css/ckeditor/ckeditor.admin.css b/core/themes/stable/css/ckeditor/ckeditor.admin.css index cfbeae061b44..b25f3b9a5501 100644 --- a/core/themes/stable/css/ckeditor/ckeditor.admin.css +++ b/core/themes/stable/css/ckeditor/ckeditor.admin.css @@ -16,9 +16,9 @@ margin: 5px 0; /* Disallow any user selections in the drag-and-drop toolbar config UI. */ -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .ckeditor-toolbar-active { margin-top: 0.25em; diff --git a/core/themes/stable/css/contextual/contextual.toolbar.css b/core/themes/stable/css/contextual/contextual.toolbar.css index dd573a7a303a..7a493ab10329 100644 --- a/core/themes/stable/css/contextual/contextual.toolbar.css +++ b/core/themes/stable/css/contextual/contextual.toolbar.css @@ -14,8 +14,8 @@ margin: 0; } .toolbar .toolbar-bar .contextual-toolbar-tab .toolbar-item.is-active { - background-image:-webkit-linear-gradient(rgb(78,159,234) 0%, rgb(69,132,221) 100%); - background-image:linear-gradient(rgb(78,159,234) 0%,rgb(69,132,221) 100%); + background-image: -webkit-linear-gradient(rgb(78,159,234) 0%, rgb(69,132,221) 100%); + background-image: linear-gradient(rgb(78,159,234) 0%, rgb(69,132,221) 100%); } /* @todo get rid of this declaration by making toolbar.module's CSS less specific */ diff --git a/core/themes/stable/css/locale/locale.admin.css b/core/themes/stable/css/locale/locale.admin.css index 985a1ade3b1c..c6cd21a9d222 100644 --- a/core/themes/stable/css/locale/locale.admin.css +++ b/core/themes/stable/css/locale/locale.admin.css @@ -49,7 +49,7 @@ } .locale-translate-filter-form .form-wrapper { - margin-bottom:0; + margin-bottom: 0; } .locale-translate-edit-form table.changed { diff --git a/core/themes/stable/css/node/node.module.css b/core/themes/stable/css/node/node.module.css index 397adb5e85e8..d95060b8db09 100644 --- a/core/themes/stable/css/node/node.module.css +++ b/core/themes/stable/css/node/node.module.css @@ -9,7 +9,7 @@ /* Narrow screens */ .layout-region { - box-sizing: border-box; + box-sizing: border-box; } /* Wide screens */ @@ -51,7 +51,7 @@ .layout-region-node-secondary .form-number, .layout-region-node-secondary .form-color, .layout-region-node-secondary textarea { - box-sizing: border-box; + box-sizing: border-box; width: 100%; max-width: 100%; } diff --git a/core/themes/stable/css/quickedit/quickedit.theme.css b/core/themes/stable/css/quickedit/quickedit.theme.css index 120573a57b41..7ca1c7c59553 100644 --- a/core/themes/stable/css/quickedit/quickedit.theme.css +++ b/core/themes/stable/css/quickedit/quickedit.theme.css @@ -107,7 +107,7 @@ } .quickedit-toolbar-container > .quickedit-toolbar-content { background-image: -webkit-linear-gradient(top, #fff, #e4e4e4); - background-image: linear-gradient(to bottom, #fff, #e4e4e4); + background-image: linear-gradient(to bottom, #fff, #e4e4e4); box-sizing: border-box; color: black; padding: 0.1667em; @@ -237,7 +237,7 @@ color: white; background-color: #50a0e9; background-image: -webkit-linear-gradient(top, #50a0e9, #4481dc); - background-image: linear-gradient(to bottom, #50a0e9, #4481dc); + background-image: linear-gradient(to bottom, #50a0e9, #4481dc); border: 1px solid transparent; } .quickedit-button.action-save:hover, 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 80c5743a6702..284ee02c3905 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 @@ -575,7 +575,7 @@ td.group-title { border-top: none; } .views-filterable-options { - border-top: 1px solid #ccc; + border-top: 1px solid #ccc; } .filterable-option .form-item { margin-bottom: 0; From e8a584a7d3de8a8e9ee3a8cc1bf8b697de39886e Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Fri, 5 Jan 2018 09:49:13 +0000 Subject: [PATCH 101/232] Issue #2866800 by BrightBold: Update stylelint rule block-no-empty to be consistent with Drupal's CSS standards --- core/.stylelintrc.json | 1 - core/modules/locale/css/locale.admin.css | 2 -- core/themes/stable/css/locale/locale.admin.css | 2 -- 3 files changed, 5 deletions(-) diff --git a/core/.stylelintrc.json b/core/.stylelintrc.json index 28d0e93a8577..f28d19abb3be 100644 --- a/core/.stylelintrc.json +++ b/core/.stylelintrc.json @@ -4,7 +4,6 @@ "stylelint-no-browser-hacks/lib" ], "rules": { - "block-no-empty": null, "color-hex-case": null, "color-hex-length": null, "comment-empty-line-before": null, diff --git a/core/modules/locale/css/locale.admin.css b/core/modules/locale/css/locale.admin.css index e15ccf925796..1e08bb705358 100644 --- a/core/modules/locale/css/locale.admin.css +++ b/core/modules/locale/css/locale.admin.css @@ -68,8 +68,6 @@ #locale-translation-status-form th.title { width: 25%; } -#locale-translation-status-form th.description { -} #locale-translation-status-form td { vertical-align: top; } diff --git a/core/themes/stable/css/locale/locale.admin.css b/core/themes/stable/css/locale/locale.admin.css index c6cd21a9d222..2d94e2f3aba5 100644 --- a/core/themes/stable/css/locale/locale.admin.css +++ b/core/themes/stable/css/locale/locale.admin.css @@ -68,8 +68,6 @@ #locale-translation-status-form th.title { width: 25%; } -#locale-translation-status-form th.description { -} #locale-translation-status-form td { vertical-align: top; } From 5616f9b877e41b1cabe481ed9f35a7d564d90b29 Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Fri, 5 Jan 2018 09:56:09 +0000 Subject: [PATCH 102/232] Issue #2866808 by BrightBold, pk188, idebr: Update stylelint rule declaration-block-trailing-semicolon to be consistent with Drupal's CSS standards --- core/.stylelintrc.json | 1 - core/modules/ckeditor/css/ckeditor.admin.css | 2 +- core/modules/locale/css/locale.admin.css | 2 +- core/modules/views_ui/css/views_ui.admin.theme.css | 2 +- core/themes/seven/css/theme/ckeditor-dialog.css | 2 +- core/themes/stable/css/ckeditor/ckeditor.admin.css | 2 +- core/themes/stable/css/locale/locale.admin.css | 2 +- core/themes/stable/css/views_ui/views_ui.admin.theme.css | 2 +- 8 files changed, 7 insertions(+), 8 deletions(-) diff --git a/core/.stylelintrc.json b/core/.stylelintrc.json index f28d19abb3be..bb4b610ca24c 100644 --- a/core/.stylelintrc.json +++ b/core/.stylelintrc.json @@ -10,7 +10,6 @@ "declaration-block-no-duplicate-properties": null, "declaration-block-no-redundant-longhand-properties": null, "declaration-block-no-shorthand-property-overrides": null, - "declaration-block-trailing-semicolon": null, "function-comma-space-after": null, "function-linear-gradient-no-nonstandard-direction": null, "function-whitespace-after": null, diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css index b25f3b9a5501..d66982bcc535 100644 --- a/core/modules/ckeditor/css/ckeditor.admin.css +++ b/core/modules/ckeditor/css/ckeditor.admin.css @@ -296,7 +296,7 @@ ul.ckeditor-buttons li.ckeditor-button-separator a { height: 18px; width: 1px; display: block; - box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5) + box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5); } .ckeditor-button-arrow { width: 0; diff --git a/core/modules/locale/css/locale.admin.css b/core/modules/locale/css/locale.admin.css index 1e08bb705358..76bb2e463806 100644 --- a/core/modules/locale/css/locale.admin.css +++ b/core/modules/locale/css/locale.admin.css @@ -37,7 +37,7 @@ table-layout: fixed; } .locale-translate-edit-form td { - vertical-align: top + vertical-align: top; } .locale-translate-edit-form tr.changed { 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 2c72e1b6a98f..5306466b8500 100644 --- a/core/modules/views_ui/css/views_ui.admin.theme.css +++ b/core/modules/views_ui/css/views_ui.admin.theme.css @@ -431,7 +431,7 @@ td.group-title { background-color: #ddd; } .edit-display-settings { - margin: 12px 12px 0 12px + margin: 12px 12px 0 12px; } .edit-display-settings-top.views-ui-display-tab-bucket { border: 1px solid #f3f3f3; diff --git a/core/themes/seven/css/theme/ckeditor-dialog.css b/core/themes/seven/css/theme/ckeditor-dialog.css index b7d8dbbccb4a..0228442e85f4 100644 --- a/core/themes/seven/css/theme/ckeditor-dialog.css +++ b/core/themes/seven/css/theme/ckeditor-dialog.css @@ -192,7 +192,7 @@ background-image: linear-gradient(to bottom, #fcfcfa, #e9e9dd); color: #1a1a1a; text-decoration: none; - box-shadow: 0 1px 2px hsla(0, 0%, 0%, 0.125) + box-shadow: 0 1px 2px hsla(0, 0%, 0%, 0.125); } .cke_reset_all .cke_dialog_footer_buttons a.cke_dialog_ui_button:focus { z-index: 10; diff --git a/core/themes/stable/css/ckeditor/ckeditor.admin.css b/core/themes/stable/css/ckeditor/ckeditor.admin.css index b25f3b9a5501..d66982bcc535 100644 --- a/core/themes/stable/css/ckeditor/ckeditor.admin.css +++ b/core/themes/stable/css/ckeditor/ckeditor.admin.css @@ -296,7 +296,7 @@ ul.ckeditor-buttons li.ckeditor-button-separator a { height: 18px; width: 1px; display: block; - box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5) + box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5); } .ckeditor-button-arrow { width: 0; diff --git a/core/themes/stable/css/locale/locale.admin.css b/core/themes/stable/css/locale/locale.admin.css index 2d94e2f3aba5..759a76c699da 100644 --- a/core/themes/stable/css/locale/locale.admin.css +++ b/core/themes/stable/css/locale/locale.admin.css @@ -37,7 +37,7 @@ table-layout: fixed; } .locale-translate-edit-form td { - vertical-align: top + vertical-align: top; } .locale-translate-edit-form tr.changed { 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 284ee02c3905..21af7ddefe6a 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 @@ -431,7 +431,7 @@ td.group-title { background-color: #ddd; } .edit-display-settings { - margin: 12px 12px 0 12px + margin: 12px 12px 0 12px; } .edit-display-settings-top.views-ui-display-tab-bucket { border: 1px solid #f3f3f3; From e538433ffc7f4b1ac053ef6aa2fda910fb41b594 Mon Sep 17 00:00:00 2001 From: xjm Date: Fri, 5 Jan 2018 17:06:05 -0600 Subject: [PATCH 103/232] Issue #2863354 by benqwerty, DuaelFr, andrewmacpherson, xjm, mgifford, lauriii, ckrina: Add border to dialog [x] close button for hover and focus states --- core/themes/seven/css/components/dialog.css | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/core/themes/seven/css/components/dialog.css b/core/themes/seven/css/components/dialog.css index b4459d1a6ea8..1660fc302e8e 100644 --- a/core/themes/seven/css/components/dialog.css +++ b/core/themes/seven/css/components/dialog.css @@ -34,14 +34,22 @@ -webkit-font-smoothing: antialiased; } .ui-dialog .ui-dialog-titlebar-close { - border: 0; + border: 3px solid #6b6b6b; + border-radius: 5px; background: none; - right: 20px; /* LTR */ - top: 20px; + right: 12px; /* LTR */ + top: 10px; margin: 0; - height: 16px; - width: 16px; + padding: 0; + height: 30px; + width: 30px; position: absolute; + -webkit-transition: all 0.1s; + transition: all 0.1s; +} +.ui-dialog .ui-dialog-titlebar-close:hover, +.ui-dialog .ui-dialog-titlebar-close:focus { + border-color: #ffffff; } [dir="rtl"] .ui-dialog .ui-dialog-titlebar-close { right: auto; @@ -49,7 +57,7 @@ } .ui-dialog .ui-icon.ui-icon-closethick { background: url(../../../../misc/icons/ffffff/ex.svg) 0 0 no-repeat; - margin-top: -12px; + margin-top: -8px; } .ui-dialog .ui-widget-content.ui-dialog-content { background: #ffffff; From 891c39b37c91f17c3a941264ef62400f17f28eed Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:20:01 +1000 Subject: [PATCH 104/232] Issue #2933991 by alexpott, neclimdul: Deprecation tests fail when all PHPUnit tests are run via PHPUnit --- .../Plugin/migrate/cckfield/LinkCckTest.php | 3 +- ...DrupalStandardsListenerDeprecatedClass.php | 28 +++++++++++++++++++ ...DrupalStandardsListenerDeprecationTest.php | 7 ++--- .../Listeners/DeprecationListenerTrait.php | 14 +++++++++- .../DrupalStandardsListenerTrait.php | 26 +++++++++++++++++ 5 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 core/modules/system/tests/modules/deprecation_test/src/Deprecation/DrupalStandardsListenerDeprecatedClass.php diff --git a/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php b/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php index dc3fe7731ee4..fb4742ecac0d 100644 --- a/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php +++ b/core/modules/link/tests/src/Unit/Plugin/migrate/cckfield/LinkCckTest.php @@ -46,8 +46,7 @@ protected function setUp() { /** * @covers ::processCckFieldValues - * @expectedDeprecation CckFieldPluginBase is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase instead. - * @expectedDeprecation MigrateCckFieldInterface is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\migrate_drupal\Annotation\MigrateField instead. + * @expectedDeprecation LinkField is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\link\Plugin\migrate\field\d6\LinkField instead. */ public function testProcessCckFieldValues() { $this->plugin->processCckFieldValues($this->migration, 'somefieldname', []); diff --git a/core/modules/system/tests/modules/deprecation_test/src/Deprecation/DrupalStandardsListenerDeprecatedClass.php b/core/modules/system/tests/modules/deprecation_test/src/Deprecation/DrupalStandardsListenerDeprecatedClass.php new file mode 100644 index 000000000000..de4becba6837 --- /dev/null +++ b/core/modules/system/tests/modules/deprecation_test/src/Deprecation/DrupalStandardsListenerDeprecatedClass.php @@ -0,0 +1,28 @@ +getName(FALSE); + if (strpos($method, 'testLegacy') === 0 + || strpos($method, 'provideLegacy') === 0 + || strpos($method, 'getLegacy') === 0 + || strpos(get_class($test), '\Legacy') + || in_array('legacy', $util_test_class::getGroups(get_class($test), $method), TRUE)) { + // This is a legacy test don't skip deprecations. + return; + } + + // Need to edit the file of deprecations to remove any skipped + // deprecations. $deprecations = file_get_contents($file); $deprecations = $deprecations ? unserialize($deprecations) : []; $resave = FALSE; diff --git a/core/tests/Drupal/Tests/Listeners/DrupalStandardsListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DrupalStandardsListenerTrait.php index 2676c46a4fe6..0ad0e1e239aa 100644 --- a/core/tests/Drupal/Tests/Listeners/DrupalStandardsListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DrupalStandardsListenerTrait.php @@ -143,6 +143,28 @@ private function checkValidCoversForTest(TestCase $test) { } } + /** + * Handles errors to ensure deprecation messages are not triggered. + * + * @param int $type + * The severity level of the error. + * @param string $msg + * The error message. + * @param $file + * The file that caused the error. + * @param $line + * The line number that caused the error. + * @param array $context + * The error context. + */ + public static function errorHandler($type, $msg, $file, $line, $context = array()) { + if ($type === E_USER_DEPRECATED) { + return; + } + $error_handler = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler'; + return $error_handler::handleError($type, $msg, $file, $line, $context); + } + /** * Reacts to the end of a test. * @@ -166,7 +188,11 @@ private function doEndTest($test, $time) { // our purpose, so we have to distinguish between the different known // subclasses. if ($test instanceof TestCase) { + // Change the error handler to ensure deprecation messages are not + // triggered. + set_error_handler([$this, 'errorHandler']); $this->checkValidCoversForTest($test); + restore_error_handler(); } elseif ($this->isTestSuite($test)) { foreach ($test->getGroupDetails() as $tests) { From a46090e0aae181ecd98996d41fc193f9157d1915 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:31:10 +1000 Subject: [PATCH 105/232] Issue #2934152 by Wim Leers, neclimdul: ContentTypeHeaderMatcher should not run for GET, HEAD, OPTIONS or TRACE requests --- .../Core/Routing/ContentTypeHeaderMatcher.php | 5 +++-- .../Routing/ContentTypeHeaderMatcherTest.php | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php index 96a10c1e34dd..719eeb132891 100644 --- a/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php +++ b/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php @@ -16,8 +16,9 @@ class ContentTypeHeaderMatcher implements FilterInterface { */ public function filter(RouteCollection $collection, Request $request) { // The Content-type header does not make sense on GET requests, because GET - // requests do not carry any content. Nothing to filter in this case. - if ($request->isMethod('GET')) { + // requests do not carry any content. Nothing to filter in this case. Same + // for all other safe methods. + if ($request->isMethodSafe(FALSE)) { return $collection; } diff --git a/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php b/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php index e2c73bbe95de..87e3f3e6c94c 100644 --- a/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php +++ b/core/tests/Drupal/Tests/Core/Routing/ContentTypeHeaderMatcherTest.php @@ -42,17 +42,28 @@ protected function setUp() { } /** - * Tests that routes are not filtered on GET requests. + * Tests that routes are not filtered on safe requests. + * + * @dataProvider providerTestSafeRequestFilter */ - public function testGetRequestFilter() { + public function testSafeRequestFilter($method) { $collection = $this->fixtures->sampleRouteCollection(); $collection->addCollection($this->fixtures->contentRouteCollection()); - $request = Request::create('path/two', 'GET'); + $request = Request::create('path/two', $method); $routes = $this->matcher->filter($collection, $request); $this->assertEquals(count($routes), 7, 'The correct number of routes was found.'); } + public function providerTestSafeRequestFilter() { + return [ + ['GET'], + ['HEAD'], + ['OPTIONS'], + ['TRACE'], + ]; + } + /** * Tests that XML-restricted routes get filtered out on JSON requests. */ From 71584842ea9dded0fff23e3eafaff14a298fd582 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:35:02 +1000 Subject: [PATCH 106/232] Issue #2931047 by Dropa, D34dMan, borisson_, catch: hook_post_update_NAME documentation is misleading --- core/lib/Drupal/Core/Extension/module.api.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 17627523644d..45355cc3f6a2 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -676,9 +676,10 @@ function hook_update_N(&$sandbox) { * These updates are executed after all hook_update_N() implementations. At this * stage Drupal is already fully repaired so you can use any API as you wish. * - * NAME can be arbitrary machine names. In contrast to hook_update_N() the order - * of functions in the file is the only thing which ensures the execution order - * of those functions. + * NAME can be arbitrary machine names. In contrast to hook_update_N() the + * alphanumeric naming of functions in the file is the only thing which ensures + * the execution order of those functions. If update order is mandatory, + * you should add numerical prefix to NAME or make it completely numerical. * * Drupal also ensures to not execute the same hook_post_update_NAME() function * twice. From 0f2bebf2e0e65f65d65e9fc5487de111472aab98 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:40:50 +1000 Subject: [PATCH 107/232] Issue #2861840 by tim.plunkett, lauriii: Preprocess functions are not merged when a module registers a theme hook for a theme-provided template --- core/lib/Drupal/Core/Theme/Registry.php | 11 ++++++++++- .../tests/src/Kernel/LayoutTest.php | 11 +++++++++++ .../templates/test-layout-theme.html.twig | 1 + .../test_layout_theme.info.yml | 6 ++++++ .../test_layout_theme.layouts.yml | 7 +++++++ .../modules/theme_test/theme_test.module | 10 ++++++++++ .../theme-test-registered-by-module.html.twig | 2 ++ .../KernelTests/Core/Theme/RegistryTest.php | 19 +++++++++++++++++++ 8 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 core/modules/layout_discovery/tests/themes/test_layout_theme/templates/test-layout-theme.html.twig create mode 100644 core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.info.yml create mode 100644 core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.layouts.yml create mode 100644 core/modules/system/tests/themes/test_theme/templates/theme-test-registered-by-module.html.twig diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index a0af70261cbe..7cf4b3c43666 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Theme; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\DestructableInterface; @@ -565,10 +566,18 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path) $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']); } $result[$hook]['preprocess functions'] = $info['preprocess functions']; + + // If a theme implementation definition provides both 'template' and + // 'function', the 'function' will be used. In this case, if the new + // result provides a 'template' value, any existing 'function' value + // must be removed for the override to be called. + if (isset($result[$hook]['template'])) { + unset($cache[$hook]['function']); + } } // Merge the newly created theme hooks into the existing cache. - $cache = $result + $cache; + $cache = NestedArray::mergeDeep($cache, $result); } // Let themes have variable preprocessors even if they didn't register a diff --git a/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php b/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php index c04785055e9b..c7be49ba2b15 100644 --- a/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php +++ b/core/modules/layout_discovery/tests/src/Kernel/LayoutTest.php @@ -33,6 +33,17 @@ protected function setUp() { $this->layoutPluginManager = $this->container->get('plugin.manager.core.layout'); } + /** + * Tests that a layout provided by a theme has the preprocess function set. + */ + public function testThemeProvidedLayout() { + $this->container->get('theme_installer')->install(['test_layout_theme']); + $this->config('system.theme')->set('default', 'test_layout_theme')->save(); + + $theme_definitions = $this->container->get('theme.registry')->get(); + $this->assertTrue(in_array('template_preprocess_layout', $theme_definitions['test_layout_theme']['preprocess functions'])); + } + /** * Test rendering a layout. * diff --git a/core/modules/layout_discovery/tests/themes/test_layout_theme/templates/test-layout-theme.html.twig b/core/modules/layout_discovery/tests/themes/test_layout_theme/templates/test-layout-theme.html.twig new file mode 100644 index 000000000000..67675f64c1a7 --- /dev/null +++ b/core/modules/layout_discovery/tests/themes/test_layout_theme/templates/test-layout-theme.html.twig @@ -0,0 +1 @@ +{{ content.content }} diff --git a/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.info.yml b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.info.yml new file mode 100644 index 000000000000..021d43fd272c --- /dev/null +++ b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.info.yml @@ -0,0 +1,6 @@ +name: 'Test layout theme' +type: theme +description: 'Theme for testing a theme-provided layout' +version: VERSION +base theme: classy +core: 8.x diff --git a/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.layouts.yml b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.layouts.yml new file mode 100644 index 000000000000..9da19ddcf3cb --- /dev/null +++ b/core/modules/layout_discovery/tests/themes/test_layout_theme/test_layout_theme.layouts.yml @@ -0,0 +1,7 @@ +test_layout_theme: + label: 'Test Layout - Theme' + category: 'Test Layout Theme' + template: templates/test-layout-theme + regions: + content: + label: Content diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index 6a701abee915..cd8aba7da7bc 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -66,6 +66,10 @@ function theme_test_theme($existing, $type, $theme, $path) { 'bar' => '', ], ]; + $items['theme_test_registered_by_module'] = [ + 'render element' => 'content', + 'base hook' => 'container', + ]; return $items; } @@ -213,3 +217,9 @@ function theme_test_theme_suggestions_node(array $variables) { return $suggestions; } + +/** + * Implements template_preprocess_HOOK() for theme_test_registered_by_module. + */ +function template_preprocess_theme_test_registered_by_module() { +} diff --git a/core/modules/system/tests/themes/test_theme/templates/theme-test-registered-by-module.html.twig b/core/modules/system/tests/themes/test_theme/templates/theme-test-registered-by-module.html.twig new file mode 100644 index 000000000000..3432e019a6d0 --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/theme-test-registered-by-module.html.twig @@ -0,0 +1,2 @@ +{# Output for Theme API test #} +Template provided by theme is registered by module. diff --git a/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php index f453852d2e9a..7f6d2eccda8e 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php @@ -192,4 +192,23 @@ public function testThemeSuggestions() { ], $suggestions, 'Found expected page node suggestions.'); } + /** + * Tests theme-provided templates that are registered by modules. + */ + public function testThemeTemplatesRegisteredByModules() { + $theme_handler = \Drupal::service('theme_handler'); + $theme_handler->install(['test_theme']); + + $registry_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); + $registry_theme->setThemeManager(\Drupal::theme()); + + $expected = [ + 'template_preprocess', + 'template_preprocess_container', + 'template_preprocess_theme_test_registered_by_module' + ]; + $registry = $registry_theme->get(); + $this->assertEquals($expected, array_values($registry['theme_test_registered_by_module']['preprocess functions'])); + } + } From 3e0376353b9579909f54372edfa7d5ff6e152b70 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:42:03 +1000 Subject: [PATCH 108/232] Issue #2934233 by neclimdul: Fix reference to DrupalKernelTest in system module --- core/tests/Drupal/KernelTests/KernelTestBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index ca1a517d6abc..0a611d87c263 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -251,7 +251,7 @@ protected function setUp() { * Should not be called by tests. Only visible for DrupalKernel integration * tests. * - * @see \Drupal\system\Tests\DrupalKernel\DrupalKernelTest + * @see \Drupal\KernelTests\Core\DrupalKernel\DrupalKernelTest * @internal */ protected function bootEnvironment() { From 6930e1b18b544b2a2c97696760322a29e1ce2bf7 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:44:26 +1000 Subject: [PATCH 109/232] Issue #2929835 by amateescu, dawehner: [regression] Modules can no longer alter the the table queue of a \Drupal\views\Plugin\views\query\Sql query object --- .../views/src/Plugin/views/query/Sql.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php index 2771a3d7c035..09a754bb433e 100644 --- a/core/modules/views/src/Plugin/views/query/Sql.php +++ b/core/modules/views/src/Plugin/views/query/Sql.php @@ -198,6 +198,27 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o ]; } + /** + * Returns a reference to the table queue array for this query. + * + * Because this method returns by reference, alter hooks may edit the tables + * array directly to make their changes. If just adding tables, however, the + * use of the addTable() method is preferred. + * + * Note that if you want to manipulate the table queue array, this method must + * be called by reference as well: + * + * @code + * $tables =& $query->getTableQueue(); + * @endcode + * + * @return array + * A reference to the table queue array structure. + */ + public function &getTableQueue() { + return $this->tableQueue; + } + /** * Set the view to be distinct (per base field). * From a0ce0b879d35e62dcc5af694e93b35c11cc865a2 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:45:59 +1000 Subject: [PATCH 110/232] Issue #2765849 by kiamlaluno: The description for template_preprocess() has not been update for Drupal 8 --- core/includes/theme.inc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 68b10b3e42e7..ac085b4c9768 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1190,10 +1190,9 @@ function template_preprocess_maintenance_task_list(&$variables) { /** * Adds a default set of helper variables for preprocessors and templates. * - * This function is called for theme hooks implemented as templates only, not - * for theme hooks implemented as functions. This preprocess function is the - * first in the sequence of preprocessing functions that are called when - * preparing variables for a template. + * This function is called for every theme hook. It is the first in the + * sequence of preprocessing functions called when preparing variables for a + * template. * * See the @link themeable Default theme implementations topic @endlink for * details. From 0cfb103b423ad2cf74fe2c00f1da80c41e2477ce Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 14:51:30 +1000 Subject: [PATCH 111/232] Issue #2933980 by richgerdes, Maheshwaran.j: JQuery UI CSS dependencies are not met in core.libraries.yml --- core/core.libraries.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 159182aeb6de..85b4b5007ab9 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -496,6 +496,7 @@ jquery.ui.checkboxradio: css: component: assets/vendor/jquery.ui/themes/base/checkboxradio.css: {} + assets/vendor/jquery.ui/themes/base/button.css: {} dependencies: - core/jquery.ui - core/jquery.ui.widget @@ -759,6 +760,7 @@ jquery.ui.selectmenu: css: component: assets/vendor/jquery.ui/themes/base/selectmenu.css: {} + assets/vendor/jquery.ui/themes/base/button.css: {} dependencies: - core/jquery.ui - core/jquery.ui.menu From 0428e20aa15c44daeced4afd9269664a0dbfd530 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 15:01:14 +1000 Subject: [PATCH 112/232] Issue #2931883 by moshe weitzman, David_Rothstein, Wim Leers: Unneeded always_populate_raw_post_data requirements check while on CLI --- core/modules/rest/rest.install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/modules/rest/rest.install b/core/modules/rest/rest.install index 2113b0bbc3bd..754c27f4642a 100644 --- a/core/modules/rest/rest.install +++ b/core/modules/rest/rest.install @@ -14,7 +14,7 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; function rest_requirements($phase) { $requirements = []; - if (version_compare(PHP_VERSION, '5.6.0', '>=') && version_compare(PHP_VERSION, '7', '<') && ini_get('always_populate_raw_post_data') != -1) { + if ($phase == 'runtime' && PHP_SAPI !== 'cli' && version_compare(PHP_VERSION, '5.6.0', '>=') && version_compare(PHP_VERSION, '7', '<') && ini_get('always_populate_raw_post_data') != -1) { $requirements['always_populate_raw_post_data'] = [ 'title' => t('always_populate_raw_post_data PHP setting'), 'value' => t('Not set to -1.'), From ad1e35c7b014e428862687b5cc0de7161dc41904 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 17:27:28 +1000 Subject: [PATCH 113/232] Issue #2816861 by dpi, jibran, bucefal91, harsha012, tim.plunkett, catch: Action configuration form does not support #ajax --- core/modules/action/src/ActionFormBase.php | 49 +++++----- .../action_form_ajax_test.info.yml | 7 ++ .../schema/action_form_ajax_test.schema.yml | 7 ++ .../src/Plugin/Action/ActionAjaxTest.php | 89 +++++++++++++++++++ .../ActionFormAjaxTest.php | 70 +++++++++++++++ 5 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 core/modules/action/tests/action_form_ajax_test/action_form_ajax_test.info.yml create mode 100644 core/modules/action/tests/action_form_ajax_test/config/schema/action_form_ajax_test.schema.yml create mode 100644 core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php create mode 100644 core/modules/action/tests/src/FunctionalJavascript/ActionFormAjaxTest.php diff --git a/core/modules/action/src/ActionFormBase.php b/core/modules/action/src/ActionFormBase.php index 3fafd035eeef..46b70ede45a5 100644 --- a/core/modules/action/src/ActionFormBase.php +++ b/core/modules/action/src/ActionFormBase.php @@ -14,18 +14,18 @@ abstract class ActionFormBase extends EntityForm { /** - * The action plugin being configured. + * The action storage. * - * @var \Drupal\Core\Action\ActionInterface + * @var \Drupal\Core\Entity\EntityStorageInterface */ - protected $plugin; + protected $storage; /** - * The action storage. + * The action entity. * - * @var \Drupal\Core\Entity\EntityStorageInterface + * @var \Drupal\system\ActionConfigEntityInterface */ - protected $storage; + protected $entity; /** * Constructs a new action form. @@ -46,14 +46,6 @@ public static function create(ContainerInterface $container) { ); } - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $this->plugin = $this->entity->getPlugin(); - return parent::buildForm($form, $form_state); - } - /** * {@inheritdoc} */ @@ -85,8 +77,8 @@ public function form(array $form, FormStateInterface $form_state) { '#value' => $this->entity->getType(), ]; - if ($this->plugin instanceof PluginFormInterface) { - $form += $this->plugin->buildConfigurationForm($form, $form_state); + if ($plugin = $this->getPlugin()) { + $form += $plugin->buildConfigurationForm($form, $form_state); } return parent::form($form, $form_state); @@ -96,7 +88,7 @@ public function form(array $form, FormStateInterface $form_state) { * Determines if the action already exists. * * @param string $id - * The action ID + * The action ID. * * @return bool * TRUE if the action exists, FALSE otherwise. @@ -120,9 +112,8 @@ protected function actions(array $form, FormStateInterface $form_state) { */ public function validateForm(array &$form, FormStateInterface $form_state) { parent::validateForm($form, $form_state); - - if ($this->plugin instanceof PluginFormInterface) { - $this->plugin->validateConfigurationForm($form, $form_state); + if ($plugin = $this->getPlugin()) { + $plugin->validateConfigurationForm($form, $form_state); } } @@ -131,9 +122,8 @@ public function validateForm(array &$form, FormStateInterface $form_state) { */ public function submitForm(array &$form, FormStateInterface $form_state) { parent::submitForm($form, $form_state); - - if ($this->plugin instanceof PluginFormInterface) { - $this->plugin->submitConfigurationForm($form, $form_state); + if ($plugin = $this->getPlugin()) { + $plugin->submitConfigurationForm($form, $form_state); } } @@ -147,4 +137,17 @@ public function save(array $form, FormStateInterface $form_state) { $form_state->setRedirect('entity.action.collection'); } + /** + * Gets the action plugin while ensuring it implements configuration form. + * + * @return \Drupal\Core\Action\ActionInterface|\Drupal\Core\Plugin\PluginFormInterface|null + * The action plugin, or NULL if it does not implement configuration forms. + */ + protected function getPlugin() { + if ($this->entity->getPlugin() instanceof PluginFormInterface) { + return $this->entity->getPlugin(); + } + return NULL; + } + } diff --git a/core/modules/action/tests/action_form_ajax_test/action_form_ajax_test.info.yml b/core/modules/action/tests/action_form_ajax_test/action_form_ajax_test.info.yml new file mode 100644 index 000000000000..020e9ef45d5e --- /dev/null +++ b/core/modules/action/tests/action_form_ajax_test/action_form_ajax_test.info.yml @@ -0,0 +1,7 @@ +name: action_form_ajax_test +type: module +description: 'module used for testing ajax in action config entity forms.' +package: Core +version: VERSION +core: 8.x +hidden: true diff --git a/core/modules/action/tests/action_form_ajax_test/config/schema/action_form_ajax_test.schema.yml b/core/modules/action/tests/action_form_ajax_test/config/schema/action_form_ajax_test.schema.yml new file mode 100644 index 000000000000..7ebe2e1b6df9 --- /dev/null +++ b/core/modules/action/tests/action_form_ajax_test/config/schema/action_form_ajax_test.schema.yml @@ -0,0 +1,7 @@ +action.configuration.action_form_ajax_test: + type: action_configuration_default + label: 'action_form_ajax_test action' + mapping: + party_time: + type: string + label: 'The time of the party.' diff --git a/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php b/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php new file mode 100644 index 000000000000..8afc98cf5411 --- /dev/null +++ b/core/modules/action/tests/action_form_ajax_test/src/Plugin/Action/ActionAjaxTest.php @@ -0,0 +1,89 @@ + '', + ]; + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowed(); + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function execute() { + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $having_a_party = $form_state->getValue('having_a_party', !empty($this->configuration['party_time'])); + $form['having_a_party'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Are we having a party?'), + '#ajax' => [ + 'wrapper' => 'party-container', + 'callback' => [$this, 'partyCallback'], + ], + '#default_value' => $having_a_party, + ]; + $form['container'] = [ + '#type' => 'container', + '#prefix' => '
', + '#suffix' => '
', + ]; + + if ($having_a_party) { + $form['container']['party_time'] = [ + '#type' => 'textfield', + '#title' => $this->t('Party time'), + '#default_value' => $this->configuration['party_time'], + ]; + } + + return $form; + } + + /** + * Callback for party checkbox. + */ + public function partyCallback(array $form, FormStateInterface $form_state) { + return $form['container']; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['party_time'] = $form_state->getValue('party_time'); + } + +} diff --git a/core/modules/action/tests/src/FunctionalJavascript/ActionFormAjaxTest.php b/core/modules/action/tests/src/FunctionalJavascript/ActionFormAjaxTest.php new file mode 100644 index 000000000000..bc314a573d5f --- /dev/null +++ b/core/modules/action/tests/src/FunctionalJavascript/ActionFormAjaxTest.php @@ -0,0 +1,70 @@ +drupalCreateUser(['administer actions']); + $this->drupalLogin($user); + } + + /** + * Tests action plugins with AJAX save their configuration. + */ + public function testActionConfigurationWithAjax() { + $url = Url::fromRoute('action.admin_add', ['action_id' => 'action_form_ajax_test']); + $this->drupalGet($url); + $this->assertSession()->statusCodeEquals(200); + $page = $this->getSession()->getPage(); + + $id = 'test_plugin'; + $page->find('css', '[name="id"]') + ->setValue($id); + + $page->find('css', '[name="having_a_party"]') + ->check(); + $this->assertSession()->waitForElement('css', '[name="party_time"]'); + + $party_time = 'Evening'; + $page->find('css', '[name="party_time"]') + ->setValue($party_time); + + $page->find('css', '[value="Save"]') + ->click(); + + $url = Url::fromRoute('entity.action.collection'); + $this->assertSession()->pageTextContains('The action has been successfully saved.'); + $this->assertSession()->addressEquals($url); + $this->assertSession()->statusCodeEquals(200); + + // Check storage. + $instance = Action::load($id); + $configuration = $instance->getPlugin()->getConfiguration(); + $this->assertEquals(['party_time' => $party_time], $configuration); + + // Configuration should be shown in edit form. + $this->drupalGet($instance->toUrl('edit-form')); + $this->assertSession()->checkboxChecked('having_a_party'); + $this->assertSession()->fieldValueEquals('party_time', $party_time); + } + +} From 128cb2622524e3cc3c01f1c80b4399a99375b8ce Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 17:33:07 +1000 Subject: [PATCH 114/232] =?UTF-8?q?Issue=20#2856950=20by=20dmsmidt,=20marc?= =?UTF-8?q?vangend,=20tim.plunkett,=20andrewmacpherson,=20Lendude,=20tamee?= =?UTF-8?q?shb,=20Berdir,=20xjm,=20tedbow,=20G=C3=A1bor=20Hojtsy,=20Suthar?= =?UTF-8?q?san:=20Add=20a=20possibility=20to=20disable=20inline=20form=20e?= =?UTF-8?q?rrors=20for=20a=20complete=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inline_form_errors.module | 9 ++++ .../src/FormErrorHandler.php | 14 ++++-- .../src/RenderElementHelper.php | 49 +++++++++++++++++++ .../src/Kernel/FormElementInlineErrorTest.php | 48 ++++++++++++++++++ .../tests/src/Unit/FormErrorHandlerTest.php | 11 ++--- .../quickedit/src/Form/QuickEditFieldForm.php | 4 ++ 6 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 core/modules/inline_form_errors/src/RenderElementHelper.php create mode 100644 core/modules/inline_form_errors/tests/src/Kernel/FormElementInlineErrorTest.php diff --git a/core/modules/inline_form_errors/inline_form_errors.module b/core/modules/inline_form_errors/inline_form_errors.module index d5c40b43b146..dcb140b7a511 100644 --- a/core/modules/inline_form_errors/inline_form_errors.module +++ b/core/modules/inline_form_errors/inline_form_errors.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\inline_form_errors\RenderElementHelper; /** * Implements hook_help(). @@ -54,6 +55,14 @@ function inline_form_errors_preprocess_datetime_wrapper(&$variables) { _inline_form_errors_set_errors($variables); } +/** + * Implements hook_element_info_alter(). + */ +function inline_form_errors_element_info_alter(array &$info) { + \Drupal::classResolver()->getInstanceFromDefinition(RenderElementHelper::class) + ->alterElementInfo($info); +} + /** * Populates form errors in the template. */ diff --git a/core/modules/inline_form_errors/src/FormErrorHandler.php b/core/modules/inline_form_errors/src/FormErrorHandler.php index 2b892e99dd68..2bcbfc60be7f 100644 --- a/core/modules/inline_form_errors/src/FormErrorHandler.php +++ b/core/modules/inline_form_errors/src/FormErrorHandler.php @@ -47,15 +47,23 @@ public function __construct(TranslationInterface $string_translation, LinkGenera /** * Loops through and displays all form errors. * + * To disable inline form errors for an entire form set the + * #disable_inline_form_errors property to TRUE on the top level of the $form + * array: + * @code + * $form['#disable_inline_form_errors'] = TRUE; + * @endcode + * This should only be done when another appropriate accessibility strategy is + * in place. + * * @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. */ protected function displayErrorMessages(array $form, FormStateInterface $form_state) { - // Use the original error display for Quick Edit forms, because in this case - // the errors are already near the form element. - if ($form['#form_id'] === 'quickedit_field_form') { + // Skip generating inline form errors when opted out. + if (!empty($form['#disable_inline_form_errors'])) { parent::displayErrorMessages($form, $form_state); return; } diff --git a/core/modules/inline_form_errors/src/RenderElementHelper.php b/core/modules/inline_form_errors/src/RenderElementHelper.php new file mode 100644 index 000000000000..a96d77d68b04 --- /dev/null +++ b/core/modules/inline_form_errors/src/RenderElementHelper.php @@ -0,0 +1,49 @@ + $element_info) { + $info[$element_type]['#process'][] = [static::class, 'processElement']; + } + } + + /** + * Process all render elements. + * + * @param array $element + * An associative array containing the properties and children of the + * element. Note that $element must be taken by reference here, so processed + * child elements are taken over into $form_state. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The processed element. + */ + public static function processElement(array &$element, FormStateInterface $form_state, array &$complete_form) { + // Prevent displaying inline form errors when disabled for the whole form. + if (!empty($complete_form['#disable_inline_form_errors'])) { + $element['#error_no_message'] = TRUE; + } + + return $element; + } + +} diff --git a/core/modules/inline_form_errors/tests/src/Kernel/FormElementInlineErrorTest.php b/core/modules/inline_form_errors/tests/src/Kernel/FormElementInlineErrorTest.php new file mode 100644 index 000000000000..4bf389e60fb3 --- /dev/null +++ b/core/modules/inline_form_errors/tests/src/Kernel/FormElementInlineErrorTest.php @@ -0,0 +1,48 @@ + [], + '#disable_inline_form_errors' => TRUE, + '#array_parents' => [], + ]; + $form['test'] = [ + '#type' => 'textfield', + '#title' => 'Test', + '#parents' => ['test'], + '#id' => 'edit-test', + '#array_parents' => ['test'], + ]; + $form_state = new FormState(); + + \Drupal::formBuilder()->prepareForm($form_id, $form, $form_state); + \Drupal::formBuilder()->processForm($form_id, $form, $form_state); + + // Just test if the #error_no_message property is TRUE. FormErrorHandlerTest + // tests if the property actually hides the error message. + $this->assertArraySubset(['#error_no_message' => TRUE], $form['test']); + } + +} diff --git a/core/modules/inline_form_errors/tests/src/Unit/FormErrorHandlerTest.php b/core/modules/inline_form_errors/tests/src/Unit/FormErrorHandlerTest.php index acd255ba2715..f110b36af959 100644 --- a/core/modules/inline_form_errors/tests/src/Unit/FormErrorHandlerTest.php +++ b/core/modules/inline_form_errors/tests/src/Unit/FormErrorHandlerTest.php @@ -140,12 +140,9 @@ public function testSetElementErrorsFromFormState() { } /** - * Test that Quick Edit forms show non-inline errors. - * - * @covers ::handleFormErrors - * @covers ::displayErrorMessages + * Tests that opting out of Inline Form Errors works. */ - public function testDisplayErrorMessagesNotInlineQuickEdit() { + public function testDisplayErrorMessagesNotInline() { $form_error_handler = $this->getMockBuilder(FormErrorHandler::class) ->setConstructorArgs([$this->getStringTranslationStub(), $this->getMock(LinkGeneratorInterface::class), $this->getMock(RendererInterface::class)]) ->setMethods(['drupalSetMessage']) @@ -157,7 +154,7 @@ public function testDisplayErrorMessagesNotInlineQuickEdit() { $form = [ '#parents' => [], - '#form_id' => 'quickedit_field_form', + '#disable_inline_form_errors' => TRUE, '#array_parents' => [], ]; $form['test'] = [ @@ -165,7 +162,7 @@ public function testDisplayErrorMessagesNotInlineQuickEdit() { '#title' => 'Test', '#parents' => ['test'], '#id' => 'edit-test', - '#array_parents' => ['test'] + '#array_parents' => ['test'], ]; $form_state = new FormState(); $form_state->setErrorByName('test', 'invalid'); diff --git a/core/modules/quickedit/src/Form/QuickEditFieldForm.php b/core/modules/quickedit/src/Form/QuickEditFieldForm.php index 31cc00463a1f..a9e0671a12b1 100644 --- a/core/modules/quickedit/src/Form/QuickEditFieldForm.php +++ b/core/modules/quickedit/src/Form/QuickEditFieldForm.php @@ -116,6 +116,10 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt '#attributes' => ['class' => ['quickedit-form-submit']], ]; + // Use the non-inline form error display for Quick Edit forms, because in + // this case the errors are already near the form element. + $form['#disable_inline_form_errors'] = TRUE; + // Simplify it for optimal in-place use. $this->simplify($form, $form_state); From 895db7dd8a9b83860cdf1f04686ea4c153665199 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Sat, 6 Jan 2018 17:45:39 +1000 Subject: [PATCH 115/232] Issue #2934517 by Berdir: Setting a revision ID on a new entity sets the newRevision flag to false --- core/lib/Drupal/Core/Entity/ContentEntityBase.php | 2 +- .../src/Functional/Entity/EntityRevisionsTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 6094bdfefddb..d755c6f7e6b5 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -768,7 +768,7 @@ public function onChange($name) { // If the revision identifier field is being populated with the original // value, we need to make sure the "new revision" flag is reset // accordingly. - if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId()) { + if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId() && !$this->isNew()) { $this->newRevision = FALSE; } } diff --git a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php index 863f95b332d9..bf8bcb337fd2 100644 --- a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php +++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php @@ -256,6 +256,16 @@ public function testNewRevisionRevert() { $this->assertNull($entity->getRevisionId()); $this->assertEquals($revision_id, $entity->getLoadedRevisionId()); $this->assertTrue($entity->isNewRevision()); + + // Check that calling setNewRevision() on a new entity without a revision ID + // and then with a revision ID does not unset the revision ID. + $entity = EntityTestMulRev::create(['name' => 'EntityLoadedRevisionTest']); + $entity->set('revision_id', NULL); + $entity->set('revision_id', 5); + $this->assertTrue($entity->isNewRevision()); + $entity->setNewRevision(); + $this->assertEquals(5, $entity->get('revision_id')->value); + } } From b23aebd77eb1a798b837f84c31ffaad68f70b60a Mon Sep 17 00:00:00 2001 From: xjm Date: Sat, 6 Jan 2018 15:00:13 -0500 Subject: [PATCH 116/232] Issue #2932369 by marcoscano, balsama, Berdir, Grimreaper, alexpott: Media Types missing access control handler result in empty column in media overview page --- core/modules/media/src/Entity/MediaType.php | 1 + .../src/MediaTypeAccessControlHandler.php | 34 ++++ .../src/Functional/MediaOverviewPageTest.php | 147 ++++++++++++++++++ .../tests/src/Kernel/MediaCreationTest.php | 23 +++ 4 files changed, 205 insertions(+) create mode 100644 core/modules/media/src/MediaTypeAccessControlHandler.php create mode 100644 core/modules/media/tests/src/Functional/MediaOverviewPageTest.php diff --git a/core/modules/media/src/Entity/MediaType.php b/core/modules/media/src/Entity/MediaType.php index 43017b8a4014..89001222184e 100644 --- a/core/modules/media/src/Entity/MediaType.php +++ b/core/modules/media/src/Entity/MediaType.php @@ -21,6 +21,7 @@ * plural = "@count media types" * ), * handlers = { + * "access" = "Drupal\media\MediaTypeAccessControlHandler", * "form" = { * "add" = "Drupal\media\MediaTypeForm", * "edit" = "Drupal\media\MediaTypeForm", diff --git a/core/modules/media/src/MediaTypeAccessControlHandler.php b/core/modules/media/src/MediaTypeAccessControlHandler.php new file mode 100644 index 000000000000..2f134a6db3c4 --- /dev/null +++ b/core/modules/media/src/MediaTypeAccessControlHandler.php @@ -0,0 +1,34 @@ +drupalLogin($this->nonAdminUser); + } + + /** + * Test that the Media overview page (/admin/content/media). + */ + public function testMediaOverviewPage() { + $assert_session = $this->assertSession(); + + // Check the view exists, is access-restricted, and some defaults are there. + $this->drupalGet('/admin/content/media'); + $assert_session->statusCodeEquals(403); + $role = Role::load(RoleInterface::AUTHENTICATED_ID); + $this->grantPermissions($role, ['access media overview']); + $this->drupalGet('/admin/content/media'); + $assert_session->statusCodeEquals(200); + $assert_session->titleEquals('Media | Drupal'); + $assert_session->fieldExists('Media name'); + $assert_session->selectExists('source'); + $assert_session->selectExists('status'); + $assert_session->selectExists('langcode'); + $assert_session->buttonExists('Filter'); + $header = $assert_session->elementExists('css', 'th#view-thumbnail-target-id-table-column'); + $this->assertEquals('Thumbnail', $header->getText()); + $header = $assert_session->elementExists('css', 'th#view-name-table-column'); + $this->assertEquals('Media name', $header->getText()); + $header = $assert_session->elementExists('css', 'th#view-bundle-table-column'); + $this->assertEquals('Source', $header->getText()); + $header = $assert_session->elementExists('css', 'th#view-uid-table-column'); + $this->assertEquals('Author', $header->getText()); + $header = $assert_session->elementExists('css', 'th#view-status-table-column'); + $this->assertEquals('Status', $header->getText()); + $header = $assert_session->elementExists('css', 'th#view-changed-table-column'); + $this->assertEquals('Updated Sort ascending', $header->getText()); + $header = $assert_session->elementExists('css', 'th#view-operations-table-column'); + $this->assertEquals('Operations', $header->getText()); + $assert_session->pageTextContains('No content available.'); + + // Create some content for the view. + $media_type1 = $this->createMediaType(); + $media_type2 = $this->createMediaType(); + $media1 = Media::create([ + 'bundle' => $media_type1->id(), + 'name' => 'Media 1', + 'uid' => $this->adminUser->id(), + ]); + $media1->save(); + $media2 = Media::create([ + 'bundle' => $media_type2->id(), + 'name' => 'Media 2', + 'uid' => $this->adminUser->id(), + 'status' => FALSE, + ]); + $media2->save(); + $media3 = Media::create([ + 'bundle' => $media_type1->id(), + 'name' => 'Media 3', + 'uid' => $this->nonAdminUser->id(), + ]); + $media3->save(); + + // Verify the view is now correctly populated. + $this->grantPermissions($role, [ + 'view media', + 'update any media', + 'delete any media', + ]); + $this->drupalGet('/admin/content/media'); + $row1 = $assert_session->elementExists('css', 'table tbody tr:nth-child(1)'); + $row2 = $assert_session->elementExists('css', 'table tbody tr:nth-child(2)'); + $row3 = $assert_session->elementExists('css', 'table tbody tr:nth-child(3)'); + + // Media thumbnails. + $assert_session->elementExists('css', 'td.views-field-thumbnail__target-id img', $row1); + $assert_session->elementExists('css', 'td.views-field-thumbnail__target-id img', $row2); + $assert_session->elementExists('css', 'td.views-field-thumbnail__target-id img', $row3); + + // Media names. + $name1 = $assert_session->elementExists('css', 'td.views-field-name a', $row1); + $this->assertEquals($media1->label(), $name1->getText()); + $name2 = $assert_session->elementExists('css', 'td.views-field-name a', $row2); + $this->assertEquals($media2->label(), $name2->getText()); + $name3 = $assert_session->elementExists('css', 'td.views-field-name a', $row3); + $this->assertEquals($media3->label(), $name3->getText()); + $assert_session->linkByHrefExists('/media/' . $media1->id()); + $assert_session->linkByHrefExists('/media/' . $media2->id()); + $assert_session->linkByHrefExists('/media/' . $media3->id()); + + // Media types. + $type_element1 = $assert_session->elementExists('css', 'td.views-field-bundle', $row1); + $this->assertEquals($media_type1->label(), $type_element1->getText()); + $type_element2 = $assert_session->elementExists('css', 'td.views-field-bundle', $row2); + $this->assertEquals($media_type2->label(), $type_element2->getText()); + $type_element3 = $assert_session->elementExists('css', 'td.views-field-bundle', $row3); + $this->assertEquals($media_type1->label(), $type_element3->getText()); + + // Media authors. + $author_element1 = $assert_session->elementExists('css', 'td.views-field-uid', $row1); + $this->assertEquals($this->adminUser->getDisplayName(), $author_element1->getText()); + $author_element2 = $assert_session->elementExists('css', 'td.views-field-uid', $row2); + $this->assertEquals($this->adminUser->getDisplayName(), $author_element2->getText()); + $author_element3 = $assert_session->elementExists('css', 'td.views-field-uid', $row3); + $this->assertEquals($this->nonAdminUser->getDisplayName(), $author_element3->getText()); + + // Media publishing status. + $status_element1 = $assert_session->elementExists('css', 'td.views-field-status', $row1); + $this->assertEquals('Published', $status_element1->getText()); + $status_element2 = $assert_session->elementExists('css', 'td.views-field-status', $row2); + $this->assertEquals('Unpublished', $status_element2->getText()); + $status_element3 = $assert_session->elementExists('css', 'td.views-field-status', $row3); + $this->assertEquals('Published', $status_element3->getText()); + + // Timestamp. + $expected = \Drupal::service('date.formatter')->format($media1->getChangedTime(), 'short'); + $changed_element1 = $assert_session->elementExists('css', 'td.views-field-changed', $row1); + $this->assertEquals($expected, $changed_element1->getText()); + + // Operations. + $edit_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.edit a', $row1); + $this->assertEquals('Edit', $edit_link1->getText()); + $assert_session->linkByHrefExists('/media/' . $media1->id() . '/edit'); + $delete_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.delete a', $row1); + $this->assertEquals('Delete', $delete_link1->getText()); + $assert_session->linkByHrefExists('/media/' . $media1->id() . '/delete'); + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaCreationTest.php b/core/modules/media/tests/src/Kernel/MediaCreationTest.php index baf837f2c258..93d43842263a 100644 --- a/core/modules/media/tests/src/Kernel/MediaCreationTest.php +++ b/core/modules/media/tests/src/Kernel/MediaCreationTest.php @@ -6,6 +6,8 @@ use Drupal\media\Entity\MediaType; use Drupal\media\MediaInterface; use Drupal\media\MediaTypeInterface; +use Drupal\user\Entity\Role; +use Drupal\user\Entity\User; /** * Tests creation of media types and media items. @@ -33,6 +35,27 @@ public function testMediaTypeCreation() { // be created automatically when a config is being imported. $this->assertEquals(['source_field' => '', 'test_config_value' => 'Kakec'], $test_media_type->get('source_configuration'), 'Could not assure the correct media source configuration.'); $this->assertEquals(['metadata_attribute' => 'field_attribute_config_test'], $test_media_type->get('field_map'), 'Could not assure the correct field map.'); + // Check the Media Type access handler behavior. + // We grant access to the 'view label' operation to all users having + // permission to 'view media'. + $user1 = User::create([ + 'name' => 'username1', + 'status' => 1, + ]); + $user1->save(); + $user2 = User::create([ + 'name' => 'username2', + 'status' => 1, + ]); + $user2->save(); + $role = Role::create([ + 'id' => 'role1', + 'label' => 'role1', + ]); + $role->grantPermission('view media')->trustData()->save(); + $user2->addRole($role->id()); + $this->assertFalse($test_media_type->access('view label', $user1)); + $this->assertTrue($test_media_type->access('view label', $user2)); } /** From 32fc1125e7a184fd5c63b6700b0fe927aacc3ce3 Mon Sep 17 00:00:00 2001 From: xjm Date: Sat, 6 Jan 2018 15:19:42 -0500 Subject: [PATCH 117/232] Issue #2934840 by robpowell, marcoscano: MediaAccessTest wrongly extends another test class, instead of base class --- .../tests/src/Functional/MediaAccessTest.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/core/modules/media/tests/src/Functional/MediaAccessTest.php b/core/modules/media/tests/src/Functional/MediaAccessTest.php index d5c23041fa3c..b7ec0f8c40c6 100644 --- a/core/modules/media/tests/src/Functional/MediaAccessTest.php +++ b/core/modules/media/tests/src/Functional/MediaAccessTest.php @@ -12,10 +12,27 @@ * * @group media */ -class MediaAccessTest extends MediaUiFunctionalTest { +class MediaAccessTest extends MediaFunctionalTestBase { use AssertPageCacheContextsAndTagsTrait; + /** + * {@inheritdoc} + */ + public static $modules = [ + 'block', + 'media_test_source', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + // This is needed to provide the user cache context for a below assertion. + $this->drupalPlaceBlock('local_tasks_block'); + } + /** * Test some access control functionality. */ From 319f023727e0b735d32dc41d62fbf65e69c5a946 Mon Sep 17 00:00:00 2001 From: xjm Date: Sat, 6 Jan 2018 16:32:21 -0500 Subject: [PATCH 118/232] Issue #2934850 by balsama, Berdir: Media Images should be rendered at a reasonable size by default --- .../src/FunctionalJavascript/MediaDisplayTest.php | 13 ++++++++----- ...core.entity_view_display.media.image.default.yml | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php index 3bd1ccbc099f..47bcf287dde4 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaDisplayTest.php @@ -81,10 +81,13 @@ public function testMediaDisplay() { // Here we expect to see only the image, nothing else. // Assert only one element in the content region. $this->assertEquals(1, count($page->findAll('css', '.media--type-image > div'))); - // Assert the image is present inside the media element, with "medium" - // image style. + // Assert the image is present inside the media element. $media_item = $assert_session->elementExists('css', '.media--type-image > div'); - $assert_session->elementExists('css', 'img.image-style-medium', $media_item); + $assert_session->elementExists('css', 'img', $media_item); + // Assert that the image src is the original image and not an image style. + $media_image = $assert_session->elementExists('css', '.media--type-image img'); + $expected_image_src = file_url_transform_relative(file_create_url(\Drupal::token()->replace('public://[date:custom:Y]-[date:custom:m]/example_1.jpeg'))); + $this->assertEquals($expected_image_src, $media_image->getAttribute('src')); $test_filename = $this->randomMachineName() . '.txt'; $test_filepath = 'public://' . $test_filename; @@ -163,8 +166,8 @@ public function testMediaDisplay() { $assert_session->pageTextNotContains($image_media_name); // Only one element is present inside the media container. $this->assertEquals(1, count($page->findAll('css', '.field--name-field-related-media article.media--type-image > div'))); - // Assert the image is present, with "medium" image style. - $assert_session->elementExists('css', '.field--name-field-related-media article.media--type-image img.image-style-medium'); + // Assert the image is present. + $assert_session->elementExists('css', '.field--name-field-related-media article.media--type-image img'); } } diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml index eee9348a2399..728150f4e7a4 100644 --- a/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml +++ b/core/profiles/standard/config/optional/core.entity_view_display.media.image.default.yml @@ -15,7 +15,7 @@ content: field_media_image: label: visually_hidden settings: - image_style: medium + image_style: '' image_link: file third_party_settings: { } type: image From 28830054bb760b12d3377c8fcd8c2c6850a815bc Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Sat, 6 Jan 2018 09:48:39 +0000 Subject: [PATCH 119/232] Issue #2866805 by Mukeysh, joelpittet, harsha012: Update stylelint rule declaration-block-no-redundant-longhand-properties to be consistent with Drupal's CSS standards --- core/.stylelintrc.json | 1 - 1 file changed, 1 deletion(-) diff --git a/core/.stylelintrc.json b/core/.stylelintrc.json index bb4b610ca24c..b5256711ea4e 100644 --- a/core/.stylelintrc.json +++ b/core/.stylelintrc.json @@ -8,7 +8,6 @@ "color-hex-length": null, "comment-empty-line-before": null, "declaration-block-no-duplicate-properties": null, - "declaration-block-no-redundant-longhand-properties": null, "declaration-block-no-shorthand-property-overrides": null, "function-comma-space-after": null, "function-linear-gradient-no-nonstandard-direction": null, From db680529531a341f5553cfba6daa7fa0c4c16ea4 Mon Sep 17 00:00:00 2001 From: xjm Date: Sun, 7 Jan 2018 10:36:01 -0500 Subject: [PATCH 120/232] =?UTF-8?q?Issue=20#2924631=20by=20marcoscano,=20s?= =?UTF-8?q?tarshaped,=20chr.fritsch,=20xjm,=20phenaproxima,=20G=C3=A1bor?= =?UTF-8?q?=20Hojtsy,=20seanB,=20yoroy,=20evankay:=20Media=20sources=20for?= =?UTF-8?q?=20local=20video=20and=20audio=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/config/schema/media.schema.yml | 8 ++ core/modules/media/images/icons/audio.png | Bin 0 -> 5611 bytes core/modules/media/images/icons/video.png | Bin 0 -> 4436 bytes .../src/Plugin/media/Source/AudioFile.php | 39 +++++++ .../src/Plugin/media/Source/VideoFile.php | 39 +++++++ .../MediaSourceAudioVideoTest.php | 105 ++++++++++++++++++ 6 files changed, 191 insertions(+) create mode 100644 core/modules/media/images/icons/audio.png create mode 100644 core/modules/media/images/icons/video.png create mode 100644 core/modules/media/src/Plugin/media/Source/AudioFile.php create mode 100644 core/modules/media/src/Plugin/media/Source/VideoFile.php create mode 100644 core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml index dad518985a7c..b9156b23f86c 100644 --- a/core/modules/media/config/schema/media.schema.yml +++ b/core/modules/media/config/schema/media.schema.yml @@ -52,6 +52,14 @@ media.source.image: type: media.source.field_aware label: '"Image" media source configuration' +media.source.audio_file: + type: media.source.field_aware + label: '"Audio" media source configuration' + +media.source.video_file: + type: media.source.field_aware + label: '"Video" media source configuration' + media.source.field_aware: type: mapping mapping: diff --git a/core/modules/media/images/icons/audio.png b/core/modules/media/images/icons/audio.png new file mode 100644 index 0000000000000000000000000000000000000000..23efc4a83fd1b102ad579faac2bf14d00d735f19 GIT binary patch literal 5611 zcmZ{Ibx>4M|M#+hEX{(z0uq7}3ro3lNVkO2yM%N~2uKRjOLq!V64JG#fV3=WAR>*x z(y@S)_}gcG&pYqTJM;c=&z(DW=6mirpYQp^Md|CRQ<5>00RR9>O%0W&_;G z*>NiR%TQ#>1~mNsO2<~A?PAv}M*FBqm@;O$MrB1F{xS@>7l>q_L{VlUZ)j-BvR}=+@Q?GLIK8Ht(phWx}>{CiBEFbhC4F z5WWm64L5zk3`Xpa`ggD5)C%N7q<^8^S$gkELlKkOb;ypicTLgJ(PJHZ4-EWM{Mt9b z+ZD+n8_@skmp- zzkZX05GMK@=?WI06CBDh;n{%51oL3fo z+^%40{iDvJu+Mt^cz11vYPexA+_1(Ia~mGSw6ZsA#?AS#M?xQT0ki32!PrRLL%Y?D{gB?`x(p zgt?O<<1?ujR}LRJL4;_ySEfORHIqo86MX7zHr8fCL^@)6etrzUd;Lbd?6qHPKc_uJ zJ$}`cXDXJ*5@}3>2(M6QEfCIQsc`9jrWQYpd->DgQZ0jCH@)1OtNQ7nW?6b4N9h}V zBwh8A({M@_)8N+&8s05mo7i*Mb8TaQ2^6VcMV+ohZ#Wz0rfOiIj~JX^cp9ji*&F(p zi80@C5LxJ)5x%Ea3I9Y|daPm5?cdn6s0*QVJs!sOlMC@O9VE;&pw*Mv+|KuYqM~K& z;Whie+Ui-<+FDy5GIKg$zgK5NC=2-iwMS}|YhbZh>kDnNo8#<2W7jlm#yXi1XL|in zC;AMZ-}lSb^;Q0bKW1G!sX~5@gD1p^?j=9IJ=#Nc*4YS5l}k~DwK2Uk0f;)%i!Dl`)8Tja`N7f zaG#9x^K-w$FRZT?3_kaFSZI!VKCPS)4Z6L->6v+dfUm3Jp?#eBrfED^YQh5wN?eoB zvud=7(s{KBhiSyqgUrJ{ghT)RZOa~C){^`Y5n!}#o)9gXyQ&fN_v{Bh`lyXo*61X1 z{4|9xDs{2bH{Id`1>Go=2S$@#`P~WI;j^7)Xi=D4p!jAU3%-lAdz&CgYhmEWmulPt zzNC&awMA?-YIL2?q(|I!h06=&!|(OJg-&^0*fLtt-dA)lY>=NKF_KxMtd$}iaU&dR5xHyImYRD1{icqu-3cdRf3r7 ztS~E=w^;&)*av@Jm8J0m#wP(4O~n7;!JVs!9{lfj?=qLo4bhasPlN3MXql(4yIQLEsEb`8Gx}m z^g2))5yLH2USDlj)R;cV>N=lyrNgP%Pv7>_z8^IV^we}S{@8S2nN~U^X<;;kZI7Gp zQBz=XApquuiARRr7c;_OR|jI6iJ(tK*LBf`;=M& zWObvuzW$G&b{Ll;%O|qwwnaad0Z2Hcp-xc@+3xx2C6b!t9P}~h&-om?-O19&Ah}*X zJnP|3o=dIQwk?@sf0b5foJ@_CuA_Jgj~EgF$XlL9nE3V}imo^}u&HZxy(^|IXD4$tp%i=F(`5Y+Kh33+=bkFfB(vXhy+ zc0u;C_Eh==f}Cw4YAMd*D#DPR+9u06jGnH2E5cOD{Js)Vkw~}(tVA!qzTVFrN$p5* zk=+dRv9@qer87nX?{%eakaj>QhdQ}yepjh5jcwZaFhQ|#iT|m_QAA1bB>n(DBybl| z-nZ7E6!~v63_1blX@Jq+Xi6yw3W90~aq|Zs|5z$-mOsPo*7r8S5a=>~L;V9}f*+IJ zN|c#SXEw8=dkj*cYs?NzAt1I%WiaUTb*m#x+}+JM#PoRV4|EF3<GtiNZWg+7&o{6vn(z!@qy(!Al@lQ0J!`Y9uYIgC)gk!_^)f3QlS$-%E4$#mSMKwqfg^2@A*u>DiBuUiT<6KxOn*|foDs3 zB+Qj!5)vk}TngEf{{6!c25rRD2OFRHvHWp81s0|Ju>UV0q)y5p$kS(;3BwA1=NfG(6)Vfj zKMYIj+v_l{97L-n-K%onQ>L86B6S}Hj^~IJ{wjjMgs}x;?_0V12D2WMjzhc{oOHf2 zi@9W&)WQ`!=373K5VLWWbBkWxCE?TsE`Ix#5wNz3_jaZTM1Fl<&Zw!0$(HsbRxc{- zOsV>#n<3(!c<$i@d&IJ@k59w13f)T;>^$HL4_tMSuZTc+?(N+bnLH8!_w*+u{=2np z{tSneuCJr@oZ@01DXa(707Z8g7J_7VNIdWqD$mRPB=2{8eu60h^Dll>C|+nO=d z!Y7?rIbJrbXg;M79cGjCR2VOD7c9`LR2r0=CKKm#eB)s#aEL8F)=U)8=Y$#}qYi2Ew-@U+ z3}}yKd8-AxL8c-D=dh?niF6>Fgv}hinSVFmMa~Pig{Nv*NbcDdsxUKPEMW?RR-$ zDoo%G^87!7^M5(!e_Oh=V5W|uW$RP2p4}7|m#Oh}{_4#oE<&33RtPxHWKmnW$(l1p z>@%DE!QL$akArab&yKJc?(P%sy9J_-6{UnqTlc0>dRKoIUQ&}!HuI9F82AB^!csz` zDfPryUS+<&kBPkHq;Khz9W_BT)0!aXPF)38pr|GVK|y3n`7TilH%I#5e_t%hJW)u4 zNYE7V#IWou7&|G2foe%LXxHxmRh%T|-_%S;0>I{UDG!DziDIW;v=EvVlM-;^3uq>= zfSlfyWVD)xT%VuG7u%ZnT-ONtD3NwHV_&ja&a?@A>Cc9x*?J<>~*G0bWTviVrw6hKkh&udKeG9>BzD zid*|kE$`3G5r$d?9}{2iHBXHpQvV@zEt0-S6c>`g#l-`!MV}M)i;{5q+TOJs3Xy1_3?Gb)fYZldG!4lQqA9NJ)76T{cfQ5lrUyUB@SXqvlnA*( z1A_wJ&xPCETpZWnQOxXL4*A1+jV(>isYNN!E8mm8ue9YxaZwvas3LIu*!#DJuJg&7 z-s8V;7D-QQ?Amm(GSlh6P#-?8KsqhsNmZ~3wMz>N<16yv3I!q$LaZ654;U;JbP zPRcT-^#K*abN-~bibm`2vjP(CnG<}mSJQk1ZZ0k-!F?#ZP0qNJu7rFdTz9 zvNj68>NFG_R?X&AZan$F(%H>y1LA4y`}!wVxy9Ix*v z7_wbo3pvXE$bsTlZ)j-ATbIxJbb3yVqXM*>cq_%$n9TlIko$VF5kZfZ(REne{fLsBmZVp+(LudCtjY(Bjw3Ci_|Hr=WxEA2t^EzVIvrRwh(0(W+Ix%RJq zr|Zq({TXLHjy#8vRs#O#a_b$gM$D^*RH+*M;Hu{0D*D}FMPc9v8XH?%OxdFOtHY%U zDt7s|PU7+vw%yXuqUzN+y>4C>-wL|ohp~Qsehf-(vbpP{3CRx-Z7+;=I4PLnqu2Ai zBFozShF=S0Ua48s&HHfr=zR6IS=Wr^@ymM-&o$k{#T-x@7sZy+UhtlLB4KYh?EPnH zERZ3NKO5qHdvp5IdJ6x72BImK&Ru0|jgwTIqOyjyzwdmnmispzfJ$UE^l@z>l3i^S z(OZb24Yo7ueI2r=(=E`6c^VBWD0wg0czw3JCD&-QXxV5efo+cGfTjNWcM#bAE<*>w=%rIYBc z7E}nRf;~aAnh7d~fnWqtX=CvKaA&u>dX_exwzL(B%T72~WQ&i@a>H3b8O5--{~AO? zZ-XVw(`$H%fwP6vIV?KG(%^=l=vmN{cb?Vho^UeCWsL7vh(yu5yYhd7 zS@qm}fNe~Zv%kO_PPf5W@RFNoK04*V4P^c08$JQca{jVJS{F?JZlH55be+2y^84=NJH3n*Fumya=90lf*!|>C|}sl)tHLvBfWr)s;q+ dhaLEj5YvSk46~<1;{VXfX*{|9<}kca>P literal 0 HcmV?d00001 diff --git a/core/modules/media/images/icons/video.png b/core/modules/media/images/icons/video.png new file mode 100644 index 0000000000000000000000000000000000000000..4364cf3a8f4c031b647c198c73250dda5301d4e3 GIT binary patch literal 4436 zcmZu#XH-+&wxt9qk={!b1O!An(o5*Q`Jtgm7eg^f37t@alu!i?AT@N55^6+1I?^F@ z=}1>ZsY>VOzW46?^WKj$_C9B?bM_u<%{Av72W4ukLruj&MMOkIt*5K`m@qc{`=Yo- zL`1AVC{skZ0YKG^)QE^`lYy5GWJE;tKs`-0^8n(#929m+ldCVLkCb`iv12RA9nITR z^o+bFhT@vNq$p%Kg&_C2YJLHIG8=X^g25?4u%H77kPlg<*T|)(dlODU5fH+;J-3Hj zaNS!o52dxP8bhxVvqyz_vEBTmu1wW7kH^4TXz&L#Ne`7Sno zy)Rj|x0E+i2_kWuG+FsiM$;-1IDyOjlcl5+2hBs^gd7ys-a~WsX!1f12hv=#luS6y z=F-VFi>GC13bn}CBy7#WqDQR{O4bkO`No2sa!Y~f7uSi&ca)oJ#*5Met@+2uY42oj zkj*OL0;B0615MHeR^h2$&t6S9jATCg-ra@Z7t()NlPb`CZ&&|I*!9`AzP7VN;t9Go z8h5}k=3M=tQ8LKrOGR1!aMP2m$d|q%m4Qn=R*m#S^F_EmXBz3i1QxL)ZH``?APkmV zyz!jp(88mUy4y6@i#PfxVOuh7T4?^oC#w^=-PaOKjkmX-JaQKduKX#H01*Pu)g0?K zh7U%YfuAzh;g4~ti{`iW*Y%^@Z?1z6zL(|=D(PGEGd)Qxc+TIgFZzPPQYw3>DbBXI zFzjaO6x%xBiB3z`KElnIT3nosRYaA`xl0?$XFyB!IOjBd0U3=c%1=MVr;fZ;5%smY zKf@f7;X!-hZra~z<=tsG}Dx&VGZBCkT*>&aMv6`Gnj+w8DVCm%O z&{b>IA*+O#4_Xc_=6yaiKj`x@3Izl-TrIUvNXPfE0 z<^Pnv<0^aGpD}rLb)_Gsxhwa6#K~~dvRfv(5N>68r=;!dz7^)3bSQV|#hxsqv(Vhv zPd?D^{u-#`tXFO2=9mi2OW9v>@bVHH`NzfCf;pmB_f)QqCmrz^wI|>PL-&G~`Bb=x zNwQgt87JfHMVhfmuF@y|#%2LukQh1!Zdhu0l&$&9CQC%mAO;3IdQkW{4O05e9@O_) zk-E<->H`DPlOuhKS@@~o1?ctUKCY$3`k`f==D>k{cJ#;YXlllz?J+T5CBf2fdl?c= zU7qJhW2$1ld^z?T{VCtOOAwoP(@V0M@138Y15}*VY_A_AyNO6~b)BtBSCK>+SC~qs z+}EG@Qhnzzt3B){v^MlAh!G5gu?Z-9EH)Kw%lQ77>QeVLVe_|AfG@y4@{igDZlWaz zzs_VR#VKVf2m5&sZYaCE7uBOJn&GZhG+{F|NjdU~T{17fh9~;fh|&hc66=mB|INnL zPlWrH?q08_(QLu0NWSJ-YV1t{#fcrP)VS zy;NCPNIR^t39z{^8!Ys2Mr_4D2B}0$i)3MTQ!6r(F~TGjSENK`dEN{~I2osJ8PT6>A#j-Tm1cz}xaafZn-cT3&})u@|FrtoHGTJ74^dlX;@=q7gti@z5oR z+De`1^I#7kIP1+ zcW0`k996I_{%bv`dEA2jc%zV9zb>AJyO08T-$e(&#mu;ooPWtvCs7#9TW!<}AXm8R zn*3Vp^i~x(v8x>XCpM8&cCfn)*BRk)V*9s9?=3(@GUU%3)N!Q^aVWk6^TbU0EWS%= z*{yCNth63{;anrUeJEZ=Sg{srPFO^e0dJ*d%ll}rvODBZ(vT;xEmBqomy#-skom8O z3pt@*=_<5;zglI2kr>8)Mz4ZrpGXJs?x0_D6p$sB&IWw?b^LuYzYex9EQhZW1?ePl z(Z6cH^3x!5l&7G9`|Zq>7-jWT+q5`-v)qAtVkDjq{jr8eNk7b`s1AnKR@)jeHt=2o zjPOYmau1^8l9-PXf0wrVeS>>%-&I*`@~Zj;vOP0gDU0#Q4*pR+`+wUD?LD9O+Z}$kly_ZXeHOukODq5?lmqI|q9B*o-;(3j5z z+|@uQ^$=@zx=KP86rGOU`ZiQ@+tVv=~dVml_Z zr=bVsxoW2Wa$sqOxC2rTM~$OUh;rH*en0RAXa26%204o9;T{qmjl&pNC|{qhuXO>%9_l>_ z&BP3`qXcpT)Onul@&T4I$2#UL>t9g+oSoxw(Q+e*HN^0JUIOOm6I(J&7@sjOFlB#f zmNpib8RF3Xv({od2!}4Hc)IKyg!I$Q!cflB6YKPvjsx~$!wVhDTnG<4%q0G&Tp6p^ z$o1NGE~f)R-r}yWLv;jyvM_SFP_j>_6{=`hWJ5l(BS~{0Jh!=TcCqa}?%2;;Ft25$ z_76Q;M^3Q=4hN!xbT`n)kFx5ZveYzTf=>qt+MnK9=MdDAvlo z|Htjst5*%_C_!+ZqQ7y-ekXZgn>J{6Mi#B^#*Xxx;`rZ|`)``j&?599Nm5*X!GEe! zi^SF1R`9Zr_I|aHojrw3^v1K&$_?k-Q0`A0sK%!STl8B9xI`rFeafLJGf_Fy6llSd zd?t8@sxGI21ayHfGwgZPXm0Kuuk}RPI3A7Yo-X47la8oVU!Fp7UJSbR*S@AzdJ+v3 zQhpAwp@dxZ0BrJXinIUk{O9sCv@e^OVHM5Wp`CAVLTQRWnUjEq0|a-7IHN?T1;A>v z{}q72kiKMUHqRp>v&jSb+|unhr7Lds<&t_~ct`=NC~z`r@e6)un{J?RcMI5iPb%XJ z0a~GSs#nMVoJ;?`;a`1%&ww6ZU;Ji;*!VJYYD5qPd>cv{JDIi1+mJ(?H=OPiCih_5 zuRqx5U%d@rl&`WD-|b>aAVrpftkICenV7bVUoAo} zB1b~TwOCPPKo*M11a~)f$H-suQkzEjCUvO{0U`;FDLPrrW!FyYbPeX?>#K`iR=W@$ z<6Nck%wmy=@6}WaLB}q&@b8bkPWQh(_hvUlPf>O*Fk3PIIzQP}G#>u&(tIKjq$p## zxSk(#C(b*97OhY+2@u<9s9(gIti}mf>#{uV+%0dio~X`8xX)CJlvrZD*1w?8!jQQ4 zQcyQ5DyiUo#Ey7v@9*i?l#BqC;6Fd$r#yOdl4`rMOneseb(v8&Id0&c=5*W*XB`RCa%kdp^A*S%9OpCFT zTvr0SRH|yS1*2q1hYH-VYV#{(ak<;AZhI_`HQ-M8T%tNH6MRR6DLgEaoR)=FcOm8@D->9*ihXLel6n?0p-qw5)IaoT#Zr{vlr@hKB5igff_vRbTl==lfRA3(&1T0jSX_3VCY1{@pJd2=R?oM!SV;JUH<2A*r zltCWWV+loJR5wM;9{gPi`@1IE!ev`iZ)yz?bg_Q!ItwJGng{x^xWyBZ!Qvn81@VB@ zf;&gmVe}XbETz~+j1VUF)v`>4gsDNKu34@JZ2t!tVb4|(WlHXjG?a7~5@!R^U~UIxerC4=ZN7z1vICFw2KO}<$1;9raKweGW`wN8CB zUIeX4O%0d_5Y7(hCE?tuUi2Ok86elIsTsq;7J5FegbIgHV;_1p!U}|TQcEhmqeLap zm4T9ob9e+T$yQ@WLf*4iH3`}0fmF?}(Rymo_^$A);NPG9xjFuU!RG3s+-Yd&%G)&5 zAkY^g3^Dmib#xW~|@Rj()>&zX0qVqn+xrPj?gz_)?t= z8N+)Y5d?z+)#tHTgfWnd@s1kx@gd;)XXx{enxPcpROQ=x_|w8B|KH0S#uOzD&Mx=C zYO+$44{n*Td0{z*uoCYW3Hf3>Yf^VYcvOPej^OtE7F9VVn5z6IvI*5aZHSAWt&yy^ za|hEtlFn&FWI6n)@;KipZ3s!>As!u$VITf?!sRlVj#GGbQmcD9BgqZ)H@`O%c&~Yb z>2RQLc1Pd72o#ARVS-bxe}K3srD;PP3F`7w|6NU^S0+*he6llMs0Tq}9oWOmU6Dvp z;;$YO2$2YpoK8GHZ|t|5ALVLZ7L0c%cz zu7Uu<4ir`3y9opgCgvCA(-Gjp(?Eg{*A7R@M5jipbA#1l@Knd0Y=ru=>2pv;5RrlY z{|`{u;=5yLWuIDdOz_faSOZk$To2ZIgQ_|fL0TOP%p5+A5)A^xd9Yn0JOY8(o6D?3 z@6X|u0aw!yJASNC?bSj&k*%`E!Z}ss(<-aY6VQ2ewdnZdB=3ErOui3o?b|?(AsAnH zdV~as^!3l+a_Zkm56x~~20ZU-)lmMnjL%!J3V{IffKI&pU^Sh2Pq-pMFLK7BYEu|6 x7+#oMbB;+ka&)`vt1dxjQe?Q6Mg{h16%LkKpovsK!fz5HJuPF+YIS?$zW{Yjc47bk literal 0 HcmV?d00001 diff --git a/core/modules/media/src/Plugin/media/Source/AudioFile.php b/core/modules/media/src/Plugin/media/Source/AudioFile.php new file mode 100644 index 000000000000..22291434e971 --- /dev/null +++ b/core/modules/media/src/Plugin/media/Source/AudioFile.php @@ -0,0 +1,39 @@ +set('settings', ['file_extensions' => 'mp3 wav aac']); + } + + /** + * {@inheritdoc} + */ + public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) { + $display->setComponent($this->getSourceFieldDefinition($type)->getName(), [ + 'type' => 'file_audio', + ]); + } + +} diff --git a/core/modules/media/src/Plugin/media/Source/VideoFile.php b/core/modules/media/src/Plugin/media/Source/VideoFile.php new file mode 100644 index 000000000000..214d36c41a3d --- /dev/null +++ b/core/modules/media/src/Plugin/media/Source/VideoFile.php @@ -0,0 +1,39 @@ +set('settings', ['file_extensions' => 'mp4']); + } + + /** + * {@inheritdoc} + */ + public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) { + $display->setComponent($this->getSourceFieldDefinition($type)->getName(), [ + 'type' => 'file_video', + ]); + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php new file mode 100644 index 000000000000..4ad3c5be08e8 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php @@ -0,0 +1,105 @@ +assertSession(); + $page = $this->getSession()->getPage(); + + $source_id = 'audio_file'; + $type_name = 'audio_type'; + $field_name = 'field_media_' . $source_id; + $this->doTestCreateMediaType($type_name, $source_id); + + // Check that the source field was created with the correct settings. + $storage = FieldStorageConfig::load("media.$field_name"); + $this->assertInstanceOf(FieldStorageConfig::class, $storage); + $field = FieldConfig::load("media.$type_name.$field_name"); + $this->assertInstanceOf(FieldConfig::class, $field); + $this->assertSame('mp3 wav aac', FieldConfig::load("media.$type_name.$field_name")->get('settings')['file_extensions']); + + // Check that the display holds the correct formatter configuration. + $display = EntityViewDisplay::load("media.$type_name.default"); + $this->assertInstanceOf(EntityViewDisplay::class, $display); + $formatter = $display->getComponent($field_name)['type']; + $this->assertSame('file_audio', $formatter); + + // Create a media asset and verify that the