brandfolder-8.x-1.x-dev/brandfolder.module

brandfolder.module
<?php

/**
 * @file
 * Contains miscellaneous Brandfolder module functions.
 */

use Drupal\brandfolder\Service\BrandfolderGatekeeper;
use Drupal\brandfolder\Plugin\media\Source\BrandfolderImage;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\StatusMessages;
use Drupal\Core\Routing\RouteMatchInterface;
use Brandfolder\BrandfolderClient;
use Drupal\Core\Render\Element;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\media\Entity\Media;
use Drupal\media\MediaInterface;
use Drupal\media\MediaSourceInterface;
use Drupal\media\MediaTypeInterface;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\media_library\MediaLibraryState;
use Drupal\views\ViewExecutable;
use Drupal\media\Entity\MediaType;
use Drupal\Core\Database\Query\Merge;
use Drupal\Core\Entity\EntityStorageException;

/**
 * Helper function for use in procedural code. De-emphasize this in favor of
 * services, etc.
 *
 * Get a Brandfolder API client.
 *
 * @param string|null $stored_key_type
 *  The type of key to use when retrieving an API key stored in module
 *  configuration. Options are 'admin', 'collaborator', and 'guest' (default).
 * @param string|null $custom_api_key
 *   If provided, an API key to use when connecting to Brandfolder. If not
 *   provided, an API key set in the module configuration will be used.
 *
 * @return BrandfolderClient|false
 *  A \Brandfolder\BrandfolderClient instance for interacting with the
 *  Brandfolder API, or FALSE on failure.
 */
function brandfolder_api(?string $stored_key_type = 'guest', ?string $custom_api_key = NULL): bool|BrandfolderClient {
  $config = \Drupal::config('brandfolder.settings');

  $api_key = NULL;
  if (!empty($stored_key_type)) {
    $api_key = \Drupal::service('brandfolder.key_service')->getApiKey($stored_key_type);
  }
  if (is_null($api_key) && !is_null($custom_api_key)) {
    $api_key = $custom_api_key;
  }
  if (empty($api_key)) {

    return FALSE;
  }

  // Set Brandfolder based on module config if present (NULL otherwise).
  $brandfolder_id = \Drupal::config('brandfolder.settings')->get('brandfolder_id');

  $bf = new BrandfolderClient($api_key, $brandfolder_id);

  if ($config->get('verbose_log_mode')) {
    $bf->enableVerboseLogging();
  }

  return $bf;
}

/**
 * Implements hook_help().
 */
function brandfolder_help($route_name, RouteMatchInterface $route_match) {
  $output = '';

  switch ($route_name) {
    // Main module help for the Brandfolder module.
    case 'help.page.brandfolder':
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Allows Brandfolder assets to be used by Drupal\'s Media/image/file systems.') . '</p>';
  }

  return $output;
}

/**
 * Implements hook_field_info_alter().
 */
function brandfolder_field_info_alter(&$info) {
  // Override the core Image field type.
  $info['image']['class'] = '\Drupal\brandfolder\Plugin\Field\FieldType\BrandfolderCompatibleImageItem';
  $info['image']['description'] = [
    new TranslatableMarkup("For uploading images or selecting images from Brandfolder"),
    new TranslatableMarkup(
      "Can be configured with options such as allowed file extensions, maximum file size and image dimensions minimums/maximums"
    ),
  ];
}

/**
 * Implements hook_form_FORM_ID_alter() for field_config_edit_form.
 */
function brandfolder_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $route_params = \Drupal::routeMatch()->getParameters();
  if ($field_config = $route_params->get('field_config', FALSE)) {
    if ($field_config->getType() === 'image') {
      // Add this entity builder to attach our own third-party config data
      // to the config entity for image fields (i.e. field settings).
      $form['#entity_builders'][] = 'brandfolder_field_config_edit_form_entity_builder';
    }
  }
}

/**
 * Implements hook_form_alter().
 */
function brandfolder_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // If this is a form comprising the main media entity listing in a Media
  // Library context, for a Brandfolder-sourced media type, replace the
  // user-facing form elements with our own browsing experience.
  if (preg_match('/^views_form_media_library_widget_(.+)$/', $form_id, $matches)) {
    $media_type_id = $matches[1];
    $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($media_type_id);
    if ($media_type && $media_type->getSource() instanceof BrandfolderImage) {
      brandfolder_browser_media_library_form_manipulator($form, $form_state, $media_type);
    }
  }
  // Media entity edit forms for Brandfolder-sourced media.
  elseif (preg_match('/^media_(.+)_edit_form$/', $form_id, $matches)) {
    $media_type_id = $matches[1];
    $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($media_type_id);
    if ($media_type && $media_type->getSource() instanceof BrandfolderImage) {
      // Make the all-important source field (BF attachment ID text field)
      // read-only.
      if (isset($form['field_brandfolder_attachment_id'])) {
        $form['field_brandfolder_attachment_id']['#disabled'] = TRUE;
      }
      // Limit users' ability to interact with the auto-managed BF Image field.
      // @see brandfolder_process_image_widget_for_internal_field().
      if (isset($form['bf_image'])) {
        if (isset($form['bf_image']['widget'][0])) {
          $form['bf_image']['widget'][0]['#process'][] = 'brandfolder_process_image_widget_for_internal_field';
        }
        // Show the image width and height values and give users an option to
        // swap them if they appear to be inverted.
        $form['bf_image']['image_width'] = [
          '#type' => 'number',
          '#title' => t('Image width'),
          '#disabled' => TRUE,
          '#default_value' => $form['bf_image']['widget'][0]['#default_value']['width'],
        ];
        $form['bf_image']['image_height'] = [
          '#type' => 'number',
          '#title' => t('Image height'),
          '#disabled' => TRUE,
          '#default_value' => $form['bf_image']['widget'][0]['#default_value']['height'],
        ];
        $form['bf_image']['transpose_image_width_and_height'] = [
          '#type' => 'checkbox',
          '#title' => t('Swap image width and height'),
          '#description' => t('If it looks like the width and height values are inverted, check this box to switch them. There is a known issue when Brandfolder processes images from certain cameras in certain orientations. If the image appears "squished" or "stretched" in the preview, try checking this box.'),
          '#default_value' => FALSE,
        ];
        array_unshift($form['actions']['submit']['#submit'], 'brandfolder_image_media_entity_form_submit');
        array_unshift($form['#submit'], 'brandfolder_image_media_entity_form_submit');
      }
    }
  }
//  elseif ($form_id == 'field_config_edit_form') {
    // @todo: consider working around this core bug: https://www.drupal.org/project/drupal/issues/2833650.
    // Simply remove the default value selection option until the entity
    // reference field has been configured with allowed types.
    // This is outside the scope of the Brandfolder module, however.
//    unset($form['default_value']);
//  }
}

/**
 * Entity builder for field_config_edit_form. When a field config/edit form is
 * submitted, this function is called to save any Brandfolder settings to the
 * field config entity.
 *
 * @param $entity_type
 * @param $entity
 * @param $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *
 * @return void
 */
function brandfolder_field_config_edit_form_entity_builder($entity_type, $entity, $form, FormStateInterface $form_state) {
  if ($settings = $form_state->getValue('settings')) {
    if (isset($settings['brandfolder'])) {
      $entity->setThirdPartySetting('brandfolder', 'brandfolder_settings', json_encode($settings['brandfolder']));
    }
  }
}

/**
 * Widget process callback for image widgets pertaining to special image field
 * that we auto-generate/populate based on linked Brandfolder attachment.
 *
 * @param $element
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 * @param $form
 *
 * @return array
 *
 * @see brandfolder_form_alter().
 */
function brandfolder_process_image_widget_for_internal_field($element, FormStateInterface $form_state, $form) {
  // Disable the main file manipulation elements while leaving supplemental
  // elements like alt text field accessible.
  $off_limits_items = [
    'upload_button',
    'remove_button',
    'upload',
  ];
  foreach ($off_limits_items as $key) {
    $element[$key]['#access'] = FALSE;
  }

  return $element;
}

/**
 * Supplementary submit handler for BrandfolderImage-sourced media entity edit
 * forms.
 *
 * @param $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *
 * @return void
 */
function brandfolder_image_media_entity_form_submit(&$form, FormStateInterface $form_state) {
  // If the user has checked the "Transpose image width and height" checkbox,
  // swap the width and height values in the image field and in our BF registry
  // table.
  if ($form_state->getValue('transpose_image_width_and_height')) {
    $logger = \Drupal::logger('brandfolder');
    $bf_attachment_id_item = $form_state->getValue('field_brandfolder_attachment_id');
    if (is_array($bf_attachment_id_item) && isset($bf_attachment_id_item[0]['value'])) {
      $bf_attachment_id = $bf_attachment_id_item[0]['value'];
      $bf_image = $form_state->getValue('bf_image');
      if (!empty($bf_image)) {
        $height = $bf_image[0]['height'];
        $width = $bf_image[0]['width'];
        if ($width > 0 && $height > 0) {
          // Swap the width and height values in the image field.
          $bf_image[0]['height'] = $width;
          $bf_image[0]['width'] = $height;
          $form_state->setValue('bf_image', $bf_image);
          // Update brandfolder_file table to swap width and height fields
          // there as well.
          $db = \Drupal::database();
          $result = $db->merge('brandfolder_file')
            ->key('bf_attachment_id', $bf_attachment_id)
            ->fields([
              'height' => $width,
              'width'  => $height,
            ])
            ->execute();
          if ($result != Merge::STATUS_UPDATE) {
            $logger->error('Tried to transpose image width and height but could not find an existing record in the brandfolder_file table for attachment ID !attachment_id.', [
              '!attachment_id' => $bf_attachment_id
            ]);
          }
        }
        else {
          $logger->error('Tried to transpose image width and height but could not find a valid width and height for attachment ID !attachment_id. Width: !width. Height: !height.', [
            '!attachment_id' => $bf_attachment_id,
            '!width' => $width,
            '!height' => $height,
          ]);
        }
      }
    }
  }
}

/**
 * Implements hook_views_pre_build().
 *
 * @todo: Remove/short-circuit more Views processes that are unnecessary for Brandfolder media given that we're replacing the view with our custom browser.
 */
function brandfolder_views_pre_build(ViewExecutable $view) {
  // If this is a Media Library view pertaining to a Brandfolder-sourced media
  // type, disable any exposed form, as it is not part of our custom
  // browsing experience.
  if ($view->storage->id() == 'media_library') {
    if (isset($view->args[0])) {
      $media_type_id = $view->args[0];
      if (is_media_type_brandfolder_sourced($media_type_id)) {
        // Tell view not to use exposed form.
        $view->display_handler->has_exposed = FALSE;
      }
    }
  }
}

/**
 * Implements hook_views_pre_render().
 */
function brandfolder_views_pre_render(ViewExecutable $view) {
  // If this is a Media Library view pertaining to a Brandfolder-sourced media
  // type, remove view header/footer/pager, as they are not part of our custom
  // browsing experience.
  if ($view->storage->id() == 'media_library') {
    if (isset($view->args[0])) {
      $media_type_id = $view->args[0];
      if (is_media_type_brandfolder_sourced($media_type_id)) {
        $view->header = $view->footer = [];
        unset($view->pager);
      }
    }
  }
}

/**
 * Procedural method to construct basic BF browser in a Media Library context.
 *
 * @param $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 * @param \Drupal\media\Entity\MediaType $media_type
 */
function brandfolder_browser_media_library_form_manipulator(&$form, FormStateInterface $form_state, MediaType $media_type) {
  $media_source = $media_type->getSource();
  /* @var Drupal\brandfolder\Service\BrandfolderGatekeeper $gatekeeper */
  $gatekeeper = \Drupal::getContainer()
    ->get('brandfolder.gatekeeper');
  $gatekeeper->loadFromMediaSource($media_source);

  $context = ['media_library'];
  $selection_limit = NULL;
  $build_info = $form_state->getBuildInfo();
  if ($build_info['base_form_id'] == 'views_form_media_library_widget' && isset($build_info['args'][0]) && $build_info['args'][0] instanceof ViewExecutable) {
    $view = $build_info['args'][0];
    if (str_starts_with($view->current_display, 'widget')) {
      try {
        $state = MediaLibraryState::fromRequest($view->getRequest());
        $selection_limit = $state->getAvailableSlots();
        $ml_opener_params = $state->getOpenerParameters();
        $field_name = $ml_opener_params['field_name'];
        $context[] = $field_name;
      } catch (Exception $e) {}
    }
  }

  // Note that we don't want to pass any previously-selected attachments
  // through to the Brandfolder browser, because the way the Media Library
  // widget works is to always add new selections to previous ones (and only
  // allow itself to be opened if slots remain). Selecting the same media entity
  // multiple times in the same multi-cardinality Drupal media reference field
  // is perfectly valid, too. Displaying prior selections and associated counts
  // within the BF browser would be confusing/conflicting/misleading (e.g.
  // deselecting them would not cause the associated media entities to be
  // removed from the lower deltas on the parent field; the BF browser would
  // incorrectly prevent those same items from being reselected; and the "x of y
  // items selected" messaging would be inaccurate).

  $bf_browser_settings = [
    'layoutHostSelector' => '#drupal-modal',
  ];
  $bf_config = \Drupal::config('brandfolder.settings');
  $preferred_browser_height = $bf_config->get('media_library_bf_browser_height');
  if ($preferred_browser_height) {
    $bf_browser_settings['height'] = $preferred_browser_height;
  }

  $context_string = implode('-', $context);
  brandfolder_browser_init($form, $form_state, $gatekeeper, [], $selection_limit, $context_string, $bf_browser_settings);

  // Completely replace standard validation with our own custom
  // validators/processors.
  $form['#validate'] = [
    'brandfolder_browser_to_media_library_selection_converter',
  ];

  // Relocate the BF browser element, using it to replace the standard Media
  // Library output entirely.
  $main_bf_browser_element = $form['brandfolder_browser'];
  unset($form['brandfolder_browser']);
  $form['output'] = [
    'brandfolder_browser' => $main_bf_browser_element
  ];
}

/**
 * Basic, agnostic function to embellish a form with elements needed for the
 * core BF browsing experience.
 *
 * @param array $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 * @param \Drupal\brandfolder\Service\BrandfolderGatekeeper $gatekeeper
 * @param array $selected_attachments
 *  An array of lightweight attachment objects keyed by attachment IDs.
 * @param int|null $selection_limit
 * @param string|null $context_id
 * @param array $bf_browser_settings
 */
function brandfolder_browser_init(array &$form, FormStateInterface $form_state, BrandfolderGatekeeper $gatekeeper, array $selected_attachments = [], int $selection_limit = NULL, string $context_id = NULL, array $bf_browser_settings = []): void {

  $bf_browser_id = $form_state->getValue('brandfolder_browser_id');
  if (empty($bf_browser_id)) {
    $all_input = $form_state->getUserInput();
    if (!empty($all_input['brandfolder_browser_id'])) {
      $bf_browser_id = $all_input['brandfolder_browser_id'];
    }
    else {
      // @todo: The ID is currently never stored in form state, so the preceding checks are pointless, and we will always generate a new UUID here. However, it seems it would be good to persist IDs sometimes, if only to avoid cluttering the tempstore. If doing so, consider whether we need to accommodate unlimited browsers within a single form.
      $uuid_service = \Drupal::service('uuid');
      $bf_browser_id = $uuid_service->generate();
    }
  }

  $bf_browser_settings_defaults = [
    'selectedAttachments' => $selected_attachments,
    'selectionLimit' => $selection_limit,
  ];

  $bf_browser_settings = array_merge($bf_browser_settings_defaults, $bf_browser_settings);

  $bf_browser_data = [
    'selection_limit' => $selection_limit,
    'gatekeeper_criteria' => $gatekeeper->getCriteria(FALSE),
    'selected_attachments' => $selected_attachments,
    'context_id' => $context_id,
  ];
  $shared_tempstore = \Drupal::service('tempstore.shared');
  $bf_browser_shared_storage = $shared_tempstore->get('brandfolder_browser_data');
  $bf_browser_shared_storage->set($bf_browser_id, $bf_browser_data);

  $form['brandfolder_browser'] = [
    '#theme' => 'brandfolder_browser',
    '#bf_browser_id' => $bf_browser_id,
    '#browser_settings' => $bf_browser_settings,
  ];

  $form['selected_bf_attachment_ids'] = [
    '#type' => 'hidden',
    '#attributes' => [
      'class' => 'selected-bf-attachment-ids',
    ],
    '#default_value' => implode(',', array_keys($selected_attachments)),
  ];

  $form['#attached']['library'][] = 'brandfolder/bf-browser-host-manager';
}

/**
 * Recursive function to generate nested select list from label tree.
 *
 * @param $labels
 * @param $select_options
 */
function brandfolder_build_labels_select_list($labels, &$select_options) {
  foreach ($labels as $id => $data) {
    $label = $data->label;
    // Add this label to the list, indented according to depth.
    $display_value = '';
    $label_depth = $label->attributes->depth;
    if ($label_depth > 1) {
      for ($i = 1; $i < $label_depth; $i++) {
        $display_value .= '-';
      }
      $display_value .= ' ';
    }
    $display_value .= $label->attributes->name;
    $select_options[$label->id] = $display_value;

    // Recurse through any children/descendants.
    if (isset($data->children) && is_array($data->children)) {
      brandfolder_build_labels_select_list($data->children, $select_options);
    }
  }
}

/**
 * Helper function to translate Brandfolder browser selections to Drupal form
 * values in a Media Library context. To be used as a form validation handler.
 *
 * @param array $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 */
function brandfolder_browser_to_media_library_selection_converter(array &$form, FormStateInterface &$form_state) {
  $selected_media_entities = [];
  $selected_media_entities_list = '';
  $selected_attachment_list = $form_state->getValue('selected_bf_attachment_ids');
  if (!empty($selected_attachment_list)) {
    $selected_attachments = explode(',', $selected_attachment_list);
    // Determine the relevant media type from the form ID.
    if (preg_match('/^views_form_media_library_widget_(.+)$/', $form['#form_id'], $matches)) {
      $media_type_id = $matches[1];
      $selected_media_entities = array_map(function ($attachment_id) use ($media_type_id) {
        return brandfolder_map_attachment_to_media_entity($attachment_id, $media_type_id);
      }, $selected_attachments);
      $selected_media_entities_list = implode(',', $selected_media_entities);
    }
  }
  $media_select_form_values = array_combine($selected_media_entities, $selected_media_entities);
  $form_state->setValue('media_library_select_form', $media_select_form_values);
  $form_state->setValue('media_library_select_form_selection', $selected_media_entities_list);
}

/**
 * Implements hook_theme().
 */
function brandfolder_theme($existing, $type, $theme, $path): array {
  return [
    'brandfolder_image_browser_widget' => [
      'render element' => 'element',
    ],
    'brandfolder_browser' => [
      'variables' => [
        'bf_browser_id' => NULL,
        'browser_format' => NULL,
        'browser_settings' => NULL,
      ],
    ],
    'page__brandfolder_browser_host_page' => [
      'base hook' => 'page',
      'path' => $path . '/templates',
    ],
  ];
}

///**
// * Prepares variables for Brandfolder Browser widget templates.
// *
// * Default template: brandfolder-browser-widget.html.twig.
// *
// * @param array $variables
// *   An associative array containing:
// *   - element: A render element representing the Brandfolder Browser widget.
// */
//function template_preprocess_brandfolder_browser_widget(&$variables) {
//  $element = $variables['element'];
//
//  $variables['attributes'] = ['class' => ['brandfolder-browser-widget', 'js-form-managed-file', 'form-managed-file', 'clearfix']];
//
//  $variables['data'] = [];
//  foreach (Element::children($element) as $child) {
//    $variables['data'][$child] = $element[$child];
//  }
//}


/**
 * Implements hook_preprocess_page__brandfolder_browser_host_page().
 */
function brandfolder_preprocess_page__brandfolder_browser_host_page(&$variables) {
  // Do not display system messages in the modal/iframe hosting the BF browser.
  $config = \Drupal::config('brandfolder.settings');
  $disable_messages = $config->get('disable_system_messages_on_browser_pages');
  $variables['messages'] = $disable_messages === TRUE ? '' : StatusMessages::renderMessages();
}

/**
 * Implements hook_theme_suggestions_alter().
 */
function brandfolder_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
  // Customize the theming for Entity Browser modal/iframe pages when they
  // involve Brandfolder browsers (if users haven't opted out of this).
  if ($hook == 'page' && (in_array('page__entity_browser__modal', $suggestions) || in_array('page__entity_browser__iframe', $suggestions))) {
    // Check module config to see if the Entity Browser modal page
    // customization option is enabled.
    $config = \Drupal::config('brandfolder.settings');
    $is_customization_enabled = $config->get('customize_entity_browser_modal_pages');
    if ($is_customization_enabled !== FALSE) {
      // Determine the EB ID from the suggestion names.
      $entity_browser_id = NULL;
      foreach ($suggestions as $suggestion) {
        if (preg_match('/^page__entity_browser__(modal|iframe)__([a-z0-9_\-]+)$/', $suggestion, $matches)) {
          $entity_browser_id = $matches[2];
          break;
        }
      }
      // Check to see if the entity browser contains a Brandfolder widget.
      try {
        /* @var \Drupal\entity_browser\Entity\EntityBrowser $eb */
        $eb = \Drupal::entityTypeManager()
          ->getStorage('entity_browser')
          ->load($entity_browser_id);
        if ($eb) {
          foreach ($eb->getWidgets() as $widget) {
            if ($widget->getPluginId() == 'brandfolder_browser') {
              // Use a custom suggestion name that's logical but won't
              // conflict with standard generated ones.
              $suggestions[] = 'page__brandfolder_browser_host_page';
              break;
            }
          }
        }
      } catch (\Exception $e) {
        return;
      }
    }
  }
}


//function brandfolder_preprocess_html(&$variables) {
//  $original_path = \Drupal::request()->query->get('original_path');
//  if ($original_path && preg_match('/entity-embed\/dialog/', $original_path)) {
//    $variables['attributes']['class'][] = 'bf-context--modal';
//  }
//}

/**
 * Given a Brandfolder attachment, return a Drupal file ID.
 *
 * If no managed file yet exists in Drupal for the given attachment, attempt to
 * create one.
 *
 * @param string|object $attachment
 *   Unique Brandfolder attachment ID, or an object representing a Brandfolder
 *   attachment. Objects must include the attachment ID, CDN URL, and parent
 *   asset ID.
 * @param bool $create_new_file
 *   If TRUE, and no Drupal file exists for the given attachment, create a new
 *   Drupal file entity and link it to the attachment.
 *
 * @return bool|int
 *   The file ID on success; FALSE on failure.
 *
 * @todo Move to class.
 */
function brandfolder_map_attachment_to_file($attachment, $create_new_file = TRUE) {
  $fid = FALSE;

  $logger = \Drupal::logger('brandfolder');

  $attachment_id = is_string($attachment) ? $attachment : $attachment->id;

  $db = \Drupal::database();
  $query = $db->select('brandfolder_file', 'bf')
    ->fields('bf', ['fid'])
    ->condition('bf_attachment_id', $attachment_id);
  if ($query->countQuery()->execute()->fetchField()) {
    $result = $query->execute();
    $fid = $result->fetch()->fid;
  }
  elseif ($create_new_file) {
    $bf = brandfolder_api();
    $params = [
      'fields' => 'cdn_url',
      'include' => 'asset',
    ];
    if (is_string($attachment)) {
      if ($attachment = $bf->fetchAttachment($attachment_id, $params)) {
        $attachment = $attachment->data;
      }
    }
    if ($bf->verboseLoggingIsEnabled()) {
      foreach ($bf->getLogData() as $log_entry) {
        $logger->debug($log_entry);
      }
      $bf->clearLogData();
    }
    if ($attachment) {
      if (!empty($attachment->relationships->asset)) {
        $asset_id = $attachment->relationships->asset->data->id;
      }
      else {
        $logger->error('Could not determine the asset for the attachment with ID !attachment_id', ['!attachment_id' => $attachment_id]);

        return FALSE;
      }
      $file_data = [
        // Note: Start with 0 (temporary) file status, which will be changed to
        // 1 (permanent) by the File module if/when the host entity is saved.
        // File module will also create a file_usage record.
        // @todo: If the file is never made permanent, remove its brandfolder_file record? Or see if we can just jump straight to permanent status? That might run afoul of file/entity reference validation.
        'status' => 0,
        'uid' => \Drupal::currentUser()->id(),
      ];
      $bf_file_data = [
        'bf_attachment_id' => $attachment_id,
        'bf_asset_id' => $asset_id,
        'timestamp' => \Drupal::time()->getRequestTime(),
      ];
      $cdn_url = $attachment->attributes->cdn_url;
      $info = brandfolder_parse_cdn_url($cdn_url);
      $bf_file_data['cdn_id'] = $info['cdn_id'];

      $filename = $attachment->attributes->filename;
      $mime_type = $attachment->attributes->mimetype;
      $mimetype_handler = \Drupal::getContainer()
        ->get('file.mime_type.guesser.brandfolder');
      if (empty($mime_type)) {
        if (!$mime_type = $mimetype_handler->guessMimeTypeFromExtension($filename)) {
          $logger->error('Attempting to map an attachment to a Drupal file but this attachment appears not to have a mimetype (attachment ID !attachment_id).', ['!attachment_id' => $attachment_id]);

          return FALSE;
        }
      }
      $file_data['filemime'] = $bf_file_data['mime_type'] = $mime_type;

      // Consider scenarios where this metadata is (temporarily) unavailable
      // due to delays in processing files for a recently created/updated
      // attachment.
      if (is_null($attachment->attributes->size) || is_null($attachment->attributes->width) || is_null($attachment->attributes->height)) {
        $logger->error('Attempting to map an attachment to a Drupal file but this attachment appears not to have the necessary file size data (attachment ID !attachment_id).', ['!attachment_id' => $attachment_id]);

        return FALSE;
      }
      $bf_file_data['filesize'] = $attachment->attributes->size;
      $bf_file_data['width'] = $attachment->attributes->width;
      $bf_file_data['height'] = $attachment->attributes->height;

      $uri = $info['uri'];
      // If the attachment has a mime type but its filename has no extension,
      // add an appropriate extension to the Drupal file's filename.
      if (!preg_match('/\.[^\.]+$/', $filename)) {
        if ($extension = $mimetype_handler->getExtensionFromMimetype($mime_type)) {
          $filename .= '.' . $extension;
          // Be sure to use that same extension in the URI so we can reliably
          // look up files by URI later. The regex op below will update the URI
          // to use that extension whether it already has an extension or not.
          $uri = preg_replace('/([^\.]+)(\.[^\.]+)?$/', "$1.$extension", $uri);
        }
      }
      $file_data['filename'] = $filename;
      $file_data['uri'] = $bf_file_data['uri'] = $uri;

      // Store data in the brandfolder_file table before saving the file, so
      // we can use it to look up things like filesize when the file is being
      // saved (@see BrandfolderStreamWrapper).
      try {
        $db->insert('brandfolder_file')
          ->fields($bf_file_data)
          ->execute();
      }
      catch (\Exception $e) {
        $logger->error('Could not insert a record into the brandfolder_file table for attachment ID !attachment_id.', ['!attachment_id' => $attachment_id]);

        return FALSE;
      }

      try {
        $file = File::Create($file_data);
        $file->save();
        $fid = $file->id();

        // Update the brandfolder_file table with the newly created FID.
        $result = $db->merge('brandfolder_file')
          ->keys([
            'uri'              => $uri,
            'bf_attachment_id' => $attachment_id,
            'bf_asset_id'      => $asset_id,
          ])
          ->fields(['fid' => $fid])
          ->execute();
        if ($result != Merge::STATUS_UPDATE) {
          $logger->error('Could not find an existing record in the brandfolder_file table for attachment ID !attachment_id and file ID !fid.', [
            '!fid'      => $fid,
            '!attachment_id' => $attachment_id
          ]);

          return FALSE;
        }

        \Drupal::moduleHandler()->invokeAll('brandfolder_file_insert', [
          $file,
          $attachment_id
        ]);
      }
      catch (EntityStorageException $e) {
        $logger->error('There was an error saving a new file for Brandfolder attachment !attachment_id.', ['!attachment_id' => $attachment_id]);

        return FALSE;
      }
    }
  }

  return $fid;
}

/**
 * Map a single Drupal file ID to a corresponding Brandfolder attachment ID
 * or lightweight attachment array.
 *
 * @param int $fid
 *  The Drupal file ID to map to a Brandfolder attachment ID.
 * @param ?string $result_format
 *  The format of the result. If 'id' (default), return the Brandfolder
 *  attachment ID. If 'array' or any other value, return a simple attachment
 *  data array, containing the attachment ID and CDN URL.
 *
 * @return bool|string
 *  The Brandfolder attachment ID if $result_format is 'id', or a lightweight
 *  attachment array if $result_format is 'array' or any other value. Returns
 *  FALSE if no mapping is found.
 */
function brandfolder_map_file_to_attachment(int $fid, ?string $result_format = 'id'): bool|string {
  $attachment = FALSE;

  $attachments = brandfolder_map_files_to_attachments([$fid], $result_format);
  if (!empty($attachments)) {
    // If the result format is 'id', return the first attachment ID.
    // Otherwise, return the first lightweight attachment array.
    $attachment = reset($attachments);
  }

  return $attachment;
}

/**
 * Map an array of Drupal file IDs to corresponding Brandfolder attachment IDs
 * or lightweight attachment arrays.
 *
 * @param string[] $file_ids
 *  An array of Drupal file IDs to map to Brandfolder attachment IDs.
 * @param ?string $result_format
 *  The format of the result. If 'id' (default), return an array of
 *  Brandfolder attachment IDs. If 'array' or any other value, return an array
 *  of simple attachment data arrays, each containing the attachment ID and
 *  CDN URL.
 *
 * @return string[]|array[]
 *  If $result_format is 'id', returns an associative array keyed by
 *  Drupal file ID, with values being the corresponding Brandfolder
 *  attachment IDs.
 *  If $result_format is 'array' or any other value, returns an
 *  associative array keyed by Brandfolder attachment ID, with values being
 *  lightweight attachment arrays containing the attachment ID and CDN URL.
 */
function brandfolder_map_files_to_attachments(array $file_ids, ?string $result_format = 'id'): array {
  $result = [];

  if (empty($file_ids)) {
    return $result;
  }

  try {
    $db = \Drupal::database();
    $query = $db->select('brandfolder_file', 'bf')
      ->condition('fid', $file_ids, 'IN');
    if ($result_format === 'id') {
      $query->fields('bf', ['fid', 'bf_attachment_id']);
      if ($query->countQuery()->execute()->fetchField()) {
        $result = $query->execute()->fetchAllKeyed();
      }
    }
    else {
      // Return an array of lightweight attachment objects, keyed by
      // attachment ID.
      $query->fields('bf', ['fid', 'bf_attachment_id', 'uri']);
      if ($query->countQuery()->execute()->fetchField()) {
        $query_result = $query->execute();
        $url_generator = \Drupal::service('file_url_generator');
        foreach ($query_result as $row) {
          $attachment_id = $row->bf_attachment_id;
          $result[$attachment_id] = [
            'id'      => $attachment_id,
            'cdn_url' => $url_generator->generateAbsoluteString($row->uri),
          ];
        }
      }
    }
  }
  catch (\Exception $e) {
    $logger = \Drupal::logger('brandfolder');
    $logger->error('Could not retrieve Brandfolder attachment IDs for the provided file IDs (@ids). Message: @message', ['@ids' => implode(', ', $file_ids), '@message' => $e->getMessage()]);

    return [];
  }

  return $result;
}

/**
 * Given a Brandfolder attachment, return a Drupal media entity ID.
 *
 * If no media entity yet exists for the given attachment, attempt to
 * create one.
 *
 * @param string|object $attachment
 *    Unique Brandfolder attachment ID, or an object representing a Brandfolder
 *    attachment.
 * @param bool $create_new_entity
 *    If TRUE, and no Drupal media entity exists for the given attachment,
 *    create a new media entity and link it to the attachment.
 *
 * @return bool|int
 *   The media entity ID on success; FALSE on failure.
 */
function brandfolder_map_attachment_to_media_entity($attachment, $media_type_id, $create_new_entity = TRUE) {
  $entity_id = FALSE;

  $logger = \Drupal::logger('brandfolder');

  $attachment_id = is_string($attachment) ? $attachment : $attachment->id;
  // @todo: Global constant, config, etc.?
  $bf_media_source_field_name = 'field_brandfolder_attachment_id';

  // Check to see if a media entity of the given type already exists for this
  // BF attachment.
  $results = \Drupal::entityQuery('media')
    ->condition('bundle', $media_type_id)
    ->condition($bf_media_source_field_name, $attachment_id)
    ->range(0, 1)
    ->accessCheck(FALSE)
    ->execute();

  if (!empty($results)) {
    $entity_id = reset($results);
  }
  elseif ($create_new_entity) {
    if ($attachment_id) {
      $bf_client = brandfolder_api();
      try {
        // Generate a default name.
        // @todo: Improve on this by allowing admins to configure patterns for auto-created entity names?
        $default_name = "Auto-Created Entity for Brandfolder Attachment $attachment_id";
        $asset = FALSE;
        if ($attachment = $bf_client->fetchAttachment($attachment_id, ['include' => 'asset'])) {
          $asset_id = $attachment->data->relationships->asset->data->id ?? FALSE;
          if ($asset_id) {
            $asset = $bf_client->fetchAsset($asset_id);
          }
          $default_name = $asset ? "{$asset->data->attributes->name} - {$attachment->data->attributes->filename}" : $attachment->data->attributes->filename;
        }
        $media = Media::create([
          'name' => $default_name,
          'bundle' => $media_type_id,
          $bf_media_source_field_name => $attachment_id,
        ]);
        $media->save();
        $entity_id = $media->id();
      }
      catch (EntityStorageException $e) {
        $logger->error('There was an error saving a new media entity for Brandfolder attachment !attachment_id.', ['!attachment_id' => $attachment_id]);
      }
      if ($bf_client->verboseLoggingIsEnabled()) {
        foreach ($bf_client->getLogData() as $log_entry) {
          $logger->debug($log_entry);
        }
        $bf_client->clearLogData();
      }
    }
  }

  return $entity_id;
}

/**
 * Given a Drupal media entity ID, return a Brandfolder attachment ID.
 *
 * @param int $media_entity_id
 *
 * @return bool|string
 *   The attachment ID on success; FALSE on failure.
 */
function brandfolder_map_media_entity_to_attachment(int $media_entity_id): bool|string {
  $attachment_id = FALSE;
  try {
    /* @var MediaInterface $media */
    $media = \Drupal::entityTypeManager()->getStorage('media')->load($media_entity_id);
    if ($media) {
      $attachment_id = $media?->getSource()?->getSourceFieldValue($media);
    }
  }
  catch (\Exception $e) {}

  if (!$attachment_id) {
    $logger = \Drupal::logger('brandfolder');
    $logger->error('Could not determine a Brandfolder attachment ID for media entity ID !media_entity_id', ['!media_entity_id' => $media_entity_id]);
    $attachment_id = FALSE;
  }

  return $attachment_id;
}

/**
 * Given an array of Drupal media entity IDs, return corresponding Brandfolder
 * attachment IDs.
 *
 * @param array $media_entity_ids
 *
 * @return string[] | false
 *  An array for each of the provided entities that are Brandfolder-sourced,
 *  keyed by attachment ID. Array values are lightweight attachment
 *  arrays (currently consisting only of id and cdn_url). FALSE on failure.
 */
function brandfolder_map_media_entities_to_attachments(array $media_entity_ids): array|bool {
  $result = [];
  try {
    if ($media_entities = \Drupal::entityTypeManager()->getStorage('media')->loadMultiple($media_entity_ids)) {
      $url_generator = \Drupal::service('file_url_generator');
      foreach ($media_entities as $media_entity_id => /* @var MediaInterface $media */ $media) {
        if (!is_media_brandfolder_sourced($media)) {
          continue;
        }
        $attachment_id = $media?->getSource()?->getSourceFieldValue($media);
        if ($attachment_id) {
          $attachment = [
            'id' => $attachment_id,
          ];
          $image_file = $media?->bf_image?->entity;
          if ($image_file) {
            if ($image_file_uri = $image_file->getFileUri()) {
              $url = $url_generator?->generateAbsoluteString($image_file_uri);
              if ($url) {
                $attachment['cdn_url'] = $url;
              }
            }
            $attachment['filename'] = $image_file?->filename?->first()?->value;
            $attachment['mimetype'] = $image_file?->filemime?->first()?->value;
            $attachment['size'] = $image_file?->filesize?->first()?->value;
          }
          $result[$attachment_id] = $attachment;
        }
        else {
          $logger = \Drupal::logger('brandfolder');
          $logger->error('Could not determine a Brandfolder attachment ID for media entity ID !media_entity_id', ['!media_entity_id' => $media_entity_id]);
        }
      }
    }
  }
  catch (\Exception $e) {
    $logger = \Drupal::logger('brandfolder');
    $logger->error('Could not determine Brandfolder attachment IDs for media entity IDs !media_entity_ids', ['!media_entity_ids' => implode(', ', $media_entity_ids)]);
    $result = FALSE;
  }

  return $result;
}

/**
 * Helper function to map an attachment ID to an asset ID, prioritizing our
 * local registry (which is updated on asset.update webhook events).
 *
 * @param string $attachment_id
 *
 * @return string|bool
 */
function brandfolder_get_asset_from_attachment(string $attachment_id) {
  $asset_id = FALSE;

  $db = \Drupal::database();
  $query = $db->select('brandfolder_file', 'bf')
    ->fields('bf', ['bf_asset_id'])
    ->condition('bf_attachment_id', $attachment_id);
  if ($query->countQuery()->execute()->fetchField()) {
    $result = $query->execute();
    $asset_id = $result->fetch()->bf_asset_id;
  }
  else {
    // If we don't have a local record of this attachment, try to fetch it
    // from the Brandfolder API.
    $bf_client = brandfolder_api();
    $attachment = $bf_client->fetchAttachment($attachment_id, ['include' => 'asset']);
    if ($attachment) {
      $asset_id = $attachment->data->relationships->asset->data->id;
    }
  }

  return $asset_id;
}

/**
 * Helper function to parse a CDN URL and return useful derived data.
 *
 * @param string $cdn_url
 *   A full URL via which a Brandfolder attachment is served on the Brandfolder CDN.
 *
 * @return array
 *   Array containing the following keys:
 *   ['uri', 'cdn_id', 'id', 'filename', 'type'].
 */
function brandfolder_parse_cdn_url($cdn_url) {
  $return = [
    'uri' => '',
    'cdn_id' => '',
    'id' => '',
    'filename' => '',
    'type' => '',
    'query' => '',
  ];

  if (preg_match("/.*cdn\.(brandfolder\.io|bfldr\.com)\/(([^\/]+)\/(as|at)\/([\w\-]+)\/([^\?]+))(\?(.*))?$/", $cdn_url, $matches)) {
    $uri_suffix = $matches[2];
    $return = [
      'uri' => 'bf://' . $uri_suffix,
      'cdn_id' => $matches[3],
      'id' => $matches[5],
      'filename' => $matches[6],
      'query' => $matches[8] ?? '',
    ];
    $return['type'] = ($matches[4] == 'as') ? 'asset' : 'attachment';
  }

  return $return;
}

/**
 * Helper function to parse a Brandfolder URI and return useful derived data.
 *
 * @param string $uri
 *
 * @return bool|array
 *  If the given URI pertains to Brandfolder, return an array containing the
 *  following keys:
 *   ['cdn_id', 'id', 'type', 'filename', 'query'].
 *  Otherwise, return FALSE.
 */
function brandfolder_parse_uri(string $uri) {
  $return = FALSE;

  if (preg_match("/^bf:\/\/(styles\/(?P<image_style>[^\/]+)\/bf\/)?((?P<cdn_id>[^\/]+)\/(?P<type_id>as|at)\/(?P<id>[\w\-]+)\/(?P<filename>[^\?]*)(\?(?P<query>.*))?)$/", $uri, $matches)) {
    $defaults = [
      'image_style' => '',
      'cdn_id' => '',
      'type_id' => 'at',
      'id' => '',
      'filename' => '',
      'query' => '',
    ];
    $return = array_intersect_key($matches, $defaults) + $defaults;
    $return['type'] = ($return['type_id'] === 'as') ? 'asset' : 'attachment';
  }

  return $return;
}

/**
 * Implements hook_ENTITY_TYPE_insert().
 */
function brandfolder_media_type_insert(MediaTypeInterface $media_type) {
  // Do not alter configuration during config sync.
  // @todo
  if ($media_type->isSyncing()) {
    return;
  }
  // Create an image field on new BrandfolderImage-sourced media types.
  if ($media_type->getSource() instanceof BrandfolderImage) {
    $source = $media_type->getSource();
    $image_field_storage = FieldStorageConfig::loadByName('media', 'bf_image');
    if (!$image_field_storage) {
      $image_field_storage = $source->createImageFieldStorage();
      $image_field_storage->save();
    }

    $image_field = $source->createImageField($media_type);
    $image_field->save();
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function brandfolder_file_delete($file) {
  // If this was a Brandfolder-related file, remove its entry from our
  // file mapping table.
  $db = \Drupal::database();
  $db->delete('brandfolder_file')
    ->condition('fid', $file->id())
    ->execute();
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 *
 * @throws EntityMalformedException|\Drupal\Core\TypedData\Exception\MissingDataException
 */
function brandfolder_media_presave(MediaInterface $media) {
  $source = $media->getSource();
  if ($source instanceof BrandfolderImage) {
    $logger = \Drupal::logger('brandfolder');
    $bf_attachment_id = $source->getSourceFieldValue($media);

    // Create an image field on media entities of a BrandfolderImage-sourced
    // type if one does not already exist. If one does exist, see if it could
    // benefit from Brandfolder metadata.
    $imageItem = $media->get('bf_image');
    $image_field_data = ($imageItem && $imageItem->count() > 0) ? $imageItem->first()->getValue() : [];
    $is_image_update_required = FALSE;
    $is_metadata_fetch_required = FALSE;
    if (empty($image_field_data)) {
      if (!empty($bf_attachment_id)) {
        // @todo: If changed. Dupe reconciliation, etc.
        if ($fid = brandfolder_map_attachment_to_file($bf_attachment_id)) {
          $file = File::load($fid);
          $image_field_data = [
            'target_id' => $fid,
          ];
          $is_image_update_required = TRUE;
          $is_metadata_fetch_required = TRUE;
          $image = Drupal::service('image.factory')->get($file->getFileUri());
          if ($image->isValid()) {
            $image_field_data['width'] = $image->getWidth();
            $image_field_data['height'] = $image->getHeight();
          }
        }
      }
    }
    else {
      if (empty($image_field_data['alt'])) {
        $is_metadata_fetch_required = TRUE;
      }
    }

    // Ensure that attachment maps to an asset.
    $bf_asset_id = brandfolder_get_asset_from_attachment($bf_attachment_id);
    if (!$bf_asset_id) {
      $msg = 'Could not map attachment ID @attachment_id to an asset.';
      $msg_vars = ['@attachment_id' => $bf_attachment_id];
      $logger->error($msg, $msg_vars);
      throw new EntityMalformedException(t($msg, $msg_vars));
    }

    if ($is_metadata_fetch_required) {
      if ($alt_text = brandfolder_get_alt_text_from_asset($bf_asset_id, $logger)) {
        $image_field_data['alt'] = $alt_text;
        $is_image_update_required = TRUE;
      }
    }
    if ($is_image_update_required) {
      $media->set('bf_image', $image_field_data);
    }

    // Also update the media "thumbnail" with image data if applicable.
    $thumbnail_default = ['target_id' => NULL, 'alt' => NULL, 'title' => NULL, 'width' => NULL, 'height' => NULL];
    $existing_thumbnail = $media->get('thumbnail');
    $existing_thumbnail_data = $existing_thumbnail ? $media->get('thumbnail')->first()->getValue() : $thumbnail_default;
    $new_thumbnail_data = array_intersect_key($image_field_data, $thumbnail_default);
    if ($new_thumbnail_data != $existing_thumbnail_data) {
      $media->set('thumbnail', $new_thumbnail_data);
    }

    // Enforce Brandfolder configuration defined at the media type level.
    // Any violations will typically be caught elsewhere in a more user-friendly
    // fashion (e.g. on media entity create/edit form), but we check here to
    // ensure nothing falls through the cracks during programmatic entity
    // updates, etc.
    $gatekeeper = \Drupal::getContainer()
      ->get('brandfolder.gatekeeper');
    $gatekeeper->loadFromMediaSource($source);
    $valid = $gatekeeper->validateBrandfolderEntities(['attachments' => [$bf_attachment_id]]);
    if (!$valid) {
      \Drupal::messenger()->addError('The asset you selected does not appear to be allowed here. Please contact an administrator for assistance.');
      $msg = $gatekeeper->getMessage();
      $logger->error($msg);
      throw new EntityMalformedException($msg);
    }
  }
}

/**
 * Helper function to retrieve alt text for a given Brandfolder asset.
 *
 * @param string $asset_id
 *   The ID of the Brandfolder asset.
 * @param \Psr\Log\LoggerInterface|null $logger
 *   Optional logger to log debug messages. If not provided, uses the default
 *   Brandfolder logger.
 *
 * @return bool|string
 *   Returns the alt text if available, FALSE otherwise.
 */
function brandfolder_get_alt_text_from_asset(string $asset_id, \Psr\Log\LoggerInterface|null $logger = NULL): bool|string {
  $alt_text = FALSE;

  if (is_null($logger)) {
    $logger = \Drupal::logger('brandfolder');
  }

  // Pull alt text from Brandfolder if a custom field has been
  // designated for that.
  $config = \Drupal::config('brandfolder.settings');
  $alt_text_custom_field_id = $config->get('alt_text_custom_field');
  if (!empty($alt_text_custom_field_id)) {
    // @todo: Update per new SDK.
    $bf = brandfolder_api();
    $params = [
      'include' => 'custom_fields',
    ];
    if ($asset = $bf->fetchAsset($asset_id, $params)) {
      // Look up the current name associated with the given custom field
      // key ID.
      if ($custom_field_keys = $bf->listCustomFields(NULL, FALSE, TRUE)) {
        if (isset($custom_field_keys[$alt_text_custom_field_id])) {
          $custom_field_name = $custom_field_keys[$alt_text_custom_field_id];
          if (!empty($asset->data->custom_field_values[$custom_field_name])) {
            $alt_text = $asset->data->custom_field_values[$custom_field_name];
          }
        }
      }
    }
    if ($bf->verboseLoggingIsEnabled()) {
      foreach ($bf->getLogData() as $log_entry) {
        $logger->debug($log_entry);
      }
      $bf->clearLogData();
    }
  }

  return $alt_text;
}

/**
 * Helper function to retrieve alt text for a given Brandfolder attachment.
 *
 * @param string $attachment_id
 *   The ID of the Brandfolder attachment.
 * @param \Psr\Log\LoggerInterface|null $logger
 *   Optional logger to log debug messages. If not provided, uses the default
 *   Brandfolder logger.
 *
 * @return bool|string
 *   Returns the alt text if available, FALSE otherwise.
 */
function brandfolder_get_alt_text_from_attachment(string $attachment_id, \Psr\Log\LoggerInterface|null $logger = NULL): bool|string {
  $alt_text = FALSE;

  $asset_id = brandfolder_get_asset_from_attachment($attachment_id);
  if ($asset_id) {
    $alt_text = brandfolder_get_alt_text_from_asset($asset_id, $logger);
  }
  else {
    if (is_null($logger)) {
      $logger = \Drupal::logger('brandfolder');
    }
    $msg = 'Could not retrieve alt text for attachment ID @attachment_id because it does not appear to be associated with an asset.';
    $msg_vars = ['@attachment_id' => $attachment_id];
    $logger->error($msg, $msg_vars);
  }

  return $alt_text;
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
//function brandfolder_media_delete($media) {
//  // If this was a Brandfolder-related media entity, remove associated
//  // auto-generated files.
//  // @todo: Decide whether we want to do this. Users may explicitly want to keep those files or at least not expect this behavior. We could prompt users upon media deletion, and/or provide a global config option dictating cascading delete behavior.
//  $source = $media->getSource();
//  // @todo: Support other BF source types if/when those are added.
//  if ($source instanceof BrandfolderImage) {
//    // Delete the file referenced by the 'bf_image' field [if it is not in use
//    // elsewhere]...
//  }
//}

/**
 * Implements hook_entity_embed_media_image_source_field_alter().
 *
 * Tell the Entity Embed module what field to use as an image "source" field
 * when dealing with BrandfolderImage media entities.
 */
function brandfolder_entity_embed_media_image_source_field_alter(&$field_name, $media) {
  $source = $media->getSource();
  $bf_image_field_name = 'bf_image';
  if ($source instanceof BrandfolderImage && $media->hasField($bf_image_field_name)) {
    $field_name = $bf_image_field_name;
  }
}

/**
 * Helper function to determine whether a given media type is sourced from
 * Brandfolder.
 *
 * @param string $media_type_id
 *
 * @return bool
 */
function is_media_type_brandfolder_sourced(string $media_type_id): bool {
  try {
    $media_type = \Drupal::entityTypeManager()
      ->getStorage('media_type')
      ->load($media_type_id);
    if ($media_type instanceof MediaTypeInterface) {

      return is_media_source_brandfolder_related($media_type->getSource());
    }
  }
  catch (\Exception $e) {}

  return FALSE;
}

/**
 * Helper function to determine whether a given media entity is sourced from
 * Brandfolder.
 *
 * @param int|\Drupal\media\MediaInterface $media
 *  Media entity ID or object.
 *
 * @return bool
 */
function is_media_brandfolder_sourced(int | MediaInterface $media): bool {
  if (is_int($media)) {
    $media = Media::load($media);
  }
  if ($media instanceof MediaInterface) {

    return is_media_source_brandfolder_related($media->getSource());
  }

  return FALSE;
}

/**
 * Helper function to determine whether a given media source is
 * Brandfolder-related.
 *
 * @param \Drupal\media\MediaSourceInterface $source
 *
 * @return bool
 */
function is_media_source_brandfolder_related(MediaSourceInterface $source): bool {

  return str_starts_with($source->getPluginId(), 'brandfolder_');
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc