htools-8.x-1.x-dev/modules/htools_migrate/src/Plugin/migrate/process/RemoteMedia.php
modules/htools_migrate/src/Plugin/migrate/process/RemoteMedia.php
<?php
namespace Drupal\htools_migrate\Plugin\migrate\process;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\file\FileInterface;
use Drupal\media\MediaInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipProcessException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
/**
* Process remote URL images and create them as media entities.
*
* @MigrateProcessPlugin(
* id = "remote_media"
* )
*
* This plugin fetch image file from remote URL's and migrate them as media
* entities. By default if the download fails during the migration process the
* media entity is created and the file downloading process to a queue, so it
* can be done later unattended. Also can be configured to always delegate to
* the queue instead of downloading the file.
*
* Example usage with default values configuration:
*
* @code
* destination:
* plugin: 'entity:node'
* process:
* images:
* plugin: htools_media_image
* source: urlImages
* @endcode
*
* Example usage with queue option configuration:
* @code
* destination:
* plugin: 'entity:node'
* process:
* images:
* plugin: htools_media_image
* source: urlImages
* name: longName
* queue: true
* media_field: image
* bundle: image
* @endcode
*
* @see \Drupal\htools_migrate\Plugin\QueueWorker\DownloadRemoteFile
*/
class RemoteMedia extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The migration instance..
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The entity-type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* HTTP client service.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The language manager service.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $account;
/**
* Extension guesser.
*
* @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser
*/
protected $extensionGuesser;
/**
* The queue instance.
*
* @var \Drupal\Core\Queue\QueueInterface
*/
protected $queue;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityTypeManagerInterface $entity_type_manager, ClientInterface $http_client, LanguageManagerInterface $language_manager, QueueFactory $queue, AccountProxyInterface $account = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->entityTypeManager = $entity_type_manager;
$this->httpClient = $http_client;
$this->languageManager = $language_manager;
$this->queue = $queue->get('download_remote_file');
$this->account = $account;
$this->extensionGuesser = ExtensionGuesser::getInstance();
$this->extensionGuesser->register(new MimeTypeExtensionGuesser());
$this->configuration['queue'] = !empty($this->configuration['queue']);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager'),
$container->get('http_client'),
$container->get('language_manager'),
$container->get('queue'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
try {
if (empty($value)) {
throw new \Exception('Value is empty');
}
// @FIXME This is a workaround for URL's with no schema.
if (!(bool) preg_match('/^(?:ftp|https?|feed):/xi', $value)) {
$value = 'http:' . $value;
}
// @FIXME Use a configuration option to pass a collection of source fields to generate the alt property.
$alt = $row->getSourceProperty($this->configuration['name']);
$title = $alt;
$explode_value = !empty($this->configuration['explode']) ? explode($this->configuration['explode']['delimiter'], $value) : NULL;
if ($explode_value !== NULL && is_array($explode_value) && is_numeric($this->configuration['explode']['value_key'])) {
$value = $explode_value[$this->configuration['explode']['value_key']];
$title = !empty($this->configuration['explode']['title_key']) ? $explode_value[$this->configuration['explode']['title_key']] : $title;
$alt = !empty($this->configuration['explode']['alt_key']) ? $explode_value[$this->configuration['explode']['alt_key']] : $alt;
}
if (empty($value)) {
throw new \Exception('Value is empty');
}
$this->configuration['bundle'] = !empty($this->configuration['bundle']) ? $this->configuration['bundle'] : 'image';
// Replace encoded characters.
$value = urldecode($value);
// Check if the image is already imported using the URL.
$media = $this->lookupEntity($value, $this->configuration['bundle']);
if ($media !== NULL) {
$this->updateTranslations($media, $alt);
return $media->id();
}
$file = FALSE;
if (empty($this->configuration['queue'])) {
$file = $this->fetchFile($value);
}
$properties = [
'media_field' => $this->configuration['media_field'],
'bundle' => $this->configuration['bundle'],
];
$media = \Drupal::service('htools.media_image_utils')
->createMedia($value, $file, $alt, $properties);
// Queue the media entity for file downloading.
if (!($file instanceof FileInterface)) {
$this->queue->createItem([
'type' => $media->getEntityTypeId(),
'bundle' => $media->bundle(),
'id' => $media->id(),
'source' => $value,
'field' => $this->configuration['media_field'],
'keep_original_file_name' => isset($this->configuration['keep_original_file_name']) ? $this->configuration['keep_original_file_name'] : FALSE,
'properties' => [
'alt' => $alt,
'title' => $title,
],
// @FIXME Use DI.
'attempts' => \Drupal::state()
->get('remote_file_download_attempts', 288),
]);
}
return $media->id();
} catch (\Exception $e) {
watchdog_exception('migrate', $e, 'Process plugin %plugin failed for value %value with error @error', [
'%plugin' => $this->getPluginId(),
'%value' => var_export($value, TRUE),
'@error' => $e->getMessage(),
]);
/* @noinspection PhpUnhandledExceptionInspection */
throw new MigrateSkipProcessException($this->t('An error occurred while executing %plugin plugin. Check the logs for more information.', [
'%plugin' => $this->getPluginId(),
]));
}
}
/**
* Lookup the media entity using the source URL.
*
* @param string $url
* The source URL.
* @param string $bundle
* The entity bundle.
*
* @return \Drupal\media\MediaInterface|null
* The media entity instance, or NULL if cannot be found.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function lookupEntity($url, $bundle): ?MediaInterface {
$storage = $this->entityTypeManager->getStorage('media');
$query = $storage->getQuery()
->condition('remote_source', $url)
->condition('bundle', $bundle);
$result = $query->execute();
$result = reset($result);
if (empty($result)) {
return NULL;
}
/** @var \Drupal\media\MediaInterface $media */
$media = $storage->load($result);
return $media;
}
/**
* Update media entity translations.
*
* @param \Drupal\media\MediaInterface $media
* The media instance.
* @param string $alt
* The alt field.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function updateTranslations(MediaInterface $media, $alt): void {
if (empty($this->configuration['langcode'])) {
// If no langcode is configured use current language.
$this->configuration['langcode'] = $this->languageManager->getCurrentLanguage()
->getId();
}
if ($this->languageManager->getLanguage($this->configuration['langcode']) === NULL) {
$e = new \InvalidArgumentException('Cannot update the entity translations, the configured langcode is invalid.');
watchdog_exception('migrate', $e);
return;
}
$media_field = $this->configuration['media_field'];
$langcode = $this->configuration['langcode'];
$original = $media->language()->getId();
if ((string) $langcode === $original ) {
$value = $media->get($media_field)->getValue();
if (!$media->get($media_field)->isEmpty() && array_key_exists('alt', $value[0]) && $value[0]['alt'] !== $alt) {
$value[0]['alt'] = $alt;
$value[0]['title'] = $alt;
$media->set($media_field, $value);
$media->save();
}
return;
}
// Remove the previous translation if exists.
if ($media->hasTranslation($langcode)) {
$media->removeTranslation($langcode);
}
$translation = $media->getTranslation($original)->toArray();
$translation[$media_field][0]['alt'] = $alt;
$translation[$media_field][0]['title'] = $alt;
$translation['content_translation_source'][0]['value'] = $original;
$media
->addTranslation($langcode, $translation)
->save();
}
/**
* Fetch file from remote URL and stores it.
*
* @param string $url
* The file url.
*
* @return bool|\Drupal\file\FileInterface
* The file instance or FALSE if cannot be fetched.
*/
protected function fetchFile($url) {
try {
$response = $this->httpClient->request('GET', $url);
$path = file_build_uri('migrate');
$writable = \Drupal::service('file_system')
->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
if ($writable === FALSE) {
$e = new \RuntimeException('Cannot create or modify permissions for ' . $path . ' directory.');
watchdog_exception('migrate', $e);
return NULL;
}
$path .= '/' . Crypt::hashBase64($url);
$header = $response->getHeader('Content-Type');
$extension = $this->extensionGuesser->guess($header[0]);
if (!empty($extension)) {
$path .= '.' . $extension;
}
$data = (string) $response->getBody();
return file_save_data($data, $path, FileSystemInterface::EXISTS_REPLACE);
} catch (GuzzleException $e) {
// Just log the exception for debugging purposes.
watchdog_exception('migrate', $e);
} catch (\Exception $e) {
// Just log the exception for debugging purposes.
watchdog_exception('migrate', $e);
}
return FALSE;
}
}
