From 25a6735fb346896989af8abd34f71d9656c079a1 Mon Sep 17 00:00:00 2001 From: Pantheon Automation Date: Thu, 19 Nov 2015 07:04:44 -0800 Subject: [PATCH] Update to Drupal 8.0.0. For more information, see https://www.drupal.org/node/2619030 --- core/CHANGELOG.txt | 212 +++++++++------ core/lib/Drupal.php | 2 +- .../lib/Drupal/Core/Config/ConfigImporter.php | 6 +- .../Core/Config/Entity/ConfigEntityType.php | 18 +- .../Drupal/Core/Entity/ContentEntityBase.php | 41 +-- .../Core/Entity/ContentEntityStorageBase.php | 52 +++- .../Entity/ContentEntityStorageInterface.php | 31 +++ .../Drupal/Core/Entity/ContentEntityType.php | 16 ++ core/lib/Drupal/Core/Entity/EntityType.php | 14 + .../KeyValueContentEntityStorage.php | 25 ++ core/lib/Drupal/Core/Entity/entity.api.php | 88 ++++++- .../Core/TypedData/TranslatableInterface.php | 8 + .../aggregator/src/FeedStorageInterface.php | 4 +- .../aggregator/src/ItemStorageInterface.php | 4 +- .../src/BlockContentViewsData.php | 25 +- .../Tests/Views/RevisionRelationshipsTest.php | 8 +- ...ws.view.test_block_content_revision_id.yml | 21 +- ...est_block_content_revision_revision_id.yml | 21 +- .../comment/src/CommentStorageInterface.php | 4 +- .../views.view.test_comment_rest.yml | 2 +- .../src/Form/ConfigSingleImportForm.php | 193 ++++++++++++-- .../config/src/StorageReplaceDataWrapper.php | 204 +++++++++++++++ .../Tests/ConfigSingleImportExportTest.php | 42 ++- .../views.view.test_argument_datetime.yml | 4 +- .../views.view.test_filter_datetime.yml | 4 +- .../views.view.test_sort_datetime.yml | 4 +- .../views.view.test_view_fieldapi.yml | 4 +- .../modules/file/src/FileStorageInterface.php | 4 +- .../modules/node/src/NodeStorageInterface.php | 4 +- core/modules/node/src/NodeViewsData.php | 2 +- .../views.view.test_nid_argument.yml | 2 +- ...iew.test_serializer_node_display_field.yml | 2 +- ...ew.test_serializer_node_exposed_filter.yml | 2 +- .../Tests/Entity/EntityTranslationTest.php | 2 + .../keyvalue_test/keyvalue_test.module | 2 +- .../taxonomy/src/TermStorageInterface.php | 4 +- .../views.view.test_groupwise_term.yml | 2 +- ...s.view.test_taxonomy_term_relationship.yml | 2 +- .../modules/user/src/UserStorageInterface.php | 4 +- core/modules/views/src/EntityViewsData.php | 36 ++- .../Tests/Update/FieldHandlersUpdateTest.php | 48 ++++ .../update/duplicate-field-handler.php | 11 + ...ews.view.test_duplicate_field_handlers.yml | 246 ++++++++++++++++++ .../views.view.test_row_render_cache.yml | 2 +- .../tests/src/Unit/EntityViewsDataTest.php | 72 +++-- core/modules/views/views.post_update.php | 96 +++++++ .../views_ui/src/Tests/HandlerTest.php | 42 ++- sites/default/default.settings.php | 17 +- sites/default/settings.pantheon.php | 16 +- 49 files changed, 1394 insertions(+), 281 deletions(-) create mode 100644 core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php create mode 100644 core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php create mode 100644 core/modules/config/src/StorageReplaceDataWrapper.php create mode 100644 core/modules/views/src/Tests/Update/FieldHandlersUpdateTest.php create mode 100644 core/modules/views/tests/fixtures/update/duplicate-field-handler.php create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.test_duplicate_field_handlers.yml diff --git a/core/CHANGELOG.txt b/core/CHANGELOG.txt index 1c5f8a588..6b8e812d5 100644 --- a/core/CHANGELOG.txt +++ b/core/CHANGELOG.txt @@ -1,8 +1,9 @@ -Drupal 8.0.x, xxxx-xx-xx (development version) ----------------------- -- Dramatically improved the front end: +Drupal 8.0.0, 2015-11-19 +------------------------ +- Significantly improved the front end: * Made all built-in themes responsive. * Added support for responsive images. + * Made built-in tables responsive with three levels of column importance. * Added Twig as the default template engine and converted all .tpl.php templates and theme functions to .html.twig. * Removed the PHPTemplate engine. @@ -10,17 +11,16 @@ Drupal 8.0.x, xxxx-xx-xx (development version) * Added Classy as a base theme to maintain CSS classes and wrappers. * Added Stable as the default base theme to maintain backwards compatibility for core template and CSS changes, because templates and CSS outside - Stable can be improved in minor releases (8.1.0, 8.2.0 …). + Stable can be improved in minor releases (8.1.0, 8.2.0, etc.). * Redesigned several key elements of the Seven theme. * Added support for HTML5 elements. - * Included the HTML5 Shiv library to support HTML5 elements in IE 8 and - below. * Included Backbone.js and Underscore.js JavaScript frameworks. * Updated to jQuery 2.1.4. * Updated to jQuery UI 1.11.4. * Removed jquery.bbq. * Removed the Garland theme from core. - * Removed the Overlay module from core. + * Removed the Overlay module from core and replaced it with a simple, + dynamic "Back to site" link. * Improved the asset library system to manage CSS and JavaScript files and their dependencies. Allowing for smaller AJAX request payloads. * jQuery is no longer loaded on all pages, only when another asset needs it. @@ -29,41 +29,72 @@ Drupal 8.0.x, xxxx-xx-xx (development version) * Implemented SMACSS-style categorization for CSS files. * Removed most support for Internet Explorer 8 and below. * Added Modernizr for making styling changes based on browser support. - * All page template variables converted to blocks. -- Added tour module. Provides highly contextual tips for UI elements. -- Improved entity system. - * Added support for saving and deleting entities through the controller. - * Base entity fields (such as labels) support widgets, formatters and - translation. - * Form modes introduced, similar to display modes. - * Entities are now classed objects, implementing EntityInterface. - * Drupal now understands the concept of a "default" revision, tracked - independently from the latest revision, allowing for the creation of - drafts while the current revision stays published. - * All entity types, not just nodes, now have support for revisions. + * All page template variables converted to blocks (title, breadcrumb, + branding, etc). + * Added the Breakpoint module to manage breakpoints of responsive designs. + * Introduced native Schema.org output in pages. + * Made use of semantic HTML 5 tags when possible. This also makes form input + on mobile devices much easier for users. + * Redesigned icons to look good on high resolution (retina) displays too. +- Made the site administration experience simpler: + * Redesigned the installer. + * Visually updated and extended the Seven (administration) theme. + * Made the administration toolbar responsive and touch friendly. + * Added search to the module listing and made the page easier to read. + * Added the tour module to provide highly contextual tips for UI elements. +- Improved the entity system: + * Added a full CRUD API for entities. + * Improved the field API and entity query API. + * Added support for widgets, formatters, and translation to base entity + fields (such as labels). + * Made view modes configurable for reusable display variants. + * Introduced form modes for reusable form variants. + * Added ability to handle a "default" revision that may not be the latest. + * All content entity types (custom blocks, terms, comments, etc.), not just + nodes, have support for revisions. + * Database schema of content entities is automatically generated based on + entity type and field definitions. +- Added the Typed Data system to manage complex types. - Refactored routing system based on Symfony2 components. -- Reworked menu links, local actions, and local tasks based upon the new routing - system. +- Made declarative information (libraries, permissions, routes etc.) use YAML + files for definitions instead of PHP. +- Improved the menu handling systems: + * Moved custom menu item handling to its own module. + * Reworked menu links, local actions, and local tasks based upon the new + routing system. - Added plugin system to standardize implementation of several core APIs. -- Configuration: - * Added a centralized file-based configuration system. - * Allows module authors to provide configuration in a standard format. - * Implements functionality to get, set, add and remove configuration. - * Includes ability to override configuration values with language variants - and other runtime values. - * Supports configuration schema, dependencies, and validation to maintain - data-integrity between deployments and updates. +- Introduced a new configuration management system: + * Added a centralized configuration system with export and import + functionality. + * Allowed module authors to provide configuration in a YAML file format. + * Implemented functionality to get, set, add, and remove configuration. + * Provided the ability to override configuration values with language + variants and other runtime values. + * Added configuration schema, dependencies, and validation to maintain + data integrity between deployments and updates. + * Support added for both global configuration and configuration entities. - Improved authoring experience: + * Redesigned the content creation and editing form. + * Content preview is now displayed on the frontend. * Added the CKEditor WYSIWYG editor. Clean markup guaranteed thanks to tight integration with the filter system. - * Includes uploading, aligning and captioning of images. - * Correspondingly modernized the default text formats. - * Provides a drag-and-drop configuration UI, which automatically updates the + * Made uploading, aligning, and captioning of images possible in the editor. + * Modernized the default text formats. + * Added a drag-and-drop configuration UI, which automatically updates the HTML filter settings, making configuring text formats trivial for typical use cases. * Added align and caption filters that can be applied to any element: images, blockquotes, code snippets, videos… - * In-place editing of any entity: nodes, blocks… + * Made possible to in-place edit any entity: nodes, blocks… + * Added the Text Editor module to help map other editors to text formats. +- Improved media management: + * Added ability to configure when unused files get deleted with the option + to keep them, useful for media libraries. + * Added a customizable view under the content administration screen that + lists all files uploaded on the system. + * Made uploads immediate when selecting files in file fields. + * Added ability to upload multiple files at once. + * Added local image input filter, to enable secure image posting. - Included the following Symfony2 components: * ClassLoader - PSR-0-compatible autoload routines. * DependencyInjection - Flexible dependency injection container. @@ -86,16 +117,14 @@ Drupal 8.0.x, xxxx-xx-xx (development version) * Blog * Dashboard * OpenID - * PHP Filter * Poll * Profile * Trigger -- Removed the Statistics module's accesslog functionality and reports from core. +- Removed the Statistics module's accesslog functionality and reports. - Removed XML-RPC functionality from core. - Removed user signatures support from core. -- Universally Unique IDentifier (UUID): - * Support for generating and validating UUIDs. -- Tremendously improved language support all around. +- Added ability to generate and validate Universally Unique IDentifiers (UUIDs). +- Tremendously improved language support all around: * Great language improvements for users: * Improved language selection with user preference detection in the installer based on browser settings. @@ -143,12 +172,6 @@ Drupal 8.0.x, xxxx-xx-xx (development version) configuration with translatable values (blocks, views, fields, etc.). * Added language options to block visibility. * Much improved language APIs for developers: - * Added simple APIs and hooks to save/delete/update languages. - * New Language class wraps language information, used universally. - * Unified database schemas and APIs to make it easier to spot where - language codes are referenced. - * Made the language negotiation system APIs more consistent for - developers. * Made it possible for users to have a preferred language separate from their user entity language. * The text formatter from t() is now available as FormattableMarkup. @@ -157,49 +180,54 @@ Drupal 8.0.x, xxxx-xx-xx (development version) menu items and contextual links. * Removed textgroups support from interface translation in favor of native configuration language support. - * Added configuration schema system to support generating translation - forms for any configuration. - * Reworked Gettext PO support to use pluggable read/write handlers. - * Added language select form element in the Form API. * Added a transliteration API. (Only used for machine names in core.) + * Added a language fallback capability to the interface translation API. - New field types added to core: - - Email - - Link - - Phone number - - Entity reference - - Date - - Comment (allows comment threads on entity types other than node). -- Added local image input filter, to enable secure image posting. + * Email + * Link + * Telephone number + * Entity reference + * Date +- Made commenting more flexible: + * Added the notion of comment types (for reviews, greetings, and so on), + each of which can be configured with a different set of fields. + * Made commenting a field to allow comment threads on entity types other + than nodes. - Added Views and Views UI module to core: - * Various core listings: /node, /admin/content/node, /admin/people etc. are - now served by views. - * REST API support built in. - * Rewrote caching integration for better performance. -- Custom blocks are now fieldable, revisionable, and translatable entities. -- An accessible modal API based on improvements made in collaboration with the - jQuery UI team and the Views team. -- Fieldable contact forms allowing site-builders to easily build custom forms - for soliciting feedback from users. + * Added simple bulk operations functionality to Views. + * Converted various core listings to views, including /node, + /admin/content/node, /admin/people, and several blocks. + * Built in REST API support. + * Rewrote caching integration for better performance. + * Made it possible to configure responsive tables in Views. +- Greatly improved block management: + * Made custom blocks fieldable, revisionable, and translatable entities. + * Added the notion of custom block types. + * Added the ability to place the same block in multiple locations. + * Introduced a block library with categorized blocks. +- Introduced an accessible modal API based on improvements made in collaboration + with the jQuery UI team. +- Made it possible to add fields to contact forms allowing site-builders to + easily build custom forms for soliciting feedback from users. - Added a Web Services module package. * Added a RESTful web services provider module. * Added a serialization module using the Symfony serialization component. * Added a Hypertext Application Language (HAL) serialization module. * Added a HTTP Basic authentication provider module. -- Significant performance/scalability improvements: - * Cache tags, which allow content to be invalidated accurately and instantly, - including reverse proxies and CDNs. - * Cache contexts, which allow content to be cached correctly, and placeholdered - to improve cache hit rates. - * Cacheability bubbling, which allows strict tracking of assets and - cacheability throughout page rendering. - * Page caching has been factored out to its own module and is enabled by - default. - * Authenticated page caching has been added to core via the Dynamic Page Cache - module and is enabled by default. - * APCu, memory, and PHP file caching backends added to core, alongside support - for a chained, consistent cache backend to support correctly using fast - local cache implementations with multiple web servers. -- When using MySQL, the MyISAM engine is no longer supported. +- Improved performance/scalability significantly: + * Introduced cache tags, which allow content to be invalidated accurately + and instantly, including for reverse proxies and CDNs. + * Added cache contexts, which allow content to be cached correctly, and + placeholdered to improve cache hit rates. + * Implemented cacheability bubbling, which allows strict tracking of assets + and cacheability throughout page rendering. + * Factored out page caching to its own module and enabled it by default. + * Added the Dynamic Page Cache module for authenticated page caching and + enabled it by default. + * Added APCu, memory, and PHP file caching backends to core, alongside + support for a chained, consistent cache backend to support correctly using + fast local cache implementations with multiple web servers. +- Removed support for MyISAM, when using MySQL. - Testing improvements * Added PHPUnit for proper unit testing, see https://phpunit.de/manual/4.8/en/index.html so you can run tests via @@ -209,6 +237,32 @@ Drupal 8.0.x, xxxx-xx-xx (development version) * Added KernelTestBase to provide a fast API testing of integration of different components * Core branch nightly tests include PHP 5.5, 5.6, 7, sqlite and PostgreSQL. +- Added the migrate module (experimental) with support for migrating content and + configuration from earlier Drupal versions. +- Introduced support for Composer. +- Moved the automated cron execution functionality to its own module. +- Refactored IP address based banning functionality to its own module. +- Security improvements: + * Removed PHP filter, including the ability to use PHP for block visibility. + * Managing fields for each entity type is now a separate permission. + * PDO drivers other than MySQL are now limited to executing single + statements to limit SQL injection vectors. + * Added an autoescape API to prevent cross-site scripting in many of the + places where Drupal outputs HTML. + * Hardened user session and session ID handling. + * Automated CSRF protection in route definitions. + * Clickjacking protection enabled by default. + * Made the core JavaScript API compatible with Content Security Policy + (CSP). + * Trusted host patterns enforced for requests preventing cache and link + poisoning. +- Switched to semantic versioning with significant updates planned every 6 + months in 8.1, 8.2, etc. +- Numerous other important changes and additions. See + https://www.drupal.org/list-changes/drupal for a detailed list. +- Numerous bug fixes. +- Numerous API documentation improvements. +- Additional automated test coverage. Drupal 7.0, 2011-01-05 ---------------------- diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index d75f50b72..45ba2c548 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -81,7 +81,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '8.0.0-dev-2015-11-17'; + const VERSION = '8.0.0'; /** * Core API compatibility. diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 2d0de14b4..837abc036 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -511,8 +511,6 @@ class ConfigImporter { * If the configuration is already importing. */ public function initialize() { - $this->createExtensionChangelist(); - // Ensure that the changes have been validated. $this->validate(); @@ -710,8 +708,10 @@ class ConfigImporter { * @throws \Drupal\Core\Config\ConfigImporterException * Exception thrown if the validate event logged any errors. */ - protected function validate() { + public function validate() { if (!$this->validated) { + // Create the list of installs and uninstalls. + $this->createExtensionChangelist(); // Validate renames. foreach ($this->getUnprocessedConfiguration('rename') as $name) { $names = $this->storageComparer->extractRenameNames($name); diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php index ab94c27f6..527a336f3 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php @@ -69,9 +69,6 @@ class ConfigEntityType extends EntityType implements ConfigEntityTypeInterface { // Always add a default 'uuid' key. $this->entity_keys['uuid'] = 'uuid'; $this->entity_keys['langcode'] = 'langcode'; - if (isset($this->handlers['storage'])) { - $this->checkStorageClass($this->handlers['storage']); - } $this->handlers += array( 'storage' => 'Drupal\Core\Config\Entity\ConfigEntityStorage', ); @@ -135,23 +132,12 @@ class ConfigEntityType extends EntityType implements ConfigEntityTypeInterface { /** * {@inheritdoc} * + * @see \Drupal\Core\Config\Entity\ConfigEntityStorage. + * * @throws \Drupal\Core\Config\Entity\Exception\ConfigEntityStorageClassException * Exception thrown when the provided class is not an instance of * \Drupal\Core\Config\Entity\ConfigEntityStorage. */ - public function setStorageClass($class) { - $this->checkStorageClass($class); - parent::setStorageClass($class); - } - - /** - * Checks that the provided class is an instance of ConfigEntityStorage. - * - * @param string $class - * The class to check. - * - * @see \Drupal\Core\Config\Entity\ConfigEntityStorage. - */ protected function checkStorageClass($class) { if (!is_a($class, 'Drupal\Core\Config\Entity\ConfigEntityStorage', TRUE)) { throw new ConfigEntityStorageClassException("$class is not \\Drupal\\Core\\Config\\Entity\\ConfigEntityStorage or it does not extend it"); diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 4f11f1e51..1e198fe01 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -808,6 +808,13 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C return !empty($this->translations[$langcode]['status']); } + /** + * {@inheritdoc} + */ + public function isNewTranslation() { + return $this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_CREATED; + } + /** * {@inheritdoc} */ @@ -822,37 +829,11 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C throw new \InvalidArgumentException("The entity cannot be translated since it is language neutral ({$this->defaultLangcode})."); } - // Instantiate a new empty entity so default values will be populated in the - // specified language. - $entity_type = $this->getEntityType(); - - $default_values = array( - $entity_type->getKey('bundle') => $this->bundle(), - $this->langcodeKey => $langcode, - ); - $entity = $this->entityManager() - ->getStorage($this->getEntityTypeId()) - ->create($default_values); - - foreach ($entity as $name => $field) { - if (!isset($values[$name]) && !$field->isEmpty()) { - $values[$name] = $field->getValue(); - } - } - $values[$this->langcodeKey] = $langcode; - $values[$this->defaultLangcodeKey] = FALSE; - + // Initialize the translation object. + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->entityManager()->getStorage($this->getEntityTypeId()); $this->translations[$langcode]['status'] = static::TRANSLATION_CREATED; - $translation = $this->getTranslation($langcode); - $definitions = $translation->getFieldDefinitions(); - - foreach ($values as $name => $value) { - if (isset($definitions[$name]) && $definitions[$name]->isTranslatable()) { - $translation->values[$name][$langcode] = $value; - } - } - - return $translation; + return $storage->createTranslation($this, $langcode, $values); } /** diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 2a5475e2f..5eb5c30bb 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -13,7 +13,10 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -abstract class ContentEntityStorageBase extends EntityStorageBase implements DynamicallyFieldableEntityStorageInterface { +/** + * Base class for content entity storage handlers. + */ +abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface { /** * The entity bundle key. @@ -87,13 +90,32 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn $bundle = $values[$this->bundleKey]; } $entity = new $this->entityClass(array(), $this->entityTypeId, $bundle); + $this->initFieldValues($entity, $values); + return $entity; + } + /** + * Initializes field values. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * An entity object. + * @param array $values + * (optional) An associative array of initial field values keyed by field + * name. If none is provided default values will be applied. + * @param array $field_names + * (optional) An associative array of field names to be initialized. If none + * is provided all fields will be initialized. + */ + protected function initFieldValues(ContentEntityInterface $entity, array $values = [], array $field_names = []) { + // Populate field values. foreach ($entity as $name => $field) { - if (isset($values[$name])) { - $entity->$name = $values[$name]; - } - elseif (!array_key_exists($name, $values)) { - $entity->get($name)->applyDefaultValue(); + if (!$field_names || isset($field_names[$name])) { + if (isset($values[$name])) { + $entity->$name = $values[$name]; + } + elseif (!array_key_exists($name, $values)) { + $entity->get($name)->applyDefaultValue(); + } } unset($values[$name]); } @@ -102,7 +124,23 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn foreach ($values as $name => $value) { $entity->$name = $value; } - return $entity; + + // Make sure modules can alter field initial values. + $this->invokeHook('field_values_init', $entity); + } + + /** + * {@inheritdoc} + */ + public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) { + $translation = $entity->getTranslation($langcode); + $definitions = array_filter($translation->getFieldDefinitions(), function(FieldDefinitionInterface $definition) { return $definition->isTranslatable(); }); + $field_names = array_map(function(FieldDefinitionInterface $definition) { return $definition->getName(); }, $definitions); + $values[$this->langcodeKey] = $langcode; + $values[$this->getEntityType()->getKey('default_langcode')] = FALSE; + $this->initFieldValues($translation, $values, $field_names); + $this->invokeHook('translation_create', $entity); + return $translation; } /** diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php new file mode 100644 index 000000000..e973c3977 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php @@ -0,0 +1,31 @@ +handlers += array( 'access' => 'Drupal\Core\Entity\EntityAccessControlHandler', ); + if (isset($this->handlers['storage'])) { + $this->checkStorageClass($this->handlers['storage']); + } // Automatically add the EntityChanged constraint if the entity type tracks // the changed time. @@ -459,9 +462,20 @@ class EntityType implements EntityTypeInterface { * {@inheritdoc} */ public function setStorageClass($class) { + $this->checkStorageClass($class); $this->handlers['storage'] = $class; } + /** + * Checks that the provided class is an instance of ConfigEntityStorage. + * + * @param string $class + * The class to check. + */ + protected function checkStorageClass($class) { + // Nothing to check by default. + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php new file mode 100644 index 000000000..2a866ef32 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php @@ -0,0 +1,25 @@ +foo->value) { - $entity->foo->value = 'some_initial_value'; - } + \Drupal::logger('example')->info('Entity created: @label', ['@label' => $entity->label()]); } /** - * Act on a newly created entity of a specific type. + * Acts when creating a new entity of a specific type. * - * This hook runs after a new entity object has just been instantiated. It can - * be used to set initial values, e.g. to provide defaults. + * This hook runs after a new entity object has just been instantiated. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity object. @@ -810,9 +806,7 @@ function hook_entity_create(\Drupal\Core\Entity\EntityInterface $entity) { * @see hook_entity_create() */ function hook_ENTITY_TYPE_create(\Drupal\Core\Entity\EntityInterface $entity) { - if (!$entity->foo->value) { - $entity->foo->value = 'some_initial_value'; - } + \Drupal::logger('example')->info('ENTITY_TYPE created: @label', ['@label' => $entity->label()]); } /** @@ -1011,6 +1005,38 @@ function hook_ENTITY_TYPE_update(Drupal\Core\Entity\EntityInterface $entity) { ->execute(); } +/** + * Acts when creating a new entity translation. + * + * This hook runs after a new entity translation object has just been + * instantiated. + * + * @param \Drupal\Core\Entity\EntityInterface $translation + * The entity object. + * + * @ingroup entity_crud + * @see hook_ENTITY_TYPE_translation_create() + */ +function hook_entity_translation_create(\Drupal\Core\Entity\EntityInterface $translation) { + \Drupal::logger('example')->info('Entity translation created: @label', ['@label' => $translation->label()]); +} + +/** + * Acts when creating a new entity translation of a specific type. + * + * This hook runs after a new entity translation object has just been + * instantiated. + * + * @param \Drupal\Core\Entity\EntityInterface $translation + * The entity object. + * + * @ingroup entity_crud + * @see hook_entity_translation_create() + */ +function hook_ENTITY_TYPE_translation_create(\Drupal\Core\Entity\EntityInterface $translation) { + \Drupal::logger('example')->info('ENTITY_TYPE translation created: @label', ['@label' => $translation->label()]); +} + /** * Respond to creation of a new entity translation. * @@ -1886,6 +1912,44 @@ function hook_entity_field_access_alter(array &$grants, array $context) { } } +/** + * Acts when initializing a fieldable entity object. + * + * This hook runs after a new entity object or a new entity translation object + * has just been instantiated. It can be used to set initial values, e.g. to + * provide defaults. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity object. + * + * @ingroup entity_crud + * @see hook_ENTITY_TYPE_field_values_init() + */ +function hook_entity_field_values_init(\Drupal\Core\Entity\FieldableEntityInterface $entity) { + if ($entity instanceof \Drupal\Core\Entity\ContentEntityInterface && !$entity->foo->value) { + $entity->foo->value = 'some_initial_value'; + } +} + +/** + * Acts when initializing a fieldable entity object. + * + * This hook runs after a new entity object or a new entity translation object + * has just been instantiated. It can be used to set initial values, e.g. to + * provide defaults. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity object. + * + * @ingroup entity_crud + * @see hook_entity_field_values_init() + */ +function hook_ENTITY_TYPE_field_values_init(\Drupal\Core\Entity\FieldableEntityInterface $entity) { + if (!$entity->foo->value) { + $entity->foo->value = 'some_initial_value'; + } +} + /** * Exposes "pseudo-field" components on content entities. * diff --git a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php index f87e7c891..f64242c0a 100644 --- a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php +++ b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php @@ -28,6 +28,14 @@ interface TranslatableInterface { */ public function isDefaultTranslation(); + /** + * Checks whether the translation is new. + * + * @return bool + * TRUE if the translation is new, FALSE otherwise. + */ + public function isNewTranslation(); + /** * Returns the languages the data is translated to. * diff --git a/core/modules/aggregator/src/FeedStorageInterface.php b/core/modules/aggregator/src/FeedStorageInterface.php index 48b2732d2..80c34d1e7 100644 --- a/core/modules/aggregator/src/FeedStorageInterface.php +++ b/core/modules/aggregator/src/FeedStorageInterface.php @@ -7,12 +7,12 @@ namespace Drupal\aggregator; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; /** * Defines an interface for aggregator feed entity storage classes. */ -interface FeedStorageInterface extends EntityStorageInterface { +interface FeedStorageInterface extends ContentEntityStorageInterface { /** * Returns the fids of feeds that need to be refreshed. diff --git a/core/modules/aggregator/src/ItemStorageInterface.php b/core/modules/aggregator/src/ItemStorageInterface.php index f6526a4f6..d0ea897a0 100644 --- a/core/modules/aggregator/src/ItemStorageInterface.php +++ b/core/modules/aggregator/src/ItemStorageInterface.php @@ -7,12 +7,12 @@ namespace Drupal\aggregator; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; /** * Defines an interface for aggregator item entity storage classes. */ -interface ItemStorageInterface extends EntityStorageInterface { +interface ItemStorageInterface extends ContentEntityStorageInterface { /** * Returns the count of the items in a feed. diff --git a/core/modules/block_content/src/BlockContentViewsData.php b/core/modules/block_content/src/BlockContentViewsData.php index abb2cb211..40963ce91 100644 --- a/core/modules/block_content/src/BlockContentViewsData.php +++ b/core/modules/block_content/src/BlockContentViewsData.php @@ -36,25 +36,24 @@ class BlockContentViewsData extends EntityViewsData { ), ); // Advertise this table as a possible base table. - $data['block_content_revision']['table']['base']['help'] = $this->t('Block Content revision is a history of changes to block content.'); - $data['block_content_revision']['table']['base']['defaults']['title'] = 'info'; + $data['block_content_field_revision']['table']['base']['help'] = $this->t('Block Content revision is a history of changes to block content.'); + $data['block_content_field_revision']['table']['base']['defaults']['title'] = 'info'; // @todo EntityViewsData should add these relationships by default. // https://www.drupal.org/node/2410275 - $data['block_content_revision']['id']['relationship']['id'] = 'standard'; - $data['block_content_revision']['id']['relationship']['base'] = 'block_content'; - $data['block_content_revision']['id']['relationship']['base field'] = 'id'; - $data['block_content_revision']['id']['relationship']['title'] = $this->t('Block Content'); - $data['block_content_revision']['id']['relationship']['label'] = $this->t('Get the actual block content from a block content revision.'); + $data['block_content_field_revision']['id']['relationship']['id'] = 'standard'; + $data['block_content_field_revision']['id']['relationship']['base'] = 'block_content_field_data'; + $data['block_content_field_revision']['id']['relationship']['base field'] = 'id'; + $data['block_content_field_revision']['id']['relationship']['title'] = $this->t('Block Content'); + $data['block_content_field_revision']['id']['relationship']['label'] = $this->t('Get the actual block content from a block content revision.'); - $data['block_content_revision']['revision_id']['relationship']['id'] = 'standard'; - $data['block_content_revision']['revision_id']['relationship']['base'] = 'block_content'; - $data['block_content_revision']['revision_id']['relationship']['base field'] = 'revision_id'; - $data['block_content_revision']['revision_id']['relationship']['title'] = $this->t('Block Content'); - $data['block_content_revision']['revision_id']['relationship']['label'] = $this->t('Get the actual block content from a block content revision.'); + $data['block_content_field_revision']['revision_id']['relationship']['id'] = 'standard'; + $data['block_content_field_revision']['revision_id']['relationship']['base'] = 'block_content_field_data'; + $data['block_content_field_revision']['revision_id']['relationship']['base field'] = 'revision_id'; + $data['block_content_field_revision']['revision_id']['relationship']['title'] = $this->t('Block Content'); + $data['block_content_field_revision']['revision_id']['relationship']['label'] = $this->t('Get the actual block content from a block content revision.'); return $data; - } } diff --git a/core/modules/block_content/src/Tests/Views/RevisionRelationshipsTest.php b/core/modules/block_content/src/Tests/Views/RevisionRelationshipsTest.php index 828f124e6..bd943eef4 100644 --- a/core/modules/block_content/src/Tests/Views/RevisionRelationshipsTest.php +++ b/core/modules/block_content/src/Tests/Views/RevisionRelationshipsTest.php @@ -60,7 +60,7 @@ class RevisionRelationshipsTest extends ViewTestBase { $column_map = array( 'revision_id' => 'revision_id', 'id_1' => 'id_1', - 'block_content_block_content_revision_id' => 'block_content_block_content_revision_id', + 'block_content_field_data_block_content_field_revision_id' => 'block_content_field_data_block_content_field_revision_id', ); // Here should be two rows. @@ -70,12 +70,12 @@ class RevisionRelationshipsTest extends ViewTestBase { array( 'revision_id' => '1', 'id_1' => '1', - 'block_content_block_content_revision_id' => '1', + 'block_content_field_data_block_content_field_revision_id' => '1', ), array( 'revision_id' => '2', 'id_1' => '1', - 'block_content_block_content_revision_id' => '1', + 'block_content_field_data_block_content_field_revision_id' => '1', ), ); $this->assertIdenticalResultset($view_id, $resultset_id, $column_map); @@ -87,7 +87,7 @@ class RevisionRelationshipsTest extends ViewTestBase { array( 'revision_id' => '2', 'id_1' => '1', - 'block_content_block_content_revision_id' => '1', + 'block_content_field_data_block_content_field_revision_id' => '1', ), ); $this->assertIdenticalResultset($view_revision_id, $resultset_revision_id, $column_map); diff --git a/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml b/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml index efcabc388..e7a481019 100644 --- a/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml +++ b/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml @@ -8,7 +8,7 @@ label: null module: views description: '' tag: '' -base_table: block_content_revision +base_table: block_content_field_revision base_field: revision_id core: '8' display: @@ -17,28 +17,28 @@ display: relationships: id: id: id - table: block_content_revision + table: block_content_field_revision field: id required: true plugin_id: standard fields: revision_id: id: revision_id - table: block_content_revision + table: block_content_field_revision field: revision_id plugin_id: field entity_type: block_content entity_field: revision_id id_1: id: id_1 - table: block_content_revision + table: block_content_field_revision field: id plugin_id: field entity_type: block_content entity_field: id id: id: id - table: block_content + table: block_content_field_data field: id relationship: id plugin_id: field @@ -47,11 +47,20 @@ display: arguments: id: id: id - table: block_content_revision + table: block_content_field_revision field: id plugin_id: numeric entity_type: block_content entity_field: id + sorts: + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + order: ASC + plugin_id: field + entity_type: block_content + entity_field: revision_id display_plugin: default display_title: Master id: default diff --git a/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml b/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml index 566f2c56c..a304198f4 100644 --- a/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml +++ b/core/modules/block_content/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml @@ -8,7 +8,7 @@ label: null module: views description: '' tag: '' -base_table: block_content_revision +base_table: block_content_field_revision base_field: revision_id core: '8' display: @@ -17,7 +17,7 @@ display: relationships: revision_id: id: revision_id - table: block_content_revision + table: block_content_field_revision field: revision_id required: true entity_type: block_content @@ -26,21 +26,21 @@ display: fields: revision_id: id: revision_id - table: block_content_revision + table: block_content_field_revision field: revision_id plugin_id: field entity_type: block_content entity_field: revision_id id_1: id: id_1 - table: block_content_revision + table: block_content_field_revision field: id plugin_id: field entity_type: block_content entity_field: id id: id: id - table: block_content + table: block_content_field_data field: id relationship: revision_id plugin_id: field @@ -49,11 +49,20 @@ display: arguments: id: id: id - table: block_content_revision + table: block_content_field_revision field: id plugin_id: block_content_id entity_type: block_content entity_field: id + sorts: + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + order: ASC + plugin_id: field + entity_type: block_content + entity_field: revision_id display_extenders: { } display_plugin: default display_title: Master diff --git a/core/modules/comment/src/CommentStorageInterface.php b/core/modules/comment/src/CommentStorageInterface.php index 318c0b4e4..b660fc29f 100644 --- a/core/modules/comment/src/CommentStorageInterface.php +++ b/core/modules/comment/src/CommentStorageInterface.php @@ -8,13 +8,13 @@ namespace Drupal\comment; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; /** * Defines an interface for comment entity storage classes. */ -interface CommentStorageInterface extends EntityStorageInterface { +interface CommentStorageInterface extends ContentEntityStorageInterface { /** * Gets the maximum encoded thread value for the top level comments. diff --git a/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml b/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml index 2688bd605..fabd46c9e 100644 --- a/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml +++ b/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_comment_rest.yml @@ -338,7 +338,7 @@ display: arguments: nid: id: nid - table: node + table: node_field_data field: nid relationship: node group_type: group diff --git a/core/modules/config/src/Form/ConfigSingleImportForm.php b/core/modules/config/src/Form/ConfigSingleImportForm.php index 15f173dcc..7255e028e 100644 --- a/core/modules/config/src/Form/ConfigSingleImportForm.php +++ b/core/modules/config/src/Form/ConfigSingleImportForm.php @@ -8,12 +8,24 @@ namespace Drupal\config\Form; use Drupal\Component\Serialization\Yaml; +use Drupal\config\StorageReplaceDataWrapper; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\StorageComparer; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\ConfirmFormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Provides a form for importing a single configuration file. @@ -34,6 +46,62 @@ class ConfigSingleImportForm extends ConfirmFormBase { */ protected $configStorage; + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface; + */ + protected $configManager; + + /** + * The database lock object. + * + * @var \Drupal\Core\Lock\LockBackendInterface + */ + protected $lock; + + /** + * The typed config manager. + * + * @var \Drupal\Core\Config\TypedConfigManagerInterface + */ + protected $typedConfigManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** + * The module installer. + * + * @var \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected $moduleInstaller; + /** * If the config exists, this is that object. Otherwise, FALSE. * @@ -55,10 +123,36 @@ class ConfigSingleImportForm extends ConfirmFormBase { * The entity manager. * @param \Drupal\Core\Config\StorageInterface $config_storage * The config storage. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher used to notify subscribers of config import events. + * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager + * The configuration manager. + * @param \Drupal\Core\Lock\LockBackendInterface $lock + * The lock backend to ensure multiple imports do not occur at the same time. + * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config + * The typed configuration manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer + * The module installer. + * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler + * The theme handler. */ - public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage) { + public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage, RendererInterface $renderer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler) { $this->entityManager = $entity_manager; $this->configStorage = $config_storage; + $this->renderer = $renderer; + + // Services necessary for \Drupal\Core\Config\ConfigImporter. + $this->eventDispatcher = $event_dispatcher; + $this->configManager = $config_manager; + $this->lock = $lock; + $this->typedConfigManager = $typed_config; + $this->moduleHandler = $module_handler; + $this->moduleInstaller = $module_installer; + $this->themeHandler = $theme_handler; } /** @@ -67,7 +161,15 @@ class ConfigSingleImportForm extends ConfirmFormBase { public static function create(ContainerInterface $container) { return new static( $container->get('entity.manager'), - $container->get('config.storage') + $container->get('config.storage'), + $container->get('renderer'), + $container->get('event_dispatcher'), + $container->get('config.manager'), + $container->get('lock.persistent'), + $container->get('config.typed'), + $container->get('module_handler'), + $container->get('module_installer'), + $container->get('theme_handler') ); } @@ -204,6 +306,8 @@ class ConfigSingleImportForm extends ConfirmFormBase { $form_state->setErrorByName('import', $this->t('Missing ID key "@id_key" for this @entity_type import.', array('@id_key' => $id_key, '@entity_type' => $definition->getLabel()))); return; } + + $config_name = $definition->getConfigPrefix() . '.' . $data[$id_key]; // If there is an existing entity, ensure matching ID and UUID. if ($entity = $entity_storage->load($data[$id_key])) { $this->configExists = $entity; @@ -222,10 +326,53 @@ class ConfigSingleImportForm extends ConfirmFormBase { } } else { - $config = $this->config($form_state->getValue('config_name')); + $config_name = $form_state->getValue('config_name'); + $config = $this->config($config_name); $this->configExists = !$config->isNew() ? $config : FALSE; } + // Use ConfigImporter validation. + if (!$form_state->getErrors()) { + $source_storage = new StorageReplaceDataWrapper($this->configStorage); + $source_storage->replaceData($config_name, $data); + $storage_comparer = new StorageComparer( + $source_storage, + $this->configStorage, + $this->configManager + ); + + if (!$storage_comparer->createChangelist()->hasChanges()) { + $form_state->setErrorByName('import', $this->t('There are no changes to import.')); + } + else { + $config_importer = new ConfigImporter( + $storage_comparer, + $this->eventDispatcher, + $this->configManager, + $this->lock, + $this->typedConfigManager, + $this->moduleHandler, + $this->moduleInstaller, + $this->themeHandler, + $this->getStringTranslation() + ); + + try { + $config_importer->validate(); + $form_state->set('config_importer', $config_importer); + } + catch (ConfigImporterException $e) { + // There are validation errors. + $item_list = [ + '#theme' => 'item_list', + '#items' => $config_importer->getErrors(), + '#title' => $this->t('The configuration cannot be imported because it failed validation for the following reasons:'), + ]; + $form_state->setErrorByName('import', $this->renderer->render($item_list)); + } + } + } + // Store the decoded version of the submitted import. $form_state->setValueForElement($form['import'], $data); } @@ -241,26 +388,34 @@ class ConfigSingleImportForm extends ConfirmFormBase { return; } - // If a simple configuration file was added, set the data and save. - if ($this->data['config_type'] === 'system.simple') { - $this->configFactory()->getEditable($this->data['config_name'])->setData($this->data['import'])->save(); - drupal_set_message($this->t('The %name configuration was imported.', array('%name' => $this->data['config_name']))); + /** @var \Drupal\Core\Config\ConfigImporter $config_importer */ + $config_importer = $form_state->get('config_importer'); + if ($config_importer->alreadyImporting()) { + drupal_set_message($this->t('Another request may be importing configuration already.'), 'error'); } - // For a config entity, create an entity and save it. - else { + else{ try { - $entity_storage = $this->entityManager->getStorage($this->data['config_type']); - if ($this->configExists) { - $entity = $entity_storage->updateFromStorageRecord($this->configExists, $this->data['import']); + $sync_steps = $config_importer->initialize(); + $batch = [ + 'operations' => [], + 'finished' => [ConfigSync::class, 'finishBatch'], + 'title' => $this->t('Importing configuration'), + 'init_message' => $this->t('Starting configuration import.'), + 'progress_message' => $this->t('Completed @current step of @total.'), + 'error_message' => $this->t('Configuration import has encountered an error.'), + ]; + foreach ($sync_steps as $sync_step) { + $batch['operations'][] = [[ConfigSync::class, 'processBatch'], [$config_importer, $sync_step]]; } - else { - $entity = $entity_storage->createFromStorageRecord($this->data['import']); - } - $entity->save(); - drupal_set_message($this->t('The @entity_type %label was imported.', array('@entity_type' => $entity->getEntityTypeId(), '%label' => $entity->label()))); + + batch_set($batch); } - catch (\Exception $e) { - drupal_set_message($e->getMessage(), 'error'); + catch (ConfigImporterException $e) { + // There are validation errors. + drupal_set_message($this->t('The configuration import failed for the following reasons:'), 'error'); + foreach ($config_importer->getErrors() as $message) { + drupal_set_message($message, 'error'); + } } } } diff --git a/core/modules/config/src/StorageReplaceDataWrapper.php b/core/modules/config/src/StorageReplaceDataWrapper.php new file mode 100644 index 000000000..c59cae83f --- /dev/null +++ b/core/modules/config/src/StorageReplaceDataWrapper.php @@ -0,0 +1,204 @@ +storage = $storage; + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function exists($name) { + return isset($this->replacementData[$this->collection][$name]) || $this->storage->exists($name); + } + + /** + * {@inheritdoc} + */ + public function read($name) { + if (isset($this->replacementData[$this->collection][$name])) { + return $this->replacementData[$this->collection][$name]; + } + return $this->storage->read($name); + } + + /** + * {@inheritdoc} + */ + public function readMultiple(array $names) { + $data = $this->storage->readMultiple(($names)); + foreach ($names as $name) { + if (isset($this->replacementData[$this->collection][$name])) { + $data[$name] = $this->replacementData[$this->collection][$name]; + } + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function write($name, array $data) { + if (isset($this->replacementData[$this->collection][$name])) { + unset($this->replacementData[$this->collection][$name]); + } + return $this->storage->write($name, $data); + } + + /** + * {@inheritdoc} + */ + public function delete($name) { + if (isset($this->replacementData[$this->collection][$name])) { + unset($this->replacementData[$this->collection][$name]); + } + return $this->storage->delete($name); + } + + /** + * {@inheritdoc} + */ + public function rename($name, $new_name) { + if (isset($this->replacementData[$this->collection][$name])) { + $this->replacementData[$this->collection][$new_name] = $this->replacementData[$this->collection][$name]; + unset($this->replacementData[$this->collection][$name]); + } + return $this->rename($name, $new_name); + } + + /** + * {@inheritdoc} + */ + public function encode($data) { + return $this->storage->encode($data); + } + + /** + * {@inheritdoc} + */ + public function decode($raw) { + return $this->storage->decode($raw); + } + + /** + * {@inheritdoc} + */ + public function listAll($prefix = '') { + $names = $this->storage->listAll($prefix); + $additional_names = []; + if ($prefix === '') { + $additional_names = array_keys($this->replacementData[$this->collection]); + } + else { + foreach (array_keys($this->replacementData[$this->collection]) as $name) { + if (strpos($name, $prefix) === 0) { + $additional_names[] = $name; + } + } + } + if (!empty($additional_names)) { + $names = array_unique(array_merge($names, $additional_names)); + } + return $names; + } + + /** + * {@inheritdoc} + */ + public function deleteAll($prefix = '') { + if ($prefix === '') { + $this->replacementData[$this->collection] = []; + } + else { + foreach (array_keys($this->replacementData[$this->collection]) as $name) { + if (strpos($name, $prefix) === 0) { + unset($this->replacementData[$this->collection][$name]); + } + } + } + return $this->storage->deleteAll($prefix); + } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + $this->collection = $collection; + return $this->storage->createCollection($collection); + } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames() { + return $this->storage->getAllCollectionNames(); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName() { + return $this->collection; + } + + /** + * Replaces the configuration object data with the supplied data. + * + * @param $name + * The configuration object name whose data to replace. + * @param array $data + * The configuration data. + * + * @return $this + */ + public function replaceData($name, array $data) { + $this->replacementData[$this->collection][$name] = $data; + return $this; + } +} diff --git a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php index 7b3336dce..817bf2298 100644 --- a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php +++ b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php @@ -66,7 +66,7 @@ EOD; $this->assertIdentical($entity->label(), 'First'); $this->assertIdentical($entity->id(), 'first'); $this->assertTrue($entity->status()); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); // Attempt an import with an existing ID but missing UUID. $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); @@ -82,8 +82,7 @@ EOD; $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); $this->assertRaw(t('Are you sure you want to create a new %name @type?', array('%name' => 'custom_id', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); - $entity = $storage->load('custom_id'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); // Perform an import with a unique ID and UUID. $import = <<assertRaw(t('Are you sure you want to create a new %name @type?', array('%name' => 'second', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); $entity = $storage->load('second'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); $this->assertIdentical($entity->label(), 'Second'); $this->assertIdentical($entity->id(), 'second'); $this->assertFalse($entity->status()); @@ -126,8 +125,27 @@ EOD; $this->assertRaw(t('Are you sure you want to update the %name @type?', array('%name' => 'second', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); $entity = $storage->load('second'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); $this->assertIdentical($entity->label(), 'Second updated'); + + // Try to perform an update which adds missing dependencies. + $import = << 'config_test', + 'import' => $import, + ); + $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); + $this->assertRaw(t('Configuration %name depends on the %owner module that will not be installed after import.', ['%name' => 'config_test.dynamic.second', '%owner' => 'does_not_exist'])); } /** @@ -150,6 +168,20 @@ EOD; $this->drupalPostForm(NULL, array(), t('Confirm')); $this->drupalGet(''); $this->assertText('Test simple import'); + + // Ensure that ConfigImporter validation is running when importing simple + // configuration. + $config_data = $this->config('core.extension')->get(); + // Simulate uninstalling the Config module. + unset($config_data['module']['config']); + $edit = array( + 'config_type' => 'system.simple', + 'config_name' => 'core.extension', + 'import' => Yaml::encode($config_data), + ); + $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); + $this->assertText(t('Can not uninstall the Configuration module as part of a configuration synchronization through the user interface.')); + } /** diff --git a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_argument_datetime.yml b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_argument_datetime.yml index e22ad30c8..7f3abec22 100644 --- a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_argument_datetime.yml +++ b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_argument_datetime.yml @@ -27,7 +27,7 @@ display: field: nid id: nid relationship: none - table: node + table: node_field_data plugin_id: numeric pager: options: @@ -39,7 +39,7 @@ display: id: nid order: ASC relationship: none - table: node + table: node_field_data plugin_id: numeric display_plugin: default display_title: Master diff --git a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_filter_datetime.yml b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_filter_datetime.yml index c9b23ded6..abd0e4f1a 100644 --- a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_filter_datetime.yml +++ b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_filter_datetime.yml @@ -24,7 +24,7 @@ display: nid: field: nid id: nid - table: node + table: node_field_data plugin_id: node filters: field_date_value: @@ -38,7 +38,7 @@ display: id: nid order: ASC relationship: none - table: node + table: node_field_data plugin_id: numeric pager: type: full diff --git a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_sort_datetime.yml b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_sort_datetime.yml index 8b8d052ae..de808302f 100644 --- a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_sort_datetime.yml +++ b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_sort_datetime.yml @@ -24,7 +24,7 @@ display: nid: field: nid id: nid - table: node + table: node_field_data plugin_id: node sorts: field_date_value: @@ -39,7 +39,7 @@ display: id: nid order: ASC relationship: none - table: node + table: node_field_data plugin_id: numeric pager: type: full diff --git a/core/modules/field/tests/modules/field_test_views/test_views/views.view.test_view_fieldapi.yml b/core/modules/field/tests/modules/field_test_views/test_views/views.view.test_view_fieldapi.yml index 869e44194..ab39c2791 100644 --- a/core/modules/field/tests/modules/field_test_views/test_views/views.view.test_view_fieldapi.yml +++ b/core/modules/field/tests/modules/field_test_views/test_views/views.view.test_view_fieldapi.yml @@ -9,7 +9,7 @@ label: test_view_fieldapi module: views description: '' tag: default -base_table: node +base_table: node_field_data base_field: nid core: '8' display: @@ -21,7 +21,7 @@ display: nid: field: nid id: nid - table: node + table: node_field_data plugin_id: field entity_type: node entity_field: nid diff --git a/core/modules/file/src/FileStorageInterface.php b/core/modules/file/src/FileStorageInterface.php index 75bc56485..2a0d64ac8 100644 --- a/core/modules/file/src/FileStorageInterface.php +++ b/core/modules/file/src/FileStorageInterface.php @@ -7,12 +7,12 @@ namespace Drupal\file; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; /** * Defines an interface for file entity storage classes. */ -interface FileStorageInterface extends EntityStorageInterface { +interface FileStorageInterface extends ContentEntityStorageInterface { /** * Determines total disk space used by a single user or the whole filesystem. diff --git a/core/modules/node/src/NodeStorageInterface.php b/core/modules/node/src/NodeStorageInterface.php index 4b9fb585b..803edc372 100644 --- a/core/modules/node/src/NodeStorageInterface.php +++ b/core/modules/node/src/NodeStorageInterface.php @@ -7,14 +7,14 @@ namespace Drupal\node; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Session\AccountInterface; /** * Defines an interface for node entity storage classes. */ -interface NodeStorageInterface extends EntityStorageInterface { +interface NodeStorageInterface extends ContentEntityStorageInterface { /** * Gets a list of node revision IDs for a specific node. diff --git a/core/modules/node/src/NodeViewsData.php b/core/modules/node/src/NodeViewsData.php index 3c0d50956..895008ef5 100644 --- a/core/modules/node/src/NodeViewsData.php +++ b/core/modules/node/src/NodeViewsData.php @@ -240,7 +240,7 @@ class NodeViewsData extends EntityViewsData { 'title' => t('Content'), 'label' => t('Get the actual content from a content revision.'), ), - ) + $data['node_revision']['vid']; + ) + $data['node_field_revision']['vid']; $data['node_field_revision']['langcode']['help'] = t('The language the original content is in.'); diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_nid_argument.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_nid_argument.yml index b61f1ad91..f52109371 100644 --- a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_nid_argument.yml +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_nid_argument.yml @@ -56,7 +56,7 @@ display: fields: nid: id: nid - table: node + table: node_field_data field: nid relationship: none group_type: group diff --git a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_display_field.yml b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_display_field.yml index ab5092056..7699fb14d 100644 --- a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_display_field.yml +++ b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_display_field.yml @@ -41,7 +41,7 @@ display: fields: nid: id: nid - table: node + table: node_field_data field: nid plugin_id: field entity_type: node diff --git a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_exposed_filter.yml b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_exposed_filter.yml index b5af1e96b..6165641ad 100644 --- a/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_exposed_filter.yml +++ b/core/modules/rest/tests/modules/rest_test_views/test_views/views.view.test_serializer_node_exposed_filter.yml @@ -41,7 +41,7 @@ display: fields: nid: id: nid - table: node + table: node_field_data field: nid plugin_id: field entity_type: node diff --git a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php index 07d0bd7b5..a2e9be4c1 100644 --- a/core/modules/system/src/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/src/Tests/Entity/EntityTranslationTest.php @@ -328,6 +328,7 @@ class EntityTranslationTest extends EntityLanguageTestBase { // Verify that we obtain the entity object itself when we attempt to // retrieve a translation referring to it. $translation = $entity->getTranslation(LanguageInterface::LANGCODE_NOT_SPECIFIED); + $this->assertFalse($translation->isNewTranslation(), 'Existing translations are not marked as new.'); $this->assertIdentical($entity, $translation, 'The translation object corresponding to a non-default language is the entity object itself when the entity is language-neutral.'); $entity->{$langcode_key}->value = $default_langcode; $translation = $entity->getTranslation($default_langcode); @@ -353,6 +354,7 @@ class EntityTranslationTest extends EntityLanguageTestBase { $entity->name->value = $name; $name_translated = $langcode . '_' . $this->randomMachineName(); $translation = $entity->addTranslation($langcode); + $this->assertTrue($translation->isNewTranslation(), 'Newly added translations are marked as new.'); $this->assertNotIdentical($entity, $translation, 'The entity and the translation object differ from one another.'); $this->assertTrue($entity->hasTranslation($langcode), 'The new translation exists.'); $this->assertEqual($translation->language()->getId(), $langcode, 'The translation language matches the specified one.'); diff --git a/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module b/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module index d8c3666f1..ed0599605 100644 --- a/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module +++ b/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module @@ -11,7 +11,7 @@ function keyvalue_test_entity_type_alter(array &$entity_types) { /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ if (isset($entity_types['entity_test_label'])) { - $entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage'); + $entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueContentEntityStorage'); $entity_keys = $entity_types['entity_test_label']->getKeys(); $entity_types['entity_test_label']->set('entity_keys', $entity_keys + array('uuid' => 'uuid')); } diff --git a/core/modules/taxonomy/src/TermStorageInterface.php b/core/modules/taxonomy/src/TermStorageInterface.php index a002f9c5b..56e1a5c40 100644 --- a/core/modules/taxonomy/src/TermStorageInterface.php +++ b/core/modules/taxonomy/src/TermStorageInterface.php @@ -8,12 +8,12 @@ namespace Drupal\taxonomy; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; /** * Defines an interface for taxonomy_term entity storage classes. */ -interface TermStorageInterface extends EntityStorageInterface { +interface TermStorageInterface extends ContentEntityStorageInterface { /** * Removed reference to terms from term_hierarchy. diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_groupwise_term.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_groupwise_term.yml index a802762bb..5ba97e0fa 100644 --- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_groupwise_term.yml +++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_groupwise_term.yml @@ -59,7 +59,7 @@ display: subquery_namespace: '' subquery_order: DESC subquery_regenerate: true - subquery_sort: node.nid + subquery_sort: node_field_data.nid subquery_view: '' table: taxonomy_term_field_data plugin_id: groupwise_max diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml index 96d0559dc..80295b6ea 100644 --- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml +++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml @@ -156,7 +156,7 @@ display: plugin_id: field nid: id: nid - table: node + table: node_field_data field: nid entity_type: node entity_field: nid diff --git a/core/modules/user/src/UserStorageInterface.php b/core/modules/user/src/UserStorageInterface.php index 2614bc34d..fed130071 100644 --- a/core/modules/user/src/UserStorageInterface.php +++ b/core/modules/user/src/UserStorageInterface.php @@ -7,13 +7,13 @@ namespace Drupal\user; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\ContentEntityStorageInterface; use Drupal\Core\Session\AccountInterface; /** * Defines an interface for user entity storage classes. */ -interface UserStorageInterface extends EntityStorageInterface{ +interface UserStorageInterface extends ContentEntityStorageInterface { /** * Update the last login timestamp of the user. diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php index 20692719b..bc355f9c9 100644 --- a/core/modules/views/src/EntityViewsData.php +++ b/core/modules/views/src/EntityViewsData.php @@ -116,11 +116,29 @@ class EntityViewsData implements EntityHandlerInterface, EntityViewsDataInterfac public function getViewsData() { $data = []; - $base_table = $this->entityType->getBaseTable(); + $base_table = $this->entityType->getBaseTable() ?: $this->entityType->id(); + $revisionable = $this->entityType->isRevisionable(); $base_field = $this->entityType->getKey('id'); - $data_table = $this->entityType->getDataTable(); - $revision_table = $this->entityType->getRevisionTable(); - $revision_data_table = $this->entityType->getRevisionDataTable(); + + $revision_table = ''; + if ($revisionable) { + $revision_table = $this->entityType->getRevisionTable() ?: $this->entityType->id() . '_revision'; + } + + $translatable = $this->entityType->isTranslatable(); + $data_table = ''; + if ($translatable) { + $data_table = $this->entityType->getDataTable() ?: $this->entityType->id() . '_field_data'; + } + + // Some entity types do not have a revision data table defined, but still + // have a revision table name set in + // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout() so we + // apply the same kind of logic. + $revision_data_table = ''; + if ($revisionable && $translatable) { + $revision_data_table = $this->entityType->getRevisionDataTable() ?: $this->entityType->id() . '_field_revision'; + } $revision_field = $this->entityType->getKey('revision'); // Setup base information of the views data. @@ -213,6 +231,10 @@ class EntityViewsData implements EntityHandlerInterface, EntityViewsDataInterfac // the entity base, revision, data tables. $field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityType->id()); if ($table_mapping = $this->storage->getTableMapping()) { + // Fetch all fields that can appear in both the base table and the data + // table. + $entity_keys = $this->entityType->getKeys(); + $duplicate_fields = array_intersect_key($entity_keys, array_flip(['id', 'revision', 'bundle'])); // Iterate over each table we have so far and collect field data for each. // Based on whether the field is in the field_definitions provided by the // entity manager. @@ -221,6 +243,12 @@ class EntityViewsData implements EntityHandlerInterface, EntityViewsDataInterfac // @todo https://www.drupal.org/node/2337511 foreach ($table_mapping->getTableNames() as $table) { foreach ($table_mapping->getFieldNames($table) as $field_name) { + // To avoid confusing duplication in the user interface, for fields + // that are on both base and data tables, only add them on the data + // table (same for revision vs. revision data). + if ($data_table && ($table === $base_table || $table === $revision_table) && in_array($field_name, $duplicate_fields)) { + continue; + } $this->mapFieldDefinition($table, $field_name, $field_definitions[$field_name], $table_mapping, $data[$table]); } } diff --git a/core/modules/views/src/Tests/Update/FieldHandlersUpdateTest.php b/core/modules/views/src/Tests/Update/FieldHandlersUpdateTest.php new file mode 100644 index 000000000..c5ccd93b4 --- /dev/null +++ b/core/modules/views/src/Tests/Update/FieldHandlersUpdateTest.php @@ -0,0 +1,48 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../tests/fixtures/update/duplicate-field-handler.php', + ]; + } + + /** + * Tests that field handlers are updated properly. + */ + public function testViewsUpdate8004() { + $this->runUpdates(); + + // Load and initialize our test view. + $view = View::load('test_duplicate_field_handlers'); + $data = $view->toArray(); + // Check that the field is using the expected base table. + $this->assertEqual('node_field_data', $data['display']['default']['display_options']['fields']['nid']['table']); + $this->assertEqual('node_field_data', $data['display']['default']['display_options']['filters']['type']['table']); + $this->assertEqual('node_field_data', $data['display']['default']['display_options']['sorts']['vid']['table']); + $this->assertEqual('node_field_data', $data['display']['default']['display_options']['arguments']['nid']['table']); + } + +} diff --git a/core/modules/views/tests/fixtures/update/duplicate-field-handler.php b/core/modules/views/tests/fixtures/update/duplicate-field-handler.php new file mode 100644 index 000000000..2b78f123b --- /dev/null +++ b/core/modules/views/tests/fixtures/update/duplicate-field-handler.php @@ -0,0 +1,11 @@ +insert('config') + ->fields(array( + 'collection' => '', + 'name' => 'views.view.test_duplicate_field_handlers', + 'data' => serialize(\Drupal\Component\Serialization\Yaml::decode(file_get_contents('core/modules/views/tests/modules/views_test_config/test_views/views.view.test_duplicate_field_handlers.yml'))), + )) + ->execute(); diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_duplicate_field_handlers.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_duplicate_field_handlers.yml new file mode 100644 index 000000000..41a9dfa0c --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_duplicate_field_handlers.yml @@ -0,0 +1,246 @@ +langcode: en +status: true +dependencies: + config: + - node.type.article + module: + - node + - user +id: test_duplicate_field_handlers +label: 'Test Duplicate Field Handlers' +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: none + options: + items_per_page: null + offset: 0 + style: + type: default + row: + type: fields + fields: + nid: + id: nid + table: node + field: nid + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: nid + plugin_id: field + filters: + type: + id: type + table: node + field: type + relationship: none + group_type: group + admin_label: '' + operator: in + value: + article: article + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + vid: + id: vid + table: node + field: vid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: node + entity_field: vid + plugin_id: standard + title: 'Test Duplicate Field Handlers' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + nid: + id: nid + table: node + field: nid + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + 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: numeric + display_extenders: { } + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - '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-duplicate-field-handlers + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + cacheable: false diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_row_render_cache.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_row_render_cache.yml index ec19b13ee..2a79e74b1 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_row_render_cache.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_row_render_cache.yml @@ -130,7 +130,7 @@ display: fields: nid: id: nid - table: node + table: node_field_data field: nid relationship: none group_type: group diff --git a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php index dfdc0bd4e..feefe0aeb 100644 --- a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php +++ b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php @@ -93,7 +93,12 @@ class EntityViewsDataTest extends UnitTestCase { 'base_table' => 'entity_test', 'id' => 'entity_test', 'label' => 'Entity test', - 'entity_keys' => ['id' => 'id', 'langcode' => 'langcode'], + 'entity_keys' => [ + 'id' => 'id', + 'langcode' => 'langcode', + 'bundle' => 'type', + 'revision' => 'revision_id', + ], 'provider' => 'entity_test', 'list_cache_contexts' => ['entity_test_list_cache_context'], ]); @@ -190,6 +195,7 @@ class EntityViewsDataTest extends UnitTestCase { $entity_type = $this->baseEntityType ->set('data_table', 'entity_test_mul_property_data') ->set('id', 'entity_test_mul') + ->set('translatable', TRUE) ->setKey('label', 'label'); $this->viewsData->setEntityType($entity_type); @@ -259,6 +265,7 @@ class EntityViewsDataTest extends UnitTestCase { ->set('revision_table', 'entity_test_mulrev_revision') ->set('revision_data_table', 'entity_test_mulrev_property_revision') ->set('id', 'entity_test_mulrev') + ->set('translatable', TRUE) ->setKey('revision', 'revision_id') ; $this->viewsData->setEntityType($entity_type); @@ -297,6 +304,7 @@ class EntityViewsDataTest extends UnitTestCase { ->set('revision_table', 'entity_test_mulrev_revision') ->set('revision_data_table', 'entity_test_mulrev_property_revision') ->set('id', 'entity_test_mulrev') + ->set('translatable', TRUE) ->setKey('revision', 'revision_id') ; $this->viewsData->setEntityType($entity_type); @@ -316,7 +324,7 @@ class EntityViewsDataTest extends UnitTestCase { $revision_data = $data['entity_test_mulrev_property_revision']; $this->assertCount(2, $revision_data['table']['join']); $this->assertEquals([ - 'entity_test' => ['left_field' => 'revision_id', 'field' => 'revision_id', 'type' => 'INNER'], + 'entity_test_mulrev_field_data' => ['left_field' => 'revision_id', 'field' => 'revision_id', 'type' => 'INNER'], 'entity_test_mulrev_revision' => ['left_field' => 'revision_id', 'field' => 'revision_id', 'type' => 'INNER'], ], $revision_data['table']['join']); $this->assertFalse(isset($data['data_table'])); @@ -526,10 +534,19 @@ class EntityViewsDataTest extends UnitTestCase { $table_mapping->expects($this->any()) ->method('getFieldNames') ->willReturnMap([ - ['entity_test_mul', ['id', 'uuid', 'type', 'langcode']], - ['entity_test_mul_property_data', ['id', 'langcode', 'name', 'description', 'homepage', 'user_id']], + ['entity_test_mul', ['uuid']], + ['entity_test_mul_property_data', ['id', 'type', 'langcode', 'name', 'description', 'homepage', 'user_id']], ]); + $table_mapping->expects($this->any()) + ->method('getFieldTableName') + ->willReturnCallback(function($field) { + if ($field == 'uuid') { + return 'entity_test_mul'; + } + return 'entity_test_mul_property_data'; + }); + $this->entityStorage->expects($this->once()) ->method('getTableMapping') ->willReturn($table_mapping); @@ -547,17 +564,13 @@ class EntityViewsDataTest extends UnitTestCase { $data = $this->viewsData->getViewsData(); // Check the base fields. - $this->assertNumericField($data['entity_test_mul']['id']); - $this->assertField($data['entity_test_mul']['id'], 'id'); + $this->assertFalse(isset($data['entity_test_mul']['id'])); + $this->assertFalse(isset($data['entity_test_mul']['type'])); $this->assertUuidField($data['entity_test_mul']['uuid']); $this->assertField($data['entity_test_mul']['uuid'], 'uuid'); - $this->assertBundleField($data['entity_test_mul']['type']); - $this->assertField($data['entity_test_mul']['type'], 'type'); $this->assertFalse(isset($data['entity_test_mul']['type']['relationship'])); - $this->assertLanguageField($data['entity_test_mul']['langcode']); - $this->assertField($data['entity_test_mul']['langcode'], 'langcode'); // Also ensure that field_data only fields don't appear on the base table. $this->assertFalse(isset($data['entity_test_mul']['name'])); $this->assertFalse(isset($data['entity_test_mul']['description'])); @@ -570,6 +583,9 @@ class EntityViewsDataTest extends UnitTestCase { $this->assertNumericField($data['entity_test_mul_property_data']['id']); $this->assertField($data['entity_test_mul_property_data']['id'], 'id'); + $this->assertBundleField($data['entity_test_mul_property_data']['type']); + $this->assertField($data['entity_test_mul_property_data']['type'], 'type'); + $this->assertLanguageField($data['entity_test_mul_property_data']['langcode']); $this->assertField($data['entity_test_mul_property_data']['langcode'], 'langcode'); $this->assertEquals('Translation language', $data['entity_test_mul_property_data']['langcode']['title']); @@ -600,7 +616,8 @@ class EntityViewsDataTest extends UnitTestCase { ->set('revision_table', 'entity_test_mulrev_revision') ->set('data_table', 'entity_test_mulrev_property_data') ->set('revision_data_table', 'entity_test_mulrev_property_revision') - ->set('id', 'entity_test_mulrev'); + ->set('id', 'entity_test_mulrev') + ->set('translatable', TRUE); $base_field_definitions = $this->setupBaseFields(EntityTestMulRev::baseFieldDefinitions($this->baseEntityType)); $user_base_field_definitions = [ 'uid' => BaseFieldDefinition::create('integer') @@ -645,6 +662,15 @@ class EntityViewsDataTest extends UnitTestCase { ['entity_test_mulrev_property_revision', ['id', 'revision_id', 'langcode', 'name', 'description', 'homepage', 'user_id']], ]); + $table_mapping->expects($this->any()) + ->method('getFieldTableName') + ->willReturnCallback(function($field) { + if ($field == 'uuid') { + return 'entity_test_mulrev'; + } + return 'entity_test_mulrev_property_data'; + }); + $this->entityStorage->expects($this->once()) ->method('getTableMapping') ->willReturn($table_mapping); @@ -654,14 +680,11 @@ class EntityViewsDataTest extends UnitTestCase { $data = $this->viewsData->getViewsData(); // Check the base fields. - $this->assertNumericField($data['entity_test_mulrev']['id']); - $this->assertField($data['entity_test_mulrev']['id'], 'id'); - $this->assertNumericField($data['entity_test_mulrev']['revision_id']); - $this->assertField($data['entity_test_mulrev']['revision_id'], 'revision_id'); + $this->assertFalse(isset($data['entity_test_mulrev']['id'])); + $this->assertFalse(isset($data['entity_test_mulrev']['type'])); + $this->assertFalse(isset($data['entity_test_mulrev']['revision_id'])); $this->assertUuidField($data['entity_test_mulrev']['uuid']); $this->assertField($data['entity_test_mulrev']['uuid'], 'uuid'); - $this->assertStringField($data['entity_test_mulrev']['type']); - $this->assertField($data['entity_test_mulrev']['type'], 'type'); // Also ensure that field_data only fields don't appear on the base table. $this->assertFalse(isset($data['entity_test_mulrev']['name'])); @@ -672,17 +695,12 @@ class EntityViewsDataTest extends UnitTestCase { $this->assertFalse(isset($data['entity_test_mulrev']['langcode'])); $this->assertFalse(isset($data['entity_test_mulrev']['user_id'])); - // Check the revision fields. - $this->assertNumericField($data['entity_test_mulrev_revision']['id']); - $this->assertField($data['entity_test_mulrev_revision']['id'], 'id'); - $this->assertNumericField($data['entity_test_mulrev_revision']['revision_id']); - $this->assertField($data['entity_test_mulrev_revision']['revision_id'], 'revision_id'); - - $this->assertLanguageField($data['entity_test_mulrev_revision']['langcode']); - $this->assertField($data['entity_test_mulrev_revision']['langcode'], 'langcode'); - $this->assertEquals('Original language', $data['entity_test_mulrev_revision']['langcode']['title']); + // Check the revision fields. The revision ID should only appear in the data + // table. + $this->assertFalse(isset($data['entity_test_mulrev_revision']['revision_id'])); // Also ensure that field_data only fields don't appear on the revision table. + $this->assertFalse(isset($data['entity_test_mulrev_revision']['id'])); $this->assertFalse(isset($data['entity_test_mulrev_revision']['name'])); $this->assertFalse(isset($data['entity_test_mulrev_revision']['description'])); $this->assertFalse(isset($data['entity_test_mulrev_revision']['description__value'])); @@ -693,6 +711,8 @@ class EntityViewsDataTest extends UnitTestCase { // Check the data fields. $this->assertNumericField($data['entity_test_mulrev_property_data']['id']); $this->assertField($data['entity_test_mulrev_property_data']['id'], 'id'); + $this->assertNumericField($data['entity_test_mulrev_property_data']['revision_id']); + $this->assertField($data['entity_test_mulrev_property_data']['revision_id'], 'revision_id'); $this->assertLanguageField($data['entity_test_mulrev_property_data']['langcode']); $this->assertField($data['entity_test_mulrev_property_data']['langcode'], 'langcode'); $this->assertStringField($data['entity_test_mulrev_property_data']['name']); diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index 5d248a5c7..fea4f7e7d 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -5,6 +5,9 @@ * Post update functions for Views. */ +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\views\Views; + /** * @addtogroup updates-8.0.0-beta * @{ @@ -31,6 +34,99 @@ function views_post_update_update_cacheability_metadata() { } +/** + * Update some views fields that were previously duplicated. + */ +function views_post_update_cleanup_duplicate_views_data() { + $config_factory = \Drupal::configFactory(); + $ids = []; + $message = NULL; + $data_tables = []; + $base_tables = []; + $revision_tables = []; + $entities_by_table = []; + $duplicate_fields = []; + $handler_types = Views::getHandlerTypes(); + + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ + $entity_type_manager = \Drupal::service('entity_type.manager'); + // This will allow us to create an index of all entity types of the site. + foreach ($entity_type_manager->getDefinitions() as $entity_type_id => $entity_type) { + // Store the entity keyed by base table. If it has a data table, use that as + // well. + if ($data_table = $entity_type->getDataTable()) { + $entities_by_table[$data_table] = $entity_type; + } + if ($base_table = $entity_type->getBaseTable()) { + $entities_by_table[$base_table] = $entity_type; + } + + // The following code basically contains the same kind of logic as + // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout() to + // prefetch all tables (base, data, revision, and revision data). + $base_tables[$entity_type_id] = $entity_type->getBaseTable() ?: $entity_type->id(); + $revisionable = $entity_type->isRevisionable(); + + $revision_table = ''; + if ($revisionable) { + $revision_table = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision'; + } + $revision_tables[$entity_type_id] = $revision_table; + + $translatable = $entity_type->isTranslatable(); + $data_table = ''; + // For example the data table just exists, when the entity type is + // translatable. + if ($translatable) { + $data_table = $entity_type->getDataTable() ?: $entity_type->id() . '_field_data'; + } + $data_tables[$entity_type_id] = $data_table; + + $duplicate_fields[$entity_type_id] = array_intersect_key($entity_type->getKeys(), array_flip(['id', 'revision', 'bundle'])); + } + + foreach ($config_factory->listAll('views.view.') as $view_config_name) { + $changed = FALSE; + $view = $config_factory->getEditable($view_config_name); + + $displays = $view->get('display'); + if (isset($entities_by_table[$view->get('base_table')])) { + $entity_type = $entities_by_table[$view->get('base_table')]; + $entity_type_id = $entity_type->id(); + $data_table = $data_tables[$entity_type_id]; + $base_table = $base_tables[$entity_type_id]; + $revision_table = $revision_tables[$entity_type_id]; + + if ($data_table) { + foreach ($displays as $display_name => &$display) { + foreach ($handler_types as $handler_type) { + if (!empty($display['display_options'][$handler_type['plural']])) { + foreach ($display['display_options'][$handler_type['plural']] as $field_name => &$field) { + $table = $field['table']; + if (($table === $base_table || $table === $revision_table) && in_array($field_name, $duplicate_fields[$entity_type_id])) { + $field['table'] = $data_table; + $changed = TRUE; + } + } + } + } + } + } + } + + if ($changed) { + $view->set('display', $displays); + $view->save(); + $ids[] = $view->get('id'); + } + } + if (!empty($ids)) { + $message = new TranslatableMarkup('Updated tables for field handlers for views: @ids', ['@ids' => implode(', ', array_unique($ids))]); + } + + return $message; +} + /** * @} End of "addtogroup updates-8.0.0-beta". */ diff --git a/core/modules/views_ui/src/Tests/HandlerTest.php b/core/modules/views_ui/src/Tests/HandlerTest.php index 8bfc6867f..214397a68 100644 --- a/core/modules/views_ui/src/Tests/HandlerTest.php +++ b/core/modules/views_ui/src/Tests/HandlerTest.php @@ -10,6 +10,7 @@ namespace Drupal\views_ui\Tests; use Drupal\Component\Utility\SafeMarkup; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\views\Tests\ViewTestData; use Drupal\views\ViewExecutable; /** @@ -20,12 +21,17 @@ use Drupal\views\ViewExecutable; */ class HandlerTest extends UITestBase { + /** + * {@inheritdoc} + */ + public static $modules = array('node_test_views'); + /** * Views used by this test. * * @var array */ - public static $testViews = array('test_view_empty', 'test_view_broken', 'node'); + public static $testViews = array('test_view_empty', 'test_view_broken', 'node', 'test_node_view'); /** * {@inheritdoc} @@ -34,6 +40,7 @@ class HandlerTest extends UITestBase { parent::setUp(); $this->drupalPlaceBlock('page_title_block'); + ViewTestData::createTestViews(get_class($this), array('node_test_views')); } /** @@ -218,4 +225,37 @@ class HandlerTest extends UITestBase { } } + /** + * Ensures that neither node type or node ID appears multiple times. + * + * @see \Drupal\views\EntityViewsData + */ + public function testNoDuplicateFields() { + $handler_types = ['field', 'filter', 'sort', 'argument']; + + foreach ($handler_types as $handler_type) { + $add_handler_url = 'admin/structure/views/nojs/add-handler/test_node_view/default/' . $handler_type; + $this->drupalGet($add_handler_url); + + $this->assertNoDuplicateField('Node ID', 'Content'); + $this->assertNoDuplicateField('Node ID', 'Content revision'); + $this->assertNoDuplicateField('Type', 'Content'); + $this->assertNoDuplicateField('UUID', 'Content'); + $this->assertNoDuplicateField('Revision ID', 'Content'); + $this->assertNoDuplicateField('Revision ID', 'Content revision'); + } + } + + /** + * Asserts that fields only appear once. + * + * @param string $field_name + * The field name. + * @param string $entity_type + * The entity type to which the field belongs. + */ + public function assertNoDuplicateField($field_name, $entity_type) { + $elements = $this->xpath('//td[.=:entity_type]/preceding-sibling::td[@class="title" and .=:title]', [':title' => $field_name, ':entity_type' => $entity_type]); + $this->assertEqual(1, count($elements), $field_name . ' appears just once in ' . $entity_type . '.'); + } } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 1a6d14800..0d573e496 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -699,6 +699,17 @@ $settings['container_yamls'][] = __DIR__ . '/services.yml'; * example.org, with all subdomains included. */ +/** + * Include the Pantheon-specific settings file. + * + * n.b. The settings.pantheon.php file makes some changes + * that affect all envrionments that this site + * exists in. Always include this file, even in + * a local development environment, to insure that + * the site settings remain consistent. + */ +include __DIR__ . "/settings.pantheon.php"; + /** * Load local development override configuration, if available. * @@ -709,6 +720,6 @@ $settings['container_yamls'][] = __DIR__ . '/services.yml'; * * Keep this code block at the end of this file to take full effect. */ -# if (file_exists(__DIR__ . '/settings.local.php')) { -# include __DIR__ . '/settings.local.php'; -# } +if (file_exists(__DIR__ . '/settings.local.php')) { + include __DIR__ . '/settings.local.php'; +} diff --git a/sites/default/settings.pantheon.php b/sites/default/settings.pantheon.php index a3ab25cd4..6d5aadc2e 100644 --- a/sites/default/settings.pantheon.php +++ b/sites/default/settings.pantheon.php @@ -47,20 +47,6 @@ else { ); } -/** - * Override the $install_state variable to let Drupal know that the settings are verified - * since they are being passed directly by the Pantheon. - * - * Issue: https://github.com/pantheon-systems/drops-8/issues/9 - * - */ -if ( - isset($_ENV['PANTHEON_ENVIRONMENT']) && - $is_installer_url && - (php_sapi_name() != "cli") -) { - $GLOBALS['install_state']['settings_verified'] = TRUE; -} /** * Allow Drupal 8 to Cleanly Redirect to Install.php For New Sites. @@ -73,7 +59,7 @@ if ( if ( isset($_ENV['PANTHEON_ENVIRONMENT']) && !$is_installer_url && - (!is_dir(__DIR__ . '/files/styles')) && + (isset($_SERVER['PANTHEON_DATABASE_STATE']) && ($_SERVER['PANTHEON_DATABASE_STATE'] == 'empty')) && (empty($GLOBALS['install_state'])) && (php_sapi_name() != "cli") ) {