media.module 16.5 KB
Newer Older
1 2 3 4 5 6 7
<?php

/**
 * @file
 * Provides media items.
 */

8
use Drupal\Component\Plugin\DerivativeInspectionInterface;
9 10
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
11
use Drupal\Core\Form\FormStateInterface;
12
use Drupal\Core\Render\Element;
13
use Drupal\Core\Render\Element\RenderElement;
14
use Drupal\Core\Routing\RouteMatchInterface;
15
use Drupal\Core\Session\AccountInterface;
16
use Drupal\Core\Template\Attribute;
17
use Drupal\Core\Url;
18
use Drupal\field\FieldConfigInterface;
19
use Drupal\media\Plugin\media\Source\OEmbedInterface;
20 21 22 23 24 25 26 27

/**
 * Implements hook_help().
 */
function media_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.media':
      $output = '<h3>' . t('About') . '</h3>';
28
      $output .= '<p>' . t('The Media module manages the creation, editing, deletion, settings, and display of media. Items are typically images, documents, slideshows, YouTube videos, tweets, Instagram photos, etc. You can reference media items from any other content on your site. For more information, see the <a href=":media">online documentation for the Media module</a>.', [':media' => 'https://www.drupal.org/docs/8/core/modules/media']) . '</p>';
29 30 31 32
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Creating media items') . '</dt>';
      $output .= '<dd>' . t('When a new media item is created, the Media module records basic information about it, including the author, date of creation, and the <a href=":media-type">media type</a>. It also manages the <em>publishing options</em>, which define whether or not the item is published. Default settings can be configured for each type of media on your site.', [':media-type' => Url::fromRoute('entity.media_type.collection')->toString()]) . '</dd>';
33 34 35 36
      $output .= '<dt>' . t('Listing media items') . '</dt>';
      $output .= '<dd>' . t('Media items are listed at the <a href=":media-collection">media administration page</a>.', [
        ':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
      ]) . '</dd>';
37
      $output .= '<dt>' . t('Creating custom media types') . '</dt>';
38 39 40 41
      $output .= '<dd>' . t('The Media module gives users with the <em>Administer media types</em> permission the ability to <a href=":media-new">create new media types</a> in addition to the default ones already configured. Each media type has an associated media source (such as the image source) which support thumbnail generation and metadata extraction. Fields managed by the <a href=":field">Field module</a> may be added for storing that metadata, such as width and height, as well as any other associated values.', [
        ':media-new' => Url::fromRoute('entity.media_type.add_form')->toString(),
        ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(),
      ]) . '</dd>';
42 43 44
      $output .= '<dt>' . t('Creating revisions') . '</dt>';
      $output .= '<dd>' . t('The Media module also enables you to create multiple versions of any media item, and revert to older versions using the <em>Revision information</em> settings.') . '</dd>';
      $output .= '<dt>' . t('User permissions') . '</dt>';
45 46 47 48 49
      $output .= '<dd>' . t('The Media module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>.', [
        ':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-media'])->toString(),
      ]) . '</dd>';
      $output .= '<dt>' . t('Adding media to other content') . '</dt>';
      $output .= '<dd>' . t('Users with permission to administer content types can add media support by adding a media reference field to the content type on the content type administration page. (The same is true of block types, taxonomy terms, user profiles, and other content that supports fields.) A media reference field can refer to any configured media type. It is possible to allow multiple media types in the same field.') . '</dd>';
50
      $output .= '</dl>';
51 52 53 54 55 56 57 58 59 60
      $output .= '<h3>' . t('Differences between Media, File, and Image reference fields') . '</h3>';
      $output .= '<p>' . t('<em>Media</em> reference fields offer several advantages over basic <em>File</em> and <em>Image</em> references:') . '</p>';
      $output .= '<ul>';
      $output .= '<li>' . t('Media reference fields can reference multiple media types in the same field.') . '</li>';
      $output .= '<li>' . t('Fields can also be added to media types themselves, which means that custom metadata like descriptions and taxonomy tags can be added for the referenced media. (Basic file and image fields do not support this.)') . '</li>';
      $output .= '<li>' . t('Media types for audio and video files are provided by default, so there is no need for additional configuration to upload these media.') . '</li>';
      $output .= '<li>' . t('Contributed or custom projects can provide additional media sources (such as third-party websites, Twitter, etc.).') . '</li>';
      $output .= '<li>' . t('Existing media items can be reused on any other content items with a media reference field.') . '</li>';
      $output .= '</ul>';
      $output .= '<p>' . t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before enabling the Media module.') . '</p>';
61 62 63 64 65 66 67 68 69 70 71 72
      return $output;
  }
}

/**
 * Implements hook_theme().
 */
function media_theme() {
  return [
    'media' => [
      'render element' => 'elements',
    ],
73 74 75 76
    'media_reference_help' => [
      'render element' => 'element',
      'base hook' => 'field_multiple_value_form',
    ],
77 78 79 80 81
    'media_oembed_iframe' => [
      'variables' => [
        'media' => NULL,
      ],
    ],
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
  ];
}

/**
 * Implements hook_entity_access().
 */
function media_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
  if ($operation === 'delete' && $entity instanceof FieldConfigInterface && $entity->getTargetEntityTypeId() === 'media') {
    /** @var \Drupal\media\MediaTypeInterface $media_type */
    $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($entity->getTargetBundle());
    return AccessResult::forbiddenIf($entity->id() === 'media.' . $media_type->id() . '.' . $media_type->getSource()->getConfiguration()['source_field']);
  }
  return AccessResult::neutral();
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function media_theme_suggestions_media(array $variables) {
  $suggestions = [];
102
  /** @var \Drupal\media\MediaInterface $media */
103 104 105 106 107 108 109
  $media = $variables['elements']['#media'];
  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');

  $suggestions[] = 'media__' . $sanitized_view_mode;
  $suggestions[] = 'media__' . $media->bundle();
  $suggestions[] = 'media__' . $media->bundle() . '__' . $sanitized_view_mode;

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
  // Add suggestions based on the source plugin ID.
  $source = $media->getSource();
  if ($source instanceof DerivativeInspectionInterface) {
    $source_id = $source->getBaseId();
    $derivative_id = $source->getDerivativeId();
    if ($derivative_id) {
      $source_id .= '__derivative_' . $derivative_id;
    }
  }
  else {
    $source_id = $source->getPluginId();
  }
  $suggestions[] = "media__source_$source_id";

  // If the source plugin uses oEmbed, add a suggestion based on the provider
  // name, if available.
  if ($source instanceof OEmbedInterface) {
    $provider_id = $source->getMetadata($media, 'provider_name');
    if ($provider_id) {
      $provider_id = \Drupal::transliteration()->transliterate($provider_id);
      $provider_id = preg_replace('/[^a-z0-9_]+/', '_', mb_strtolower($provider_id));
      $suggestions[] = end($suggestions) . "__provider_$provider_id";
    }
  }

135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
  return $suggestions;
}

/**
 * Prepares variables for media templates.
 *
 * Default template: media.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - elements: An array of elements to display in view mode.
 *   - media: The media item.
 *   - name: The label for the media item.
 *   - view_mode: View mode; e.g., 'full', 'teaser', etc.
 */
function template_preprocess_media(array &$variables) {
  $variables['media'] = $variables['elements']['#media'];
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  $variables['name'] = $variables['media']->label();

  // Helpful $content variable for templates.
  foreach (Element::children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }
}
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178

/**
 * Implements hook_field_ui_preconfigured_options_alter().
 */
function media_field_ui_preconfigured_options_alter(array &$options, $field_type) {
  // If the field is not an "entity_reference"-based field, bail out.
  /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
  $class = $field_type_manager->getPluginClass($field_type);
  if (!is_a($class, 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', TRUE)) {
    return;
  }

  // Set the default formatter for media in entity reference fields to be the
  // "Rendered entity" formatter.
  if (!empty($options['media'])) {
    $options['media']['entity_view_display']['type'] = 'entity_reference_entity_view';
  }
}
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

/**
 * Implements hook_form_FORM_ID_alter().
 */
function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Provide some help text to aid users decide whether they need a Media,
  // File, or Image reference field.
  $description_text = t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before enabling the Media module.');
  if (\Drupal::moduleHandler()->moduleExists('help')) {
    $description_text .= ' ' . t('For more information, see the <a href="@help_url">Media help page</a>.', [
      '@help_url' => Url::fromRoute('help.page', ['name' => 'media'])->toString(),
    ]);
  }
  $form['add']['description_wrapper'] = [
    '#type' => 'container',
  ];
  $field_types = [
    'file',
    'image',
    'field_ui:entity_reference:media',
  ];
  foreach ($field_types as $field_name) {
    $form['add']['description_wrapper']["description_{$field_name}"] = [
      '#type' => 'item',
      '#markup' => $description_text,
      '#states' => [
        'visible' => [
          ':input[name="new_storage_type"]' => ['value' => $field_name],
        ],
      ],
    ];
  }
  $form['add']['new_storage_type']['#weight'] = 0;
  $form['add']['description_wrapper']['#weight'] = 1;
}
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245

/**
 * Implements hook_field_widget_multivalue_form_alter().
 */
function media_field_widget_multivalue_form_alter(array &$elements, FormStateInterface $form_state, array $context) {
  // Do not alter the default settings form.
  if ($context['default']) {
    return;
  }

  // Only act on entity reference fields that reference media.
  $field_type = $context['items']->getFieldDefinition()->getType();
  $target_type = $context['items']->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
  if ($field_type !== 'entity_reference' ||  $target_type !== 'media') {
    return;
  }

  // Autocomplete widgets need different help text than options widgets.
  $widget_plugin_id = $context['widget']->getPluginId();
  if (in_array($widget_plugin_id, ['entity_reference_autocomplete', 'entity_reference_autocomplete_tags'])) {
    $is_autocomplete = TRUE;
  }
  else {
    // @todo We can't yet properly alter non-autocomplete fields. Resolve this
    //   in https://www.drupal.org/node/2943020 and remove this condition.
    return;
  }
  $elements['#media_help'] = [];

  // Retrieve the media bundle list and add information for the user based on
  // which bundles are available to be created or referenced.
  $settings = $context['items']->getFieldDefinition()->getSetting('handler_settings');
246 247 248
  $allowed_bundles = !empty($settings['target_bundles']) ? $settings['target_bundles'] : [];
  $add_url = _media_get_add_url($allowed_bundles);
  if ($add_url) {
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
    $elements['#media_help']['#media_add_help'] = t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]);
  }

  $elements['#theme'] = 'media_reference_help';
  // @todo template_preprocess_field_multiple_value_form() assumes this key
  //   exists, but it does not exist in the case of a single widget that
  //   accepts multiple values. This is for some reason necessary to use
  //   our template for the entity_autocomplete_tags widget.
  //   Research and resolve this in https://www.drupal.org/node/2943020.
  if (empty($elements['#cardinality_multiple'])) {
    $elements['#cardinality_multiple'] = NULL;
  }

  // Use the title set on the element if it exists, otherwise fall back to the
  // field label.
  $elements['#media_help']['#original_label'] = isset($elements['#title']) ? $elements['#title'] : $context['items']->getFieldDefinition()->getLabel();

  // Customize the label for the field widget.
  // @todo Research a better approach https://www.drupal.org/node/2943024.
  $use_existing_label = t('Use existing media');
  if (!empty($elements[0]['target_id']['#title'])) {
    $elements[0]['target_id']['#title'] = $use_existing_label;
  }
  if (!empty($elements['#title'])) {
    $elements['#title'] = $use_existing_label;
  }
  if (!empty($elements['target_id']['#title'])) {
    $elements['target_id']['#title'] = $use_existing_label;
  }

  // This help text is only relevant for autocomplete widgets. When the user
  // is presented with options, they don't need to type anything or know what
  // types of media are allowed.
  if ($is_autocomplete) {
    $elements['#media_help']['#media_list_help'] = t('Type part of the media name.');

    $overview_url = Url::fromRoute('entity.media.collection');
    if ($overview_url->access()) {
      $elements['#media_help']['#media_list_link'] = t('See the <a href=":list_url" target="_blank">media list</a> (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]);
    }
289 290 291 292
    $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
    $bundle_labels = array_map(function ($bundle) use ($all_bundles) {
      return $all_bundles[$bundle]['label'];
    }, $allowed_bundles);
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    $elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]);
  }
}

/**
 * Implements hook_preprocess_HOOK() for media reference widgets.
 */
function media_preprocess_media_reference_help(&$variables) {
  // Most of these attribute checks are copied from
  // template_preprocess_fieldset(). Our template extends
  // field-multiple-value-form.html.twig to provide our help text, but also
  // groups the information within a semantic fieldset with a legend. So, we
  // incorporate parity for both.
  $element = $variables['element'];
  Element::setAttributes($element, ['id']);
  RenderElement::setAttributes($element);
  $variables['attributes'] = isset($element['#attributes']) ? $element['#attributes'] : [];
  $variables['legend_attributes'] = new Attribute();
  $variables['header_attributes'] = new Attribute();
  $variables['description']['attributes'] = new Attribute();
  $variables['legend_span_attributes'] = new Attribute();

  if (!empty($element['#media_help'])) {
    foreach ($element['#media_help'] as $key => $text) {
      $variables[substr($key, 1)] = $text;
    }
  }
}
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349

/**
 * Returns the appropriate URL to add media for the current user.
 *
 * @todo Remove in https://www.drupal.org/project/drupal/issues/2938116
 *
 * @param string[] $allowed_bundles
 *   An array of bundles that should be checked for create access.
 *
 * @return bool|\Drupal\Core\Url
 *   The URL to add media, or FALSE if the user cannot create any media.
 *
 * @internal
 *   This function is internal and may be removed in a minor release.
 */
function _media_get_add_url($allowed_bundles) {
  $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media');
  $create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess']);

  // Add a section about how to create media if the user has access to do so.
  if (count($create_bundles) === 1) {
    return Url::fromRoute('entity.media.add_form', ['media_type' => reset($create_bundles)])->toString();
  }
  elseif (count($create_bundles) > 1) {
    return Url::fromRoute('entity.media.add_page')->toString();
  }

  return FALSE;
}