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