Skip to content
Snippets Groups Projects

Issue #3214409 by codebymikey: Improve the Editor plugin behaviour supports image styles and uses DOM parser

Merged Issue #3214409 by codebymikey: Improve the Editor plugin behaviour supports image styles and uses DOM parser

Files

@@ -2,13 +2,17 @@
namespace Drupal\file_update\Plugin\FileUpdate;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Logger\LoggerChannel;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\file\FileUsage\DatabaseFileUsageBackend;
use Drupal\Core\Site\Settings;
use Drupal\file\FileUsage\FileUsageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -26,14 +30,14 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
/**
* Drupal\Core\Entity\EntityTypeManager definition.
*
* @var \Drupal\Core\Entity\EntityTypeManager
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Drupal\Core\Extension\ModuleHandler definition.
*
* @var \Drupal\Core\Extension\ModuleHandler
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
@@ -54,14 +58,37 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
/**
* Drupal\file\FileUsage\DatabaseFileUsageBackend definition.
*
* @var \Drupal\file\FileUsage\DatabaseFileUsageBackend
* @var \Drupal\file\FileUsage\FileUsageInterface
*/
protected $fileUsage;
/**
* List of supported image styles.
*
* @var \Drupal\image\ImageStyleInterface[]
*/
protected $imageStyles;
/**
* The image style paths.
*
* @var array
*/
protected $imageStylePaths;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManager $entity_type_manager, ModuleHandler $module_handler, LoggerChannel $logger_channel, Messenger $messenger, DatabaseFileUsageBackend $file_usage) {
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
EntityTypeManagerInterface $entity_type_manager,
ModuleHandlerInterface $module_handler,
LoggerChannelInterface $logger_channel,
MessengerInterface $messenger,
FileUsageInterface $file_usage
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
@@ -90,7 +117,7 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
* {@inheritdoc}
*/
public function isRequired() {
return TRUE;
return $this->moduleHandler->moduleExists('editor');
}
/**
@@ -113,8 +140,10 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
*/
public function updateUri($uri) {
$uuid = $this->getFileEntity()->uuid();
$url = file_url_transform_relative(file_create_url($uri));
$pattern = '/<[^>]*' . preg_quote($uuid) . '[^>]*>/';
$new_url = file_url_transform_relative(file_create_url($uri));
$previous_uri = $this->configuration['file']['uri'];
$previous_url = file_url_transform_relative(file_create_url($previous_uri));
try {
foreach ($this->getReferrer() as $field_name => $usage) {
foreach ($usage as $entity_type => $entity_ids) {
@@ -122,31 +151,36 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
->loadMultiple($entity_ids);
foreach ($entities as $entity_id => $entity) {
if ($entity instanceof FieldableEntityInterface) {
$values = $entity->get($field_name)->getValue();
foreach ($values as $key => $value) {
$edit_str = $value['value'];
preg_match($pattern, $value['value'], $matches);
foreach ($matches as $match) {
$new_value = preg_replace('/src="([^"]*)"/', "src=\"{$url}\"", $match);
$edit_str = str_replace($match, $new_value, $edit_str);
}
$values[$key]['value'] = $edit_str;
$context = [
'entity' => $entity,
'entity_type' => $entity_type,
'entity_id' => $entity_id,
'field_name' => $field_name,
'uuid' => $uuid,
'previous_url' => $previous_url,
'uri' => $uri,
'previous_uri' => $previous_uri,
'new_url' => $new_url,
];
$field_item = $entity->get($field_name);
$field_value = $this->processFieldValue($field_item, $context);
$field_item->setValue($field_value);
if ($entity instanceof RevisionLogInterface) {
// Add a revision message stating why the entity has changed.
$entity->setRevisionLogMessage("file_update editor ($previous_uri) reference update.");
}
$entity->get($field_name)->setValue($values);
if ($entity->save() == SAVED_UPDATED) {
$message = $this->t('Updated %type:%id entity at field %field with new image url "%url".', [
$params = [
'%type' => $entity_type,
'%id' => $entity_id,
'%field' => $field_name,
'%url' => $url,
]);
'%url' => $new_url,
];
$message = $this->t('Updated %type:%id entity at field %field with new image url "%url".', $params);
$this->messenger->addMessage($message);
$this->loggerChannel->info('Updated %type:%id entity at field %field with new image url "%url".', [
'%type' => $entity_type,
'%id' => $entity_id,
'%field' => $field_name,
'%url' => $url,
]);
$this->loggerChannel->info('Updated %type:%id entity at field %field with new image url "%url".', $params);
}
}
}
@@ -159,6 +193,96 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
}
}
/**
* Process the entity field values.
*/
protected function processFieldValue(FieldItemListInterface $field_item, array $context) {
$uuid = $context['uuid'];
$values = $field_item->getValue();
foreach ($values as $key => $value) {
$properties = ['value'];
if ($field_item->getFieldDefinition()->getType() === 'text_with_summary') {
// Include the summary field.
$properties[] = 'summary';
}
foreach ($properties as $property) {
if (!empty($value[$property])) {
// Normalize the line endings before loading.
// https://www.drupal.org/project/drupal/issues/2975602
$html = str_replace(["\r\n", "\r"], "\n", $value[$property]);
$dom = Html::load($html);
$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid="' . $uuid . '"]') as $node) {
$this->processDomNode($node, $context);
}
$new_value = Html::serialize($dom);
$values[$key][$property] = $new_value;
}
}
}
return $values;
}
/**
* Process a DOM node element.
*/
protected function processDomNode($node, array $context) {
$previous_url = $context['previous_url'];
$uri = $context['uri'];
$previous_uri = $context['previous_uri'];
$new_url = $context['new_url'];
// Additional attributes that may be processed.
$additional_attributes = Settings::get('file_update.editor_attributes', []);
$attributes = array_unique(array_merge(['src'], $additional_attributes));
foreach ($attributes as $attribute) {
if ($attribute === 'srcset') {
// This will be handled specially below.
continue;
}
// Process the "src" attribute among others.
/** @var \DOMElement $node */
if ($node->hasAttribute($attribute)) {
$src = $node->getAttribute($attribute);
$new_src = $this->getUpdatedSrc(
$src,
$previous_url,
$uri,
$previous_uri,
$new_url,
['attribute' => $attribute] + $context
);
// Update the attribute.
$node->setAttribute($attribute, $new_src);
}
}
if ($node->hasAttribute('srcset')) {
// Update the srcset attribute specially if it exists.
/** @see \Drupal\Component\Utility\Html::transformRootRelativeUrlsToAbsolute */
$srcset = explode(',', $node->getAttribute('srcset'));
$srcset = array_map('trim', $srcset);
foreach ($srcset as $i => $srcset_entry) {
$srcset_entry_components = explode(' ', $srcset_entry);
// Apply the updated src.
$new_srcset_url = $this->getUpdatedSrc(
$srcset_entry_components[0],
$previous_url,
$uri,
$previous_uri,
$new_url,
['attribute' => 'srcset'] + $context
);
$srcset_entry_components[0] = $new_srcset_url;
// Apply the updated components if it's changed.
$srcset[$i] = implode(' ', $srcset_entry_components);
}
$node->setAttribute('srcset', implode(', ', $srcset));
}
}
/**
* {@inheritdoc}
*/
@@ -186,11 +310,11 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
->loadMultiple(array_keys($references));
foreach ($entities as $entity) {
if ($entity instanceof FieldableEntityInterface) {
foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) {
$field_types = ['text', 'text_long', 'text_with_summary'];
if (in_array($field_definition->getType(), $field_types)) {
if (strpos($entity->get($field_name)->getString(), $uuid) >= 0) {
$collector[$field_name][$entity_type][$entity->id()] = $entity->id();
if ($field_uuids = _editor_get_file_uuids_by_field($entity)) {
$entity_id = $entity->id();
foreach ($field_uuids as $field_name => $uuids) {
if (in_array($uuid, $uuids, TRUE)) {
$collector[$field_name][$entity_type][$entity_id] = $entity_id;
}
}
}
@@ -207,4 +331,89 @@ class FileUpdateEditor extends FileUpdateBase implements FileUpdateInterface, Co
return $collector;
}
/**
* Returns a list of image style entities.
*
* @return \Drupal\image\ImageStyleInterface[]
* The image styles.
*/
protected function getImageStyles() {
if ($this->imageStyles === NULL) {
if ($this->moduleHandler->moduleExists('image')) {
// Load the image styles.
$this->imageStyles = $this->entityTypeManager
->getStorage('image_style')
->loadMultiple();
}
else {
// Nothing to load.
$this->imageStyles = [];
}
}
return $this->imageStyles;
}
/**
* Returns a list of image style path prefixes.
*/
protected function getImageStylePaths() {
if ($this->imageStylePaths === NULL) {
$this->imageStylePaths = [];
foreach ($this->getImageStyles() as $image_style) {
$uri = $image_style->buildUri('');
$uri = file_url_transform_relative(file_create_url($uri));
$this->imageStylePaths[$image_style->id()] = rtrim($uri, '/') . '/';
}
}
return $this->imageStylePaths;
}
/**
* Retrieves the appropriate URI for the given src.
*
* @return string
* The URI.
*/
protected function getUpdatedSrc($current_src, $previous_src, $new_uri, $previous_uri, $new_src, $context) {
if (strpos($current_src, $previous_src) !== FALSE) {
// URI matches the src given the previous name.
return $new_src;
}
if (strpos($current_src, $previous_src) === FALSE) {
// Doesn't match the original URL, check for image styles.
foreach ($this->getImageStylePaths() as $style_id => $path_prefix) {
if (strpos($current_src, $path_prefix) !== FALSE) {
// Found a matching image style path. Try to recreate it with the new
// file.
$image_style = $this->getImageStyles()[$style_id] ?? NULL;
if ($image_style && $image_style->supportsUri($new_uri)) {
$new_style_derivative_url = $image_style->buildUrl($new_uri);
return file_url_transform_relative($new_style_derivative_url);
}
}
}
}
$params = [
'%src' => $current_src,
'%previous_src' => $previous_src,
'%attribute' => $context['attribute'],
'%type' => $context['entity_type'],
'%id' => $context['entity_id'],
'%field' => $context['field_name'],
];
// Didn't match the expected value. Log it for debugging purposes.
$this->loggerChannel->info(
'The %attribute attribute had an unexpected URL value of "%src" on %type:%id entity at field %field. Expected it to contain "%previous_src".',
$params
);
// Fallback to the new URL.
return $new_src;
}
}
Loading