Skip to content
Snippets Groups Projects
Commit f31055f8 authored by Ivan Duarte's avatar Ivan Duarte
Browse files

Issue #3337321: Provide a Href to Media action

parent 4ff690a8
No related branches found
No related tags found
No related merge requests found
...@@ -11,3 +11,6 @@ services: ...@@ -11,3 +11,6 @@ services:
html_processors.media_generator: html_processors.media_generator:
class: Drupal\html_processors\Service\MediaGenerator class: Drupal\html_processors\Service\MediaGenerator
arguments: ['@file_system', '@entity_type.manager', '@token', '@current_user', '@file.repository', '@logger.channel.html_processors', '@http_client'] arguments: ['@file_system', '@entity_type.manager', '@token', '@current_user', '@file.repository', '@logger.channel.html_processors', '@http_client']
html_processors.href_to_media_replacer:
class: Drupal\html_processors\Service\HrefToMediaReplacer
arguments: ['@html_processors.media_generator']
...@@ -6,6 +6,7 @@ use Drupal\Core\Form\FormStateInterface; ...@@ -6,6 +6,7 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\tamper\TamperableItemInterface; use Drupal\tamper\TamperableItemInterface;
use Drupal\tamper\TamperBase; use Drupal\tamper\TamperBase;
use Drupal\html_processors\Service\MediaGenerator; use Drupal\html_processors\Service\MediaGenerator;
use Drupal\html_processors\Service\HrefToMediaReplacer;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
...@@ -31,6 +32,13 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface ...@@ -31,6 +32,13 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface
*/ */
protected $mediaGenerator; protected $mediaGenerator;
/**
* The href to media replacer.
*
* @var \Drupal\html_processors\Service\HrefToMediaReplacer
*/
protected $hrefToMediaReplacer;
/** /**
* Constructs a new HrefToMedia instance. * Constructs a new HrefToMedia instance.
* *
...@@ -45,10 +53,13 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface ...@@ -45,10 +53,13 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface
* The plugin implementation definition. * The plugin implementation definition.
* @param \Drupal\html_processors\Service\MediaGenerator $media_generator * @param \Drupal\html_processors\Service\MediaGenerator $media_generator
* The media generator service. * The media generator service.
* @param \Drupal\html_processors\Service\HrefToMediaReplacer $href_to_media_replacer
* The href to media replacer.
*/ */
public function __construct(array $configuration, $plugin_id, $plugin_definition, MediaGenerator $media_generator) { public function __construct(array $configuration, $plugin_id, $plugin_definition, MediaGenerator $media_generator, HrefToMediaReplacer $href_to_media_replacer) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $configuration['source_definition']); parent::__construct($configuration, $plugin_id, $plugin_definition, $configuration['source_definition']);
$this->mediaGenerator = $media_generator; $this->mediaGenerator = $media_generator;
$this->hrefToMediaReplacer = $href_to_media_replacer;
} }
/** /**
...@@ -62,7 +73,8 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface ...@@ -62,7 +73,8 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface
$configuration, $configuration,
$plugin_id, $plugin_id,
$plugin_definition, $plugin_definition,
$container->get('html_processors.media_generator') $container->get('html_processors.media_generator'),
$container->get('html_processors.href_to_media_replacer')
); );
} }
...@@ -133,60 +145,16 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface ...@@ -133,60 +145,16 @@ class HrefToMedia extends TamperBase implements ContainerFactoryPluginInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
* *
* @throws \Drupal\Core\File\Exception\FileException
* @throws \Drupal\Core\Entity\EntityStorageException * @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\Core\File\Exception\FileException
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \GuzzleHttp\Exception\GuzzleException
*/ */
public function tamper($data, TamperableItemInterface $item = NULL) { public function tamper($data, TamperableItemInterface $item = NULL) {
$remote_url = $this->getSetting(self::REMOTE_SITE_URL); $result = $this->hrefToMediaReplacer->process($data, $this->configuration);
$only_relative = $this->getSetting(self::ONLY_RELATIVE);
// Load the HTML as a DOM document to traverse through.
$source = new \DOMDocument('5.0', 'UTF-8');
// Remove comments.
$data = preg_replace('/<!--(.|\s)*?-->/', '', $data);
// Replace &nbsp; to space.
$data = str_replace("&nbsp;", " ", $data);
try {
@$source->loadHTML(mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'));
}
catch (\Exception $th) {
// Skipping malformed HTML.
}
$source->encoding = 'utf-8';
$links = $source->getElementsByTagName('a');
// Iterate each link.
foreach ($links as $link) {
$href = $link->getAttribute('href');
if ($href) {
$remote_file_path = $href;
// Skip when only relative is enable.
if ($only_relative && str_starts_with($href, 'http')) {
continue;
}
// Prepend source site URL if relative path.
if (!str_starts_with($href, 'http')) {
$remote_file_path = $remote_url . $href;
}
/** @var \Drupal\media\MediaInterface $media */
$media = $this->mediaGenerator->generateFromRemoteFile($remote_file_path, $this->configuration);
if ($media) {
/** @var \Drupal\media\MediaTypeInterface $media_type */
$media_type = $media->bundle->entity;
$source_field = $media->getSource()->getSourceFieldDefinition($media_type)->getName();
/** @var \Drupal\file\FileInterface $file */
$file = $media->get($source_field)->entity;
// Updating link href.
$link->setAttribute('href', $file->createFileUrl());
}
}
}
$result = preg_replace('/<!DOCTYPE .*>/', '', $source->saveHTML());
$trim_off_front = strlen('<html><body>') + 1;
$trim_off_end = (strrpos($result, '</body></html>')) - strlen($result);
$result = substr($result, $trim_off_front, $trim_off_end);
return $result; return $result;
} }
......
...@@ -83,11 +83,8 @@ class HtmlToGutenberg extends TamperBase implements ContainerFactoryPluginInterf ...@@ -83,11 +83,8 @@ class HtmlToGutenberg extends TamperBase implements ContainerFactoryPluginInterf
* @throws \Drupal\Component\Plugin\Exception\PluginException * @throws \Drupal\Component\Plugin\Exception\PluginException
*/ */
public function tamper($data, TamperableItemInterface $item = NULL) { public function tamper($data, TamperableItemInterface $item = NULL) {
$result = "";
$processors_config = $this->configuration['gutenberg_processors']; $processors_config = $this->configuration['gutenberg_processors'];
if (!empty($data)) {
$result = $this->htmlGutenbergParser->parse($data, $processors_config); $result = $this->htmlGutenbergParser->parse($data, $processors_config);
}
return $result; return $result;
} }
......
<?php
namespace Drupal\html_processors\Plugin\Action;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\html_processors\Service\MediaGenerator;
use Drupal\html_processors\Service\HrefToMediaReplacer;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Provides a HREF to Media action.
*
* @Action(
* id = "html_processors_href_to_media",
* label = @Translation("Create media items from body content Href attributes"),
* type = "node",
* category = @Translation("HtmlProcessors")
* )
*/
class HrefToMedia extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
const REMOTE_SITE_URL = 'remote_url';
const ONLY_RELATIVE = 'only_relative';
/**
* The media generator service.
*
* @var \Drupal\html_processors\Service\MediaGenerator
*/
protected $mediaGenerator;
/**
* The href to media replacer.
*
* @var \Drupal\html_processors\Service\HrefToMediaReplacer
*/
protected $hrefToMediaReplacer;
/**
* Constructs a new HrefToMedia instance.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\html_processors\Service\MediaGenerator $media_generator
* The media generator service.
* @param \Drupal\html_processors\Service\HrefToMediaReplacer $href_to_media_replacer
* The href to media replacer.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MediaGenerator $media_generator, HrefToMediaReplacer $href_to_media_replacer) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->mediaGenerator = $media_generator;
$this->hrefToMediaReplacer = $href_to_media_replacer;
}
/**
* {@inheritdoc}
*
* @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException
* @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('html_processors.media_generator'),
$container->get('html_processors.href_to_media_replacer')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$config = parent::defaultConfiguration();
$config[self::REMOTE_SITE_URL] = '';
$config[self::ONLY_RELATIVE] = FALSE;
return $config;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
// Remote site URL.
$form[self::REMOTE_SITE_URL] = [
'#type' => 'url',
'#title' => $this->t('Remote site URL'),
'#default_value' => $this->configuration[self::REMOTE_SITE_URL],
'#description' => $this->t('The remote site URL without trailing "/"'),
'#required' => TRUE,
];
// Only relative paths.
$form[self::ONLY_RELATIVE] = [
'#type' => 'checkbox',
'#title' => $this->t('Only process relative paths'),
'#default_value' => $this->configuration[self::ONLY_RELATIVE],
'#description' => $this->t('If checked the absolute paths will be skipped.'),
];
// Media types paths.
$form['media_paths'] = [
'#type' => 'details',
'#title' => $this->t('Media Paths'),
'#open' => TRUE,
];
foreach ($this->mediaGenerator->getMediaSettingsMap() as $media_type_id => $settings) {
$form['media_paths'][$media_type_id] = [
'#type' => 'textfield',
'#title' => $this->t('Path to store files of type: @media_type', [
'@media_type' => $settings['label'],
]),
'#default_value' => $this->configuration[$media_type_id] ?? '',
'#description' => $this->t('Leave empty to use media type file directory.'),
'#field_prefix' => 'public://',
'#attributes' => [
'placeholder' => $settings['file_directory'] ?? '',
],
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$media_paths = $form_state->getValue('media_paths') ?? [];
$this->setConfiguration([
self::REMOTE_SITE_URL => $form_state->getValue(self::REMOTE_SITE_URL),
self::ONLY_RELATIVE => $form_state->getValue(self::ONLY_RELATIVE),
] + $media_paths);
}
/**
* {@inheritdoc}
*/
public function access($node, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $node */
$access = $node->access('update', $account, TRUE)
->andIf($node->title->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \InvalidArgumentException
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Core\File\Exception\FileException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function execute($node = NULL) {
/** @var \Drupal\node\NodeInterface $node */
$body = $node->body->value;
$processed_body = $this->hrefToMediaReplacer->process($body, $this->configuration);
$node->set('body', $processed_body)->save();
}
}
...@@ -123,6 +123,7 @@ class Image extends HtmlGutenbergProcessorBase implements ContainerFactoryPlugin ...@@ -123,6 +123,7 @@ class Image extends HtmlGutenbergProcessorBase implements ContainerFactoryPlugin
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \DOMException * @throws \DOMException
* @throws \GuzzleHttp\Exception\GuzzleException
*/ */
public function process(\DOMDocument &$source) { public function process(\DOMDocument &$source) {
$tag = $this->pluginDefinition['tag']; $tag = $this->pluginDefinition['tag'];
......
<?php
namespace Drupal\html_processors\Service;
use Drupal\html_processors\Service\MediaGenerator;
/**
* Service to parse HTML to Gutenberg.
*/
class HrefToMediaReplacer {
/**
* The media generator service.
*
* @var \Drupal\html_processors\Service\MediaGenerator
*/
protected $mediaGenerator;
/**
* Constructs a HrefToMediaReplacer object.
*
* @param \Drupal\html_processors\Service\MediaGenerator $media_generator
* The media generator service.
*/
public function __construct(MediaGenerator $media_generator) {
$this->mediaGenerator = $media_generator;
}
/**
* Process HTML to generate media items and replace href attributes.
*
* @param string $data
* The original HTML.
* @param array $config
* The config to process media items.
*
* @return string
* The processed HTML.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\Core\File\Exception\FileException
* @throws \InvalidArgumentException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function process($data, array $config) {
$remote_url = $config['remote_url'];
$only_relative = $config['only_relative'];
// Load the HTML as a DOM document to traverse through.
$source = new \DOMDocument('5.0', 'UTF-8');
try {
@$source->loadHTML(mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'));
}
catch (\Exception $th) {
// Skipping malformed HTML.
}
$source->encoding = 'utf-8';
$links = $source->getElementsByTagName('a');
// Iterate each link.
foreach ($links as $link) {
$href = $link->getAttribute('href');
if ($href) {
$remote_file_path = $href;
// Skip when only relative is enable.
if ($only_relative && str_starts_with($href, 'http')) {
continue;
}
// Prepend source site URL if relative path.
if (!str_starts_with($href, 'http')) {
$remote_file_path = $remote_url . $href;
}
/** @var \Drupal\media\MediaInterface $media */
$media = $this->mediaGenerator->generateFromRemoteFile($remote_file_path, $config);
if ($media) {
/** @var \Drupal\media\MediaTypeInterface $media_type */
$media_type = $media->bundle->entity;
$source_field = $media->getSource()->getSourceFieldDefinition($media_type)->getName();
/** @var \Drupal\file\FileInterface $file */
$file = $media->get($source_field)->entity;
// Updating link href.
$link->setAttribute('href', $file->createFileUrl());
}
}
}
$result = preg_replace('/<!DOCTYPE .*>/', '', $source->saveHTML());
$result = preg_replace('/<html><body>/', '', $result);
$result = preg_replace('/<\/body><\/html>/', '', $result);
return $result;
}
}
...@@ -136,12 +136,13 @@ class MediaGenerator { ...@@ -136,12 +136,13 @@ class MediaGenerator {
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \GuzzleHttp\Exception\GuzzleException
*/ */
public function generateFromRemoteFile($remote_file_path, array $paths_overrides = []) { public function generateFromRemoteFile($remote_file_path, array $paths_overrides = []) {
$generated_media = NULL; $generated_media = NULL;
$ext = pathinfo($remote_file_path, PATHINFO_EXTENSION); $ext = pathinfo($remote_file_path, PATHINFO_EXTENSION);
// Check if file extension. // Check if file extension.
if (strlen($ext) === 3) { if (strlen($ext) >= 3 && strlen($ext) <= 4) {
$media_type_settings = $this->getMediaTypeSettingsFromExt($ext); $media_type_settings = $this->getMediaTypeSettingsFromExt($ext);
if ($media_type_settings) { if ($media_type_settings) {
$media_type_id = $media_type_settings['id']; $media_type_id = $media_type_settings['id'];
...@@ -265,6 +266,9 @@ class MediaGenerator { ...@@ -265,6 +266,9 @@ class MediaGenerator {
* - If it succeeds a \Drupal\file\FileInterface * - If it succeeds a \Drupal\file\FileInterface
* object which describes the file. * object which describes the file.
* - If it fails, FALSE. * - If it fails, FALSE.
*
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Drupal\Core\Entity\EntityStorageException
*/ */
protected function retrieveFile($url, $destination, $replace = FileSystemInterface::EXISTS_RENAME) { protected function retrieveFile($url, $destination, $replace = FileSystemInterface::EXISTS_RENAME) {
$parsed_url = parse_url($url); $parsed_url = parse_url($url);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment