diff --git a/.eslintrc b/.eslintrc index 0ebfd5a86..9c0e3e698 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,97 +1,3 @@ { - "extends": "eslint:recommended", - "env": { - "browser": true - }, - "globals": { - "Drupal": true, - "drupalSettings": true, - "drupalTranslations": true, - "domready": true, - "jQuery": true, - "_": true, - "matchMedia": true, - "Backbone": true, - "Modernizr": true, - "CKEDITOR": true - }, - "rules": { - // Errors. - "array-bracket-spacing": [2, "never"], - "block-scoped-var": 2, - "brace-style": [2, "stroustrup", {"allowSingleLine": true}], - "comma-dangle": [2, "never"], - "comma-spacing": 2, - "comma-style": [2, "last"], - "computed-property-spacing": [2, "never"], - "curly": [2, "all"], - "eol-last": 2, - "eqeqeq": [2, "smart"], - "guard-for-in": 2, - "indent": [2, 2, {"SwitchCase": 1}], - "key-spacing": [2, {"beforeColon": false, "afterColon": true}], - "linebreak-style": [2, "unix"], - "lines-around-comment": [2, {"beforeBlockComment": true, "afterBlockComment": false}], - "new-parens": 2, - "no-array-constructor": 2, - "no-caller": 2, - "no-catch-shadow": 2, - "no-empty-label": 2, - "no-eval": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-parens": [2, "functions"], - "no-implied-eval": 2, - "no-iterator": 2, - "no-label-var": 2, - "no-labels": 2, - "no-lone-blocks": 2, - "no-loop-func": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-native-reassign": 2, - "no-nested-ternary": 2, - "no-new-func": 2, - "no-new-object": 2, - "no-new-wrappers": 2, - "no-octal-escape": 2, - "no-process-exit": 2, - "no-proto": 2, - "no-return-assign": 2, - "no-script-url": 2, - "no-sequences": 2, - "no-shadow-restricted-names": 2, - "no-spaced-func": 2, - "no-trailing-spaces": 2, - "no-undef-init": 2, - "no-undefined": 2, - "no-unused-expressions": 2, - "no-unused-vars": [2, {"vars": "all", "args": "none"}], - "no-with": 2, - "object-curly-spacing": [2, "never"], - "one-var": [2, "never"], - "quote-props": [2, "consistent-as-needed"], - "quotes": [2, "single", "avoid-escape"], - "semi": [2, "always"], - "semi-spacing": [2, {"before": false, "after": true}], - "space-after-keywords": [2, "always"], - "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], - "space-in-parens": [2, "never"], - "space-infix-ops": 2, - "space-return-throw-case": 2, - "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-comment": [2, "always"], - "strict": 2, - "yoda": [2, "never"], - // Warnings. - "max-nested-callbacks": [1, 3], - "valid-jsdoc": [1, { - "prefer": { - "returns": "return", - "property": "prop" - }, - "requireReturn": false - }] - } + "extends": "./core/.eslintrc" } diff --git a/.htaccess b/.htaccess index 01c63af98..974999a9c 100644 --- a/.htaccess +++ b/.htaccess @@ -3,7 +3,7 @@ # # Protect files and directories from prying eyes. - + Require all denied diff --git a/core/.eslintrc b/core/.eslintrc new file mode 100644 index 000000000..0ebfd5a86 --- /dev/null +++ b/core/.eslintrc @@ -0,0 +1,97 @@ +{ + "extends": "eslint:recommended", + "env": { + "browser": true + }, + "globals": { + "Drupal": true, + "drupalSettings": true, + "drupalTranslations": true, + "domready": true, + "jQuery": true, + "_": true, + "matchMedia": true, + "Backbone": true, + "Modernizr": true, + "CKEDITOR": true + }, + "rules": { + // Errors. + "array-bracket-spacing": [2, "never"], + "block-scoped-var": 2, + "brace-style": [2, "stroustrup", {"allowSingleLine": true}], + "comma-dangle": [2, "never"], + "comma-spacing": 2, + "comma-style": [2, "last"], + "computed-property-spacing": [2, "never"], + "curly": [2, "all"], + "eol-last": 2, + "eqeqeq": [2, "smart"], + "guard-for-in": 2, + "indent": [2, 2, {"SwitchCase": 1}], + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "linebreak-style": [2, "unix"], + "lines-around-comment": [2, {"beforeBlockComment": true, "afterBlockComment": false}], + "new-parens": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 2, + "no-empty-label": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-parens": [2, "functions"], + "no-implied-eval": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-nested-ternary": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-wrappers": 2, + "no-octal-escape": 2, + "no-process-exit": 2, + "no-proto": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-undef-init": 2, + "no-undefined": 2, + "no-unused-expressions": 2, + "no-unused-vars": [2, {"vars": "all", "args": "none"}], + "no-with": 2, + "object-curly-spacing": [2, "never"], + "one-var": [2, "never"], + "quote-props": [2, "consistent-as-needed"], + "quotes": [2, "single", "avoid-escape"], + "semi": [2, "always"], + "semi-spacing": [2, {"before": false, "after": true}], + "space-after-keywords": [2, "always"], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-return-throw-case": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always"], + "strict": 2, + "yoda": [2, "never"], + // Warnings. + "max-nested-callbacks": [1, 3], + "valid-jsdoc": [1, { + "prefer": { + "returns": "return", + "property": "prop" + }, + "requireReturn": false + }] + } +} diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 769e34cf5..25cc7c470 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -328,10 +328,10 @@ function theme_get_setting($setting_name, $theme = NULL) { // Generate the path to the logo image. if ($cache[$theme]->get('logo.use_default')) { - $cache[$theme]->set('logo.url', file_create_url($theme_object->getPath() . '/logo.svg')); + $cache[$theme]->set('logo.url', file_url_transform_relative(file_create_url($theme_object->getPath() . '/logo.svg'))); } elseif ($logo_path = $cache[$theme]->get('logo.path')) { - $cache[$theme]->set('logo.url', file_create_url($logo_path)); + $cache[$theme]->set('logo.url', file_url_transform_relative(file_create_url($logo_path))); } // Generate the path to the favicon. @@ -339,14 +339,14 @@ function theme_get_setting($setting_name, $theme = NULL) { $favicon_path = $cache[$theme]->get('favicon.path'); if ($cache[$theme]->get('favicon.use_default')) { if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) { - $cache[$theme]->set('favicon.url', file_create_url($favicon)); + $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url($favicon))); } else { - $cache[$theme]->set('favicon.url', file_create_url('core/misc/favicon.ico')); + $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url('core/misc/favicon.ico'))); } } elseif ($favicon_path) { - $cache[$theme]->set('favicon.url', file_create_url($favicon_path)); + $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url($favicon_path))); } else { $cache[$theme]->set('features.favicon', FALSE); @@ -741,7 +741,7 @@ function template_preprocess_links(&$variables) { */ function template_preprocess_image(&$variables) { if (!empty($variables['uri'])) { - $variables['attributes']['src'] = file_create_url($variables['uri']); + $variables['attributes']['src'] = file_url_transform_relative(file_create_url($variables['uri'])); } // Generate a srcset attribute conforming to the spec at // http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-srcset @@ -749,7 +749,7 @@ function template_preprocess_image(&$variables) { $srcset = array(); foreach ($variables['srcset'] as $src) { // URI is mandatory. - $source = file_create_url($src['uri']); + $source = file_url_transform_relative(file_create_url($src['uri'])); if (isset($src['width']) && !empty($src['width'])) { $source .= ' ' . $src['width']; } diff --git a/core/install.php b/core/install.php index b70cec530..50deb38f8 100644 --- a/core/install.php +++ b/core/install.php @@ -7,6 +7,8 @@ // Change the directory to the Drupal root. chdir('..'); +// Store the Drupal root path. +$root_path = realpath(''); /** * Global flag to indicate the site is in installation mode. @@ -32,6 +34,6 @@ if (function_exists('opcache_get_status') && opcache_get_status()['opcache_enabl } // Start the installer. -$class_loader = require_once 'autoload.php'; -require_once __DIR__ . '/includes/install.core.inc'; +$class_loader = require_once $root_path . '/autoload.php'; +require_once $root_path . '/core/includes/install.core.inc'; install_drupal($class_loader); diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 8a7ddb0e1..16231e931 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -81,7 +81,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '8.0.2'; + const VERSION = '8.0.3'; /** * Core API compatibility. diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php index 49e4675e1..0f70dbf59 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php @@ -135,7 +135,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface { // assets: output a LINK tag for a file CSS asset. if (count($css_assets) <= 31) { $element = $link_element_defaults; - $element['#attributes']['href'] = file_create_url($css_asset['data']) . $query_string_separator . $query_string; + $element['#attributes']['href'] = file_url_transform_relative(file_create_url($css_asset['data'])) . $query_string_separator . $query_string; $element['#attributes']['media'] = $css_asset['media']; $element['#browsers'] = $css_asset['browsers']; $elements[] = $element; @@ -148,7 +148,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface { // LINK tag. if (!$css_asset['preprocess']) { $element = $link_element_defaults; - $element['#attributes']['href'] = file_create_url($css_asset['data']) . $query_string_separator . $query_string; + $element['#attributes']['href'] = file_url_transform_relative(file_create_url($css_asset['data'])) . $query_string_separator . $query_string; $element['#attributes']['media'] = $css_asset['media']; $element['#browsers'] = $css_asset['browsers']; $elements[] = $element; @@ -168,7 +168,7 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface { // control browser-caching. IE7 does not support a media type on // the @import statement, so we instead specify the media for // the group on the STYLE tag. - $import[] = '@import url("' . Html::escape(file_create_url($next_css_asset['data']) . '?' . $query_string) . '");'; + $import[] = '@import url("' . Html::escape(file_url_transform_relative(file_create_url($next_css_asset['data'])) . '?' . $query_string) . '");'; // Move the outer for loop skip the next item, since we // processed it here. $i = $j; diff --git a/core/lib/Drupal/Core/Asset/CssOptimizer.php b/core/lib/Drupal/Core/Asset/CssOptimizer.php index dc34a237a..704ee571e 100644 --- a/core/lib/Drupal/Core/Asset/CssOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssOptimizer.php @@ -265,7 +265,7 @@ class CssOptimizer implements AssetOptimizerInterface { $last = $path; $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path); } - return 'url(' . file_create_url($path) . ')'; + return 'url(' . file_url_transform_relative(file_create_url($path)) . ')'; } } diff --git a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php index 70832e9d9..b0d22f15c 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php @@ -79,7 +79,7 @@ class JsCollectionRenderer implements AssetCollectionRendererInterface { case 'file': $query_string = $js_asset['version'] == -1 ? $default_query_string : 'v=' . $js_asset['version']; $query_string_separator = (strpos($js_asset['data'], '?') !== FALSE) ? '&' : '?'; - $element['#attributes']['src'] = file_create_url($js_asset['data']); + $element['#attributes']['src'] = file_url_transform_relative(file_create_url($js_asset['data'])); // Only add the cache-busting query string if this isn't an aggregate // file. if (!isset($js_asset['preprocessed'])) { diff --git a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php index 2dc6caaab..136238cf6 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php +++ b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideBase.php @@ -18,7 +18,7 @@ abstract class ConfigFactoryOverrideBase implements EventSubscriberInterface { * Reacts to the ConfigEvents::COLLECTION_INFO event. * * @param \Drupal\Core\Config\ConfigCollectionInfo $collection_info - * The configuration collection names event. + * The configuration collection info event. */ abstract public function addCollections(ConfigCollectionInfo $collection_info); diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 8c0c376c8..6627b3d2f 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -39,16 +39,6 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface */ protected $originalId; - /** - * The name of the property that is used to store plugin configuration. - * - * This is needed when the entity uses a LazyPluginCollection, to dictate - * where the plugin configuration should be stored. - * - * @var string - */ - protected $pluginConfigKey; - /** * The enabled/disabled status of the configuration entity. * diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php index 766fa6ffe..42c9ab5c9 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php @@ -169,9 +169,6 @@ interface ConfigEntityInterface extends EntityInterface, ThirdPartySettingsInter * Dependency types are, for example, entity, module and theme. * * @return bool - * TRUE if the entity has changed, FALSE if not. - * - * @return bool * TRUE if the entity has been changed as a result, FALSE if not. * * @see \Drupal\Core\Config\Entity\ConfigDependencyManager diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index c630fec2e..03b0464b5 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -95,12 +95,13 @@ class FileStorage implements StorageInterface { if (!$this->exists($name)) { return FALSE; } - $data = file_get_contents($this->getFilePath($name)); + $filepath = $this->getFilePath($name); + $data = file_get_contents($filepath); try { $data = $this->decode($data); } catch (InvalidDataTypeException $e) { - throw new UnsupportedDataTypeConfigException("Invalid data type in config $name: {$e->getMessage()}"); + throw new UnsupportedDataTypeConfigException('Invalid data type in config ' . $name . ', found in file' . $filepath . ' : ' . $e->getMessage()); } return $data; } diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index ca0eb5070..b2c94ed3d 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -86,6 +86,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI // Add default values for data type and replace variables. $definition += array('type' => 'undefined'); + $replace = []; $type = $definition['type']; if (strpos($type, ']')) { // Replace variable names in definition. @@ -102,7 +103,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI unset($definition['type']); } // Add default values from type definition. - $definition += $this->getDefinition($type); + $definition += $this->_getDefinitionWithReplacements($type, $replace); $data_definition = $this->createDataDefinition($definition['type']); @@ -116,10 +117,17 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI } /** - * {@inheritdoc} + * Determines the typed config type for a plugin ID. + * + * @param string $base_plugin_id + * The plugin ID. + * @param array $definitions + * An array of typed config definitions. + * + * @return string + * The typed config type for the given plugin ID. */ - public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) { - $definitions = $this->getDefinitions(); + protected function _determineType($base_plugin_id, array $definitions) { if (isset($definitions[$base_plugin_id])) { $type = $base_plugin_id; } @@ -131,6 +139,27 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI // If we don't have definition, return the 'undefined' element. $type = 'undefined'; } + return $type; + } + + /** + * Gets a schema definition with replacements for dynamic names. + * + * @param string $base_plugin_id + * A plugin ID. + * @param array $replacements + * An array of replacements for dynamic type names. + * @param bool $exception_on_invalid + * (optional) This parameter is passed along to self::getDefinition(). + * However, self::getDefinition() does not respect this parameter, so it is + * effectively useless in this context. + * + * @return array + * A schema definition array. + */ + protected function _getDefinitionWithReplacements($base_plugin_id, array $replacements, $exception_on_invalid = TRUE) { + $definitions = $this->getDefinitions(); + $type = $this->_determineType($base_plugin_id, $definitions); $definition = $definitions[$type]; // Check whether this type is an extension of another one and compile it. if (isset($definition['type'])) { @@ -138,6 +167,15 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI // Preserve integer keys on merge, so sequence item types can override // parent settings as opposed to adding unused second, third, etc. items. $definition = NestedArray::mergeDeepArray(array($merge, $definition), TRUE); + + // Replace dynamic portions of the definition type. + if (!empty($replacements) && strpos($definition['type'], ']')) { + $sub_type = $this->_determineType($this->replaceName($definition['type'], $replacements), $definitions); + // Merge the newly determined subtype definition with the original + // definition. + $definition = NestedArray::mergeDeepArray([$definitions[$sub_type], $definition], TRUE); + } + // Unset type so we try the merge only once per type. unset($definition['type']); $this->definitions[$type] = $definition; @@ -150,6 +188,13 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI return $definition; } + /** + * {@inheritdoc} + */ + public function getDefinition($base_plugin_id, $exception_on_invalid = TRUE) { + return $this->_getDefinitionWithReplacements($base_plugin_id, [], $exception_on_invalid); + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php b/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php index 95cdf53dc..2a811e6dc 100644 --- a/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php +++ b/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php @@ -64,10 +64,9 @@ class PagerSelectExtender extends SelectExtender { * to it. */ public function execute() { - - // Add convenience tag to mark that this is an extended query. We have to - // do this in the constructor to ensure that it is set before preExecute() - // gets called. + // By calling preExecute() here, we force it to preprocess the extender + // object rather than just the base query object. That means + // hook_query_alter() gets access to the extended object. if (!$this->preExecute($this)) { return NULL; } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 06938a49b..d6c6f4501 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -968,11 +968,13 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { } } - // If the class loader is still the same, possibly upgrade to the APCu class + // If the class loader is still the same, possibly upgrade to the APC class // loader. + // ApcClassLoader does not support APCu without backwards compatibility + // enabled. if ($class_loader_class == get_class($this->classLoader) && Settings::get('class_loader_auto_detect', TRUE) - && function_exists('apcu_fetch')) { + && extension_loaded('apc')) { $prefix = Settings::getApcuPrefix('class_loader', $this->root); $apc_loader = new \Symfony\Component\ClassLoader\ApcClassLoader($prefix, $this->classLoader); $this->classLoader->unregister(); diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 5eb5c30bb..3e58501ae 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -440,7 +440,8 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con protected function invokeFieldMethod($method, ContentEntityInterface $entity) { $result = []; $args = array_slice(func_get_args(), 2); - foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { + $langcodes = array_keys($entity->getTranslationLanguages()); + foreach ($langcodes as $langcode) { $translation = $entity->getTranslation($langcode); // For non translatable fields, there is only one field object instance // across all translations and it has as parent entity the entity in the @@ -453,6 +454,20 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}(); } } + + // We need to call the delete method for field items of removed + // translations. + if ($method == 'postSave' && !empty($entity->original)) { + $original_langcodes = array_keys($entity->original->getTranslationLanguages()); + foreach (array_diff($original_langcodes, $langcodes) as $removed_langcode) { + $translation = $entity->original->getTranslation($removed_langcode); + $fields = $translation->getTranslatableFields(); + foreach ($fields as $name => $items) { + $items->delete(); + } + } + } + return $result; } diff --git a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php index 4273a4165..d94e2b5fb 100644 --- a/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php +++ b/core/lib/Drupal/Core/Entity/Element/EntityAutocomplete.php @@ -330,11 +330,11 @@ class EntityAutocomplete extends Textfield { // Take "label (entity id)', match the ID from parenthesis when it's a // number. - if (preg_match("/.+\((\d+)\)/", $input, $matches)) { + if (preg_match("/.+\s\((\d+)\)/", $input, $matches)) { $match = $matches[1]; } // Match the ID when it's a string (e.g. for config entity types). - elseif (preg_match("/.+\(([\w.]+)\)/", $input, $matches)) { + elseif (preg_match("/.+\s\(([\w.]+)\)/", $input, $matches)) { $match = $matches[1]; } diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php index 6fd0b9997..da0ff6646 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php @@ -83,8 +83,11 @@ class AuthenticationSubscriber implements EventSubscriberInterface { $account = $this->authenticationProvider->authenticate($request); if ($account) { $this->accountProxy->setAccount($account); + return; } } + // No account has been set explicitly, initialize the timezone here. + date_default_timezone_set(drupal_get_user_timezone()); } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index d614fdbcd..3d61d2e61 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -237,7 +237,9 @@ interface ModuleHandlerInterface { * * @return array * An array of return values of the hook implementations. If modules return - * arrays from their implementations, those are merged into one array. + * 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(). */ public function invokeAll($hook, array $args = array()); diff --git a/core/lib/Drupal/Core/Field/FieldItemBase.php b/core/lib/Drupal/Core/Field/FieldItemBase.php index c73c7fd03..033b82e63 100644 --- a/core/lib/Drupal/Core/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php @@ -70,7 +70,7 @@ abstract class FieldItemBase extends Map implements FieldItemInterface { * {@inheritdoc} */ public function getLangcode() { - return $this->parent->getLangcode(); + return $this->getParent()->getLangcode(); } /** diff --git a/core/lib/Drupal/Core/Field/FieldItemListInterface.php b/core/lib/Drupal/Core/Field/FieldItemListInterface.php index df595e520..8671c5bdc 100644 --- a/core/lib/Drupal/Core/Field/FieldItemListInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemListInterface.php @@ -102,28 +102,28 @@ interface FieldItemListInterface extends ListInterface, AccessibleInterface { /** * Magic method: Gets a property value of to the first field item. * - * @see \Drupal\Core\Field\FieldItemInterface::__get() + * @see \Drupal\Core\Field\FieldItemInterface::__set() */ public function __get($property_name); /** * Magic method: Sets a property value of the first field item. * - * @see \Drupal\Core\Field\FieldItemInterface::__set() + * @see \Drupal\Core\Field\FieldItemInterface::__get() */ public function __set($property_name, $value); /** * Magic method: Determines whether a property of the first field item is set. * - * @see \Drupal\Core\Field\FieldItemInterface::__isset() + * @see \Drupal\Core\Field\FieldItemInterface::__unset() */ public function __isset($property_name); /** * Magic method: Unsets a property of the first field item. * - * @see \Drupal\Core\Field\FieldItemInterface::__unset() + * @see \Drupal\Core\Field\FieldItemInterface::__isset() */ public function __unset($property_name); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/BooleanFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/BooleanFormatter.php index 28229051c..74c7523f1 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/BooleanFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/BooleanFormatter.php @@ -83,6 +83,7 @@ class BooleanFormatter extends FormatterBase { } } + $field_name = $this->fieldDefinition->getName(); $form['format'] = [ '#type' => 'select', '#title' => $this->t('Output format'), @@ -95,7 +96,7 @@ class BooleanFormatter extends FormatterBase { '#default_value' => $this->getSetting('format_custom_true'), '#states' => [ 'visible' => [ - 'select[name="fields[field_boolean][settings_edit_form][settings][format]"]' => ['value' => 'custom'], + 'select[name="fields[' . $field_name . '][settings_edit_form][settings][format]"]' => ['value' => 'custom'], ], ], ]; @@ -105,7 +106,7 @@ class BooleanFormatter extends FormatterBase { '#default_value' => $this->getSetting('format_custom_false'), '#states' => [ 'visible' => [ - 'select[name="fields[field_boolean][settings_edit_form][settings][format]"]' => ['value' => 'custom'], + 'select[name="fields[' . $field_name . '][settings_edit_form][settings][format]"]' => ['value' => 'custom'], ], ], ]; diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/DecimalFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/DecimalFormatter.php index 0e43ac11d..a53e0b9f6 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/DecimalFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/DecimalFormatter.php @@ -55,7 +55,7 @@ class DecimalFormatter extends NumericFormatterBase { $range = range(0, 10); $elements['scale'] = array( '#type' => 'select', - '#title' => t('Scale', array(), array('decimal places')), + '#title' => t('Scale', array(), array('context' => 'decimal places')), '#options' => array_combine($range, $range), '#default_value' => $this->getSetting('scale'), '#description' => t('The number of digits to the right of the decimal.'), diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php index e7030844d..afa7586f9 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php @@ -131,7 +131,7 @@ class TimestampFormatter extends FormatterBase implements ContainerFactoryPlugin ); $elements['custom_date_format']['#states']['visible'][] = array( - ':input[name="options[settings][date_format]"]' => array('value' => 'custom'), + ':input[name="name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][date_format]"]' => array('value' => 'custom'), ); $elements['timezone'] = array( diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php index e3867d4ba..1f40b298b 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php @@ -81,7 +81,7 @@ class DecimalItem extends NumericItemBase { $range = range(0, 10); $element['scale'] = array( '#type' => 'select', - '#title' => t('Scale', array(), array('decimal places')), + '#title' => t('Scale', array(), array('context' => 'decimal places')), '#options' => array_combine($range, $range), '#default_value' => $settings['scale'], '#description' => t('The number of digits to the right of the decimal.'), diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php index 7caa26fa4..581adf24c 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/PasswordItem.php @@ -62,6 +62,9 @@ class PasswordItem extends StringItem { $this->value = $entity->original->{$this->getFieldDefinition()->getName()}->value; } } + // Ensure that the existing password is unset to minimise risks of it + // getting serialized and stored somewhere. + $this->existing = NULL; } /** diff --git a/core/lib/Drupal/Core/Field/WidgetBaseInterface.php b/core/lib/Drupal/Core/Field/WidgetBaseInterface.php index 541f7c5e5..18683c6f4 100644 --- a/core/lib/Drupal/Core/Field/WidgetBaseInterface.php +++ b/core/lib/Drupal/Core/Field/WidgetBaseInterface.php @@ -75,7 +75,7 @@ interface WidgetBaseInterface extends PluginSettingsInterface { /** * Retrieves processing information about the widget from $form_state. * - * This method is static so that is can be used in static Form API callbacks. + * This method is static so that it can be used in static Form API callbacks. * * @param array $parents * The array of #parents where the field lives in the form. @@ -95,7 +95,7 @@ interface WidgetBaseInterface extends PluginSettingsInterface { /** * Stores processing information about the widget in $form_state. * - * This method is static so that is can be used in static Form API #callbacks. + * This method is static so that it can be used in static Form API #callbacks. * * @param array $parents * The array of #parents where the widget lives in the form. diff --git a/core/lib/Drupal/Core/Form/form.api.php b/core/lib/Drupal/Core/Form/form.api.php index adb2d34d8..19c2543c4 100644 --- a/core/lib/Drupal/Core/Form/form.api.php +++ b/core/lib/Drupal/Core/Form/form.api.php @@ -146,8 +146,6 @@ function callback_batch_finished($success, $results, $operations) { * * @param \Drupal\Core\Ajax\CommandInterface[] $data * An array of all the rendered commands that will be sent to the client. - * - * @see \Drupal\Core\Ajax\AjaxResponse::ajaxRender() */ function hook_ajax_render_alter(array &$data) { // Inject any new status messages into the content area. diff --git a/core/lib/Drupal/Core/Language/LanguageManager.php b/core/lib/Drupal/Core/Language/LanguageManager.php index 4c0172dfd..d29b27394 100644 --- a/core/lib/Drupal/Core/Language/LanguageManager.php +++ b/core/lib/Drupal/Core/Language/LanguageManager.php @@ -253,6 +253,7 @@ class LanguageManager implements LanguageManagerInterface { 'dz' => array('Dzongkha', 'རྫོང་ཁ'), 'el' => array('Greek', 'Ελληνικά'), 'en' => array('English', 'English'), + 'en-x-simple' => array('Simple English', 'Simple English'), 'eo' => array('Esperanto', 'Esperanto'), 'es' => array('Spanish', 'Español'), 'et' => array('Estonian', 'Eesti'), diff --git a/core/lib/Drupal/Core/Logger/LoggerChannel.php b/core/lib/Drupal/Core/Logger/LoggerChannel.php index 77b84a85e..8e0890c99 100644 --- a/core/lib/Drupal/Core/Logger/LoggerChannel.php +++ b/core/lib/Drupal/Core/Logger/LoggerChannel.php @@ -93,9 +93,17 @@ class LoggerChannel implements LoggerChannelInterface { $context['request_uri'] = $request->getUri(); $context['referer'] = $request->headers->get('Referer', ''); $context['ip'] = $request->getClientIP(); - if ($this->currentUser) { - $context['user'] = $this->currentUser; - $context['uid'] = $this->currentUser->id(); + try { + if ($this->currentUser) { + $context['user'] = $this->currentUser; + $context['uid'] = $this->currentUser->id(); + } + } + catch (\Exception $e) { + // An exception might be thrown if the database connection is not + // available or due to another unexpected reason. It is more important + // to log the error that we already have so any additional exceptions + // are ignored. } } diff --git a/core/lib/Drupal/Core/Logger/LoggerChannelFactoryInterface.php b/core/lib/Drupal/Core/Logger/LoggerChannelFactoryInterface.php index 67d178ccb..a8125d025 100644 --- a/core/lib/Drupal/Core/Logger/LoggerChannelFactoryInterface.php +++ b/core/lib/Drupal/Core/Logger/LoggerChannelFactoryInterface.php @@ -23,10 +23,7 @@ interface LoggerChannelFactoryInterface { public function get($channel); /** - * Adds a logger. - * - * Here is were all services tagged as 'logger' are being retrieved and then - * passed to the channels after instantiation. + * Adds a logger to all the channels. * * @param \Psr\Log\LoggerInterface $logger * The PSR-3 logger to add. diff --git a/core/lib/Drupal/Core/Menu/LocalActionManager.php b/core/lib/Drupal/Core/Menu/LocalActionManager.php index eb1e2d037..f98e60e22 100644 --- a/core/lib/Drupal/Core/Menu/LocalActionManager.php +++ b/core/lib/Drupal/Core/Menu/LocalActionManager.php @@ -8,6 +8,9 @@ namespace Drupal\Core\Menu; use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManagerInterface; @@ -182,8 +185,10 @@ class LocalActionManager extends DefaultPluginManager implements LocalActionMana $links = array(); /** @var $plugin \Drupal\Core\Menu\LocalActionInterface */ foreach ($this->instances[$route_appears] as $plugin_id => $plugin) { + $cacheability = new CacheableMetadata(); $route_name = $plugin->getRouteName(); $route_parameters = $plugin->getRouteParameters($this->routeMatch); + $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE); $links[$plugin_id] = array( '#theme' => 'menu_local_action', '#link' => array( @@ -191,10 +196,22 @@ class LocalActionManager extends DefaultPluginManager implements LocalActionMana 'url' => Url::fromRoute($route_name, $route_parameters), 'localized_options' => $plugin->getOptions($this->routeMatch), ), - '#access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE), + '#access' => $access, '#weight' => $plugin->getWeight(), ); + $cacheability->addCacheableDependency($access); + // For backward compatibility in 8.0.x, plugins that do not implement + // the \Drupal\Core\Cache\CacheableDependencyInterface are assumed + // to be cacheable forever. + if ($plugin instanceof CacheableDependencyInterface) { + $cacheability->addCacheableDependency($plugin); + } + else { + $cacheability->setCacheMaxAge(Cache::PERMANENT); + } + $cacheability->applyTo($links[$plugin_id]); } + $links['#cache']['contexts'][] = 'route'; return $links; } diff --git a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php index da16e918a..eda4a8785 100644 --- a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php +++ b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php @@ -8,7 +8,6 @@ namespace Drupal\Core\Menu\Plugin\Block; use Drupal\Core\Block\BlockBase; -use Drupal\Core\Cache\Cache; use Drupal\Core\Menu\LocalActionManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -84,18 +83,8 @@ class LocalActionsBlock extends BlockBase implements ContainerFactoryPluginInter public function build() { $route_name = $this->routeMatch->getRouteName(); $local_actions = $this->localActionManager->getActionsForRoute($route_name); - if (empty($local_actions)) { - return []; - } return $local_actions; } - /** - * {@inheritdoc} - */ - public function getCacheContexts() { - return Cache::mergeContexts(parent::getCacheContexts(), ['route']); - } - } diff --git a/core/lib/Drupal/Core/Render/Element/ImageButton.php b/core/lib/Drupal/Core/Render/Element/ImageButton.php index 4ee39a634..f22ae1dec 100644 --- a/core/lib/Drupal/Core/Render/Element/ImageButton.php +++ b/core/lib/Drupal/Core/Render/Element/ImageButton.php @@ -75,7 +75,7 @@ class ImageButton extends Submit { $element['#attributes']['type'] = 'image'; Element::setAttributes($element, array('id', 'name', 'value')); - $element['#attributes']['src'] = file_create_url($element['#src']); + $element['#attributes']['src'] = file_url_transform_relative(file_create_url($element['#src'])); if (!empty($element['#title'])) { $element['#attributes']['alt'] = $element['#title']; $element['#attributes']['title'] = $element['#title']; diff --git a/core/lib/Drupal/Core/Render/Element/Radios.php b/core/lib/Drupal/Core/Render/Element/Radios.php index 5423ef03e..a50fc70d3 100644 --- a/core/lib/Drupal/Core/Render/Element/Radios.php +++ b/core/lib/Drupal/Core/Render/Element/Radios.php @@ -22,8 +22,8 @@ use Drupal\Component\Utility\Html as HtmlUtility; * $form['settings']['active'] = array( * '#type' => 'radios', * '#title' => t('Poll status'), - * '#default_value' => 1 - * '#options' => array(0 => t('Closed'), 1 => t('Active'), + * '#default_value' => 1, + * '#options' => array(0 => t('Closed'), 1 => t('Active')), * ); * @endcode * diff --git a/core/lib/Drupal/Core/Render/Element/Table.php b/core/lib/Drupal/Core/Render/Element/Table.php index 63e15ed07..393daa678 100644 --- a/core/lib/Drupal/Core/Render/Element/Table.php +++ b/core/lib/Drupal/Core/Render/Element/Table.php @@ -34,7 +34,7 @@ use Drupal\Component\Utility\Html as HtmlUtility; * @code * $form['contacts'] = array( * '#type' => 'table', - * '#title' => 'Sample Table', + * '#caption' => 'Sample Table', * '#header' => array('Name', 'Phone'), * ); * diff --git a/core/lib/Drupal/Core/Render/Element/Tableselect.php b/core/lib/Drupal/Core/Render/Element/Tableselect.php index cc6aefd33..27338cfa7 100644 --- a/core/lib/Drupal/Core/Render/Element/Tableselect.php +++ b/core/lib/Drupal/Core/Render/Element/Tableselect.php @@ -16,7 +16,7 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; * Provides a form element for a table with radios or checkboxes in left column. * * Properties: - * - #headers: Table headers used in the table. + * - #header: Table headers used in the table. * - #options: An associative array where each key is the value returned when * a user selects the radio button or checkbox, and each value is the row of * table data. diff --git a/core/lib/Drupal/Core/Session/AccountProxy.php b/core/lib/Drupal/Core/Session/AccountProxy.php index 4d6db573a..9bc8c1bcd 100644 --- a/core/lib/Drupal/Core/Session/AccountProxy.php +++ b/core/lib/Drupal/Core/Session/AccountProxy.php @@ -56,7 +56,7 @@ class AccountProxy implements AccountProxyInterface { // After the container is rebuilt, DrupalKernel sets the initial // account to the id of the logged in user. This is necessary in order // to refresh the user account reference here. - $this->account = $this->loadUserEntity($this->initialAccountId); + $this->setAccount($this->loadUserEntity($this->initialAccountId)); } else { $this->account = new AnonymousUserSession(); diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 69f329805..5f225d318 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -135,7 +135,9 @@ class TwigExtension extends \Twig_Extension { new \Twig_SimpleFunction('url', array($this, 'getUrl'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), new \Twig_SimpleFunction('path', array($this, 'getPath'), array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))), new \Twig_SimpleFunction('link', array($this, 'getLink')), - new \Twig_SimpleFunction('file_url', 'file_create_url'), + new \Twig_SimpleFunction('file_url', function ($uri) { + return file_url_transform_relative(file_create_url($uri)); + }), new \Twig_SimpleFunction('attach_library', [$this, 'attachLibrary']), new \Twig_SimpleFunction('active_theme_path', [$this, 'getActiveThemePath']), new \Twig_SimpleFunction('active_theme', [$this, 'getActiveTheme']), diff --git a/core/lib/Drupal/Core/Theme/ThemeInitialization.php b/core/lib/Drupal/Core/Theme/ThemeInitialization.php index 88c06e2c7..d84ddd070 100644 --- a/core/lib/Drupal/Core/Theme/ThemeInitialization.php +++ b/core/lib/Drupal/Core/Theme/ThemeInitialization.php @@ -318,7 +318,6 @@ class ThemeInitialization implements ThemeInitializationInterface { $stylesheets_remove = array(); // Grab stylesheets from base theme. foreach ($base_themes as $base) { - $base_theme_path = $base->getPath(); if (!empty($base->info['stylesheets-remove'])) { foreach ($base->info['stylesheets-remove'] as $css_file) { $css_file = $this->resolveStyleSheetPlaceholders($css_file); diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 6d25ded5a..34b4ceaf8 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -293,6 +293,11 @@ class Url { * @see \Drupal\Core\Url::fromUserInput() */ public static function fromUri($uri, $options = []) { + // parse_url() incorrectly parses base:number/... as hostname:port/... + // and not the scheme. Prevent that by prefixing the path with a slash. + if (preg_match('/^base:\d/', $uri)) { + $uri = str_replace('base:', 'base:/', $uri); + } $uri_parts = parse_url($uri); if ($uri_parts === FALSE) { throw new \InvalidArgumentException("The URI '$uri' is malformed."); diff --git a/core/misc/dialog/dialog.js b/core/misc/dialog/dialog.js index 5683cfb16..197fae227 100644 --- a/core/misc/dialog/dialog.js +++ b/core/misc/dialog/dialog.js @@ -31,6 +31,7 @@ // `jQuery(event.target).remove()` as well, to remove the dialog on // closing. close: function (event) { + Drupal.dialog(event.target).close(); Drupal.detachBehaviors(event.target, null, 'unload'); } }; @@ -60,6 +61,19 @@ * @return {Drupal.dialog~dialogDefinition} */ Drupal.dialog = function (element, options) { + var undef; + var $element = $(element); + var dialog = { + open: false, + returnValue: undef, + show: function () { + openDialog({modal: false}); + }, + showModal: function () { + openDialog({modal: true}); + }, + close: closeDialog + }; function openDialog(settings) { settings = $.extend({}, drupalSettings.dialog, options, settings); @@ -78,20 +92,6 @@ $(window).trigger('dialog:afterclose', [dialog, $element]); } - var undef; - var $element = $(element); - var dialog = { - open: false, - returnValue: undef, - show: function () { - openDialog({modal: false}); - }, - showModal: function () { - openDialog({modal: true}); - }, - close: closeDialog - }; - return dialog; }; diff --git a/core/misc/throbber-active.gif b/core/misc/throbber-active.gif index f66414a1c..abf5c5d5b 100644 Binary files a/core/misc/throbber-active.gif and b/core/misc/throbber-active.gif differ diff --git a/core/modules/action/src/Plugin/Action/MessageAction.php b/core/modules/action/src/Plugin/Action/MessageAction.php index f754405e6..c8478ffc4 100644 --- a/core/modules/action/src/Plugin/Action/MessageAction.php +++ b/core/modules/action/src/Plugin/Action/MessageAction.php @@ -28,6 +28,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class MessageAction extends ConfigurableActionBase implements ContainerFactoryPluginInterface { /** + * The token service. + * * @var \Drupal\Core\Utility\Token */ protected $token; @@ -48,10 +50,8 @@ class MessageAction extends ConfigurableActionBase implements ContainerFactoryPl * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. - * @param \Drupal\Core\Utility\Token - * The token service. * @param \Drupal\Core\Utility\Token $token - * The token replacement service. + * The token service. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. */ diff --git a/core/modules/book/book.module b/core/modules/book/book.module index abf11eb2a..f55f5d6fd 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -323,14 +323,22 @@ function book_node_prepare_form(NodeInterface $node, $operation, FormStateInterf } /** - * Implements hook_form_FORM_ID_alter() for node_delete_confirm(). + * Implements hook_form_BASE_FORM_ID_alter(). * * Alters the confirm form for a single node deletion. - * - * @see node_delete_confirm() */ -function book_form_node_delete_confirm_alter(&$form, FormStateInterface $form_state) { - $node = Node::load($form['nid']['#value']); +function book_form_node_confirm_form_alter(&$form, FormStateInterface $form_state) { + // Only need to alter the delete operation form. + if ($form_state->getFormObject()->getOperation() !== 'delete') { + return; + } + + /** @var \Drupal\node\NodeInterface $node */ + $node = $form_state->getFormObject()->getEntity(); + if (!book_type_is_allowed($node->getType())) { + // Not a book node. + return; + } if (isset($node->book) && $node->book['has_children']) { $form['book_warning'] = array( diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php index 5b72052cf..77da924de 100644 --- a/core/modules/book/src/BookManager.php +++ b/core/modules/book/src/BookManager.php @@ -439,9 +439,9 @@ class BookManager implements BookManagerInterface { if ($nid == $original['bid']) { // Handle deletion of a top-level post. $result = $this->bookOutlineStorage->loadBookChildren($nid); - - foreach ($result as $child) { - $child['bid'] = $child['nid']; + $children = $this->entityManager->getStorage('node')->loadMultiple(array_keys($result)); + foreach ($children as $child) { + $child->book['bid'] = $child->id(); $this->updateOutline($child); } } diff --git a/core/modules/book/src/Tests/BookTest.php b/core/modules/book/src/Tests/BookTest.php index 54d0fafd1..16c7027d8 100644 --- a/core/modules/book/src/Tests/BookTest.php +++ b/core/modules/book/src/Tests/BookTest.php @@ -30,7 +30,7 @@ class BookTest extends WebTestBase { /** * A book node. * - * @var object + * @var \Drupal\node\NodeInterface */ protected $book; @@ -77,11 +77,13 @@ class BookTest extends WebTestBase { $this->bookAuthor = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books')); $this->webUser = $this->drupalCreateUser(array('access printer-friendly version', 'node test view')); $this->webUserWithoutNodeAccess = $this->drupalCreateUser(array('access printer-friendly version')); - $this->adminUser = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books', 'administer blocks', 'administer permissions', 'administer book outlines', 'node test view', 'administer content types', 'administer site configuration')); + $this->adminUser = $this->drupalCreateUser(array('create new books', 'create book content', 'edit any book content', 'delete any book content', 'add content to books', 'administer blocks', 'administer permissions', 'administer book outlines', 'node test view', 'administer content types', 'administer site configuration')); } /** * Creates a new book with a page hierarchy. + * + * @return \Drupal\node\NodeInterface[] */ function createBook() { // Create new book. @@ -498,6 +500,25 @@ class BookTest extends WebTestBase { $node_storage->resetCache(array($this->book->id())); $node = $node_storage->load($this->book->id()); $this->assertTrue(empty($node->book), 'Deleting childless top-level book node properly allowed.'); + + // Tests directly deleting a book parent. + $nodes = $this->createBook(); + $this->drupalLogin($this->adminUser); + $this->drupalGet($this->book->urlInfo('delete-form')); + $this->assertRaw(t('%title is part of a book outline, and has associated child pages. If you proceed with deletion, the child pages will be relocated automatically.', ['%title' => $this->book->label()])); + // Delete parent, and visit a child page. + $this->drupalPostForm($this->book->urlInfo('delete-form'), [], t('Delete')); + $this->drupalGet($nodes[0]->urlInfo()); + $this->assertResponse(200); + $this->assertText($nodes[0]->label()); + // The book parents should be updated. + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + $node_storage->resetCache(); + $child = $node_storage->load($nodes[0]->id()); + $this->assertEqual($child->id(), $child->book['bid'], 'Child node book ID updated when parent is deleted.'); + // 3rd-level children should now be 2nd-level. + $second = $node_storage->load($nodes[1]->id()); + $this->assertEqual($child->id(), $second->book['bid'], '3rd-level child node is now second level when top-level node is deleted.'); } /** diff --git a/core/modules/ckeditor/ckeditor.api.php b/core/modules/ckeditor/ckeditor.api.php index 5a295ecc9..0e65da8b6 100644 --- a/core/modules/ckeditor/ckeditor.api.php +++ b/core/modules/ckeditor/ckeditor.api.php @@ -35,10 +35,11 @@ function hook_ckeditor_plugin_info_alter(array &$plugins) { * iframe versions of CKEditor. * * Front-end themes (and base themes) can easily specify CSS files to be used in - * iframe instances of CKEditor through an entry in their .info file: + * iframe instances of CKEditor through an entry in their .info.yml file: * * @code - * ckeditor_stylesheets[] = css/ckeditor-iframe.css + * ckeditor_stylesheets: + * - css/ckeditor-iframe.css * @endcode * * @param array &$css diff --git a/core/modules/ckeditor/src/Annotation/CKEditorPlugin.php b/core/modules/ckeditor/src/Annotation/CKEditorPlugin.php index 197cf95da..04a315674 100644 --- a/core/modules/ckeditor/src/Annotation/CKEditorPlugin.php +++ b/core/modules/ckeditor/src/Annotation/CKEditorPlugin.php @@ -19,6 +19,7 @@ use Drupal\Component\Annotation\Plugin; * @see \Drupal\ckeditor\CKEditorPluginInterface * @see \Drupal\ckeditor\CKEditorPluginBase * @see \Drupal\ckeditor\CKEditorPluginManager + * @see hook_ckeditor_plugin_info_alter() * @see plugin_api * * @Annotation diff --git a/core/modules/ckeditor/src/CKEditorPluginBase.php b/core/modules/ckeditor/src/CKEditorPluginBase.php index 9342b0e90..6388328f0 100644 --- a/core/modules/ckeditor/src/CKEditorPluginBase.php +++ b/core/modules/ckeditor/src/CKEditorPluginBase.php @@ -17,11 +17,11 @@ use Drupal\editor\Entity\Editor; * than the one provided by Drupal core is used. Most CKEditor plugins don't * need to provide additional settings forms. * - * This base assumes that your plugin has buttons that you want to be enabled - * through the toolbar builder UI. It is still possible to also implement the - * CKEditorPluginContextualInterface (for contextual enabling) and + * This base class assumes that your plugin has buttons that you want to be + * enabled through the toolbar builder UI. It is still possible to also + * implement the CKEditorPluginContextualInterface (for contextual enabling) and * CKEditorPluginConfigurableInterface interfaces (for configuring plugin - * settings) though. + * settings). * * NOTE: the Drupal plugin ID should correspond to the CKEditor plugin name. * diff --git a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php index 5ae5e85b2..807206069 100644 --- a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php +++ b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php @@ -298,8 +298,11 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { ); // Finally, set Drupal-specific CKEditor settings. + $root_relative_file_url = function ($uri) { + return file_url_transform_relative(file_create_url($uri)); + }; $settings += array( - 'drupalExternalPlugins' => array_map('file_create_url', $external_plugin_files), + 'drupalExternalPlugins' => array_map($root_relative_file_url, $external_plugin_files), ); // Parse all CKEditor plugin JavaScript files for translations. @@ -421,6 +424,7 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface { $this->moduleHandler->alter('ckeditor_css', $css, $editor); $css = array_merge($css, _ckeditor_theme_css()); $css = array_map('file_create_url', $css); + $css = array_map('file_url_transform_relative', $css); return array_values($css); } diff --git a/core/modules/ckeditor/src/Tests/CKEditorTest.php b/core/modules/ckeditor/src/Tests/CKEditorTest.php index baef0bec3..4346cd63d 100644 --- a/core/modules/ckeditor/src/Tests/CKEditorTest.php +++ b/core/modules/ckeditor/src/Tests/CKEditorTest.php @@ -90,8 +90,8 @@ class CKEditorTest extends KernelTestBase { 'language' => 'en', 'stylesSet' => FALSE, 'drupalExternalPlugins' => array( - 'drupalimage' => file_create_url('core/modules/ckeditor/js/plugins/drupalimage/plugin.js'), - 'drupallink' => file_create_url('core/modules/ckeditor/js/plugins/drupallink/plugin.js'), + 'drupalimage' => file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/plugin.js')), + 'drupallink' => file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupallink/plugin.js')), ), ); $expected_config = $this->castSafeStrings($expected_config); @@ -114,9 +114,9 @@ class CKEditorTest extends KernelTestBase { $expected_config['toolbar'][0]['items'][] = 'Format'; $expected_config['format_tags'] = 'p;h2;h3;h4;h5;h6'; $expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button'; - $expected_config['drupalExternalPlugins']['llama_contextual'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js'); - $expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js'); - $expected_config['contentsCss'][] = file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css'); + $expected_config['drupalExternalPlugins']['llama_contextual'] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js')); + $expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js')); + $expected_config['contentsCss'][] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css')); ksort($expected_config); $this->assertIdentical($expected_config, $this->castSafeStrings($this->ckeditor->getJSSettings($editor)), 'Generated JS settings are correct for customized configuration.'); @@ -261,15 +261,15 @@ class CKEditorTest extends KernelTestBase { // Enable the editor_test module, which implements hook_ckeditor_css_alter(). $this->enableModules(array('ckeditor_test')); - $expected[] = file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css'); + $expected[] = file_url_transform_relative(file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css')); $this->assertIdentical($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a hook_ckeditor_css_alter() implementation exists.'); // Enable the Bartik theme, which specifies a CKEditor stylesheet. \Drupal::service('theme_handler')->install(['bartik']); $this->config('system.theme')->set('default', 'bartik')->save(); - $expected[] = file_create_url('core/themes/bartik/css/base/elements.css'); - $expected[] = file_create_url('core/themes/bartik/css/components/captions.css'); - $expected[] = file_create_url('core/themes/bartik/css/components/table.css'); + $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/base/elements.css')); + $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/captions.css')); + $expected[] = file_url_transform_relative(file_create_url('core/themes/bartik/css/components/table.css')); $this->assertIdentical($expected, $this->ckeditor->buildContentsCssJSSetting($editor), '"contentsCss" configuration part of JS settings built correctly while a theme providing a CKEditor stylesheet exists.'); } @@ -478,8 +478,8 @@ class CKEditorTest extends KernelTestBase { protected function getDefaultContentsCssConfig() { return array( - file_create_url('core/modules/ckeditor/css/ckeditor-iframe.css'), - file_create_url('core/modules/system/css/components/align.module.css'), + file_url_transform_relative(file_create_url('core/modules/ckeditor/css/ckeditor-iframe.css')), + file_url_transform_relative(file_create_url('core/modules/system/css/components/align.module.css')), ); } diff --git a/core/modules/ckeditor/src/Tests/CKEditorToolbarButtonTest.php b/core/modules/ckeditor/src/Tests/CKEditorToolbarButtonTest.php index 5751f8629..4b241d799 100644 --- a/core/modules/ckeditor/src/Tests/CKEditorToolbarButtonTest.php +++ b/core/modules/ckeditor/src/Tests/CKEditorToolbarButtonTest.php @@ -57,7 +57,6 @@ class CKEditorToolbarButtonTest extends WebTestBase { * Method tests CKEditor image buttons. */ public function testImageButtonDisplay() { - global $base_url; $this->drupalLogin($this->admin_user); // Install the Arabic language (which is RTL) and configure as the default. @@ -75,7 +74,7 @@ class CKEditorToolbarButtonTest extends WebTestBase { $json_encode = function($html) { return trim(Json::encode($html), '"'); }; - $markup = $json_encode($base_url . '/core/modules/ckeditor/js/plugins/drupalimage/image.png'); + $markup = $json_encode(file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/image.png'))); $this->assertRaw($markup); } diff --git a/core/modules/color/color.module b/core/modules/color/color.module index 15619ff4c..d266b2ee1 100644 --- a/core/modules/color/color.module +++ b/core/modules/color/color.module @@ -126,7 +126,7 @@ function color_block_view_pre_render(array $build) { // Override logo. $logo = $config->get('logo'); if ($logo && $build['content']['site_logo'] && preg_match('!' . $theme_key . '/logo.svg$!', $build['content']['site_logo']['#uri'])) { - $build['content']['site_logo']['#uri'] = file_create_url($logo); + $build['content']['site_logo']['#uri'] = file_url_transform_relative(file_create_url($logo)); } return $build; diff --git a/core/modules/color/src/Tests/ColorTest.php b/core/modules/color/src/Tests/ColorTest.php index 4a4106444..8e3d7714a 100644 --- a/core/modules/color/src/Tests/ColorTest.php +++ b/core/modules/color/src/Tests/ColorTest.php @@ -121,7 +121,7 @@ class ColorTest extends WebTestBase { $this->drupalGet(''); $stylesheets = $this->config('color.theme.' . $theme)->get('stylesheets'); foreach ($stylesheets as $stylesheet) { - $this->assertPattern('|' . file_create_url($stylesheet) . '|', 'Make sure the color stylesheet is included in the content. (' . $theme . ')'); + $this->assertPattern('|' . file_url_transform_relative(file_create_url($stylesheet)) . '|', 'Make sure the color stylesheet is included in the content. (' . $theme . ')'); $stylesheet_content = join("\n", file($stylesheet)); $this->assertTrue(strpos($stylesheet_content, 'color: #123456') !== FALSE, 'Make sure the color we changed is in the color stylesheet. (' . $theme . ')'); } @@ -191,7 +191,7 @@ class ColorTest extends WebTestBase { // Ensure that the overridden logo is present in Bartik, which is colorable. $this->drupalGet('admin/appearance/settings/bartik'); - $this->assertIdentical($GLOBALS['base_url'] . '/' . 'core/misc/druplicon.png', $this->getDrupalSettings()['color']['logo']); + $this->assertIdentical($GLOBALS['base_path'] . 'core/misc/druplicon.png', $this->getDrupalSettings()['color']['logo']); } /** diff --git a/core/modules/color/tests/modules/color_test/color_test.module b/core/modules/color/tests/modules/color_test/color_test.module deleted file mode 100644 index 603906ee4..000000000 --- a/core/modules/color/tests/modules/color_test/color_test.module +++ /dev/null @@ -1,14 +0,0 @@ -getErrors()) { $comment->in_preview = TRUE; - $comment_build = comment_view($comment); + $comment_build = \Drupal::entityTypeManager()->getViewBuilder('comment')->view($comment); $comment_build['#weight'] = -100; $preview_build['comment_preview'] = $comment_build; @@ -573,7 +573,7 @@ function comment_preview(CommentInterface $comment, FormStateInterface $form_sta $build = array(); $parent = $comment->getParentComment(); if ($parent && $parent->isPublished()) { - $build = comment_view($parent); + $build = \Drupal::entityTypeManager()->getViewBuilder('comment')->view($parent); } } else { diff --git a/core/modules/comment/src/Plugin/views/row/Rss.php b/core/modules/comment/src/Plugin/views/row/Rss.php index f4e62c72e..df7200ff4 100644 --- a/core/modules/comment/src/Plugin/views/row/Rss.php +++ b/core/modules/comment/src/Plugin/views/row/Rss.php @@ -104,7 +104,7 @@ class Rss extends RssPluginBase { // The comment gets built and modules add to or modify // $comment->rss_elements and $comment->rss_namespaces. - $build = comment_view($comment, 'rss'); + $build = $this->entityManager->getViewBuilder('comment')->view($comment, 'rss'); unset($build['#theme']); if (!empty($comment->rss_namespaces)) { diff --git a/core/modules/comment/src/Tests/CommentTranslationUITest.php b/core/modules/comment/src/Tests/CommentTranslationUITest.php index 9b27a3e73..1246aa8d4 100644 --- a/core/modules/comment/src/Tests/CommentTranslationUITest.php +++ b/core/modules/comment/src/Tests/CommentTranslationUITest.php @@ -164,7 +164,7 @@ class CommentTranslationUITest extends ContentTranslationUITestBase { 'created' => REQUEST_TIME - mt_rand(0, 1000), ); $edit = array( - 'uid' => $user->getUsername() . '(' . $user->id() . ')', + 'uid' => $user->getUsername() . ' (' . $user->id() . ')', 'date[date]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d'), 'date[time]' => format_date($values[$langcode]['created'], 'custom', 'H:i:s'), ); diff --git a/core/modules/config/src/Tests/ConfigEventsTest.php b/core/modules/config/src/Tests/ConfigEventsTest.php index fb6eca61e..431552e4e 100644 --- a/core/modules/config/src/Tests/ConfigEventsTest.php +++ b/core/modules/config/src/Tests/ConfigEventsTest.php @@ -33,7 +33,6 @@ class ConfigEventsTest extends KernelTestBase { $config = new Config($name, \Drupal::service('config.storage'), \Drupal::service('event_dispatcher'), \Drupal::service('config.typed')); $config->set('key', 'initial'); - \Drupal::state()->get('config_events_test.event', FALSE); $this->assertIdentical(\Drupal::state()->get('config_events_test.event', array()), array(), 'No events fired by creating a new configuration object'); $config->save(); diff --git a/core/modules/config/src/Tests/ConfigSchemaTest.php b/core/modules/config/src/Tests/ConfigSchemaTest.php index 8735db16d..a34e6a6d6 100644 --- a/core/modules/config/src/Tests/ConfigSchemaTest.php +++ b/core/modules/config/src/Tests/ConfigSchemaTest.php @@ -506,4 +506,70 @@ class ConfigSchemaTest extends KernelTestBase { $this->assertEqual($definitions['config_schema_test.hook']['additional_metadata'], 'new schema info'); } + /** + * Tests saving config when the type is wrapped by a dynamic type. + */ + public function testConfigSaveWithWrappingSchema() { + $untyped_values = [ + 'tests' => [ + [ + 'wrapper_value' => 'foo', + 'plugin_id' => 'wrapper:foo', + 'internal_value' => 100, + ], + ], + ]; + + $typed_values = [ + 'tests' => [ + [ + 'wrapper_value' => 'foo', + 'plugin_id' => 'wrapper:foo', + 'internal_value' => '100', + ], + ], + ]; + + // Save config which has a schema that enforces types. + \Drupal::configFactory()->getEditable('wrapping.config_schema_test.plugin_types') + ->setData($untyped_values) + ->save(); + $this->assertIdentical(\Drupal::config('wrapping.config_schema_test.plugin_types') + ->get(), $typed_values); + } + + /** + * Tests dynamic config schema type with multiple sub-key references. + */ + public function testConfigSaveWithWrappingSchemaDoubleBrackets() { + $untyped_values = [ + 'tests' => [ + [ + 'wrapper_value' => 'foo', + 'foo' => 'cat', + 'bar' => 'dog', + 'another_key' => 100, + ], + ], + ]; + + $typed_values = [ + 'tests' => [ + [ + 'wrapper_value' => 'foo', + 'foo' => 'cat', + 'bar' => 'dog', + 'another_key' => '100', + ], + ], + ]; + + // Save config which has a schema that enforces types. + \Drupal::configFactory()->getEditable('wrapping.config_schema_test.double_brackets') + ->setData($untyped_values) + ->save(); + $this->assertIdentical(\Drupal::config('wrapping.config_schema_test.double_brackets') + ->get(), $typed_values); + } + } diff --git a/core/modules/config/src/Tests/Storage/FileStorageTest.php b/core/modules/config/src/Tests/Storage/FileStorageTest.php index 932c54e62..fb17f1e14 100644 --- a/core/modules/config/src/Tests/Storage/FileStorageTest.php +++ b/core/modules/config/src/Tests/Storage/FileStorageTest.php @@ -9,6 +9,7 @@ namespace Drupal\config\Tests\Storage; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Config\FileStorage; +use Drupal\Core\Config\UnsupportedDataTypeConfigException; /** * Tests FileStorage operations. @@ -76,4 +77,19 @@ class FileStorageTest extends ConfigStorageTestBase { $this->assertIdentical($config_files, $expected_files, 'Absolute path, two config files found.'); } + /** + * Test UnsupportedDataTypeConfigException displays path of + * erroneous file during read. + */ + public function testReadUnsupportedDataTypeConfigException() { + file_put_contents($this->storage->getFilePath('core.extension'), PHP_EOL . 'foo : [bar}', FILE_APPEND); + try { + $config_parsed = $this->storage->read('core.extension'); + } + catch (UnsupportedDataTypeConfigException $e) { + $this->pass('Exception thrown when trying to read a field containing invalid data type.'); + $this->assertTrue((strpos($e->getMessage(), $this->storage->getFilePath('core.extension')) !== FALSE), 'Erroneous file path is displayed.'); + } + } + } diff --git a/core/modules/config/tests/config_collection_install_test/src/EventSubscriber.php b/core/modules/config/tests/config_collection_install_test/src/EventSubscriber.php index f06aaa5fa..a4aea3924 100644 --- a/core/modules/config/tests/config_collection_install_test/src/EventSubscriber.php +++ b/core/modules/config/tests/config_collection_install_test/src/EventSubscriber.php @@ -32,10 +32,10 @@ class EventSubscriber implements EventSubscriberInterface { } /** - * Reacts to the ConfigEvents::COLLECTION_NAMES event. + * Reacts to the ConfigEvents::COLLECTION_INFO event. * * @param \Drupal\Core\Config\ConfigCollectionInfo $collection_info - * The configuration collection names event. + * The configuration collection info event. */ public function addCollections(ConfigCollectionInfo $collection_info) { $collections = $this->state->get('config_collection_install_test.collection_names', array()); diff --git a/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml b/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml index 004ae2bec..d7ec9a558 100644 --- a/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml +++ b/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml @@ -213,3 +213,50 @@ config_test.dynamic.*.third_party.config_schema_test: type: integer string: type: string + +wrapping.config_schema_test.plugin_types: + type: config_object + mapping: + tests: + type: sequence + sequence: + - type: wrapping.test.plugin_types.[plugin_id] + +wrapping.test.plugin_types.*: + type: test.plugin_types.[plugin_id] + mapping: + wrapper_value: + type: string + +test.plugin_types.wrapper:*: + type: test.plugin_types + mapping: + internal_value: + type: string + +wrapping.config_schema_test.double_brackets: + type: config_object + mapping: + tests: + type: sequence + sequence: + - type: wrapping.test.double_brackets.[another_key] + +wrapping.test.double_brackets.*: + type: test.double_brackets.[foo].[bar] + mapping: + wrapper_value: + type: string + +test.double_brackets.cat.dog: + type: test.double_brackets + mapping: + another_key: + type: string + foo: + type: string + bar: + type: string + +test.double_brackets.*: + type: mapping diff --git a/core/modules/dblog/dblog.views.inc b/core/modules/dblog/dblog.views.inc index 90be37c20..1012d6f95 100644 --- a/core/modules/dblog/dblog.views.inc +++ b/core/modules/dblog/dblog.views.inc @@ -35,9 +35,6 @@ function dblog_views_data() { 'sort' => array( 'id' => 'standard', ), - 'search' => array( - 'id' => 'standard', - ), ); $data['watchdog']['uid'] = array( @@ -52,9 +49,6 @@ function dblog_views_data() { 'argument' => array( 'id' => 'numeric', ), - 'search' => array( - 'id' => 'standard', - ), 'relationship' => array( 'title' => t('User'), 'help' => t('The user on which the log entry as written.'), diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php index 6ed4843ac..9ee63b073 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php @@ -308,7 +308,7 @@ class EntityReferenceAdminTest extends WebTestBase { $edit = array( 'title[0][value]' => 'Test', - 'field_test_entity_ref_field[0][target_id]' => $node1->getTitle() . '(' . $node1->id() . ')' + 'field_test_entity_ref_field[0][target_id]' => $node1->getTitle() . ' (' . $node1->id() . ')' ); $this->drupalPostForm('node/add/' . $this->type, $edit, t('Save')); $this->assertLink($node1->getTitle()); diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php index f629ce8cc..02456388c 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceItemTest.php @@ -12,6 +12,7 @@ use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Unicode; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Language\LanguageInterface; use Drupal\entity_test\Entity\EntityTest; @@ -76,6 +77,7 @@ class EntityReferenceItemTest extends FieldUnitTestBase { $this->installEntitySchema('file'); $this->installSchema('comment', ['comment_entity_statistics']); + $this->installSchema('node', ['node_access']); $this->vocabulary = entity_create('taxonomy_vocabulary', array( 'name' => $this->randomMachineName(), @@ -100,7 +102,7 @@ class EntityReferenceItemTest extends FieldUnitTestBase { $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_term', 'Test content entity reference', 'taxonomy_term'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_entity_test_string_id', 'Test content entity reference with string ID', 'entity_test_string_id'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_taxonomy_vocabulary', 'Test config entity reference', 'taxonomy_vocabulary'); - $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_node', 'Test node entity reference', 'node'); + $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_node', 'Test node entity reference', 'node', 'default', [], FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_user', 'Test user entity reference', 'user'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_comment', 'Test comment entity reference', 'comment'); $this->createEntityReferenceField('entity_test', 'entity_test', 'field_test_file', 'Test file entity reference', 'file'); @@ -406,6 +408,64 @@ class EntityReferenceItemTest extends FieldUnitTestBase { $errors = $entity->validate(); $this->assertEqual(0, count($errors)); + // Test with a mix of valid and invalid nodes. + $unsaved_unpublished_node_title = $this->randomString(); + $unsaved_unpublished_node = Node::create([ + 'title' => $unsaved_unpublished_node_title, + 'type' => 'node', + 'status' => NODE_NOT_PUBLISHED, + ]); + + $saved_unpublished_node_title = $this->randomString(); + $saved_unpublished_node = Node::create([ + 'title' => $saved_unpublished_node_title, + 'type' => 'node', + 'status' => NODE_NOT_PUBLISHED, + ]); + $saved_unpublished_node->save(); + + $saved_published_node_title = $this->randomString(); + $saved_published_node = Node::create([ + 'title' => $saved_published_node_title, + 'type' => 'node', + 'status' => NODE_PUBLISHED, + ]); + $saved_published_node->save(); + + $entity = EntityTest::create([ + 'field_test_node' => [ + [ + 'entity' => $unsaved_unpublished_node, + ], + [ + 'target_id' => $saved_unpublished_node->id(), + ], + [ + 'target_id' => $saved_published_node->id(), + ], + ], + ]); + + $errors = $entity->validate(); + $this->assertEqual(2, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'node', '%label' => $unsaved_unpublished_node_title])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_node.0.entity'); + $this->assertEqual($errors[1]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'node', '%label' => $saved_unpublished_node->id()])); + $this->assertEqual($errors[1]->getPropertyPath(), 'field_test_node.1.target_id'); + + // Publish one of the nodes and try again. + $saved_unpublished_node->setPublished(TRUE); + $saved_unpublished_node->save(); + $errors = $entity->validate(); + $this->assertEqual(1, count($errors)); + $this->assertEqual($errors[0]->getMessage(), new FormattableMarkup('This entity (%type: %label) cannot be referenced.', ['%type' => 'node', '%label' => $unsaved_unpublished_node_title])); + $this->assertEqual($errors[0]->getPropertyPath(), 'field_test_node.0.entity'); + + // Publish the last invalid node and try again. + $unsaved_unpublished_node->setPublished(TRUE); + $errors = $entity->validate(); + $this->assertEqual(0, count($errors)); + // Test with an unpublished and unsaved comment. $title = $this->randomString(); $comment = Comment::create([ diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 08e94384b..984773ace 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -977,7 +977,12 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta break; case 'url': + // Ideally, this would use file_url_transform_relative(), but because + // tokens are also often used in e-mails, it's better to keep absolute + // file URLs. The 'url.site' cache context is associated to ensure the + // correct absolute URL is used in case of a multisite setup. $replacements[$original] = file_create_url($file->getFileUri()); + $bubbleable_metadata->addCacheContexts(['url.site']); break; // These tokens are default variations on the chained tokens handled below. @@ -1228,7 +1233,13 @@ function template_preprocess_file_link(&$variables) { $options = array(); $file_entity = ($file instanceof File) ? $file : File::load($file->fid); + // @todo Wrap in file_url_transform_relative(). This is currently + // impossible. As a work-around, we currently add the 'url.site' cache context + // to ensure different file URLs are generated for different sites in a + // multisite setup, including HTTP and HTTPS versions of the same site. + // Fix in https://www.drupal.org/node/2646744. $url = file_create_url($file_entity->getFileUri()); + $variables['#cache']['contexts'][] = 'url.site'; $mime_type = $file->getMimeType(); // Set options as per anchor format described at diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index ae5ca764b..6417488e5 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -70,6 +70,8 @@ class File extends ContentEntityBase implements FileInterface { /** * {@inheritdoc} + * + * @see file_url_transform_relative() */ public function url($rel = 'canonical', $options = array()) { return file_create_url($this->getFileUri()); diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php b/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php index fbe14f09b..798babd73 100644 --- a/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/BaseFieldFileFormatterBase.php @@ -52,6 +52,8 @@ abstract class BaseFieldFileFormatterBase extends FormatterBase { $url = NULL; // Add support to link to the entity itself. if ($this->getSetting('link_to_file')) { + // @todo Wrap in file_url_transform_relative(). This is currently + // impossible. See below. $url = file_create_url($items->getEntity()->uri->value); } @@ -63,6 +65,16 @@ abstract class BaseFieldFileFormatterBase extends FormatterBase { '#type' => 'link', '#title' => $view_value, '#url' => Url::fromUri($url), + // @todo Remove the 'url.site' cache context by using a relative file + // URL (file_url_transform_relative()). This is currently impossible + // because #type => link requires a Url object, and Url objects do not + // support relative URLs: they require fully qualified URLs. Fix in + // https://www.drupal.org/node/2646744. + '#cache' => [ + 'contexts' => [ + 'url.site', + ], + ], ]; } else { diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php index 0a2a855bc..c86cc8e0d 100644 --- a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php @@ -55,6 +55,9 @@ class FileUriFormatter extends BaseFieldFileFormatterBase { protected function viewValue(FieldItemInterface $item) { $value = $item->value; if ($this->getSetting('file_download_path')) { + // @todo Wrap in file_url_transform_relative(). This is currently + // impossible. See BaseFieldFileFormatterBase::viewElements(). Fix in + // https://www.drupal.org/node/2646744. $value = file_create_url($value); } return $value; diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/RSSEnclosureFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/RSSEnclosureFormatter.php index ad3418f8f..cce0dc5e6 100644 --- a/core/modules/file/src/Plugin/Field/FieldFormatter/RSSEnclosureFormatter.php +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/RSSEnclosureFormatter.php @@ -33,6 +33,9 @@ class RSSEnclosureFormatter extends FileFormatterBase { $entity->rss_elements[] = array( 'key' => 'enclosure', 'attributes' => array( + // In RSS feeds, it is necessary to use absolute URLs. The 'url.site' + // cache context is already associated with RSS feed responses, so it + // does not need to be specified here. 'url' => file_create_url($file->getFileUri()), 'length' => $file->getSize(), 'type' => $file->getMimeType(), diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/UrlPlainFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/UrlPlainFormatter.php index 6f2264e91..0795be200 100644 --- a/core/modules/file/src/Plugin/Field/FieldFormatter/UrlPlainFormatter.php +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/UrlPlainFormatter.php @@ -30,7 +30,7 @@ class UrlPlainFormatter extends FileFormatterBase { foreach ($this->getEntitiesToView($items, $langcode) as $delta => $file) { $elements[$delta] = array( - '#markup' => file_create_url($file->getFileUri()), + '#markup' => file_url_transform_relative(file_create_url($file->getFileUri())), '#cache' => array( 'tags' => $file->getCacheTags(), ), diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php index d28d26f21..34f170a4a 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php @@ -56,9 +56,11 @@ class FileFieldItemList extends EntityReferenceFieldItemList { $original_ids = array(); $langcode = $this->getLangcode(); $original = $entity->original; - $original_items = $original->hasTranslation($langcode) ? $original->getTranslation($langcode)->{$field_name} : $original->{$field_name}; - foreach ($original_items as $item) { - $original_ids[] = $item->target_id; + if ($original->hasTranslation($langcode)) { + $original_items = $original->getTranslation($langcode)->{$field_name}; + foreach ($original_items as $item) { + $original_ids[] = $item->target_id; + } } // Decrement file usage by 1 for files that were removed from the field. diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php index 944e1028a..887e3772c 100644 --- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php +++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php @@ -186,7 +186,6 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface { $elements['#description'] = $description; $elements['#field_name'] = $field_name; $elements['#language'] = $items->getLangcode(); - $elements['#display_field'] = (bool) $this->getFieldSetting('display_field'); // The field settings include defaults for the field type. However, this // widget is a base class for other widgets (e.g., ImageWidget) that may // act on field types without these expected settings. diff --git a/core/modules/file/src/Plugin/migrate/cckfield/FileField.php b/core/modules/file/src/Plugin/migrate/cckfield/d6/FileField.php similarity index 91% rename from core/modules/file/src/Plugin/migrate/cckfield/FileField.php rename to core/modules/file/src/Plugin/migrate/cckfield/d6/FileField.php index 022f01e98..4e4e45607 100644 --- a/core/modules/file/src/Plugin/migrate/cckfield/FileField.php +++ b/core/modules/file/src/Plugin/migrate/cckfield/d6/FileField.php @@ -2,10 +2,10 @@ /** * @file - * Contains \Drupal\file\Plugin\migrate\cckfield\FileField. + * Contains \Drupal\file\Plugin\migrate\cckfield\d6\FileField. */ -namespace Drupal\file\Plugin\migrate\cckfield; +namespace Drupal\file\Plugin\migrate\cckfield\d6; use Drupal\migrate\Entity\MigrationInterface; use Drupal\migrate\Row; diff --git a/core/modules/file/src/Plugin/migrate/cckfield/d7/FileField.php b/core/modules/file/src/Plugin/migrate/cckfield/d7/FileField.php new file mode 100644 index 000000000..d9426fd87 --- /dev/null +++ b/core/modules/file/src/Plugin/migrate/cckfield/d7/FileField.php @@ -0,0 +1,67 @@ + 'file_generic', + ]; + } + + /** + * {@inheritdoc} + */ + public function getFieldFormatterMap() { + return [ + 'default' => 'file_default', + 'url_plain' => 'file_url_plain', + 'path_plain' => 'file_url_plain', + 'image_plain' => 'image', + 'image_nodelink' => 'image', + 'image_imagelink' => 'image', + ]; + } + + /** + * {@inheritdoc} + */ + public function processCckFieldValues(MigrationInterface $migration, $field_name, $data) { + $process = [ + 'plugin' => 'iterator', + 'source' => $field_name, + 'process' => [ + 'target_id' => 'fid', + 'display' => 'display', + 'description' => 'description', + ], + ]; + $migration->mergeProcessOfProperty($field_name, $process); + } + + /** + * {@inheritdoc} + */ + public function getFieldType(Row $row) { + return $row->getSourceProperty('widget_type') == 'imagefield_widget' ? 'image' : 'file'; + } + +} diff --git a/core/modules/file/src/Plugin/migrate/cckfield/d7/ImageField.php b/core/modules/file/src/Plugin/migrate/cckfield/d7/ImageField.php new file mode 100644 index 000000000..e818d70ad --- /dev/null +++ b/core/modules/file/src/Plugin/migrate/cckfield/d7/ImageField.php @@ -0,0 +1,45 @@ + 'iterator', + 'source' => $field_name, + 'process' => [ + 'target_id' => 'fid', + 'alt' => 'alt', + 'title' => 'title', + 'width' => 'width', + 'height' => 'height', + ], + ]; + $migration->mergeProcessOfProperty($field_name, $process); + } + +} diff --git a/core/modules/file/src/Plugin/views/field/File.php b/core/modules/file/src/Plugin/views/field/File.php index 615aa9f8e..0b9c6965d 100644 --- a/core/modules/file/src/Plugin/views/field/File.php +++ b/core/modules/file/src/Plugin/views/field/File.php @@ -69,6 +69,12 @@ class File extends FieldPluginBase { protected function renderLink($data, ResultRow $values) { if (!empty($this->options['link_to_file']) && $data !== NULL && $data !== '') { $this->options['alter']['make_link'] = TRUE; + // @todo Wrap in file_url_transform_relative(). This is currently + // impossible. As a work-around, we could add the 'url.site' cache context + // to ensure different file URLs are generated for different sites in a + // multisite setup, including HTTP and HTTPS versions of the same site. + // But unfortunately it's impossible to bubble a cache context here. + // Fix in https://www.drupal.org/node/2646744. $this->options['alter']['path'] = file_create_url($this->getValue($values, 'uri')); } diff --git a/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php b/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php new file mode 100644 index 000000000..c9a3fb167 --- /dev/null +++ b/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php @@ -0,0 +1,208 @@ +drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + + // Create a file field on the "Basic page" node type. + $this->fieldName = strtolower($this->randomMachineName()); + $this->createFileField($this->fieldName, 'node', 'page'); + + // Create and login user. + $permissions = array( + 'access administration pages', + 'administer content translation', + 'administer content types', + 'administer languages', + 'create content translations', + 'create page content', + 'edit any page content', + 'translate any entity', + 'delete any page content', + ); + $admin_user = $this->drupalCreateUser($permissions); + $this->drupalLogin($admin_user); + + // Add a second and third language. + $edit = array(); + $edit['predefined_langcode'] = 'fr'; + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + + $edit = array(); + $edit['predefined_langcode'] = 'nl'; + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + + // Enable translation for "Basic page" nodes. + $edit = array( + 'entity_types[node]' => 1, + 'settings[node][page][translatable]' => 1, + "settings[node][page][fields][$this->fieldName]" => 1, + ); + $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration')); + \Drupal::entityManager()->clearCachedDefinitions(); + } + + /** + * Tests synced file fields on translated nodes. + */ + public function testSyncedFiles() { + // Verify that the file field on the "Basic page" node type is translatable. + $definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'page'); + $this->assertTrue($definitions[$this->fieldName]->isTranslatable(), 'Node file field is translatable.'); + + // Create a default language node. + $default_language_node = $this->drupalCreateNode(array('type' => 'page', 'title' => 'Lost in translation')); + + // Edit the node to upload a file. + $edit = array(); + $name = 'files[' . $this->fieldName . '_0]'; + $edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[0]->uri); + $this->drupalPostForm('node/' . $default_language_node->id() . '/edit', $edit, t('Save')); + $first_fid = $this->getLastFileId(); + + // Translate the node into French: remove the existing file. + $this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/fr', array(), t('Remove')); + + // Upload a different file. + $edit = array(); + $edit['title[0][value]'] = 'Bill Murray'; + $name = 'files[' . $this->fieldName . '_0]'; + $edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[1]->uri); + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + // This inspects the HTML after the post of the translation, the file + // should be displayed on the original node. + $this->assertRaw('file--mime-text-plain'); + $second_fid = $this->getLastFileId(); + + \Drupal::entityTypeManager()->getStorage('file')->resetCache(); + + /* @var $file \Drupal\file\FileInterface */ + + // Ensure the file status of the first file permanent. + $file = File::load($first_fid); + $this->assertTrue($file->isPermanent()); + + // Ensure the file status of the second file is permanent. + $file = File::load($second_fid); + $this->assertTrue($file->isPermanent()); + + // Translate the node into dutch: remove the existing file. + $this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/nl', array(), t('Remove')); + + // Upload a different file. + $edit = array(); + $edit['title[0][value]'] = 'Scarlett Johansson'; + $name = 'files[' . $this->fieldName . '_0]'; + $edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[2]->uri); + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $third_fid = $this->getLastFileId(); + + \Drupal::entityTypeManager()->getStorage('file')->resetCache(); + + // Ensure the first file is untouched. + $file = File::load($first_fid); + $this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.'); + // This inspects the HTML after the post of the translation, the file + // should be displayed on the original node. + $this->assertRaw('file--mime-text-plain'); + + // Ensure the file status of the second file is permanent. + $file = File::load($second_fid); + $this->assertTrue($file->isPermanent()); + + // Ensure the file status of the third file is permanent. + $file = File::load($third_fid); + $this->assertTrue($file->isPermanent()); + + // Edit the second translation: remove the existing file. + $this->drupalPostForm('fr/node/' . $default_language_node->id() . '/edit', array(), t('Remove')); + + // Upload a different file. + $edit = array(); + $edit['title[0][value]'] = 'David Bowie'; + $name = 'files[' . $this->fieldName . '_0]'; + $edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[3]->uri); + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $replaced_second_fid = $this->getLastFileId(); + + \Drupal::entityTypeManager()->getStorage('file')->resetCache(); + + // Ensure the first and third files are untouched. + $file = File::load($first_fid); + $this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.'); + + $file = File::load($third_fid); + $this->assertTrue($file->isPermanent()); + + // Ensure the file status of the replaced second file is permanent. + $file = File::load($replaced_second_fid); + $this->assertTrue($file->isPermanent()); + + // Ensure the file status of the old second file is now temporary. + $file = File::load($second_fid); + $this->assertTrue($file->isTemporary()); + + // Delete the third translation. + $this->drupalPostForm('nl/node/' . $default_language_node->id() . '/delete', array(), t('Delete Dutch translation')); + + \Drupal::entityTypeManager()->getStorage('file')->resetCache(); + + // Ensure the first and replaced second files are untouched. + $file = File::load($first_fid); + $this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.'); + + $file = File::load($replaced_second_fid); + $this->assertTrue($file->isPermanent()); + + // Ensure the file status of the third file is now temporary. + $file = File::load($third_fid); + $this->assertTrue($file->isTemporary()); + + // Delete the all translations. + $this->drupalPostForm('node/' . $default_language_node->id() . '/delete', array(), t('Delete all translations')); + + \Drupal::entityTypeManager()->getStorage('file')->resetCache(); + + // Ensure the file status of the all files are now temporary. + $file = File::load($first_fid); + $this->assertTrue($file->isTemporary(), 'First file still exists and is temporary.'); + + $file = File::load($replaced_second_fid); + $this->assertTrue($file->isTemporary()); + } + +} diff --git a/core/modules/file/src/Tests/FileTokenReplaceTest.php b/core/modules/file/src/Tests/FileTokenReplaceTest.php index 93a6c67a8..bfc85de3d 100644 --- a/core/modules/file/src/Tests/FileTokenReplaceTest.php +++ b/core/modules/file/src/Tests/FileTokenReplaceTest.php @@ -66,7 +66,8 @@ class FileTokenReplaceTest extends FileFieldTestBase { $metadata_tests['[file:path]'] = $base_bubbleable_metadata; $metadata_tests['[file:mime]'] = $base_bubbleable_metadata; $metadata_tests['[file:size]'] = $base_bubbleable_metadata; - $metadata_tests['[file:url]'] = $base_bubbleable_metadata; + $bubbleable_metadata = clone $base_bubbleable_metadata; + $metadata_tests['[file:url]'] = $bubbleable_metadata->addCacheContexts(['url.site']); $bubbleable_metadata = clone $base_bubbleable_metadata; $metadata_tests['[file:created]'] = $bubbleable_metadata->addCacheTags(['rendered']); $metadata_tests['[file:created:short]'] = $bubbleable_metadata; diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php index 76a112583..627379e8a 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php +++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php @@ -72,16 +72,18 @@ class FilterCaption extends FilterBase { $altered_html = drupal_render($filter_caption); // Load the altered HTML into a new DOMDocument and retrieve the element. - $updated_node = Html::load($altered_html)->getElementsByTagName('body') + $updated_nodes = Html::load($altered_html)->getElementsByTagName('body') ->item(0) - ->childNodes - ->item(0); + ->childNodes; - // Import the updated node from the new DOMDocument into the original - // one, importing also the child nodes of the updated node. - $updated_node = $dom->importNode($updated_node, TRUE); - // Finally, replace the original node with the new node. - $node->parentNode->replaceChild($updated_node, $node); + foreach ($updated_nodes as $updated_node) { + // Import the updated node from the new DOMDocument into the original + // one, importing also the child nodes of the updated node. + $updated_node = $dom->importNode($updated_node, TRUE); + $node->parentNode->insertBefore($updated_node, $node); + } + // Finally, remove the original data-caption node. + $node->parentNode->removeChild($node); } $result->setProcessedText(Html::serialize($dom)) diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtml.php b/core/modules/filter/src/Plugin/Filter/FilterHtml.php index 63c22b35e..63985ffdd 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterHtml.php +++ b/core/modules/filter/src/Plugin/Filter/FilterHtml.php @@ -49,7 +49,7 @@ class FilterHtml extends FilterBase { '#type' => 'textfield', '#title' => $this->t('Allowed HTML tags'), '#default_value' => $this->settings['allowed_html'], - '#maxlength' => 1024, + '#maxlength' => 2048, '#description' => $this->t('A list of HTML tags that can be used. By default only the lang and dir attributes are allowed for all HTML tags. Each HTML tag may have attributes which are treated as allowed attribute names for that HTML tag. Each attribute may allow all values, or only allow specific values. Attribute names or values may be written as a prefix and wildcard like jump-*. JavaScript event attributes, JavaScript URLs, and CSS are always stripped.'), '#size' => 250, '#attached' => array( diff --git a/core/modules/filter/src/Tests/FilterCaptionTwigDebugTest.php b/core/modules/filter/src/Tests/FilterCaptionTwigDebugTest.php new file mode 100644 index 000000000..8b3d7cabb --- /dev/null +++ b/core/modules/filter/src/Tests/FilterCaptionTwigDebugTest.php @@ -0,0 +1,109 @@ +container->getParameter('twig.config'); + if (!$parameters['debug']) { + $parameters['debug'] = TRUE; + $this->setContainerParameter('twig.config', $parameters); + $this->rebuildContainer(); + $this->resetAll(); + } + } + + /** + * Disables Twig debugging. + */ + protected function debugOff() { + // Disable debug, rebuild the service container, and clear all caches. + $parameters = $this->container->getParameter('twig.config'); + if ($parameters['debug']) { + $parameters['debug'] = FALSE; + $this->setContainerParameter('twig.config', $parameters); + $this->rebuildContainer(); + $this->resetAll(); + } + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->debugOn(); + + $manager = $this->container->get('plugin.manager.filter'); + $bag = new FilterPluginCollection($manager, []); + $this->filters = $bag->getAll(); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + $this->debugOff(); + } + + /** + * Test the caption filter with Twig debugging on. + */ + function testCaptionFilter() { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $filter = $this->filters['filter_caption']; + + $test = function ($input) use ($filter, $renderer) { + return $renderer->executeInRenderContext(new RenderContext(), function () use ($input, $filter) { + return $filter->process($input, 'und'); + }); + }; + + // No data-caption attribute. + $input = ''; + $expected = $input; + $this->assertIdentical($expected, $test($input)->getProcessedText()); + + // Data-caption attribute. + $input = ''; + $expected = '
Loquacious llama!
'; + $output = $test($input); + $output = $output->getProcessedText(); + $this->assertTrue(strpos($output, $expected) !== FALSE, "\"$output\" contains \"$expected\""); + $this->assertTrue(strpos($output, '') !== FALSE, 'filter_caption theme hook debug comment is present.'); + } + +} diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index c82c167b5..342ed7b99 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -501,7 +501,12 @@ function template_preprocess_forums(&$variables) { } $row[] = array( - 'data' => $topic->comment_count . $new_replies, + 'data' => [ + [ + '#prefix' => $topic->comment_count, + '#markup' => $new_replies, + ], + ], 'class' => array('forum__replies'), ); $row[] = array( diff --git a/core/modules/forum/src/Tests/ForumTest.php b/core/modules/forum/src/Tests/ForumTest.php index 974e4009c..c81828eca 100644 --- a/core/modules/forum/src/Tests/ForumTest.php +++ b/core/modules/forum/src/Tests/ForumTest.php @@ -514,6 +514,9 @@ class ForumTest extends WebTestBase { // Check that forum renders properly. $this->drupalGet("forum/{$this->forum['tid']}"); $this->assertResponse(200); + + // Verify there is no unintentional HTML tag escaping. + $this->assertNoEscaped('<', ''); } /** diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc index b3d4a6e7d..777e43855 100644 --- a/core/modules/image/image.admin.inc +++ b/core/modules/image/image.admin.inc @@ -34,7 +34,7 @@ function template_preprocess_image_style_preview(&$variables) { $original_path = \Drupal::config('image.settings')->get('preview_image'); $original_image = $image_factory->get($original_path); $variables['original'] = array( - 'url' => file_create_url($original_path), + 'url' => file_url_transform_relative(file_create_url($original_path)), 'width' => $original_image->getWidth(), 'height' => $original_image->getHeight(), ); @@ -55,7 +55,7 @@ function template_preprocess_image_style_preview(&$variables) { } $preview_image = $image_factory->get($preview_file); $variables['derivative'] = array( - 'url' => file_create_url($preview_file), + 'url' => file_url_transform_relative(file_create_url($preview_file)), 'width' => $preview_image->getWidth(), 'height' => $preview_image->getHeight(), ); diff --git a/core/modules/image/image.post_update.php b/core/modules/image/image.post_update.php new file mode 100644 index 000000000..04d8c4b7b --- /dev/null +++ b/core/modules/image/image.post_update.php @@ -0,0 +1,22 @@ +save(); + } +} diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php index f3d020785..85f4cebe0 100644 --- a/core/modules/image/src/Entity/ImageStyle.php +++ b/core/modules/image/src/Entity/ImageStyle.php @@ -36,6 +36,7 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; * "flush" = "Drupal\image\Form\ImageStyleFlushForm" * }, * "list_builder" = "Drupal\image\ImageStyleListBuilder", + * "storage" = "Drupal\image\ImageStyleStorage", * }, * admin_permission = "administer image styles", * config_prefix = "style", @@ -58,13 +59,6 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; */ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, EntityWithPluginCollectionInterface { - /** - * The name of the image style to use as replacement upon delete. - * - * @var string - */ - protected $replacementID; - /** * The name of the image style. * @@ -128,17 +122,13 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, Entity public static function postDelete(EntityStorageInterface $storage, array $entities) { parent::postDelete($storage, $entities); + /** @var \Drupal\image\ImageStyleInterface[] $entities */ foreach ($entities as $style) { // Flush cached media for the deleted style. $style->flush(); - // Check whether field settings need to be updated. - // In case no replacement style was specified, all image fields that are - // using the deleted style are left in a broken state. - if (!$style->isSyncing() && $new_id = $style->getReplacementID()) { - // The deleted ID is still set as originalID. - $style->setName($new_id); - static::replaceImageStyle($style); - } + // Clear the replacement ID, if one has been previously stored. + /** @var \Drupal\image\ImageStyleStorageInterface $storage */ + $storage->clearReplacementId($style->id()); } } @@ -380,7 +370,9 @@ class ImageStyle extends ConfigEntityBase implements ImageStyleInterface, Entity * {@inheritdoc} */ public function getReplacementID() { - return $this->get('replacementID'); + /** @var \Drupal\image\ImageStyleStorageInterface $storage */ + $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId()); + return $storage->getReplacementId($this->id()); } /** diff --git a/core/modules/image/src/Form/ImageStyleDeleteForm.php b/core/modules/image/src/Form/ImageStyleDeleteForm.php index 5c45d940c..0638369ac 100644 --- a/core/modules/image/src/Form/ImageStyleDeleteForm.php +++ b/core/modules/image/src/Form/ImageStyleDeleteForm.php @@ -15,6 +15,13 @@ use Drupal\Core\Form\FormStateInterface; */ class ImageStyleDeleteForm extends EntityDeleteForm { + /** + * Replacement options. + * + * @var array + */ + protected $replacementOptions; + /** * {@inheritdoc} */ @@ -25,20 +32,28 @@ class ImageStyleDeleteForm extends EntityDeleteForm { * {@inheritdoc} */ public function getDescription() { - return $this->t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted.'); + if (count($this->getReplacementOptions()) > 1) { + return $this->t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted. If no replacement style is selected, the dependent configurations might need manual reconfiguration.'); + } + return $this->t('All images that have been generated for this style will be permanently deleted. The dependent configurations might need manual reconfiguration.'); } /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { - $replacement_styles = array_diff_key(image_style_options(), array($this->entity->id() => '')); - $form['replacement'] = array( - '#title' => $this->t('Replacement style'), - '#type' => 'select', - '#options' => $replacement_styles, - '#empty_option' => $this->t('No replacement, just delete'), - ); + $replacement_styles = $this->getReplacementOptions(); + // If there are non-empty options in the list, allow the user to optionally + // pick up a replacement. + if (count($replacement_styles) > 1) { + $form['replacement'] = [ + '#type' => 'select', + '#title' => $this->t('Replacement style'), + '#options' => $replacement_styles, + '#empty_option' => $this->t('- No replacement -'), + '#weight' => -5, + ]; + } return parent::form($form, $form_state); } @@ -47,9 +62,27 @@ class ImageStyleDeleteForm extends EntityDeleteForm { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $this->entity->set('replacementID', $form_state->getValue('replacement')); - + // Save a selected replacement in the image style storage. It will be used + // later, in the same request, when resolving dependencies. + if ($replacement = $form_state->getValue('replacement')) { + /** @var \Drupal\image\ImageStyleStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($this->entity->getEntityTypeId()); + $storage->setReplacementId($this->entity->id(), $replacement); + } parent::submitForm($form, $form_state); } + /** + * Returns a list of image style replacement options. + * + * @return array + * An option list suitable for the form select '#options'. + */ + protected function getReplacementOptions() { + if (!isset($this->replacementOptions)) { + $this->replacementOptions = array_diff_key(image_style_options(), [$this->getEntity()->id() => '']); + } + return $this->replacementOptions; + } + } diff --git a/core/modules/image/src/ImageStyleInterface.php b/core/modules/image/src/ImageStyleInterface.php index 950729151..39bb583d7 100644 --- a/core/modules/image/src/ImageStyleInterface.php +++ b/core/modules/image/src/ImageStyleInterface.php @@ -17,8 +17,14 @@ interface ImageStyleInterface extends ConfigEntityInterface { /** * Returns the replacement ID. * - * @return string - * The name of the image style to use as replacement upon delete. + * @return string|null + * The replacement image style ID or NULL if no replacement has been + * selected. + * + * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.x. Use + * \Drupal\image\ImageStyleStorageInterface::getReplacementId() instead. + * + * @see \Drupal\image\ImageStyleStorageInterface::getReplacementId() */ public function getReplacementID(); @@ -70,6 +76,7 @@ interface ImageStyleInterface extends ConfigEntityInterface { * in an tag. Requesting the URL will cause the image to be created. * * @see \Drupal\image\Controller\ImageStyleDownloadController::deliver() + * @see file_url_transform_relative() */ public function buildUrl($path, $clean_urls = NULL); diff --git a/core/modules/image/src/ImageStyleListBuilder.php b/core/modules/image/src/ImageStyleListBuilder.php index b55a2fded..91cdec1ec 100644 --- a/core/modules/image/src/ImageStyleListBuilder.php +++ b/core/modules/image/src/ImageStyleListBuilder.php @@ -91,9 +91,9 @@ class ImageStyleListBuilder extends ConfigEntityListBuilder { */ public function render() { $build = parent::render(); - $build['#empty'] = $this->t('There are currently no styles. Add a new one.', array( + $build['table']['#empty'] = $this->t('There are currently no styles. Add a new one.', [ ':url' => $this->urlGenerator->generateFromRoute('image.style_add'), - )); + ]); return $build; } diff --git a/core/modules/image/src/ImageStyleStorage.php b/core/modules/image/src/ImageStyleStorage.php new file mode 100644 index 000000000..606d3efa8 --- /dev/null +++ b/core/modules/image/src/ImageStyleStorage.php @@ -0,0 +1,51 @@ +replacement[$name] = $replacement; + } + + /** + * {@inheritdoc} + */ + public function getReplacementId($name) { + return isset($this->replacement[$name]) ? $this->replacement[$name] : NULL; + } + + /** + * {@inheritdoc} + */ + public function clearReplacementId($name) { + unset($this->replacement[$name]); + } + +} diff --git a/core/modules/image/src/ImageStyleStorageInterface.php b/core/modules/image/src/ImageStyleStorageInterface.php new file mode 100644 index 000000000..014ff7cbe --- /dev/null +++ b/core/modules/image/src/ImageStyleStorageInterface.php @@ -0,0 +1,57 @@ + $file) { + $cache_contexts = array(); if (isset($link_file)) { $image_uri = $file->getFileUri(); + // @todo Wrap in file_url_transform_relative(). This is currently + // impossible. As a work-around, we currently add the 'url.site' cache + // context to ensure different file URLs are generated for different + // sites in a multisite setup, including HTTP and HTTPS versions of the + // same site. Fix in https://www.drupal.org/node/2646744. $url = Url::fromUri(file_create_url($image_uri)); + $cache_contexts[] = 'url.site'; } $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags()); @@ -219,6 +227,7 @@ class ImageFormatter extends ImageFormatterBase implements ContainerFactoryPlugi '#url' => $url, '#cache' => array( 'tags' => $cache_tags, + 'contexts' => $cache_contexts, ), ); } @@ -226,4 +235,41 @@ class ImageFormatter extends ImageFormatterBase implements ContainerFactoryPlugi return $elements; } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + $style_id = $this->getSetting('image_style'); + /** @var \Drupal\image\ImageStyleInterface $style */ + if ($style_id && $style = ImageStyle::load($style_id)) { + // If this formatter uses a valid image style to display the image, add + // the image style configuration entity as dependency of this formatter. + $dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName(); + } + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + $style_id = $this->getSetting('image_style'); + /** @var \Drupal\image\ImageStyleInterface $style */ + if ($style_id && $style = ImageStyle::load($style_id)) { + if (!empty($dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) { + $replacement_id = $this->imageStyleStorage->getReplacementId($style_id); + // If a valid replacement has been provided in the storage, replace the + // image style with the replacement and signal that the formatter plugin + // settings were updated. + if ($replacement_id && ImageStyle::load($replacement_id)) { + $this->setSetting('image_style', $replacement_id); + $changed = TRUE; + } + } + } + return $changed; + } + } diff --git a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php index 82092e65a..10ebfb5a8 100644 --- a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php +++ b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php @@ -12,6 +12,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\file\Entity\File; use Drupal\file\Plugin\Field\FieldWidget\FileWidget; +use Drupal\image\Entity\ImageStyle; /** * Plugin implementation of the 'image_image' widget. @@ -273,4 +274,49 @@ class ImageWidget extends FileWidget { } } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + $style_id = $this->getSetting('preview_image_style'); + /** @var \Drupal\image\ImageStyleInterface $style */ + if ($style_id && $style = ImageStyle::load($style_id)) { + // If this widget uses a valid image style to display the preview of the + // uploaded image, add that image style configuration entity as dependency + // of this widget. + $dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName(); + } + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + $style_id = $this->getSetting('preview_image_style'); + /** @var \Drupal\image\ImageStyleInterface $style */ + if ($style_id && $style = ImageStyle::load($style_id)) { + if (!empty($dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) { + /** @var \Drupal\image\ImageStyleStorageInterface $storage */ + $storage = \Drupal::entityManager()->getStorage($style->getEntityTypeId()); + $replacement_id = $storage->getReplacementId($style_id); + // If a valid replacement has been provided in the storage, replace the + // preview image style with the replacement. + if ($replacement_id && ImageStyle::load($replacement_id)) { + $this->setSetting('preview_image_style', $replacement_id); + } + // If there's no replacement or the replacement is invalid, disable the + // image preview. + else { + $this->setSetting('preview_image_style', ''); + } + // Signal that the formatter plugin settings were updated. + $changed = TRUE; + } + } + return $changed; + } + } diff --git a/core/modules/image/src/Tests/ImageAdminStylesTest.php b/core/modules/image/src/Tests/ImageAdminStylesTest.php index ed3854fcb..474579b3a 100644 --- a/core/modules/image/src/Tests/ImageAdminStylesTest.php +++ b/core/modules/image/src/Tests/ImageAdminStylesTest.php @@ -8,6 +8,7 @@ namespace Drupal\image\Tests; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\image\Entity\ImageStyle; use Drupal\image\ImageStyleInterface; use Drupal\node\Entity\Node; @@ -273,6 +274,19 @@ class ImageAdminStylesTest extends ImageFieldTestBase { $this->assertFalse(ImageStyle::load($style_name), format_string('Image style %style successfully deleted.', array('%style' => $style->label()))); + // Test empty text when there are no image styles. + + // Delete all image styles. + foreach (ImageStyle::loadMultiple() as $image_style) { + $image_style->delete(); + } + + // Confirm that the empty text is correct on the image styles page. + $this->drupalGet($admin_path); + $this->assertRaw(t('There are currently no styles. Add a new one.', [ + ':url' => \Drupal::url('image.style_add'), + ])); + } /** @@ -307,7 +321,7 @@ class ImageAdminStylesTest extends ImageFieldTestBase { // Test that image is displayed using newly created style. $this->drupalGet('node/' . $nid); - $this->assertRaw($style->buildUrl($original_uri), format_string('Image displayed using style @style.', array('@style' => $style_name))); + $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)), format_string('Image displayed using style @style.', array('@style' => $style_name))); // Rename the style and make sure the image field is updated. $new_style_name = strtolower($this->randomMachineName(10)); @@ -322,7 +336,7 @@ class ImageAdminStylesTest extends ImageFieldTestBase { // Reload the image style using the new name. $style = ImageStyle::load($new_style_name); - $this->assertRaw($style->buildUrl($original_uri), 'Image displayed using style replacement style.'); + $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)), 'Image displayed using style replacement style.'); // Delete the style and choose a replacement style. $edit = array( @@ -334,7 +348,7 @@ class ImageAdminStylesTest extends ImageFieldTestBase { $replacement_style = ImageStyle::load('thumbnail'); $this->drupalGet('node/' . $nid); - $this->assertRaw($replacement_style->buildUrl($original_uri), 'Image displayed using style replacement style.'); + $this->assertRaw(file_url_transform_relative($replacement_style->buildUrl($original_uri)), 'Image displayed using style replacement style.'); } /** @@ -441,11 +455,16 @@ class ImageAdminStylesTest extends ImageFieldTestBase { // Test that image is displayed using newly created style. $this->drupalGet('node/' . $nid); - $this->assertRaw($style->buildUrl($original_uri), format_string('Image displayed using style @style.', array('@style' => $style_name))); + $this->assertRaw(file_url_transform_relative($style->buildUrl($original_uri)), format_string('Image displayed using style @style.', array('@style' => $style_name))); // Copy config to sync, and delete the image style. $sync = $this->container->get('config.storage.sync'); $active = $this->container->get('config.storage'); + // Remove the image field from the display, to avoid a dependency error + // during import. + EntityViewDisplay::load('node.article.default') + ->removeComponent($field_name) + ->save(); $this->copyConfig($active, $sync); $sync->delete('image.style.' . $style_name); $this->configImporter()->import(); diff --git a/core/modules/image/src/Tests/ImageDimensionsTest.php b/core/modules/image/src/Tests/ImageDimensionsTest.php index c7a738dca..984e06084 100644 --- a/core/modules/image/src/Tests/ImageDimensionsTest.php +++ b/core/modules/image/src/Tests/ImageDimensionsTest.php @@ -41,7 +41,7 @@ class ImageDimensionsTest extends WebTestBase { $style = entity_create('image_style', array('name' => 'test', 'label' => 'Test')); $style->save(); $generated_uri = 'public://styles/test/public/'. \Drupal::service('file_system')->basename($original_uri); - $url = $style->buildUrl($original_uri); + $url = file_url_transform_relative($style->buildUrl($original_uri)); $variables = array( '#theme' => 'image_style', @@ -70,7 +70,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); @@ -91,7 +91,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); @@ -113,7 +113,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); @@ -135,7 +135,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); @@ -153,7 +153,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); @@ -174,7 +174,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); @@ -194,7 +194,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); @@ -215,7 +215,7 @@ class ImageDimensionsTest extends WebTestBase { $style->save(); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); @@ -251,10 +251,10 @@ class ImageDimensionsTest extends WebTestBase { ]; // PNG original image. Should be resized to 100x100. $generated_uri = 'public://styles/test_uri/public/'. \Drupal::service('file_system')->basename($original_uri); - $url = $style->buildUrl($original_uri); + $url = file_url_transform_relative($style->buildUrl($original_uri)); $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); @@ -264,11 +264,11 @@ class ImageDimensionsTest extends WebTestBase { $file = $files[1]; $original_uri = file_unmanaged_copy($file->uri, 'public://', FILE_EXISTS_RENAME); $generated_uri = 'public://styles/test_uri/public/'. \Drupal::service('file_system')->basename($original_uri); - $url = $style->buildUrl($original_uri); + $url = file_url_transform_relative($style->buildUrl($original_uri)); $variables['#uri'] = $original_uri; $this->assertEqual($this->getImageTag($variables), ''); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $this->drupalGet($url); + $this->drupalGet($this->getAbsoluteUrl($url)); $this->assertResponse(200, 'Image was generated at the URL.'); $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.'); $image_file = $image_factory->get($generated_uri); diff --git a/core/modules/image/src/Tests/ImageFieldDisplayTest.php b/core/modules/image/src/Tests/ImageFieldDisplayTest.php index 53ba09f07..2ae0fa45f 100644 --- a/core/modules/image/src/Tests/ImageFieldDisplayTest.php +++ b/core/modules/image/src/Tests/ImageFieldDisplayTest.php @@ -127,6 +127,8 @@ class ImageFieldDisplayTest extends ImageFieldTestBase { $default_output = '' . $renderer->renderRoot($image) . ''; $this->drupalGet('node/' . $nid); $this->assertCacheTag($file->getCacheTags()[0]); + // @todo Remove in https://www.drupal.org/node/2646744. + $this->assertCacheContext('url.site'); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); $this->assertRaw($default_output, 'Image linked to file formatter displaying correctly on full node view.'); @@ -165,7 +167,7 @@ class ImageFieldDisplayTest extends ImageFieldTestBase { '//a[@href=:path]/img[@src=:url and @alt=:alt and @width=:width and @height=:height]', array( ':path' => $node->url(), - ':url' => file_create_url($image['#uri']), + ':url' => file_url_transform_relative(file_create_url($image['#uri'])), ':width' => $image['#width'], ':height' => $image['#height'], ':alt' => $alt, @@ -256,7 +258,7 @@ class ImageFieldDisplayTest extends ImageFieldTestBase { $node = $node_storage->load($nid); $file = $node->{$field_name}->entity; - $url = file_create_url(ImageStyle::load('medium')->buildUrl($file->getFileUri())); + $url = file_url_transform_relative(file_create_url(ImageStyle::load('medium')->buildUrl($file->getFileUri()))); $this->assertTrue($this->cssSelect('img[width=40][height=20][class=image-style-medium][src="' . $url . '"]')); // Add alt/title fields to the image and verify that they are displayed. diff --git a/core/modules/image/src/Tests/ImageFieldTestBase.php b/core/modules/image/src/Tests/ImageFieldTestBase.php index 271b969ae..9984a76a7 100644 --- a/core/modules/image/src/Tests/ImageFieldTestBase.php +++ b/core/modules/image/src/Tests/ImageFieldTestBase.php @@ -66,9 +66,11 @@ abstract class ImageFieldTestBase extends WebTestBase { * @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. + * Widget settings to be added to the widget defaults. + * @param array $formatter_settings + * Formatter settings to be added to the formatter defaults. */ - function createImageField($name, $type_name, $storage_settings = array(), $field_settings = array(), $widget_settings = array()) { + function createImageField($name, $type_name, $storage_settings = array(), $field_settings = array(), $widget_settings = array(), $formatter_settings = array()) { entity_create('field_storage_config', array( 'field_name' => $name, 'entity_type' => 'node', @@ -95,7 +97,10 @@ abstract class ImageFieldTestBase extends WebTestBase { ->save(); entity_get_display('node', $type_name, 'default') - ->setComponent($name) + ->setComponent($name, array( + 'type' => 'image', + 'settings' => $formatter_settings, + )) ->save(); return $field_config; diff --git a/core/modules/image/src/Tests/ImageStyleDeleteTest.php b/core/modules/image/src/Tests/ImageStyleDeleteTest.php new file mode 100644 index 000000000..7bcedb33a --- /dev/null +++ b/core/modules/image/src/Tests/ImageStyleDeleteTest.php @@ -0,0 +1,86 @@ +createImageField('foo', 'page', [], [], ['preview_image_style' => 'medium'], ['image_style' => 'medium']); + } + + /** + * Tests image style deletion. + */ + public function testDelete() { + $this->drupalGet('admin/config/media/image-styles/manage/medium/delete'); + // Checks that the 'replacement' select element is displayed. + $this->assertFieldByName('replacement'); + // Checks that UI messages are correct. + $this->assertRaw(t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted. If no replacement style is selected, the dependent configurations might need manual reconfiguration.')); + $this->assertNoRaw(t('All images that have been generated for this style will be permanently deleted. The dependent configurations might need manual reconfiguration.')); + + // Delete 'medium' image style but replace it with 'thumbnail'. This style + // is involved in 'node.page.default' display view and form. + $this->drupalPostForm(NULL, ['replacement' => 'thumbnail'], t('Delete')); + + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */ + $view_display = EntityViewDisplay::load('node.page.default'); + // Checks that the formatter setting is replaced. + if ($this->assertNotNull($component = $view_display->getComponent('foo'))) { + $this->assertIdentical($component['settings']['image_style'], 'thumbnail'); + } + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = EntityFormDisplay::load('node.page.default'); + // Check that the widget setting is replaced. + if ($this->assertNotNull($component = $form_display->getComponent('foo'))) { + $this->assertIdentical($component['settings']['preview_image_style'], 'thumbnail'); + } + + $this->drupalGet('admin/config/media/image-styles/manage/thumbnail/delete'); + // Checks that the 'replacement' select element is displayed. + $this->assertFieldByName('replacement'); + // Checks that UI messages are correct. + $this->assertRaw(t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted. If no replacement style is selected, the dependent configurations might need manual reconfiguration.')); + $this->assertNoRaw(t('All images that have been generated for this style will be permanently deleted. The dependent configurations might need manual reconfiguration.')); + + // Delete 'thumbnail' image style. Provide no replacement. + $this->drupalPostForm(NULL, [], t('Delete')); + + $view_display = EntityViewDisplay::load('node.page.default'); + // Checks that the formatter setting is disabled. + $this->assertNull($view_display->getComponent('foo')); + $this->assertNotNull($view_display->get('hidden')['foo']); + // Checks that widget setting is preserved with the image preview disabled. + $form_display = EntityFormDisplay::load('node.page.default'); + $this->assertNotNull($widget = $form_display->getComponent('foo')); + $this->assertIdentical($widget['settings']['preview_image_style'], ''); + + // Now, there's only one image style configured on the system: 'large'. + $this->drupalGet('admin/config/media/image-styles/manage/large/delete'); + // Checks that the 'replacement' select element is not displayed. + $this->assertNoFieldByName('replacement'); + // Checks that UI messages are correct. + $this->assertNoRaw(t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted. If no replacement style is selected, the dependent configurations might need manual reconfiguration.')); + $this->assertRaw(t('All images that have been generated for this style will be permanently deleted. The dependent configurations might need manual reconfiguration.')); + } + +} diff --git a/core/modules/image/src/Tests/ImageThemeFunctionTest.php b/core/modules/image/src/Tests/ImageThemeFunctionTest.php index 54b471b78..4d4dd9815 100644 --- a/core/modules/image/src/Tests/ImageThemeFunctionTest.php +++ b/core/modules/image/src/Tests/ImageThemeFunctionTest.php @@ -74,7 +74,7 @@ class ImageThemeFunctionTest extends WebTestBase { // Create a style. $style = entity_create('image_style', array('name' => 'test', 'label' => 'Test')); $style->save(); - $url = $style->buildUrl($original_uri); + $url = file_url_transform_relative($style->buildUrl($original_uri)); // Create a test entity with the image field set. $entity = entity_create('entity_test'); @@ -136,7 +136,7 @@ class ImageThemeFunctionTest extends WebTestBase { // Create a style. $style = entity_create('image_style', array('name' => 'image_test', 'label' => 'Test')); $style->save(); - $url = $style->buildUrl($original_uri); + $url = file_url_transform_relative($style->buildUrl($original_uri)); // Create the base element that we'll use in the tests below. $base_element = array( diff --git a/core/modules/image/src/Tests/Update/ImageUpdateTest.php b/core/modules/image/src/Tests/Update/ImageUpdateTest.php new file mode 100644 index 000000000..f37ca8fed --- /dev/null +++ b/core/modules/image/src/Tests/Update/ImageUpdateTest.php @@ -0,0 +1,59 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8-rc1.bare.standard.php.gz', + ]; + } + + /** + * Tests image_post_update_image_style_dependencies(). + * + * @see image_post_update_image_style_dependencies() + */ + public function testPostUpdateImageStylesDependencies() { + $view = 'core.entity_view_display.node.article.default'; + $form = 'core.entity_form_display.node.article.default'; + + // Check that view display 'node.article.default' doesn't depend on image + // style 'image.style.large'. + $dependencies = $this->config($view)->get('dependencies.config'); + $this->assertFalse(in_array('image.style.large', $dependencies)); + // Check that form display 'node.article.default' doesn't depend on image + // style 'image.style.thumbnail'. + $dependencies = $this->config($form)->get('dependencies.config'); + $this->assertFalse(in_array('image.style.thumbnail', $dependencies)); + + // Run updates. + $this->runUpdates(); + + // Check that view display 'node.article.default' depend on image style + // 'image.style.large'. + $dependencies = $this->config($view)->get('dependencies.config'); + $this->assertTrue(in_array('image.style.large', $dependencies)); + // Check that form display 'node.article.default' depend on image style + // 'image.style.thumbnail'. + $dependencies = $this->config($view)->get('dependencies.config'); + $this->assertTrue(in_array('image.style.large', $dependencies)); + } + +} diff --git a/core/modules/image/tests/src/Kernel/ImageStyleIntegrationTest.php b/core/modules/image/tests/src/Kernel/ImageStyleIntegrationTest.php new file mode 100644 index 000000000..304718feb --- /dev/null +++ b/core/modules/image/tests/src/Kernel/ImageStyleIntegrationTest.php @@ -0,0 +1,117 @@ + 'main_style']); + $style->save(); + /** @var \Drupal\image\ImageStyleInterface $replacement */ + $replacement = ImageStyle::create(['name' => 'replacement_style']); + $replacement->save(); + + // Create a node-type, named 'note'. + $node_type = NodeType::create(['type' => 'note']); + $node_type->save(); + + // Create an image field and attach it to the 'note' node-type. + FieldStorageConfig::create([ + 'entity_type' => 'node', + 'field_name' => 'sticker', + 'type' => 'image', + ])->save(); + FieldConfig::create([ + 'entity_type' => 'node', + 'field_name' => 'sticker', + 'bundle' => 'note', + ])->save(); + + // Create the default entity view display and set the 'sticker' field to use + // the 'main_style' images style in formatter. + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */ + $view_display = EntityViewDisplay::create([ + 'targetEntityType' => 'node', + 'bundle' => 'note', + 'mode' => 'default', + 'status' => TRUE, + ])->setComponent('sticker', ['settings' => ['image_style' => 'main_style']]); + $view_display->save(); + + // Create the default entity form display and set the 'sticker' field to use + // the 'main_style' images style in the widget. + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = EntityFormDisplay::create([ + 'targetEntityType' => 'node', + 'bundle' => 'note', + 'mode' => 'default', + 'status' => TRUE, + ])->setComponent('sticker', ['settings' => ['preview_image_style' => 'main_style']]); + $form_display->save(); + + // Check that the entity displays exists before dependency removal. + $this->assertNotNull(EntityViewDisplay::load($view_display->id())); + $this->assertNotNull(EntityFormDisplay::load($form_display->id())); + + // Delete the 'main_style' image style. Before that, emulate the UI process + // of selecting a replacement style by setting the replacement image style + // ID in the image style storage. + /** @var \Drupal\image\ImageStyleStorageInterface $storage */ + $storage = $this->container->get('entity.manager')->getStorage($style->getEntityTypeId()); + $storage->setReplacementId('main_style', 'replacement_style'); + $style->delete(); + + // Check that the entity displays exists after dependency removal. + $this->assertNotNull($view_display = EntityViewDisplay::load($view_display->id())); + $this->assertNotNull($form_display = EntityFormDisplay::load($form_display->id())); + // Check that the 'sticker' formatter component exists in both displays. + $this->assertNotNull($formatter = $view_display->getComponent('sticker')); + $this->assertNotNull($widget = $form_display->getComponent('sticker')); + // Check that both displays are using now 'replacement_style' for images. + $this->assertSame('replacement_style', $formatter['settings']['image_style']); + $this->assertSame('replacement_style', $widget['settings']['preview_image_style']); + + // Delete the 'replacement_style' without setting a replacement image style. + $replacement->delete(); + + // The entity view and form displays exists after dependency removal. + $this->assertNotNull($view_display = EntityViewDisplay::load($view_display->id())); + $this->assertNotNull($form_display = EntityFormDisplay::load($form_display->id())); + // The 'sticker' formatter component should be hidden in view display. + $this->assertNull($view_display->getComponent('sticker')); + $this->assertTrue($view_display->get('hidden')['sticker']); + // The 'sticker' widget component should be active in form displays, but the + // image preview should be disabled. + $this->assertNotNull($widget = $form_display->getComponent('sticker')); + $this->assertSame('', $widget['settings']['preview_image_style']); + } + +} diff --git a/core/modules/link/src/Tests/LinkFieldTest.php b/core/modules/link/src/Tests/LinkFieldTest.php index eada49430..79c2cbba5 100644 --- a/core/modules/link/src/Tests/LinkFieldTest.php +++ b/core/modules/link/src/Tests/LinkFieldTest.php @@ -101,6 +101,10 @@ class LinkFieldTest extends WebTestBase { // strings displayed to the user). $valid_external_entries = array( 'http://www.example.com/' => 'http://www.example.com/', + // Strings within parenthesis without leading space char. + 'http://www.example.com/strings_(string_within_parenthesis)' => 'http://www.example.com/strings_(string_within_parenthesis)', + // Numbers within parenthesis without leading space char. + 'http://www.example.com/numbers_(9999)' => 'http://www.example.com/numbers_(9999)', ); $valid_internal_entries = array( '/entity_test/add' => '/entity_test/add', diff --git a/core/modules/link/src/Tests/Views/LinkViewsTokensTest.php b/core/modules/link/src/Tests/Views/LinkViewsTokensTest.php new file mode 100644 index 000000000..558c5a850 --- /dev/null +++ b/core/modules/link/src/Tests/Views/LinkViewsTokensTest.php @@ -0,0 +1,102 @@ +drupalCreateContentType(array( + 'type' => 'page', + 'name' => 'Basic page' + )); + + // Create a field. + FieldStorageConfig::create(array( + 'field_name' => $this->fieldName, + 'type' => 'link', + 'entity_type' => 'node', + 'cardinality' => 1, + ))->save(); + FieldConfig::create(array( + 'field_name' => $this->fieldName, + 'entity_type' => 'node', + 'bundle' => 'page', + 'label' => 'link field', + ))->save(); + + } + + public function testLinkViewsTokens() { + // Array of URI's to test. + $uris = [ + 'http://www.drupal.org' => 'Drupal.org', + ]; + + // Add nodes with the URI's and titles. + foreach ($uris as $uri => $title) { + $values = array('type' => 'page'); + $values[$this->fieldName][] = ['uri' => $uri, 'title' => $title, 'options' => ['attributes' => ['class' => 'test-link-class']]]; + $this->drupalCreateNode($values); + } + + $this->drupalGet('test_link_tokens'); + + foreach ($uris as $uri => $title) { + // Formatted link: {{ field_link }}
+ $this->assertRaw("Formated: $title"); + + // Raw uri: {{ field_link__uri }}
+ $this->assertRaw("Raw uri: $uri"); + + // Raw title: {{ field_link__title }}
+ $this->assertRaw("Raw title: $title"); + + // Raw options: {{ field_link__options }}
+ // Options is an array and should return empty after token replace. + $this->assertRaw("Raw options: ."); + } + } +} diff --git a/core/modules/link/tests/modules/link_test_views/link_test_views.info.yml b/core/modules/link/tests/modules/link_test_views/link_test_views.info.yml new file mode 100644 index 000000000..235ecb693 --- /dev/null +++ b/core/modules/link/tests/modules/link_test_views/link_test_views.info.yml @@ -0,0 +1,10 @@ +name: 'Link test views' +type: module +description: 'Provides default views for views link tests.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - node + - views + - link diff --git a/core/modules/link/tests/modules/link_test_views/test_views/views.view.test_link_tokens.yml b/core/modules/link/tests/modules/link_test_views/test_views/views.view.test_link_tokens.yml new file mode 100644 index 000000000..2372b1251 --- /dev/null +++ b/core/modules/link/tests/modules/link_test_views/test_views/views.view.test_link_tokens.yml @@ -0,0 +1,206 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_link + module: + - link + - node + - user +id: test_link_tokens +label: link +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: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ previous' + next: 'next ›' + first: '« first' + last: 'last »' + quantity: 9 + style: + type: default + row: + type: fields + options: + default_field_elements: true + inline: { } + separator: '' + hide_empty: false + fields: + field_link: + id: field_link + table: node__field_link + field: field_link + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: true + text: "Formated: {{ field_link }}
\nRaw uri: {{ field_link__uri }}
\nRaw title: {{ field_link__title }}
\nRaw options: {{ field_link__options }}." + make_link: false + path: '{{ field_link__uri }}' + 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: uri + type: link + settings: + trim_length: 80 + url_only: false + url_plain: false + rel: '0' + target: '0' + group_column: '' + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + plugin_id: field + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: link + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + cacheable: false + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test_link_tokens + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + cacheable: false diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php index 0932cdb05..dfbea9eed 100644 --- a/core/modules/menu_ui/src/MenuForm.php +++ b/core/modules/menu_ui/src/MenuForm.php @@ -292,8 +292,6 @@ class MenuForm extends EntityForm { $form['links'][$id]['#attributes'] = $element['#attributes']; $form['links'][$id]['#attributes']['class'][] = 'draggable'; - $form['links'][$id]['#item'] = $element['#item']; - // TableDrag: Sort the table row according to its existing/configured weight. $form['links'][$id]['#weight'] = $element['#item']->link->getWeight(); diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php index 76b3bf6e7..d8cdf5879 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php @@ -34054,7 +34054,7 @@ $connection->insert('vocabulary') 'hierarchy' => '1', 'multiple' => '1', 'required' => '0', - 'tags' => '0', + 'tags' => '1', 'module' => 'taxonomy', 'weight' => '5', )) @@ -34079,7 +34079,7 @@ $connection->insert('vocabulary') 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', - 'required' => '0', + 'required' => '1', 'tags' => '0', 'module' => 'taxonomy', 'weight' => '0', diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal7.php b/core/modules/migrate_drupal/tests/fixtures/drupal7.php index 0e94e8f9b..15ca13048 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal7.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal7.php @@ -4330,6 +4330,33 @@ $connection->schema()->createTable('field_data_field_file', array( 'mysql_character_set' => 'utf8', )); +$connection->insert('field_data_field_file') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_file_fid', + 'field_file_display', + 'field_file_description', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_file_fid' => '2', + 'field_file_display' => '1', + 'field_file_description' => 'file desc', +)) +->execute(); + $connection->schema()->createTable('field_data_field_float', array( 'fields' => array( 'entity_type' => array( @@ -6065,6 +6092,33 @@ $connection->schema()->createTable('field_revision_field_file', array( 'mysql_character_set' => 'utf8', )); +$connection->insert('field_revision_field_file') +->fields(array( + 'entity_type', + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'language', + 'delta', + 'field_file_fid', + 'field_file_display', + 'field_file_description', +)) +->values(array( + 'entity_type' => 'node', + 'bundle' => 'test_content_type', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'language' => 'und', + 'delta' => '0', + 'field_file_fid' => '2', + 'field_file_display' => '1', + 'field_file_description' => 'file desc', +)) +->execute(); + $connection->schema()->createTable('field_revision_field_float', array( 'fields' => array( 'entity_type' => array( @@ -7337,6 +7391,13 @@ $connection->insert('file_usage') 'module' => 'file', 'type' => 'node', 'id' => '1', + 'count' => '2', +)) +->values(array( + 'fid' => '2', + 'module' => 'file', + 'type' => 'node', + 'id' => '1', 'count' => '1', )) ->execute(); diff --git a/core/modules/node/node.module b/core/modules/node/node.module index a54fc0a2c..91d27e883 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -883,7 +883,9 @@ function node_form_system_themes_admin_form_submit($form, FormStateInterface $fo * with node access to ensure only nodes to which the user has access are * retrieved, through the use of hook_query_TAG_alter(). See the * @link entity_api Entity API topic @endlink for more information on entity - * queries. + * queries. Tagging a query with "node_access" does not check the + * published/unpublished status of nodes, so the base query is responsible + * for ensuring that unpublished nodes are not displayed to inappropriate users. * * Note: Even a single module returning an AccessResultInterface object from * hook_node_access() whose isForbidden() method equals TRUE will block access diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php index 6b6f2806a..eb87a4dd0 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ b/core/modules/node/src/NodeGrantDatabaseStorage.php @@ -65,6 +65,11 @@ class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface { * {@inheritdoc} */ public function access(NodeInterface $node, $operation, AccountInterface $account) { + // Grants only support these operations. + if (!in_array($operation, ['view', 'update', 'delete'])) { + return AccessResult::neutral(); + } + // If no module implements the hook or the node does not have an id there is // no point in querying the database for access grants. if (!$this->moduleHandler->getImplementations('node_grants') || !$node->id()) { diff --git a/core/modules/node/src/Tests/Migrate/d7/MigrateNodeTest.php b/core/modules/node/src/Tests/Migrate/d7/MigrateNodeTest.php index 9a9024bab..deed28cf6 100644 --- a/core/modules/node/src/Tests/Migrate/d7/MigrateNodeTest.php +++ b/core/modules/node/src/Tests/Migrate/d7/MigrateNodeTest.php @@ -39,6 +39,7 @@ class MigrateNodeTest extends MigrateDrupal7TestBase { $this->installEntitySchema('node'); $this->installEntitySchema('comment'); $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('file'); $this->installConfig(static::$modules); $this->installSchema('node', ['node_access']); $this->installSchema('system', ['sequences']); @@ -51,6 +52,7 @@ class MigrateNodeTest extends MigrateDrupal7TestBase { 'd7_field', 'd7_field_instance', 'd7_node__test_content_type', + 'd7_node__article', ]); } @@ -134,6 +136,17 @@ class MigrateNodeTest extends MigrateDrupal7TestBase { $this->assertIdentical('Some more text', $node->field_text_list[0]->value); $this->assertIdentical('7', $node->field_integer_list[0]->value); $this->assertIdentical('qwerty', $node->field_text->value); + $this->assertIdentical('2', $node->field_file->target_id); + $this->assertIdentical('file desc', $node->field_file->description); + $this->assertTrue($node->field_file->display); + $this->assertIdentical('1', $node->field_images->target_id); + $this->assertIdentical('alt text', $node->field_images->alt); + $this->assertIdentical('title text', $node->field_images->title); + $this->assertIdentical('93', $node->field_images->width); + $this->assertIdentical('93', $node->field_images->height); + + $node = Node::load(2); + $this->assertIdentical("...is that it's the absolute best show ever. Trust me, I would know.", $node->body->value); } } diff --git a/core/modules/node/src/Tests/NodeAccessGrantsTest.php b/core/modules/node/src/Tests/NodeAccessGrantsTest.php index 99dedaf0b..30faca406 100644 --- a/core/modules/node/src/Tests/NodeAccessGrantsTest.php +++ b/core/modules/node/src/Tests/NodeAccessGrantsTest.php @@ -26,4 +26,13 @@ class NodeAccessGrantsTest extends NodeAccessTest { */ public static $modules = array('node_access_test_empty'); + /** + * Test operations not supported by node grants. + */ + function testUnsupportedOperation() { + $web_user = $this->drupalCreateUser(['access content']); + $node = $this->drupalCreateNode(); + $this->assertNodeAccess(['random_operation' => FALSE], $node, $web_user); + } + } diff --git a/core/modules/rdf/src/Tests/NodeAttributesTest.php b/core/modules/rdf/src/Tests/NodeAttributesTest.php index 43f736a52..29c9882c7 100644 --- a/core/modules/rdf/src/Tests/NodeAttributesTest.php +++ b/core/modules/rdf/src/Tests/NodeAttributesTest.php @@ -80,17 +80,17 @@ class NodeAttributesTest extends NodeTestBase { 'lang' => 'en', ); $this->assertTrue($graph->hasProperty($node_uri, 'http://purl.org/dc/terms/title', $expected_value), 'Node title found in RDF output (dc:title).'); - // Node date. + // Node date (date format must be UTC). $expected_value = array( 'type' => 'literal', - 'value' => date('c', $node->getCreatedTime()), + 'value' => \Drupal::service('date.formatter')->format($node->getCreatedTime(), 'custom', 'c', 'UTC'), 'datatype' => 'http://www.w3.org/2001/XMLSchema#dateTime', ); $this->assertTrue($graph->hasProperty($node_uri, 'http://purl.org/dc/terms/date', $expected_value), 'Node date found in RDF output (dc:date).'); - // Node date. + // Node date (date format must be UTC). $expected_value = array( 'type' => 'literal', - 'value' => date('c', $node->getCreatedTime()), + 'value' => \Drupal::service('date.formatter')->format($node->getCreatedTime(), 'custom', 'c', 'UTC'), 'datatype' => 'http://www.w3.org/2001/XMLSchema#dateTime', ); $this->assertTrue($graph->hasProperty($node_uri, 'http://purl.org/dc/terms/created', $expected_value), 'Node date found in RDF output (dc:created).'); diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module index af87c5bfa..aa959a3ff 100644 --- a/core/modules/responsive_image/responsive_image.module +++ b/core/modules/responsive_image/responsive_image.module @@ -391,7 +391,7 @@ function responsive_image_build_source_attributes(ImageInterface $image, array $ // Use the image width as key so we can sort the array later on. // Images within a srcset should be sorted from small to large, since // the first matching source will be used. - $srcset[intval($dimensions['width'])] = file_create_url(_responsive_image_image_style_url($image_style_name, $image->getSource())) . ' ' . $dimensions['width'] . 'w'; + $srcset[intval($dimensions['width'])] = _responsive_image_image_style_url($image_style_name, $image->getSource()) . ' ' . $dimensions['width'] . 'w'; $sizes = array_merge(explode(',', $image_style_mapping['image_mapping']['sizes']), $sizes); } break; @@ -405,7 +405,7 @@ function responsive_image_build_source_attributes(ImageInterface $image, array $ // be sorted from small to large, since the first matching source will // be used. We multiply it by 100 so multipliers with up to two decimals // can be used. - $srcset[intval(Unicode::substr($multiplier, 0, -1) * 100)] = file_create_url(_responsive_image_image_style_url($image_style_mapping['image_mapping'], $image->getSource())) . ' ' . $multiplier; + $srcset[intval(Unicode::substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $image->getSource()) . ' ' . $multiplier; break; } } @@ -495,9 +495,9 @@ function _responsive_image_image_style_url($style_name, $path) { } $entity = ImageStyle::load($style_name); if ($entity instanceof Drupal\image\Entity\ImageStyle) { - return $entity->buildUrl($path); + return file_url_transform_relative($entity->buildUrl($path)); } - return file_create_url($path); + return file_url_transform_relative(file_create_url($path)); } /** diff --git a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php index d1290cb2a..e66cb8225 100644 --- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php +++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php @@ -230,7 +230,7 @@ class ResponsiveImageFormatter extends ImageFormatterBase implements ContainerFa foreach ($files as $delta => $file) { // Link the element to the original file. if (isset($link_file)) { - $url = Url::fromUri(file_create_url($file->getFileUri())); + $url = file_url_transform_relative(file_create_url($file->getFileUri())); } // Extract field item attributes for the theme function, and unset them // from the $item so that the field template does not re-render them. diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php index b42831b50..a6f293058 100644 --- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php +++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldDisplayTest.php @@ -244,7 +244,7 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { $display->setComponent($field_name, $display_options) ->save(); - $default_output = 'drupalGet('node/' . $nid); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); @@ -289,10 +289,10 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); $thumbnail_style = ImageStyle::load('thumbnail'); // Assert the output of the 'srcset' attribute (small multipliers first). - $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . $thumbnail_style->buildUrl($image_uri) . ' 1.5x'); + $this->assertRaw('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== 1x, ' . file_url_transform_relative($thumbnail_style->buildUrl($image_uri)) . ' 1.5x'); $this->assertRaw('/styles/medium/'); // Assert the output of the original image. - $this->assertRaw(file_create_url($image_uri) . ' 3x'); + $this->assertRaw(file_url_transform_relative(file_create_url($image_uri)) . ' 3x'); // Assert the output of the breakpoints. $this->assertRaw('media="(min-width: 0px)"'); $this->assertRaw('media="(min-width: 560px)"'); @@ -301,7 +301,7 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { $this->assertPattern('/media="\(min-width: 560px\)".+?sizes="\(min-width: 700px\) 700px, 100vw"/'); // Assert the output of the 'srcset' attribute (small images first). $medium_style = ImageStyle::load('medium'); - $this->assertRaw($medium_style->buildUrl($image_uri) . ' 220w, ' . $large_style->buildUrl($image_uri) . ' 360w'); + $this->assertRaw(file_url_transform_relative($medium_style->buildUrl($image_uri)) . ' 220w, ' . file_url_transform_relative($large_style->buildUrl($image_uri)) . ' 360w'); $this->assertRaw('media="(min-width: 851px)"'); } $this->assertRaw('/styles/large/'); @@ -321,7 +321,7 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { '#alt' => $alt, '#srcset' => array( array( - 'uri' => $large_style->buildUrl($image->getSource()), + 'uri' => file_url_transform_relative($large_style->buildUrl($image->getSource())), ), ), ); @@ -403,7 +403,7 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { $thumbnail_style = ImageStyle::load('thumbnail'); $node = $node_storage->load($nid); $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); - $this->assertPattern('/srcset="' . preg_quote($thumbnail_style->buildUrl($image_uri), '/') . ' 1x".+?media="\(min-width: 0px\)"/'); + $this->assertPattern('/srcset="' . preg_quote(file_url_transform_relative($thumbnail_style->buildUrl($image_uri)), '/') . ' 1x".+?media="\(min-width: 0px\)"/'); } /** @@ -449,7 +449,7 @@ class ResponsiveImageFieldDisplayTest extends ImageFieldTestBase { $medium_style = ImageStyle::load('medium'); $node = $node_storage->load($nid); $image_uri = File::load($node->{$field_name}->target_id)->getFileUri(); - $this->assertRaw('assertRaw('assertPattern('/assertPattern('/getLinkTemplate($link_relation)) { - $this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = '/' . $link_template; + $this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $link_template; } else { $this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $default_uri; diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 4b4d0278a..34b0bdaa5 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -29,7 +29,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException; * } * ) * - * @see \Drupal\rest\Plugin\Derivative\EntityDerivative + * @see \Drupal\rest\Plugin\Deriver\EntityDeriver */ class EntityResource extends ResourceBase { diff --git a/core/modules/rest/src/Plugin/views/row/DataFieldRow.php b/core/modules/rest/src/Plugin/views/row/DataFieldRow.php index d9ed30a07..ef81b680d 100644 --- a/core/modules/rest/src/Plugin/views/row/DataFieldRow.php +++ b/core/modules/rest/src/Plugin/views/row/DataFieldRow.php @@ -88,6 +88,10 @@ class DataFieldRow extends RowPluginBase { if ($fields = $this->view->display_handler->getOption('fields')) { foreach ($fields as $id => $field) { + // Don't show the field if it has been excluded. + if (!empty($field['exclude'])) { + continue; + } $form['field_options'][$id]['field'] = array( '#markup' => $id, ); @@ -138,6 +142,10 @@ class DataFieldRow extends RowPluginBase { $output = array(); foreach ($this->view->field as $id => $field) { + // Don't render anything if this field is excluded. + if (!empty($field->options['exclude'])) { + continue; + } // If the raw output option has been set, just get the raw value. if (!empty($this->rawOutputOptions[$id])) { $value = $field->getValue($row); diff --git a/core/modules/rest/src/Tests/ResourceTest.php b/core/modules/rest/src/Tests/ResourceTest.php index f699a4bd9..c20b75114 100644 --- a/core/modules/rest/src/Tests/ResourceTest.php +++ b/core/modules/rest/src/Tests/ResourceTest.php @@ -106,4 +106,19 @@ class ResourceTest extends RESTTestBase { $this->curlClose(); } + /** + * Tests that resource URI paths are formatted properly. + */ + public function testUriPaths() { + $this->enableService('entity:entity_test'); + /** @var \Drupal\rest\Plugin\Type\ResourcePluginManager $manager */ + $manager = \Drupal::service('plugin.manager.rest'); + + foreach ($manager->getDefinitions() as $resource => $definition) { + foreach ($definition['uri_paths'] as $key => $uri_path) { + $this->assertFalse(strpos($uri_path, '//'), 'The resource URI path does not have duplicate slashes.'); + } + } + } + } diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php index b42da4218..b590504dc 100644 --- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php +++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php @@ -470,6 +470,32 @@ class StyleSerializerTest extends PluginTestBase { $this->assertIdentical($values['created'], $view->result[$index]->views_test_data_created, 'Expected raw created value found.'); $this->assertIdentical($values['name'], $view->result[$index]->views_test_data_name, 'Expected raw name value found.'); } + + // Test result with an excluded field. + $view->setDisplay('rest_export_1'); + $view->displayHandlers->get('rest_export_1')->overrideOption('fields', [ + 'name' => [ + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + ], + 'created' => [ + 'id' => 'created', + 'exclude' => TRUE, + 'table' => 'views_test_data', + 'field' => 'created', + 'relationship' => 'none', + ], + ]); + $view->save(); + $this->executeView($view); + foreach ($this->drupalGetJSON('test/serialize/field') as $index => $values) { + $this->assertTrue(!isset($values['created']), 'Excluded value not found.'); + } + // Test that the excluded field is not shown in the row options. + $this->drupalGet('admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options'); + $this->assertNoText('created'); } /** diff --git a/core/modules/simpletest/src/BlockCreationTrait.php b/core/modules/simpletest/src/BlockCreationTrait.php new file mode 100644 index 000000000..446d842d1 --- /dev/null +++ b/core/modules/simpletest/src/BlockCreationTrait.php @@ -0,0 +1,72 @@ +drupalPlaceBlock('system_powered_by_block', array( + * 'label' => t('Hello, world!'), + * )); + * @endcode + * The following defaults are provided: + * - label: Random string. + * - ID: Random string. + * - region: 'sidebar_first'. + * - theme: The default theme. + * - visibility: Empty array. + * + * @return \Drupal\block\Entity\Block + * The block entity. + * + * @todo + * Add support for creating custom block instances. + */ + protected function placeBlock($plugin_id, array $settings = array()) { + $config = \Drupal::configFactory(); + $settings += array( + 'plugin' => $plugin_id, + 'region' => 'sidebar_first', + 'id' => strtolower($this->randomMachineName(8)), + 'theme' => $config->get('system.theme')->get('default'), + 'label' => $this->randomMachineName(8), + 'visibility' => array(), + 'weight' => 0, + ); + $values = []; + foreach (array('region', 'id', 'theme', 'plugin', 'weight', 'visibility') as $key) { + $values[$key] = $settings[$key]; + // Remove extra values that do not belong in the settings array. + unset($settings[$key]); + } + foreach ($values['visibility'] as $id => $visibility) { + $values['visibility'][$id]['id'] = $id; + } + $values['settings'] = $settings; + $block = Block::create($values); + $block->save(); + return $block; + } + +} diff --git a/core/modules/simpletest/src/ContentTypeCreationTrait.php b/core/modules/simpletest/src/ContentTypeCreationTrait.php new file mode 100644 index 000000000..8152ab6f0 --- /dev/null +++ b/core/modules/simpletest/src/ContentTypeCreationTrait.php @@ -0,0 +1,58 @@ + 'foo'. + * + * @return \Drupal\node\Entity\NodeType + * Created content type. + */ + protected function createContentType(array $values = array()) { + // Find a non-existent random type name. + if (!isset($values['type'])) { + do { + $id = strtolower($this->randomMachineName(8)); + } while (NodeType::load($id)); + } + else { + $id = $values['type']; + } + $values += array( + 'type' => $id, + 'name' => $id, + ); + $type = NodeType::create($values); + $status = $type->save(); + node_add_body_field($type); + \Drupal::service('router.builder')->rebuild(); + + if ($this instanceof \PHPUnit_Framework_TestCase) { + $this->assertSame($status, SAVED_NEW, (new FormattableMarkup('Created content type %type.', array('%type' => $type->id())))->__toString()); + } + else { + $this->assertEqual($status, SAVED_NEW, (new FormattableMarkup('Created content type %type.', array('%type' => $type->id())))->__toString()); + } + + return $type; + } + +} diff --git a/core/modules/simpletest/src/NodeCreationTrait.php b/core/modules/simpletest/src/NodeCreationTrait.php new file mode 100644 index 000000000..204af9ed8 --- /dev/null +++ b/core/modules/simpletest/src/NodeCreationTrait.php @@ -0,0 +1,88 @@ +randomMachineName(). + * @param $reset + * (optional) Whether to reset the entity cache. + * + * @return \Drupal\node\NodeInterface + * A node entity matching $title. + */ + function getNodeByTitle($title, $reset = FALSE) { + if ($reset) { + \Drupal::entityTypeManager()->getStorage('node')->resetCache(); + } + // Cast MarkupInterface objects to string. + $title = (string) $title; + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties(['title' => $title]); + // Load the first node returned from the database. + $returned_node = reset($nodes); + return $returned_node; + } + + /** + * Creates a node based on default settings. + * + * @param array $settings + * (optional) An associative array of settings for the node, as used in + * entity_create(). Override the defaults by specifying the key and value + * in the array, for example: + * @code + * $this->drupalCreateNode(array( + * 'title' => t('Hello, world!'), + * 'type' => 'article', + * )); + * @endcode + * The following defaults are provided: + * - body: Random string using the default filter format: + * @code + * $settings['body'][0] = array( + * 'value' => $this->randomMachineName(32), + * 'format' => filter_default_format(), + * ); + * @endcode + * - title: Random string. + * - type: 'page'. + * - uid: The currently logged in user, or anonymous. + * + * @return \Drupal\node\NodeInterface + * The created node entity. + */ + protected function createNode(array $settings = array()) { + // Populate defaults array. + $settings += array( + 'body' => array(array( + 'value' => $this->randomMachineName(32), + 'format' => filter_default_format(), + )), + 'title' => $this->randomMachineName(8), + 'type' => 'page', + 'uid' => \Drupal::currentUser()->id(), + ); + $node = Node::create($settings); + $node->save(); + + return $node; + } + +} diff --git a/core/modules/simpletest/src/Tests/SimpleTestTest.php b/core/modules/simpletest/src/Tests/SimpleTestTest.php index 41d114ea6..812c1b249 100644 --- a/core/modules/simpletest/src/Tests/SimpleTestTest.php +++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php @@ -333,7 +333,7 @@ EOD; $assertion['file'] = $this->asText($row->td[2]); $assertion['line'] = $this->asText($row->td[3]); $assertion['function'] = $this->asText($row->td[4]); - $ok_url = file_create_url('core/misc/icons/73b355/check.svg'); + $ok_url = file_url_transform_relative(file_create_url('core/misc/icons/73b355/check.svg')); $assertion['status'] = ($row->td[5]->img['src'] == $ok_url) ? 'Pass' : 'Fail'; $results['assertions'][] = $assertion; } diff --git a/core/modules/simpletest/src/Tests/TimeZoneTest.php b/core/modules/simpletest/src/Tests/TimeZoneTest.php new file mode 100644 index 000000000..00c93f6f6 --- /dev/null +++ b/core/modules/simpletest/src/Tests/TimeZoneTest.php @@ -0,0 +1,42 @@ +adminUser = $this->drupalCreateUser(['administer site configuration']); + } + + /** + * Tests that user accounts have the default time zone set. + */ + function testAccountTimeZones() { + $expected = 'Australia/Sydney'; + $this->assertEqual($this->rootUser->getTimeZone(), $expected, 'Root user has correct time zone.'); + $this->assertEqual($this->adminUser->getTimeZone(), $expected, 'Admin user has correct time zone.'); + } + +} diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 3ad7d56b9..667f705df 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -29,7 +29,6 @@ use Drupal\Core\Session\UserSession; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Url; -use Drupal\node\Entity\NodeType; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Zend\Diactoros\Uri; @@ -42,7 +41,16 @@ use Zend\Diactoros\Uri; abstract class WebTestBase extends TestBase { use AssertContentTrait; - + use BlockCreationTrait { + placeBlock as drupalPlaceBlock; + } + use ContentTypeCreationTrait { + createContentType as drupalCreateContentType; + } + use NodeCreationTrait { + getNodeByTitle as drupalGetNodeByTitle; + createNode as drupalCreateNode; + } use UserCreationTrait { createUser as drupalCreateUser; createRole as drupalCreateRole; @@ -230,108 +238,6 @@ abstract class WebTestBase extends TestBase { $this->classLoader = require DRUPAL_ROOT . '/autoload.php'; } - /** - * Get a node from the database based on its title. - * - * @param string|\Drupal\Component\Render\MarkupInterface $title - * A node title, usually generated by $this->randomMachineName(). - * @param $reset - * (optional) Whether to reset the entity cache. - * - * @return \Drupal\node\NodeInterface - * A node entity matching $title. - */ - function drupalGetNodeByTitle($title, $reset = FALSE) { - if ($reset) { - \Drupal::entityManager()->getStorage('node')->resetCache(); - } - // Cast MarkupInterface objects to string. - $title = (string) $title; - $nodes = entity_load_multiple_by_properties('node', array('title' => $title)); - // Load the first node returned from the database. - $returned_node = reset($nodes); - return $returned_node; - } - - /** - * Creates a node based on default settings. - * - * @param array $settings - * (optional) An associative array of settings for the node, as used in - * entity_create(). Override the defaults by specifying the key and value - * in the array, for example: - * @code - * $this->drupalCreateNode(array( - * 'title' => t('Hello, world!'), - * 'type' => 'article', - * )); - * @endcode - * The following defaults are provided: - * - body: Random string using the default filter format: - * @code - * $settings['body'][0] = array( - * 'value' => $this->randomMachineName(32), - * 'format' => filter_default_format(), - * ); - * @endcode - * - title: Random string. - * - type: 'page'. - * - uid: The currently logged in user, or anonymous. - * - * @return \Drupal\node\NodeInterface - * The created node entity. - */ - protected function drupalCreateNode(array $settings = array()) { - // Populate defaults array. - $settings += array( - 'body' => array(array( - 'value' => $this->randomMachineName(32), - 'format' => filter_default_format(), - )), - 'title' => $this->randomMachineName(8), - 'type' => 'page', - 'uid' => \Drupal::currentUser()->id(), - ); - $node = entity_create('node', $settings); - $node->save(); - - return $node; - } - - /** - * Creates a custom content type based on default settings. - * - * @param array $values - * An array of settings to change from the defaults. - * Example: 'type' => 'foo'. - * - * @return \Drupal\node\Entity\NodeType - * Created content type. - */ - protected function drupalCreateContentType(array $values = array()) { - // Find a non-existent random type name. - if (!isset($values['type'])) { - do { - $id = strtolower($this->randomMachineName(8)); - } while (NodeType::load($id)); - } - else { - $id = $values['type']; - } - $values += array( - 'type' => $id, - 'name' => $id, - ); - $type = entity_create('node_type', $values); - $status = $type->save(); - node_add_body_field($type); - \Drupal::service('router.builder')->rebuild(); - - $this->assertEqual($status, SAVED_NEW, SafeMarkup::format('Created content type %type.', array('%type' => $type->id()))); - - return $type; - } - /** * Builds the renderable view of an entity. * @@ -390,58 +296,6 @@ abstract class WebTestBase extends TestBase { return $build; } - /** - * Creates a block instance based on default settings. - * - * @param string $plugin_id - * The plugin ID of the block type for this block instance. - * @param array $settings - * (optional) An associative array of settings for the block entity. - * Override the defaults by specifying the key and value in the array, for - * example: - * @code - * $this->drupalPlaceBlock('system_powered_by_block', array( - * 'label' => t('Hello, world!'), - * )); - * @endcode - * The following defaults are provided: - * - label: Random string. - * - ID: Random string. - * - region: 'sidebar_first'. - * - theme: The default theme. - * - visibility: Empty array. - * - * @return \Drupal\block\Entity\Block - * The block entity. - * - * @todo - * Add support for creating custom block instances. - */ - protected function drupalPlaceBlock($plugin_id, array $settings = array()) { - $settings += array( - 'plugin' => $plugin_id, - 'region' => 'sidebar_first', - 'id' => strtolower($this->randomMachineName(8)), - 'theme' => $this->config('system.theme')->get('default'), - 'label' => $this->randomMachineName(8), - 'visibility' => array(), - 'weight' => 0, - ); - $values = []; - foreach (array('region', 'id', 'theme', 'plugin', 'weight', 'visibility') as $key) { - $values[$key] = $settings[$key]; - // Remove extra values that do not belong in the settings array. - unset($settings[$key]); - } - foreach ($values['visibility'] as $id => $visibility) { - $values['visibility'][$id]['id'] = $id; - } - $values['settings'] = $settings; - $block = entity_create('block', $values); - $block->save(); - return $block; - } - /** * Checks to see whether a block appears on the page. * @@ -674,6 +528,14 @@ abstract class WebTestBase extends TestBase { * being executed. */ protected function setUp() { + // Set an explicit time zone to not rely on the system one, which may vary + // from setup to setup. The Australia/Sydney time zone is chosen so all + // tests are run using an edge case scenario (UTC+10 and DST). This choice + // is made to prevent time zone related regressions and reduce the + // fragility of the testing system in general. This is also set in config in + // \Drupal\simpletest\WebTestBase::initConfig(). + date_default_timezone_set('Australia/Sydney'); + // Preserve original batch for later restoration. $this->setBatch(); @@ -983,6 +845,7 @@ abstract class WebTestBase extends TestBase { 'name' => 'admin', 'mail' => 'admin@example.com', 'pass_raw' => $this->randomMachineName(), + 'timezone' => date_default_timezone_get(), )); // The child site derives its session name from the database prefix when @@ -2005,8 +1868,7 @@ abstract class WebTestBase extends TestBase { } // @todo Ajax commands can target any jQuery selector, but these are // hard to fully emulate with XPath. For now, just handle 'head' - // and 'body', since these are used by - // \Drupal\Core\Ajax\AjaxResponse::ajaxRender(). + // and 'body', since these are used by the Ajax renderer. elseif (in_array($command['selector'], array('head', 'body'))) { $wrapperNode = $xpath->query('//' . $command['selector'])->item(0); } diff --git a/core/modules/system/src/Form/CronForm.php b/core/modules/system/src/Form/CronForm.php index 5fec62fe0..81e44c46c 100644 --- a/core/modules/system/src/Form/CronForm.php +++ b/core/modules/system/src/Form/CronForm.php @@ -133,7 +133,6 @@ class CronForm extends FormBase { drupal_set_message(t('Cron run failed.'), 'error'); } - return new RedirectResponse($this->url('system.cron_settings', array(), array('absolute' => TRUE))); } } diff --git a/core/modules/system/src/SystemConfigSubscriber.php b/core/modules/system/src/SystemConfigSubscriber.php index 76ae5731e..036b37be7 100644 --- a/core/modules/system/src/SystemConfigSubscriber.php +++ b/core/modules/system/src/SystemConfigSubscriber.php @@ -87,7 +87,7 @@ class SystemConfigSubscriber implements EventSubscriberInterface { */ public static function getSubscribedEvents() { $events[ConfigEvents::SAVE][] = array('onConfigSave', 0); - // The empty check has a high priority so that is can stop propagation if + // The empty check has a high priority so that it can stop propagation if // there is no configuration to import. $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateNotEmpty', 512); $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateSiteUUID', 256); diff --git a/core/modules/system/src/Tests/Ajax/FrameworkTest.php b/core/modules/system/src/Tests/Ajax/FrameworkTest.php index 6ab1c5e84..f6dfd6b0c 100644 --- a/core/modules/system/src/Tests/Ajax/FrameworkTest.php +++ b/core/modules/system/src/Tests/Ajax/FrameworkTest.php @@ -22,13 +22,13 @@ use Drupal\Core\Asset\AttachedAssets; */ class FrameworkTest extends AjaxTestBase { /** - * Ensures \Drupal\Core\Ajax\AjaxResponse::ajaxRender() returns JavaScript settings from the page request. + * Verifies the Ajax rendering of a command in the settings. */ public function testAJAXRender() { // Verify that settings command is generated if JavaScript settings exist. $commands = $this->drupalGetAjax('ajax-test/render'); $expected = new SettingsCommand(array('ajax' => 'test'), TRUE); - $this->assertCommand($commands, $expected->render(), '\Drupal\Core\Ajax\AjaxResponse::ajaxRender() loads JavaScript settings.'); + $this->assertCommand($commands, $expected->render(), 'JavaScript settings command is present.'); } /** @@ -59,8 +59,8 @@ class FrameworkTest extends AjaxTestBase { // Load any page with at least one CSS file, at least one JavaScript file // and at least one #ajax-powered element. The latter is an assumption of - // drupalPostAjaxForm(), the two former are assumptions of - // AjaxResponse::ajaxRender(). + // drupalPostAjaxForm(), the two former are assumptions of the Ajax + // renderer. // @todo refactor AJAX Framework + tests to make less assumptions. $this->drupalGet('ajax_forms_test_lazy_load_form'); diff --git a/core/modules/system/src/Tests/Common/AttachedAssetsTest.php b/core/modules/system/src/Tests/Common/AttachedAssetsTest.php index e3a516e32..3ab2d3152 100644 --- a/core/modules/system/src/Tests/Common/AttachedAssetsTest.php +++ b/core/modules/system/src/Tests/Common/AttachedAssetsTest.php @@ -96,8 +96,8 @@ class AttachedAssetsTest extends KernelTestBase { $rendered_css = $this->renderer->renderPlain($css_render_array); $rendered_js = $this->renderer->renderPlain($js_render_array); $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; - $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'Rendering an external CSS file.'); - $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'Rendering an external JavaScript file.'); + $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'Rendering an external CSS file.'); + $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'Rendering an external JavaScript file.'); } /** @@ -150,7 +150,7 @@ class AttachedAssetsTest extends KernelTestBase { $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $expected_1 = ''; - $expected_2 = ''; + $expected_2 = ''; $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.'); $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.'); } @@ -166,7 +166,7 @@ class AttachedAssetsTest extends KernelTestBase { $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $expected_1 = ''; - $expected_2 = ''; + $expected_2 = ''; $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.'); $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.'); } @@ -186,7 +186,7 @@ class AttachedAssetsTest extends KernelTestBase { $rendered_footer_js = \Drupal::service('asset.js.collection_renderer')->render($footer_js); $this->assertEqual(2, count($rendered_footer_js), 'There are 2 JavaScript assets in the footer.'); $this->assertEqual('drupal-settings-json', $rendered_footer_js[0]['#attributes']['data-drupal-selector'], 'The first of the two JavaScript assets in the footer has drupal settings.'); - $this->assertEqual('http://', substr($rendered_footer_js[1]['#attributes']['src'], 0, 7), 'The second of the two JavaScript assets in the footer has the sole aggregated JavaScript asset.'); + $this->assertEqual(0, strpos($rendered_footer_js[1]['#attributes']['src'], base_path()), 'The second of the two JavaScript assets in the footer has the sole aggregated JavaScript asset.'); } /** @@ -237,9 +237,9 @@ class AttachedAssetsTest extends KernelTestBase { $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; - $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'The JS asset in common_test/js-header appears in the header.'); - $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'The JS asset in common_test/js-header appears in the header.'); + $this->assertNotIdentical(strpos($rendered_js, '' . "\n"; + $expected_1 = ""; + $expected_2 = "\n" . '' . "\n"; $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered JavaScript within downlevel-hidden conditional comments.'); $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered JavaScript within downlevel-revealed conditional comments.'); @@ -476,8 +476,8 @@ class AttachedAssetsTest extends KernelTestBase { $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; - $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'CSS file with query string gets version query string correctly appended..'); - $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'JavaScript file with query string gets version query string correctly appended.'); + $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'CSS file with query string gets version query string correctly appended..'); + $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'JavaScript file with query string gets version query string correctly appended.'); } } diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php index a2e9be4c1..a5baf4687 100644 --- a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php @@ -10,6 +10,8 @@ namespace Drupal\system\Tests\Entity; use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Language\LanguageInterface; use Drupal\entity_test\Entity\EntityTestMulRev; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\language\Entity\ConfigurableLanguage; /** @@ -810,4 +812,97 @@ class EntityTranslationTest extends EntityLanguageTestBase { } } + /** + * Tests if entity translation statuses are correct after removing two + * translation. + */ + public function testDeleteEntityTranslation() { + $entity_type = 'entity_test_mul'; + $controller = $this->entityManager->getStorage($entity_type); + + // Create a translatable test field. + $field_storage = FieldStorageConfig::create([ + 'entity_type' => $entity_type, + 'field_name' => 'translatable_test_field', + 'type' => 'field_test', + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'label' => $this->randomMachineName(), + 'bundle' => $entity_type, + ]); + $field->save(); + + // Create an untranslatable test field. + $field_storage = FieldStorageConfig::create([ + 'entity_type' => $entity_type, + 'field_name' => 'untranslatable_test_field', + 'type' => 'field_test', + 'translatable' => FALSE, + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'label' => $this->randomMachineName(), + 'bundle' => $entity_type, + ]); + $field->save(); + + // Create an entity with both translatable and untranslatable test fields. + $values = array( + 'name' => $this->randomString(), + 'translatable_test_field' => $this->randomString(), + 'untranslatable_test_field' => $this->randomString(), + ); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $controller->create($values); + + foreach ($this->langcodes as $langcode) { + $entity->addTranslation($langcode, $values); + } + $entity->save(); + + // Assert there are no deleted languages in the lists yet. + $this->assertNull(\Drupal::state()->get('entity_test.delete.translatable_test_field')); + $this->assertNull(\Drupal::state()->get('entity_test.delete.untranslatable_test_field')); + + // Remove the second and third langcodes from the entity. + $entity->removeTranslation('l1'); + $entity->removeTranslation('l2'); + $entity->save(); + + // Ensure that for the translatable test field the second and third + // langcodes are in the deleted languages list. + $actual = \Drupal::state()->get('entity_test.delete.translatable_test_field'); + $expected_translatable = ['l1', 'l2']; + sort($actual); + sort($expected_translatable); + $this->assertEqual($actual, $expected_translatable); + // Ensure that the untranslatable test field is untouched. + $this->assertNull(\Drupal::state()->get('entity_test.delete.untranslatable_test_field')); + + // Delete the entity, which removes all remaining translations. + $entity->delete(); + + // All languages have been deleted now. + $actual = \Drupal::state()->get('entity_test.delete.translatable_test_field'); + $expected_translatable[] = 'en'; + $expected_translatable[] = 'l0'; + sort($actual); + sort($expected_translatable); + $this->assertEqual($actual, $expected_translatable); + + // The untranslatable field is shared and only deleted once, for the + // default langcode. + $actual = \Drupal::state()->get('entity_test.delete.untranslatable_test_field'); + $expected_untranslatable = ['en']; + sort($actual); + sort($expected_untranslatable); + $this->assertEqual($actual, $expected_untranslatable); + } + } diff --git a/core/modules/system/src/Tests/Menu/LocalActionTest.php b/core/modules/system/src/Tests/Menu/LocalActionTest.php index 22f188dba..c876a2573 100644 --- a/core/modules/system/src/Tests/Menu/LocalActionTest.php +++ b/core/modules/system/src/Tests/Menu/LocalActionTest.php @@ -48,6 +48,22 @@ class LocalActionTest extends WebTestBase { [Url::fromRoute('menu_test.local_action3'), 'My YAML discovery action'], [Url::fromRoute('menu_test.local_action5'), 'Title override'], ]); + // Test a local action title that changes based on a config value. + $this->drupalGet(Url::fromRoute('menu_test.local_action6')); + $this->assertLocalAction([ + [Url::fromRoute('menu_test.local_action5'), 'Original title'], + ]); + // Verify the expected cache tag in the response headers. + $header_values = explode(' ', $this->drupalGetHeader('x-drupal-cache-tags')); + $this->assertTrue(in_array('config:menu_test.links.action', $header_values), "Found 'config:menu_test.links.action' cache tag in header"); + /** @var \Drupal\Core\Config\Config $config */ + $config = $this->container->get('config.factory')->getEditable('menu_test.links.action'); + $config->set('title', 'New title'); + $config->save(); + $this->drupalGet(Url::fromRoute('menu_test.local_action6')); + $this->assertLocalAction([ + [Url::fromRoute('menu_test.local_action5'), 'New title'], + ]); } /** diff --git a/core/modules/system/src/Tests/Render/AjaxPageStateTest.php b/core/modules/system/src/Tests/Render/AjaxPageStateTest.php index 11922f09d..0dce27519 100644 --- a/core/modules/system/src/Tests/Render/AjaxPageStateTest.php +++ b/core/modules/system/src/Tests/Render/AjaxPageStateTest.php @@ -48,7 +48,7 @@ class AjaxPageStateTest extends WebTestBase { ); $this->assertRaw( '/core/misc/drupalSettingsLoader.js', - 'The Dupalsettings library from core should be loaded.' + 'The drupalSettings library from core should be loaded.' ); } @@ -78,7 +78,7 @@ class AjaxPageStateTest extends WebTestBase { $this->assertRaw( '/core/misc/drupalSettingsLoader.js', - 'The Dupalsettings library from core should be loaded.' + 'The drupalSettings library from core should be loaded.' ); } @@ -107,7 +107,7 @@ class AjaxPageStateTest extends WebTestBase { $this->assertNoRaw( '/core/misc/drupalSettingsLoader.js', - 'The Dupalsettings library from core should be excluded from loading.' + 'The drupalSettings library from core should be excluded from loading.' ); } } diff --git a/core/modules/system/src/Tests/System/CronRunTest.php b/core/modules/system/src/Tests/System/CronRunTest.php index e39e91918..21deee3db 100644 --- a/core/modules/system/src/Tests/System/CronRunTest.php +++ b/core/modules/system/src/Tests/System/CronRunTest.php @@ -109,6 +109,10 @@ class CronRunTest extends WebTestBase { // fail randomly. Look for the word 'years', because without a timestamp, // the time will start at 1 January 1970. $this->assertNoText('years'); + + $this->drupalPostForm(NULL, [], t('Save configuration')); + $this->assertText(t('The configuration options have been saved.')); + $this->assertUrl('admin/config/system/cron'); } /** diff --git a/core/modules/system/src/Tests/System/HtaccessTest.php b/core/modules/system/src/Tests/System/HtaccessTest.php index 0165f196b..86563a4d6 100644 --- a/core/modules/system/src/Tests/System/HtaccessTest.php +++ b/core/modules/system/src/Tests/System/HtaccessTest.php @@ -86,6 +86,11 @@ class HtaccessTest extends WebTestBase { foreach ($file_exts_to_allow as $file_ext) { $file_paths["$path/access_test.$file_ext"] = 200; } + + // Ensure composer.json and composer.lock cannot be accessed. + $file_paths["$path/composer.json"] = 403; + $file_paths["$path/composer.lock"] = 403; + return $file_paths; } diff --git a/core/modules/system/src/Tests/System/ThemeTest.php b/core/modules/system/src/Tests/System/ThemeTest.php index 5a079ec18..c2f5a5750 100644 --- a/core/modules/system/src/Tests/System/ThemeTest.php +++ b/core/modules/system/src/Tests/System/ThemeTest.php @@ -65,27 +65,27 @@ class ThemeTest extends WebTestBase { // Raw stream wrapper URI. $file->uri => array( 'form' => file_uri_target($file->uri), - 'src' => file_create_url($file->uri), + 'src' => file_url_transform_relative(file_create_url($file->uri)), ), // Relative path within the public filesystem. file_uri_target($file->uri) => array( 'form' => file_uri_target($file->uri), - 'src' => file_create_url($file->uri), + 'src' => file_url_transform_relative(file_create_url($file->uri)), ), // Relative path to a public file. $file_relative => array( 'form' => $file_relative, - 'src' => file_create_url($file->uri), + 'src' => file_url_transform_relative(file_create_url($file->uri)), ), // Relative path to an arbitrary file. 'core/misc/druplicon.png' => array( 'form' => 'core/misc/druplicon.png', - 'src' => $GLOBALS['base_url'] . '/' . 'core/misc/druplicon.png', + 'src' => base_path() . 'core/misc/druplicon.png', ), // Relative path to a file in a theme. $default_theme_path . '/logo.svg' => array( 'form' => $default_theme_path . '/logo.svg', - 'src' => $GLOBALS['base_url'] . '/' . $default_theme_path . '/logo.svg', + 'src' => base_path() . $default_theme_path . '/logo.svg', ), ); foreach ($supported_paths as $input => $expected) { @@ -186,7 +186,7 @@ class ThemeTest extends WebTestBase { ':rel' => 'home', ) ); - $this->assertEqual($elements[0]['src'], file_create_url($uploaded_filename)); + $this->assertEqual($elements[0]['src'], file_url_transform_relative(file_create_url($uploaded_filename))); $this->container->get('theme_handler')->install(array('bartik')); diff --git a/core/modules/system/src/Tests/Theme/EngineTwigTest.php b/core/modules/system/src/Tests/Theme/EngineTwigTest.php index 283b41d4b..4e2e563af 100644 --- a/core/modules/system/src/Tests/Theme/EngineTwigTest.php +++ b/core/modules/system/src/Tests/Theme/EngineTwigTest.php @@ -125,7 +125,7 @@ class EngineTwigTest extends WebTestBase { */ public function testTwigFileUrls() { $this->drupalGet('/twig-theme-test/file-url'); - $filepath = file_create_url('core/modules/system/tests/modules/twig_theme_test/twig_theme_test.js'); + $filepath = file_url_transform_relative(file_create_url('core/modules/system/tests/modules/twig_theme_test/twig_theme_test.js')); $this->assertRaw('
file_url: ' . $filepath . '
'); } diff --git a/core/modules/system/src/Tests/Theme/ImageTest.php b/core/modules/system/src/Tests/Theme/ImageTest.php index 87b403c75..d0f93df20 100644 --- a/core/modules/system/src/Tests/Theme/ImageTest.php +++ b/core/modules/system/src/Tests/Theme/ImageTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Theme; use Drupal\simpletest\KernelTestBase; +use Symfony\Component\HttpFoundation\Request; /** * Tests built-in image theme functions. @@ -32,9 +33,17 @@ class ImageTest extends KernelTestBase { protected function setUp() { parent::setUp(); + + // The code under test uses file_url_transform_relative(), which relies on + // the Request containing the correct hostname. KernelTestBase doesn't set + // it, so push another request onto the stack to ensure it's correct. + $request = Request::create('/', 'GET', [], [], [], $_SERVER); + $this->container = $this->kernel->getContainer(); + $this->container->get('request_stack')->push($request); + $this->testImages = array( - '/core/misc/druplicon.png', - '/core/misc/loading.gif', + 'core/misc/druplicon.png', + 'core/misc/loading.gif', ); } @@ -75,7 +84,7 @@ class ImageTest extends KernelTestBase { $this->render($image); // Make sure the src attribute has the correct value. - $this->assertRaw(file_create_url($image['#uri']), 'Correct output for an image with the src attribute.'); + $this->assertRaw(file_url_transform_relative(file_create_url($image['#uri'])), 'Correct output for an image with the src attribute.'); } /** @@ -103,7 +112,7 @@ class ImageTest extends KernelTestBase { $this->render($image); // Make sure the srcset attribute has the correct value. - $this->assertRaw(file_create_url($this->testImages[0]) . ' 1x, ' . file_create_url($this->testImages[1]) . ' 2x', 'Correct output for image with srcset attribute and multipliers.'); + $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' 1x, ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' 2x', 'Correct output for image with srcset attribute and multipliers.'); } /** @@ -135,7 +144,7 @@ class ImageTest extends KernelTestBase { $this->render($image); // Make sure the srcset attribute has the correct value. - $this->assertRaw(file_create_url($this->testImages[0]) . ' ' . $widths[0] . ', ' . file_create_url($this->testImages[1]) . ' ' . $widths[1], 'Correct output for image with srcset attribute and width descriptors.'); + $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' ' . $widths[0] . ', ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' ' . $widths[1], 'Correct output for image with srcset attribute and width descriptors.'); } } diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestJavaScriptTest.php b/core/modules/system/src/Tests/Update/UpdatePathTestJavaScriptTest.php index f7ff4c336..d949dae73 100644 --- a/core/modules/system/src/Tests/Update/UpdatePathTestJavaScriptTest.php +++ b/core/modules/system/src/Tests/Update/UpdatePathTestJavaScriptTest.php @@ -43,7 +43,9 @@ class UpdatePathTestJavaScriptTest extends UpdatePathTestBase { if (!isset($script['src'])) { continue; } - $src = (string) $script['src']; + // Source is a root-relative URL. Transform it to an absolute URL to allow + // file_get_contents() to access the file. + $src = preg_replace('#^' . $GLOBALS['base_path'] . '(.*)#i', $GLOBALS['base_url'] . '/' . '${1}', (string) $script['src']); $file_content = file_get_contents($src); if (strpos($file_content, 'window.drupalSettings =') !== FALSE) { diff --git a/core/modules/system/tests/fixtures/HtaccessTest/composer.json b/core/modules/system/tests/fixtures/HtaccessTest/composer.json new file mode 100644 index 000000000..e69de29bb diff --git a/core/modules/system/tests/fixtures/HtaccessTest/composer.lock b/core/modules/system/tests/fixtures/HtaccessTest/composer.lock new file mode 100644 index 000000000..e69de29bb diff --git a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php index ff4859de5..13944471d 100644 --- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php @@ -50,9 +50,8 @@ class AjaxTestController { /** * Returns a render array that will be rendered by AjaxRenderer. * - * Ensures that \Drupal\Core\Ajax\AjaxResponse::ajaxRender() - * incorporates JavaScript settings generated during the page request by - * adding a dummy setting. + * Verifies that the response incorporates JavaScript settings generated + * during the page request by adding a dummy setting. */ public function render() { return [ diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php index 2a9137587..07564f518 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php @@ -115,4 +115,14 @@ class FieldTestItem extends FieldItemBase { return $this->getValue()['value'] == 'resave'; } + /** + * {@inheritdoc} + */ + public function delete() { + parent::delete(); + $deleted_languages = \Drupal::state()->get('entity_test.delete.' . $this->getFieldDefinition()->getName()) ?: []; + $deleted_languages[] = $this->getLangcode(); + \Drupal::state()->set('entity_test.delete.' . $this->getFieldDefinition()->getName(), $deleted_languages); + } + } diff --git a/core/modules/system/tests/modules/menu_test/config/install/menu_test.links.action.yml b/core/modules/system/tests/modules/menu_test/config/install/menu_test.links.action.yml new file mode 100644 index 000000000..e0a4853aa --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/config/install/menu_test.links.action.yml @@ -0,0 +1 @@ +title: 'Original title' diff --git a/core/modules/system/tests/modules/menu_test/config/schema/menu_test.schema.yml b/core/modules/system/tests/modules/menu_test/config/schema/menu_test.schema.yml index db6884d0b..18b85adf7 100644 --- a/core/modules/system/tests/modules/menu_test/config/schema/menu_test.schema.yml +++ b/core/modules/system/tests/modules/menu_test/config/schema/menu_test.schema.yml @@ -7,3 +7,10 @@ menu_test.menu_item: title: type: label label: 'Title' +menu_test.links.action: + type: config_object + label: 'Menu test local action' + mapping: + title: + type: label + label: 'Title' diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml index c7529900c..86768ee45 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml @@ -11,6 +11,12 @@ menu_test.local_action5: appears_on: - menu_test.local_action1 +menu_test.local_action.cache_check: + route_name: menu_test.local_action5 + class: '\Drupal\menu_test\Plugin\Menu\LocalAction\TestLocalActionWithConfig' + appears_on: + - menu_test.local_action6 + menu_test.local_action2: route_name: menu_test.local_action2 title: 'My hook_menu action' diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml index 751bdf7bc..284181152 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml @@ -118,6 +118,13 @@ menu_test.local_action5: requirements: _access: 'TRUE' +menu_test.local_action6: + path: '/menu-test-local-action/cache-check' + defaults: + _controller: '\Drupal\menu_test\TestControllers::test2' + requirements: + _access: 'TRUE' + menu_test.contextual_test: path: '/menu-test-contextual/default' defaults: 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 new file mode 100644 index 000000000..1be132977 --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/src/Plugin/Menu/LocalAction/TestLocalActionWithConfig.php @@ -0,0 +1,88 @@ +config->get('title'); + } + + /** + * Constructs a TestLocalActionWithConfig object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider to load routes by name. + * @param \Drupal\Core\Config\Config $config + * The 'menu_test.links.action' config. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, Config $config) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider); + + $this->config = $config; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('router.route_provider'), + $container->get('config.factory')->get('menu_test.links.action') + ); + } + +} 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 9d3510272..1d8c7b612 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 @@ -341,4 +341,16 @@ class SystemTestController extends ControllerBase { return $build; } + /** + * Returns the current date. + * + * @return \Symfony\Component\HttpFoundation\Response $response + * A Response object containing the current date. + */ + public function getCurrentDate() { + // Uses specific time to test that the right timezone is used. + $response = new Response(\Drupal::service('date.formatter')->format(1452702549)); + return $response; + } + } diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml index 1fb4069c1..a933048da 100644 --- a/core/modules/system/tests/modules/system_test/system_test.routing.yml +++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml @@ -150,3 +150,12 @@ system_test.respond_cacheable_response: _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithCacheableReponse' requirements: _access: 'TRUE' + +system_test.date: + path: '/system-test/date' + defaults: + _controller: '\Drupal\system_test\Controller\SystemTestController::getCurrentDate' + options: + no_cache: 'TRUE' + requirements: + _access: 'TRUE' diff --git a/core/modules/taxonomy/migration_templates/d6_vocabulary_entity_form_display.yml b/core/modules/taxonomy/migration_templates/d6_vocabulary_entity_form_display.yml index 1b9f5efb3..2608877e9 100644 --- a/core/modules/taxonomy/migration_templates/d6_vocabulary_entity_form_display.yml +++ b/core/modules/taxonomy/migration_templates/d6_vocabulary_entity_form_display.yml @@ -8,12 +8,17 @@ source: entity_type: node form_mode: default options: - type: options_select weight: 20 process: entity_type: 'constants/entity_type' form_mode: 'constants/form_mode' - options: 'constants/options' + options/type: + plugin: static_map + source: tags + map: + 0: options_select + 1: entity_reference_autocomplete_tags + options/weight: 'constants/options/weight' bundle: type field_name: plugin: migration diff --git a/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml b/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml index bda89d41c..a7bbbbbc4 100644 --- a/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml +++ b/core/modules/taxonomy/migration_templates/d6_vocabulary_field_instance.yml @@ -23,6 +23,7 @@ process: 'settings/handler': 'constants/selection_handler' 'settings/handler_settings/target_bundles/0': '@field_name' 'settings/handler_settings/auto_create': 'constants/auto_create' + required: required destination: plugin: entity:field_config migration_dependencies: diff --git a/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyEntityFormDisplayTest.php b/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyEntityFormDisplayTest.php index dc010dce7..7ecea1bec 100644 --- a/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyEntityFormDisplayTest.php +++ b/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyEntityFormDisplayTest.php @@ -41,6 +41,11 @@ class MigrateVocabularyEntityFormDisplayTest extends MigrateDrupal6TestBase { $this->assertIdentical(20, $component['weight']); // Test the Id map. $this->assertIdentical(array('node', 'article', 'default', 'tags'), Migration::load('d6_vocabulary_entity_form_display')->getIdMap()->lookupDestinationID(array(4, 'article'))); + + // Test the term widget tags setting. + $entity_form_display = EntityFormDisplay::load('node.story.default'); + $this->assertIdentical($entity_form_display->getComponent('vocabulary_1_i_0_')['type'], 'options_select'); + $this->assertIdentical($entity_form_display->getComponent('vocabulary_2_i_1_')['type'], 'entity_reference_autocomplete_tags'); } } diff --git a/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php b/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php index 967dc6824..bab6ae552 100644 --- a/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php +++ b/core/modules/taxonomy/src/Tests/Migrate/d6/MigrateVocabularyFieldInstanceTest.php @@ -40,12 +40,14 @@ class MigrateVocabularyFieldInstanceTest extends MigrateDrupal6TestBase { $field = FieldConfig::load($field_id); $this->assertIdentical($field_id, $field->id(), 'Field instance exists on article bundle.'); $this->assertIdentical('Tags', $field->label()); + $this->assertTrue($field->isRequired(), 'Field is required'); // Test the page bundle as well. $field_id = 'node.page.tags'; $field = FieldConfig::load($field_id); $this->assertIdentical($field_id, $field->id(), 'Field instance exists on page bundle.'); $this->assertIdentical('Tags', $field->label()); + $this->assertTrue($field->isRequired(), 'Field is required'); $settings = $field->getSettings(); $this->assertIdentical('default:taxonomy_term', $settings['handler'], 'The handler plugin ID is correct.'); @@ -53,6 +55,11 @@ class MigrateVocabularyFieldInstanceTest extends MigrateDrupal6TestBase { $this->assertIdentical(TRUE, $settings['handler_settings']['auto_create'], 'The "auto_create" setting is correct.'); $this->assertIdentical(array('node', 'article', 'tags'), Migration::load('d6_vocabulary_field_instance')->getIdMap()->lookupDestinationID(array(4, 'article'))); + + // Test the the field vocabulary_1_i_0_ + $field_id = 'node.story.vocabulary_1_i_0_'; + $field = FieldConfig::load($field_id); + $this->assertFalse($field->isRequired(), 'Field is not required'); } } diff --git a/core/modules/user/config/schema/user.views.schema.yml b/core/modules/user/config/schema/user.views.schema.yml index d74140f7e..ed0eb2877 100644 --- a/core/modules/user/config/schema/user.views.schema.yml +++ b/core/modules/user/config/schema/user.views.schema.yml @@ -56,10 +56,6 @@ views.argument_default.current_user: type: boolean label: 'User ID from logged in user' -views.argument_default.node: - type: boolean - label: 'Content ID from URL' - views_field_user: type: views_field mapping: diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php index b76897d8d..e7148844b 100644 --- a/core/modules/user/src/AccountSettingsForm.php +++ b/core/modules/user/src/AccountSettingsForm.php @@ -451,6 +451,8 @@ class AccountSettingsForm extends ConfigFormBase { ->set('register_no_approval_required.subject', $form_state->getValue('user_mail_register_no_approval_required_subject')) ->set('register_pending_approval.body', $form_state->getValue('user_mail_register_pending_approval_body')) ->set('register_pending_approval.subject', $form_state->getValue('user_mail_register_pending_approval_subject')) + ->set('register_pending_approval_admin.body', $form_state->getValue('register_pending_approval_admin_body')) + ->set('register_pending_approval_admin.subject', $form_state->getValue('register_pending_approval_admin_subject')) ->set('status_activated.body', $form_state->getValue('user_mail_status_activated_body')) ->set('status_activated.subject', $form_state->getValue('user_mail_status_activated_subject')) ->set('status_blocked.body', $form_state->getValue('user_mail_status_blocked_body')) diff --git a/core/modules/user/src/Plugin/views/filter/Permissions.php b/core/modules/user/src/Plugin/views/filter/Permissions.php index 6cbb70397..84f19bf48 100644 --- a/core/modules/user/src/Plugin/views/filter/Permissions.php +++ b/core/modules/user/src/Plugin/views/filter/Permissions.php @@ -94,11 +94,14 @@ class Permissions extends ManyToOne { public function query() { // @todo user_role_names() should maybe support multiple permissions. $rids = array(); - // Get all roles, that have the configured permissions. + // Get all role IDs that have the configured permissions. foreach ($this->value as $permission) { $roles = user_role_names(FALSE, $permission); - $rids += array_keys($roles); + // user_role_names() returns an array with the role IDs as keys, so take + // the array keys and merge them with previously found role IDs. + $rids = array_merge($rids, array_keys($roles)); } + // Remove any duplicate role IDs. $rids = array_unique($rids); $this->value = $rids; diff --git a/core/modules/user/src/Tests/UserAdminSettingsFormTest.php b/core/modules/user/src/Tests/UserAdminSettingsFormTest.php index 904cea463..57bfb8e79 100644 --- a/core/modules/user/src/Tests/UserAdminSettingsFormTest.php +++ b/core/modules/user/src/Tests/UserAdminSettingsFormTest.php @@ -37,6 +37,16 @@ class UserAdminSettingsFormTest extends SystemConfigFormTestBase { '#config_name' => 'user.mail', '#config_key' => 'cancel_confirm.subject', ), + 'register_pending_approval_admin_body' => array( + '#value' => $this->randomString(), + '#config_name' => 'user.mail', + '#config_key' => 'register_pending_approval_admin.body', + ), + 'register_pending_approval_admin_subject' => array( + '#value' => $this->randomString(20), + '#config_name' => 'user.mail', + '#config_key' => 'register_pending_approval_admin.subject', + ), ); } } diff --git a/core/modules/user/src/Tests/UserPictureTest.php b/core/modules/user/src/Tests/UserPictureTest.php index bd28934e6..9fa28a355 100644 --- a/core/modules/user/src/Tests/UserPictureTest.php +++ b/core/modules/user/src/Tests/UserPictureTest.php @@ -100,7 +100,7 @@ class UserPictureTest extends WebTestBase { $image_style_id = $this->config('core.entity_view_display.user.user.compact')->get('content.user_picture.settings.image_style'); $style = ImageStyle::load($image_style_id); - $image_url = $style->buildUrl($file->getfileUri()); + $image_url = file_url_transform_relative($style->buildUrl($file->getfileUri())); $alt_text = 'Profile picture for user ' . $this->webUser->getUsername(); // Verify that the image is displayed on the node page. diff --git a/core/modules/user/src/Tests/UserSaveTest.php b/core/modules/user/src/Tests/UserSaveTest.php index 0682c9708..2a4e7b1d3 100644 --- a/core/modules/user/src/Tests/UserSaveTest.php +++ b/core/modules/user/src/Tests/UserSaveTest.php @@ -49,4 +49,18 @@ class UserSaveTest extends WebTestBase { $user_by_name = user_load_by_name($test_name); $this->assertTrue($user_by_name, 'Loading user by name.'); } + + /** + * Ensures that an existing password is unset after the user was saved. + */ + function testExistingPasswordRemoval() { + /** @var \Drupal\user\Entity\User $user */ + $user = User::create(['name' => $this->randomMachineName()]); + $user->save(); + $user->setExistingPassword('existing password'); + $this->assertNotNull($user->pass->existing); + $user->save(); + $this->assertNull($user->pass->existing); + } + } diff --git a/core/modules/user/src/Tests/UserTimeZoneTest.php b/core/modules/user/src/Tests/UserTimeZoneTest.php index 300f57bd1..a06179594 100644 --- a/core/modules/user/src/Tests/UserTimeZoneTest.php +++ b/core/modules/user/src/Tests/UserTimeZoneTest.php @@ -21,7 +21,7 @@ class UserTimeZoneTest extends WebTestBase { * * @var array */ - public static $modules = array('node'); + public static $modules = array('node', 'system_test'); /** * Tests the display of dates and time when user-configurable time zones are set. @@ -73,5 +73,19 @@ class UserTimeZoneTest extends WebTestBase { $this->assertText('2007-03-11 05:00 CLT', 'Date should be Chile time; four hours ahead of PST'); $this->drupalGet('node/' . $node3->id()); $this->assertText('2007-03-21 00:00 CLT', 'Date should be Chile time; three hours ahead of PDT.'); + + // Ensure that anonymous users also use the default timezone. + $this->drupalLogout(); + $this->drupalGet('node/' . $node1->id()); + $this->assertText('2007-03-09 21:00 PST', 'Date should be PST.'); + $this->drupalGet('node/' . $node2->id()); + $this->assertText('2007-03-11 01:00 PST', 'Date should be PST.'); + $this->drupalGet('node/' . $node3->id()); + $this->assertText('2007-03-20 21:00 PDT', 'Date should be PDT.'); + + // Format a date without accessing the current user at all and + // ensure that it uses the default timezone. + $this->drupalGet('/system-test/date'); + $this->assertText('2016-01-13 08:29 PST', 'Date should be PST.'); } } diff --git a/core/modules/user/src/Tests/Views/HandlerFilterPermissionTest.php b/core/modules/user/src/Tests/Views/HandlerFilterPermissionTest.php index 5cdf6b90a..ac2644bdb 100644 --- a/core/modules/user/src/Tests/Views/HandlerFilterPermissionTest.php +++ b/core/modules/user/src/Tests/Views/HandlerFilterPermissionTest.php @@ -62,6 +62,31 @@ class HandlerFilterPermissionTest extends UserKernelTestBase { $this->assertIdenticalResultset($view, $expected, $column_map); $view->destroy(); + // Filter by not a permission. + $view->initHandlers(); + $view->filter['permission']->operator = 'not'; + $view->filter['permission']->value = array('administer users'); + $this->executeView($view); + $this->assertEqual(count($view->result), 3); + $expected = array(); + $expected[] = array('uid' => 1); + $expected[] = array('uid' => 2); + $expected[] = array('uid' => 3); + $this->assertIdenticalResultset($view, $expected, $column_map); + $view->destroy(); + + // Filter by not multiple permissions, that are present in multiple roles. + $view->initHandlers(); + $view->filter['permission']->operator = 'not'; + $view->filter['permission']->value = array('administer users', 'administer permissions'); + $this->executeView($view); + $this->assertEqual(count($view->result), 2); + $expected = array(); + $expected[] = array('uid' => 1); + $expected[] = array('uid' => 2); + $this->assertIdenticalResultset($view, $expected, $column_map); + $view->destroy(); + // Filter by another permission of a role with multiple permissions. $view->initHandlers(); $view->filter['permission']->value = array('administer users'); diff --git a/core/modules/views/src/Controller/ViewAjaxController.php b/core/modules/views/src/Controller/ViewAjaxController.php index bd2f3c18e..8dbdd1456 100644 --- a/core/modules/views/src/Controller/ViewAjaxController.php +++ b/core/modules/views/src/Controller/ViewAjaxController.php @@ -12,6 +12,8 @@ use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; +use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\RenderContext; @@ -161,7 +163,15 @@ class ViewAjaxController implements ContainerInjectionInterface { // Overwrite the destination. // @see the redirect.destination service. $origin_destination = $path; - $query = UrlHelper::buildQuery($request->query->all()); + + // Remove some special parameters you never want to have part of the + // destination query. + $used_query_parameters = $request->query->all(); + // @todo Remove this parsing once these are removed from the request in + // https://www.drupal.org/node/2504709. + unset($used_query_parameters[FormBuilderInterface::AJAX_FORM_REQUEST], $used_query_parameters[MainContentViewSubscriber::WRAPPER_FORMAT], $used_query_parameters['ajax_page_state']); + + $query = UrlHelper::buildQuery($used_query_parameters); if ($query != '') { $origin_destination .= '?' . $query; } diff --git a/core/modules/views/src/ManyToOneHelper.php b/core/modules/views/src/ManyToOneHelper.php index e6e31aa61..71a49e8bd 100644 --- a/core/modules/views/src/ManyToOneHelper.php +++ b/core/modules/views/src/ManyToOneHelper.php @@ -236,7 +236,7 @@ class ManyToOneHelper { $join = $this->getJoin(); $join->type = 'LEFT'; $join->extra = array(); - $join->extra_type = 'OR'; + $join->extraOperator = 'OR'; foreach ($this->handler->value as $value) { $join->extra[] = array( 'field' => $this->handler->realField, @@ -311,10 +311,21 @@ class ManyToOneHelper { $placeholder = $this->placeholder(); if (count($this->handler->value) > 1) { $placeholder .= '[]'; - $this->handler->query->addWhereExpression(0, "$field $operator($placeholder)", array($placeholder => $value)); + + if ($operator == 'IS NULL') { + $this->handler->query->addWhereExpression(0, "$field $operator"); + } + else { + $this->handler->query->addWhereExpression(0, "$field $operator($placeholder)", array($placeholder => $value)); + } } else { - $this->handler->query->addWhereExpression(0, "$field $operator $placeholder", array($placeholder => $value)); + if ($operator == 'IS NULL') { + $this->handler->query->addWhereExpression(0, "$field $operator"); + } + else { + $this->handler->query->addWhereExpression(0, "$field $operator $placeholder", array($placeholder => $value)); + } } } } diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index d54a0551f..1d18ee15a 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -2335,13 +2335,17 @@ abstract class DisplayPluginBase extends PluginBase implements DisplayPluginInte '#cache_properties' => ['#view_id', '#view_display_show_admin_links', '#view_display_plugin_id'], ]; + // When something passes $cache = FALSE, they're asking us not to create our + // own render cache for it. However, we still need to include certain pieces + // of cacheability metadata (e.g.: cache contexts), so they can bubble up. + // Thus, we add the cacheability metadata first, then modify / remove the + // cache keys depending on the $cache argument. + $this->applyDisplayCachablityMetadata($this->view->element); if ($cache) { $this->view->element['#cache'] += ['keys' => []]; // Places like \Drupal\views\ViewExecutable::setCurrentPage() set up an // additional cache context. $this->view->element['#cache']['keys'] = array_merge(['views', 'display', $this->view->element['#name'], $this->view->element['#display_id']], $this->view->element['#cache']['keys']); - - $this->applyDisplayCachablityMetadata($this->view->element); } else { // Remove the cache keys, to ensure render caching is not triggered. We diff --git a/core/modules/views/src/Plugin/views/display/EntityReference.php b/core/modules/views/src/Plugin/views/display/EntityReference.php index de2c0851c..38d8f290b 100644 --- a/core/modules/views/src/Plugin/views/display/EntityReference.php +++ b/core/modules/views/src/Plugin/views/display/EntityReference.php @@ -138,7 +138,8 @@ class EntityReference extends DisplayPluginBase { foreach ($style_options['options']['search_fields'] as $field_id) { if (!empty($field_id)) { // Get the table and field names for the checked field. - $field_alias = $this->view->query->addField($this->view->field[$field_id]->table, $field_id); + $field_handler = $this->view->field[$field_id]; + $field_alias = $this->view->query->addField($field_handler->table, $field_handler->realField); $field = $this->view->query->fields[$field_alias]; // Add an OR condition for the field. $conditions->condition($field['table'] . '.' . $field['field'], $value, 'LIKE'); diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php index c9ab37a7e..af3dad126 100644 --- a/core/modules/views/src/Plugin/views/field/Field.php +++ b/core/modules/views/src/Plugin/views/field/Field.php @@ -22,6 +22,7 @@ use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\TypedData\TypedDataInterface; use Drupal\views\FieldAPIHandlerTrait; use Drupal\views\Entity\Render\EntityFieldRenderer; use Drupal\views\Plugin\views\display\DisplayPluginBase; @@ -820,9 +821,10 @@ class Field extends FieldPluginBase implements CacheableDependencyInterface, Mul 'settings' => $this->options['settings'], 'label' => 'hidden', ]; - $build_list = $this->createEntityForGroupBy($this->getEntity($values), $values) - ->{$this->definition['field_name']} - ->view($display); + // Some bundles might not have a specific field, in which case the faked + // entity doesn't have it either. + $entity = $this->createEntityForGroupBy($this->getEntity($values), $values); + $build_list = isset($entity->{$this->definition['field_name']}) ? $entity->{$this->definition['field_name']}->view($display) : NULL; } if (!$build_list) { @@ -932,8 +934,10 @@ class Field extends FieldPluginBase implements CacheableDependencyInterface, Mul if (is_object($raw)) { $property = $raw->get($id); - if (!empty($property)) { - $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($property->getValue()); + // Check if TypedDataInterface is implemented so we know how to render + // the item as a string. + if (!empty($property) && $property instanceof TypedDataInterface) { + $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($property->getString()); } else { // Make sure that empty values are replaced as well. @@ -1002,8 +1006,17 @@ class Field extends FieldPluginBase implements CacheableDependencyInterface, Mul * {@inheritdoc} */ public function getValue(ResultRow $values, $field = NULL) { + $entity = $this->getEntity($values); + // Some bundles might not have a specific field, in which case the entity + // (potentially a fake one) doesn't have it either. /** @var \Drupal\Core\Field\FieldItemListInterface $field_item_list */ - $field_item_list = $this->getEntity($values)->{$this->definition['field_name']}; + $field_item_list = isset($entity->{$this->definition['field_name']}) ? $entity->{$this->definition['field_name']} : NULL; + + if (!isset($field_item_list)) { + // There isn't anything we can do without a valid field. + return NULL; + } + $field_item_definition = $field_item_list->getFieldDefinition(); if ($field_item_definition->getFieldStorageDefinition()->getCardinality() == 1) { diff --git a/core/modules/views/src/Plugin/views/style/StylePluginBase.php b/core/modules/views/src/Plugin/views/style/StylePluginBase.php index 719ee58e4..148e053c3 100644 --- a/core/modules/views/src/Plugin/views/style/StylePluginBase.php +++ b/core/modules/views/src/Plugin/views/style/StylePluginBase.php @@ -482,9 +482,17 @@ abstract class StylePluginBase extends PluginBase { * grouping. * * @param $sets - * Array containing the grouping sets to render. + * An array keyed by group content containing the grouping sets to render. + * Each set contains the following associative array: + * - group: The group content. + * - level: The hierarchical level of the grouping. + * - rows: The result rows to be rendered in this group.. * @param $level - * Integer indicating the hierarchical level of the grouping. + * (deprecated) This is no longer used and will be removed in Drupal 9. The + * 'level' key in $sets is used to indicate the hierarchical level of the + * grouping. + * + * @todo Remove the $level parameter in https://www.drupal.org/node/2633890. * * @return string * Rendered output of given grouping sets. @@ -493,16 +501,16 @@ abstract class StylePluginBase extends PluginBase { $output = array(); $theme_functions = $this->view->buildThemeFunctions($this->groupingTheme); foreach ($sets as $set) { + $level = isset($set['level']) ? $set['level'] : 0; + $row = reset($set['rows']); // Render as a grouping set. if (is_array($row) && isset($row['group'])) { - $output[] = array( + $single_output = array( '#theme' => $theme_functions, '#view' => $this->view, '#grouping' => $this->options['grouping'][$level], - '#grouping_level' => $level, '#rows' => $set['rows'], - '#title' => $set['group'], ); } // Render as a record set. @@ -515,10 +523,11 @@ abstract class StylePluginBase extends PluginBase { } $single_output = $this->renderRowGroup($set['rows']); - $single_output['#grouping_level'] = $level; - $single_output['#title'] = $set['group']; - $output[] = $single_output; } + + $single_output['#grouping_level'] = $level; + $single_output['#title'] = $set['group']; + $output[] = $single_output; } unset($this->view->row_index); return $output; @@ -546,9 +555,11 @@ abstract class StylePluginBase extends PluginBase { * array( * 'grouping_field_1:grouping_1' => array( * 'group' => 'grouping_field_1:content_1', + * 'level' => 0, * 'rows' => array( * 'grouping_field_2:grouping_a' => array( * 'group' => 'grouping_field_2:content_a', + * 'level' => 1, * 'rows' => array( * $row_index_1 => $row_1, * $row_index_2 => $row_2, @@ -580,7 +591,7 @@ abstract class StylePluginBase extends PluginBase { // hierarchically positioned set where the current row belongs to. // While iterating, parent groups, that do not exist yet, are added. $set = &$sets; - foreach ($groupings as $info) { + foreach ($groupings as $level => $info) { $field = $info['field']; $rendered = isset($info['rendered']) ? $info['rendered'] : $group_rendered; $rendered_strip = isset($info['rendered_strip']) ? $info['rendered_strip'] : FALSE; @@ -613,6 +624,7 @@ abstract class StylePluginBase extends PluginBase { // Create the group if it does not exist yet. if (empty($set[$grouping])) { $set[$grouping]['group'] = $group_content; + $set[$grouping]['level'] = $level; $set[$grouping]['rows'] = array(); } diff --git a/core/modules/views/src/Tests/Plugin/CacheWebTest.php b/core/modules/views/src/Tests/Plugin/CacheWebTest.php index 85194e257..de8f932b4 100644 --- a/core/modules/views/src/Tests/Plugin/CacheWebTest.php +++ b/core/modules/views/src/Tests/Plugin/CacheWebTest.php @@ -84,4 +84,15 @@ class CacheWebTest extends PluginTestBase { $this->assertCacheTags($cache_tags); } + /** + * Tests that a display without caching still contains the cache metadata. + */ + public function testDisplayWithoutCacheStillBubblesMetadata() { + $view = Views::getView('test_display'); + + $uncached_block = $view->buildRenderable('block_1', [], FALSE); + $cached_block = $view->buildRenderable('block_1', [], TRUE); + $this->assertEqual($uncached_block['#cache']['contexts'], $cached_block['#cache']['contexts'], 'Cache contexts are the same when you render the view cached and uncached.'); + } + } diff --git a/core/modules/views/src/Tests/Plugin/DisplayEntityReferenceTest.php b/core/modules/views/src/Tests/Plugin/DisplayEntityReferenceTest.php new file mode 100644 index 000000000..29f09f4ee --- /dev/null +++ b/core/modules/views/src/Tests/Plugin/DisplayEntityReferenceTest.php @@ -0,0 +1,134 @@ +drupalLogin($this->drupalCreateUser(array('administer views'))); + + // Create the text field. + $this->fieldName = 'field_test_entity_ref_display'; + $this->fieldStorage = FieldStorageConfig::create([ + 'field_name' => $this->fieldName, + 'entity_type' => 'entity_test', + 'type' => 'text', + ]); + $this->fieldStorage->save(); + + // Create an instance of the text field on the content type. + $this->field = FieldConfig::create([ + 'field_storage' => $this->fieldStorage, + 'bundle' => 'entity_test', + ]); + $this->field->save(); + + // Create some entities to search. Add a common string to the name and + // the text field in two entities so we can test that we can search in both. + for ($i = 0; $i < 5; $i++) { + EntityTest::create([ + 'bundle' => 'entity_test', + 'name' => 'name' . $i, + $this->fieldName => 'text', + ])->save(); + EntityTest::create([ + 'bundle' => 'entity_test', + 'name' => 'name', + $this->fieldName => 'text' . $i, + ])->save(); + } + } + + /** + * Tests the entity reference display plugin. + */ + public function testEntityReferenceDisplay() { + // Add the new field to the fields. + $this->drupalPostForm('admin/structure/views/nojs/add-handler/test_display_entity_reference/default/field', ['name[entity_test__' . $this->fieldName . '.' . $this->fieldName . ']' => TRUE], t('Add and configure fields')); + $this->drupalPostForm(NULL, [], t('Apply')); + + // Test that the right fields are shown on the display settings form. + $this->drupalGet('admin/structure/views/nojs/display/test_display_entity_reference/entity_reference_1/style_options'); + $this->assertText('Test entity: Name'); + $this->assertText('Test entity: ' . $this->field->label()); + + // Add the new field to the search fields. + $this->drupalPostForm(NULL, ['style_options[search_fields][' . $this->fieldName . ']' => $this->fieldName], t('Apply')); + $this->drupalPostForm(NULL, [], t('Save')); + + $view = Views::getView('test_display_entity_reference'); + $view->setDisplay('entity_reference_1'); + + // Add the required settings to test a search operation. + $options = [ + 'match' => '1', + 'match_operator' => 'CONTAINS', + 'limit' => 0, + 'ids' => NULL, + ]; + $view->display_handler->setOption('entity_reference_options', $options); + + $this->executeView($view); + + // Test that we have searched in both fields. + $this->assertEqual(count($view->result), 2, 'Search returned two rows'); + } + +} diff --git a/core/modules/views/src/Tests/Plugin/StyleTest.php b/core/modules/views/src/Tests/Plugin/StyleTest.php index 68ce82eb1..c44eda6fa 100644 --- a/core/modules/views/src/Tests/Plugin/StyleTest.php +++ b/core/modules/views/src/Tests/Plugin/StyleTest.php @@ -149,8 +149,10 @@ class StyleTest extends ViewTestBase { $expected = array(); $expected['Job: Singer'] = array(); $expected['Job: Singer']['group'] = 'Job: Singer'; + $expected['Job: Singer']['level'] = 0; $expected['Job: Singer']['rows']['Age: 25'] = array(); $expected['Job: Singer']['rows']['Age: 25']['group'] = 'Age: 25'; + $expected['Job: Singer']['rows']['Age: 25']['level'] = 1; $expected['Job: Singer']['rows']['Age: 25']['rows'][0] = new ResultRow(['index' => 0]); $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_name = 'John'; $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_job = 'Singer'; @@ -158,6 +160,7 @@ class StyleTest extends ViewTestBase { $expected['Job: Singer']['rows']['Age: 25']['rows'][0]->views_test_data_id = '1'; $expected['Job: Singer']['rows']['Age: 27'] = array(); $expected['Job: Singer']['rows']['Age: 27']['group'] = 'Age: 27'; + $expected['Job: Singer']['rows']['Age: 27']['level'] = 1; $expected['Job: Singer']['rows']['Age: 27']['rows'][1] = new ResultRow(['index' => 1]); $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_name = 'George'; $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_job = 'Singer'; @@ -165,8 +168,10 @@ class StyleTest extends ViewTestBase { $expected['Job: Singer']['rows']['Age: 27']['rows'][1]->views_test_data_id = '2'; $expected['Job: Drummer'] = array(); $expected['Job: Drummer']['group'] = 'Job: Drummer'; + $expected['Job: Drummer']['level'] = 0; $expected['Job: Drummer']['rows']['Age: 28'] = array(); $expected['Job: Drummer']['rows']['Age: 28']['group'] = 'Age: 28'; + $expected['Job: Drummer']['rows']['Age: 28']['level'] = 1; $expected['Job: Drummer']['rows']['Age: 28']['rows'][2] = new ResultRow(['index' => 2]); $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_data_name = 'Ringo'; $expected['Job: Drummer']['rows']['Age: 28']['rows'][2]->views_test_data_job = 'Drummer'; diff --git a/core/modules/views/src/Tests/QueryGroupByTest.php b/core/modules/views/src/Tests/QueryGroupByTest.php index 2a7ae227b..0cb38a1d3 100644 --- a/core/modules/views/src/Tests/QueryGroupByTest.php +++ b/core/modules/views/src/Tests/QueryGroupByTest.php @@ -25,7 +25,7 @@ class QueryGroupByTest extends ViewKernelTestBase { * * @var array */ - public static $testViews = array('test_group_by_in_filters', 'test_aggregate_count', 'test_group_by_count', 'test_group_by_count_multicardinality'); + public static $testViews = array('test_group_by_in_filters', 'test_aggregate_count', 'test_group_by_count', 'test_group_by_count_multicardinality', 'test_group_by_field_not_within_bundle'); /** * Modules to enable. @@ -292,4 +292,48 @@ class QueryGroupByTest extends ViewKernelTestBase { $this->assertEqual('6', $view->getStyle()->getField(5, 'field_test')); } + /** + * Tests groupby with a field not existing on some bundle. + */ + public function testGroupByWithFieldsNotExistingOnBundle() { + $field_storage = FieldStorageConfig::create([ + 'type' => 'integer', + 'field_name' => 'field_test', + 'cardinality' => 4, + 'entity_type' => 'entity_test_mul', + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_name' => 'field_test', + 'entity_type' => 'entity_test_mul', + 'bundle' => 'entity_test_mul', + ]); + $field->save(); + + $entities = []; + $entity = EntityTestMul::create([ + 'field_test' => [1], + 'type' => 'entity_test_mul', + ]); + $entity->save(); + $entities[] = $entity; + + $entity = EntityTestMul::create([ + 'type' => 'entity_test_mul2', + ]); + $entity->save(); + $entities[] = $entity; + + $view = Views::getView('test_group_by_field_not_within_bundle'); + $this->executeView($view); + + $this->assertEqual(2, count($view->result)); + // The first result is coming from entity_test_mul2, so no field could be + // rendered. + $this->assertEqual('', $view->getStyle()->getField(0, 'field_test')); + // The second result is coming from entity_test_mul, so its field value + // could be rendered. + $this->assertEqual('1', $view->getStyle()->getField(1, 'field_test')); + } + } diff --git a/core/modules/views/src/Tests/RenderCacheWebTest.php b/core/modules/views/src/Tests/RenderCacheWebTest.php new file mode 100644 index 000000000..b63a2fd5f --- /dev/null +++ b/core/modules/views/src/Tests/RenderCacheWebTest.php @@ -0,0 +1,80 @@ +drupalCreateContentType(['type' => 'test_type']); + $node = Node::create([ + 'title' => 'test title 1', + 'type' => $node_type->id(), + ]); + $node->save(); + $this->nodes[] = $node; + + $node = Node::create([ + 'title' => 'test title 2', + 'type' => $node_type->id(), + ]); + $node->save(); + $this->nodes[] = $node; + + $this->placeBlock('views_block:node_id_argument-block_1', ['region' => 'header']); + } + + /** + * Tests rendering caching of a views block with arguments. + */ + public function testEmptyView() { + $this->drupalGet(''); + $this->assertEqual([], $this->cssSelect('div.region-header div.views-field-title')); + + $this->drupalGet($this->nodes[0]->toUrl()); + $result = (string) $this->cssSelect('div.region-header div.views-field-title')[0]->span; + $this->assertEqual('test title 1', $result); + + $this->drupalGet($this->nodes[1]->toUrl()); + $result = (string) $this->cssSelect('div.region-header div.views-field-title')[0]->span; + $this->assertEqual('test title 2', $result); + + $this->drupalGet($this->nodes[0]->toUrl()); + $result = (string) $this->cssSelect('div.region-header div.views-field-title')[0]->span; + $this->assertEqual('test title 1', $result); + } + +} diff --git a/core/modules/views/src/Tests/TokenReplaceTest.php b/core/modules/views/src/Tests/TokenReplaceTest.php index 0462756b8..149280f08 100644 --- a/core/modules/views/src/Tests/TokenReplaceTest.php +++ b/core/modules/views/src/Tests/TokenReplaceTest.php @@ -77,4 +77,23 @@ class TokenReplaceTest extends ViewKernelTestBase { } } + /** + * Tests core token replacements generated from a view without results. + */ + function testTokenReplacementNoResults() { + $token_handler = \Drupal::token(); + $view = Views::getView('test_tokens'); + $view->setDisplay('page_2'); + $this->executeView($view); + + $expected = array( + '[view:page-count]' => '1', + ); + + foreach ($expected as $token => $expected_output) { + $output = $token_handler->replace($token, array('view' => $view)); + $this->assertIdentical($output, $expected_output, format_string('Token %token replaced correctly.', array('%token' => $token))); + } + } + } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.node_id_argument.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.node_id_argument.yml new file mode 100644 index 000000000..746b56bc1 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.node_id_argument.yml @@ -0,0 +1,215 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: node_id_argument +label: node_id_argument +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: 5 + offset: 0 + style: + type: default + row: + type: fields + fields: + title: + id: title + table: node_field_data + field: title + settings: + link_to_entity: false + plugin_id: field + 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: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: 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 + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: node_id_argument + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: node + default_argument_options: { } + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + not: false + entity_type: node + entity_field: nid + plugin_id: node_nid + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } + block_1: + display_plugin: block + id: block_1 + display_title: Block + position: 1 + display_options: + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_entity_reference.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_entity_reference.yml new file mode 100644 index 000000000..6989964e8 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_entity_reference.yml @@ -0,0 +1,50 @@ +langcode: en +status: true +dependencies: { } +id: test_display_entity_reference +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: tag + fields: + name: + id: name + table: entity_test + field: name + plugin_id: field + entity_type: entity_test + entity_field: name + style: + type: html_list + display_plugin: default + display_title: Master + id: default + position: 0 + entity_reference_1: + display_plugin: entity_reference + id: entity_reference_1 + display_title: 'Entity Reference' + position: 1 + display_options: + display_extenders: { } + style: + type: entity_reference + options: + search_fields: + name: name + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + tags: { } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_group_by_field_not_within_bundle.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_group_by_field_not_within_bundle.yml new file mode 100644 index 000000000..dac925b2f --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_group_by_field_not_within_bundle.yml @@ -0,0 +1,61 @@ +langcode: en +status: true +dependencies: { } +id: test_group_by_field_not_within_bundle +label: '' +module: views +description: '' +tag: '' +base_table: entity_test_mul_property_data +base_field: id +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: tag + exposed_form: + type: basic + fields: + field_test: + alter: + alter_text: false + ellipsis: true + html: false + make_link: false + strip_tags: false + trim: false + word_boundary: true + group_type: group + group_column: value + empty_zero: false + field: field_test + hide_empty: false + id: field_test + table: entity_test_mul__field_test + entity_type: entity_test_mul + entity_field: field_test + plugin_id: field + sorts: + field_test_value: + table: entity_test_mul__field_test + field: field_test_value + id: field_test_value + entity_type: entity_test_mul + entity_field: field_test + group_type: group + order: ASC + plugin_id: standard + group_by: true + pager: + type: some + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: 0 diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_tokens.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_tokens.yml index bf9341b9c..2bb82167f 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_tokens.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_tokens.yml @@ -57,3 +57,23 @@ display: type: views_query options: { } path: test_tokens + page_2: + id: page_2 + display_title: Page + display_plugin: page + position: 2 + display_options: + defaults: + filters: false + query: + type: views_query + options: { } + filters: + name: + field: name + id: test_filter + table: views_test_data + plugin_id: string + operator: '=' + value: 'not an existing name' + path: test_tokens_empty diff --git a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php index 9c4ce819e..9e1f96f78 100644 --- a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php +++ b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php @@ -185,6 +185,10 @@ class ViewAjaxControllerTest extends UnitTestCase { $request = new Request(); $request->request->set('view_name', 'test_view'); $request->request->set('view_display_id', 'page_1'); + $request->request->set('view_path', '/test-page'); + $request->request->set('_wrapper_format', 'ajax'); + $request->request->set('ajax_page_state', 'drupal.settings[]'); + $request->request->set('type', 'article'); list($view, $executable) = $this->setupValidMocks(); @@ -205,6 +209,10 @@ class ViewAjaxControllerTest extends UnitTestCase { $executable->displayHandlers = $display_collection; + $this->redirectDestination->expects($this->atLeastOnce()) + ->method('set') + ->with('/test-page?type=article'); + $response = $this->viewAjaxController->ajaxView($request); $this->assertTrue($response instanceof ViewAjaxResponse); diff --git a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php index feefe0aeb..14b7a07c7 100644 --- a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php +++ b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php @@ -138,8 +138,8 @@ class EntityViewsDataTest extends UnitTestCase { // Add a description field to the fields supplied by the EntityTest // classes. This example comes from the taxonomy Term entity. $base_fields['description'] = BaseFieldDefinition::create('text_long') - ->setLabel(t('Description')) - ->setDescription(t('A description of the term.')) + ->setLabel('Description') + ->setDescription('A description of the term.') ->setTranslatable(TRUE) ->setDisplayOptions('view', array( 'label' => 'hidden', @@ -155,8 +155,8 @@ class EntityViewsDataTest extends UnitTestCase { // Add a URL field; this example is from the Comment entity. $base_fields['homepage'] = BaseFieldDefinition::create('uri') - ->setLabel(t('Homepage')) - ->setDescription(t("The comment author's home page address.")) + ->setLabel('Homepage') + ->setDescription("The comment author's home page address.") ->setTranslatable(TRUE) ->setSetting('max_length', 255); @@ -409,8 +409,8 @@ class EntityViewsDataTest extends UnitTestCase { $base_field_definitions = $this->setupBaseFields(EntityTest::baseFieldDefinitions($this->baseEntityType)); $user_base_field_definitions = [ 'uid' => BaseFieldDefinition::create('integer') - ->setLabel(t('ID')) - ->setDescription(t('The ID of the user entity.')) + ->setLabel('ID') + ->setDescription('The ID of the user entity.') ->setReadOnly(TRUE) ->setSetting('unsigned', TRUE) ]; @@ -498,8 +498,8 @@ class EntityViewsDataTest extends UnitTestCase { $base_field_definitions = $this->setupBaseFields($base_field_definitions); $user_base_field_definitions = [ 'uid' => BaseFieldDefinition::create('integer') - ->setLabel(t('ID')) - ->setDescription(t('The ID of the user entity.')) + ->setLabel('ID') + ->setDescription('The ID of the user entity.') ->setReadOnly(TRUE) ->setSetting('unsigned', TRUE) ]; @@ -621,8 +621,8 @@ class EntityViewsDataTest extends UnitTestCase { $base_field_definitions = $this->setupBaseFields(EntityTestMulRev::baseFieldDefinitions($this->baseEntityType)); $user_base_field_definitions = [ 'uid' => BaseFieldDefinition::create('integer') - ->setLabel(t('ID')) - ->setDescription(t('The ID of the user entity.')) + ->setLabel('ID') + ->setDescription('The ID of the user entity.') ->setReadOnly(TRUE) ->setSetting('unsigned', TRUE) ]; @@ -970,10 +970,11 @@ class TestEntityType extends EntityType { } -namespace { +namespace Drupal\entity_test\Entity { if (!function_exists('t')) { function t($string, array $args = []) { return strtr($string, $args); } } } + diff --git a/core/modules/views/views.tokens.inc b/core/modules/views/views.tokens.inc index 6d9de956f..59faec39b 100644 --- a/core/modules/views/views.tokens.inc +++ b/core/modules/views/views.tokens.inc @@ -124,7 +124,7 @@ function views_tokens($type, $tokens, array $data, array $options, BubbleableMet case 'page-count': // If there are no items per page, set this to 1 for the division. $per_page = $view->getItemsPerPage() ?: 1; - $replacements[$original] = (int) ceil(count($view->result) / $per_page); + $replacements[$original] = max(1, (int) ceil(count($view->result) / $per_page)); break; } } diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml index 189737cac..79156b2e8 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml @@ -6,6 +6,7 @@ dependencies: - field.field.node.article.comment - field.field.node.article.field_image - field.field.node.article.field_tags + - image.style.thumbnail - node.type.article module: - comment diff --git a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml index 107d36362..466b6e0b3 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml @@ -3,6 +3,7 @@ status: true dependencies: config: - field.field.user.user.user_picture + - image.style.thumbnail module: - image - user diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml index e0d378238..f880cfd5f 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml @@ -6,6 +6,7 @@ dependencies: - field.field.node.article.comment - field.field.node.article.field_image - field.field.node.article.field_tags + - image.style.large - node.type.article module: - comment diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml index 1cf18dc76..43ee079e1 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml @@ -7,6 +7,7 @@ dependencies: - field.field.node.article.comment - field.field.node.article.field_image - field.field.node.article.field_tags + - image.style.medium - node.type.article module: - image diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml index 9c74439bb..4c1379244 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml @@ -4,6 +4,7 @@ dependencies: config: - core.entity_view_mode.user.compact - field.field.user.user.user_picture + - image.style.thumbnail module: - image - user diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml index 807fefe3d..9e4621d5e 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml @@ -3,6 +3,7 @@ status: true dependencies: config: - field.field.user.user.user_picture + - image.style.thumbnail module: - image - user diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 44657f198..b9c7a0170 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Timer; use Drupal\Component\Uuid\Php; use Drupal\Core\Database\Database; +use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Test\TestRunnerKernel; use Drupal\simpletest\Form\SimpletestResultsForm; @@ -1440,8 +1441,14 @@ function simpletest_script_open_browser() { // not have created one. $directory = PublicStream::basePath() . '/simpletest/verbose'; file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - $uuid = new Php(); - $filename = $directory . '/results-' . $uuid->generate() . '.html'; + $php = new Php(); + $uuid = $php->generate(); + $filename = $directory . '/results-' . $uuid . '.html'; + $base_url = getenv('SIMPLETEST_BASE_URL'); + if (empty($base_url)) { + simpletest_script_print_error("--browser needs argument --url."); + } + $url = $base_url . '/' . PublicStream::basePath() . '/simpletest/verbose/results-' . $uuid . '.html'; file_put_contents($filename, $html); // See if we can find an OS helper to open URLs in default browser. @@ -1457,7 +1464,7 @@ function simpletest_script_open_browser() { } if ($browser) { - shell_exec($browser . ' ' . escapeshellarg($filename)); + shell_exec($browser . ' ' . escapeshellarg($url)); } else { // Can't find assets valid browser. diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php index eab4fe4cd..48a92ee1e 100644 --- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php @@ -9,9 +9,9 @@ namespace { /** - * CssRenderer uses file_create_url(), which *is* available when using the - * Simpletest test runner, but not when using the PHPUnit test runner; hence - * this hack. + * CssCollectionRenderer uses file_create_url() & file_url_transform_relative(), + * which *are* available when using the Simpletest test runner, but not when + * using the PHPUnit test runner; hence this hack. */ if (!function_exists('file_create_url')) { @@ -23,6 +23,17 @@ if (!function_exists('file_create_url')) { return 'file_create_url:' . $uri; } +} +if (!function_exists('file_url_transform_relative')) { + + /** + * Temporary mock of file_url_transform_relative, until that is moved into + * Component/Utility. + */ + function file_url_transform_relative($uri) { + return 'file_url_transform_relative:' . $uri; + } + } } @@ -153,7 +164,7 @@ class CssCollectionRendererUnitTest extends UnitTestCase { 0 => array('group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all', 'browsers' => array()), ), array( - 0 => $create_link_element(file_create_url('public://css/file-all') . '?0', 'all'), + 0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/file-all')) . '?0', 'all'), ), ), // 31 file CSS assets: expect 31 link elements. @@ -192,37 +203,37 @@ class CssCollectionRendererUnitTest extends UnitTestCase { 30 => $create_file_css_asset('public://css/31.css'), ), array( - 0 => $create_link_element(file_create_url('public://css/1.css') . '?0'), - 1 => $create_link_element(file_create_url('public://css/2.css') . '?0'), - 2 => $create_link_element(file_create_url('public://css/3.css') . '?0'), - 3 => $create_link_element(file_create_url('public://css/4.css') . '?0'), - 4 => $create_link_element(file_create_url('public://css/5.css') . '?0'), - 5 => $create_link_element(file_create_url('public://css/6.css') . '?0'), - 6 => $create_link_element(file_create_url('public://css/7.css') . '?0'), - 7 => $create_link_element(file_create_url('public://css/8.css') . '?0'), - 8 => $create_link_element(file_create_url('public://css/9.css') . '?0'), - 9 => $create_link_element(file_create_url('public://css/10.css') . '?0'), - 10 => $create_link_element(file_create_url('public://css/11.css') . '?0'), - 11 => $create_link_element(file_create_url('public://css/12.css') . '?0'), - 12 => $create_link_element(file_create_url('public://css/13.css') . '?0'), - 13 => $create_link_element(file_create_url('public://css/14.css') . '?0'), - 14 => $create_link_element(file_create_url('public://css/15.css') . '?0'), - 15 => $create_link_element(file_create_url('public://css/16.css') . '?0'), - 16 => $create_link_element(file_create_url('public://css/17.css') . '?0'), - 17 => $create_link_element(file_create_url('public://css/18.css') . '?0'), - 18 => $create_link_element(file_create_url('public://css/19.css') . '?0'), - 19 => $create_link_element(file_create_url('public://css/20.css') . '?0'), - 20 => $create_link_element(file_create_url('public://css/21.css') . '?0'), - 21 => $create_link_element(file_create_url('public://css/22.css') . '?0'), - 22 => $create_link_element(file_create_url('public://css/23.css') . '?0'), - 23 => $create_link_element(file_create_url('public://css/24.css') . '?0'), - 24 => $create_link_element(file_create_url('public://css/25.css') . '?0'), - 25 => $create_link_element(file_create_url('public://css/26.css') . '?0'), - 26 => $create_link_element(file_create_url('public://css/27.css') . '?0'), - 27 => $create_link_element(file_create_url('public://css/28.css') . '?0'), - 28 => $create_link_element(file_create_url('public://css/29.css') . '?0'), - 29 => $create_link_element(file_create_url('public://css/30.css') . '?0'), - 30 => $create_link_element(file_create_url('public://css/31.css') . '?0'), + 0 => $create_link_element(file_url_transform_relative(file_create_url('public://css/1.css')) . '?0'), + 1 => $create_link_element(file_url_transform_relative(file_create_url('public://css/2.css')) . '?0'), + 2 => $create_link_element(file_url_transform_relative(file_create_url('public://css/3.css')) . '?0'), + 3 => $create_link_element(file_url_transform_relative(file_create_url('public://css/4.css')) . '?0'), + 4 => $create_link_element(file_url_transform_relative(file_create_url('public://css/5.css')) . '?0'), + 5 => $create_link_element(file_url_transform_relative(file_create_url('public://css/6.css')) . '?0'), + 6 => $create_link_element(file_url_transform_relative(file_create_url('public://css/7.css')) . '?0'), + 7 => $create_link_element(file_url_transform_relative(file_create_url('public://css/8.css')) . '?0'), + 8 => $create_link_element(file_url_transform_relative(file_create_url('public://css/9.css')) . '?0'), + 9 => $create_link_element(file_url_transform_relative(file_create_url('public://css/10.css')) . '?0'), + 10 => $create_link_element(file_url_transform_relative(file_create_url('public://css/11.css')) . '?0'), + 11 => $create_link_element(file_url_transform_relative(file_create_url('public://css/12.css')) . '?0'), + 12 => $create_link_element(file_url_transform_relative(file_create_url('public://css/13.css')) . '?0'), + 13 => $create_link_element(file_url_transform_relative(file_create_url('public://css/14.css')) . '?0'), + 14 => $create_link_element(file_url_transform_relative(file_create_url('public://css/15.css')) . '?0'), + 15 => $create_link_element(file_url_transform_relative(file_create_url('public://css/16.css')) . '?0'), + 16 => $create_link_element(file_url_transform_relative(file_create_url('public://css/17.css')) . '?0'), + 17 => $create_link_element(file_url_transform_relative(file_create_url('public://css/18.css')) . '?0'), + 18 => $create_link_element(file_url_transform_relative(file_create_url('public://css/19.css')) . '?0'), + 19 => $create_link_element(file_url_transform_relative(file_create_url('public://css/20.css')) . '?0'), + 20 => $create_link_element(file_url_transform_relative(file_create_url('public://css/21.css')) . '?0'), + 21 => $create_link_element(file_url_transform_relative(file_create_url('public://css/22.css')) . '?0'), + 22 => $create_link_element(file_url_transform_relative(file_create_url('public://css/23.css')) . '?0'), + 23 => $create_link_element(file_url_transform_relative(file_create_url('public://css/24.css')) . '?0'), + 24 => $create_link_element(file_url_transform_relative(file_create_url('public://css/25.css')) . '?0'), + 25 => $create_link_element(file_url_transform_relative(file_create_url('public://css/26.css')) . '?0'), + 26 => $create_link_element(file_url_transform_relative(file_create_url('public://css/27.css')) . '?0'), + 27 => $create_link_element(file_url_transform_relative(file_create_url('public://css/28.css')) . '?0'), + 28 => $create_link_element(file_url_transform_relative(file_create_url('public://css/29.css')) . '?0'), + 29 => $create_link_element(file_url_transform_relative(file_create_url('public://css/30.css')) . '?0'), + 30 => $create_link_element(file_url_transform_relative(file_create_url('public://css/31.css')) . '?0'), ), ), // 32 file CSS assets with the same properties: expect 2 style elements. @@ -263,40 +274,40 @@ class CssCollectionRendererUnitTest extends UnitTestCase { ), array( 0 => $create_style_element(' -@import url("' . file_create_url('public://css/1.css') . '?0"); -@import url("' . file_create_url('public://css/2.css') . '?0"); -@import url("' . file_create_url('public://css/3.css') . '?0"); -@import url("' . file_create_url('public://css/4.css') . '?0"); -@import url("' . file_create_url('public://css/5.css') . '?0"); -@import url("' . file_create_url('public://css/6.css') . '?0"); -@import url("' . file_create_url('public://css/7.css') . '?0"); -@import url("' . file_create_url('public://css/8.css') . '?0"); -@import url("' . file_create_url('public://css/9.css') . '?0"); -@import url("' . file_create_url('public://css/10.css') . '?0"); -@import url("' . file_create_url('public://css/11.css') . '?0"); -@import url("' . file_create_url('public://css/12.css') . '?0"); -@import url("' . file_create_url('public://css/13.css') . '?0"); -@import url("' . file_create_url('public://css/14.css') . '?0"); -@import url("' . file_create_url('public://css/15.css') . '?0"); -@import url("' . file_create_url('public://css/16.css') . '?0"); -@import url("' . file_create_url('public://css/17.css') . '?0"); -@import url("' . file_create_url('public://css/18.css') . '?0"); -@import url("' . file_create_url('public://css/19.css') . '?0"); -@import url("' . file_create_url('public://css/20.css') . '?0"); -@import url("' . file_create_url('public://css/21.css') . '?0"); -@import url("' . file_create_url('public://css/22.css') . '?0"); -@import url("' . file_create_url('public://css/23.css') . '?0"); -@import url("' . file_create_url('public://css/24.css') . '?0"); -@import url("' . file_create_url('public://css/25.css') . '?0"); -@import url("' . file_create_url('public://css/26.css') . '?0"); -@import url("' . file_create_url('public://css/27.css') . '?0"); -@import url("' . file_create_url('public://css/28.css') . '?0"); -@import url("' . file_create_url('public://css/29.css') . '?0"); -@import url("' . file_create_url('public://css/30.css') . '?0"); -@import url("' . file_create_url('public://css/31.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/1.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/2.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/3.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/4.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/5.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/6.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/7.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/8.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/9.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/10.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/11.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/12.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/13.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/14.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/15.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/16.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/17.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/18.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/19.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/20.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/21.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/22.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/23.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/24.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/25.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/26.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/27.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/28.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/29.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/30.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/31.css')) . '?0"); ', 'all'), 1 => $create_style_element(' -@import url("' . file_create_url('public://css/32.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/32.css')) . '?0"); ', 'all'), ), ), @@ -340,46 +351,46 @@ class CssCollectionRendererUnitTest extends UnitTestCase { ), array( 0 => $create_style_element(' -@import url("' . file_create_url('public://css/1.css') . '?0"); -@import url("' . file_create_url('public://css/2.css') . '?0"); -@import url("' . file_create_url('public://css/3.css') . '?0"); -@import url("' . file_create_url('public://css/4.css') . '?0"); -@import url("' . file_create_url('public://css/5.css') . '?0"); -@import url("' . file_create_url('public://css/6.css') . '?0"); -@import url("' . file_create_url('public://css/7.css') . '?0"); -@import url("' . file_create_url('public://css/8.css') . '?0"); -@import url("' . file_create_url('public://css/9.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/1.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/2.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/3.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/4.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/5.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/6.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/7.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/8.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/9.css')) . '?0"); ', 'all'), 1 => $create_style_element(' -@import url("' . file_create_url('public://css/10.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/10.css')) . '?0"); ', 'screen'), 2 => $create_style_element(' -@import url("' . file_create_url('public://css/11.css') . '?0"); -@import url("' . file_create_url('public://css/12.css') . '?0"); -@import url("' . file_create_url('public://css/13.css') . '?0"); -@import url("' . file_create_url('public://css/14.css') . '?0"); -@import url("' . file_create_url('public://css/15.css') . '?0"); -@import url("' . file_create_url('public://css/16.css') . '?0"); -@import url("' . file_create_url('public://css/17.css') . '?0"); -@import url("' . file_create_url('public://css/18.css') . '?0"); -@import url("' . file_create_url('public://css/19.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/11.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/12.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/13.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/14.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/15.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/16.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/17.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/18.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/19.css')) . '?0"); ', 'all'), 3 => $create_style_element(' -@import url("' . file_create_url('public://css/20.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/20.css')) . '?0"); ', 'print'), 4 => $create_style_element(' -@import url("' . file_create_url('public://css/21.css') . '?0"); -@import url("' . file_create_url('public://css/22.css') . '?0"); -@import url("' . file_create_url('public://css/23.css') . '?0"); -@import url("' . file_create_url('public://css/24.css') . '?0"); -@import url("' . file_create_url('public://css/25.css') . '?0"); -@import url("' . file_create_url('public://css/26.css') . '?0"); -@import url("' . file_create_url('public://css/27.css') . '?0"); -@import url("' . file_create_url('public://css/28.css') . '?0"); -@import url("' . file_create_url('public://css/29.css') . '?0"); -@import url("' . file_create_url('public://css/30.css') . '?0"); -@import url("' . file_create_url('public://css/31.css') . '?0"); -@import url("' . file_create_url('public://css/32.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/21.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/22.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/23.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/24.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/25.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/26.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/27.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/28.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/29.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/30.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/31.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/32.css')) . '?0"); ', 'all'), ), ), @@ -422,40 +433,40 @@ class CssCollectionRendererUnitTest extends UnitTestCase { ), array( 0 => $create_style_element(' -@import url("' . file_create_url('public://css/1.css') . '?0"); -@import url("' . file_create_url('public://css/2.css') . '?0"); -@import url("' . file_create_url('public://css/3.css') . '?0"); -@import url("' . file_create_url('public://css/4.css') . '?0"); -@import url("' . file_create_url('public://css/5.css') . '?0"); -@import url("' . file_create_url('public://css/6.css') . '?0"); -@import url("' . file_create_url('public://css/7.css') . '?0"); -@import url("' . file_create_url('public://css/8.css') . '?0"); -@import url("' . file_create_url('public://css/9.css') . '?0"); -@import url("' . file_create_url('public://css/10.css') . '?0"); -@import url("' . file_create_url('public://css/11.css') . '?0"); -@import url("' . file_create_url('public://css/12.css') . '?0"); -@import url("' . file_create_url('public://css/13.css') . '?0"); -@import url("' . file_create_url('public://css/14.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/1.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/2.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/3.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/4.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/5.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/6.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/7.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/8.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/9.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/10.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/11.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/12.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/13.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/14.css')) . '?0"); ', 'all'), - 1 => $create_link_element(file_create_url('public://css/15.css') . '?0'), + 1 => $create_link_element(file_url_transform_relative(file_create_url('public://css/15.css')) . '?0'), 2 => $create_style_element(' -@import url("' . file_create_url('public://css/16.css') . '?0"); -@import url("' . file_create_url('public://css/17.css') . '?0"); -@import url("' . file_create_url('public://css/18.css') . '?0"); -@import url("' . file_create_url('public://css/19.css') . '?0"); -@import url("' . file_create_url('public://css/20.css') . '?0"); -@import url("' . file_create_url('public://css/21.css') . '?0"); -@import url("' . file_create_url('public://css/22.css') . '?0"); -@import url("' . file_create_url('public://css/23.css') . '?0"); -@import url("' . file_create_url('public://css/24.css') . '?0"); -@import url("' . file_create_url('public://css/25.css') . '?0"); -@import url("' . file_create_url('public://css/26.css') . '?0"); -@import url("' . file_create_url('public://css/27.css') . '?0"); -@import url("' . file_create_url('public://css/28.css') . '?0"); -@import url("' . file_create_url('public://css/29.css') . '?0"); -@import url("' . file_create_url('public://css/30.css') . '?0"); -@import url("' . file_create_url('public://css/31.css') . '?0"); -@import url("' . file_create_url('public://css/32.css') . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/16.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/17.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/18.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/19.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/20.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/21.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/22.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/23.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/24.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/25.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/26.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/27.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/28.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/29.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/30.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/31.css')) . '?0"); +@import url("' . file_url_transform_relative(file_create_url('public://css/32.css')) . '?0"); ', 'all'), ), ), diff --git a/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php index 03bb980f3..5922bf1f6 100644 --- a/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php @@ -9,7 +9,8 @@ namespace { /** - * CssOptimizer uses file_create_url(), which *is* available when using the + * CssOptimizer uses file_create_url(), file_uri_scheme() and + * file_url_transform_relative(), which *are* available when using the * Simpletest test runner, but not when using the PHPUnit test runner; hence * this hack. */ @@ -31,6 +32,17 @@ if (!function_exists('file_uri_scheme')) { return FALSE; } +} +if (!function_exists('file_url_transform_relative')) { + + /** + * Temporary mock of file_url_transform_relative, until that is moved into + * Component/Utility. + */ + function file_url_transform_relative($uri) { + return 'file_url_transform_relative:' . $uri; + } + } } @@ -48,6 +60,11 @@ use Drupal\Tests\UnitTestCase; */ class CssOptimizerUnitTest extends UnitTestCase { + /** + * {@inheritdoc} + */ + protected $backupGlobals = FALSE; + /** * A CSS asset optimizer. * @@ -65,7 +82,8 @@ class CssOptimizerUnitTest extends UnitTestCase { * Provides data for the CSS asset optimizing test. */ function providerTestOptimize() { - $path = dirname(__FILE__) . '/css_test_files/'; + $path = 'core/tests/Drupal/Tests/Core/Asset/css_test_files/'; + $absolute_path = dirname(__FILE__) . '/css_test_files/'; return array( // File. Tests: // - Stripped comments and white-space. @@ -82,7 +100,7 @@ class CssOptimizerUnitTest extends UnitTestCase { 'browsers' => array('IE' => TRUE, '!IE' => TRUE), 'basename' => 'css_input_without_import.css', ), - file_get_contents($path . 'css_input_without_import.css.optimized.css'), + file_get_contents($absolute_path . 'css_input_without_import.css.optimized.css'), ), // File. Tests: // - Proper URLs in imported files. (https://www.drupal.org/node/265719) @@ -102,7 +120,7 @@ class CssOptimizerUnitTest extends UnitTestCase { 'browsers' => array('IE' => TRUE, '!IE' => TRUE), 'basename' => 'css_input_with_import.css', ), - str_replace('url(images/icon.png)', 'url(' . file_create_url($path . 'images/icon.png') . ')', file_get_contents($path . 'css_input_with_import.css.optimized.css')), + str_replace('url(images/icon.png)', 'url(' . file_url_transform_relative(file_create_url($path . 'images/icon.png')) . ')', file_get_contents($absolute_path . 'css_input_with_import.css.optimized.css')), ), // File. Tests: // - Retain comment hacks. @@ -117,7 +135,7 @@ class CssOptimizerUnitTest extends UnitTestCase { 'browsers' => array('IE' => TRUE, '!IE' => TRUE), 'basename' => 'comment_hacks.css', ), - file_get_contents($path . 'comment_hacks.css.optimized.css'), + file_get_contents($absolute_path . 'comment_hacks.css.optimized.css'), ), // File in subfolder. Tests: // - CSS import path is properly interpreted. @@ -134,7 +152,7 @@ class CssOptimizerUnitTest extends UnitTestCase { 'browsers' => array('IE' => TRUE, '!IE' => TRUE), 'basename' => 'css_input_with_import.css', ), - str_replace('url(../images/icon.png)', 'url(' . file_create_url($path . 'images/icon.png') . ')', file_get_contents($path . 'css_subfolder/css_input_with_import.css.optimized.css')), + str_replace('url(../images/icon.png)', 'url(' . file_url_transform_relative(file_create_url($path . 'images/icon.png')) . ')', file_get_contents($absolute_path . 'css_subfolder/css_input_with_import.css.optimized.css')), ), // File. Tests: // - Any @charaset declaration at the beginning of a file should be @@ -150,7 +168,7 @@ class CssOptimizerUnitTest extends UnitTestCase { 'browsers' => array('IE' => TRUE, '!IE' => TRUE), 'basename' => 'charset_sameline.css', ), - file_get_contents($path . 'charset.css.optimized.css'), + file_get_contents($absolute_path . 'charset.css.optimized.css'), ), array( array( @@ -163,7 +181,7 @@ class CssOptimizerUnitTest extends UnitTestCase { 'browsers' => array('IE' => TRUE, '!IE' => TRUE), 'basename' => 'charset_newline.css', ), - file_get_contents($path . 'charset.css.optimized.css'), + file_get_contents($absolute_path . 'charset.css.optimized.css'), ), array( array( @@ -226,7 +244,18 @@ class CssOptimizerUnitTest extends UnitTestCase { * @dataProvider providerTestOptimize */ function testOptimize($css_asset, $expected) { + global $base_path; + $original_base_path = $base_path; + $base_path = '/'; + + // \Drupal\Core\Asset\CssOptimizer::loadFile() relies on the current working + // directory being the one that is used when index.php is the entry point. + // Note: PHPUnit automatically restores the original working directory. + chdir(realpath(__DIR__ . '/../../../../../../')); + $this->assertEquals($expected, $this->optimizer->optimize($css_asset), 'Group of file CSS assets optimized correctly.'); + + $base_path = $original_base_path; } /** diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php index dcc4f55dd..c29799dbc 100644 --- a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php @@ -12,6 +12,7 @@ use Drupal\Component\Plugin\Factory\FactoryInterface; use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultForbidden; +use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Menu\LocalActionManager; @@ -197,6 +198,9 @@ class LocalActionManagerTest extends UnitTestCase { ), ), array( + '#cache' => array( + 'contexts' => array('route'), + ), 'plugin_id_1' => array( '#theme' => 'menu_local_action', '#link' => array( @@ -206,6 +210,14 @@ class LocalActionManagerTest extends UnitTestCase { ), '#access' => AccessResult::forbidden(), '#weight' => 0, + '#cache' => array( + 'contexts' => array(), + 'tags' => array(), + // For back-compatibility in 8.0.x the max-age is Cache::PERMANENT + // instead of 0 for any class that does not implement + // \Drupal\Core\Cache\CacheableDependencyInterface. + 'max-age' => Cache::PERMANENT, + ), ), ), ); @@ -231,6 +243,9 @@ class LocalActionManagerTest extends UnitTestCase { ), ), array( + '#cache' => array( + 'contexts' => array('route'), + ), 'plugin_id_1' => array( '#theme' => 'menu_local_action', '#link' => array( @@ -240,6 +255,11 @@ class LocalActionManagerTest extends UnitTestCase { ), '#access' => AccessResult::forbidden(), '#weight' => 0, + '#cache' => array( + 'contexts' => array(), + 'tags' => array(), + 'max-age' => Cache::PERMANENT, + ), ), ), ); @@ -266,6 +286,9 @@ class LocalActionManagerTest extends UnitTestCase { ), ), array( + '#cache' => array( + 'contexts' => array('route'), + ), 'plugin_id_1' => array( '#theme' => 'menu_local_action', '#link' => array( @@ -275,6 +298,11 @@ class LocalActionManagerTest extends UnitTestCase { ), '#access' => AccessResult::forbidden(), '#weight' => 1, + '#cache' => array( + 'contexts' => array(), + 'tags' => array(), + 'max-age' => Cache::PERMANENT, + ), ), 'plugin_id_2' => array( '#theme' => 'menu_local_action', @@ -285,6 +313,11 @@ class LocalActionManagerTest extends UnitTestCase { ), '#access' => AccessResult::forbidden(), '#weight' => 0, + '#cache' => array( + 'contexts' => array(), + 'tags' => array(), + 'max-age' => Cache::PERMANENT, + ), ), ), ); @@ -313,6 +346,9 @@ class LocalActionManagerTest extends UnitTestCase { ), ), array( + '#cache' => array( + 'contexts' => array('route'), + ), 'plugin_id_1' => array( '#theme' => 'menu_local_action', '#link' => array( @@ -322,6 +358,11 @@ class LocalActionManagerTest extends UnitTestCase { ), '#access' => AccessResult::forbidden(), '#weight' => 1, + '#cache' => array( + 'contexts' => array(), + 'tags' => array(), + 'max-age' => Cache::PERMANENT, + ), ), 'plugin_id_2' => array( '#theme' => 'menu_local_action', @@ -332,6 +373,11 @@ class LocalActionManagerTest extends UnitTestCase { ), '#access' => AccessResult::forbidden(), '#weight' => 0, + '#cache' => array( + 'contexts' => array(), + 'tags' => array(), + 'max-age' => Cache::PERMANENT, + ), ), ), ); diff --git a/core/tests/Drupal/Tests/Core/Render/Element/MachineNameTest.php b/core/tests/Drupal/Tests/Core/Render/Element/MachineNameTest.php index f8948c706..51b9f9fdc 100644 --- a/core/tests/Drupal/Tests/Core/Render/Element/MachineNameTest.php +++ b/core/tests/Drupal/Tests/Core/Render/Element/MachineNameTest.php @@ -110,7 +110,7 @@ class MachineNameTest extends UnitTestCase { } -namespace { +namespace Drupal\Core\Render\Element { if (!function_exists('t')) { function t($string, array $args = []) { return strtr($string, $args); diff --git a/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php b/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php index fc35ec578..d403db39e 100644 --- a/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php +++ b/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php @@ -69,7 +69,7 @@ class TwigSandboxTest extends UnitTestCase { * Tests that white listed classes can be extended. */ public function testExtendedClass() { - $this->twig->render('{{ attribute.addClass("kitten") }}', ['attribute' => new TestAttribute()]); + $this->assertEquals(' class="kitten"', $this->twig->render('{{ attribute.addClass("kitten") }}', ['attribute' => new TestAttribute()])); } /** diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index 635eec338..1b8f080b8 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -748,6 +748,16 @@ class UrlTest extends UnitTestCase { ]; } + /** + * Tests the fromUri() method with a base: URI starting with a number. + * + * @covers ::fromUri + */ + public function testFromUriNumber() { + $url = Url::fromUri('base:2015/10/06'); + $this->assertSame($url->toUriString(), 'base:/2015/10/06'); + } + /** * Tests the toUriString() method with route: URIs. * diff --git a/core/themes/stable/images/core/throbber-active.gif b/core/themes/stable/images/core/throbber-active.gif index f66414a1c..abf5c5d5b 100644 Binary files a/core/themes/stable/images/core/throbber-active.gif and b/core/themes/stable/images/core/throbber-active.gif differ diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 0d573e496..b42ffefb5 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -313,20 +313,25 @@ $settings['update_free_access'] = FALSE; /** * External access proxy settings: * - * If your site must access the Internet via a web proxy then you can enter - * the proxy settings here. Currently only basic authentication is supported - * by using the username and password variables. The proxy_user_agent variable - * can be set to NULL for proxies that require no User-Agent header or to a - * non-empty string for proxies that limit requests to a specific agent. The - * proxy_exceptions variable is an array of host names to be accessed directly, - * not via proxy. + * If your site must access the Internet via a web proxy then you can enter the + * proxy settings here. Set the full URL of the proxy, including the port, in + * variables: + * - $settings['http_client_config']['proxy']['http']: The proxy URL for HTTP + * requests. + * - $settings['http_client_config']['proxy']['https']: The proxy URL for HTTPS + * requests. + * You can pass in the user name and password for basic authentication in the + * URLs in these settings. + * + * You can also define an array of host names that can be accessed directly, + * bypassing the proxy, in $settings['http_client_config']['proxy']['no']. + * + * If these settings are not configured, the system environment variables + * HTTP_PROXY, HTTPS_PROXY, and NO_PROXY on the web server will be used instead. */ -# $settings['proxy_server'] = ''; -# $settings['proxy_port'] = 8080; -# $settings['proxy_username'] = ''; -# $settings['proxy_password'] = ''; -# $settings['proxy_user_agent'] = ''; -# $settings['proxy_exceptions'] = array('127.0.0.1', 'localhost'); +# $settings['http_client_config']['proxy']['http'] = 'http://proxy_user:proxy_pass@example.com:8080'; +# $settings['http_client_config']['proxy']['https'] = 'http://proxy_user:proxy_pass@example.com:8080'; +# $settings['http_client_config']['proxy']['no'] = ['127.0.0.1', 'localhost']; /** * Reverse Proxy Configuration: diff --git a/web.config b/web.config index 782d4ae50..a0535a10d 100644 --- a/web.config +++ b/web.config @@ -22,7 +22,7 @@ - +