Drupal core update

This commit is contained in:
Robert 2022-07-07 14:42:09 +02:00
parent d1f95f27f0
commit 7d59824d2d
56 changed files with 1228 additions and 749 deletions

View File

@ -534,6 +534,10 @@ Decoupled Menus Initiative
Media Initiative Media Initiative
- Janez Urevc 'slashrsm' https://www.drupal.org/u/slashrsm - 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 Core mentoring coordinators
--------------------------- ---------------------------

View File

@ -558,7 +558,9 @@ function drupal_flush_all_caches($kernel = NULL) {
// Wipe the Twig PHP Storage cache. // Wipe the Twig PHP Storage cache.
\Drupal::service('twig')->invalidate(); \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(); \Drupal::service('theme_handler')->refreshInfo();
// In case the active theme gets requested later in the same request we need // In case the active theme gets requested later in the same request we need
// to reset the theme manager. // to reset the theme manager.

View File

@ -75,7 +75,7 @@ class Drupal {
/** /**
* The current system version. * The current system version.
*/ */
const VERSION = '9.4.1'; const VERSION = '9.4.2';
/** /**
* Core API compatibility. * Core API compatibility.

View File

@ -130,13 +130,6 @@ class Composer {
$vendor_dir . '/symfony/http-kernel/TerminableInterface.php', $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)) { if ($repository->findPackage('symfony/dependency-injection', $constraint)) {
$autoload['classmap'] = array_merge($autoload['classmap'], [ $autoload['classmap'] = array_merge($autoload['classmap'], [
$vendor_dir . '/symfony/dependency-injection/ContainerAwareInterface.php', $vendor_dir . '/symfony/dependency-injection/ContainerAwareInterface.php',

View File

@ -16,12 +16,34 @@ class DependencySerializationTraitPass implements CompilerPassInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function process(ContainerBuilder $container) { public function process(ContainerBuilder $container) {
$decorations = new \SplPriorityQueue();
$order = PHP_INT_MAX;
foreach ($container->getDefinitions() as $service_id => $definition) { foreach ($container->getDefinitions() as $service_id => $definition) {
// Only add the property to services that are public (as private services // Only add the property to services that are public (as private services
// can not be reloaded through Container::get()) and are objects. // can not be reloaded through Container::get()) and are objects.
if (!$definition->hasTag('parameter_service') && $definition->isPublic()) { if (!$definition->hasTag('parameter_service') && $definition->isPublic()) {
$definition->setProperty('_serviceId', $service_id); $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);
}
} }
} }

View File

@ -207,7 +207,7 @@ final class Settings {
// the database. Therefore, allow the connection info to specify an // the database. Therefore, allow the connection info to specify an
// autoload directory for the driver. // autoload directory for the driver.
if (isset($info['autoload'])) { if (isset($info['autoload'])) {
$class_loader->addPsr4($info['namespace'] . '\\', $info['autoload']); $class_loader->addPsr4($info['namespace'] . '\\', $app_root . '/' . $info['autoload']);
} }
} }
} }

View File

@ -127,7 +127,7 @@ ckeditor5.plugin.ckeditor5_list:
# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media # Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media
ckeditor5.plugin.media_media: ckeditor5.plugin.media_media:
type: mapping type: mapping
label: List label: Media
mapping: mapping:
allow_view_mode_override: allow_view_mode_override:
type: boolean type: boolean

View File

@ -136,7 +136,7 @@ class ConfigSingleExportForm extends FormBase {
*/ */
public function updateConfigurationType($form, FormStateInterface $form_state) { public function updateConfigurationType($form, FormStateInterface $form_state) {
$form['config_name']['#options'] = $this->findConfiguration($form_state->getValue('config_type')); $form['config_name']['#options'] = $this->findConfiguration($form_state->getValue('config_type'));
unset($form['export']['#value']); $form['export']['#value'] = NULL;
return $form; return $form;
} }

View File

@ -4,6 +4,7 @@ namespace Drupal\config_override_test;
use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryOverrideInterface; use Drupal\Core\Config\ConfigFactoryOverrideInterface;
use Drupal\Core\Config\StorageInterface;
/** /**
* Tests module overrides for configuration. * Tests module overrides for configuration.

View File

@ -49,7 +49,7 @@ class FieldInstanceSettings extends ProcessPluginBase {
case 'imagefield_widget': case 'imagefield_widget':
$settings['file_extensions'] = $widget_settings['file_extensions']; $settings['file_extensions'] = $widget_settings['file_extensions'];
$settings['file_directory'] = $widget_settings['file_path']; $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'] = $widget_settings['alt'];
$settings['alt_field_required'] = $widget_settings['custom_alt']; $settings['alt_field_required'] = $widget_settings['custom_alt'];
$settings['title_field'] = $widget_settings['title']; $settings['title_field'] = $widget_settings['title'];

View File

@ -0,0 +1,81 @@
<?php
namespace Drupal\Tests\field\Unit\Plugin\migrate\process\d6;
use Drupal\field\Plugin\migrate\process\d6\FieldInstanceSettings;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
// cspell:ignore imagefield
/**
* @coversDefaultClass \Drupal\field\Plugin\migrate\process\d6\FieldInstanceSettings
* @group field
*/
class FieldInstanceSettingsTest extends UnitTestCase {
/**
* @covers ::getSettings
*
* @dataProvider getSettingsProvider
*/
public function testGetSettings($field_type, $instance_settings, $expected) {
$instance_settings = unserialize($instance_settings);
$migration = $this->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' => '',
],
],
];
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Drupal\Tests\hal\Functional\quickedit;
use Drupal\Tests\hal\Functional\layout_builder\LayoutBuilderEntityViewDisplayHalJsonAnonTest;
/**
* @group hal
* @group legacy
*/
class QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest extends LayoutBuilderEntityViewDisplayHalJsonAnonTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,18 @@
<?php
namespace Drupal\Tests\hal\Functional\quickedit;
use Drupal\Tests\hal\Functional\layout_builder\LayoutBuilderEntityViewDisplayHalJsonBasicAuthTest;
/**
* @group hal
* @group legacy
*/
class QuickEditLayoutBuilderEntityViewDisplayHalJsonBasicAuthTest extends LayoutBuilderEntityViewDisplayHalJsonBasicAuthTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,18 @@
<?php
namespace Drupal\Tests\hal\Functional\quickedit;
use Drupal\Tests\hal\Functional\layout_builder\LayoutBuilderEntityViewDisplayHalJsonCookieTest;
/**
* @group hal
* @group legacy
*/
class QuickEditLayoutBuilderEntityViewDisplayHalJsonCookieTest extends LayoutBuilderEntityViewDisplayHalJsonCookieTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -192,7 +192,7 @@ class ResourceObjectNormalizer extends NormalizerBase {
// @todo Replace this workaround after https://www.drupal.org/node/3043245 // @todo Replace this workaround after https://www.drupal.org/node/3043245
// or remove the need for this in https://www.drupal.org/node/2942975. // or remove the need for this in https://www.drupal.org/node/2942975.
// See \Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer. // See \Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer.
if ($context['resource_object']->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']); unset($field['layout_builder']['sections']);
} }

View File

@ -26,7 +26,6 @@ use Drupal\layout_builder\InlineBlockEntityOperations;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\QuickEditIntegration;
/** /**
* Implements hook_help(). * 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) { if ($display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) {
unset($build['#contextual_links']); 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(). * Implements hook_entity_translation_create().
*/ */

View File

@ -18,7 +18,6 @@ use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\LayoutEntityHelperTrait;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\QuickEditIntegration;
use Drupal\layout_builder\Section; use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent; use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionListTrait; use Drupal\layout_builder\SectionListTrait;
@ -473,7 +472,7 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getComponent($name) { 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(); $plugin = $section_component->getPlugin();
if ($plugin instanceof ConfigurableInterface) { if ($plugin instanceof ConfigurableInterface) {
$configuration = $plugin->getConfiguration(); $configuration = $plugin->getConfiguration();
@ -485,43 +484,6 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
return parent::getComponent($name); 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. * Gets the component for a given field name if any.
* *

View File

@ -44,18 +44,12 @@ class InlineBlockEntityOperations implements ContainerInjectionInterface {
/** /**
* Constructs a new EntityOperations object. * 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 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service. * The entity type manager service.
* @param \Drupal\layout_builder\InlineBlockUsageInterface $usage * @param \Drupal\layout_builder\InlineBlockUsageInterface $usage
* Inline block usage tracking service. * Inline block usage tracking service.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager * @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) { public function __construct(EntityTypeManagerInterface $entityTypeManager, InlineBlockUsageInterface $usage, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entityTypeManager; $this->entityTypeManager = $entityTypeManager;

View File

@ -2,21 +2,9 @@
namespace Drupal\layout_builder; namespace Drupal\layout_builder;
use Drupal\Component\Utility\NestedArray; @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\Core\Cache\CacheableMetadata;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\quickedit\LayoutBuilderIntegration;
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;
/** /**
* Helper methods for Quick Edit module integration. * Helper methods for Quick Edit module integration.
@ -24,298 +12,4 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal * @internal
* This is an internal utility class wrapping hook implementations. * This is an internal utility class wrapping hook implementations.
*/ */
class QuickEditIntegration implements ContainerInjectionInterface { class QuickEditIntegration extends LayoutBuilderIntegration {}
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;
}
}

View File

@ -76,8 +76,11 @@ class DownloadFunctionalTest extends BrowserTestBase {
$this->assertCount(1, $messages); $this->assertCount(1, $messages);
$message = reset($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(); $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); $this->assertEquals(MigrationInterface::MESSAGE_ERROR, $message->level);
// Check that the second row was migrated successfully. // Check that the second row was migrated successfully.

View File

@ -75,6 +75,7 @@ class Vid extends NumericArgument {
->accessCheck(FALSE) ->accessCheck(FALSE)
->allRevisions() ->allRevisions()
->groupBy('title') ->groupBy('title')
->condition('vid', $this->value, 'IN')
->execute(); ->execute();
foreach ($results as $result) { foreach ($results as $result) {

View File

@ -13,45 +13,12 @@ base_table: node_field_data
base_field: nid base_field: nid
display: display:
default: default:
display_plugin: default
id: default id: default
display_title: Default display_title: Default
display_plugin: default
position: 0 position: 0
display_options: display_options:
access: title: test_node_revision_id_argument
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
items_per_page: null
offset: 0
style:
type: default
row:
type: fields
fields: fields:
title: title:
id: title id: title
@ -60,6 +27,9 @@ display:
relationship: none relationship: none
group_type: group group_type: group
admin_label: '' admin_label: ''
entity_type: node
entity_field: title
plugin_id: field
label: '' label: ''
exclude: false exclude: false
alter: alter:
@ -115,16 +85,30 @@ display:
multi_type: separator multi_type: separator
separator: ', ' separator: ', '
field_api_classes: false field_api_classes: false
entity_type: node pager:
entity_field: title type: none
plugin_id: field options:
filters: { } offset: 0
sorts: { } items_per_page: null
title: test_node_revision_id_argument exposed_form:
header: { } type: basic
footer: { } 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: { } empty: { }
relationships: { } sorts: { }
arguments: arguments:
vid: vid:
id: vid id: vid
@ -133,13 +117,16 @@ display:
relationship: none relationship: none
group_type: group group_type: group
admin_label: '' admin_label: ''
entity_type: node
entity_field: vid
plugin_id: node_vid
default_action: ignore default_action: ignore
exception: exception:
value: all value: all
title_enable: false title_enable: false
title: All title: All
title_enable: false title_enable: true
title: '' title: '{{ arguments.vid }}'
default_argument_type: fixed default_argument_type: fixed
default_argument_options: default_argument_options:
argument: '' argument: ''
@ -147,8 +134,8 @@ display:
summary_options: summary_options:
base_path: '' base_path: ''
count: true count: true
items_per_page: 25
override: false override: false
items_per_page: 25
summary: summary:
sort_order: asc sort_order: asc
number_of_records: 0 number_of_records: 0
@ -160,38 +147,51 @@ display:
validate_options: { } validate_options: { }
break_phrase: false break_phrase: false
not: false not: false
entity_type: node filters: { }
entity_field: vid
plugin_id: node_vid
display_extenders: { }
filter_groups: filter_groups:
operator: AND operator: AND
groups: { } 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: cache_metadata:
max-age: -1
contexts: contexts:
- 'languages:language_content' - 'languages:language_content'
- 'languages:language_interface' - 'languages:language_interface'
- url - url
- 'user.node_grants:view' - 'user.node_grants:view'
- user.permissions - user.permissions
cacheable: false
max-age: -1
tags: { } tags: { }
cacheable: false
page_1: page_1:
display_plugin: page
id: page_1 id: page_1
display_title: Page display_title: Page
display_plugin: page
position: 1 position: 1
display_options: display_options:
display_extenders: { } display_extenders: { }
path: test-revision-vid-argument path: test-revision-vid-argument
cache_metadata: cache_metadata:
max-age: -1
contexts: contexts:
- 'languages:language_content' - 'languages:language_content'
- 'languages:language_interface' - 'languages:language_interface'
- url - url
- 'user.node_grants:view' - 'user.node_grants:view'
- user.permissions - user.permissions
cacheable: false
max-age: -1
tags: { } tags: { }
cacheable: false

View File

@ -46,13 +46,16 @@ class ArgumentNodeRevisionIdTest extends ViewsKernelTestBase {
NodeType::create(['type' => 'page', 'name' => 'page'])->save(); NodeType::create(['type' => 'page', 'name' => 'page'])->save();
$node = Node::create(['type' => 'page', 'title' => 'test1', 'uid' => 1]); $node = Node::create(['type' => 'page', 'title' => 'test1', 'uid' => 1]);
$node->save(); $node->save();
$first_revision_id = $node->getRevisionId();
$node->setNewRevision(); $node->setNewRevision();
$node->setTitle('test2'); $node->setTitle('test2');
$node->save(); $node->save();
$second_revision_id = $node->getRevisionId();
$view_nid = Views::getView('test_node_revision_id_argument'); $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->assertIdenticalResultset($view_nid, [['title' => 'test2']]);
$this->assertSame('test2', $view_nid->getTitle());
} }
/** /**

View File

@ -16,6 +16,8 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\quickedit\Entity\QuickEditLayoutBuilderEntityViewDisplay;
use Drupal\quickedit\LayoutBuilderIntegration;
/** /**
* Implements hook_help(). * 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(). * Implements hook_entity_view_alter().
*/ */
function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) { 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'])) { if (isset($build['#embed'])) {
return; return;
} }
@ -189,3 +209,12 @@ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityVie
$build['#attributes']['data-quickedit-entity-id'] = $entity->getEntityTypeId() . '/' . $entity->id(); $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);
}

View File

@ -0,0 +1,68 @@
<?php
namespace Drupal\quickedit\Entity;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\quickedit\LayoutBuilderIntegration;
/**
* Provides an entity view display entity that has a layout with quickedit.
*/
class QuickEditLayoutBuilderEntityViewDisplay extends LayoutBuilderEntityViewDisplay {
/**
* {@inheritdoc}
*/
public function getComponent($name) {
if ($this->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;
}
}

View File

@ -0,0 +1,321 @@
<?php
namespace Drupal\quickedit;
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;
/**
* Helper methods for Layout Builder module integration.
*
* @internal
* This is an internal utility class wrapping hook implementations.
*/
class LayoutBuilderIntegration 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 LayoutBuilderIntegration 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 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;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Drupal\Tests\quickedit\Functional\Jsonapi;
use Drupal\Tests\layout_builder\Functional\Jsonapi\LayoutBuilderEntityViewDisplayTest;
/**
* JSON:API integration test for the "EntityViewDisplay" config entity type.
*
* @group jsonapi
* @group layout_builder
* @group quickedit
*/
class QuickEditLayoutBuilderEntityViewDisplayTest extends LayoutBuilderEntityViewDisplayTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\quickedit\Functional\Rest;
use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayJsonAnonTest;
/**
* @group quickedit
* @group layout_builder
* @group rest
*/
class QuickEditLayoutBuilderEntityViewDisplayJsonAnonTest extends LayoutBuilderEntityViewDisplayJsonAnonTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\quickedit\Functional\Rest;
use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayJsonBasicAuthTest;
/**
* @group quickedit
* @group layout_builder
* @group rest
*/
class QuickEditLayoutBuilderEntityViewDisplayJsonBasicAuthTest extends LayoutBuilderEntityViewDisplayJsonBasicAuthTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\quickedit\Functional\Rest;
use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayJsonCookieTest;
/**
* @group quickedit
* @group layout_builder
* @group rest
*/
class QuickEditLayoutBuilderEntityViewDisplayJsonCookieTest extends LayoutBuilderEntityViewDisplayJsonCookieTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\quickedit\Functional\Rest;
use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayXmlAnonTest;
/**
* @group quickedit
* @group layout_builder
* @group rest
*/
class QuickEditLayoutBuilderEntityViewDisplayXmlAnonTest extends LayoutBuilderEntityViewDisplayXmlAnonTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\quickedit\Functional\Rest;
use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayXmlBasicAuthTest;
/**
* @group quickedit
* @group layout_builder
* @group rest
*/
class QuickEditLayoutBuilderEntityViewDisplayXmlBasicAuthTest extends LayoutBuilderEntityViewDisplayXmlBasicAuthTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\quickedit\Functional\Rest;
use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayXmlCookieTest;
/**
* @group quickedit
* @group layout_builder
* @group rest
*/
class QuickEditLayoutBuilderEntityViewDisplayXmlCookieTest extends LayoutBuilderEntityViewDisplayXmlCookieTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['quickedit'];
}

View File

@ -14,7 +14,7 @@ use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
/** /**
* @group quickedit * @group quickedit
*/ */
class QuickEditIntegrationTest extends QuickEditJavascriptTestBase { class LayoutBuilderIntegrationTest extends QuickEditJavascriptTestBase {
use EntityReferenceTestTrait; use EntityReferenceTestTrait;

View File

@ -0,0 +1,5 @@
name: 'Decorated Service Test'
type: module
description: 'Support module for decorated service test.'
package: Testing
version: VERSION

View File

@ -0,0 +1,17 @@
services:
test_service:
class: 'Drupal\decorated_service_test\TestService'
test_service_decorator:
class: 'Drupal\decorated_service_test\TestServiceDecorator'
public: false
decorates: test_service
test_service2:
class: 'Drupal\decorated_service_test\TestService'
test_service2_decorator:
class: 'Drupal\decorated_service_test\TestServiceDecorator'
public: false
decorates: test_service2
test_service2_decorator2:
class: 'Drupal\decorated_service_test\TestServiceDecorator'
public: false
decorates: test_service2

View File

@ -0,0 +1,7 @@
<?php
namespace Drupal\decorated_service_test;
class TestService {
}

View File

@ -0,0 +1,7 @@
<?php
namespace Drupal\decorated_service_test;
class TestServiceDecorator extends TestService {
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\system\Kernel;
use Drupal\decorated_service_test\TestServiceDecorator;
use Drupal\KernelTests\KernelTestBase;
/**
* Test handling of decorated services in DependencySerializationTraitPass.
*
* @group system
*/
class DecoratedServiceTest extends KernelTestBase {
protected static $modules = [
'decorated_service_test',
];
/**
* Check that decorated services keep their original service ID.
*/
public function testDecoratedServiceId() {
// Service decorated once.
$test_service = $this->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);
}
}

View File

@ -2492,8 +2492,6 @@ class ViewExecutable {
// state during unserialization. // state during unserialization.
$this->serializationData = [ $this->serializationData = [
'storage' => $this->storage->id(), 'storage' => $this->storage->id(),
'views_data' => $this->viewsData->_serviceId,
'route_provider' => $this->routeProvider->_serviceId,
'current_display' => $this->current_display, 'current_display' => $this->current_display,
'args' => $this->args, 'args' => $this->args,
'current_page' => $this->current_page, 'current_page' => $this->current_page,
@ -2520,8 +2518,8 @@ class ViewExecutable {
// Attach all necessary services. // Attach all necessary services.
$this->user = \Drupal::currentUser(); $this->user = \Drupal::currentUser();
$this->viewsData = \Drupal::service($this->serializationData['views_data']); $this->viewsData = \Drupal::service('views.views_data');
$this->routeProvider = \Drupal::service($this->serializationData['route_provider']); $this->routeProvider = \Drupal::service('router.route_provider');
// Restore the state of this executable. // Restore the state of this executable.
if ($request = \Drupal::request()) { if ($request = \Drupal::request()) {

View File

@ -155,9 +155,6 @@ class BasicTest extends WizardTestBase {
$this->assertSession()->pageTextContains($node1->label()); $this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->pageTextNotContains($node2->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. // Create a view with only a REST export.
$view4 = []; $view4 = [];
$view4['label'] = $this->randomMachineName(16); $view4['label'] = $this->randomMachineName(16);

View File

@ -77,7 +77,7 @@ const assetsFolder = `${coreFolder}/assets/vendor`;
{ {
pack: 'backbone', pack: 'backbone',
library: 'internal.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. // Only used to update the version number of the deprecated library.
{ {

View File

@ -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->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(); $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. // Ensure we used the project from our codebase.
$this->assertErrorOutputContains("Installing $project ($simulated_core_version): Symlinking from $package_dir"); $this->assertErrorOutputContains("Installing $project ($simulated_core_version): Symlinking from $package_dir");
@ -386,6 +394,16 @@ JSON;
"version" => $version, "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'];
}
}
} }
} }

View File

@ -265,6 +265,16 @@ abstract class BuildTestBase extends TestCase {
$this->assertStringContainsString($expected, $this->commandProcess->getErrorOutput()); $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. * Assert that text is present in the output of the most recent command.
* *

View File

@ -760,6 +760,9 @@ class BrowserTestBaseTest extends BrowserTestBase {
public function testInstall() { public function testInstall() {
$htaccess_filename = $this->tempFilesDirectory . '/.htaccess'; $htaccess_filename = $this->tempFilesDirectory . '/.htaccess';
$this->assertFileExists($htaccess_filename); $this->assertFileExists($htaccess_filename);
// Ensure the update module is not installed.
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('update'), 'The Update module is not installed.');
} }
/** /**

View File

@ -105,7 +105,8 @@ abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
// existing configuration. // existing configuration.
unset($parameters['forms']['install_configure_form']['site_name']); unset($parameters['forms']['install_configure_form']['site_name']);
unset($parameters['forms']['install_configure_form']['site_mail']); 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; return $parameters;
} }

View File

@ -131,6 +131,9 @@ class InstallerTest extends InstallerTestBase {
$module = $database->getProvider(); $module = $database->getProvider();
$module_handler = \Drupal::service('module_handler'); $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 // Assert that the module that is providing the database driver has been
// installed. // installed.
$this->assertTrue($module_handler->moduleExists($module)); $this->assertTrue($module_handler->moduleExists($module));

View File

@ -73,6 +73,19 @@ abstract class InstallerTestBase extends BrowserTestBase {
*/ */
protected $isInstalled = FALSE; 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} * {@inheritdoc}
*/ */

View File

@ -4,19 +4,30 @@ namespace Drupal\KernelTests\Core\Image;
use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageInterface; use Drupal\Core\Image\ImageInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Site\Settings;
use Drupal\KernelTests\KernelTestBase; use Drupal\KernelTests\KernelTestBase;
/** /**
* Tests that core image manipulations work properly: scale, resize, rotate, * Tests for the GD image toolkit.
* crop, scale and crop, and desaturate.
* *
* @coversDefaultClass \Drupal\system\Plugin\ImageToolkit\GDToolkit
* @group Image * @group Image
* @requires extension gd * @requires extension gd
*/ */
class ToolkitGdTest extends KernelTestBase { 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. * The image factory service.
* *
@ -25,33 +36,14 @@ class ToolkitGdTest extends KernelTestBase {
protected $imageFactory; 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 $directory;
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];
/** /**
* Used as rotate background colors. * {@inheritdoc}
*
* @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
*/ */
protected static $modules = ['system']; protected static $modules = ['system'];
@ -62,32 +54,41 @@ class ToolkitGdTest extends KernelTestBase {
parent::setUp(); parent::setUp();
$this->installConfig(['system']); $this->installConfig(['system']);
// Set the image factory service. // Set the image factory service.
$this->imageFactory = $this->container->get('image.factory'); $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) { protected function assertColorsAreEqual(array $expected, array $actual, int $tolerance, string $message = ''): void {
// Fully transparent pixels are equal, regardless of RGB. // Fully transparent colors are equal, regardless of RGB.
if ($color_a[3] == 127 && $color_b[3] == 127) { if ($actual[3] == 127 && $expected[3] == 127) {
return TRUE; return;
} }
$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);
foreach ($color_a as $key => $value) { $this->assertLessThanOrEqual($tolerance, $distance, $message . " - Actual: {" . implode(',', $actual) . "}, Expected: {" . implode(',', $expected) . "}, Distance: " . $distance . ", Tolerance: " . $tolerance);
if ($color_b[$key] != $value) {
return FALSE;
}
}
return TRUE;
} }
/** /**
* Function for finding a pixel's RGBa values. * 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(); $toolkit = $image->getToolkit();
$color_index = imagecolorat($toolkit->getResource(), $x, $y); $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 * Data provider for ::testManipulations().
* properly, build a list of expected color values for each of the corners and
* the expected height and widths for the final images.
*/ */
public function testManipulations() { public function providerTestImageFiles(): array {
// 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);
}
// Typically the corner colors will be unchanged. These colors are in the // Typically the corner colors will be unchanged. These colors are in the
// order of top-left, top-right, bottom-right, bottom-left. // order of top-left, top-right, bottom-right, bottom-left.
$default_corners = [$this->red, $this->green, $this->blue, $this->transparent]; $default_corners = [static::RED, static::GREEN, static::BLUE, static::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',
];
// Setup a list of tests to perform on each type. // Setup a list of tests to perform on each type.
$operations = [ $test_cases = [
'resize' => [ 'resize' => [
'function' => 'resize', 'operation' => 'resize',
'arguments' => ['width' => 20, 'height' => 10], 'arguments' => ['width' => 20, 'height' => 10],
'width' => 20, 'width' => 20,
'height' => 10, 'height' => 10,
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'scale_x' => [ 'scale_x' => [
'function' => 'scale', 'operation' => 'scale',
'arguments' => ['width' => 20], 'arguments' => ['width' => 20],
'width' => 20, 'width' => 20,
'height' => 10, 'height' => 10,
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'scale_y' => [ 'scale_y' => [
'function' => 'scale', 'operation' => 'scale',
'arguments' => ['height' => 10], 'arguments' => ['height' => 10],
'width' => 20, 'width' => 20,
'height' => 10, 'height' => 10,
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'upscale_x' => [ 'upscale_x' => [
'function' => 'scale', 'operation' => 'scale',
'arguments' => ['width' => 80, 'upscale' => TRUE], 'arguments' => ['width' => 80, 'upscale' => TRUE],
'width' => 80, 'width' => 80,
'height' => 40, 'height' => 40,
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'upscale_y' => [ 'upscale_y' => [
'function' => 'scale', 'operation' => 'scale',
'arguments' => ['height' => 40, 'upscale' => TRUE], 'arguments' => ['height' => 40, 'upscale' => TRUE],
'width' => 80, 'width' => 80,
'height' => 40, 'height' => 40,
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'crop' => [ 'crop' => [
'function' => 'crop', 'operation' => 'crop',
'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12], 'arguments' => ['x' => 12, 'y' => 4, 'width' => 16, 'height' => 12],
'width' => 16, 'width' => 16,
'height' => 12, 'height' => 12,
'corners' => array_fill(0, 4, $this->white), 'corners' => array_fill(0, 4, static::WHITE),
], ],
'scale_and_crop' => [ 'scale_and_crop' => [
'function' => 'scale_and_crop', 'operation' => 'scale_and_crop',
'arguments' => ['width' => 10, 'height' => 8], 'arguments' => ['width' => 10, 'height' => 8],
'width' => 10, 'width' => 10,
'height' => 8, 'height' => 8,
'corners' => array_fill(0, 4, $this->black), 'corners' => array_fill(0, 4, static::BLACK),
], ],
'convert_jpg' => [ 'convert_jpg' => [
'function' => 'convert', 'operation' => 'convert',
'width' => 40, 'width' => 40,
'height' => 20, 'height' => 20,
'arguments' => ['extension' => 'jpeg'], 'arguments' => ['extension' => 'jpeg'],
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'convert_gif' => [ 'convert_gif' => [
'function' => 'convert', 'operation' => 'convert',
'width' => 40, 'width' => 40,
'height' => 20, 'height' => 20,
'arguments' => ['extension' => 'gif'], 'arguments' => ['extension' => 'gif'],
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'convert_png' => [ 'convert_png' => [
'function' => 'convert', 'operation' => 'convert',
'width' => 40, 'width' => 40,
'height' => 20, 'height' => 20,
'arguments' => ['extension' => 'png'], 'arguments' => ['extension' => 'png'],
'corners' => $default_corners, 'corners' => $default_corners,
], ],
'convert_webp' => [ 'convert_webp' => [
'function' => 'convert', 'operation' => 'convert',
'width' => 40, 'width' => 40,
'height' => 20, 'height' => 20,
'arguments' => ['extension' => 'webp'], 'arguments' => ['extension' => 'webp'],
@ -224,49 +189,51 @@ class ToolkitGdTest extends KernelTestBase {
], ],
]; ];
// Systems using non-bundled GD2 don't have imagerotate. Test if available. // Systems using non-bundled GD2 may miss imagerotate(). Test if available.
// @todo Remove the version check once if (function_exists('imagerotate')) {
// https://www.drupal.org/project/drupal/issues/2670966 is resolved. $test_cases += [
if (function_exists('imagerotate') && (version_compare(phpversion(), '7.0.26') < 0)) {
$operations += [
'rotate_5' => [ 'rotate_5' => [
'function' => 'rotate', 'operation' => 'rotate',
// Fuchsia background. // Fuchsia background.
'arguments' => ['degrees' => 5, 'background' => '#FF00FF'], 'arguments' => ['degrees' => 5, 'background' => '#FF00FF'],
'width' => 41, // @todo Re-enable dimensions' check once
'height' => 23, // https://www.drupal.org/project/drupal/issues/2921123 is resolved.
'corners' => array_fill(0, 4, $this->fuchsia), // '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' => [ 'rotate_90' => [
'function' => 'rotate', 'operation' => 'rotate',
// Fuchsia background. // Fuchsia background.
'arguments' => ['degrees' => 90, 'background' => '#FF00FF'], 'arguments' => ['degrees' => 90, 'background' => '#FF00FF'],
'width' => 20, 'width' => 20,
'height' => 40, 'height' => 40,
'corners' => [$this->transparent, $this->red, $this->green, $this->blue], 'corners' => [static::TRANSPARENT, static::RED, static::GREEN, static::BLUE],
],
'rotate_transparent_5' => [
'function' => 'rotate',
'arguments' => ['degrees' => 5],
'width' => 41,
'height' => 23,
'corners' => array_fill(0, 4, $this->rotateTransparent),
], ],
'rotate_transparent_90' => [ 'rotate_transparent_90' => [
'function' => 'rotate', 'operation' => 'rotate',
'arguments' => ['degrees' => 90], 'arguments' => ['degrees' => 90],
'width' => 20, 'width' => 20,
'height' => 40, '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')) { if (function_exists('imagefilter')) {
$operations += [ $test_cases += [
'desaturate' => [ 'desaturate' => [
'function' => 'desaturate', 'operation' => 'desaturate',
'arguments' => [], 'arguments' => [],
'height' => 20, 'height' => 20,
'width' => 40, 'width' => 40,
@ -283,148 +250,202 @@ class ToolkitGdTest extends KernelTestBase {
]; ];
} }
// Prepare a directory for test file results. $ret = [];
$directory = Settings::get('file_public_path') . '/imagetest'; foreach ([
\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY); 'image-test.png',
'image-test.gif',
foreach ($files as $file) { 'image-test-no-transparency.gif',
foreach ($operations as $op => $values) { 'image-test.jpg',
// Load up a fresh image. 'img-test.webp',
$image = $this->imageFactory->get('core/tests/fixtures/files/' . $file); ] as $file_name) {
$toolkit = $image->getToolkit(); foreach ($test_cases as $test_case => $values) {
if (!$image->isValid()) { $operation = $values['operation'];
$this->fail(new FormattableMarkup('Could not load image %file.', ['%file' => $file])); $arguments = $values['arguments'];
continue 2; unset($values['operation'], $values['arguments']);
} $ret[] = [$file_name, $test_case, $operation, $arguments, $values];
$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();
} }
} }
// Test creation of image from scratch, and saving to storage. return $ret;
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]));
// Reload saved image. /**
$image_reloaded = $this->imageFactory->get($file_path); * Since PHP can't visually check that our images have been manipulated
if (!$image_reloaded->isValid()) { * properly, build a list of expected color values for each of the corners and
$this->fail(new FormattableMarkup('Could not load image %file.', ['%file' => $file])); * 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; 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])); // JPEG has small differences in color after processing.
$this->assertEquals(image_type_to_mime_type($type), $image_reloaded->getMimeType(), new FormattableMarkup('Image file %file has the correct MIME type.', ['%file' => $file])); $tolerance = $image_original_type === IMAGETYPE_JPEG ? 3 : 0;
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])); $this->assertColorsAreEqual($expected_color, $actual_color, $tolerance, "Image '$file_name' object after '$test_case' action has the correct color placement at corner '$key'");
}
else {
$this->assertNull($image_reloaded->getToolkit()->getTransparentColor(), new FormattableMarkup('Image file %file has no color channel set.', ['%file' => $file]));
}
} }
// 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 = $this->imageFactory->get();
$image->createNew(-50, 20); $image->createNew(-50, 20);
$this->assertFalse($image->isValid(), 'CreateNew with negative width fails.'); $this->assertFalse($image->isValid(), 'CreateNew with negative width fails.');
@ -472,11 +493,7 @@ class ToolkitGdTest extends KernelTestBase {
/** /**
* Tests for GIF images with transparency. * Tests for GIF images with transparency.
*/ */
public function testGifTransparentImages() { public function testGifTransparentImages(): void {
// Prepare a directory for test file results.
$directory = Settings::get('file_public_path') . '/imagetest';
\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
// Test loading an indexed GIF image with transparent color set. // Test loading an indexed GIF image with transparent color set.
// Color at top-right pixel should be fully transparent. // Color at top-right pixel should be fully transparent.
$file = 'image-test-transparent-indexed.gif'; $file = 'image-test-transparent-indexed.gif';
@ -484,20 +501,20 @@ class ToolkitGdTest extends KernelTestBase {
$resource = $image->getToolkit()->getResource(); $resource = $image->getToolkit()->getResource();
$color_index = imagecolorat($resource, $image->getWidth() - 1, 0); $color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
$color = array_values(imagecolorsforindex($resource, $color_index)); $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. // Test deliberately creating a GIF image with no transparent color set.
// Color at top-right pixel should be fully transparent while in memory, // Color at top-right pixel should be fully transparent while in memory,
// fully opaque after flushing image to file. // fully opaque after flushing image to file.
$file = 'image-test-no-transparent-color-set.gif'; $file = 'image-test-no-transparent-color-set.gif';
$file_path = $directory . '/' . $file; $file_path = $this->directory . '/' . $file;
// Create image. // Create image.
$image = $this->imageFactory->get(); $image = $this->imageFactory->get();
$image->createNew(50, 20, 'gif', NULL); $image->createNew(50, 20, 'gif', NULL);
$resource = $image->getToolkit()->getResource(); $resource = $image->getToolkit()->getResource();
$color_index = imagecolorat($resource, $image->getWidth() - 1, 0); $color_index = imagecolorat($resource, $image->getWidth() - 1, 0);
$color = array_values(imagecolorsforindex($resource, $color_index)); $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. // Save image.
$this->assertTrue($image->save($file_path), "New GIF image {$file} was saved."); $this->assertTrue($image->save($file_path), "New GIF image {$file} was saved.");
// Reload image. // Reload image.
@ -522,37 +539,20 @@ class ToolkitGdTest extends KernelTestBase {
// can be loaded correctly. // can be loaded correctly.
$file = 'image-test-transparent-out-of-range.gif'; $file = 'image-test-transparent-out-of-range.gif';
$image = $this->imageFactory->get('core/tests/fixtures/files/' . $file); $image = $this->imageFactory->get('core/tests/fixtures/files/' . $file);
$toolkit = $image->getToolkit(); $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.");
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]));
}
} }
/** /**
* Tests calling a missing image operation plugin. * Tests calling a missing image operation plugin.
*/ */
public function testMissingOperation() { public function testMissingOperation(): void {
// 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';
// Load up a fresh image. // Load up a fresh image.
$image = $this->imageFactory->get('core/tests/fixtures/files/' . $file); $image = $this->imageFactory->get('core/tests/fixtures/files/image-test.png');
if (!$image->isValid()) { $this->assertTrue($image->isValid(), "Image 'image-test.png' after load should be valid, but it is not.");
$this->fail(new FormattableMarkup('Could not load image %file.', ['%file' => $file]));
}
// Try perform a missing toolkit operation. // 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.');
} }
} }

View File

@ -179,11 +179,14 @@ trait BrowserHtmlDebugTrait {
$html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line']; $html_output = 'Called from ' . $caller['function'] . ' line ' . $caller['line'];
$html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri(); $html_output .= '<hr />' . $request->getMethod() . ' request to: ' . $request->getUri();
// Get the response body as a string. Any errors are silenced as /** @var \Psr\Http\Message\StreamInterface $stream */
// tests should not fail if there is a problem. On PHP 7.4 $stream = $response->getBody();
// \Drupal\Tests\migrate\Functional\process\DownloadFunctionalTest
// fails without the usage of a silence operator. // Get the response body as a string.
$body = @(string) $response->getBody(); $body = $stream->isReadable()
? (string) $stream
: 'Response is not readable.';
// On redirect responses (status code starting with '3') we need // On redirect responses (status code starting with '3') we need
// to remove the meta tag that would do a browser refresh. We // to remove the meta tag that would do a browser refresh. We
// don't want to redirect developers away when they look at the // don't want to redirect developers away when they look at the

View File

@ -3,10 +3,10 @@
namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional; namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait; use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait; use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures; use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use PHPUnit\Framework\TestCase;
/** /**
* Tests Composer Hooks that run scaffold operations. * Tests Composer Hooks that run scaffold operations.
@ -22,7 +22,8 @@ use PHPUnit\Framework\TestCase;
* *
* @group Scaffold * @group Scaffold
*/ */
class ComposerHookTest extends TestCase { class ComposerHookTest extends BuildTestBase {
use ExecTrait; use ExecTrait;
use AssertUtilsTrait; use AssertUtilsTrait;
@ -120,9 +121,10 @@ class ComposerHookTest extends TestCase {
$this->mustExec("composer install --no-ansi", $sut); $this->mustExec("composer install --no-ansi", $sut);
// Require a project that is not allowed to scaffold and confirm that we // Require a project that is not allowed to scaffold and confirm that we
// get a warning, and it does not scaffold. // 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->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');
} }
/** /**

View File

@ -64,5 +64,10 @@
"libraries/{$name}": ["type:drupal-library"], "libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"] "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
} }
},
"config": {
"allow-plugins": {
"drupal/core-composer-scaffold": true
}
} }
} }

View File

@ -67,5 +67,10 @@
"libraries/{$name}": ["type:drupal-library"], "libraries/{$name}": ["type:drupal-library"],
"drush/Commands/contrib/{$name}": ["type:drupal-drush"] "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
} }
} },
"config": {
"allow-plugins": {
"drupal/core-composer-scaffold": true
}
}
} }

View File

@ -353,7 +353,7 @@ class SettingsTest extends UnitTestCase {
if (!empty($expected_autoload)) { if (!empty($expected_autoload)) {
$class_loader->expects($this->once()) $class_loader->expects($this->once())
->method('addPsr4') ->method('addPsr4')
->with($expected_namespace . '\\', $expected_autoload); ->with($expected_namespace . '\\', vfsStream::url('root') . '/' . $expected_autoload);
} }
else { else {
$class_loader->expects($this->never()) $class_loader->expects($this->never())

View File

@ -14,7 +14,7 @@
{% set drupal_community = 'https://www.drupal.org/community' %} {% set drupal_community = 'https://www.drupal.org/community' %}
{% set drupal_values = 'https://www.drupal.org/about/values-and-principles' %} {% 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 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_extend = 'https://www.drupal.org/docs/extending-drupal' %}
{% set drupal_global_training_days = 'https://groups.drupal.org/global-training-days' %} {% set drupal_global_training_days = 'https://groups.drupal.org/global-training-days' %}
{% set drupal_events = 'https://www.drupal.org/community/events' %} {% set drupal_events = 'https://www.drupal.org/community/events' %}

View File

@ -38,7 +38,7 @@
{% apply spaceless %} {% apply spaceless %}
<li class="pager__item pager__item--control pager__item--next"> <li class="pager__item pager__item--control pager__item--next">
<a href="{{ items.next.href }}" class="pager__link" title="{{ 'Go to next page'|t }}" rel="next"{{ items.next.attributes|without('href', 'title', 'rel') }}> <a href="{{ items.next.href }}" class="pager__link" title="{{ 'Go to next page'|t }}" rel="next"{{ items.next.attributes|without('href', 'title', 'rel') }}>
<span class="visually-hidden">{{ 'Previous page'|t }}</span> <span class="visually-hidden">{{ 'Next page'|t }}</span>
{% include "@olivero/../images/pager-previous.svg" %} {% include "@olivero/../images/pager-previous.svg" %}
</a> </a>
</li> </li>