clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\media_library\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* AJAX command for adding media items to the media library selection.
*
* This command instructs the client to add the given media item IDs to the
* current selection of the media library stored in
* Drupal.MediaLibrary.currentSelection.
*
* This command is implemented by
* Drupal.AjaxCommands.prototype.updateMediaLibrarySelection() defined in
* media_library.ui.js.
*
* @ingroup ajax
*
* @internal
* This is an internal part of Media Library and may be subject to change in
* minor releases. External code should not instantiate or extend this class.
*/
class UpdateSelectionCommand implements CommandInterface {
/**
* An array of media IDs to add to the current selection.
*
* @var int[]
*/
protected $mediaIds;
/**
* Constructs an UpdateSelectionCommand object.
*
* @param int[] $media_ids
* An array of media IDs to add to the current selection.
*/
public function __construct(array $media_ids) {
$this->mediaIds = $media_ids;
}
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'updateMediaLibrarySelection',
'mediaIds' => $this->mediaIds,
];
}
}

View File

@@ -0,0 +1,899 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\FocusFirstCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
use Drupal\media_library\Ajax\UpdateSelectionCommand;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\OpenerResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for creating media items from within the media library.
*/
abstract class AddFormBase extends FormBase implements BaseFormIdInterface, TrustedCallbackInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The media library UI builder.
*
* @var \Drupal\media_library\MediaLibraryUiBuilder
*/
protected $libraryUiBuilder;
/**
* The type of media items being created by this form.
*
* @var \Drupal\media\MediaTypeInterface
*/
protected $mediaType;
/**
* The media view builder.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $viewBuilder;
/**
* The opener resolver.
*
* @var \Drupal\media_library\OpenerResolverInterface
*/
protected $openerResolver;
/**
* Constructs an AddFormBase object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
* The media library UI builder.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, OpenerResolverInterface $opener_resolver) {
$this->entityTypeManager = $entity_type_manager;
$this->libraryUiBuilder = $library_ui_builder;
$this->viewBuilder = $this->entityTypeManager->getViewBuilder('media');
$this->openerResolver = $opener_resolver;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('media_library.ui_builder'),
$container->get('media_library.opener_resolver')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'media_library_add_form';
}
/**
* Get the media type from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaTypeInterface
* The media type.
*
* @throws \InvalidArgumentException
* If the selected media type does not exist.
*/
protected function getMediaType(FormStateInterface $form_state) {
if ($this->mediaType) {
return $this->mediaType;
}
$state = $this->getMediaLibraryState($form_state);
$selected_type_id = $state->getSelectedTypeId();
$this->mediaType = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id);
if (!$this->mediaType) {
throw new \InvalidArgumentException("The '$selected_type_id' media type does not exist.");
}
return $this->mediaType;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// @todo Remove the ID when we can use selectors to replace content via
// AJAX in https://www.drupal.org/project/drupal/issues/2821793.
$form['#prefix'] = '<div id="media-library-add-form-wrapper">';
$form['#suffix'] = '</div>';
// The media library is loaded via AJAX, which means that the form action
// URL defaults to the current URL. However, to add media, we always need to
// submit the form to the media library URL, not whatever the current URL
// may be.
$form['#action'] = Url::fromRoute('media_library.ui', [], [
'query' => $this->getMediaLibraryState($form_state)->all(),
])->toString();
// The form is posted via AJAX. When there are messages set during the
// validation or submission of the form, the messages need to be shown to
// the user.
$form['status_messages'] = [
'#type' => 'status_messages',
];
$form['#attributes']['class'] = [
'js-media-library-add-form',
];
$added_media = $this->getAddedMediaItems($form_state);
if (empty($added_media)) {
$form = $this->buildInputElement($form, $form_state);
}
else {
$form['#attributes']['data-input'] = 'true';
// This deserves to be themeable, but it doesn't need to be its own "real"
// template.
$form['description'] = [
'#type' => 'inline_template',
'#template' => '<p>{{ text }}</p>',
'#context' => [
'text' => $this->formatPlural(count($added_media), 'The media item has been created but has not yet been saved. Fill in any required fields and save to add it to the media library.', 'The media items have been created but have not yet been saved. Fill in any required fields and save to add them to the media library.'),
],
];
$form['media'] = [
'#pre_render' => [
[$this, 'preRenderAddedMedia'],
],
'#attributes' => [
'class' => [
// This needs to be focus-able by an AJAX response.
// @see ::updateFormCallback()
'js-media-library-add-form-added-media',
],
'aria-label' => $this->t('Added media items'),
// Add the tabindex '-1' to allow the focus to be shifted to the added
// media wrapper when items are added. We set focus to the container
// because a media item does not necessarily have required fields and
// we do not want to set focus to the remove button automatically.
// @see ::updateFormCallback()
'tabindex' => '-1',
],
];
foreach ($added_media as $delta => $media) {
$form['media'][$delta] = $this->buildEntityFormElement($media, $form, $form_state, $delta);
}
$form['selection'] = $this->buildCurrentSelectionArea($form, $form_state);
$form['actions'] = $this->buildActions($form, $form_state);
}
// Allow the current selection to be set in a hidden field so the selection
// can be passed between different states of the form. This field is filled
// via JavaScript so the default value should be empty.
// @see Drupal.behaviors.MediaLibraryItemSelection
$form['current_selection'] = [
'#type' => 'hidden',
'#default_value' => '',
'#attributes' => [
'class' => [
'js-media-library-add-form-current-selection',
],
],
];
return $form;
}
/**
* Builds the element for submitting source field value(s).
*
* The input element needs to have a submit handler to create media items from
* the user input and store them in the form state using
* ::processInputValues().
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* The complete form, with the element added.
*
* @see ::processInputValues()
*/
abstract protected function buildInputElement(array $form, FormStateInterface $form_state);
/**
* Builds the sub-form for setting required fields on a new media item.
*
* @param \Drupal\media\MediaInterface $media
* A new, unsaved media item.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param int $delta
* The delta of the media item.
*
* @return array
* The element containing the required fields sub-form.
*/
protected function buildEntityFormElement(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
// We need to make sure each button has a unique name attribute. The default
// name for button elements is 'op'. If the name is not unique, the
// triggering element is not set correctly and the wrong media item is
// removed.
// @see ::removeButtonSubmit()
$parents = $form['#parents'] ?? [];
$id_suffix = $parents ? '-' . implode('-', $parents) : '';
$element = [
'#wrapper_attributes' => [
'aria-label' => $media->getName(),
// Add the tabindex '-1' to allow the focus to be shifted to the next
// media item when an item is removed. We set focus to the container
// because a media item does not necessarily have required fields and we
// do not want to set focus to the remove button automatically.
// @see ::updateFormCallback()
'tabindex' => '-1',
// Add a data attribute containing the delta to allow us to easily shift
// the focus to a specific media item.
// @see ::updateFormCallback()
'data-media-library-added-delta' => $delta,
],
'preview' => [
'#type' => 'container',
'#weight' => 10,
],
'fields' => [
'#type' => 'container',
'#weight' => 20,
// The '#parents' are set here because the entity form display needs it
// to build the entity form fields.
'#parents' => ['media', $delta, 'fields'],
],
'remove_button' => [
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#name' => 'media-' . $delta . '-remove-button' . $id_suffix,
'#weight' => 30,
'#attributes' => [
'aria-label' => $this->t('Remove @label', ['@label' => $media->getName()]),
],
'#ajax' => [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-add-form-wrapper',
'message' => $this->t('Removing @label.', ['@label' => $media->getName()]),
],
'#submit' => ['::removeButtonSubmit'],
// Ensure errors in other media items do not prevent removal.
'#limit_validation_errors' => [],
],
];
// @todo Make the image style configurable in
// https://www.drupal.org/node/2988223
$source = $media->getSource();
$plugin_definition = $source->getPluginDefinition();
if ($thumbnail_uri = $source->getMetadata($media, $plugin_definition['thumbnail_uri_metadata_attribute'])) {
$element['preview']['thumbnail'] = [
'#theme' => 'image_style',
'#style_name' => 'media_library',
'#uri' => $thumbnail_uri,
];
}
$form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
// When the name is not added to the form as an editable field, output
// the name as a fixed element to confirm the right file was uploaded.
if (!$form_display->getComponent('name')) {
$element['fields']['name'] = [
'#type' => 'item',
'#title' => $this->t('Name'),
'#markup' => $media->getName(),
];
}
$form_display->buildForm($media, $element['fields'], $form_state);
// Add source field name so that it can be identified in form alter and
// widget alter hooks.
$element['fields']['#source_field_name'] = $this->getSourceFieldName($media->bundle->entity);
// The revision log field is currently not configurable from the form
// display, so hide it by changing the access.
// @todo Make the revision_log_message field configurable in
// https://www.drupal.org/project/drupal/issues/2696555
if (isset($element['fields']['revision_log_message'])) {
$element['fields']['revision_log_message']['#access'] = FALSE;
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderAddedMedia'];
}
/**
* Converts the set of newly added media into an item list for rendering.
*
* @param array $element
* The render element to transform.
*
* @return array
* The transformed render element.
*/
public function preRenderAddedMedia(array $element) {
// Transform the element into an item list for rendering.
$element['#theme'] = 'item_list__media_library_add_form_media_list';
$element['#list_type'] = 'ul';
foreach (Element::children($element) as $delta) {
$element['#items'][$delta] = $element[$delta];
unset($element[$delta]);
}
return $element;
}
/**
* Returns a render array containing the current selection.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* A render array containing the current selection.
*/
protected function buildCurrentSelectionArea(array $form, FormStateInterface $form_state) {
$pre_selected_items = $this->getPreSelectedMediaItems($form_state);
if (!$pre_selected_items || !$this->isAdvancedUi()) {
return [];
}
$selection = [
'#type' => 'details',
'#theme_wrappers' => [
'details__media_library_add_form_selected_media',
],
'#open' => FALSE,
'#title' => $this->t('Additional selected media'),
];
foreach ($pre_selected_items as $media_id => $media) {
$selection[$media_id] = $this->buildSelectedItemElement($media, $form, $form_state);
}
return $selection;
}
/**
* Returns a render array for a single pre-selected media item.
*
* @param \Drupal\media\MediaInterface $media
* The media item.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* A render array of a pre-selected media item.
*/
protected function buildSelectedItemElement(MediaInterface $media, array $form, FormStateInterface $form_state) {
return [
'#theme' => 'media_library_item__small',
'#attributes' => [
'class' => [
'js-media-library-item',
'js-click-to-select',
],
],
'select' => [
'#type' => 'container',
'#attributes' => [
'class' => [
'js-click-to-select-checkbox',
],
],
'select_checkbox' => [
'#type' => 'checkbox',
'#title' => $this->t('Select @name', ['@name' => $media->label()]),
'#title_display' => 'invisible',
'#return_value' => $media->id(),
// The checkbox's value is never processed by this form. It is present
// for usability and accessibility reasons, and only used by
// JavaScript to track whether or not this media item is selected. The
// hidden 'current_selection' field is used to store the actual IDs of
// selected media items.
'#value' => FALSE,
],
],
'rendered_entity' => $this->viewBuilder->view($media, 'media_library'),
];
}
/**
* Returns an array of supported actions for the form.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* An actions element containing the actions of the form.
*/
protected function buildActions(array $form, FormStateInterface $form_state) {
$actions = [
'#type' => 'actions',
'save_select' => [
'#type' => 'submit',
'#button_type' => 'primary',
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => '::updateLibrary',
'wrapper' => 'media-library-add-form-wrapper',
],
],
];
if ($this->isAdvancedUi()) {
$actions['save_select']['#value'] = $this->t('Save and select');
$actions['save_insert'] = [
'#type' => 'submit',
'#value' => $this->t('Save and insert'),
'#ajax' => [
'callback' => '::updateWidget',
'wrapper' => 'media-library-add-form-wrapper',
],
];
}
return $actions;
}
/**
* Creates media items from source field input values.
*
* @param mixed[] $source_field_values
* The values for source fields of the media items.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*/
protected function processInputValues(array $source_field_values, array $form, FormStateInterface $form_state) {
$media_type = $this->getMediaType($form_state);
$media_storage = $this->entityTypeManager->getStorage('media');
$source_field_name = $this->getSourceFieldName($media_type);
$media = array_map(function ($source_field_value) use ($media_type, $media_storage, $source_field_name) {
return $this->createMediaFromValue($media_type, $media_storage, $source_field_name, $source_field_value);
}, $source_field_values);
// Re-key the media items before setting them in the form state.
$form_state->set('media', array_values($media));
// Save the selected items in the form state so they are remembered when an
// item is removed.
$media = $this->entityTypeManager->getStorage('media')
->loadMultiple(explode(',', $form_state->getValue('current_selection')));
// Any ID can be passed to the form, so we have to check access.
$form_state->set('current_selection', array_filter($media, function ($media_item) {
return $media_item->access('view');
}));
$form_state->setRebuild();
}
/**
* Creates a new, unsaved media item from a source field value.
*
* @param \Drupal\media\MediaTypeInterface $media_type
* The media type of the media item.
* @param \Drupal\Core\Entity\EntityStorageInterface $media_storage
* The media storage.
* @param string $source_field_name
* The name of the media type's source field.
* @param mixed $source_field_value
* The value for the source field of the media item.
*
* @return \Drupal\media\MediaInterface
* An unsaved media entity.
*/
protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $source_field_value) {
$media = $media_storage->create([
'bundle' => $media_type->id(),
$source_field_name => $source_field_value,
]);
$media->setName($media->getName());
return $media;
}
/**
* Prepares a created media item to be permanently saved.
*
* @param \Drupal\media\MediaInterface $media
* The unsaved media item.
*/
protected function prepareMediaEntityForSave(MediaInterface $media) {
// Intentionally empty by default.
}
/**
* Submit handler for the remove button.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function removeButtonSubmit(array $form, FormStateInterface $form_state) {
// Retrieve the delta of the media item from the parents of the remove
// button.
$triggering_element = $form_state->getTriggeringElement();
$delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
$added_media = $form_state->get('media');
$removed_media = $added_media[$delta];
// Update the list of added media items in the form state.
unset($added_media[$delta]);
// Update the media items in the form state.
$form_state->set('media', $added_media)->setRebuild();
// Show a message to the user to confirm the media is removed.
$this->messenger()->addStatus($this->t('The media item %label has been removed.', ['%label' => $removed_media->label()]));
}
/**
* AJAX callback to update the entire form based on source field input.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse|array
* The form render array or an AJAX response object.
*/
public function updateFormCallback(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$wrapper_id = $triggering_element['#ajax']['wrapper'];
$added_media = $form_state->get('media');
$response = new AjaxResponse();
// When the source field input contains errors, replace the existing form to
// let the user change the source field input. If the user input is valid,
// the entire modal is replaced with the second step of the form to show the
// form fields for each media item.
if ($form_state::hasAnyErrors()) {
$response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $form));
return $response;
}
// Check if the remove button is clicked.
if (end($triggering_element['#parents']) === 'remove_button') {
// When the list of added media is empty, return to the media library and
// shift focus back to the first tabbable element (which should be the
// source field).
if (empty($added_media)) {
$response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
$response->addCommand(new FocusFirstCommand('#media-library-add-form-wrapper'));
}
// When there are still more items, update the form and shift the focus to
// the next media item. If the last list item is removed, shift focus to
// the previous item.
else {
$response->addCommand(new ReplaceCommand("#$wrapper_id", $form));
// Find the delta of the next media item. If there is no item with a
// bigger delta, we automatically use the delta of the previous item and
// shift the focus there.
$removed_delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
$delta_to_focus = 0;
foreach ($added_media as $delta => $media) {
$delta_to_focus = $delta;
if ($delta > $removed_delta) {
break;
}
}
$response->addCommand(new InvokeCommand("[data-media-library-added-delta=$delta_to_focus]", 'focus'));
}
}
// Update the form and shift focus to the added media items.
else {
$response->addCommand(new ReplaceCommand("#$wrapper_id", $form));
$response->addCommand(new InvokeCommand('.js-media-library-add-form-added-media', 'focus'));
}
return $response;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
foreach ($this->getAddedMediaItems($form_state) as $delta => $media) {
$this->validateMediaEntity($media, $form, $form_state, $delta);
}
}
/**
* Validate a created media item.
*
* @param \Drupal\media\MediaInterface $media
* The media item to validate.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param int $delta
* The delta of the media item.
*/
protected function validateMediaEntity(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
$form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
$form_display->extractFormValues($media, $form['media'][$delta]['fields'], $form_state);
$form_display->validateFormValues($media, $form['media'][$delta]['fields'], $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($this->getAddedMediaItems($form_state) as $delta => $media) {
EntityFormDisplay::collectRenderDisplay($media, 'media_library')
->extractFormValues($media, $form['media'][$delta]['fields'], $form_state);
$this->prepareMediaEntityForSave($media);
$media->save();
}
}
/**
* AJAX callback to send the new media item(s) to the media library.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array|\Drupal\Core\Ajax\AjaxResponse
* The form array if there are validation errors, or an AJAX response to add
* the created items to the current selection.
*/
public function updateLibrary(array &$form, FormStateInterface $form_state) {
if ($form_state::hasAnyErrors()) {
return $form;
}
$media_ids = array_map(function (MediaInterface $media) {
return $media->id();
}, $this->getAddedMediaItems($form_state));
$selected_count = $this->getSelectedMediaItemCount($media_ids, $form_state);
$response = new AjaxResponse();
$response->addCommand(new UpdateSelectionCommand($media_ids));
$media_id_to_focus = array_pop($media_ids);
$response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
$response->addCommand(new InvokeCommand("#media-library-content [value=$media_id_to_focus]", 'focus'));
$available_slots = $this->getMediaLibraryState($form_state)->getAvailableSlots();
if ($available_slots > 0 && $selected_count > $available_slots) {
$warning = $this->formatPlural($selected_count - $available_slots, 'There are currently @total items selected. The maximum number of items for the field is @max. Remove @count item from the selection.', 'There are currently @total items selected. The maximum number of items for the field is @max. Remove @count items from the selection.', [
'@total' => $selected_count,
'@max' => $available_slots,
]);
$response->addCommand(new MessageCommand($warning, '#media-library-messages', ['type' => 'warning']));
}
return $response;
}
/**
* Build the render array of the media library UI.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* The render array for the media library.
*/
protected function buildMediaLibraryUi(FormStateInterface $form_state) {
// Get the render array for the media library. The media library state might
// contain the 'media_library_content' when it has been opened from a
// vertical tab. We need to remove that to make sure the render array
// contains the vertical tabs. Besides that, we also need to force the media
// library to create a new instance of the media add form.
// @see \Drupal\media_library\MediaLibraryUiBuilder::buildMediaTypeAddForm()
$state = $this->getMediaLibraryState($form_state);
$state->remove('media_library_content');
$state->set('_media_library_form_rebuild', TRUE);
return $this->libraryUiBuilder->buildUi($state);
}
/**
* AJAX callback to send the new media item(s) to the calling code.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array|\Drupal\Core\Ajax\AjaxResponse
* The form array when there are form errors or an AJAX response to select
* the created items in the media library.
*/
public function updateWidget(array &$form, FormStateInterface $form_state) {
if ($form_state::hasAnyErrors()) {
return $form;
}
// The added media items get an ID when they are saved in ::submitForm().
// For that reason the added media items are keyed by delta in the form
// state and we have to do an array map to get each media ID.
$media_ids = array_map(function (MediaInterface $media) {
return $media->id();
}, $this->getCurrentMediaItems($form_state));
// Allow the opener service to respond to the selection.
$state = $this->getMediaLibraryState($form_state);
$selected_count = $this->getSelectedMediaItemCount($media_ids, $form_state);
$available_slots = $this->getMediaLibraryState($form_state)->getAvailableSlots();
if ($available_slots > 0 && $selected_count > $available_slots) {
// Return to library where we display a warning about the overage.
return $this->updateLibrary($form, $form_state);
}
return $this->openerResolver->get($state)
->getSelectionResponse($state, $media_ids)
->addCommand(new CloseDialogCommand());
}
/**
* Get the number of selected media.
*
* @param array $media_ids
* Array with the media IDs.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return int
* The number of media currently selected.
*/
private function getSelectedMediaItemCount(array $media_ids, FormStateInterface $form_state): int {
$selected_count = count($media_ids);
if ($current_selection = $form_state->getValue('current_selection')) {
$selected_count += count(explode(',', $current_selection));
}
return $selected_count;
}
/**
* Get the media library state from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media_library\MediaLibraryState
* The media library state.
*
* @throws \InvalidArgumentException
* If the media library state is not present in the form state.
*/
protected function getMediaLibraryState(FormStateInterface $form_state) {
$state = $form_state->get('media_library_state');
if (!$state) {
throw new \InvalidArgumentException('The media library state is not present in the form state.');
}
return $state;
}
/**
* Returns the name of the source field for a media type.
*
* @param \Drupal\media\MediaTypeInterface $media_type
* The media type to get the source field name for.
*
* @return string
* The name of the media type's source field.
*/
protected function getSourceFieldName(MediaTypeInterface $media_type) {
return $media_type->getSource()
->getSourceFieldDefinition($media_type)
->getName();
}
/**
* Get all pre-selected media items from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaInterface[]
* An array containing the pre-selected media items keyed by ID.
*/
protected function getPreSelectedMediaItems(FormStateInterface $form_state) {
// Get the pre-selected media items from the form state.
// @see ::processInputValues()
return $form_state->get('current_selection') ?: [];
}
/**
* Get all added media items from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaInterface[]
* An array containing the added media items keyed by delta. The media items
* won't have an ID until they are saved in ::submitForm().
*/
protected function getAddedMediaItems(FormStateInterface $form_state) {
return $form_state->get('media') ?: [];
}
/**
* Get all pre-selected and added media items from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaInterface[]
* An array containing all pre-selected and added media items with
* renumbered numeric keys.
*/
protected function getCurrentMediaItems(FormStateInterface $form_state) {
$pre_selected_media = $this->getPreSelectedMediaItems($form_state);
$added_media = $this->getAddedMediaItems($form_state);
// Using array_merge will renumber the numeric keys.
return array_merge($pre_selected_media, $added_media);
}
/**
* Determines if the "advanced UI" of the Media Library is enabled.
*
* This exposes additional features that are useful to power users.
*
* @return bool
* TRUE if the advanced UI is enabled, FALSE otherwise.
*
* @see ::buildActions()
* @see ::buildCurrentSelectionArea()
*/
protected function isAdvancedUi() {
return (bool) $this->config('media_library.settings')->get('advanced_ui');
}
}

View File

@@ -0,0 +1,393 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\File\Exception\FileWriteException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\file\FileRepositoryInterface;
use Drupal\file\FileInterface;
use Drupal\file\FileUsage\FileUsageInterface;
use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
use Drupal\file\Plugin\Field\FieldType\FileItem;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\OpenerResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Creates a form to create media entities from uploaded files.
*
* @internal
* Form classes are internal.
*/
class FileUploadForm extends AddFormBase {
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $renderer;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The file usage service.
*
* @var \Drupal\file\FileUsage\FileUsageInterface
*/
protected $fileUsage;
/**
* The file repository service.
*
* @var \Drupal\file\FileRepositoryInterface
*/
protected $fileRepository;
/**
* Constructs a new FileUploadForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
* The media library UI builder.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
* @param \Drupal\file\FileUsage\FileUsageInterface $file_usage
* The file usage service.
* @param \Drupal\file\FileRepositoryInterface $file_repository
* The file repository service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, ElementInfoManagerInterface $element_info, RendererInterface $renderer, FileSystemInterface $file_system, OpenerResolverInterface $opener_resolver, FileUsageInterface $file_usage, FileRepositoryInterface $file_repository) {
parent::__construct($entity_type_manager, $library_ui_builder, $opener_resolver);
$this->elementInfo = $element_info;
$this->renderer = $renderer;
$this->fileSystem = $file_system;
$this->fileUsage = $file_usage;
$this->fileRepository = $file_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('media_library.ui_builder'),
$container->get('element_info'),
$container->get('renderer'),
$container->get('file_system'),
$container->get('media_library.opener_resolver'),
$container->get('file.usage'),
$container->get('file.repository')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return $this->getBaseFormId() . '_upload';
}
/**
* {@inheritdoc}
*/
protected function getMediaType(FormStateInterface $form_state) {
if ($this->mediaType) {
return $this->mediaType;
}
$media_type = parent::getMediaType($form_state);
// The file upload form only supports media types which use a file field as
// a source field.
$field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type);
if (!is_a($field_definition->getClass(), FileFieldItemList::class, TRUE)) {
throw new \InvalidArgumentException('Can only add media types which use a file field as a source field.');
}
return $media_type;
}
/**
* {@inheritdoc}
*/
protected function buildInputElement(array $form, FormStateInterface $form_state) {
// Create a file item to get the upload validators.
$media_type = $this->getMediaType($form_state);
$item = $this->createFileItem($media_type);
/** @var \Drupal\media_library\MediaLibraryState $state */
$state = $this->getMediaLibraryState($form_state);
if (!$state->hasSlotsAvailable()) {
return $form;
}
$slots = $state->getAvailableSlots();
// Add a container to group the input elements for styling purposes.
$form['container'] = [
'#type' => 'container',
];
$process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
$form['container']['upload'] = [
'#type' => 'managed_file',
'#title' => $this->formatPlural($slots, 'Add file', 'Add files'),
// @todo Move validation in https://www.drupal.org/node/2988215
'#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']),
'#upload_validators' => $item->getUploadValidators(),
// Set multiple to true only if available slots is not exactly one
// to ensure correct language (singular or plural) in UI
'#multiple' => $slots != 1 ? TRUE : FALSE,
// Do not limit the number uploaded. There is validation based on the
// number selected in the media library that prevents overages.
// @see Drupal\media_library\Form\AddFormBase::updateLibrary()
'#cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'#remaining_slots' => $slots,
];
$file_upload_help = [
'#theme' => 'file_upload_help',
'#upload_validators' => $form['container']['upload']['#upload_validators'],
'#cardinality' => $slots,
];
// The file upload help needs to be rendered since the description does not
// accept render arrays. The FileWidget::formElement() method adds the file
// upload help in the same way, so any theming improvements made to file
// fields would also be applied to this upload field.
// @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formElement()
$form['container']['upload']['#description'] = $this->renderer->renderInIsolation($file_upload_help);
return $form;
}
/**
* Validates the upload element.
*
* @param array $element
* The upload element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The processed upload element.
*/
public function validateUploadElement(array $element, FormStateInterface $form_state) {
if ($form_state::hasAnyErrors()) {
// When an error occurs during uploading files, remove all files so the
// user can re-upload the files.
$element['#value'] = [];
}
$values = $form_state->getValue('upload', []);
if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
$form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [
'@count' => $element['#cardinality'],
]));
$form_state->setValue('upload', []);
$element['#value'] = [];
}
return $element;
}
/**
* Processes an upload (managed_file) element.
*
* @param array $element
* The upload element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The processed upload element.
*/
public function processUploadElement(array $element, FormStateInterface $form_state) {
$element['upload_button']['#submit'] = ['::uploadButtonSubmit'];
// Limit the validation errors to make sure
// FormValidator::handleErrorsWithLimitedValidation doesn't remove the
// current selection from the form state.
// @see Drupal\Core\Form\FormValidator::handleErrorsWithLimitedValidation()
$element['upload_button']['#limit_validation_errors'] = [
['upload'],
['current_selection'],
];
$element['upload_button']['#ajax'] = [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-wrapper',
// Add a fixed URL to post the form since AJAX forms are automatically
// posted to <current> instead of $form['#action'].
// @todo Remove when https://www.drupal.org/project/drupal/issues/2504115
// is fixed.
'url' => Url::fromRoute('media_library.ui'),
'options' => [
'query' => $this->getMediaLibraryState($form_state)->all() + [
FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
],
],
];
return $element;
}
/**
* {@inheritdoc}
*/
protected function buildEntityFormElement(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
$element = parent::buildEntityFormElement($media, $form, $form_state, $delta);
$source_field = $this->getSourceFieldName($media->bundle->entity);
if (isset($element['fields'][$source_field])) {
$element['fields'][$source_field]['widget'][0]['#process'][] = [static::class, 'hideExtraSourceFieldComponents'];
}
return $element;
}
/**
* Processes an image or file source field element.
*
* @param array $element
* The entity form source field element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param array $form
* The complete form.
*
* @return array
* The processed form element.
*/
public static function hideExtraSourceFieldComponents($element, FormStateInterface $form_state, $form) {
// Remove original button added by ManagedFile::processManagedFile().
if (!empty($element['remove_button'])) {
$element['remove_button']['#access'] = FALSE;
}
// Remove preview added by ImageWidget::process().
if (!empty($element['preview'])) {
$element['preview']['#access'] = FALSE;
}
$element['#title_display'] = 'none';
$element['#description_display'] = 'none';
// Remove the filename display.
foreach ($element['#files'] as $file) {
$element['file_' . $file->id()]['filename']['#access'] = FALSE;
}
return $element;
}
/**
* Submit handler for the upload button, inside the managed_file element.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function uploadButtonSubmit(array $form, FormStateInterface $form_state) {
$files = $this->entityTypeManager
->getStorage('file')
->loadMultiple($form_state->getValue('upload', []));
$this->processInputValues($files, $form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $file) {
if (!($file instanceof FileInterface)) {
throw new \InvalidArgumentException('Cannot create a media item without a file entity.');
}
// Create a file item to get the upload location.
$item = $this->createFileItem($media_type);
$upload_location = $item->getUploadLocation();
if (!$this->fileSystem->prepareDirectory($upload_location, FileSystemInterface::CREATE_DIRECTORY)) {
throw new FileWriteException("The destination directory '$upload_location' is not writable");
}
$file = $this->fileRepository->move($file, $upload_location);
if (!$file) {
throw new \RuntimeException("Unable to move file to '$upload_location'");
}
return parent::createMediaFromValue($media_type, $media_storage, $source_field_name, $file);
}
/**
* Create a file field item.
*
* @param \Drupal\media\MediaTypeInterface $media_type
* The media type of the media item.
*
* @return \Drupal\file\Plugin\Field\FieldType\FileItem
* A created file item.
*/
protected function createFileItem(MediaTypeInterface $media_type) {
$field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type);
$data_definition = FieldItemDataDefinition::create($field_definition);
return new FileItem($data_definition);
}
/**
* {@inheritdoc}
*/
protected function prepareMediaEntityForSave(MediaInterface $media) {
/** @var \Drupal\file\FileInterface $file */
$file = $media->get($this->getSourceFieldName($media->bundle->entity))->entity;
$file->setPermanent();
$file->save();
}
/**
* Submit handler for the remove button.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function removeButtonSubmit(array $form, FormStateInterface $form_state) {
// Retrieve the delta of the media item from the parents of the remove
// button.
$triggering_element = $form_state->getTriggeringElement();
$delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
/** @var \Drupal\media\MediaInterface $removed_media */
$removed_media = $form_state->get(['media', $delta]);
$file = $removed_media->get($this->getSourceFieldName($removed_media->bundle->entity))->entity;
if ($file instanceof FileInterface && empty($this->fileUsage->listUsage($file))) {
$file->delete();
}
parent::removeButtonSubmit($form, $form_state);
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\media\OEmbed\ResourceException;
use Drupal\media\OEmbed\ResourceFetcherInterface;
use Drupal\media\OEmbed\UrlResolverInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\OpenerResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Creates a form to create media entities from oEmbed URLs.
*
* @internal
* Form classes are internal.
*/
class OEmbedForm extends AddFormBase {
/**
* The oEmbed URL resolver service.
*
* @var \Drupal\media\OEmbed\UrlResolverInterface
*/
protected $urlResolver;
/**
* The oEmbed resource fetcher service.
*
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
*/
protected $resourceFetcher;
/**
* Constructs a new OEmbedForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
* The media library UI builder.
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
* The oEmbed URL resolver service.
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
* The oEmbed resource fetcher service.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, UrlResolverInterface $url_resolver, ResourceFetcherInterface $resource_fetcher, ?OpenerResolverInterface $opener_resolver = NULL) {
parent::__construct($entity_type_manager, $library_ui_builder, $opener_resolver);
$this->urlResolver = $url_resolver;
$this->resourceFetcher = $resource_fetcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('media_library.ui_builder'),
$container->get('media.oembed.url_resolver'),
$container->get('media.oembed.resource_fetcher'),
$container->get('media_library.opener_resolver')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return $this->getBaseFormId() . '_oembed';
}
/**
* {@inheritdoc}
*/
protected function getMediaType(FormStateInterface $form_state) {
if ($this->mediaType) {
return $this->mediaType;
}
$media_type = parent::getMediaType($form_state);
if (!$media_type->getSource() instanceof OEmbedInterface) {
throw new \InvalidArgumentException('Can only add media types which use an oEmbed source plugin.');
}
return $media_type;
}
/**
* {@inheritdoc}
*/
protected function buildInputElement(array $form, FormStateInterface $form_state) {
$media_type = $this->getMediaType($form_state);
$providers = $media_type->getSource()->getProviders();
// Add a container to group the input elements for styling purposes.
$form['container'] = [
'#type' => 'container',
];
$form['container']['url'] = [
'#type' => 'url',
'#title' => $this->t('Add @type via URL', [
'@type' => $this->getMediaType($form_state)->label(),
]),
'#description' => $this->t('Allowed providers: @providers.', [
'@providers' => implode(', ', $providers),
]),
'#required' => TRUE,
'#attributes' => [
'placeholder' => 'https://',
],
];
$form['container']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Add'),
'#button_type' => 'primary',
'#validate' => ['::validateUrl'],
'#submit' => ['::addButtonSubmit'],
// @todo Move validation in https://www.drupal.org/node/2988215
'#ajax' => [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-wrapper',
// Add a fixed URL to post the form since AJAX forms are automatically
// posted to <current> instead of $form['#action'].
// @todo Remove when https://www.drupal.org/project/drupal/issues/2504115
// is fixed.
'url' => Url::fromRoute('media_library.ui'),
'options' => [
'query' => $this->getMediaLibraryState($form_state)->all() + [
FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
],
],
],
];
return $form;
}
/**
* Validates the oEmbed URL.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*/
public function validateUrl(array &$form, FormStateInterface $form_state) {
$url = $form_state->getValue('url');
if ($url) {
try {
$resource_url = $this->urlResolver->getResourceUrl($url);
$this->resourceFetcher->fetchResource($resource_url);
}
catch (ResourceException $e) {
$form_state->setErrorByName('url', $e->getMessage());
}
}
}
/**
* Submit handler for the add button.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function addButtonSubmit(array $form, FormStateInterface $form_state) {
$this->processInputValues([$form_state->getValue('url')], $form, $form_state);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a form for configuring the Media Library module.
*
* @internal
* Form classes are internal.
*/
class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getEditableConfigNames() {
return ['media_library.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'media_library_settings_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['advanced_ui'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable advanced UI'),
'#default_value' => $this->config('media_library.settings')->get('advanced_ui'),
'#description' => $this->t('If checked, users creating new media items in the media library will see a summary of their selected media items, and they will be able to insert their selection directly into the media field or text editor.'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('media_library.settings')
->set('advanced_ui', (bool) $form_state->getValue('advanced_ui'))
->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\editor\Ajax\EditorDialogSave;
/**
* The media library opener for text editors.
*
* @internal
* This is an internal part of Media Library's text editor integration.
*/
class MediaLibraryEditorOpener implements MediaLibraryOpenerInterface {
/**
* The text format entity storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $filterStorage;
/**
* The media storage.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $mediaStorage;
/**
* The MediaLibraryEditorOpener constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->filterStorage = $entity_type_manager->getStorage('filter_format');
$this->mediaStorage = $entity_type_manager->getStorage('media');
}
/**
* {@inheritdoc}
*/
public function checkAccess(MediaLibraryState $state, AccountInterface $account) {
$filter_format_id = $state->getOpenerParameters()['filter_format_id'];
$filter_format = $this->filterStorage->load($filter_format_id);
if (empty($filter_format)) {
return AccessResult::forbidden()
->addCacheTags(['filter_format_list'])
->setReason("The text format '$filter_format_id' could not be loaded.");
}
$filters = $filter_format->filters();
return $filter_format->access('use', $account, TRUE)
->andIf(AccessResult::allowedIf($filters->has('media_embed') && $filters->get('media_embed')->status === TRUE));
}
/**
* {@inheritdoc}
*/
public function getSelectionResponse(MediaLibraryState $state, array $selected_ids) {
$selected_media = $this->mediaStorage->load(reset($selected_ids));
$response = new AjaxResponse();
$values = [
'attributes' => [
'data-entity-type' => 'media',
'data-entity-uuid' => $selected_media->uuid(),
],
];
$response->addCommand(new EditorDialogSave($values));
return $response;
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Field\EntityReferenceFieldItemList;
/**
* The media library opener for field widgets.
*
* @internal
* This service is an internal part of Media Library's field widget.
*/
class MediaLibraryFieldWidgetOpener implements MediaLibraryOpenerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* MediaLibraryFieldWidgetOpener constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function checkAccess(MediaLibraryState $state, AccountInterface $account) {
$parameters = $state->getOpenerParameters() + ['entity_id' => NULL];
// Forbid access if any of the required parameters are missing.
foreach (['entity_type_id', 'bundle', 'field_name'] as $key) {
if (empty($parameters[$key])) {
return AccessResult::forbidden("$key parameter is missing.")->addCacheableDependency($state);
}
}
$entity_type_id = $parameters['entity_type_id'];
$bundle = $parameters['bundle'];
$field_name = $parameters['field_name'];
// Since we defer to a field to determine access, ensure we are dealing with
// a fieldable entity type.
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
throw new \LogicException("The media library can only be opened by fieldable entities.");
}
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$access_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
if (!empty($parameters['revision_id'])) {
$entity = $storage->loadRevision($parameters['revision_id']);
$entity_access = $access_handler->access($entity, 'update', $account, TRUE);
}
elseif ($parameters['entity_id']) {
$entity = $storage->load($parameters['entity_id']);
$entity_access = $access_handler->access($entity, 'update', $account, TRUE);
}
else {
$entity_access = $access_handler->createAccess($bundle, $account, [], TRUE);
}
// If entity-level access is denied, there's no point in continuing.
if (!$entity_access->isAllowed()) {
if ($entity_access instanceof RefinableCacheableDependencyInterface) {
$entity_access->addCacheableDependency($state);
}
return $entity_access;
}
// If the entity has not been loaded, create it in memory now.
if (!isset($entity)) {
$values = [];
if ($bundle_key = $entity_type->getKey('bundle')) {
$values[$bundle_key] = $bundle;
}
/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = $storage->create($values);
}
$items = $entity->get($field_name);
$field_definition = $items->getFieldDefinition();
// Check that the field is an entity reference, or subclass of it, since we
// need to check the target_type setting.
if (!$items instanceof EntityReferenceFieldItemList) {
throw new \LogicException('Expected the media library to be opened by an entity reference field.');
}
if ($field_definition->getFieldStorageDefinition()->getSetting('target_type') !== 'media') {
throw new \LogicException('Expected the media library to be opened by an entity reference field that target media items.');
}
$field_access = $access_handler->fieldAccess('edit', $field_definition, $account, $items, TRUE);
$access = $entity_access->andIf($field_access);
if ($access instanceof RefinableCacheableDependencyInterface) {
$access->addCacheableDependency($state);
}
return $access;
}
/**
* {@inheritdoc}
*/
public function getSelectionResponse(MediaLibraryState $state, array $selected_ids) {
$response = new AjaxResponse();
$parameters = $state->getOpenerParameters();
if (empty($parameters['field_widget_id'])) {
throw new \InvalidArgumentException('field_widget_id parameter is missing.');
}
// Create a comma-separated list of media IDs, insert them in the hidden
// field of the widget, and trigger the field update via the hidden submit
// button.
$widget_id = $parameters['field_widget_id'];
$ids = implode(',', $selected_ids);
$response
->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [$ids]))
->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']));
return $response;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an interface for media library openers.
*
* Media library opener services allow modules to check access to the media
* library selection dialog and respond to selections. Example use cases that
* require different handling:
* - when used in an entity reference field widget;
* - when used in a text editor.
*
* Openers that require additional parameters or metadata should retrieve them
* from the MediaLibraryState object.
*
* @see \Drupal\media_library\MediaLibraryState
* @see \Drupal\media_library\MediaLibraryState::getOpenerParameters()
*/
interface MediaLibraryOpenerInterface {
/**
* Checks media library access.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The media library.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*
* @see https://www.drupal.org/project/drupal/issues/3038254
*/
public function checkAccess(MediaLibraryState $state, AccountInterface $account);
/**
* Generates a response after selecting media items in the media library.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The state the media library was in at the time of selection, allowing the
* response to be customized based on that state.
* @param int[] $selected_ids
* The IDs of the selected media items.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The response to update the page after selecting media.
*/
public function getSelectionResponse(MediaLibraryState $state, array $selected_ids);
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Drupal\media_library;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
/**
* Service provider for media library services.
*/
class MediaLibraryServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$container->registerForAutoconfiguration(MediaLibraryOpenerInterface::class)
->addTag('media_library.opener');
}
}

View File

@@ -0,0 +1,298 @@
<?php
namespace Drupal\media_library;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* A value object for the media library state.
*
* When the media library is opened it needs several parameters to work
* properly. These parameters are normally extracted from the current URL, then
* retrieved from and managed by the MediaLibraryState value object. The
* following parameters are required in order to open the media library:
* - media_library_opener_id: The ID of a container service which implements
* \Drupal\media_library\MediaLibraryOpenerInterface and is responsible for
* interacting with the media library on behalf of the "thing" (e.g., a field
* widget or text editor button) which opened it.
* - media_library_allowed_types: The media types available in the library can
* be restricted to a list of allowed types. This should be an array of media
* type IDs.
* - media_library_selected_type: The media library contains tabs to navigate
* between the different media types. The selected type contains the ID of the
* media type whose tab that should be opened.
* - media_library_remaining: When the opener wants to limit the amount of media
* items that can be selected, it can pass the number of remaining slots. When
* the number of remaining slots is a negative number, an unlimited amount of
* items can be selected.
*
* This object can also carry an optional opener-specific array of arbitrary
* values, under the media_library_opener_parameters key. These values are
* included in the hash generated by ::getHash(), so the end user cannot tamper
* with them either.
*
* @see \Drupal\media_library\MediaLibraryOpenerInterface
*/
class MediaLibraryState extends ParameterBag implements CacheableDependencyInterface {
/**
* {@inheritdoc}
*/
public function __construct(array $parameters = []) {
$this->validateRequiredParameters($parameters['media_library_opener_id'], $parameters['media_library_allowed_types'], $parameters['media_library_selected_type'], $parameters['media_library_remaining']);
$parameters += [
'media_library_opener_parameters' => [],
];
parent::__construct($parameters);
$this->set('hash', $this->getHash());
}
/**
* Creates a new MediaLibraryState object.
*
* @param string $opener_id
* The opener ID.
* @param string[] $allowed_media_type_ids
* The allowed media type IDs.
* @param string $selected_type_id
* The selected media type ID.
* @param int $remaining_slots
* The number of remaining items the user is allowed to select or add in the
* library.
* @param array $opener_parameters
* (optional) Any additional opener-specific parameter values.
*
* @return static
* A state object.
*/
public static function create($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots, array $opener_parameters = []) {
$state = new static([
'media_library_opener_id' => $opener_id,
'media_library_allowed_types' => $allowed_media_type_ids,
'media_library_selected_type' => $selected_type_id,
'media_library_remaining' => $remaining_slots,
'media_library_opener_parameters' => $opener_parameters,
]);
return $state;
}
/**
* Get the media library state from a request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return static
* A state object.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the hash query parameter is invalid.
*/
public static function fromRequest(Request $request) {
$query = $request->query;
// Create a MediaLibraryState object through the create method to make sure
// all validation runs.
$state = static::create(
$query->get('media_library_opener_id'),
$query->all('media_library_allowed_types'),
$query->get('media_library_selected_type'),
$query->get('media_library_remaining'),
$query->all('media_library_opener_parameters')
);
// The request parameters need to contain a valid hash to prevent a
// malicious user modifying the query string to attempt to access
// inaccessible information.
if (!$state->isValidHash($query->get('hash'))) {
throw new BadRequestHttpException("Invalid media library parameters specified.");
}
// @todo Review parameters passed and remove irrelevant ones in
// https://www.drupal.org/i/3396650
// Once we have validated the required parameters, we restore the parameters
// from the request since there might be additional values.
$state->replace($query->all());
return $state;
}
/**
* Validates the required parameters for a new MediaLibraryState object.
*
* @param string $opener_id
* The media library opener service ID.
* @param string[] $allowed_media_type_ids
* The allowed media type IDs.
* @param string $selected_type_id
* The selected media type ID.
* @param int $remaining_slots
* The number of remaining items the user is allowed to select or add in the
* library.
*
* @throws \InvalidArgumentException
* If one of the passed arguments is missing or does not pass the
* validation.
*/
protected function validateRequiredParameters($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots) {
// The opener ID must be a non-empty string.
if (!is_string($opener_id) || empty(trim($opener_id))) {
throw new \InvalidArgumentException('The opener ID parameter is required and must be a string.');
}
// The allowed media type IDs must be an array of non-empty strings.
if (empty($allowed_media_type_ids) || !is_array($allowed_media_type_ids)) {
throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
}
foreach ($allowed_media_type_ids as $allowed_media_type_id) {
if (!is_string($allowed_media_type_id) || empty(trim($allowed_media_type_id))) {
throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
}
}
// The selected type ID must be a non-empty string.
if (!is_string($selected_type_id) || empty(trim($selected_type_id))) {
throw new \InvalidArgumentException('The selected type parameter is required and must be a string.');
}
// The selected type ID must be present in the list of allowed types.
if (!in_array($selected_type_id, $allowed_media_type_ids, TRUE)) {
throw new \InvalidArgumentException('The selected type parameter must be present in the list of allowed types.');
}
// The remaining slots must be numeric.
if (!is_numeric($remaining_slots)) {
throw new \InvalidArgumentException('The remaining slots parameter is required and must be numeric.');
}
}
/**
* Get the hash for the state object.
*
* @return string
* The hashed parameters.
*/
public function getHash() {
// Create a hash from the required state parameters and the serialized
// optional opener-specific parameters. Sort the allowed types and
// opener parameters so that differences in order do not result in
// different hashes.
$allowed_media_type_ids = array_values($this->getAllowedTypeIds());
sort($allowed_media_type_ids);
$opener_parameters = $this->getOpenerParameters();
ksort($opener_parameters);
$hash = implode(':', [
$this->getOpenerId(),
implode(':', $allowed_media_type_ids),
$this->getSelectedTypeId(),
$this->getAvailableSlots(),
serialize($opener_parameters),
]);
return Crypt::hmacBase64($hash, \Drupal::service('private_key')->get() . Settings::getHashSalt());
}
/**
* Validate a hash for the state object.
*
* @param string $hash
* The hash to validate.
*
* @return string
* The hashed parameters.
*/
public function isValidHash($hash) {
return hash_equals($this->getHash(), $hash);
}
/**
* Returns the ID of the media library opener service.
*
* @return string
* The media library opener service ID.
*/
public function getOpenerId() {
return $this->get('media_library_opener_id');
}
/**
* Returns the media type IDs which can be selected.
*
* @return string[]
* The media type IDs.
*/
public function getAllowedTypeIds() {
return $this->all('media_library_allowed_types');
}
/**
* Returns the selected media type.
*
* @return string
* The selected media type.
*/
public function getSelectedTypeId() {
return $this->get('media_library_selected_type');
}
/**
* Determines if additional media items can be selected.
*
* @return bool
* TRUE if additional items can be selected, otherwise FALSE.
*/
public function hasSlotsAvailable() {
return $this->getAvailableSlots() !== 0;
}
/**
* Returns the number of additional media items that can be selected.
*
* When the value is not available in the URL the default is 0. When a
* negative integer is passed, an unlimited amount of media items can be
* selected.
*
* @return int
* The number of additional media items that can be selected.
*/
public function getAvailableSlots() {
return $this->getInt('media_library_remaining');
}
/**
* Returns all opener-specific parameter values.
*
* @return array
* An associative array of all opener-specific parameter values.
*/
public function getOpenerParameters() {
return $this->all('media_library_opener_parameters');
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url.query_args'];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
}

View File

@@ -0,0 +1,349 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\views\ViewExecutableFactory;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Service which builds the media library.
*
* @internal
* This service is an internal part of the modal media library dialog and
* does not provide any extension points.
*/
class MediaLibraryUiBuilder {
use StringTranslationTrait;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The currently active request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The views executable factory.
*
* @var \Drupal\views\ViewExecutableFactory
*/
protected $viewsExecutableFactory;
/**
* The media library opener resolver.
*
* @var \Drupal\media_library\OpenerResolverInterface
*/
protected $openerResolver;
/**
* Constructs a MediaLibraryUiBuilder instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\views\ViewExecutableFactory $views_executable_factory
* The views executable factory.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The currently active request object.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory, FormBuilderInterface $form_builder, OpenerResolverInterface $opener_resolver) {
$this->entityTypeManager = $entity_type_manager;
$this->request = $request_stack->getCurrentRequest();
$this->viewsExecutableFactory = $views_executable_factory;
$this->formBuilder = $form_builder;
$this->openerResolver = $opener_resolver;
}
/**
* Get media library dialog options.
*
* @return array
* The media library dialog options.
*/
public static function dialogOptions() {
return [
'classes' => [
'ui-dialog' => 'media-library-widget-modal',
],
'title' => t('Add or select media'),
'height' => '75%',
'width' => '75%',
];
}
/**
* Build the media library UI.
*
* @param \Drupal\media_library\MediaLibraryState $state
* (optional) The current state of the media library, derived from the
* current request.
*
* @return array
* The render array for the media library.
*/
public function buildUi(?MediaLibraryState $state = NULL) {
if (!$state) {
$state = MediaLibraryState::fromRequest($this->request);
}
// When navigating to a media type through the vertical tabs, we only want
// to load the changed library content. This is not only more efficient, but
// also provides a more accessible user experience for screen readers.
if ($state->get('media_library_content') === '1') {
return $this->buildLibraryContent($state);
}
else {
return [
'#theme' => 'media_library_wrapper',
'#attributes' => [
'id' => 'media-library-wrapper',
],
'menu' => $this->buildMediaTypeMenu($state),
'content' => $this->buildLibraryContent($state),
// Attach the JavaScript for the media library UI. The number of
// available slots needs to be added to make sure users can't select
// more items than allowed.
'#attached' => [
'library' => ['media_library/ui'],
'drupalSettings' => [
'media_library' => [
'selection_remaining' => $state->getAvailableSlots(),
],
],
],
];
}
}
/**
* Build the media library content area.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media library.
*/
protected function buildLibraryContent(MediaLibraryState $state) {
return [
'#type' => 'container',
'#theme_wrappers' => [
'container__media_library_content',
],
'#attributes' => [
'id' => 'media-library-content',
],
'form' => $this->buildMediaTypeAddForm($state),
'view' => $this->buildMediaLibraryView($state),
];
}
/**
* Check access to the media library.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
* @param \Drupal\media_library\MediaLibraryState $state
* (optional) The current state of the media library, derived from the
* current request.
*
* @return \Drupal\Core\Access\AccessResult
* The access result.
*/
public function checkAccess(AccountInterface $account, ?MediaLibraryState $state = NULL) {
if (!$state) {
try {
$state = MediaLibraryState::fromRequest($this->request);
}
catch (BadRequestHttpException $e) {
return AccessResult::forbidden($e->getMessage());
}
catch (\InvalidArgumentException $e) {
return AccessResult::forbidden($e->getMessage());
}
}
// Deny access if the view or display are removed.
$view = $this->entityTypeManager->getStorage('view')->load('media_library');
if (!$view) {
return AccessResult::forbidden('The media library view does not exist.')
->setCacheMaxAge(0);
}
if (!$view->getDisplay('widget')) {
return AccessResult::forbidden('The media library widget display does not exist.')
->addCacheableDependency($view);
}
// The user must at least be able to view media in order to access the media
// library.
$can_view_media = AccessResult::allowedIfHasPermission($account, 'view media')
->addCacheableDependency($view);
// Delegate any further access checking to the opener service nominated by
// the media library state.
return $this->openerResolver->get($state)->checkAccess($state, $account)
->andIf($can_view_media);
}
/**
* Get the media type menu for the media library.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media type menu.
*/
protected function buildMediaTypeMenu(MediaLibraryState $state) {
// Add the menu for each type if we have more than 1 media type enabled for
// the field.
$allowed_type_ids = $state->getAllowedTypeIds();
if (count($allowed_type_ids) <= 1) {
return [];
}
// @todo Add a class to the li element.
// https://www.drupal.org/project/drupal/issues/3029227
$menu = [
'#theme' => 'links__media_library_menu',
'#links' => [],
'#attributes' => [
'class' => ['js-media-library-menu'],
],
];
$allowed_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($allowed_type_ids);
$selected_type_id = $state->getSelectedTypeId();
foreach ($allowed_types as $allowed_type_id => $allowed_type) {
$link_state = MediaLibraryState::create($state->getOpenerId(), $state->getAllowedTypeIds(), $allowed_type_id, $state->getAvailableSlots(), $state->getOpenerParameters());
// Add the 'media_library_content' parameter so the response will contain
// only the updated content for the tab.
// @see self::buildUi()
$link_state->set('media_library_content', 1);
$title = $allowed_type->label();
$display_title = [
'#markup' => $this->t('<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span>', ['@title' => $title]),
];
if ($allowed_type_id === $selected_type_id) {
$display_title = [
'#markup' => $this->t('<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span><span class="active-tab visually-hidden"> (selected)</span>', ['@title' => $title]),
];
}
$menu['#links']['media-library-menu-' . $allowed_type_id] = [
'title' => $display_title,
'url' => Url::fromRoute('media_library.ui', [], [
'query' => $link_state->all(),
]),
'attributes' => [
'role' => 'button',
'data-title' => $title,
],
];
}
// Set the active menu item.
$menu['#links']['media-library-menu-' . $selected_type_id]['attributes']['class'][] = 'active';
return $menu;
}
/**
* Get the add form for the selected media type.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media type add form.
*/
protected function buildMediaTypeAddForm(MediaLibraryState $state) {
$selected_type_id = $state->getSelectedTypeId();
$access_handler = $this->entityTypeManager->getAccessControlHandler('media');
$context = [
'media_library_state' => $state,
];
if (!$access_handler->createAccess($selected_type_id, NULL, $context)) {
return [];
}
$selected_type = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id);
$plugin_definition = $selected_type->getSource()->getPluginDefinition();
if (empty($plugin_definition['forms']['media_library_add'])) {
return [];
}
// After the form to add new media is submitted, we need to rebuild the
// media library with a new instance of the media add form. The form API
// allows us to do that by forcing empty user input.
// @see \Drupal\Core\Form\FormBuilder::doBuildForm()
$form_state = new FormState();
if ($state->get('_media_library_form_rebuild')) {
$form_state->setUserInput([]);
$state->remove('_media_library_form_rebuild');
}
$form_state->set('media_library_state', $state);
return $this->formBuilder->buildForm($plugin_definition['forms']['media_library_add'], $form_state);
}
/**
* Get the media library view.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media library view.
*/
protected function buildMediaLibraryView(MediaLibraryState $state) {
// @todo Make the view configurable in
// https://www.drupal.org/project/drupal/issues/2971209
$view = $this->entityTypeManager->getStorage('view')->load('media_library');
$view_executable = $this->viewsExecutableFactory->get($view);
$display_id = $state->get('views_display_id', 'widget');
// Make sure the state parameters are set in the request so the view can
// pass the parameters along in the pager, filters etc.
$view_request = $view_executable->getRequest();
$view_request->query->add($state->all());
$view_executable->setRequest($view_request);
$args = [$state->getSelectedTypeId()];
$view_executable->setDisplay($display_id);
$view_executable->preExecute($args);
$view_executable->execute($display_id);
return $view_executable->buildRenderable($display_id, $args, FALSE);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\media_library;
/**
* Defines a class to resolve media library openers.
*
* This is intended to be a very thin interface-verifying wrapper around
* services which implement \Drupal\media_library\MediaLibraryOpenerInterface.
* It is not an API and should not be extended or used by code that does not
* interact with the Media Library module.
*
* @internal
* This service is an internal part of the modal media library dialog and
* does not provide any extension points or public API.
*/
class OpenerResolver implements OpenerResolverInterface {
/**
* @var \Drupal\media_library\MediaLibraryOpenerInterface[]
*/
protected array $openers = [];
/**
* Registers an opener.
*
* @param \Drupal\media_library\MediaLibraryOpenerInterface $opener
* The opener.
* @param string $id
* The service ID.
*/
public function addOpener(MediaLibraryOpenerInterface $opener, string $id): void {
$this->openers[$id] = $opener;
}
/**
* {@inheritdoc}
*/
public function get(MediaLibraryState $state) {
$service_id = $state->getOpenerId();
$service = $this->openers[$service_id] ?? NULL;
if ($service instanceof MediaLibraryOpenerInterface) {
return $service;
}
throw new \RuntimeException("$service_id must be an instance of " . MediaLibraryOpenerInterface::class);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\media_library;
/**
* Defines an interface to get a media library opener from the container.
*
* This is intended to be a very thin interface-verifying wrapper around
* services which implement \Drupal\media_library\MediaLibraryOpenerInterface.
* It is not an API and should not be extended or used by code that does not
* interact with the Media Library module.
*
* @internal
* This interface is an internal part of the modal media library dialog and
* is only implemented by \Drupal\media_library\OpenerResolver. It is not a
* public API.
*/
interface OpenerResolverInterface {
/**
* Gets a media library opener service from the container.
*
* @param \Drupal\media_library\MediaLibraryState $state
* A value object representing the state of the media library.
*
* @return \Drupal\media_library\MediaLibraryOpenerInterface
* The media library opener service.
*
* @throws \RuntimeException
* If the requested opener service does not implement
* \Drupal\media_library\MediaLibraryOpenerInterface.
*/
public function get(MediaLibraryState $state);
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Drupal\media_library\Plugin\views\field;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\media_library\MediaLibraryState;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\Render\ViewsRenderPipelineMarkup;
use Drupal\views\ResultRow;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a field that outputs a checkbox and form for selecting media.
*
* @internal
* Plugin classes are internal.
*/
#[ViewsField("media_library_select_form")]
class MediaLibrarySelectForm extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getValue(ResultRow $row, $field = NULL) {
return '<!--form-item-' . $this->options['id'] . '--' . $row->mid . '-->';
}
/**
* Return the name of a form field.
*
* @see \Drupal\views\Form\ViewsFormMainForm
*
* @return string
* The form field name.
*/
public function form_element_name(): string {
return $this->field;
}
/**
* Return a media entity ID from a views result row.
*
* @see \Drupal\views\Form\ViewsFormMainForm
*
* @param int $row_id
* The index of a views result row.
*
* @return string
* The ID of a media entity.
*/
public function form_element_row_id(int $row_id): string {
return $this->view->result[$row_id]->mid;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
return ViewsRenderPipelineMarkup::create($this->getValue($values));
}
/**
* Form constructor for the media library select form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function viewsForm(array &$form, FormStateInterface $form_state) {
$form['#attributes']['class'] = ['js-media-library-views-form'];
// Add target for AJAX messages.
$form['media_library_messages'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'media-library-messages',
],
'#weight' => -10,
];
// Add an attribute that identifies the media type displayed in the form.
if (isset($this->view->args[0])) {
$form['#attributes']['data-drupal-media-type'] = $this->view->args[0];
}
// Render checkboxes for all rows.
$form[$this->options['id']]['#tree'] = TRUE;
foreach ($this->view->result as $row_index => $row) {
$entity = $this->getEntity($row);
if (!$entity) {
$form[$this->options['id']][$row_index] = [];
continue;
}
$form[$this->options['id']][$row->mid] = [
'#type' => 'checkbox',
'#title' => $this->t('Select @label', [
'@label' => $entity->label(),
]),
'#title_display' => 'invisible',
'#return_value' => $entity->id(),
];
}
// The selection is persistent across different pages in the media library
// and populated via JavaScript.
$selection_field_id = $this->options['id'] . '_selection';
$form[$selection_field_id] = [
'#type' => 'hidden',
'#attributes' => [
// This is used to identify the hidden field in the form via JavaScript.
'id' => 'media-library-modal-selection',
],
];
// @todo Remove in https://www.drupal.org/project/drupal/issues/2504115
// Currently the default URL for all AJAX form elements is the current URL,
// not the form action. This causes bugs when this form is rendered from an
// AJAX path like /views/ajax, which cannot process AJAX form submits.
$query = $this->view->getRequest()->query->all();
$query[FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
$query['views_display_id'] = $this->view->getDisplay()->display['id'];
$form['actions']['submit']['#ajax'] = [
'url' => Url::fromRoute('media_library.ui'),
'options' => [
'query' => $query,
],
'callback' => [static::class, 'updateWidget'],
];
$form['actions']['submit']['#value'] = $this->t('Insert selected');
$form['actions']['submit']['#button_type'] = 'primary';
$form['actions']['submit']['#field_id'] = $selection_field_id;
}
/**
* Submit handler for the media library select form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* A command to send the selection to the current field widget.
*/
public static function updateWidget(array &$form, FormStateInterface $form_state, Request $request) {
$field_id = $form_state->getTriggeringElement()['#field_id'];
$selected_ids = $form_state->getValue($field_id);
$selected_ids = $selected_ids ? array_filter(explode(',', $selected_ids)) : [];
// Allow the opener service to handle the selection.
$state = MediaLibraryState::fromRequest($request);
$current_selection = $form_state->getValue('media_library_select_form_selection');
$available_slots = $state->getAvailableSlots();
$selected_count = count(explode(',', $current_selection));
if ($available_slots > 0 && $selected_count > $available_slots) {
$response = new AjaxResponse();
$error = \Drupal::translation()->formatPlural($selected_count - $available_slots, 'There are currently @total items selected. The maximum number of items for the field is @max. Remove @count item from the selection.', 'There are currently @total items selected. The maximum number of items for the field is @max. Remove @count items from the selection.', [
'@total' => $selected_count,
'@max' => $available_slots,
]);
$response->addCommand(new MessageCommand($error, '#media-library-messages', ['type' => 'error']));
return $response;
}
return \Drupal::service('media_library.opener_resolver')
->get($state)
->getSelectionResponse($state, $selected_ids)
->addCommand(new CloseDialogCommand());
}
/**
* {@inheritdoc}
*/
public function viewsFormValidate(array &$form, FormStateInterface $form_state) {
$selected = array_filter($form_state->getValue($this->options['id']));
if (empty($selected)) {
$form_state->setErrorByName('', $this->t('No items selected.'));
}
}
/**
* {@inheritdoc}
*/
public function clickSortable() {
return FALSE;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\media_library\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for media library routes.
*
* @internal
* Tagged services are internal.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
// Add the media library UI access checks to the widget displays of the
// media library view.
if ($route = $collection->get('view.media_library.widget')) {
$route->addRequirements(['_custom_access' => 'media_library.ui_builder:checkAccess']);
}
if ($route = $collection->get('view.media_library.widget_table')) {
$route->addRequirements(['_custom_access' => 'media_library.ui_builder:checkAccess']);
}
}
}