bynder-4.0.0-beta1/src/Plugin/media/Source/Bynder.php
src/Plugin/media/Source/Bynder.php
<?php namespace Drupal\bynder\Plugin\media\Source; use Drupal\bynder\BynderApiInterface; use Drupal\bynder\Plugin\Field\FieldType\BynderMetadataItem; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Component\Serialization\Json; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\field\FieldConfigInterface; use Drupal\Core\Utility\Error; use Drupal\media\MediaInterface; use Drupal\media\MediaSourceBase; use Drupal\media\MediaTypeInterface; use GuzzleHttp\Exception\GuzzleException; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\File\FileSystemInterface; /** * Provides media source plugin for Bynder. * * @MediaSource( * id = "bynder", * label = @Translation("Bynder"), * description = @Translation("Provides business logic and metadata for Bynder."), * default_thumbnail_filename = "bynder-logo.png", * allowed_field_types = {"string", "string_long"} * ) */ class Bynder extends MediaSourceBase { const TRANSFORMATIONS_FIELD_NAME = 'bynder_transformations'; /** * Bynder api service. * * @var \Drupal\bynder\BynderApiInterface * Bynder api service. */ protected $bynderApi; /** * Account proxy. * * @var \Drupal\Core\Session\AccountProxyInterface */ protected $accountProxy; /** * The url generator. * * @var \Drupal\Core\Routing\UrlGeneratorInterface */ protected $urlGenerator; /** * Statically cached metadata information for the given assets. * * @var array */ protected $metadata; /** * The logger factory service. * * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface */ protected $logger; /** * The cache service. * * @var \Drupal\Core\Cache\CacheBackendInterface */ protected $cache; /** * The time service. * * @var \Drupal\Component\Datetime\TimeInterface */ protected $time; /** * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ protected $moduleHandler; /** * The language manager. * * @var \Drupal\Core\Language\LanguageManagerInterface */ protected LanguageManagerInterface $languageManager; /** * Statically cached metadata attributes. * * @var array|null */ protected static array|null $metadataAttributes; /** * Constructs a new class instance. * * @param array $configuration * A configuration array containing information about the plugin instance. * @param string $plugin_id * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity type manager service. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager * Entity field manager service. * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager * The field type plugin manager service. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory service. * @param \Drupal\bynder\BynderApiInterface $bynder_api_service * Bynder api service. * @param \Drupal\Core\Session\AccountProxyInterface $account_proxy * Account proxy. * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator * The url generator service. * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger * The logger factory service. * @param \Drupal\Core\Cache\CacheBackendInterface $cache * The cache service. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. */ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, BynderApiInterface $bynder_api_service, AccountProxyInterface $account_proxy, UrlGeneratorInterface $url_generator, LoggerChannelFactoryInterface $logger, CacheBackendInterface $cache, TimeInterface $time, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory); $this->bynderApi = $bynder_api_service; $this->accountProxy = $account_proxy; $this->urlGenerator = $url_generator; $this->logger = $logger; $this->cache = $cache; $this->time = $time; $this->moduleHandler = $module_handler; $this->languageManager = $language_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('entity_type.manager'), $container->get('entity_field.manager'), $container->get('plugin.manager.field.field_type'), $container->get('config.factory'), $container->get('bynder_api'), $container->get('current_user'), $container->get('url_generator'), $container->get('logger.factory'), $container->get('cache.data'), $container->get('datetime.time'), $container->get('module_handler'), $container->get('language_manager'), ); } /** * {@inheritdoc} */ public function getMetadataAttributes() { if (isset(static::$metadataAttributes)) { return static::$metadataAttributes; } static::$metadataAttributes = [ 'uuid' => $this->t('ID'), 'name' => $this->t('Name'), 'description' => $this->t('Description'), 'tags' => $this->t('Tags'), 'type' => $this->t('Type'), 'video_preview_urls' => $this->t('Video preview urls'), 'thumbnail_urls' => $this->t('Thumbnail urls'), 'width' => $this->t('Width'), 'height' => $this->t('Height'), 'created' => $this->t('Date created'), 'modified' => $this->t('Data modified'), 'propertyOptions' => $this->t('Meta-property option IDs'), ]; try { $meta_properties = $this->bynderApi->getMetaProperties(); foreach ($meta_properties as $meta_property) { static::$metadataAttributes[$meta_property['name']] = $meta_property['label']; } } catch (\Exception $exception) { // Ignore exceptions here, might not be set up and the settings form will // show more information. } return static::$metadataAttributes; } /** * Returns a list of remote metadata properties. * * @return array * A list of remote metadata properties. */ public function getRemoteMetadataProperties() { return [ 'id', 'name', 'description', 'type', 'videoPreviewURLs', 'thumbnails', 'original', 'width', 'height', 'dateCreated', 'dateModified', 'propertyOptions', 'extension', ]; } /** * {@inheritdoc} */ public function defaultConfiguration() { return [ 'source_field' => '', 'enforce_mapped_values' => FALSE, 'mapping_language_overrides' => [], ]; } /** * Ensures the given media entity has Bynder metadata information in place. * * @param \Drupal\media\MediaInterface $media * The media entity. * @param bool $force * (optional) By default, this will not attempt to check for updated * metadata if there is local data available. Pass TRUE to always check for * changed metadata. * * @return bool * TRUE if the metadata is ensured. Otherwise, FALSE. */ public function ensureMetadata(MediaInterface $media, $force = FALSE) { $media_uuid = $this->getSourceFieldValue($media); if (!empty($this->metadata[$media_uuid]) && !$force) { return TRUE; } // If we cannot get a value for $media_uuid we should better exit early. // This helps avoid circular references introduced due to ::ensureMetadata() // calling hook_bynder_media_update_alter(), which in turn may call this // method in a loop. if (empty($media_uuid)) { return FALSE; } if (!$media->hasField(BynderMetadataItem::METADATA_FIELD_NAME)) { $this->logger->get('bynder')->error('The media type @type must have a Bynder metadata field named "bynder_metadata".', [ '@type' => $media->bundle(), ]); return FALSE; } if (!$media->get(BynderMetadataItem::METADATA_FIELD_NAME)->isEmpty() && !$force) { $metadata = Json::decode($media->get(BynderMetadataItem::METADATA_FIELD_NAME)->value); if (is_array($metadata)) { $this->metadata[$media_uuid] = $metadata; return TRUE; } } try { $media_uuid = $this->getSourceFieldValue($media); $item = (array) $this->bynderApi->getMediaInfo($media_uuid); $this->metadata[$media_uuid] = $item; $has_changed = FALSE; if ($this->hasMetadataChanged($media, $this->metadata[$media_uuid])) { $encoded_metadata = Json::encode($this->metadata[$media_uuid]); if (!$encoded_metadata) { $this->logger->get('bynder')->error('Unable to JSON encode the returned API response for the media UUID @uuid.', [ '@uuid' => $media_uuid, ]); return FALSE; } $media->set(BynderMetadataItem::METADATA_FIELD_NAME, $encoded_metadata); $has_changed = TRUE; } // Allow other modules to alter the media entity. $this->moduleHandler->alter('bynder_media_update', $media, $item, $has_changed); // If the media entity is still new it will be saved and we don't have // do that twice. if ($has_changed && !$media->isNew()) { $media->save(); } return $has_changed; } catch (GuzzleException $e) { $this->logger->get('bynder')->error('Unable to fetch info about the asset represented by media @name (@id) with message @message.', [ '@name' => $media->label(), '@id' => $media->id(), '@message' => $e->getMessage(), ]); } return FALSE; } /** * Returns a list of filtered remote metadata properties. * * @param array $metadata * The metadata items. * * @return array * Filtered list of remote metadata properties. */ public function filterRemoteMetadata(array $metadata) { return array_intersect_key($metadata, array_combine($this->getRemoteMetadataProperties(), $this->getRemoteMetadataProperties())); } /** * Compares the local metadata and the remote metadata in case it changed. * * @param \Drupal\media\MediaInterface $media * The media entity. * @param array $remote_metadata * The remote metadata. * * @return bool * TRUE if the remote metadata has changed. Otherwise, FALSE. */ public function hasMetadataChanged(MediaInterface $media, array $remote_metadata) { if ($media->get(BynderMetadataItem::METADATA_FIELD_NAME)->isEmpty() && !empty($remote_metadata)) { return TRUE; } $local_metadata = (array) Json::decode((string) $media->get(BynderMetadataItem::METADATA_FIELD_NAME)->value); return $local_metadata !== $remote_metadata; } /** * {@inheritdoc} */ public function getMetadata(MediaInterface $media, $name) { $remote_uuid = $this->getSourceFieldValue($media); if ($name == 'uuid') { return $remote_uuid; } // Ensure the metadata information are fetched. if ($this->ensureMetadata($media)) { switch ($name) { case 'video_preview_urls': return $this->metadata[$remote_uuid]['videoPreviewURLs'] ?? NULL; case 'thumbnail_urls': return $this->metadata[$remote_uuid]['thumbnails'] ?? NULL; case 'thumbnail_uri': if (!empty($this->metadata[$remote_uuid]['thumbnails']['webimage'])) { if ($this->useRemoteImages()) { return $this->metadata[$remote_uuid]['thumbnails']['webimage']; } else { /** @var \Drupal\file\FileRepositoryInterface $file_repository */ $file_repository = \Drupal::service('file.repository'); /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); $image_url = $this->metadata[$remote_uuid]['thumbnails']['webimage']; $field_definition = $media->getFieldDefinition('thumbnail'); $location = $field_definition->getFieldStorageDefinition()->getSetting('uri_scheme') . '://' . $field_definition->getSetting('file_directory'); $location = \Drupal::token()->replace($location); $file_system->prepareDirectory($location, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); try { $data = (string) \Drupal::httpClient()->get($image_url)->getBody(); $file = $file_repository->writeData($data, ltrim($location, '/') . '/' . $file_system->basename(parse_url($image_url, PHP_URL_PATH))); return $file->getFileUri(); } catch (\Exception $exception) { Error::logException(\Drupal::logger('bynder'), $exception); } } } return parent::getMetadata($media, 'thumbnail_uri'); case 'created': return $this->metadata[$remote_uuid]['dateCreated'] ?? NULL; case 'modified': return $this->metadata[$remote_uuid]['dateModified'] ?? NULL; case 'default_name': return $this->metadata[$remote_uuid]['name'] ?? parent::getMetadata($media, 'default_name'); default: if (isset($this->metadata[$remote_uuid][$name])) { return $this->metadata[$remote_uuid][$name]; } elseif (isset($this->metadata[$remote_uuid]['property_' . $name])) { return $this->metadata[$remote_uuid]['property_' . $name]; } else { return NULL; } } } return parent::getMetadata($media, $name); } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { try { // Check the connection with Bynder. $this->bynderApi->getBrands(); $form['enforce_mapped_values'] = [ '#type' => 'checkbox', '#title' => $this->t('Enforce mapped field values'), '#description' => $this->t('By default, mapped fields are only set if they are empty. By enabling this, they will always be updated to match the configured property.'), '#default_value' => $this->configuration['enforce_mapped_values'], ]; $meta_attributes = $this->getMetadataAttributes(); $entity = $form_state->get('type'); if ($this->languageManager->isMultilingual() && $entity && !$entity->isNew()) { $translatable_fields = []; $exclude_fields = [$this->configuration['source_field'], static::TRANSFORMATIONS_FIELD_NAME, BynderMetadataItem::METADATA_FIELD_NAME]; foreach ($this->entityFieldManager->getFieldDefinitions('media', $entity->id()) as $field_name => $field_definition) { if (!in_array($field_name, $exclude_fields) && $field_definition->isTranslatable() && $field_definition instanceof FieldConfigInterface) { $translatable_fields[$field_name] = $field_definition->getLabel(); } } if ($translatable_fields) { $form['mapping_language_overrides'] = [ '#type' => 'details', '#title' => $this->t('Per-language mapping'), '#description' => $this->t('Allows to map different properties to specific translations.'), '#open' => TRUE, ]; foreach ($this->languageManager->getLanguages() as $langcode => $language) { $form['mapping_language_overrides'][$langcode] = [ '#type' => 'details', '#title' => $language->getName() ]; foreach ($translatable_fields as $field_name => $field_label) { $form['mapping_language_overrides'][$langcode][$field_name] = [ '#type' => 'select', '#title' => $field_label, '#options' => $meta_attributes, '#empty_option' => $this->t('- Default field mapping -'), '#default_value' => $this->configuration['mapping_language_overrides'][$langcode][$field_name] ?? NULL, ]; } } } } } catch (\Exception $exception) { if ($this->accountProxy->hasPermission('administer bynder configuration')) { $this->messenger()->addError($this->t('Connecting with Bynder failed. Check if the configuration is set properly <a href=":url">here</a>.', [ ':url' => $this->urlGenerator->generateFromRoute('bynder.configuration_form'), ])); } else { $this->messenger()->addError($this->t('Something went wrong with the Bynder connection. Please contact the site administrator.')); } } return parent::buildConfigurationForm($form, $form_state); } /** * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { parent::submitConfigurationForm($form, $form_state); // Clean empty mapping overrides. if (!empty($this->configuration['mapping_language_overrides'])) { foreach ($this->configuration['mapping_language_overrides'] as $langcode => $language_mappings) { $this->configuration['mapping_language_overrides'][$langcode] = array_filter($language_mappings); } $this->configuration['mapping_language_overrides'] = array_filter($this->configuration['mapping_language_overrides']); } } /** * Extend and enforce metadata mapping. * * @param \Drupal\media\MediaInterface $media * The media entity. * * @see bynder_media_presave() * @see \Drupal\media\Entity\Media::prepareSave() */ public function prepareSave(MediaInterface $media): void { $field_map = $media->get('bundle')->entity->getFieldMap(); foreach (array_keys($media->getTranslationLanguages()) as $langcode) { // Merge the language overrides plus the default field mapping for this // language, replace possibly existing mapping for a given field // if a language overide is given. $language_field_map = $field_map; foreach ($this->configuration['mapping_language_overrides'][$langcode] ?? [] as $field_name => $metadata_attribute_name) { $existing_position = array_search($field_name, $language_field_map); if ($existing_position !== FALSE) { unset($language_field_map[$existing_position]); } $language_field_map[$metadata_attribute_name] = $field_name; } $translation = $media->getTranslation($langcode); foreach ($language_field_map as $metadata_attribute_name => $entity_field_name) { // Only save value in the entity if the field is empty, if the // source field changed or if enforced mapping is enabled. if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $this->hasSourceFieldChanged($translation) || $this->configuration['enforce_mapped_values'])) { $translation->set($entity_field_name, $this->getMetadata($translation, $metadata_attribute_name)); } } } } /** * Determines if the source field value has changed. * * @param \Drupal\media\MediaInterface $media * The media entity. * * @return bool * If the source field has changed. * * @see \Drupal\media\Entity\Media::hasSourceFieldChanged()) */ protected function hasSourceFieldChanged(MediaInterface $media): bool { return isset($media->original) && $this->getSourceFieldValue($media) !== $this->getSourceFieldValue($media->original); } /** * Creates the metadata field storage definition. * * @return \Drupal\field\FieldStorageConfigInterface * The unsaved field storage definition. */ public function createMetadataFieldStorage() { return $this->entityTypeManager->getStorage('field_storage_config') ->create([ 'entity_type' => 'media', 'field_name' => BynderMetadataItem::METADATA_FIELD_NAME, 'type' => 'bynder_metadata', 'cardinality' => 1, 'locked' => TRUE, ]); } /** * Creates the metadata field definition. * * @param \Drupal\media\MediaTypeInterface $type * The media type. * * @return \Drupal\field\FieldConfigInterface * The unsaved field definition. The field storage definition, if new, * should also be unsaved. */ public function createMetadataField(MediaTypeInterface $type) { return $this->entityTypeManager->getStorage('field_config') ->create([ 'entity_type' => 'media', 'field_name' => BynderMetadataItem::METADATA_FIELD_NAME, 'bundle' => $type->id(), 'label' => 'Bynder Metadata', 'translatable' => FALSE, 'field_type' => 'bynder_metadata', ]); } /** * Creates the transformations field storage definition. * * @return \Drupal\field\FieldStorageConfigInterface * The unsaved field storage definition. */ public function createTransformationsFieldStorage() { return $this->entityTypeManager->getStorage('field_storage_config') ->create([ 'entity_type' => 'media', 'field_name' => static::TRANSFORMATIONS_FIELD_NAME, 'type' => 'string', 'cardinality' => 1, 'locked' => TRUE, ]); } /** * Creates the transformations field definition. * * @param \Drupal\media\MediaTypeInterface $type * The media type. * * @return \Drupal\field\FieldConfigInterface * The unsaved field definition. The field storage definition, if new, * should also be unsaved. */ public function createTransformationsField(MediaTypeInterface $type) { return $this->entityTypeManager->getStorage('field_config') ->create([ 'entity_type' => 'media', 'field_name' => static::TRANSFORMATIONS_FIELD_NAME, 'bundle' => $type->id(), 'label' => 'Bynder Transformations', 'translatable' => FALSE, 'field_type' => 'string', ]); } /** * Checks if remote images should be used. * * This checks to see if the configuration for using remote images is enabled * and that the Remote Stream Wrapper module is still enabled. * * @return bool * TRUE if remote images should be used, or FALSE otherwise. */ protected function useRemoteImages() { return $this->configFactory->get('bynder.settings')->get('use_remote_images') || $this->moduleHandler->moduleExists('remote_stream_wrapper'); } }