diff --git a/frontend/drupal9/web/core/MAINTAINERS.txt b/frontend/drupal9/web/core/MAINTAINERS.txt index e8ab8076c..ab0f57697 100644 --- a/frontend/drupal9/web/core/MAINTAINERS.txt +++ b/frontend/drupal9/web/core/MAINTAINERS.txt @@ -534,6 +534,10 @@ Decoupled Menus Initiative Media Initiative - Janez Urevc 'slashrsm' https://www.drupal.org/u/slashrsm +Project Browser Initiative +- Leslie Glynn 'leslieg' https://www.drupal.org/u/leslieg +- Chris Wells 'chrisfromredfin' https://www.drupal.org/u/chrisfromredfin + Core mentoring coordinators --------------------------- diff --git a/frontend/drupal9/web/core/includes/common.inc b/frontend/drupal9/web/core/includes/common.inc index ed359612c..2bee0e554 100644 --- a/frontend/drupal9/web/core/includes/common.inc +++ b/frontend/drupal9/web/core/includes/common.inc @@ -558,7 +558,9 @@ function drupal_flush_all_caches($kernel = NULL) { // Wipe the Twig PHP Storage cache. \Drupal::service('twig')->invalidate(); - // Rebuild theme data that is stored in state. + // Rebuild profile, profile, theme_engine and theme data. + \Drupal::service('extension.list.profile')->reset(); + \Drupal::service('extension.list.theme_engine')->reset(); \Drupal::service('theme_handler')->refreshInfo(); // In case the active theme gets requested later in the same request we need // to reset the theme manager. diff --git a/frontend/drupal9/web/core/lib/Drupal.php b/frontend/drupal9/web/core/lib/Drupal.php index 9e903214b..c2d79f15a 100644 --- a/frontend/drupal9/web/core/lib/Drupal.php +++ b/frontend/drupal9/web/core/lib/Drupal.php @@ -75,7 +75,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '9.4.1'; + const VERSION = '9.4.2'; /** * Core API compatibility. diff --git a/frontend/drupal9/web/core/lib/Drupal/Core/Composer/Composer.php b/frontend/drupal9/web/core/lib/Drupal/Core/Composer/Composer.php index fc7ce25cd..4f7fcda1d 100644 --- a/frontend/drupal9/web/core/lib/Drupal/Core/Composer/Composer.php +++ b/frontend/drupal9/web/core/lib/Drupal/Core/Composer/Composer.php @@ -130,13 +130,6 @@ class Composer { $vendor_dir . '/symfony/http-kernel/TerminableInterface.php', ]); } - if ($repository->findPackage('symfony/http-kernel', $constraint)) { - $autoload['classmap'] = array_merge($autoload['classmap'], [ - $vendor_dir . '/symfony/http-kernel/HttpKernel.php', - $vendor_dir . '/symfony/http-kernel/HttpKernelInterface.php', - $vendor_dir . '/symfony/http-kernel/TerminableInterface.php', - ]); - } if ($repository->findPackage('symfony/dependency-injection', $constraint)) { $autoload['classmap'] = array_merge($autoload['classmap'], [ $vendor_dir . '/symfony/dependency-injection/ContainerAwareInterface.php', diff --git a/frontend/drupal9/web/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php b/frontend/drupal9/web/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php index 495228396..a0ba7f022 100644 --- a/frontend/drupal9/web/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php +++ b/frontend/drupal9/web/core/lib/Drupal/Core/DependencyInjection/Compiler/DependencySerializationTraitPass.php @@ -16,12 +16,34 @@ class DependencySerializationTraitPass implements CompilerPassInterface { * {@inheritdoc} */ public function process(ContainerBuilder $container) { + $decorations = new \SplPriorityQueue(); + $order = PHP_INT_MAX; + foreach ($container->getDefinitions() as $service_id => $definition) { // Only add the property to services that are public (as private services // can not be reloaded through Container::get()) and are objects. if (!$definition->hasTag('parameter_service') && $definition->isPublic()) { $definition->setProperty('_serviceId', $service_id); } + + if ($decorated = $definition->getDecoratedService()) { + $decorations->insert([$service_id, $definition], [$decorated[2], --$order]); + } + } + + foreach ($decorations as list($service_id, $definition)) { + list($inner, $renamedId) = $definition->getDecoratedService(); + if (!$renamedId) { + $renamedId = $service_id . '.inner'; + } + + $original = $container->getDefinition($inner); + if ($original->isPublic()) { + // The old service is renamed. + $original->setProperty('_serviceId', $renamedId); + // The decorating service takes over the old ID. + $definition->setProperty('_serviceId', $inner); + } } } diff --git a/frontend/drupal9/web/core/lib/Drupal/Core/Site/Settings.php b/frontend/drupal9/web/core/lib/Drupal/Core/Site/Settings.php index 31d494f8b..5509e2be2 100644 --- a/frontend/drupal9/web/core/lib/Drupal/Core/Site/Settings.php +++ b/frontend/drupal9/web/core/lib/Drupal/Core/Site/Settings.php @@ -207,7 +207,7 @@ final class Settings { // the database. Therefore, allow the connection info to specify an // autoload directory for the driver. if (isset($info['autoload'])) { - $class_loader->addPsr4($info['namespace'] . '\\', $info['autoload']); + $class_loader->addPsr4($info['namespace'] . '\\', $app_root . '/' . $info['autoload']); } } } diff --git a/frontend/drupal9/web/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml b/frontend/drupal9/web/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml index b7c670a68..c58dce3fe 100644 --- a/frontend/drupal9/web/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml +++ b/frontend/drupal9/web/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml @@ -127,7 +127,7 @@ ckeditor5.plugin.ckeditor5_list: # Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media ckeditor5.plugin.media_media: type: mapping - label: List + label: Media mapping: allow_view_mode_override: type: boolean diff --git a/frontend/drupal9/web/core/modules/config/src/Form/ConfigSingleExportForm.php b/frontend/drupal9/web/core/modules/config/src/Form/ConfigSingleExportForm.php index 22e0b4837..dc46b4581 100644 --- a/frontend/drupal9/web/core/modules/config/src/Form/ConfigSingleExportForm.php +++ b/frontend/drupal9/web/core/modules/config/src/Form/ConfigSingleExportForm.php @@ -136,7 +136,7 @@ class ConfigSingleExportForm extends FormBase { */ public function updateConfigurationType($form, FormStateInterface $form_state) { $form['config_name']['#options'] = $this->findConfiguration($form_state->getValue('config_type')); - unset($form['export']['#value']); + $form['export']['#value'] = NULL; return $form; } diff --git a/frontend/drupal9/web/core/modules/config/tests/config_override_test/src/ConfigOverrider.php b/frontend/drupal9/web/core/modules/config/tests/config_override_test/src/ConfigOverrider.php index c8ffc694b..8ec572e3a 100644 --- a/frontend/drupal9/web/core/modules/config/tests/config_override_test/src/ConfigOverrider.php +++ b/frontend/drupal9/web/core/modules/config/tests/config_override_test/src/ConfigOverrider.php @@ -4,6 +4,7 @@ namespace Drupal\config_override_test; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\ConfigFactoryOverrideInterface; +use Drupal\Core\Config\StorageInterface; /** * Tests module overrides for configuration. diff --git a/frontend/drupal9/web/core/modules/field/src/Plugin/migrate/process/d6/FieldInstanceSettings.php b/frontend/drupal9/web/core/modules/field/src/Plugin/migrate/process/d6/FieldInstanceSettings.php index be41a4209..ca5741b17 100644 --- a/frontend/drupal9/web/core/modules/field/src/Plugin/migrate/process/d6/FieldInstanceSettings.php +++ b/frontend/drupal9/web/core/modules/field/src/Plugin/migrate/process/d6/FieldInstanceSettings.php @@ -49,7 +49,7 @@ class FieldInstanceSettings extends ProcessPluginBase { case 'imagefield_widget': $settings['file_extensions'] = $widget_settings['file_extensions']; $settings['file_directory'] = $widget_settings['file_path']; - $settings['max_filesize'] = $this->convertSizeUnit($widget_settings['max_filesize_per_file']); + $settings['max_filesize'] = $this->convertSizeUnit($widget_settings['max_filesize_per_file'] ?? ''); $settings['alt_field'] = $widget_settings['alt']; $settings['alt_field_required'] = $widget_settings['custom_alt']; $settings['title_field'] = $widget_settings['title']; diff --git a/frontend/drupal9/web/core/modules/field/tests/src/Unit/Plugin/migrate/process/d6/FieldInstanceSettingsTest.php b/frontend/drupal9/web/core/modules/field/tests/src/Unit/Plugin/migrate/process/d6/FieldInstanceSettingsTest.php new file mode 100644 index 000000000..27f830a61 --- /dev/null +++ b/frontend/drupal9/web/core/modules/field/tests/src/Unit/Plugin/migrate/process/d6/FieldInstanceSettingsTest.php @@ -0,0 +1,81 @@ +createMock(MigrationInterface::class); + $plugin = new FieldInstanceSettings([], 'd6_field_field_settings', [], $migration); + + $executable = $this->createMock(MigrateExecutableInterface::class); + $row = $this->getMockBuilder(Row::class) + ->disableOriginalConstructor() + ->getMock(); + + $result = $plugin->transform([ + $field_type, + $instance_settings, + NULL, + ], $executable, $row, 'foo'); + $this->assertSame($expected, $result); + } + + /** + * Provides field settings for testGetSettings(). + */ + public function getSettingsProvider() { + return [ + 'imagefield size set' => [ + 'imagefield_widget', + 'a:14:{s:15:"file_extensions";s:11:"gif jpg png";s:9:"file_path";N;s:18:"progress_indicator";N;s:21:"max_filesize_per_file";s:3:"10M";s:21:"max_filesize_per_node";N;s:14:"max_resolution";N;s:14:"min_resolution";N;s:3:"alt";N;s:10:"custom_alt";i:1;s:5:"title";N;s:12:"custom_title";i:1;s:10:"title_type";N;s:13:"default_image";N;s:17:"use_default_image";N;}', + [ + 'file_extensions' => 'gif jpg png', + 'file_directory' => NULL, + 'max_filesize' => '10MB', + 'alt_field' => NULL, + 'alt_field_required' => 1, + 'title_field' => NULL, + 'title_field_required' => 1, + 'max_resolution' => '', + 'min_resolution' => '', + ], + ], + 'imagefield size NULL' => [ + 'imagefield_widget', + 'a:14:{s:15:"file_extensions";s:11:"gif jpg png";s:9:"file_path";N;s:18:"progress_indicator";N;s:21:"max_filesize_per_file";N;s:21:"max_filesize_per_node";N;s:14:"max_resolution";N;s:14:"min_resolution";N;s:3:"alt";N;s:10:"custom_alt";i:1;s:5:"title";N;s:12:"custom_title";i:1;s:10:"title_type";N;s:13:"default_image";N;s:17:"use_default_image";N;}', + [ + 'file_extensions' => 'gif jpg png', + 'file_directory' => NULL, + 'max_filesize' => '', + 'alt_field' => NULL, + 'alt_field_required' => 1, + 'title_field' => NULL, + 'title_field_required' => 1, + 'max_resolution' => '', + 'min_resolution' => '', + ], + ], + + ]; + } + +} diff --git a/frontend/drupal9/web/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest.php b/frontend/drupal9/web/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest.php new file mode 100644 index 000000000..128c50ebe --- /dev/null +++ b/frontend/drupal9/web/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest.php @@ -0,0 +1,18 @@ +getResourceType()->getDeserializationTargetClass() === 'Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay' && $context['resource_object']->getField('third_party_settings') === $field) { + if (is_a($context['resource_object']->getResourceType()->getDeserializationTargetClass(), 'Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay', TRUE) && $context['resource_object']->getField('third_party_settings') === $field) { unset($field['layout_builder']['sections']); } diff --git a/frontend/drupal9/web/core/modules/layout_builder/layout_builder.module b/frontend/drupal9/web/core/modules/layout_builder/layout_builder.module index 6e8d0a25f..1646139aa 100644 --- a/frontend/drupal9/web/core/modules/layout_builder/layout_builder.module +++ b/frontend/drupal9/web/core/modules/layout_builder/layout_builder.module @@ -26,7 +26,6 @@ use Drupal\layout_builder\InlineBlockEntityOperations; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; -use Drupal\layout_builder\QuickEditIntegration; /** * Implements hook_help(). @@ -162,12 +161,6 @@ function layout_builder_entity_view_alter(array &$build, EntityInterface $entity if ($display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) { unset($build['#contextual_links']); } - - if (\Drupal::moduleHandler()->moduleExists('quickedit')) { - /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */ - $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class); - $quick_edit_integration->entityViewAlter($build, $entity, $display); - } } /** @@ -351,15 +344,6 @@ function layout_builder_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMa } } -/** - * Implements hook_quickedit_render_field(). - */ -function layout_builder_quickedit_render_field(EntityInterface $entity, $field_name, $view_mode_id, $langcode) { - /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */ - $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class); - return $quick_edit_integration->quickEditRenderField($entity, $field_name, $view_mode_id, $langcode); -} - /** * Implements hook_entity_translation_create(). */ diff --git a/frontend/drupal9/web/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/frontend/drupal9/web/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index d543fe161..c63b96ff0 100644 --- a/frontend/drupal9/web/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/frontend/drupal9/web/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -18,7 +18,6 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; -use Drupal\layout_builder\QuickEditIntegration; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; use Drupal\layout_builder\SectionListTrait; @@ -473,7 +472,7 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La * {@inheritdoc} */ public function getComponent($name) { - if ($this->isLayoutBuilderEnabled() && $section_component = $this->getQuickEditSectionComponent() ?: $this->getSectionComponentForFieldName($name)) { + if ($this->isLayoutBuilderEnabled() && $section_component = $this->getSectionComponentForFieldName($name)) { $plugin = $section_component->getPlugin(); if ($plugin instanceof ConfigurableInterface) { $configuration = $plugin->getConfiguration(); @@ -485,43 +484,6 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La return parent::getComponent($name); } - /** - * Returns the Quick Edit formatter settings. - * - * @return \Drupal\layout_builder\SectionComponent|null - * The section component if it is available. - * - * @see \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() - * @see \Drupal\quickedit\MetadataGenerator::generateFieldMetadata() - */ - private function getQuickEditSectionComponent() { - // To determine the Quick Edit view_mode ID we need an originalMode set. - if ($original_mode = $this->getOriginalMode()) { - $parts = explode('-', $original_mode); - // The Quick Edit view mode ID is created by - // \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() - // concatenating together the information we need to retrieve the Layout - // Builder component. It follows the structure prescribed by the - // documentation of hook_quickedit_render_field(). - if (count($parts) === 6 && $parts[0] === 'layout_builder') { - [, $delta, $component_uuid, $entity_id] = QuickEditIntegration::deconstructViewModeId($original_mode); - $entity = $this->entityTypeManager()->getStorage($this->getTargetEntityTypeId())->load($entity_id); - $sections = $this->getEntitySections($entity); - if (isset($sections[$delta])) { - $component = $sections[$delta]->getComponent($component_uuid); - $plugin = $component->getPlugin(); - // We only care about FieldBlock because these are only components - // that provide Quick Edit integration: Quick Edit enables in-place - // editing of fields of entities, not of anything else. - if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { - return $component; - } - } - } - } - return NULL; - } - /** * Gets the component for a given field name if any. * diff --git a/frontend/drupal9/web/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/frontend/drupal9/web/core/modules/layout_builder/src/InlineBlockEntityOperations.php index ad255cd6f..5b11bbe67 100644 --- a/frontend/drupal9/web/core/modules/layout_builder/src/InlineBlockEntityOperations.php +++ b/frontend/drupal9/web/core/modules/layout_builder/src/InlineBlockEntityOperations.php @@ -44,18 +44,12 @@ class InlineBlockEntityOperations implements ContainerInjectionInterface { /** * Constructs a new EntityOperations object. * - * @todo This constructor has one optional parameter, $section_storage_manager - * and one totally unused $database parameter. Deprecate the current - * constructor signature in https://www.drupal.org/node/3031492 after the - * general policy for constructor backwards compatibility is determined in - * https://www.drupal.org/node/3030640. - * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The entity type manager service. * @param \Drupal\layout_builder\InlineBlockUsageInterface $usage * Inline block usage tracking service. * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager - * (optional) The section storage manager. + * The section storage manager. */ public function __construct(EntityTypeManagerInterface $entityTypeManager, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) { $this->entityTypeManager = $entityTypeManager; diff --git a/frontend/drupal9/web/core/modules/layout_builder/src/QuickEditIntegration.php b/frontend/drupal9/web/core/modules/layout_builder/src/QuickEditIntegration.php index c087f9097..90c082ae8 100644 --- a/frontend/drupal9/web/core/modules/layout_builder/src/QuickEditIntegration.php +++ b/frontend/drupal9/web/core/modules/layout_builder/src/QuickEditIntegration.php @@ -2,21 +2,9 @@ namespace Drupal\layout_builder; -use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Cache\CacheableMetadata; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Logger\LoggerChannelTrait; -use Drupal\Core\Plugin\Context\Context; -use Drupal\Core\Plugin\Context\ContextDefinition; -use Drupal\Core\Plugin\Context\EntityContext; -use Drupal\Core\Render\Element; -use Drupal\Core\Session\AccountInterface; -use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +@trigger_error(__NAMESPACE__ . '\QuickEditIntegration is deprecated in drupal:9.4.2 and is removed from drupal:10.0.0. Instead, use \Drupal\quickedit\LayoutBuilderIntegration. See https://www.drupal.org/node/3265518', E_USER_DEPRECATED); + +use Drupal\quickedit\LayoutBuilderIntegration; /** * Helper methods for Quick Edit module integration. @@ -24,298 +12,4 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * @internal * This is an internal utility class wrapping hook implementations. */ -class QuickEditIntegration implements ContainerInjectionInterface { - - use LoggerChannelTrait; - - /** - * The section storage manager. - * - * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface - */ - protected $sectionStorageManager; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $currentUser; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a new QuickEditIntegration object. - * - * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager - * The section storage manager. - * @param \Drupal\Core\Session\AccountInterface $current_user - * The current user. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct(SectionStorageManagerInterface $section_storage_manager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) { - $this->sectionStorageManager = $section_storage_manager; - $this->currentUser = $current_user; - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('plugin.manager.layout_builder.section_storage'), - $container->get('current_user'), - $container->get('entity_type.manager') - ); - } - - /** - * Alters the entity view build for Quick Edit compatibility. - * - * When rendering fields outside of normal view modes, Quick Edit requires - * that modules identify themselves with a view mode ID in the format - * [module_name]-[information the module needs to rerender], as prescribed by - * hook_quickedit_render_field(). - * - * @param array $build - * The built entity render array. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display - * The entity view display. - * - * @see hook_quickedit_render_field() - * @see layout_builder_quickedit_render_field() - */ - public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { - if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder'])) { - return; - } - - $build['#cache']['contexts'][] = 'user.permissions'; - if (!$this->currentUser->hasPermission('access in-place editing')) { - return; - } - - $cacheable_metadata = CacheableMetadata::createFromRenderArray($build); - $section_list = $this->sectionStorageManager->findByContext( - [ - 'display' => EntityContext::fromEntity($display), - 'entity' => EntityContext::fromEntity($entity), - 'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()), - ], - $cacheable_metadata - ); - $cacheable_metadata->applyTo($build); - - if (empty($section_list)) { - return; - } - - // Create a hash of the sections and use it in the unique Quick Edit view - // mode ID. Any changes to the sections will result in a different hash, - // forcing Quick Edit's JavaScript to recognize any changes and retrieve - // up-to-date metadata. - $sections_hash = hash('sha256', serialize($section_list->getSections())); - - // Track each component by their plugin ID, delta, region, and UUID. - $plugin_ids_to_update = []; - foreach (Element::children($build['_layout_builder']) as $delta) { - $section = $build['_layout_builder'][$delta]; - - if (!Element::isEmpty($section)) { - /** @var \Drupal\Core\Layout\LayoutDefinition $layout */ - $layout = $section['#layout']; - $regions = $layout->getRegionNames(); - - foreach ($regions as $region) { - if (isset($section[$region])) { - foreach ($section[$region] as $uuid => $component) { - if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) { - $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid; - } - } - } - } - } - } - - // @todo Remove when https://www.drupal.org/node/3041850 is resolved. - $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) { - // Delta, region, and UUID each count as one. - return count($info, COUNT_RECURSIVE) === 3; - }); - - $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE); - foreach ($plugin_ids_to_update as $delta => $regions) { - foreach ($regions as $region => $uuids) { - foreach ($uuids as $uuid => $component) { - $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash); - } - } - } - // Alter the Quick Edit view mode ID of all fields outside of the Layout - // Builder sections to force Quick Edit to request to the field metadata. - // @todo Remove this logic in https://www.drupal.org/project/node/2966136. - foreach (Element::children($build) as $field_name) { - if ($field_name !== '_layout_builder') { - $field_build = &$build[$field_name]; - if (isset($field_build['#view_mode'])) { - $field_build['#view_mode'] = "layout_builder-{$display->getMode()}-non_component-$sections_hash"; - } - } - } - } - - /** - * Generates a Quick Edit view mode ID. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display - * The entity view display. - * @param int $delta - * The delta. - * @param string $component_uuid - * The component UUID. - * @param string $sections_hash - * The hash of the sections; must change whenever the sections change. - * - * @return string - * The Quick Edit view mode ID. - * - * @see \Drupal\layout_builder\QuickEditIntegration::deconstructViewModeId() - */ - private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) { - return implode('-', [ - 'layout_builder', - $display->getMode(), - $delta, - // Replace the dashes in the component UUID because we need to - // use dashes to join the parts. - str_replace('-', '_', $component_uuid), - $entity->id(), - $sections_hash, - ]); - } - - /** - * Deconstructs the Quick Edit view mode ID into its constituent parts. - * - * @param string $quick_edit_view_mode_id - * The Quick Edit view mode ID. - * - * @return array - * An array containing the entity view mode ID, the delta, the component - * UUID, and the entity ID. - * - * @see \Drupal\layout_builder\QuickEditIntegration::getViewModeId() - */ - public static function deconstructViewModeId($quick_edit_view_mode_id) { - [, $entity_view_mode_id, $delta, $component_uuid, $entity_id] = explode('-', $quick_edit_view_mode_id, 7); - return [ - $entity_view_mode_id, - // @todo Explicitly cast delta to an integer, remove this in - // https://www.drupal.org/project/drupal/issues/2984509. - (int) $delta, - // Replace the underscores with dash to get back the component UUID. - str_replace('_', '-', $component_uuid), - $entity_id, - ]; - } - - /** - * Re-renders a field rendered by Layout Builder, edited with Quick Edit. - * - * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * The entity. - * @param string $field_name - * The field name. - * @param string $quick_edit_view_mode_id - * The Quick Edit view mode ID. - * @param string $langcode - * The language code. - * - * @return array - * The re-rendered field. - */ - public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) { - [$entity_view_mode, $delta, $component_uuid] = static::deconstructViewModeId($quick_edit_view_mode_id); - - $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode); - $this->buildEntityView($entity_build); - - if (isset($entity_build['_layout_builder'][$delta])) { - foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) { - if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) { - return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content']; - } - } - } - - $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]); - return []; - } - - /** - * {@inheritdoc} - * - * @todo Replace this hardcoded processing when - * https://www.drupal.org/project/drupal/issues/3041635 is resolved. - * - * @see \Drupal\Tests\EntityViewTrait::buildEntityView() - */ - private function buildEntityView(array &$elements) { - // If the default values for this element have not been loaded yet, - // populate them. - if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { - $elements += \Drupal::service('element_info')->getInfo($elements['#type']); - } - - // Make any final changes to the element before it is rendered. This means - // that the $element or the children can be altered or corrected before - // the element is rendered into the final text. - if (isset($elements['#pre_render'])) { - foreach ($elements['#pre_render'] as $callable) { - $elements = call_user_func($callable, $elements); - } - } - - // And recurse. - $children = Element::children($elements, TRUE); - foreach ($children as $key) { - $this->buildEntityView($elements[$key]); - } - } - - /** - * Determines whether a component has Quick Edit support. - * - * Only field_block components for display configurable fields should be - * supported. - * - * @param array $component - * The component render array. - * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * The entity being displayed. - * - * @return bool - * Whether Quick Edit is supported on the component. - * - * @see \Drupal\layout_builder\Plugin\Block\FieldBlock - */ - private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) { - if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block' && $entity->hasField($component['content']['#field_name'])) { - return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view'); - } - return FALSE; - } - -} +class QuickEditIntegration extends LayoutBuilderIntegration {} diff --git a/frontend/drupal9/web/core/modules/migrate/tests/src/Functional/process/DownloadFunctionalTest.php b/frontend/drupal9/web/core/modules/migrate/tests/src/Functional/process/DownloadFunctionalTest.php index 26f2f3527..51371ff69 100644 --- a/frontend/drupal9/web/core/modules/migrate/tests/src/Functional/process/DownloadFunctionalTest.php +++ b/frontend/drupal9/web/core/modules/migrate/tests/src/Functional/process/DownloadFunctionalTest.php @@ -76,8 +76,11 @@ class DownloadFunctionalTest extends BrowserTestBase { $this->assertCount(1, $messages); $message = reset($messages); + // Assert critical parts of the error message, but not the exact message, + // since it depends on Guzzle's internal implementation of PSR-7. $id = $migration->getPluginId(); - $this->assertEquals("$id:uri:download: Client error: `GET $invalid_url` resulted in a `404 Not Found` response ($invalid_url)", $message->message); + $this->assertStringContainsString("$id:uri:download:", $message->message); + $this->assertStringContainsString($invalid_url, $message->message); $this->assertEquals(MigrationInterface::MESSAGE_ERROR, $message->level); // Check that the second row was migrated successfully. diff --git a/frontend/drupal9/web/core/modules/node/src/Plugin/views/argument/Vid.php b/frontend/drupal9/web/core/modules/node/src/Plugin/views/argument/Vid.php index 84c31fd1c..c5a35fc31 100644 --- a/frontend/drupal9/web/core/modules/node/src/Plugin/views/argument/Vid.php +++ b/frontend/drupal9/web/core/modules/node/src/Plugin/views/argument/Vid.php @@ -75,6 +75,7 @@ class Vid extends NumericArgument { ->accessCheck(FALSE) ->allRevisions() ->groupBy('title') + ->condition('vid', $this->value, 'IN') ->execute(); foreach ($results as $result) { diff --git a/frontend/drupal9/web/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_id_argument.yml b/frontend/drupal9/web/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_id_argument.yml index e2ac01afd..bf13bdfad 100644 --- a/frontend/drupal9/web/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_id_argument.yml +++ b/frontend/drupal9/web/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_id_argument.yml @@ -13,45 +13,12 @@ base_table: node_field_data base_field: nid display: default: - display_plugin: default id: default display_title: Default + display_plugin: default 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 + title: test_node_revision_id_argument fields: title: id: title @@ -60,6 +27,9 @@ display: relationship: none group_type: group admin_label: '' + entity_type: node + entity_field: title + plugin_id: field label: '' exclude: false alter: @@ -115,16 +85,30 @@ display: multi_type: separator separator: ', ' field_api_classes: false - entity_type: node - entity_field: title - plugin_id: field - filters: { } - sorts: { } - title: test_node_revision_id_argument - header: { } - footer: { } + pager: + type: none + options: + offset: 0 + items_per_page: null + 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 + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } empty: { } - relationships: { } + sorts: { } arguments: vid: id: vid @@ -133,13 +117,16 @@ display: relationship: none group_type: group admin_label: '' + entity_type: node + entity_field: vid + plugin_id: node_vid default_action: ignore exception: value: all title_enable: false title: All - title_enable: false - title: '' + title_enable: true + title: '{{ arguments.vid }}' default_argument_type: fixed default_argument_options: argument: '' @@ -147,8 +134,8 @@ display: summary_options: base_path: '' count: true - items_per_page: 25 override: false + items_per_page: 25 summary: sort_order: asc number_of_records: 0 @@ -160,38 +147,51 @@ display: validate_options: { } break_phrase: false not: false - entity_type: node - entity_field: vid - plugin_id: node_vid - display_extenders: { } + filters: { } filter_groups: operator: AND groups: { } + style: + type: default + row: + type: fields + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: { } + footer: { } + display_extenders: { } cache_metadata: + max-age: -1 contexts: - 'languages:language_content' - 'languages:language_interface' - url - 'user.node_grants:view' - user.permissions - cacheable: false - max-age: -1 tags: { } + cacheable: false page_1: - display_plugin: page id: page_1 display_title: Page + display_plugin: page position: 1 display_options: display_extenders: { } path: test-revision-vid-argument cache_metadata: + max-age: -1 contexts: - 'languages:language_content' - 'languages:language_interface' - url - 'user.node_grants:view' - user.permissions - cacheable: false - max-age: -1 tags: { } + cacheable: false diff --git a/frontend/drupal9/web/core/modules/node/tests/src/Kernel/Views/ArgumentNodeRevisionIdTest.php b/frontend/drupal9/web/core/modules/node/tests/src/Kernel/Views/ArgumentNodeRevisionIdTest.php index 9d3898544..62dbe92be 100644 --- a/frontend/drupal9/web/core/modules/node/tests/src/Kernel/Views/ArgumentNodeRevisionIdTest.php +++ b/frontend/drupal9/web/core/modules/node/tests/src/Kernel/Views/ArgumentNodeRevisionIdTest.php @@ -46,13 +46,16 @@ class ArgumentNodeRevisionIdTest extends ViewsKernelTestBase { NodeType::create(['type' => 'page', 'name' => 'page'])->save(); $node = Node::create(['type' => 'page', 'title' => 'test1', 'uid' => 1]); $node->save(); + $first_revision_id = $node->getRevisionId(); $node->setNewRevision(); $node->setTitle('test2'); $node->save(); + $second_revision_id = $node->getRevisionId(); $view_nid = Views::getView('test_node_revision_id_argument'); - $this->executeView($view_nid, [$node->getRevisionId()]); + $this->executeView($view_nid, [$second_revision_id]); $this->assertIdenticalResultset($view_nid, [['title' => 'test2']]); + $this->assertSame('test2', $view_nid->getTitle()); } /** diff --git a/frontend/drupal9/web/core/modules/quickedit/quickedit.module b/frontend/drupal9/web/core/modules/quickedit/quickedit.module index 028fe2014..219b975eb 100644 --- a/frontend/drupal9/web/core/modules/quickedit/quickedit.module +++ b/frontend/drupal9/web/core/modules/quickedit/quickedit.module @@ -16,6 +16,8 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\quickedit\Entity\QuickEditLayoutBuilderEntityViewDisplay; +use Drupal\quickedit\LayoutBuilderIntegration; /** * Implements hook_help(). @@ -174,10 +176,28 @@ function quickedit_preprocess_field(&$variables) { } } +/** + * Implements hook_entity_type_alter(). + */ +function quickedit_entity_type_alter(array &$entity_types) { + if (\Drupal::moduleHandler()->moduleExists('layout_builder')) { + /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ + if ($entity_types['entity_view_display']->getClass() === 'Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay\LayoutBuilderEntityViewDisplay') { + $entity_types['entity_view_display']->setClass(QuickEditLayoutBuilderEntityViewDisplay::class); + } + } +} + /** * Implements hook_entity_view_alter(). */ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + if (\Drupal::moduleHandler()->moduleExists('layout_builder')) { + /** @var \Drupal\quickedit\LayoutBuilderIntegration $layout_builder_integration */ + $layout_builder_integration = \Drupal::classResolver(LayoutBuilderIntegration::class); + $layout_builder_integration->entityViewAlter($build, $entity, $display); + } + if (isset($build['#embed'])) { return; } @@ -189,3 +209,12 @@ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityVie $build['#attributes']['data-quickedit-entity-id'] = $entity->getEntityTypeId() . '/' . $entity->id(); } + +/** + * Implements hook_quickedit_render_field(). + */ +function layout_builder_quickedit_render_field(EntityInterface $entity, $field_name, $view_mode_id, $langcode) { + /** @var \Drupal\quickedit\LayoutBuilderIntegration $layout_builder_integration */ + $layout_builder_integration = \Drupal::classResolver(LayoutBuilderIntegration::class); + return $layout_builder_integration->quickEditRenderField($entity, $field_name, $view_mode_id, $langcode); +} diff --git a/frontend/drupal9/web/core/modules/quickedit/src/Entity/QuickEditLayoutBuilderEntityViewDisplay.php b/frontend/drupal9/web/core/modules/quickedit/src/Entity/QuickEditLayoutBuilderEntityViewDisplay.php new file mode 100644 index 000000000..3dd4eb9b1 --- /dev/null +++ b/frontend/drupal9/web/core/modules/quickedit/src/Entity/QuickEditLayoutBuilderEntityViewDisplay.php @@ -0,0 +1,68 @@ +isLayoutBuilderEnabled() && $section_component = $this->getQuickEditSectionComponent()) { + $plugin = $section_component->getPlugin(); + if ($plugin instanceof ConfigurableInterface) { + $configuration = $plugin->getConfiguration(); + if (isset($configuration['formatter'])) { + return $configuration['formatter']; + } + } + } + return parent::getComponent($name); + } + + /** + * Returns the Quick Edit formatter settings. + * + * @return \Drupal\layout_builder\SectionComponent|null + * The section component if it is available. + * + * @see \Drupal\quickedit\LayoutBuilderIntegration::entityViewAlter() + * @see \Drupal\quickedit\MetadataGenerator::generateFieldMetadata() + */ + private function getQuickEditSectionComponent() { + // To determine the Quick Edit view_mode ID we need an originalMode set. + if ($original_mode = $this->getOriginalMode()) { + $parts = explode('-', $original_mode); + // The Quick Edit view mode ID is created by + // \Drupal\quickedit\LayoutBuilderIntegration::entityViewAlter() + // concatenating together the information we need to retrieve the Layout + // Builder component. It follows the structure prescribed by the + // documentation of hook_quickedit_render_field(). + if (count($parts) === 6 && $parts[0] === 'layout_builder') { + [, $delta, $component_uuid, $entity_id] = LayoutBuilderIntegration::deconstructViewModeId($original_mode); + $entity = $this->entityTypeManager()->getStorage($this->getTargetEntityTypeId())->load($entity_id); + $sections = $this->getEntitySections($entity); + if (isset($sections[$delta])) { + $component = $sections[$delta]->getComponent($component_uuid); + $plugin = $component->getPlugin(); + // We only care about FieldBlock because these are only components + // that provide Quick Edit integration: Quick Edit enables in-place + // editing of fields of entities, not of anything else. + if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { + return $component; + } + } + } + } + return NULL; + } + +} diff --git a/frontend/drupal9/web/core/modules/quickedit/src/LayoutBuilderIntegration.php b/frontend/drupal9/web/core/modules/quickedit/src/LayoutBuilderIntegration.php new file mode 100644 index 000000000..49e229b98 --- /dev/null +++ b/frontend/drupal9/web/core/modules/quickedit/src/LayoutBuilderIntegration.php @@ -0,0 +1,321 @@ +sectionStorageManager = $section_storage_manager; + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.layout_builder.section_storage'), + $container->get('current_user'), + $container->get('entity_type.manager') + ); + } + + /** + * Alters the entity view build for Layout Builder compatibility. + * + * When rendering fields outside of normal view modes, Quick Edit requires + * that modules identify themselves with a view mode ID in the format + * [module_name]-[information the module needs to rerender], as prescribed by + * hook_quickedit_render_field(). + * + * @param array $build + * The built entity render array. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The entity view display. + * + * @see hook_quickedit_render_field() + * @see layout_builder_quickedit_render_field() + */ + public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder'])) { + return; + } + + $build['#cache']['contexts'][] = 'user.permissions'; + if (!$this->currentUser->hasPermission('access in-place editing')) { + return; + } + + $cacheable_metadata = CacheableMetadata::createFromRenderArray($build); + $section_list = $this->sectionStorageManager->findByContext( + [ + 'display' => EntityContext::fromEntity($display), + 'entity' => EntityContext::fromEntity($entity), + 'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()), + ], + $cacheable_metadata + ); + $cacheable_metadata->applyTo($build); + + if (empty($section_list)) { + return; + } + + // Create a hash of the sections and use it in the unique Quick Edit view + // mode ID. Any changes to the sections will result in a different hash, + // forcing Quick Edit's JavaScript to recognize any changes and retrieve + // up-to-date metadata. + $sections_hash = hash('sha256', serialize($section_list->getSections())); + + // Track each component by their plugin ID, delta, region, and UUID. + $plugin_ids_to_update = []; + foreach (Element::children($build['_layout_builder']) as $delta) { + $section = $build['_layout_builder'][$delta]; + + if (!Element::isEmpty($section)) { + /** @var \Drupal\Core\Layout\LayoutDefinition $layout */ + $layout = $section['#layout']; + $regions = $layout->getRegionNames(); + + foreach ($regions as $region) { + if (isset($section[$region])) { + foreach ($section[$region] as $uuid => $component) { + if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) { + $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid; + } + } + } + } + } + } + + // @todo Remove when https://www.drupal.org/node/3041850 is resolved. + $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) { + // Delta, region, and UUID each count as one. + return count($info, COUNT_RECURSIVE) === 3; + }); + + $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE); + foreach ($plugin_ids_to_update as $delta => $regions) { + foreach ($regions as $region => $uuids) { + foreach ($uuids as $uuid => $component) { + $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash); + } + } + } + // Alter the Quick Edit view mode ID of all fields outside of the Layout + // Builder sections to force Quick Edit to request to the field metadata. + // @todo Remove this logic in https://www.drupal.org/project/node/2966136. + foreach (Element::children($build) as $field_name) { + if ($field_name !== '_layout_builder') { + $field_build = &$build[$field_name]; + if (isset($field_build['#view_mode'])) { + $field_build['#view_mode'] = "layout_builder-{$display->getMode()}-non_component-$sections_hash"; + } + } + } + } + + /** + * Generates a Quick Edit view mode ID. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The entity view display. + * @param int $delta + * The delta. + * @param string $component_uuid + * The component UUID. + * @param string $sections_hash + * The hash of the sections; must change whenever the sections change. + * + * @return string + * The Quick Edit view mode ID. + * + * @see \Drupal\quickedit\LayoutBuilderIntegration::deconstructViewModeId() + */ + private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) { + return implode('-', [ + 'layout_builder', + $display->getMode(), + $delta, + // Replace the dashes in the component UUID because we need to + // use dashes to join the parts. + str_replace('-', '_', $component_uuid), + $entity->id(), + $sections_hash, + ]); + } + + /** + * Deconstructs the Quick Edit view mode ID into its constituent parts. + * + * @param string $quick_edit_view_mode_id + * The Quick Edit view mode ID. + * + * @return array + * An array containing the entity view mode ID, the delta, the component + * UUID, and the entity ID. + * + * @see \Drupal\quickedit\LayoutBuilderIntegration::getViewModeId() + */ + public static function deconstructViewModeId($quick_edit_view_mode_id) { + [, $entity_view_mode_id, $delta, $component_uuid, $entity_id] = explode('-', $quick_edit_view_mode_id, 7); + return [ + $entity_view_mode_id, + // @todo Explicitly cast delta to an integer, remove this in + // https://www.drupal.org/project/drupal/issues/2984509. + (int) $delta, + // Replace the underscores with dash to get back the component UUID. + str_replace('_', '-', $component_uuid), + $entity_id, + ]; + } + + /** + * Re-renders a field rendered by Layout Builder, edited with Quick Edit. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity. + * @param string $field_name + * The field name. + * @param string $quick_edit_view_mode_id + * The Quick Edit view mode ID. + * @param string $langcode + * The language code. + * + * @return array + * The re-rendered field. + */ + public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) { + [$entity_view_mode, $delta, $component_uuid] = static::deconstructViewModeId($quick_edit_view_mode_id); + + $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode); + $this->buildEntityView($entity_build); + + if (isset($entity_build['_layout_builder'][$delta])) { + foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) { + if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) { + return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content']; + } + } + } + + $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]); + return []; + } + + /** + * {@inheritdoc} + * + * @todo Replace this hardcoded processing when + * https://www.drupal.org/project/drupal/issues/3041635 is resolved. + * + * @see \Drupal\Tests\EntityViewTrait::buildEntityView() + */ + private function buildEntityView(array &$elements) { + // If the default values for this element have not been loaded yet, + // populate them. + if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { + $elements += \Drupal::service('element_info')->getInfo($elements['#type']); + } + + // Make any final changes to the element before it is rendered. This means + // that the $element or the children can be altered or corrected before + // the element is rendered into the final text. + if (isset($elements['#pre_render'])) { + foreach ($elements['#pre_render'] as $callable) { + $elements = call_user_func($callable, $elements); + } + } + + // And recurse. + $children = Element::children($elements, TRUE); + foreach ($children as $key) { + $this->buildEntityView($elements[$key]); + } + } + + /** + * Determines whether a component has Quick Edit support. + * + * Only field_block components for display configurable fields should be + * supported. + * + * @param array $component + * The component render array. + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity being displayed. + * + * @return bool + * Whether Quick Edit is supported on the component. + * + * @see \Drupal\layout_builder\Plugin\Block\FieldBlock + */ + private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) { + if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block' && $entity->hasField($component['content']['#field_name'])) { + return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view'); + } + return FALSE; + } + +} diff --git a/frontend/drupal9/web/core/modules/quickedit/tests/src/Functional/Jsonapi/QuickEditLayoutBuilderEntityViewDisplayTest.php b/frontend/drupal9/web/core/modules/quickedit/tests/src/Functional/Jsonapi/QuickEditLayoutBuilderEntityViewDisplayTest.php new file mode 100644 index 000000000..0e1509b2a --- /dev/null +++ b/frontend/drupal9/web/core/modules/quickedit/tests/src/Functional/Jsonapi/QuickEditLayoutBuilderEntityViewDisplayTest.php @@ -0,0 +1,21 @@ +container->get('test_service'); + $this->assertEquals('test_service', $test_service->_serviceId); + $this->assertInstanceOf(TestServiceDecorator::class, $test_service); + + // Service decorated twice. + $test_service2 = $this->container->get('test_service2'); + $this->assertEquals('test_service2', $test_service2->_serviceId); + $this->assertInstanceOf(TestServiceDecorator::class, $test_service2); + } + +} diff --git a/frontend/drupal9/web/core/modules/views/src/ViewExecutable.php b/frontend/drupal9/web/core/modules/views/src/ViewExecutable.php index 3da949570..5a7c6307b 100644 --- a/frontend/drupal9/web/core/modules/views/src/ViewExecutable.php +++ b/frontend/drupal9/web/core/modules/views/src/ViewExecutable.php @@ -2492,8 +2492,6 @@ class ViewExecutable { // state during unserialization. $this->serializationData = [ 'storage' => $this->storage->id(), - 'views_data' => $this->viewsData->_serviceId, - 'route_provider' => $this->routeProvider->_serviceId, 'current_display' => $this->current_display, 'args' => $this->args, 'current_page' => $this->current_page, @@ -2520,8 +2518,8 @@ class ViewExecutable { // Attach all necessary services. $this->user = \Drupal::currentUser(); - $this->viewsData = \Drupal::service($this->serializationData['views_data']); - $this->routeProvider = \Drupal::service($this->serializationData['route_provider']); + $this->viewsData = \Drupal::service('views.views_data'); + $this->routeProvider = \Drupal::service('router.route_provider'); // Restore the state of this executable. if ($request = \Drupal::request()) { diff --git a/frontend/drupal9/web/core/modules/views/tests/src/Functional/Wizard/BasicTest.php b/frontend/drupal9/web/core/modules/views/tests/src/Functional/Wizard/BasicTest.php index 0909299bf..fbbd32060 100644 --- a/frontend/drupal9/web/core/modules/views/tests/src/Functional/Wizard/BasicTest.php +++ b/frontend/drupal9/web/core/modules/views/tests/src/Functional/Wizard/BasicTest.php @@ -155,9 +155,6 @@ class BasicTest extends WizardTestBase { $this->assertSession()->pageTextContains($node1->label()); $this->assertSession()->pageTextNotContains($node2->label()); - // Make sure the listing page doesn't show disabled default views. - $this->assertSession()->pageTextNotContains('tracker'); - // Create a view with only a REST export. $view4 = []; $view4['label'] = $this->randomMachineName(16); diff --git a/frontend/drupal9/web/core/scripts/js/vendor-update.js b/frontend/drupal9/web/core/scripts/js/vendor-update.js index df014bab6..56f79e075 100644 --- a/frontend/drupal9/web/core/scripts/js/vendor-update.js +++ b/frontend/drupal9/web/core/scripts/js/vendor-update.js @@ -77,7 +77,7 @@ const assetsFolder = `${coreFolder}/assets/vendor`; { pack: 'backbone', library: 'internal.backbone', - files: ['backbone.js', 'backbone-min.js', 'backbone-min.map'], + files: ['backbone.js', 'backbone-min.js', 'backbone-min.js.map'], }, // Only used to update the version number of the deprecated library. { diff --git a/frontend/drupal9/web/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php b/frontend/drupal9/web/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php index e7e7b2801..7aad3dd79 100644 --- a/frontend/drupal9/web/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php +++ b/frontend/drupal9/web/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php @@ -244,6 +244,14 @@ class ComposerProjectTemplatesTest extends BuildTestBase { $this->executeCommand("COMPOSER_HOME=$composer_home COMPOSER_ROOT_VERSION=$simulated_core_version composer create-project --no-ansi $project testproject $simulated_core_version -vvv --repository $repository_path"); $this->assertCommandSuccessful(); + // Check the output of the project creation for the absence of warnings + // about any non-allowed composer plugins. + // Note: There are different warnings for unallowed composer plugins + // depending on running in non-interactive mode or not. It seems the Drupal + // CI environment always forces composer commands to run in the + // non-interactive mode. The only thing these messages have in common is the + // following string. + $this->assertErrorOutputNotContains('See https://getcomposer.org/allow-plugins'); // Ensure we used the project from our codebase. $this->assertErrorOutputContains("Installing $project ($simulated_core_version): Symlinking from $package_dir"); @@ -386,6 +394,16 @@ JSON; "version" => $version, ], ]; + // Ensure composer plugins are registered correctly. + $package_json = json_decode(file_get_contents($full_path . '/composer.json'), TRUE); + if (isset($package_json['type']) && $package_json['type'] === 'composer-plugin') { + $packages['packages'][$name][$version]['type'] = $package_json['type']; + $packages['packages'][$name][$version]['require'] = $package_json['require']; + $packages['packages'][$name][$version]['extra'] = $package_json['extra']; + if (isset($package_json['autoload'])) { + $packages['packages'][$name][$version]['autoload'] = $package_json['autoload']; + } + } } } diff --git a/frontend/drupal9/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/frontend/drupal9/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php index 05cb76783..a6de48990 100644 --- a/frontend/drupal9/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php +++ b/frontend/drupal9/web/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php @@ -265,6 +265,16 @@ abstract class BuildTestBase extends TestCase { $this->assertStringContainsString($expected, $this->commandProcess->getErrorOutput()); } + /** + * Assert text is not present in the error output of the most recent command. + * + * @param string $expected + * Text we expect not to find in the error output of the command. + */ + public function assertErrorOutputNotContains($expected) { + $this->assertStringNotContainsString($expected, $this->commandProcess->getErrorOutput()); + } + /** * Assert that text is present in the output of the most recent command. * diff --git a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php index a4f2a6500..ab224eca4 100644 --- a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php +++ b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php @@ -760,6 +760,9 @@ class BrowserTestBaseTest extends BrowserTestBase { public function testInstall() { $htaccess_filename = $this->tempFilesDirectory . '/.htaccess'; $this->assertFileExists($htaccess_filename); + + // Ensure the update module is not installed. + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('update'), 'The Update module is not installed.'); } /** diff --git a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php index 8a6a9308f..1e04685e2 100644 --- a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php +++ b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php @@ -105,7 +105,8 @@ abstract class InstallerExistingConfigTestBase extends InstallerTestBase { // existing configuration. unset($parameters['forms']['install_configure_form']['site_name']); unset($parameters['forms']['install_configure_form']['site_mail']); - unset($parameters['forms']['install_configure_form']['update_status_module']); + unset($parameters['forms']['install_configure_form']['enable_update_status_module']); + unset($parameters['forms']['install_configure_form']['enable_update_status_emails']); return $parameters; } diff --git a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php index 118a77124..af85b7e7f 100644 --- a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php +++ b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php @@ -131,6 +131,9 @@ class InstallerTest extends InstallerTestBase { $module = $database->getProvider(); $module_handler = \Drupal::service('module_handler'); + // Ensure the update module is not installed. + $this->assertFalse($module_handler->moduleExists('update'), 'The Update module is not installed.'); + // Assert that the module that is providing the database driver has been // installed. $this->assertTrue($module_handler->moduleExists($module)); diff --git a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php index caf4afab0..1cc8300aa 100644 --- a/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php +++ b/frontend/drupal9/web/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php @@ -73,6 +73,19 @@ abstract class InstallerTestBase extends BrowserTestBase { */ protected $isInstalled = FALSE; + /** + * {@inheritdoc} + */ + protected function installParameters() { + $params = parent::installParameters(); + // Set the checkbox values to FALSE so that + // \Drupal\Tests\BrowserTestBase::translatePostValues() does not remove + // them. + $params['forms']['install_configure_form']['enable_update_status_module'] = FALSE; + $params['forms']['install_configure_form']['enable_update_status_emails'] = FALSE; + return $params; + } + /** * {@inheritdoc} */ diff --git a/frontend/drupal9/web/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php b/frontend/drupal9/web/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php index 3854f3ef8..8db248859 100644 --- a/frontend/drupal9/web/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php +++ b/frontend/drupal9/web/core/tests/Drupal/KernelTests/Core/Image/ToolkitGdTest.php @@ -4,19 +4,30 @@ namespace Drupal\KernelTests\Core\Image; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Image\ImageInterface; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Site\Settings; use Drupal\KernelTests\KernelTestBase; /** - * Tests that core image manipulations work properly: scale, resize, rotate, - * crop, scale and crop, and desaturate. + * Tests for the GD image toolkit. * + * @coversDefaultClass \Drupal\system\Plugin\ImageToolkit\GDToolkit * @group Image * @requires extension gd */ class ToolkitGdTest extends KernelTestBase { + /** + * Colors that are used in testing. + */ + protected const BLACK = [0, 0, 0, 0]; + protected const RED = [255, 0, 0, 0]; + protected const GREEN = [0, 255, 0, 0]; + protected const BLUE = [0, 0, 255, 0]; + protected const YELLOW = [255, 255, 0, 0]; + protected const WHITE = [255, 255, 255, 0]; + protected const TRANSPARENT = [0, 0, 0, 127]; + protected const FUCHSIA = [255, 0, 255, 0]; + protected const ROTATE_TRANSPARENT = [255, 255, 255, 127]; + /** * The image factory service. * @@ -25,33 +36,14 @@ class ToolkitGdTest extends KernelTestBase { protected $imageFactory; /** - * Colors that are used in testing. + * A directory where test image files can be saved to. * - * @var array + * @var string */ - protected $black = [0, 0, 0, 0]; - protected $red = [255, 0, 0, 0]; - protected $green = [0, 255, 0, 0]; - protected $blue = [0, 0, 255, 0]; - protected $yellow = [255, 255, 0, 0]; - protected $white = [255, 255, 255, 0]; - protected $transparent = [0, 0, 0, 127]; + protected $directory; /** - * Used as rotate background colors. - * - * @var array - */ - protected $fuchsia = [255, 0, 255, 0]; - protected $rotateTransparent = [255, 255, 255, 127]; - - protected $width = 40; - protected $height = 20; - - /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ protected static $modules = ['system']; @@ -62,32 +54,41 @@ class ToolkitGdTest extends KernelTestBase { parent::setUp(); $this->installConfig(['system']); + // Set the image factory service. $this->imageFactory = $this->container->get('image.factory'); + $this->assertEquals('gd', $this->imageFactory->getToolkitId(), 'The image factory is set to use the \'gd\' image toolkit.'); + + // Prepare a directory for test file results. + $this->directory = 'public://imagetest'; + \Drupal::service('file_system')->prepareDirectory($this->directory, FileSystemInterface::CREATE_DIRECTORY); } /** - * Function to compare two colors by RGBa. + * Assert two colors are equal by RGBA, net of full transparency. + * + * @param int[] $expected + * The expected RGBA array. + * @param int[] $actual + * The actual RGBA array. + * @param int $tolerance + * The acceptable difference between the colors. + * @param string $message + * The assertion message. */ - public function colorsAreEqual($color_a, $color_b) { - // Fully transparent pixels are equal, regardless of RGB. - if ($color_a[3] == 127 && $color_b[3] == 127) { - return TRUE; + protected function assertColorsAreEqual(array $expected, array $actual, int $tolerance, string $message = ''): void { + // Fully transparent colors are equal, regardless of RGB. + if ($actual[3] == 127 && $expected[3] == 127) { + return; } - - foreach ($color_a as $key => $value) { - if ($color_b[$key] != $value) { - return FALSE; - } - } - - return TRUE; + $distance = pow(($actual[0] - $expected[0]), 2) + pow(($actual[1] - $expected[1]), 2) + pow(($actual[2] - $expected[2]), 2) + pow(($actual[3] - $expected[3]), 2); + $this->assertLessThanOrEqual($tolerance, $distance, $message . " - Actual: {" . implode(',', $actual) . "}, Expected: {" . implode(',', $expected) . "}, Distance: " . $distance . ", Tolerance: " . $tolerance); } /** * Function for finding a pixel's RGBa values. */ - public function getPixelColor(ImageInterface $image, $x, $y) { + public function getPixelColor(ImageInterface $image, int $x, int $y): array { $toolkit = $image->getToolkit(); $color_index = imagecolorat($toolkit->getResource(), $x, $y); @@ -100,123 +101,87 @@ class ToolkitGdTest extends KernelTestBase { } /** - * Since PHP can't visually check that our images have been manipulated - * properly, build a list of expected color values for each of the corners and - * the expected height and widths for the final images. + * Data provider for ::testManipulations(). */ - public function testManipulations() { - - // Test that the image factory is set to use the GD toolkit. - $this->assertEquals('gd', $this->imageFactory->getToolkitId(), 'The image factory is set to use the \'gd\' image toolkit.'); - - // Test the list of supported extensions. - $expected_extensions = ['png', 'gif', 'jpeg', 'jpg', 'jpe', 'webp']; - $supported_extensions = $this->imageFactory->getSupportedExtensions(); - $this->assertEquals($expected_extensions, array_intersect($expected_extensions, $supported_extensions)); - - // Test that the supported extensions map to correct internal GD image - // types. - $expected_image_types = [ - 'png' => IMAGETYPE_PNG, - 'gif' => IMAGETYPE_GIF, - 'jpeg' => IMAGETYPE_JPEG, - 'jpg' => IMAGETYPE_JPEG, - 'jpe' => IMAGETYPE_JPEG, - 'webp' => IMAGETYPE_WEBP, - ]; - $image = $this->imageFactory->get(); - foreach ($expected_image_types as $extension => $expected_image_type) { - $image_type = $image->getToolkit()->extensionToImageType($extension); - $this->assertSame($expected_image_type, $image_type); - } - + public function providerTestImageFiles(): array { // Typically the corner colors will be unchanged. These colors are in the // order of top-left, top-right, bottom-right, bottom-left. - $default_corners = [$this->red, $this->green, $this->blue, $this->transparent]; - - // A list of files that will be tested. - $files = [ - 'image-test.png', - 'image-test.gif', - 'image-test-no-transparency.gif', - 'image-test.jpg', - 'img-test.webp', - ]; + $default_corners = [static::RED, static::GREEN, static::BLUE, static::TRANSPARENT]; // Setup a list of tests to perform on each type. - $operations = [ + $test_cases = [ 'resize' => [ - 'function' => 'resize', + 'operation' => 'resize', 'arguments' => ['width' => 20, 'height' => 10], 'width' => 20, 'height' => 10, 'corners' => $default_corners, ], 'scale_x' => [ - 'function' => 'scale', + 'operation' => 'scale', 'arguments' => ['width' => 20], 'width' => 20, 'height' => 10, 'corners' => $default_corners, ], 'scale_y' => [ - 'function' => 'scale', + 'operation' => 'scale', 'arguments' => ['height' => 10], 'width' => 20, 'height' => 10, 'corners' => $default_corners, ], 'upscale_x' => [ - 'function' => 'scale', + 'operation' => 'scale', 'arguments' => ['width' => 80, 'upscale' => TRUE], 'width' => 80, 'height' => 40, 'corners' => $default_corners, ], 'upscale_y' => [ - 'function' => 'scale', + 'operation' => 'scale', 'arguments' => ['height' => 40, 'upscale' => TRUE], 'width' => 80, 'height' => 40, 'corners' => $default_corners, ], 'crop' => [ - 'function' => 'crop', + 'operation' => 'crop', 'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12], 'width' => 16, 'height' => 12, - 'corners' => array_fill(0, 4, $this->white), + 'corners' => array_fill(0, 4, static::WHITE), ], 'scale_and_crop' => [ - 'function' => 'scale_and_crop', + 'operation' => 'scale_and_crop', 'arguments' => ['width' => 10, 'height' => 8], 'width' => 10, 'height' => 8, - 'corners' => array_fill(0, 4, $this->black), + 'corners' => array_fill(0, 4, static::BLACK), ], 'convert_jpg' => [ - 'function' => 'convert', + 'operation' => 'convert', 'width' => 40, 'height' => 20, 'arguments' => ['extension' => 'jpeg'], 'corners' => $default_corners, ], 'convert_gif' => [ - 'function' => 'convert', + 'operation' => 'convert', 'width' => 40, 'height' => 20, 'arguments' => ['extension' => 'gif'], 'corners' => $default_corners, ], 'convert_png' => [ - 'function' => 'convert', + 'operation' => 'convert', 'width' => 40, 'height' => 20, 'arguments' => ['extension' => 'png'], 'corners' => $default_corners, ], 'convert_webp' => [ - 'function' => 'convert', + 'operation' => 'convert', 'width' => 40, 'height' => 20, 'arguments' => ['extension' => 'webp'], @@ -224,49 +189,51 @@ class ToolkitGdTest extends KernelTestBase { ], ]; - // Systems using non-bundled GD2 don't have imagerotate. Test if available. - // @todo Remove the version check once - // https://www.drupal.org/project/drupal/issues/2670966 is resolved. - if (function_exists('imagerotate') && (version_compare(phpversion(), '7.0.26') < 0)) { - $operations += [ + // Systems using non-bundled GD2 may miss imagerotate(). Test if available. + if (function_exists('imagerotate')) { + $test_cases += [ 'rotate_5' => [ - 'function' => 'rotate', + 'operation' => 'rotate', // Fuchsia background. 'arguments' => ['degrees' => 5, 'background' => '#FF00FF'], - 'width' => 41, - 'height' => 23, - 'corners' => array_fill(0, 4, $this->fuchsia), + // @todo Re-enable dimensions' check once + // https://www.drupal.org/project/drupal/issues/2921123 is resolved. + // 'width' => 41, + // 'height' => 23, + 'corners' => array_fill(0, 4, static::FUCHSIA), + ], + 'rotate_transparent_5' => [ + 'operation' => 'rotate', + 'arguments' => ['degrees' => 5], + // @todo Re-enable dimensions' check once + // https://www.drupal.org/project/drupal/issues/2921123 is resolved. + // 'width' => 41, + // 'height' => 23, + 'corners' => array_fill(0, 4, static::ROTATE_TRANSPARENT), ], 'rotate_90' => [ - 'function' => 'rotate', + 'operation' => 'rotate', // Fuchsia background. 'arguments' => ['degrees' => 90, 'background' => '#FF00FF'], 'width' => 20, 'height' => 40, - 'corners' => [$this->transparent, $this->red, $this->green, $this->blue], - ], - 'rotate_transparent_5' => [ - 'function' => 'rotate', - 'arguments' => ['degrees' => 5], - 'width' => 41, - 'height' => 23, - 'corners' => array_fill(0, 4, $this->rotateTransparent), + 'corners' => [static::TRANSPARENT, static::RED, static::GREEN, static::BLUE], ], 'rotate_transparent_90' => [ - 'function' => 'rotate', + 'operation' => 'rotate', 'arguments' => ['degrees' => 90], 'width' => 20, 'height' => 40, - 'corners' => [$this->transparent, $this->red, $this->green, $this->blue], + 'corners' => [static::TRANSPARENT, static::RED, static::GREEN, static::BLUE], ], ]; } - // Systems using non-bundled GD2 don't have imagefilter. Test if available. + // Systems using non-bundled GD2 may miss imagefilter(). Test if available. if (function_exists('imagefilter')) { - $operations += [ + $test_cases += [ 'desaturate' => [ - 'function' => 'desaturate', + 'operation' => 'desaturate', 'arguments' => [], 'height' => 20, 'width' => 40, @@ -283,148 +250,202 @@ class ToolkitGdTest extends KernelTestBase { ]; } - // Prepare a directory for test file results. - $directory = Settings::get('file_public_path') . '/imagetest'; - \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY); - - foreach ($files as $file) { - foreach ($operations as $op => $values) { - // Load up a fresh image. - $image = $this->imageFactory->get('core/tests/fixtures/files/' . $file); - $toolkit = $image->getToolkit(); - if (!$image->isValid()) { - $this->fail(new FormattableMarkup('Could not load image %file.', ['%file' => $file])); - continue 2; - } - $image_original_type = $image->getToolkit()->getType(); - - // All images should be converted to truecolor when loaded. - $image_truecolor = imageistruecolor($toolkit->getResource()); - $this->assertTrue($image_truecolor, new FormattableMarkup('Image %file after load is a truecolor image.', ['%file' => $file])); - - // Store the original GD resource. - $old_res = $toolkit->getResource(); - - // Perform our operation. - $image->apply($values['function'], $values['arguments']); - - // If the operation replaced the resource, check that the old one has - // been destroyed. - $new_res = $toolkit->getResource(); - if ($new_res !== $old_res) { - // @todo In https://www.drupal.org/node/3133236 convert this to - // $this->assertIsNotResource($old_res). - $this->assertFalse(is_resource($old_res), new FormattableMarkup("'%operation' destroyed the original resource.", ['%operation' => $values['function']])); - } - - // To keep from flooding the test with assert values, make a general - // value for whether each group of values fail. - $correct_dimensions_real = TRUE; - $correct_dimensions_object = TRUE; - - if (imagesy($toolkit->getResource()) != $values['height'] || imagesx($toolkit->getResource()) != $values['width']) { - $correct_dimensions_real = FALSE; - } - - // Check that the image object has an accurate record of the dimensions. - if ($image->getWidth() != $values['width'] || $image->getHeight() != $values['height']) { - $correct_dimensions_object = FALSE; - } - - $file_path = $directory . '/' . $op . image_type_to_extension($image->getToolkit()->getType()); - $image->save($file_path); - - $this->assertTrue($correct_dimensions_real, new FormattableMarkup('Image %file after %action action has proper dimensions.', ['%file' => $file, '%action' => $op])); - $this->assertTrue($correct_dimensions_object, new FormattableMarkup('Image %file object after %action action is reporting the proper height and width values.', ['%file' => $file, '%action' => $op])); - - // JPEG colors will always be messed up due to compression. So we skip - // these tests if the original or the result is in jpeg format. - if ($image->getToolkit()->getType() != IMAGETYPE_JPEG && $image_original_type != IMAGETYPE_JPEG) { - // Now check each of the corners to ensure color correctness. - foreach ($values['corners'] as $key => $corner) { - // The test gif that does not have transparency color set is a - // special case. - if ($file === 'image-test-no-transparency.gif') { - if ($op == 'desaturate') { - // For desaturating, keep the expected color from the test - // data, but set alpha channel to fully opaque. - $corner[3] = 0; - } - elseif ($corner === $this->transparent) { - // Set expected pixel to yellow where the others have - // transparent. - $corner = $this->yellow; - } - } - - // Get the location of the corner. - switch ($key) { - case 0: - $x = 0; - $y = 0; - break; - - case 1: - $x = $image->getWidth() - 1; - $y = 0; - break; - - case 2: - $x = $image->getWidth() - 1; - $y = $image->getHeight() - 1; - break; - - case 3: - $x = 0; - $y = $image->getHeight() - 1; - break; - } - $color = $this->getPixelColor($image, $x, $y); - // We also skip the color test for transparency for gif <-> png - // conversion. The convert operation cannot handle that correctly. - if ($image->getToolkit()->getType() == $image_original_type || $corner != $this->transparent) { - $correct_colors = $this->colorsAreEqual($color, $corner); - $this->assertTrue($correct_colors, new FormattableMarkup('Image %file object after %action action has the correct color placement at corner %corner.', - ['%file' => $file, '%action' => $op, '%corner' => $key])); - } - } - } - - // Check that saved image reloads without raising PHP errors. - $image_reloaded = $this->imageFactory->get($file_path); - $resource = $image_reloaded->getToolkit()->getResource(); + $ret = []; + foreach ([ + 'image-test.png', + 'image-test.gif', + 'image-test-no-transparency.gif', + 'image-test.jpg', + 'img-test.webp', + ] as $file_name) { + foreach ($test_cases as $test_case => $values) { + $operation = $values['operation']; + $arguments = $values['arguments']; + unset($values['operation'], $values['arguments']); + $ret[] = [$file_name, $test_case, $operation, $arguments, $values]; } } - // Test creation of image from scratch, and saving to storage. - foreach ([IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_WEBP] as $type) { - $image = $this->imageFactory->get(); - $image->createNew(50, 20, image_type_to_extension($type, FALSE), '#ffff00'); - $file = 'from_null' . image_type_to_extension($type); - $file_path = $directory . '/' . $file; - $this->assertEquals(50, $image->getWidth(), new FormattableMarkup('Image file %file has the correct width.', ['%file' => $file])); - $this->assertEquals(20, $image->getHeight(), new FormattableMarkup('Image file %file has the correct height.', ['%file' => $file])); - $this->assertEquals(image_type_to_mime_type($type), $image->getMimeType(), new FormattableMarkup('Image file %file has the correct MIME type.', ['%file' => $file])); - $this->assertTrue($image->save($file_path), new FormattableMarkup('Image %file created anew from a null image was saved.', ['%file' => $file])); + return $ret; + } - // Reload saved image. - $image_reloaded = $this->imageFactory->get($file_path); - if (!$image_reloaded->isValid()) { - $this->fail(new FormattableMarkup('Could not load image %file.', ['%file' => $file])); + /** + * Since PHP can't visually check that our images have been manipulated + * properly, build a list of expected color values for each of the corners and + * the expected height and widths for the final images. + * + * @dataProvider providerTestImageFiles + */ + public function testManipulations(string $file_name, string $test_case, string $operation, array $arguments, array $expected): void { + // Load up a fresh image. + $image = $this->imageFactory->get('core/tests/fixtures/files/' . $file_name); + $toolkit = $image->getToolkit(); + $this->assertTrue($image->isValid()); + $image_original_type = $image->getToolkit()->getType(); + + $this->assertTrue(imageistruecolor($toolkit->getResource()), "Image '$file_name' after load should be a truecolor image, but it is not."); + + // Perform our operation. + $image->apply($operation, $arguments); + + // Flush Image object to disk storage. + $file_path = $this->directory . '/' . $test_case . image_type_to_extension($image->getToolkit()->getType()); + $image->save($file_path); + + // Check that the both the GD object and the Image object have an accurate + // record of the dimensions. + if (isset($expected['height']) && isset($expected['width'])) { + $this->assertSame($expected['height'], imagesy($toolkit->getResource()), "Image '$file_name' after '$test_case' should have a proper height."); + $this->assertSame($expected['width'], imagesx($toolkit->getResource()), "Image '$file_name' after '$test_case' should have a proper width."); + $this->assertSame($expected['height'], $image->getHeight(), "Image '$file_name' after '$test_case' should have a proper height."); + $this->assertSame($expected['width'], $image->getWidth(), "Image '$file_name' after '$test_case' should have a proper width."); + } + + // Now check each of the corners to ensure color correctness. + foreach ($expected['corners'] as $key => $expected_color) { + // The test gif that does not have transparency color set is a + // special case. + if ($file_name === 'image-test-no-transparency.gif') { + if ($test_case == 'desaturate') { + // For desaturating, keep the expected color from the test + // data, but set alpha channel to fully opaque. + $expected_color[3] = 0; + } + elseif ($expected_color === static::TRANSPARENT) { + // Set expected pixel to yellow where the others have + // transparent. + $expected_color = static::YELLOW; + } + } + + // Get the location of the corner. + switch ($key) { + case 0: + $x = 0; + $y = 0; + break; + + case 1: + $x = $image->getWidth() - 1; + $y = 0; + break; + + case 2: + $x = $image->getWidth() - 1; + $y = $image->getHeight() - 1; + break; + + case 3: + $x = 0; + $y = $image->getHeight() - 1; + break; + } + $actual_color = $this->getPixelColor($image, $x, $y); + + // If image cannot handle transparent colors, skip the pixel color test. + if ($actual_color[3] === 0 && $expected_color[3] === 127) { continue; } - $this->assertEquals(50, $image_reloaded->getWidth(), new FormattableMarkup('Image file %file has the correct width.', ['%file' => $file])); - $this->assertEquals(20, $image_reloaded->getHeight(), new FormattableMarkup('Image file %file has the correct height.', ['%file' => $file])); - $this->assertEquals(image_type_to_mime_type($type), $image_reloaded->getMimeType(), new FormattableMarkup('Image file %file has the correct MIME type.', ['%file' => $file])); - if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) { - $this->assertEquals('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), new FormattableMarkup('Image file %file has the correct transparent color channel set.', ['%file' => $file])); - } - else { - $this->assertNull($image_reloaded->getToolkit()->getTransparentColor(), new FormattableMarkup('Image file %file has no color channel set.', ['%file' => $file])); - } + + // JPEG has small differences in color after processing. + $tolerance = $image_original_type === IMAGETYPE_JPEG ? 3 : 0; + + $this->assertColorsAreEqual($expected_color, $actual_color, $tolerance, "Image '$file_name' object after '$test_case' action has the correct color placement at corner '$key'"); } - // Test failures of the 'create_new' operation. + // Check that saved image reloads without raising PHP errors. + $image_reloaded = $this->imageFactory->get($file_path); + if (PHP_VERSION_ID >= 80000) { + $this->assertInstanceOf(\GDImage::class, $image_reloaded->getToolkit()->getResource()); + } + else { + $this->assertIsResource($image_reloaded->getToolkit()->getResource()); + $this->assertSame(get_resource_type($image_reloaded->getToolkit()->getResource()), 'gd'); + } + } + + /** + * @covers ::getSupportedExtensions + * @covers ::extensionToImageType + */ + public function testSupportedExtensions(): void { + // Test the list of supported extensions. + $expected_extensions = ['png', 'gif', 'jpeg', 'jpg', 'jpe', 'webp']; + $this->assertEqualsCanonicalizing($expected_extensions, $this->imageFactory->getSupportedExtensions()); + + // Test that the supported extensions map to correct internal GD image + // types. + $expected_image_types = [ + 'png' => IMAGETYPE_PNG, + 'gif' => IMAGETYPE_GIF, + 'jpeg' => IMAGETYPE_JPEG, + 'jpg' => IMAGETYPE_JPEG, + 'jpe' => IMAGETYPE_JPEG, + 'webp' => IMAGETYPE_WEBP, + ]; + $image = $this->imageFactory->get(); + foreach ($expected_image_types as $extension => $expected_image_type) { + $this->assertSame($expected_image_type, $image->getToolkit()->extensionToImageType($extension)); + } + } + + /** + * Data provider for ::testCreateImageFromScratch(). + */ + public function providerSupportedImageTypes(): array { + return [ + [IMAGETYPE_PNG], + [IMAGETYPE_GIF], + [IMAGETYPE_JPEG], + [IMAGETYPE_WEBP], + ]; + } + + /** + * Tests that GD functions for the image type are available. + * + * @dataProvider providerSupportedImageTypes + */ + public function testGdFunctionsExist(int $type): void { + $extension = image_type_to_extension($type, FALSE); + $this->assertTrue(function_exists("imagecreatefrom$extension"), "imagecreatefrom$extension should exist."); + $this->assertTrue(function_exists("image$extension"), "image$extension should exist."); + } + + /** + * Tests creation of image from scratch, and saving to storage. + * + * @dataProvider providerSupportedImageTypes + */ + public function testCreateImageFromScratch(int $type): void { + // Build an image from scratch. + $image = $this->imageFactory->get(); + $image->createNew(50, 20, image_type_to_extension($type, FALSE), '#ffff00'); + $file = 'from_null' . image_type_to_extension($type); + $file_path = $this->directory . '/' . $file; + $this->assertSame(50, $image->getWidth()); + $this->assertSame(20, $image->getHeight()); + $this->assertSame(image_type_to_mime_type($type), $image->getMimeType()); + $this->assertTrue($image->save($file_path), "Image '$file' should have been saved successfully, but it has not."); + + // Reload and check saved image. + $image_reloaded = $this->imageFactory->get($file_path); + $this->assertTrue($image_reloaded->isValid()); + $this->assertSame(50, $image_reloaded->getWidth()); + $this->assertSame(20, $image_reloaded->getHeight()); + $this->assertSame(image_type_to_mime_type($type), $image_reloaded->getMimeType()); + if ($image_reloaded->getToolkit()->getType() == IMAGETYPE_GIF) { + $this->assertSame('#ffff00', $image_reloaded->getToolkit()->getTransparentColor(), "Image '$file' after reload should have color channel set to #ffff00, but it has not."); + } + else { + $this->assertNull($image_reloaded->getToolkit()->getTransparentColor(), "Image '$file' after reload should have no color channel set, but it has."); + } + } + + /** + * Tests failures of the 'create_new' operation. + */ + public function testCreateNewFailures(): void { $image = $this->imageFactory->get(); $image->createNew(-50, 20); $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.'); @@ -472,11 +493,7 @@ class ToolkitGdTest extends KernelTestBase { /** * Tests for GIF images with transparency. */ - public function testGifTransparentImages() { - // Prepare a directory for test file results. - $directory = Settings::get('file_public_path') . '/imagetest'; - \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY); - + public function testGifTransparentImages(): void { // Test loading an indexed GIF image with transparent color set. // Color at top-right pixel should be fully transparent. $file = 'image-test-transparent-indexed.gif'; @@ -484,20 +501,20 @@ class ToolkitGdTest extends KernelTestBase { $resource = $image->getToolkit()->getResource(); $color_index = imagecolorat($resource, $image->getWidth() - 1, 0); $color = array_values(imagecolorsforindex($resource, $color_index)); - $this->assertEquals($this->rotateTransparent, $color, "Image {$file} after load has full transparent color at corner 1."); + $this->assertEquals(static::ROTATE_TRANSPARENT, $color, "Image {$file} after load has full transparent color at corner 1."); // Test deliberately creating a GIF image with no transparent color set. // Color at top-right pixel should be fully transparent while in memory, // fully opaque after flushing image to file. $file = 'image-test-no-transparent-color-set.gif'; - $file_path = $directory . '/' . $file; + $file_path = $this->directory . '/' . $file; // Create image. $image = $this->imageFactory->get(); $image->createNew(50, 20, 'gif', NULL); $resource = $image->getToolkit()->getResource(); $color_index = imagecolorat($resource, $image->getWidth() - 1, 0); $color = array_values(imagecolorsforindex($resource, $color_index)); - $this->assertEquals($this->rotateTransparent, $color, "New GIF image with no transparent color set after creation has full transparent color at corner 1."); + $this->assertEquals(static::ROTATE_TRANSPARENT, $color, "New GIF image with no transparent color set after creation has full transparent color at corner 1."); // Save image. $this->assertTrue($image->save($file_path), "New GIF image {$file} was saved."); // Reload image. @@ -522,37 +539,20 @@ class ToolkitGdTest extends KernelTestBase { // can be loaded correctly. $file = 'image-test-transparent-out-of-range.gif'; $image = $this->imageFactory->get('core/tests/fixtures/files/' . $file); - $toolkit = $image->getToolkit(); - - if (!$image->isValid()) { - $this->fail(new FormattableMarkup('Could not load image %file.', ['%file' => $file])); - } - else { - // All images should be converted to truecolor when loaded. - $image_truecolor = imageistruecolor($toolkit->getResource()); - $this->assertTrue($image_truecolor, new FormattableMarkup('Image %file after load is a truecolor image.', ['%file' => $file])); - } + $this->assertTrue($image->isValid(), "Image '$file' after load should be valid, but it is not."); + $this->assertTrue(imageistruecolor($image->getToolkit()->getResource()), "Image '$file' after load should be a truecolor image, but it is not."); } /** * Tests calling a missing image operation plugin. */ - public function testMissingOperation() { - - // Test that the image factory is set to use the GD toolkit. - $this->assertEquals('gd', $this->imageFactory->getToolkitId(), 'The image factory is set to use the \'gd\' image toolkit.'); - - // An image file that will be tested. - $file = 'image-test.png'; - + public function testMissingOperation(): void { // Load up a fresh image. - $image = $this->imageFactory->get('core/tests/fixtures/files/' . $file); - if (!$image->isValid()) { - $this->fail(new FormattableMarkup('Could not load image %file.', ['%file' => $file])); - } + $image = $this->imageFactory->get('core/tests/fixtures/files/image-test.png'); + $this->assertTrue($image->isValid(), "Image 'image-test.png' after load should be valid, but it is not."); // Try perform a missing toolkit operation. - $this->assertFalse($image->apply('missing_op', []), 'Calling a missing image toolkit operation plugin fails.'); + $this->assertFalse($image->apply('missing_op', []), 'Calling a missing image toolkit operation plugin should fail, but it did not.'); } } diff --git a/frontend/drupal9/web/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php b/frontend/drupal9/web/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php index 18deac7e3..fe559499f 100644 --- a/frontend/drupal9/web/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php +++ b/frontend/drupal9/web/core/tests/Drupal/Tests/BrowserHtmlDebugTrait.php @@ -179,11 +179,14 @@ trait BrowserHtmlDebugTrait { $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line']; $html_output .= '
' . $request->getMethod() . ' request to: ' . $request->getUri(); - // Get the response body as a string. Any errors are silenced as - // tests should not fail if there is a problem. On PHP 7.4 - // \Drupal\Tests\migrate\Functional\process\DownloadFunctionalTest - // fails without the usage of a silence operator. - $body = @(string) $response->getBody(); + /** @var \Psr\Http\Message\StreamInterface $stream */ + $stream = $response->getBody(); + + // Get the response body as a string. + $body = $stream->isReadable() + ? (string) $stream + : 'Response is not readable.'; + // On redirect responses (status code starting with '3') we need // to remove the meta tag that would do a browser refresh. We // don't want to redirect developers away when they look at the diff --git a/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php b/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php index 8ff94785d..1bc89295b 100644 --- a/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php +++ b/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php @@ -3,10 +3,10 @@ namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional; use Composer\Util\Filesystem; +use Drupal\BuildTests\Framework\BuildTestBase; use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait; use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait; use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures; -use PHPUnit\Framework\TestCase; /** * Tests Composer Hooks that run scaffold operations. @@ -22,7 +22,8 @@ use PHPUnit\Framework\TestCase; * * @group Scaffold */ -class ComposerHookTest extends TestCase { +class ComposerHookTest extends BuildTestBase { + use ExecTrait; use AssertUtilsTrait; @@ -120,9 +121,10 @@ class ComposerHookTest extends TestCase { $this->mustExec("composer install --no-ansi", $sut); // Require a project that is not allowed to scaffold and confirm that we // get a warning, and it does not scaffold. - $stdout = $this->mustExec("composer require --no-ansi --no-interaction fixtures/drupal-assets-fixture:dev-main fixtures/scaffold-override-fixture:dev-main", $sut); + $this->executeCommand("composer require --no-ansi --no-interaction fixtures/drupal-assets-fixture:dev-main fixtures/scaffold-override-fixture:dev-main", $sut); + $this->assertCommandSuccessful(); $this->assertFileDoesNotExist($sut . '/sites/default/default.settings.php'); - $this->assertStringContainsString("Not scaffolding files for fixtures/scaffold-override-fixture, because it is not listed in the element 'extra.drupal-scaffold.allowed-packages' in the root-level composer.json file.", $stdout); + $this->assertErrorOutputContains('See https://getcomposer.org/allow-plugins'); } /** diff --git a/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl b/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl index f5f5d2a28..d28a71273 100644 --- a/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl +++ b/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl @@ -64,5 +64,10 @@ "libraries/{$name}": ["type:drupal-library"], "drush/Commands/contrib/{$name}": ["type:drupal-drush"] } + }, + "config": { + "allow-plugins": { + "drupal/core-composer-scaffold": true + } } } diff --git a/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-drupal/composer.json.tmpl b/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-drupal/composer.json.tmpl index 00a91d795..bf8b11d6c 100644 --- a/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-drupal/composer.json.tmpl +++ b/frontend/drupal9/web/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/drupal-drupal/composer.json.tmpl @@ -67,5 +67,10 @@ "libraries/{$name}": ["type:drupal-library"], "drush/Commands/contrib/{$name}": ["type:drupal-drush"] } - } + }, + "config": { + "allow-plugins": { + "drupal/core-composer-scaffold": true + } + } } diff --git a/frontend/drupal9/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php b/frontend/drupal9/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php index bcc2eda6c..4e9a3ec6f 100644 --- a/frontend/drupal9/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php +++ b/frontend/drupal9/web/core/tests/Drupal/Tests/Core/Site/SettingsTest.php @@ -353,7 +353,7 @@ class SettingsTest extends UnitTestCase { if (!empty($expected_autoload)) { $class_loader->expects($this->once()) ->method('addPsr4') - ->with($expected_namespace . '\\', $expected_autoload); + ->with($expected_namespace . '\\', vfsStream::url('root') . '/' . $expected_autoload); } else { $class_loader->expects($this->never()) diff --git a/frontend/drupal9/web/core/themes/olivero/templates/includes/get-started.html.twig b/frontend/drupal9/web/core/themes/olivero/templates/includes/get-started.html.twig index e96327a7f..fd9600cb9 100644 --- a/frontend/drupal9/web/core/themes/olivero/templates/includes/get-started.html.twig +++ b/frontend/drupal9/web/core/themes/olivero/templates/includes/get-started.html.twig @@ -14,7 +14,7 @@ {% set drupal_community = 'https://www.drupal.org/community' %} {% set drupal_values = 'https://www.drupal.org/about/values-and-principles' %} {% set drupal_user_guide = 'https://www.drupal.org/docs/user_guide/en/index.html' %} -{% set create_content = '/node/add' %} +{% set create_content = path('node.add_page') %} {% set drupal_extend = 'https://www.drupal.org/docs/extending-drupal' %} {% set drupal_global_training_days = 'https://groups.drupal.org/global-training-days' %} {% set drupal_events = 'https://www.drupal.org/community/events' %} diff --git a/frontend/drupal9/web/core/themes/olivero/templates/views/views-mini-pager.html.twig b/frontend/drupal9/web/core/themes/olivero/templates/views/views-mini-pager.html.twig index b64be942d..a9acc73a0 100644 --- a/frontend/drupal9/web/core/themes/olivero/templates/views/views-mini-pager.html.twig +++ b/frontend/drupal9/web/core/themes/olivero/templates/views/views-mini-pager.html.twig @@ -38,7 +38,7 @@ {% apply spaceless %}