first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
/**
* @file media_library.click_to_select.js
*/
(($, Drupal) => {
/**
* Allows users to select an element which checks a hidden checkbox.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior for selecting media library item.
*/
Drupal.behaviors.ClickToSelect = {
attach(context) {
$(
once(
'media-library-click-to-select',
'.js-click-to-select-trigger',
context,
),
).on('click', (event) => {
// Links inside the trigger should not be click-able.
event.preventDefault();
// Click the hidden checkbox when the trigger is clicked.
const $input = $(event.currentTarget)
.closest('.js-click-to-select')
.find('.js-click-to-select-checkbox input');
$input.prop('checked', !$input.prop('checked')).trigger('change');
});
$(
once(
'media-library-click-to-select',
'.js-click-to-select-checkbox input',
context,
),
)
.on('change', ({ currentTarget }) => {
$(currentTarget)
.closest('.js-click-to-select')
.toggleClass('checked', $(currentTarget).prop('checked'));
})
// Adds is-focus class to the click-to-select element.
.on('focus blur', ({ currentTarget, type }) => {
$(currentTarget)
.closest('.js-click-to-select')
.toggleClass('is-focus', type === 'focus');
});
// Adds hover class to the click-to-select element.
$(
once(
'media-library-click-to-select-hover',
'.js-click-to-select-trigger, .js-click-to-select-checkbox',
context,
),
).on('mouseover mouseout', ({ currentTarget, type }) => {
$(currentTarget)
.closest('.js-click-to-select')
.toggleClass('is-hover', type === 'mouseover');
});
},
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,428 @@
/**
* @file media_library.ui.js
*/
(($, Drupal, window, { tabbable }) => {
/**
* Wrapper object for the current state of the media library.
*/
Drupal.MediaLibrary = {
/**
* When a user interacts with the media library we want the selection to
* persist as long as the media library modal is opened. We temporarily
* store the selected items while the user filters the media library view or
* navigates to different tabs.
*/
currentSelection: [],
};
/**
* Command to update the current media library selection.
*
* @param {Drupal.Ajax} [ajax]
* The Drupal Ajax object.
* @param {object} response
* Object holding the server response.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function (
ajax,
response,
status,
) {
Object.values(response.mediaIds).forEach((value) => {
Drupal.MediaLibrary.currentSelection.push(value);
});
};
/**
* Load media library content through AJAX.
*
* Standard AJAX links (using the 'use-ajax' class) replace the entire library
* dialog. 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.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to vertical tabs in the media library.
*
* @todo Remove when the AJAX system adds support for replacing a specific
* selector via a link.
* https://www.drupal.org/project/drupal/issues/3026636
*/
Drupal.behaviors.MediaLibraryTabs = {
attach(context) {
const $menu = $('.js-media-library-menu');
$(once('media-library-menu-item', $menu.find('a')))
.on('keypress', (e) => {
// The AJAX link has the button role, so we need to make sure the link
// is also triggered when pressing the space bar.
if (e.which === 32) {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).trigger('click');
}
})
.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Replace the library content.
const ajaxObject = Drupal.ajax({
wrapper: 'media-library-content',
url: e.currentTarget.href,
dialogType: 'ajax',
progress: {
type: 'fullscreen',
message: Drupal.t('Processing...'),
},
});
// Override the AJAX success callback to shift focus to the media
// library content.
ajaxObject.success = function (response, status) {
return Promise.resolve(
Drupal.Ajax.prototype.success.call(ajaxObject, response, status),
).then(() => {
// Set focus to the first tabbable element in the media library
// content.
const mediaLibraryContent = document.getElementById(
'media-library-content',
);
if (mediaLibraryContent) {
const tabbableContent = tabbable(mediaLibraryContent);
if (tabbableContent.length) {
tabbableContent[0].focus();
}
}
});
};
ajaxObject.execute();
// Set the selected tab.
$menu.find('.active-tab').remove();
$menu.find('a').removeClass('active');
$(e.currentTarget)
.addClass('active')
.html(
Drupal.t(
'<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span><span class="active-tab visually-hidden"> (selected)</span>',
{ '@title': $(e.currentTarget).data('title') },
),
);
// Announce the updated content.
Drupal.announce(
Drupal.t('Showing @title media.', {
'@title': $(e.currentTarget).data('title'),
}),
);
});
},
};
/**
* Load media library displays through AJAX.
*
* Standard AJAX links (using the 'use-ajax' class) replace the entire library
* dialog. When navigating to a media library views display, we only want to
* load the changed views display content. This is not only more efficient,
* but also provides a more accessible user experience for screen readers.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to vertical tabs in the media library.
*
* @todo Remove when the AJAX system adds support for replacing a specific
* selector via a link.
* https://www.drupal.org/project/drupal/issues/3026636
*/
Drupal.behaviors.MediaLibraryViewsDisplay = {
attach(context) {
const $view = $(context).hasClass('.js-media-library-view')
? $(context)
: $('.js-media-library-view', context);
// Add a class to the view to allow it to be replaced via AJAX.
// @todo Remove the custom ID when the AJAX system allows replacing
// elements by selector.
// https://www.drupal.org/project/drupal/issues/2821793
$view
.closest('.views-element-container')
.attr('id', 'media-library-view');
// We would ideally use a generic JavaScript specific class to detect the
// display links. Since we have no good way of altering display links yet,
// this is the best we can do for now.
// @todo Add media library specific classes and data attributes to the
// media library display links when we can alter display links.
// https://www.drupal.org/project/drupal/issues/3036694
$(
once(
'media-library-views-display-link',
'.views-display-link-widget, .views-display-link-widget_table',
context,
),
).on('click', (e) => {
e.preventDefault();
e.stopPropagation();
const $link = $(e.currentTarget);
// Add a loading and display announcement for screen reader users.
let loadingAnnouncement = '';
let displayAnnouncement = '';
let focusSelector = '';
if ($link.hasClass('views-display-link-widget')) {
loadingAnnouncement = Drupal.t('Loading grid view.');
displayAnnouncement = Drupal.t('Changed to grid view.');
focusSelector = '.views-display-link-widget';
} else if ($link.hasClass('views-display-link-widget_table')) {
loadingAnnouncement = Drupal.t('Loading table view.');
displayAnnouncement = Drupal.t('Changed to table view.');
focusSelector = '.views-display-link-widget_table';
}
// Replace the library view.
const ajaxObject = Drupal.ajax({
wrapper: 'media-library-view',
url: e.currentTarget.href,
dialogType: 'ajax',
progress: {
type: 'fullscreen',
message: loadingAnnouncement || Drupal.t('Processing...'),
},
});
// Override the AJAX success callback to announce the updated content
// to screen readers.
if (displayAnnouncement || focusSelector) {
const success = ajaxObject.success;
ajaxObject.success = function (response, status) {
success.bind(this)(response, status);
// The AJAX link replaces the whole view, including the clicked
// link. Move the focus back to the clicked link when the view is
// replaced.
if (focusSelector) {
$(focusSelector).focus();
}
// Announce the new view is loaded to screen readers.
if (displayAnnouncement) {
Drupal.announce(displayAnnouncement);
}
};
}
ajaxObject.execute();
// Announce the new view is being loaded to screen readers.
// @todo Replace custom announcement when
// https://www.drupal.org/project/drupal/issues/2973140 is in.
if (loadingAnnouncement) {
Drupal.announce(loadingAnnouncement);
}
});
},
};
/**
* Update the media library selection when loaded or media items are selected.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to select media items.
*/
Drupal.behaviors.MediaLibraryItemSelection = {
attach(context, settings) {
const $form = $(
'.js-media-library-views-form, .js-media-library-add-form',
context,
);
const currentSelection = Drupal.MediaLibrary.currentSelection;
if (!$form.length) {
return;
}
const $mediaItems = $(
'.js-media-library-item input[type="checkbox"]',
$form,
);
/**
* Disable media items.
*
* @param {jQuery} $items
* A jQuery object representing the media items that should be disabled.
*/
function disableItems($items) {
$items
.prop('disabled', true)
.closest('.js-media-library-item')
.addClass('media-library-item--disabled');
}
/**
* Enable media items.
*
* @param {jQuery} $items
* A jQuery object representing the media items that should be enabled.
*/
function enableItems($items) {
$items
.prop('disabled', false)
.closest('.js-media-library-item')
.removeClass('media-library-item--disabled');
}
/**
* Update the number of selected items in the button pane.
*
* @param {number} remaining
* The number of remaining slots.
*/
function updateSelectionCount(remaining) {
// When the remaining number of items is a negative number, we allow an
// unlimited number of items. In that case we don't want to show the
// number of remaining slots.
const selectItemsText =
remaining < 0
? Drupal.formatPlural(
currentSelection.length,
'1 item selected',
'@count items selected',
)
: Drupal.formatPlural(
remaining,
'@selected of @count item selected',
'@selected of @count items selected',
{
'@selected': currentSelection.length,
},
);
// The selected count div could have been created outside of the
// context, so we unfortunately can't use context here.
$('.js-media-library-selected-count').html(selectItemsText);
}
function checkEnabled() {
updateSelectionCount(settings.media_library.selection_remaining);
if (
currentSelection.length === settings.media_library.selection_remaining
) {
disableItems($mediaItems.not(':checked'));
enableItems($mediaItems.filter(':checked'));
} else {
enableItems($mediaItems);
}
}
// Update the selection array and the hidden form field when a media item
// is selected.
$(once('media-item-change', $mediaItems)).on('change', (e) => {
const id = e.currentTarget.value;
// Update the selection.
const position = currentSelection.indexOf(id);
if (e.currentTarget.checked) {
// Check if the ID is not already in the selection and add if needed.
if (position === -1) {
currentSelection.push(id);
}
} else if (position !== -1) {
// Remove the ID when it is in the current selection.
currentSelection.splice(position, 1);
}
const mediaLibraryModalSelection = document.querySelector(
'#media-library-modal-selection',
);
if (mediaLibraryModalSelection) {
// Set the selection in the hidden form element.
mediaLibraryModalSelection.value = currentSelection.join();
$(mediaLibraryModalSelection).trigger('change');
}
// Set the selection in the media library add form. Since the form is
// not necessarily loaded within the same context, we can't use the
// context here.
document
.querySelectorAll('.js-media-library-add-form-current-selection')
.forEach((item) => {
item.value = currentSelection.join();
});
});
checkEnabled();
// The hidden selection form field changes when the selection is updated.
$(
once(
'media-library-selection-change',
$form.find('#media-library-modal-selection'),
),
).on('change', (e) => {
checkEnabled();
});
// Apply the current selection to the media library view. Changing the
// checkbox values triggers the change event for the media items. The
// change event handles updating the hidden selection field for the form.
currentSelection.forEach((value) => {
$form
.find(`input[type="checkbox"][value="${value}"]`)
.prop('checked', true)
.trigger('change');
});
// Add the selection count to the button pane when a media library dialog
// is created.
if (!once('media-library-selection-info', 'html').length) {
return;
}
window.addEventListener('dialog:aftercreate', () => {
// Since the dialog HTML is not part of the context, we can't use
// context here.
const $buttonPane = $(
'.media-library-widget-modal .ui-dialog-buttonpane',
);
if (!$buttonPane.length) {
return;
}
$buttonPane.append(Drupal.theme('mediaLibrarySelectionCount'));
updateSelectionCount(settings.media_library.selection_remaining);
});
},
};
/**
* Clear the current selection.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to clear the selection when the library modal closes.
*/
Drupal.behaviors.MediaLibraryModalClearSelection = {
attach() {
if (!once('media-library-clear-selection', 'html').length) {
return;
}
window.addEventListener('dialog:afterclose', () => {
Drupal.MediaLibrary.currentSelection = [];
});
},
};
/**
* Theme function for the selection count.
*
* @return {string}
* The corresponding HTML.
*/
Drupal.theme.mediaLibrarySelectionCount = function () {
return `<div class="media-library-selected-count js-media-library-selected-count" role="status" aria-live="polite" aria-atomic="true"></div>`;
};
})(jQuery, Drupal, window, window.tabbable);

View File

@@ -0,0 +1,49 @@
/**
* @file media_library.view.js
*/
(($, Drupal) => {
/**
* Adds checkbox to select all items in the library.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to select all media items.
*/
Drupal.behaviors.MediaLibrarySelectAll = {
attach(context) {
const $view = $(
once(
'media-library-select-all',
'.js-media-library-view[data-view-display-id="page"]',
context,
),
);
if ($view.length && $view.find('.js-media-library-item').length) {
const $checkbox = $(Drupal.theme('checkbox')).on(
'click',
({ currentTarget }) => {
// Toggle all checkboxes.
const $checkboxes = $(currentTarget)
.closest('.js-media-library-view')
.find('.js-media-library-item input[type="checkbox"]');
$checkboxes
.prop('checked', $(currentTarget).prop('checked'))
.trigger('change');
// Announce the selection.
const announcement = $(currentTarget).prop('checked')
? Drupal.t('All @count items selected', {
'@count': $checkboxes.length,
})
: Drupal.t('Zero items selected');
Drupal.announce(announcement);
},
);
const $label = $('<label class="media-library-select-all"></label>');
$label[0].textContent = Drupal.t('Select all media');
$label.prepend($checkbox);
$view.find('.js-media-library-item').first().before($label);
}
},
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,106 @@
/**
* @file media_library.widget.js
*/
(($, Drupal, Sortable) => {
/**
* Allows users to re-order their selection with drag+drop.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to re-order selected media items.
*/
Drupal.behaviors.MediaLibraryWidgetSortable = {
attach(context) {
// Allow media items to be re-sorted with drag+drop in the widget.
const selection = context.querySelectorAll('.js-media-library-selection');
selection.forEach((widget) => {
Sortable.create(widget, {
draggable: '.js-media-library-item',
handle: '.js-media-library-item-preview',
onEnd: () => {
$(widget)
.children()
.each((index, child) => {
$(child).find('.js-media-library-item-weight')[0].value = index;
});
},
});
});
},
};
/**
* Allows selection order to be set without drag+drop for accessibility.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to toggle the weight field for media items.
*/
Drupal.behaviors.MediaLibraryWidgetToggleWeight = {
attach(context) {
const strings = {
show: Drupal.t('Show media item weights'),
hide: Drupal.t('Hide media item weights'),
};
const mediaLibraryToggle = once(
'media-library-toggle',
'.js-media-library-widget-toggle-weight',
context,
);
$(mediaLibraryToggle).on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
e.currentTarget.textContent = $target.hasClass('active')
? strings.show
: strings.hide;
$target
.toggleClass('active')
.closest('.js-media-library-widget')
.find('.js-media-library-item-weight')
.parent()
.toggle();
});
mediaLibraryToggle.forEach((item) => {
item.textContent = strings.show;
});
$(once('media-library-toggle', '.js-media-library-item-weight', context))
.parent()
.hide();
},
};
/**
* Disable the open button when the user is not allowed to add more items.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to disable the media library open button.
*/
Drupal.behaviors.MediaLibraryWidgetDisableButton = {
attach(context) {
// When the user returns from the modal to the widget, we want to shift
// the focus back to the open button. If the user is not allowed to add
// more items, the button needs to be disabled. Since we can't shift the
// focus to disabled elements, the focus is set back to the open button
// via JavaScript by adding the 'data-disabled-focus' attribute.
once(
'media-library-disable',
'.js-media-library-open-button[data-disabled-focus="true"]',
context,
).forEach((button) => {
$(button).focus();
// There is a small delay between the focus set by the browser and the
// focus of screen readers. We need to give screen readers time to shift
// the focus as well before the button is disabled.
setTimeout(() => {
$(button).attr('disabled', 'disabled');
}, 50);
});
},
};
})(jQuery, Drupal, Sortable);

View File

@@ -0,0 +1,92 @@
<?php
/**
* @file
* Documentation related to Media Library.
*/
/**
* @defgroup media_library_architecture Media Library Architecture
* @{
*
* Media Library is a UI for the core Media module. It provides a visual
* interface for users to manage media in their site, and it allows authors to
* visually select media for use in entity reference and text fields, using a
* modal dialog.
*
* In order to provide a consistent user experience, Media Library is
* intentionally opinionated, with few extension points and no hooks. Most of
* its code is internal and should not be extended or instantiated by external
* code.
*
* @section openers Openers
* Interaction with the modal media library dialog is mediated by "opener"
* services. All openers must implement
* \Drupal\media_library\MediaLibraryOpenerInterface.
*
* Openers are responsible for determining access to the media library, and for
* generating an AJAX response when the user has finished selecting media items
* in the library. An opener is a "bridge" between the opinionated media library
* modal dialog and whatever is consuming it, allowing the dialog to be
* triggered in a way that makes sense for that particular consumer. Examples in
* Drupal core include entity reference fields and text editors.
*
* @see \Drupal\media_library\MediaLibraryOpenerInterface
* @see \Drupal\media_library\MediaLibraryEditorOpener
* @see \Drupal\media_library\MediaLibraryFieldWidgetOpener
*
* @section state Modal dialog state
* When the media library modal is used, its configuration and state (such as
* how many items are currently selected, the maximum number that can be
* selected, which media types the user is allowed to see, and so forth) are
* stored in an instance of \Drupal\media_library\MediaLibraryState. The state
* object also stores the service ID of the opener being used, as well as any
* additional parameters or data that are specific to that opener.
*
* The media library state is passed between the user and the server in the
* URL's query parameters. Therefore, the state is also protected by a hash in
* order to prevent tampering.
*
* @see \Drupal\media_library\MediaLibraryState
*
* @section add_form Adding media in the dialog
* Users with appropriate permissions can add media to the library from directly
* within the modal dialog.
*
* This interaction is implemented using forms, and is customizable by modules.
* Since the media library is segmented by media type, each media type can
* expose a different form for adding media of that type; the type's source
* plugin specifies the actual form class to use. Here is an example of a media
* source plugin definition which provides an add form for the media library:
*
* @code
* #[MediaSource(
* id: "file",
* label: new TranslatableMarkup("File"),
* description: new TranslatableMarkup("Use local files for reusable media."),
* allowed_field_types: ["file"],
* forms = [
* "media_library_add" => "\Drupal\media_library\Form\FileUploadForm",
* ]
* )]
* @endcode
*
* This can also be done in hook_media_source_info_alter(). For example:
*
* @code
* function example_media_source_info_alter(array &$sources) {
* $sources['file']['forms']['media_library_add'] = "\Drupal\media_library\Form\FileUploadForm";
* }
* @endcode
*
* The add form is a standard form class, and can be altered by modules and
* themes just like any other form. For easier implementation, it is recommended
* that modules extend \Drupal\media_library\Form\AddFormBase when providing add
* forms.
*
* @see \Drupal\media_library\Form\AddFormBase
* @see \Drupal\media_library\Form\FileUploadForm
* @see \Drupal\media_library\Form\OEmbedForm
*
* @}
*/

View File

@@ -0,0 +1,15 @@
name: 'Media Library'
type: module
description: 'Enhances the media list with additional features to more easily find and use existing media items.'
package: Core
# version: VERSION
configure: media_library.settings
dependencies:
- drupal:media
- drupal:views
- drupal:user
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* Install, update and uninstall functions for the media_library module.
*/
use Drupal\media\Entity\MediaType;
/**
* Implements hook_install().
*/
function media_library_install($is_syncing) {
if (!$is_syncing) {
foreach (MediaType::loadMultiple() as $type) {
_media_library_configure_form_display($type);
_media_library_configure_view_display($type);
}
}
}
/**
* Implements hook_update_last_removed().
*/
function media_library_update_last_removed() {
return 8704;
}

View File

@@ -0,0 +1,37 @@
click_to_select:
version: VERSION
js:
js/media_library.click_to_select.js: {}
dependencies:
- core/drupal
- core/once
view:
version: VERSION
js:
js/media_library.view.js: {}
dependencies:
- core/drupal.announce
- core/drupal.checkbox
- media_library/click_to_select
widget:
version: VERSION
js:
js/media_library.widget.js: {}
dependencies:
- core/drupal.dialog.ajax
- core/once
- core/sortable
ui:
version: VERSION
js:
js/media_library.ui.js: {}
dependencies:
- core/drupal.ajax
- core/drupal.announce
- core/once
- core/jquery
- media_library/view
- core/tabbable

View File

@@ -0,0 +1,5 @@
media_library.add:
route_name: entity.media.add_page
title: 'Add media'
appears_on:
- view.media_library.page

View File

@@ -0,0 +1,5 @@
media_library.settings:
title: 'Media Library settings'
parent: system.admin_config_media
description: 'Manage Media Library settings.'
route_name: media_library.settings

View File

@@ -0,0 +1,10 @@
media_library.table:
title: 'Table'
parent_id: entity.media.collection
route_name: entity.media.collection
weight: 10
media_library.grid:
title: 'Grid'
parent_id: entity.media.collection
route_name: view.media_library.page
weight: 20

View File

@@ -0,0 +1,478 @@
<?php
/**
* @file
* Contains hook implementations for the media_library module.
*/
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\image\Entity\ImageStyle;
use Drupal\image\Plugin\Field\FieldType\ImageItem;
use Drupal\media\MediaTypeForm;
use Drupal\media\MediaTypeInterface;
use Drupal\media_library\Form\FileUploadForm;
use Drupal\media_library\Form\OEmbedForm;
use Drupal\media_library\MediaLibraryState;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\ViewExecutable;
/**
* Implements hook_help().
*/
function media_library_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.media_library':
$output = '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Media Library module provides a rich, visual interface for managing media, and allows media to be reused in entity reference fields or embedded into text content. It overrides the <a href=":media-collection">media administration page</a>, allowing users to toggle between the existing table-style interface and a new grid-style interface for browsing and performing administrative operations on media.', [
':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
]) . '</p>';
$output .= '<p>' . t('To learn more about media management, begin by reviewing the <a href=":media-help">documentation for the Media module</a>. For more information about the media library and related functionality, see the <a href=":media-library-handbook">online documentation for the Media Library module</a>.', [
':media-help' => Url::fromRoute('help.page', ['name' => 'media'])->toString(),
':media-library-handbook' => 'https://www.drupal.org/docs/8/core/modules/media-library-module',
]) . '</p>';
$output .= '<h2>' . t('Selection dialog') . '</h2>';
$output .= '<p>' . t('When selecting media for an entity reference field or a text editor, Media Library opens a modal dialog to help users easily find and select media. The modal dialog can toggle between a grid-style and table-style interface, and new media items can be uploaded directly into it.') . '</p>';
$output .= '<p>' . t('Within the dialog, media items are divided up by type. If more than one media type can be selected by the user, the available types will be displayed as a set of vertical tabs. To users who have appropriate permissions, each media type may also present a short form allowing you to upload or create new media items of that type.') . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Grid-style vs. table-style interface') . '</dt>';
$output .= '<dd>' . t('The Media Library module provides a new grid-style interface for the media administration page that displays media as thumbnails, with minimal textual information, allowing users to visually browse media in their site. The existing table-style interface is better suited to displaying additional information about media items, in addition to being more accessible to users with assistive technology.') . '</dd>';
$output .= '<dt>' . t('Reusing media in entity reference fields') . '</dt>';
$output .= '<dd>' . t('Any entity reference field that references media can use the media library. To enable, configure the form display for the field to use the "Media library" widget.') . '</dd>';
$output .= '<dt>' . t('Embedding media in text content') . '</dt>';
$output .= '<dd>' . t('To use the media library within CKEditor, you must add the "Insert from Media Library" button to the CKEditor toolbar, and enable the "Embed media" filter in the text format associated with the text editor.') . '</dd>';
$output .= '</dl>';
$output .= '<h2>' . t('Customize') . '</h2>';
$output .= '<ul>';
$output .= '<li>';
if (\Drupal::moduleHandler()->moduleExists('views_ui') && \Drupal::currentUser()->hasPermission('administer views')) {
$output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the <a href=":views-ui">Views UI</a>, including sorting and filtering. This is the case for both the administration page and the modal dialog.', [
':views_ui' => Url::fromRoute('entity.view.collection')->toString(),
]);
}
else {
$output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the Views UI, including sorting and filtering. This is the case for both the administration page and the modal dialog.');
}
$output .= '</li>';
$output .= '<li>' . t('In the grid-style interface, the fields that are displayed (including which image style is used for images) can be customized by configuring the "Media library" view mode for each of your <a href=":media-types">media types</a>. The thumbnail images in the grid-style interface can be customized by configuring the "Media Library thumbnail (220×220)" image style.', [
':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
]) . '</li>';
$output .= '<li>' . t('When adding new media items within the modal dialog, the fields that are displayed can be customized by configuring the "Media library" form mode for each of your <a href=":media-types">media types</a>.', [
':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
]) . '</li>';
$output .= '</ul>';
return $output;
}
}
/**
* Implements hook_media_source_info_alter().
*/
function media_library_media_source_info_alter(array &$sources) {
if (empty($sources['audio_file']['forms']['media_library_add'])) {
$sources['audio_file']['forms']['media_library_add'] = FileUploadForm::class;
}
if (empty($sources['file']['forms']['media_library_add'])) {
$sources['file']['forms']['media_library_add'] = FileUploadForm::class;
}
if (empty($sources['image']['forms']['media_library_add'])) {
$sources['image']['forms']['media_library_add'] = FileUploadForm::class;
}
if (empty($sources['video_file']['forms']['media_library_add'])) {
$sources['video_file']['forms']['media_library_add'] = FileUploadForm::class;
}
if (empty($sources['oembed:video']['forms']['media_library_add'])) {
$sources['oembed:video']['forms']['media_library_add'] = OEmbedForm::class;
}
}
/**
* Implements hook_theme().
*/
function media_library_theme() {
return [
'media__media_library' => [
'base hook' => 'media',
],
'media_library_wrapper' => [
'render element' => 'element',
],
'media_library_item' => [
'render element' => 'element',
],
];
}
/**
* Prepares variables for the media library modal dialog.
*
* Default template: media-library-wrapper.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #menu, #content.
*/
function template_preprocess_media_library_wrapper(array &$variables) {
$variables['menu'] = &$variables['element']['menu'];
$variables['content'] = &$variables['element']['content'];
}
/**
* Prepares variables for a selected media item.
*
* Default template: media-library-item.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the element.
*/
function template_preprocess_media_library_item(array &$variables) {
$element = &$variables['element'];
foreach (Element::children($element) as $key) {
$variables['content'][$key] = $element[$key];
}
}
/**
* Implements hook_views_pre_render().
*/
function media_library_views_pre_render(ViewExecutable $view) {
$add_classes = function (&$option, array $classes_to_add) {
$classes = $option ? preg_split('/\s+/', trim($option)) : [];
$classes = array_filter($classes);
$classes = array_merge($classes, $classes_to_add);
$option = implode(' ', array_unique($classes));
};
if ($view->id() === 'media_library') {
if ($view->current_display === 'page') {
$add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
if (array_key_exists('media_bulk_form', $view->field)) {
$add_classes($view->field['media_bulk_form']->options['element_class'], ['js-click-to-select-checkbox']);
}
}
elseif (str_starts_with($view->current_display, 'widget')) {
if (array_key_exists('media_library_select_form', $view->field)) {
$add_classes($view->field['media_library_select_form']->options['element_wrapper_class'], ['js-click-to-select-checkbox']);
}
$add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
}
$add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
if ($view->display_handler->options['defaults']['css_class']) {
$add_classes($view->displayHandlers->get('default')->options['css_class'], ['js-media-library-view']);
}
else {
$add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
}
}
}
/**
* Implements hook_views_post_render().
*/
function media_library_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
if ($view->id() === 'media_library') {
$output['#attached']['library'][] = 'media_library/view';
if (str_starts_with($view->current_display, 'widget')) {
try {
$query = MediaLibraryState::fromRequest($view->getRequest())->all();
}
catch (InvalidArgumentException $e) {
// MediaLibraryState::fromRequest() will throw an exception if the view
// is being previewed, since not all required query parameters will be
// present. In a preview, however, this can be omitted since we're
// merely previewing.
// @todo Use the views API for checking for the preview mode when it
// lands. https://www.drupal.org/project/drupal/issues/3060855
if (empty($view->preview) && empty($view->live_preview)) {
throw $e;
}
}
// If the current query contains any parameters we use to contextually
// filter the view, ensure they persist across AJAX rebuilds.
// The ajax_path is shared for all AJAX views on the page, but our query
// parameters are prefixed and should not interfere with any other views.
// @todo Rework or remove this in https://www.drupal.org/node/2983451
if (!empty($query)) {
$ajax_path = &$output['#attached']['drupalSettings']['views']['ajax_path'];
$parsed_url = UrlHelper::parse($ajax_path);
$query = array_merge($query, $parsed_url['query']);
// Reset the pager so that the user starts on the first page.
unset($query['page']);
$ajax_path = $parsed_url['path'] . '?' . UrlHelper::buildQuery($query);
}
}
}
}
/**
* Implements hook_preprocess_media().
*/
function media_library_preprocess_media(&$variables) {
if ($variables['view_mode'] === 'media_library') {
/** @var \Drupal\media\MediaInterface $media */
$media = $variables['media'];
$variables['#cache']['contexts'][] = 'user.permissions';
$rel = $media->access('edit') ? 'edit-form' : 'canonical';
$variables['url'] = $media->toUrl($rel, [
'language' => $media->language(),
]);
$variables += [
'preview_attributes' => new Attribute(),
'metadata_attributes' => new Attribute(),
];
$variables['status'] = $media->isPublished();
}
}
/**
* Implements hook_preprocess_views_view() for the 'media_library' view.
*/
function media_library_preprocess_views_view__media_library(array &$variables) {
$variables['attributes']['data-view-display-id'] = $variables['view']->current_display;
}
/**
* Implements hook_preprocess_views_view_fields().
*/
function media_library_preprocess_views_view_fields(&$variables) {
// Add classes to media rendered entity field so it can be targeted for
// JavaScript mouseover and click events.
if ($variables['view']->id() === 'media_library' && isset($variables['fields']['rendered_entity'])) {
if (isset($variables['fields']['rendered_entity']->wrapper_attributes)) {
$variables['fields']['rendered_entity']->wrapper_attributes->addClass('js-click-to-select-trigger');
}
}
}
/**
* Alter the bulk form to add a more accessible label.
*
* @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.
*
* @todo Remove in https://www.drupal.org/node/2983454
*/
function media_library_form_views_form_media_library_page_alter(array &$form, FormStateInterface $form_state) {
if (isset($form['media_bulk_form']) && isset($form['output'])) {
/** @var \Drupal\views\ViewExecutable $view */
$view = $form['output'][0]['#view'];
foreach (Element::getVisibleChildren($form['media_bulk_form']) as $key) {
if (isset($view->result[$key])) {
$media = $view->field['media_bulk_form']->getEntity($view->result[$key]);
$form['media_bulk_form'][$key]['#title'] = $media ? t('Select @label', ['@label' => $media->label()]) : '';
}
}
}
}
/**
* Implements hook_form_alter().
*/
function media_library_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
// Add a process callback to ensure that the media library view's exposed
// filters submit button is not moved to the modal dialog's button area.
if ($form_id === 'views_exposed_form' && str_starts_with($form['#id'], 'views-exposed-form-media-library-widget')) {
$form['#after_build'][] = '_media_library_views_form_media_library_after_build';
}
// Configures media_library displays when a type is submitted.
if ($form_state->getFormObject() instanceof MediaTypeForm) {
$form['actions']['submit']['#submit'][] = '_media_library_media_type_form_submit';
// @see field_ui_form_alter()
if (isset($form['actions']['save_continue'])) {
$form['actions']['save_continue']['#submit'][] = '_media_library_media_type_form_submit';
}
}
}
/**
* Form #after_build callback for media_library view's exposed filters form.
*/
function _media_library_views_form_media_library_after_build(array $form, FormStateInterface $form_state) {
// Remove .form-actions from the view's exposed filter actions. This prevents
// the "Apply filters" submit button from being moved into the dialog's
// button area.
// @see \Drupal\Core\Render\Element\Actions::processActions
// @see Drupal.behaviors.dialog.prepareDialogButtons
// @todo Remove this after
// https://www.drupal.org/project/drupal/issues/3089751 is fixed.
if (($key = array_search('form-actions', $form['actions']['#attributes']['class'])) !== FALSE) {
unset($form['actions']['#attributes']['class'][$key]);
}
return $form;
}
/**
* Submit callback for media type form.
*/
function _media_library_media_type_form_submit(array &$form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject();
if ($form_object->getOperation() === 'add') {
$type = $form_object->getEntity();
$form_display_created = _media_library_configure_form_display($type);
$view_display_created = _media_library_configure_view_display($type);
if ($form_display_created || $view_display_created) {
\Drupal::messenger()->addStatus(t('Media Library form and view displays have been created for the %type media type.', [
'%type' => $type->label(),
]));
}
}
}
/**
* Implements hook_field_ui_preconfigured_options_alter().
*/
function media_library_field_ui_preconfigured_options_alter(array &$options, $field_type) {
// If the field is not an "entity_reference"-based field, bail out.
$class = \Drupal::service('plugin.manager.field.field_type')->getPluginClass($field_type);
if (!is_a($class, EntityReferenceItem::class, TRUE)) {
return;
}
// Set the default field widget for media to be the Media library.
if (!empty($options['media'])) {
$options['media']['entity_form_display']['type'] = 'media_library_widget';
}
}
/**
* Implements hook_local_tasks_alter().
*
* Removes tasks for the Media library if the view display no longer exists.
*/
function media_library_local_tasks_alter(&$local_tasks) {
/** @var \Symfony\Component\Routing\RouteCollection $route_collection */
$route_collection = \Drupal::service('router')->getRouteCollection();
foreach (['media_library.grid', 'media_library.table'] as $key) {
if (isset($local_tasks[$key]) && !$route_collection->get($local_tasks[$key]['route_name'])) {
unset($local_tasks[$key]);
}
}
}
/**
* Implements hook_ENTITY_TYPE_access().
*/
function media_library_image_style_access(EntityInterface $entity, $operation, AccountInterface $account) {
// Prevent the fallback 'media_library' image style from being deleted.
// @todo Lock the image style instead of preventing delete access.
// https://www.drupal.org/project/drupal/issues/2247293
if ($operation === 'delete' && $entity->id() === 'media_library') {
return AccessResult::forbidden();
}
}
/**
* Ensures that the given media type has a media_library form display.
*
* @param \Drupal\media\MediaTypeInterface $type
* The media type to configure.
*
* @return bool
* Whether a form display has been created or not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function _media_library_configure_form_display(MediaTypeInterface $type) {
$display = EntityFormDisplay::load('media.' . $type->id() . '.media_library');
if ($display) {
return FALSE;
}
$values = [
'targetEntityType' => 'media',
'bundle' => $type->id(),
'mode' => 'media_library',
'status' => TRUE,
];
$display = EntityFormDisplay::create($values);
// Remove all default components.
foreach (array_keys($display->getComponents()) as $name) {
$display->removeComponent($name);
}
// Expose the name field when it is not mapped.
if (!in_array('name', $type->getFieldMap(), TRUE)) {
$display->setComponent('name', [
'type' => 'string_textfield',
'settings' => [
'size' => 60,
],
]);
}
// If the source field is an image field, expose it so that users can set alt
// and title text.
$source_field = $type->getSource()->getSourceFieldDefinition($type);
if ($source_field->isDisplayConfigurable('form') && is_a($source_field->getItemDefinition()->getClass(), ImageItem::class, TRUE)) {
$type->getSource()->prepareFormDisplay($type, $display);
}
return (bool) $display->save();
}
/**
* Ensures that the given media type has a media_library view display.
*
* @param \Drupal\media\MediaTypeInterface $type
* The media type to configure.
*
* @return bool
* Whether a view display has been created or not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function _media_library_configure_view_display(MediaTypeInterface $type) {
$display = EntityViewDisplay::load('media.' . $type->id() . '.media_library');
if ($display) {
return FALSE;
}
$values = [
'targetEntityType' => 'media',
'bundle' => $type->id(),
'mode' => 'media_library',
'status' => TRUE,
];
$display = EntityViewDisplay::create($values);
// Remove all default components.
foreach (array_keys($display->getComponents()) as $name) {
$display->removeComponent($name);
}
// @todo Remove dependency on 'medium' and 'thumbnail' image styles from
// media and media library modules.
// https://www.drupal.org/project/drupal/issues/3030437
$image_style = ImageStyle::load('medium');
// Expose the thumbnail component. If the medium image style doesn't exist,
// use the fallback 'media_library' image style.
$display->setComponent('thumbnail', [
'type' => 'image',
'label' => 'hidden',
'settings' => [
'image_style' => $image_style ? $image_style->id() : 'media_library',
'image_link' => '',
],
]);
return (bool) $display->save();
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* @file
* Post update functions for Media Library.
*/
/**
* Implements hook_removed_post_updates().
*/
function media_library_removed_post_updates() {
return [
'media_library_post_update_display_modes' => '9.0.0',
'media_library_post_update_table_display' => '9.0.0',
'media_library_post_update_add_media_library_image_style' => '9.0.0',
'media_library_post_update_add_status_extra_filter' => '9.0.0',
'media_library_post_update_add_buttons_to_page_view' => '9.0.0',
'media_library_post_update_update_8001_checkbox_classes' => '9.0.0',
'media_library_post_update_default_administrative_list_to_table_display' => '9.0.0',
'media_library_post_update_add_langcode_filters' => '9.0.0',
];
}

View File

@@ -0,0 +1,14 @@
media_library.ui:
path: '/media-library'
defaults:
_controller: 'media_library.ui_builder:buildUi'
requirements:
_custom_access: 'media_library.ui_builder:checkAccess'
media_library.settings:
path: '/admin/config/media/media-library'
defaults:
_form: '\Drupal\media_library\Form\SettingsForm'
_title: 'Media Library settings'
requirements:
_permission: 'administer media'

View File

@@ -0,0 +1,20 @@
services:
_defaults:
autoconfigure: true
media_library.ui_builder:
class: Drupal\media_library\MediaLibraryUiBuilder
arguments: ['@entity_type.manager', '@request_stack', '@views.executable', '@form_builder', '@media_library.opener_resolver']
Drupal\media_library\MediaLibraryUiBuilder: '@media_library.ui_builder'
media_library.route_subscriber:
class: Drupal\media_library\Routing\RouteSubscriber
media_library.opener_resolver:
class: Drupal\media_library\OpenerResolver
tags:
- { name: service_collector, tag: media_library.opener, call: addOpener }
Drupal\media_library\OpenerResolverInterface: '@media_library.opener_resolver'
media_library.opener.field_widget:
class: Drupal\media_library\MediaLibraryFieldWidgetOpener
arguments: ['@entity_type.manager']
media_library.opener.editor:
class: Drupal\media_library\MediaLibraryEditorOpener
arguments: ['@entity_type.manager']

View File

@@ -0,0 +1,22 @@
<?php
/**
* @file
* Contains Views integration for the media_library module.
*/
/**
* Implements hook_views_data().
*/
function media_library_views_data() {
$data = [];
$data['media']['media_library_select_form'] = [
'title' => t('Select media'),
'help' => t('Provides a field for selecting media entities in our media library view'),
'real field' => 'mid',
'field' => [
'id' => 'media_library_select_form',
],
];
return $data;
}

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 $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);
}

File diff suppressed because it is too large Load Diff

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']);
}
}
}

View File

@@ -0,0 +1,49 @@
{#
/**
* @file
* Default theme implementation to present a media entity in the media library.
*
* Available variables:
* - media: The entity with limited access to object properties and methods.
* Only method names starting with "get", "has", or "is" and a few common
* methods such as "id", "label", and "bundle" are available. For example:
* - entity.getEntityTypeId() will return the entity type ID.
* - entity.hasField('field_example') returns TRUE if the entity includes
* field_example. (This does not indicate the presence of a value in this
* field.)
* Calling other methods, such as entity.delete(), will result in an exception.
* See \Drupal\Core\Entity\EntityInterface for a full list of methods.
* - name: Name of the media.
* - content: Media content.
* - title_prefix: Additional output populated by modules, intended to be
* displayed in front of the main title tag that appears in the template.
* - title_suffix: Additional output populated by modules, intended to be
* displayed after the main title tag that appears in the template.
* - view_mode: View mode; for example, "teaser" or "full".
* - attributes: HTML attributes for the containing element.
* - title_attributes: Same as attributes, except applied to the main title
* tag that appears in the template.
* - url: Direct URL of the media.
* - preview_attributes: HTML attributes for the preview wrapper.
* - metadata_attributes: HTML attributes for the expandable metadata area.
* - status: Whether or not the Media is published.
*
* @see template_preprocess_media()
* @see media_library_preprocess_media()
*
* @ingroup themeable
*/
#}
<article{{ attributes }}>
{% if content %}
<div{{ preview_attributes.addClass('js-media-library-item-preview') }}>
{{ content|without('name') }}
</div>
{% if not status %}
{{ "unpublished"|t }}
{% endif %}
<div{{ metadata_attributes }}>
{{ name }}
</div>
{% endif %}
</article>

View File

@@ -0,0 +1,22 @@
{#
/**
* @file
* Default theme implementation of a media library item.
*
* This is used when displaying selected media items, either in the field
* widget or in the "Additional selected media" area when adding new
* media items in the media library modal dialog.
*
* Available variables:
* - attributes: HTML attributes for the containing element.
* - content: The content of the media library item, plus any additional
* fields or elements surrounding it.
*
* @see template_preprocess_media_library_item()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ content }}
</div>

View File

@@ -0,0 +1,21 @@
{#
/**
* @file
* Default theme implementation of a container used to wrap the media library's
* modal dialog interface.
*
* Available variables:
* - attributes: HTML attributes for the containing element.
* - menu: The menu of available media types to choose from.
* - content: The form to add new media items, followed by the grid or table of
* existing media items to choose from.
*
* @see template_preprocess_media_library_wrapper()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ menu }}
{{ content }}
</div>

View File

@@ -0,0 +1,15 @@
name: 'Media Library Form Overwrite test'
type: module
description: 'Test module for Media Library.'
package: Testing
dependencies:
- drupal:image
- drupal:media_library
- drupal:menu_ui
- drupal:node
- drupal:path
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains.
*/
use Drupal\media_library_form_overwrite_test\Form\TestAddForm;
function media_library_form_overwrite_test_media_source_info_alter(array &$sources) {
$sources['image']['forms']['media_library_add'] = TestAddForm::class;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Drupal\media_library_form_overwrite_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\media_library\Form\AddFormBase;
/**
* Test add form.
*/
class TestAddForm extends AddFormBase {
/**
* {@inheritdoc}
*/
protected function buildInputElement(array $form, FormStateInterface $form_state) {
return [];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'test_add_form';
}
}

View File

@@ -0,0 +1,16 @@
name: 'Media Library test'
type: module
description: 'Test module for Media Library.'
package: Testing
dependencies:
- drupal:image
- drupal:media_library
- drupal:media_test_source
- drupal:menu_ui
- drupal:node
- drupal:path
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains hook implementations for the media_library_test module.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\media_library_test\Form\TestNodeFormOverride;
/**
* Implements hook_ENTITY_TYPE_create_access().
*/
function media_library_test_media_create_access(AccountInterface $account, array $context, $entity_bundle) {
if (isset($context['media_library_state'])) {
/** @var \Drupal\media_library\MediaLibraryState $state */
$state = $context['media_library_state'];
return AccessResult::forbiddenIf($state->getSelectedTypeId() === 'deny_access');
}
return AccessResult::neutral();
}
/**
* Implements hook_entity_field_access().
*/
function media_library_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
$deny_fields = \Drupal::state()->get('media_library_test_entity_field_access_deny_fields', []);
// Always deny the field_media_no_access field.
$deny_fields[] = 'field_media_no_access';
return AccessResult::forbiddenIf(in_array($field_definition->getName(), $deny_fields, TRUE), 'Field access denied by test module');
}
/**
* Implements hook_entity_type_alter().
*/
function media_library_test_entity_type_alter(array &$entity_types) {
if (isset($entity_types['node'])) {
$entity_types['node']->setFormClass('default', TestNodeFormOverride::class);
$entity_types['node']->setFormClass('edit', TestNodeFormOverride::class);
}
}
/**
* Implements hook_field_widget_info_alter().
*/
function media_library_test_field_widget_info_alter(array &$info) {
$info['media_library_widget']['field_types'][] = 'entity_reference_subclass';
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Drupal\media_library_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\NodeForm;
/**
* Override NodeForm to test media library form submission semantics.
*/
class TestNodeFormOverride extends NodeForm {
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
if (in_array('open_button', $triggering_element['#parents'], TRUE)) {
throw new \Exception('The media library widget open_button element should not trigger form submit.');
}
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\media_library_test\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'entity_reference_subclass' field type.
*/
#[FieldType(
id: "entity_reference_subclass",
label: new TranslatableMarkup("Entity reference subclass"),
description: new TranslatableMarkup("An entity field containing an entity reference."),
category: "reference",
default_widget: "entity_reference_autocomplete",
default_formatter: "entity_reference_label",
list_class: EntityReferenceFieldItemList::class,
)]
class EntityReferenceItemSubclass extends EntityReferenceItem {
}

View File

@@ -0,0 +1,13 @@
name: 'Media Library test widget'
type: module
description: 'Test widget that has a nested media library widget'
package: Testing
dependencies:
- drupal:image
- drupal:media_library
- drupal:media_test_source
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\media_library_test_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget;
/**
* Plugin implementation of the 'media_library_inception_widget' widget.
*
* This widget is used to simulate the media library widget nested inside
* another widget that performs validation of required fields before there is
* an opportunity to add media.
*/
#[FieldWidget(
id: 'media_library_inception_widget',
label: new TranslatableMarkup('Media library inception widget'),
description: new TranslatableMarkup('Puts a widget in a widget for testing purposes.'),
field_types: ['entity_reference'],
multiple_values: TRUE,
)]
class MediaLibraryInceptionWidget extends MediaLibraryWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
if (empty($element['#element_validate'])) {
$element['#element_validate'] = [];
}
$element['#element_validate'][] = [$this, 'elementValidate'];
return parent::formElement($items, $delta, $element, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function elementValidate($element, FormStateInterface $form_state, $form) {
$field_name = $element['#field_name'];
$entity = $form_state->getFormObject()->getEntity();
$input = $form_state->getUserInput();
if (!empty($input['_triggering_element_name']) && str_contains($input['_triggering_element_name'], 'media-library-update')) {
// This will validate a required field before an upload is completed.
$display = EntityFormDisplay::collectRenderDisplay($entity, 'edit');
$display->extractFormValues($entity, $form, $form_state);
$display->validateFormValues($entity, $form, $form_state);
}
$form_value = $form_state->getValue($field_name);
if (!empty($form_value['media_library_selection'])) {
$entity->set($field_name, $form_value['media_library_selection']);
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for media_library.
*
* @group media_library
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,282 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\image\Entity\ImageStyle;
use Drupal\media\Plugin\media\Source\File;
use Drupal\media\Plugin\media\Source\Image;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\media\Entity\MediaType;
/**
* Tests that the Media Library automatically configures form/view modes.
*
* @group media_library
*/
class MediaLibraryDisplayModeTest extends BrowserTestBase {
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_ui',
'media',
'system',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'access media overview',
'administer media',
'administer media fields',
'administer media form display',
'administer media display',
'administer media types',
'view media',
]));
}
/**
* Tests that the Media Library can automatically configure display modes.
*/
public function testDisplayModes(): void {
$this->createMediaType('file', [
'id' => 'type_one',
]);
$this->createMediaType('file', [
'id' => 'type_two',
'field_map' => ['name' => File::METADATA_ATTRIBUTE_NAME],
]);
$this->createMediaType('image', [
'id' => 'type_three',
]);
$this->createMediaType('image', [
'id' => 'type_four',
'field_map' => ['name' => Image::METADATA_ATTRIBUTE_NAME],
]);
// Display modes are not automatically created when creating a media type
// programmatically, only when installing the module or when creating a
// media type via the UI.
$this->assertNull(EntityFormDisplay::load('media.type_one.media_library'));
$this->assertNull(EntityViewDisplay::load('media.type_one.media_library'));
$this->assertNull(EntityFormDisplay::load('media.type_two.media_library'));
$this->assertNull(EntityViewDisplay::load('media.type_two.media_library'));
$this->assertNull(EntityFormDisplay::load('media.type_three.media_library'));
$this->assertNull(EntityViewDisplay::load('media.type_three.media_library'));
$this->assertNull(EntityFormDisplay::load('media.type_four.media_library'));
$this->assertNull(EntityViewDisplay::load('media.type_four.media_library'));
// Display modes are created on install.
$this->container->get('module_installer')->install(['media_library']);
// The container was rebuilt during module installation, so ensure we have
// an up-to-date reference to it.
$this->container = $this->kernel->getContainer();
// For a non-image media type without a mapped name field, the media_library
// form mode should only contain the name field.
$this->assertFormDisplay('type_one', TRUE, FALSE);
$this->assertViewDisplay('type_one', 'medium');
// For a non-image media type with a mapped name field, the media_library
// form mode should not contain any fields.
$this->assertFormDisplay('type_two', FALSE, FALSE);
$this->assertViewDisplay('type_two', 'medium');
// For an image media type without a mapped name field, the media_library
// form mode should contain the name field and the source field.
$this->assertFormDisplay('type_three', TRUE, TRUE);
$this->assertViewDisplay('type_three', 'medium');
// For an image media type with a mapped name field, the media_library form
// mode should only contain the source field.
$this->assertFormDisplay('type_four', FALSE, TRUE);
$this->assertViewDisplay('type_four', 'medium');
// Create a non-image media type without a mapped name field in the UI.
$type_five_id = 'type_five';
$edit = [
'label' => $type_five_id,
'id' => $type_five_id,
'source' => 'file',
];
$this->drupalGet('admin/structure/media/add');
$this->submitForm($edit, 'Save and manage fields');
$this->submitForm([], 'Save and manage fields');
$this->assertSession()->pageTextContains("Media Library form and view displays have been created for the $type_five_id media type.");
$this->assertFormDisplay($type_five_id, TRUE, FALSE);
$this->assertViewDisplay($type_five_id, 'medium');
// Create a non-image media type with a mapped name field in the UI.
$type_six_id = 'type_six';
$edit = [
'label' => $type_six_id,
'id' => $type_six_id,
'source' => 'file',
];
$this->drupalGet('admin/structure/media/add');
$this->submitForm($edit, 'Save');
$edit = [
'field_map[name]' => File::METADATA_ATTRIBUTE_NAME,
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Media Library form and view displays have been created for the $type_six_id media type.");
$this->assertFormDisplay($type_six_id, FALSE, FALSE);
$this->assertViewDisplay($type_six_id, 'medium');
// Create an image media type without a mapped name field in the UI.
$type_seven_id = 'type_seven';
$edit = [
'label' => $type_seven_id,
'id' => $type_seven_id,
'source' => 'image',
];
$this->drupalGet('admin/structure/media/add');
$this->submitForm($edit, 'Save');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains("Media Library form and view displays have been created for the $type_seven_id media type.");
$this->assertFormDisplay($type_seven_id, TRUE, TRUE);
$this->assertViewDisplay($type_seven_id, 'medium');
// Create an image media type with a mapped name field in the UI.
$type_eight_id = 'type_eight';
$edit = [
'label' => $type_eight_id,
'id' => $type_eight_id,
'source' => 'image',
];
$this->drupalGet('admin/structure/media/add');
$this->submitForm($edit, 'Save');
$edit = [
'field_map[name]' => Image::METADATA_ATTRIBUTE_NAME,
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Media Library form and view displays have been created for the $type_eight_id media type.");
$this->assertFormDisplay($type_eight_id, FALSE, TRUE);
$this->assertViewDisplay($type_eight_id, 'medium');
// Create an oEmbed media type with a mapped name field in the UI.
$type_id = 'pinto_bean';
$edit = [
'label' => $type_id,
'id' => $type_id,
'source' => 'oembed:video',
];
$this->drupalGet('admin/structure/media/add');
$this->submitForm($edit, 'Save');
$edit = [
'field_map[title]' => 'name',
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Media Library form and view displays have been created for the $type_id media type.");
$this->assertFormDisplay($type_id, FALSE, FALSE);
$this->assertViewDisplay($type_id, 'medium');
// Now that all our media types have been created, ensure the bundle info
// cache is up-to-date.
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
// Delete a form and view display.
EntityFormDisplay::load('media.type_one.media_library')->delete();
EntityViewDisplay::load('media.type_one.media_library')->delete();
// Make sure the form and view display are not created when saving existing
// media types.
$this->drupalGet('admin/structure/media/manage/type_one');
$this->submitForm([], 'Save');
$this->assertNull(EntityFormDisplay::load('media.type_one.media_library'));
$this->assertNull(EntityViewDisplay::load('media.type_one.media_library'));
// Delete the medium image style.
ImageStyle::load('medium')->delete();
// Create an image media type, assert the displays are created and the
// fallback 'media_library' image style is used.
$type_nine_id = 'type_nine';
$edit = [
'label' => $type_nine_id,
'id' => $type_nine_id,
'source' => 'image',
];
$this->drupalGet('admin/structure/media/add');
$this->submitForm($edit, 'Save');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains("Media Library form and view displays have been created for the $type_nine_id media type.");
$this->assertFormDisplay($type_nine_id, TRUE, TRUE);
$this->assertViewDisplay($type_nine_id, 'media_library');
}
/**
* Asserts the media library form display components for a media type.
*
* @param string $type_id
* The media type ID.
* @param bool $has_name
* Whether the media library form display should contain the name field or
* not.
* @param bool $has_source_field
* Whether the media library form display should contain the source field or
* not.
*
* @internal
*/
protected function assertFormDisplay(string $type_id, bool $has_name, bool $has_source_field): void {
// These components are added by default and invisible.
$components = [
'revision_log_message',
'langcode',
];
// Only assert the name and source field if needed.
if ($has_name) {
$components[] = 'name';
}
if ($has_source_field) {
$type = MediaType::load($type_id);
$components[] = $type->getSource()->getSourceFieldDefinition($type)->getName();
}
$form_display = EntityFormDisplay::load('media.' . $type_id . '.media_library');
$this->assertInstanceOf(EntityFormDisplay::class, $form_display);
$actual_components = array_keys($form_display->getComponents());
sort($components);
sort($actual_components);
$this->assertSame($components, $actual_components);
}
/**
* Asserts the media library view display components for a media type.
*
* @param string $type_id
* The media type ID.
* @param string $image_style
* The ID of the image style that should be configured for the thumbnail.
*
* @internal
*/
protected function assertViewDisplay(string $type_id, string $image_style): void {
$view_display = EntityViewDisplay::load('media.' . $type_id . '.media_library');
$this->assertInstanceOf(EntityViewDisplay::class, $view_display);
// Assert the media library view display contains only the thumbnail.
$this->assertSame(['thumbnail'], array_keys($view_display->getComponents()));
// Assert the thumbnail image style.
$thumbnail = $view_display->getComponent('thumbnail');
$this->assertIsArray($thumbnail);
$this->assertSame($image_style, $thumbnail['settings']['image_style']);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Functional;
use Drupal\image\Entity\ImageStyle;
use Drupal\Tests\BrowserTestBase;
/**
* Tests access to the Media library image style.
*
* @group media_library
*/
class MediaLibraryImageStyleAccessTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['media_library'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that users can't delete the 'media_library' image style.
*/
public function testMediaLibraryImageStyleAccess(): void {
// Create a user who can manage the image styles.
$user = $this->createUser([
'access administration pages',
'administer image styles',
]);
// The user should be able to delete the 'medium' image style, but not the
// 'media_library' image style.
$medium = ImageStyle::load('medium');
$this->assertTrue($medium->access('delete', $user));
$mediaLibrary = ImageStyle::load('media_library');
$this->assertFalse($mediaLibrary->access('delete', $user));
$this->drupalLogin($user);
$this->drupalGet($medium->toUrl('delete-form'));
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet($mediaLibrary->toUrl('delete-form'));
$this->assertSession()->statusCodeEquals(403);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the Media Library settings form.
*
* @coversDefaultClass \Drupal\media_library\Form\SettingsForm
* @group media_library
*/
class SettingsFormTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['media_library'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the Media Library settings form.
*/
public function testSettingsForm(): void {
$account = $this->drupalCreateUser([
'access administration pages',
'administer media',
]);
$this->drupalLogin($account);
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('/admin/config');
$page->clickLink('Media Library settings');
$page->checkField('Enable advanced UI');
$page->pressButton('Save configuration');
$assert_session->checkboxChecked('Enable advanced UI');
$page->uncheckField('Enable advanced UI');
$page->pressButton('Save configuration');
$assert_session->checkboxNotChecked('Enable advanced UI');
}
}

View File

@@ -0,0 +1,349 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\file\Entity\File;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\media\Entity\Media;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
// cspell:ignore hoglet
/**
* Tests media library integration with content moderation.
*
* @group media_library
*/
class ContentModerationTest extends WebDriverTestBase {
use ContentModerationTestTrait;
use EntityReferenceFieldCreationTrait;
use MediaTypeCreationTrait;
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_moderation',
'field',
'media',
'media_library',
'node',
'views',
];
/**
* {@inheritdoc}
*
* @todo Remove and fix test to not rely on super user.
* @see https://www.drupal.org/project/drupal/issues/3437620
*/
protected bool $usesSuperUserAccessPolicy = TRUE;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with the 'administer media' permission.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $userAdmin;
/**
* User with the 'view media' permission.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $userViewer;
/**
* User with the 'view media' and 'view own unpublished media' permissions.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $userViewOwnUnpublished;
/**
* User with the 'view media' and 'view any unpublished content' permissions.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $userViewAnyUnpublished;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create an image media type and article node type.
$this->createMediaType('image', ['id' => 'image']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Create a media reference field on articles.
$this->createEntityReferenceField(
'node',
'article',
'field_media',
'Media',
'media',
'default',
['target_bundles' => ['image']],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Add the media field to the form display.
$form_display = \Drupal::service('entity_display.repository')->getFormDisplay('node', 'article', 'default');
$form_display->setComponent('field_media', [
'type' => 'media_library_widget',
])->save();
// Configure the "Editorial" workflow to apply to image media.
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('media', 'image');
$workflow->save();
$image = File::create([
'uri' => $this->getTestFiles('image')[0]->uri,
]);
$image->setPermanent();
$image->save();
// Create a draft, published and archived media item.
$draft_media = Media::create([
'name' => 'Hoglet',
'bundle' => 'image',
'field_media_image' => $image,
'moderation_state' => 'draft',
]);
$draft_media->save();
$published_media = Media::create([
'name' => 'Panda',
'bundle' => 'image',
'field_media_image' => $image,
'moderation_state' => 'published',
]);
$published_media->save();
$archived_media = Media::create([
'name' => 'Mammoth',
'bundle' => 'image',
'field_media_image' => $image,
'moderation_state' => 'archived',
]);
$archived_media->save();
// Create some users for our tests. We want to check with user 1, a media
// administrator with 'administer media' permissions, a user that has the
// 'view media' permissions, a user that can 'view media' and 'view own
// unpublished media', and a user that has 'view media' and 'view any
// unpublished content' permissions.
$this->userAdmin = $this->drupalCreateUser([
'access administration pages',
'access content',
'access media overview',
'edit own article content',
'create article content',
'administer media',
]);
$this->userViewer = $this->drupalCreateUser([
'access administration pages',
'access content',
'access media overview',
'edit own article content',
'create article content',
'view media',
'create media',
]);
$this->userViewOwnUnpublished = $this->drupalCreateUser([
'access administration pages',
'access content',
'access media overview',
'edit own article content',
'create article content',
'view media',
'view own unpublished media',
'create media',
]);
$this->userViewAnyUnpublished = $this->drupalCreateUser([
'access administration pages',
'access content',
'access media overview',
'edit own article content',
'create article content',
'view media',
'create media',
'view any unpublished content',
]);
}
/**
* Tests the media library widget only shows published media.
*/
public function testAdministrationPage(): void {
// User 1 should be able to see all media items.
$this->drupalLogin($this->rootUser);
$this->drupalGet('admin/content/media');
$this->assertAllMedia();
// The media admin user should be able to see all media items.
$this->drupalLogin($this->userAdmin);
$this->drupalGet('admin/content/media');
$this->assertAllMedia();
// The media viewer user should be able to see only published media items.
$this->drupalLogin($this->userViewer);
$this->drupalGet('admin/content/media');
$this->assertOnlyPublishedMedia();
// The media viewer user that can also view its own unpublished media should
// also be able to see only published media items since it is not the owner
// of the created media items.
$this->drupalLogin($this->userViewOwnUnpublished);
$this->drupalGet('admin/content/media');
$this->assertOnlyPublishedMedia();
// When content moderation is enabled, a media viewer that can view any
// unpublished content should be able to see all media.
// @see content_moderation_entity_access()
$this->drupalLogin($this->userViewAnyUnpublished);
$this->drupalGet('admin/content/media');
$this->assertAllMedia();
// Assign all media to the user with the 'view own unpublished media'
// permission.
foreach (Media::loadMultiple() as $media) {
$media->setOwner($this->userViewOwnUnpublished);
$media->save();
}
// User 1 should still be able to see all media items.
$this->drupalLogin($this->rootUser);
$this->drupalGet('admin/content/media');
$this->assertAllMedia();
// The media admin user should still be able to see all media items.
$this->drupalLogin($this->userAdmin);
$this->drupalGet('admin/content/media');
$this->assertAllMedia();
// The media viewer user should still be able to see only published media
// items.
$this->drupalLogin($this->userViewer);
$this->drupalGet('admin/content/media');
$this->assertOnlyPublishedMedia();
// The media viewer user that can also view its own unpublished media
// should now be able to see all media items since it is the owner of the
// created media items.
$this->drupalLogin($this->userViewOwnUnpublished);
$this->drupalGet('admin/content/media');
$this->assertAllMedia();
// The media viewer that can view any unpublished content should still be
// able to see all media.
$this->drupalLogin($this->userViewAnyUnpublished);
$this->drupalGet('admin/content/media');
$this->assertAllMedia();
}
/**
* Tests the media library widget only shows published media.
*/
public function testWidget(): void {
$assert_session = $this->assertSession();
// All users should only be able to see published media items.
$this->drupalLogin($this->rootUser);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userAdmin);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userViewer);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userViewOwnUnpublished);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userViewAnyUnpublished);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
// After we change the owner to the user with 'view own unpublished media'
// permission, all users should still only be able to see published media.
foreach (Media::loadMultiple() as $media) {
$media->setOwner($this->userViewOwnUnpublished);
$media->save();
}
$this->drupalLogin($this->rootUser);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userAdmin);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userViewer);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userViewOwnUnpublished);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
$this->drupalLogin($this->userViewAnyUnpublished);
$this->drupalGet('node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$this->assertOnlyPublishedMedia();
}
/**
* Asserts all media items are visible.
*
* @internal
*/
protected function assertAllMedia(): void {
$assert_session = $this->assertSession();
$assert_session->pageTextContains('Hoglet');
$assert_session->pageTextContains('Panda');
$assert_session->pageTextContains('Mammoth');
}
/**
* Asserts only published media items are visible.
*
* @internal
*/
protected function assertOnlyPublishedMedia(): void {
$assert_session = $this->assertSession();
$assert_session->pageTextNotContains('Hoglet');
$assert_session->pageTextContains('Panda');
$assert_session->pageTextNotContains('Mammoth');
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\media\Entity\Media;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests media widget nested inside another widget.
*
* @group media_library
*/
class EmbeddedFormWidgetTest extends WebDriverTestBase {
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'media_library',
'media_library_test',
'media_library_test_widget',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$display_repository = $this->container->get('entity_display.repository');
FieldStorageConfig::create([
'field_name' => 'media_image_field',
'entity_type' => 'node',
'type' => 'entity_reference',
'settings' => [
'target_type' => 'media',
'required' => TRUE,
],
])->save();
FieldConfig::create([
'label' => 'A Media Image Field',
'field_name' => 'media_image_field',
'entity_type' => 'node',
'bundle' => 'basic_page',
'field_type' => 'entity_reference',
'required' => TRUE,
'settings' => [
'handler_settings' => [
'target_bundles' => [
'type_three' => 'type_three',
],
],
],
])->save();
$display_repository->getFormDisplay('node', 'basic_page')
->setComponent('media_image_field', [
'type' => 'media_library_widget',
'region' => 'content',
'settings' => [
'media_types' => ['type_three'],
],
])
->save();
$this->config('media_library.settings')
->set('advanced_ui', TRUE)
->save();
$user = $this->drupalCreateUser([
'access content',
'access media overview',
'edit own basic_page content',
'create basic_page content',
'create media',
'view media',
]);
$this->drupalLogin($user);
}
/**
* Tests media inside another widget that validates too enthusiastically.
*
* @dataProvider insertionReselectionProvider
*/
public function testInsertionAndReselection($widget): void {
$this->container
->get('entity_display.repository')
->getFormDisplay('node', 'basic_page')
->setComponent('media_image_field', [
'type' => $widget,
'region' => 'content',
'settings' => [
'media_types' => ['type_three'],
],
])
->save();
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
foreach ($this->getTestFiles('image') as $image) {
$extension = pathinfo($image->filename, PATHINFO_EXTENSION);
if ($extension === 'jpg') {
$jpg_image = $image;
break;
}
}
$this->drupalGet('node/add/basic_page');
$wrapper = $assert_session->elementExists('css', '#media_image_field-media-library-wrapper');
$wrapper->pressButton('Add media');
$this->assertNotNull($assert_session->waitForText('Add or select media'));
$page->attachFileToField('Add file', $this->container->get('file_system')->realpath($jpg_image->uri));
$this->assertNotNull($assert_session->waitForText('Alternative text'));
$page->fillField('Alternative text', $this->randomString());
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save and insert');
$first_item_locator = "(//div[@data-drupal-selector='edit-media-image-field-selection-0'])[1]";
$this->assertNotNull($first_item = $assert_session->waitForElementVisible('xpath', $first_item_locator));
$first_item->pressButton('Remove');
$assert_session->waitForElementRemoved('xpath', $first_item_locator);
$page->waitFor(10, function () use ($wrapper) {
return $wrapper->hasButton('Add media');
});
// Test reinserting the same selection.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
$wrapper->pressButton('Add media');
$this->assertNotNull($assert_session->waitForText('Add or select media'));
$assert_session->elementExists('xpath', "(//div[contains(@class, 'media-library-item')])[1]")->click();
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotNull($assert_session->waitForElementVisible('xpath', $first_item_locator));
}
/**
* Data provider for ::testInsertionAndReselection().
*
* @return array
* Test data.
*/
public static function insertionReselectionProvider() {
return [
'using media_library_widget' => [
'widget' => 'media_library_widget',
],
'using media_library_inception_widget' => [
'widget' => 'media_library_inception_widget',
],
];
}
}

View File

@@ -0,0 +1,653 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\FunctionalJavascriptTests\SortableTestTrait;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the Media library entity reference widget.
*
* @group media_library
*/
class EntityReferenceWidgetTest extends MediaLibraryTestBase {
use SortableTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui'];
/**
* The theme to install as the default for testing.
*
* @var string
*/
protected $defaultTheme = 'starterkit_theme';
/**
* Test media items.
*
* @var \Drupal\media\MediaInterface[]
*/
protected $mediaItems = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a few example media items for use in selection.
$this->mediaItems = $this->createMediaItems([
'type_one' => [
'Horse',
'Bear',
'Cat',
'Dog',
],
'type_two' => [
'Crocodile',
'Lizard',
'Snake',
'Turtle',
],
]);
// Create a user who can use the Media library.
$user = $this->drupalCreateUser([
'access content',
'create basic_page content',
'edit own basic_page content',
'view media',
'create media',
'administer node form display',
]);
$this->drupalLogin($user);
}
/**
* Tests that disabled media items don't capture focus on page load.
*/
public function testFocusNotAppliedWithoutSelectionChange(): void {
// Create a node with the maximum number of values for the field_twin_media
// field.
$node = $this->drupalCreateNode([
'type' => 'basic_page',
'field_twin_media' => [
$this->mediaItems['Horse'],
$this->mediaItems['Bear'],
],
]);
$this->drupalGet($node->toUrl('edit-form'));
$open_button = $this->assertElementExistsAfterWait('css', '.js-media-library-open-button[name^="field_twin_media"]');
// The open button should be disabled, but not have the
// 'data-disabled-focus' attribute.
$this->assertFalse($open_button->hasAttribute('data-disabled-focus'));
$this->assertTrue($open_button->hasAttribute('disabled'));
// The button should be disabled.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":disabled")');
// The button should not have focus.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").not(":focus")');
}
/**
* Tests that the Media library's widget works as expected.
*/
public function testWidget(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
// Assert that media widget instances are present.
$assert_session->pageTextContains('Unlimited media');
$assert_session->pageTextContains('Twin media');
$assert_session->pageTextContains('Single media type');
$assert_session->pageTextContains('Empty types media');
// Assert generic media library elements.
$this->openMediaLibraryForField('field_unlimited_media');
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert that the media type menu is available when more than 1 type is
// configured for the field.
$menu = $this->openMediaLibraryForField('field_unlimited_media');
$this->assertTrue($menu->hasLink('Show Type One media (selected)'));
$this->assertFalse($menu->hasLink('Type Two'));
$this->assertTrue($menu->hasLink('Type Three'));
$this->assertFalse($menu->hasLink('Type Four'));
$this->switchToMediaType('Three');
// Assert the active tab is set correctly.
$this->assertFalse($menu->hasLink('Show Type One media (selected)'));
$this->assertTrue($menu->hasLink('Show Type Three media (selected)'));
// Assert the focus is set to the first tabbable element when a vertical tab
// is clicked.
$this->assertJsCondition('jQuery(tabbable.tabbable(document.getElementById("media-library-content"))[0]).is(":focus")');
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert that there are no links in the media library view.
$this->openMediaLibraryForField('field_unlimited_media');
$assert_session->elementNotExists('css', '.media-library-item__name a');
$assert_session->elementNotExists('css', '.view-media-library .media-library-item__edit');
$assert_session->elementNotExists('css', '.view-media-library .media-library-item__remove');
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert that the media type menu is available when the target_bundles
// setting for the entity reference field is null. All types should be
// allowed in this case.
$menu = $this->openMediaLibraryForField('field_null_types_media');
// Assert that the button to open the media library does not submit the
// parent form. We can do this by checking if the validation of the parent
// form is not triggered.
$assert_session->pageTextNotContains('Title field is required.');
$this->assertTrue($menu->hasLink('Type One'));
$this->assertTrue($menu->hasLink('Type Two'));
$this->assertTrue($menu->hasLink('Type Three'));
$this->assertTrue($menu->hasLink('Type Four'));
$this->assertTrue($menu->hasLink('Type Five'));
// Insert media to test validation with null target_bundles.
$this->switchToMediaType('One');
$this->assertAnnounceContains('Showing Type One media.');
$this->selectMediaItem(0);
$this->pressInsertSelected('Added one media item.');
// Assert that the media type menu is not available when only 1 type is
// configured for the field.
$this->openMediaLibraryForField('field_single_media_type', '#media-library-wrapper');
$this->waitForElementTextContains('.media-library-selected-count', '0 of 1 item selected');
// Select a media item, assert the hidden selection field contains the ID of
// the selected item.
$this->selectMediaItem(0);
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
$this->assertSelectedMediaCount('1 of 1 item selected');
$assert_session->elementNotExists('css', '.js-media-library-menu');
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert the menu links can be sorted through the widget configuration.
$this->openMediaLibraryForField('field_twin_media');
$links = $this->getTypesMenu()->findAll('css', 'a');
$link_titles = [];
foreach ($links as $link) {
$link_titles[] = $link->getText();
}
$expected_link_titles = ['Show Type Three media (selected)', 'Show Type One media', 'Show Type Two media', 'Show Type Four media'];
$this->assertSame($link_titles, $expected_link_titles);
$this->drupalGet('admin/structure/types/manage/basic_page/form-display');
// Ensure that the widget settings form is not displayed when only
// one media type is allowed.
$assert_session->pageTextContains('Single media type');
$assert_session->buttonNotExists('field_single_media_type_settings_edit');
$assert_session->buttonExists('field_twin_media_settings_edit')->press();
$this->assertElementExistsAfterWait('css', '#field-twin-media .tabledrag-toggle-weight')->press();
$assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_one][weight]')->selectOption('0');
$assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_three][weight]')->selectOption('1');
$assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_four][weight]')->selectOption('2');
$assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_two][weight]')->selectOption('3');
$assert_session->buttonExists('Save')->press();
$this->drupalGet('node/add/basic_page');
$this->openMediaLibraryForField('field_twin_media');
$link_titles = array_map(function ($link) {
return $link->getText();
}, $links);
$this->assertSame($link_titles, ['Show Type One media (selected)', 'Show Type Three media', 'Show Type Four media', 'Show Type Two media']);
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert the announcements for media type navigation in the media library.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
$this->assertAnnounceContains('Showing Type Three media.');
$this->switchToMediaType('One');
$this->assertAnnounceContains('Showing Type One media.');
// Assert the links can be triggered by via the space bar.
$assert_session->elementExists('named', ['link', 'Type Three'])->keyPress(32);
$this->assertAnnounceContains('Showing Type Three media.');
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert media is only visible on the tab for the related media type.
$this->openMediaLibraryForField('field_unlimited_media');
$assert_session->pageTextContains('Dog');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextNotContains('Turtle');
$this->switchToMediaType('Three');
$this->assertAnnounceContains('Showing Type Three media.');
$assert_session->elementExists('named', ['link', 'Show Type Three media (selected)']);
$assert_session->pageTextNotContains('Dog');
$assert_session->pageTextNotContains('Bear');
$assert_session->pageTextNotContains('Turtle');
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert the exposed name filter of the view.
$this->openMediaLibraryForField('field_unlimited_media');
$session = $this->getSession();
$session->getPage()->fillField('Name', 'Dog');
$session->getPage()->pressButton('Apply filters');
$this->waitForText('Dog');
$this->markTestSkipped("Skipped temporarily for random fails.");
$this->waitForNoText('Bear');
$session->getPage()->fillField('Name', '');
$session->getPage()->pressButton('Apply filters');
$this->waitForText('Dog');
$this->waitForText('Bear');
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert adding a single media item and removing it.
$this->openMediaLibraryForField('field_twin_media');
$this->selectMediaItem(0);
$this->pressInsertSelected('Added one media item.');
// Assert the focus is set back on the open button of the media field.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":focus")');
// The toggle for weight inputs' visibility should not be available when the
// field contains a single item.
$wrapper = $assert_session->elementExists('css', '.field--name-field-twin-media');
$assert_session->elementNotExists('named', ['button', 'Show media item weights'], $wrapper);
// Remove the selected item.
$button = $assert_session->buttonExists('Remove', $wrapper);
$this->assertSame('Remove Dog', $button->getAttribute('aria-label'));
$button->press();
$this->waitForText('Dog has been removed.');
// Assert the focus is set back on the open button of the media field.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":focus")');
// Assert we can select the same media item twice.
$this->openMediaLibraryForField('field_twin_media');
$page->checkField('Select Dog');
$this->pressInsertSelected('Added one media item.');
$this->openMediaLibraryForField('field_twin_media');
$page->checkField('Select Dog');
$this->pressInsertSelected('Added one media item.');
$this->waitForElementsCount('css', '.field--name-field-twin-media [data-media-library-item-delta]', 2);
// Assert that we can toggle the visibility of the weight inputs when the
// field contains more than one item.
$wrapper = $assert_session->elementExists('css', '.field--name-field-twin-media');
$wrapper->pressButton('Show media item weights');
// Ensure that the styling doesn't accidentally render the weight field
// unusable.
$assert_session->fieldExists('Weight', $wrapper)->click();
$wrapper->pressButton('Hide media item weights');
// Assert the same has been added twice and remove the items again.
$this->waitForElementsCount('css', '.field--name-field-twin-media [data-media-library-item-delta]', 2);
$assert_session->hiddenFieldValueEquals('field_twin_media[selection][0][target_id]', 4);
$assert_session->hiddenFieldValueEquals('field_twin_media[selection][1][target_id]', 4);
$wrapper->pressButton('Remove');
$this->waitForText('Dog has been removed.');
$wrapper->pressButton('Remove');
$this->waitForText('Dog has been removed.');
$result = $wrapper->waitFor(10, function ($wrapper) {
/** @var \Behat\Mink\Element\NodeElement $wrapper */
return $wrapper->findButton('Remove') == NULL;
});
$this->assertTrue($result);
// Assert the selection is persistent in the media library modal, and
// the number of selected items is displayed correctly.
$this->openMediaLibraryForField('field_twin_media');
// Assert the number of selected items is displayed correctly.
$this->assertSelectedMediaCount('0 of 2 items selected');
// Select a media item, assert the hidden selection field contains the ID of
// the selected item.
$checkboxes = $this->getCheckboxes();
$this->assertCount(4, $checkboxes);
$this->selectMediaItem(0, '1 of 2 items selected');
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
// Select another item and assert the number of selected items is updated.
$this->selectMediaItem(1, '2 of 2 items selected');
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4,3');
// Assert unselected items are disabled when the maximum allowed items are
// selected (cardinality for this field is 2).
$this->assertTrue($checkboxes[2]->hasAttribute('disabled'));
$this->assertTrue($checkboxes[3]->hasAttribute('disabled'));
// Assert the selected items are updated when deselecting an item.
$checkboxes[0]->click();
$this->assertSelectedMediaCount('1 of 2 items selected');
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', '3');
// Assert deselected items are available again.
$this->assertFalse($checkboxes[2]->hasAttribute('disabled'));
$this->assertFalse($checkboxes[3]->hasAttribute('disabled'));
// The selection should be persisted when navigating to other media types in
// the modal.
$this->switchToMediaType('Three');
$this->switchToMediaType('One');
$selected_checkboxes = [];
foreach ($this->getCheckboxes() as $checkbox) {
if ($checkbox->isChecked()) {
$selected_checkboxes[] = $checkbox->getValue();
}
}
$this->assertCount(1, $selected_checkboxes);
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', implode(',', $selected_checkboxes));
$this->assertSelectedMediaCount('1 of 2 items selected');
// Add to selection from another type.
$this->switchToMediaType('Two');
$checkboxes = $this->getCheckboxes();
$this->assertCount(4, $checkboxes);
$this->selectMediaItem(0, '2 of 2 items selected');
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', '3,8');
// Assert unselected items are disabled when the maximum allowed items are
// selected (cardinality for this field is 2).
$this->assertFalse($checkboxes[0]->hasAttribute('disabled'));
$this->assertTrue($checkboxes[1]->hasAttribute('disabled'));
$this->assertTrue($checkboxes[2]->hasAttribute('disabled'));
$this->assertTrue($checkboxes[3]->hasAttribute('disabled'));
// Assert the checkboxes are also disabled on other pages.
$this->switchToMediaType('One');
$this->assertTrue($checkboxes[0]->hasAttribute('disabled'));
$this->assertFalse($checkboxes[1]->hasAttribute('disabled'));
$this->assertTrue($checkboxes[2]->hasAttribute('disabled'));
$this->assertTrue($checkboxes[3]->hasAttribute('disabled'));
// Select the items.
$this->pressInsertSelected('Added 2 media items.');
// Assert the open button is disabled.
$open_button = $this->assertElementExistsAfterWait('css', '.js-media-library-open-button[name^="field_twin_media"]');
$this->assertTrue($open_button->hasAttribute('data-disabled-focus'));
$this->assertTrue($open_button->hasAttribute('disabled'));
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":disabled")');
// Ensure that the selection completed successfully.
$assert_session->pageTextNotContains('Add or select media');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Dog');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Cat');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Turtle');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Snake');
// Remove "Cat" (happens to be the first remove button on the page).
$button = $assert_session->buttonExists('Remove', $wrapper);
$this->assertSame('Remove Cat', $button->getAttribute('aria-label'));
$button->press();
$this->waitForText('Cat has been removed.');
// Assert the focus is set to the wrapper of the other selected item.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper [data-media-library-item-delta]").is(":focus")');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Cat');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Turtle');
// Assert the open button is no longer disabled.
$open_button = $assert_session->elementExists('css', '.js-media-library-open-button[name^="field_twin_media"]');
$this->assertFalse($open_button->hasAttribute('data-disabled-focus'));
$this->assertFalse($open_button->hasAttribute('disabled'));
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":not(:disabled)")');
// Open the media library again and select another item.
$this->openMediaLibraryForField('field_twin_media');
$this->selectMediaItem(0);
$this->pressInsertSelected('Added one media item.');
$this->waitForElementTextContains('#field_twin_media-media-library-wrapper', 'Dog');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Cat');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Turtle');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Snake');
// Assert the open button is disabled.
$this->assertTrue($assert_session->elementExists('css', '.js-media-library-open-button[name^="field_twin_media"]')->hasAttribute('data-disabled-focus'));
$this->assertTrue($assert_session->elementExists('css', '.js-media-library-open-button[name^="field_twin_media"]')->hasAttribute('disabled'));
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":disabled")');
// Assert the selection is cleared when the modal is closed.
$this->openMediaLibraryForField('field_unlimited_media');
$checkboxes = $this->getCheckboxes();
$this->assertGreaterThanOrEqual(4, count($checkboxes));
// Nothing is selected yet.
$this->assertFalse($checkboxes[0]->isChecked());
$this->assertFalse($checkboxes[1]->isChecked());
$this->assertFalse($checkboxes[2]->isChecked());
$this->assertFalse($checkboxes[3]->isChecked());
$this->assertSelectedMediaCount('0 items selected');
// Select the first 2 items.
$checkboxes[0]->click();
$this->assertSelectedMediaCount('1 item selected');
$checkboxes[1]->click();
$this->assertSelectedMediaCount('2 items selected');
$this->assertTrue($checkboxes[0]->isChecked());
$this->assertTrue($checkboxes[1]->isChecked());
$this->assertFalse($checkboxes[2]->isChecked());
$this->assertFalse($checkboxes[3]->isChecked());
// Close the dialog, reopen it and assert not is selected again.
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
$this->openMediaLibraryForField('field_unlimited_media');
$checkboxes = $this->getCheckboxes();
$this->assertGreaterThanOrEqual(4, count($checkboxes));
$this->assertFalse($checkboxes[0]->isChecked());
$this->assertFalse($checkboxes[1]->isChecked());
$this->assertFalse($checkboxes[2]->isChecked());
$this->assertFalse($checkboxes[3]->isChecked());
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Finally, save the form.
$assert_session->elementExists('css', '.js-media-library-widget-toggle-weight')->click();
$this->submitForm([
'title[0][value]' => 'My page',
'field_twin_media[selection][0][weight]' => '3',
], 'Save');
$assert_session->pageTextContains('Basic Page My page has been created');
// We removed this item earlier.
$assert_session->pageTextNotContains('Cat');
// This item was never selected.
$assert_session->pageTextNotContains('Snake');
// "Turtle" should come after "Dog", since we changed the weight.
$assert_session->elementExists('css', '.field--name-field-twin-media > .field__items > .field__item:last-child:contains("Turtle")');
// Make sure everything that was selected shows up.
$assert_session->pageTextContains('Dog');
$assert_session->pageTextContains('Turtle');
// Re-edit the content and make a new selection.
$this->drupalGet('node/1/edit');
$assert_session->pageTextContains('Dog');
$assert_session->pageTextNotContains('Cat');
$assert_session->pageTextNotContains('Bear');
$assert_session->pageTextNotContains('Horse');
$assert_session->pageTextContains('Turtle');
$assert_session->pageTextNotContains('Snake');
$this->openMediaLibraryForField('field_unlimited_media');
// Select all media items of type one (should also contain Dog, again).
$this->selectMediaItem(0);
$this->selectMediaItem(1);
$this->selectMediaItem(2);
$this->selectMediaItem(3);
$this->pressInsertSelected('Added 4 media items.');
$this->waitForText('Dog');
$assert_session->pageTextContains('Cat');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextContains('Horse');
$assert_session->pageTextContains('Turtle');
$assert_session->pageTextNotContains('Snake');
$this->submitForm([], 'Save');
$assert_session->pageTextContains('Dog');
$assert_session->pageTextContains('Cat');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextContains('Horse');
$assert_session->pageTextContains('Turtle');
$assert_session->pageTextNotContains('Snake');
}
/**
* Tests saving a required media library field.
*/
public function testRequiredMediaField(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Make field_unlimited_media required.
$field_config = FieldConfig::loadByName('node', 'basic_page', 'field_unlimited_media');
$field_config->setRequired(TRUE)->save();
$this->drupalGet('node/add/basic_page');
$page->fillField('Title', 'My page');
$page->pressButton('Save');
// Check that a clear error message is shown.
$assert_session->pageTextNotContains('This value should not be null.');
$assert_session->pageTextContains(sprintf('%s field is required.', $field_config->label()));
// Open the media library, select an item and save the node.
$this->openMediaLibraryForField('field_unlimited_media');
$this->selectMediaItem(0);
$this->pressInsertSelected('Added one media item.');
$page->pressButton('Save');
// Confirm that the node was created.
$this->assertSession()->pageTextContains('Basic page My page has been created.');
}
/**
* Tests that changed order is maintained after removing a selection.
*/
public function testRemoveAfterReordering(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('node/add/basic_page');
$page->fillField('Title', 'My page');
$this->openMediaLibraryForField('field_unlimited_media');
$page->checkField('Select Dog');
$page->checkField('Select Cat');
$page->checkField('Select Bear');
// Order: Dog - Cat - Bear.
$this->pressInsertSelected('Added 3 media items.');
// Move first item (Dog) to the end.
// Order: Cat - Bear - Dog.
$this->sortableAfter('[data-media-library-item-delta="0"]', '[data-media-library-item-delta="2"]', '.js-media-library-selection');
$wrapper = $assert_session->elementExists('css', '.field--name-field-unlimited-media');
// Remove second item (Bear).
// Order: Cat - Dog.
$wrapper->find('css', "[aria-label='Remove Bear']")->press();
$this->waitForText('Bear has been removed.');
$page->pressButton('Save');
$assert_session->elementTextContains('css', '.field--name-field-unlimited-media > .field__items > .field__item:last-child', 'Dog');
}
/**
* Tests that order is correct after re-order and adding another item.
*/
public function testAddAfterReordering(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('node/add/basic_page');
$page->fillField('Title', 'My page');
$this->openMediaLibraryForField('field_unlimited_media');
$page->checkField('Select Dog');
$page->checkField('Select Cat');
// Order: Dog - Cat.
$this->pressInsertSelected('Added 2 media items.');
// Change positions.
// Order: Cat - Dog.
$this->sortableAfter('[data-media-library-item-delta="0"]', '[data-media-library-item-delta="1"]', '.js-media-library-selection');
$this->openMediaLibraryForField('field_unlimited_media');
$this->selectMediaItem(2);
// Order: Cat - Dog - Bear.
$this->pressInsertSelected('Added one media item.');
$page->pressButton('Save');
$assert_session->elementTextContains('css', '.field--name-field-unlimited-media > .field__items > .field__item:first-child', 'Cat');
$assert_session->elementTextContains('css', '.field--name-field-unlimited-media > .field__items > .field__item:last-child', 'Bear');
}
/**
* Checks for inclusion of text in #drupal-live-announce.
*
* @param string $expected_message
* The text that is expected to be present in the #drupal-live-announce element.
*
* @internal
*/
protected function assertAnnounceContains(string $expected_message): void {
$assert_session = $this->assertSession();
$this->assertNotEmpty($assert_session->waitForElement('css', "#drupal-live-announce:contains('$expected_message')"));
}
/**
* {@inheritdoc}
*/
protected function sortableUpdate($item, $from, $to = NULL) {
// See core/modules/media_library/js/media_library.widget.js.
$script = <<<JS
(function ($) {
var selection = document.querySelectorAll('.js-media-library-selection');
selection.forEach(function (widget) {
$(widget).children().each(function (index, child) {
$(child).find('.js-media-library-item-weight').val(index);
});
});
})(jQuery)
JS;
$this->getSession()->executeScript($script);
}
/**
* Tests the preview displayed by the field widget.
*/
public function testWidgetPreview(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$node = $this->drupalCreateNode([
'type' => 'basic_page',
'field_unlimited_media' => [
$this->mediaItems['Horse'],
],
]);
$media_id = $this->mediaItems['Horse']->id();
// Assert that preview is present for current user, who can view media.
$this->drupalGet($node->toUrl('edit-form'));
$assert_session->elementTextContains('css', '[data-drupal-selector="edit-field-unlimited-media-selection-0"]', 'Horse');
$remove_button = $page->find('css', '[data-drupal-selector="edit-field-unlimited-media-selection-0-remove-button"]');
$this->assertSame('Remove Horse', $remove_button->getAttribute('aria-label'));
$assert_session->pageTextNotContains('You do not have permission to view media item');
$remove_button->press();
$this->waitForText("Removing Horse.");
$this->waitForText("Horse has been removed.");
// Logout without saving.
$this->drupalLogout();
// Create a user who can edit content but not view media.
// Must remove permission from authenticated role first, otherwise the new
// user will inherit that permission.
$role = Role::load(RoleInterface::AUTHENTICATED_ID);
$role->revokePermission('view media');
$role->save();
$non_media_editor = $this->drupalCreateUser([
'access content',
'create basic_page content',
'edit any basic_page content',
]);
$this->drupalLogin($non_media_editor);
// Assert that preview does not reveal media name.
$this->drupalGet($node->toUrl('edit-form'));
// There should be no preview name.
$assert_session->elementTextNotContains('css', '[data-drupal-selector="edit-field-unlimited-media-selection-0"]', 'Horse');
// The remove button should have a generic message.
$remove_button = $page->find('css', '[data-drupal-selector="edit-field-unlimited-media-selection-0-remove-button"]');
$this->assertSame('Remove media', $remove_button->getAttribute('aria-label'));
$assert_session->pageTextContains("You do not have permission to view media item $media_id.");
// Confirm ajax text does not reveal media name.
$remove_button->press();
$this->waitForText("Removing media.");
$this->waitForText("Media has been removed.");
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
// cspell:ignore shatner
/**
* Tests field UI integration for media library widget.
*
* @group media_library
*/
class FieldUiIntegrationTest extends MediaLibraryTestBase {
/**
* {@inheritdoc}
*/
protected $strictConfigSchema = FALSE;
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
// Create a user who can add media fields.
$user = $this->drupalCreateUser([
'access administration pages',
'administer node fields',
'administer node form display',
]);
$this->drupalLogin($user);
$this->drupalCreateContentType(['type' => 'article']);
$this->drupalCreateContentType(['type' => 'page']);
$this->createMediaItems([
'type_one' => [
'Horse',
'Bear',
'Cat',
'Dog',
],
]);
}
/**
* Tests field UI integration for media library widget.
*/
public function testFieldUiIntegration(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$user = $this->drupalCreateUser([
'access administration pages',
'administer node fields',
'administer node form display',
'view media',
'bypass node access',
]);
$this->drupalLogin($user);
$this->drupalGet('/admin/structure/types/manage/article/fields/add-field');
$page->find('css', "[name='new_storage_type'][value='field_ui:entity_reference:media']")->getParent()->click();
$page->findButton('Continue')->click();
$this->assertNotNull($assert_session->waitForField('label'));
$page->fillField('label', 'Shatner');
$this->waitForText('field_shatner');
$page->pressButton('Continue');
$this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_shatner.*/', $this->getUrl());
$assert_session->pageTextNotContains('Undefined index: target_bundles');
$this->waitForFieldExists('Type One')->check();
$this->assertElementExistsAfterWait('css', '[name="settings[handler_settings][target_bundles][type_one]"][checked="checked"]');
$page->checkField('settings[handler_settings][target_bundles][type_two]');
$this->assertElementExistsAfterWait('css', '[name="settings[handler_settings][target_bundles][type_two]"][checked="checked"]');
$page->checkField('settings[handler_settings][target_bundles][type_three]');
$this->assertElementExistsAfterWait('css', '[name="settings[handler_settings][target_bundles][type_three]"][checked="checked"]');
$page->pressButton('Save settings');
$assert_session->pageTextContains('Saved Shatner configuration.');
$this->drupalGet('/admin/structure/types/manage/article/fields/node.article.field_shatner');
$assert_session->checkboxNotChecked('set_default_value');
$page->checkField('set_default_value');
$this->assertElementExistsAfterWait('css', "#field_shatner-media-library-wrapper-default_value_input")
->pressButton('Add media');
$this->waitForText('Add or select media');
$this->selectMediaItem(0);
$this->pressInsertSelected('Added one media item.');
$page->pressButton('Save settings');
$assert_session->pageTextContains('Saved Shatner configuration.');
$this->drupalGet('/admin/structure/types/manage/article/fields/node.article.field_shatner');
$assert_session->checkboxChecked('set_default_value');
// Create a new instance of an existing field storage and assert that it
// automatically uses the media library.
$this->drupalGet('/admin/structure/types/manage/page/fields/reuse');
$this->assertSession()->elementExists('css', "input[value=Re-use][name=field_shatner]");
$this->click("input[value=Re-use][name=field_shatner]");
$this->waitForFieldExists('Type One')->check();
$this->assertElementExistsAfterWait('css', '[name="settings[handler_settings][target_bundles][type_one]"][checked="checked"]');
$page->pressButton('Save settings');
$this->drupalGet('/admin/structure/types/manage/page/form-display');
$assert_session->fieldValueEquals('fields[field_shatner][type]', 'media_library_widget');
}
}

View File

@@ -0,0 +1,491 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\media\Entity\Media;
/**
* Base class for functional tests of Media Library functionality.
*/
abstract class MediaLibraryTestBase extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['media_library_test', 'hold_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Create media items.
*
* @param array $media_items
* A nested array of media item names keyed by media type.
*
* @return \Drupal\media\MediaInterface[]
* An array of media entities keyed by the names passed in.
*/
protected function createMediaItems(array $media_items) {
$created_items = [];
$time = time();
foreach ($media_items as $type => $names) {
foreach ($names as $name) {
/** @var \Drupal\media\MediaInterface $media */
$media = Media::create([
'name' => $name,
'bundle' => $type,
]);
$source_field = $media->getSource()
->getSourceFieldDefinition($media->bundle->entity)
->getName();
$media->set($source_field, $name)->setCreatedTime(++$time)->save();
$created_items[$name] = $media;
}
}
return $created_items;
}
/**
* Asserts that text appears on page after a wait.
*
* @param string $text
* The text that should appear on the page.
* @param int $timeout
* Timeout in milliseconds, defaults to 10000.
*
* @todo replace with whatever gets added in
* https://www.drupal.org/node/3061852
*/
protected function waitForText($text, $timeout = 10000) {
$result = $this->assertSession()->waitForText($text, $timeout);
$this->assertNotEmpty($result, "\"$text\" not found");
}
/**
* Asserts that text does not appear on page after a wait.
*
* @param string $text
* The text that should not be on the page.
* @param int $timeout
* Timeout in milliseconds, defaults to 10000.
*
* @todo replace with whatever gets added in
* https://www.drupal.org/node/3061852
*/
protected function waitForNoText($text, $timeout = 10000) {
$page = $this->getSession()->getPage();
$result = $page->waitFor($timeout / 1000, function ($page) use ($text) {
$actual = preg_replace('/\s+/u', ' ', $page->getText());
$regex = '/' . preg_quote($text, '/') . '/ui';
return (bool) !preg_match($regex, $actual);
});
$this->assertNotEmpty($result, "\"$text\" was found but shouldn't be there.");
}
/**
* Checks for a specified number of specific elements on page after wait.
*
* @param string $selector_type
* Element selector type (css, xpath)
* @param string|array $selector
* Element selector.
* @param int $count
* Expected count.
* @param int $timeout
* Timeout in milliseconds, defaults to 10000.
*
* @todo replace with whatever gets added in
* https://www.drupal.org/node/3061852
*/
protected function waitForElementsCount($selector_type, $selector, $count, $timeout = 10000) {
$page = $this->getSession()->getPage();
$start = microtime(TRUE);
$end = $start + ($timeout / 1000);
do {
$nodes = $page->findAll($selector_type, $selector);
if (count($nodes) === $count) {
return;
}
usleep(100000);
} while (microtime(TRUE) < $end);
$this->assertSession()->elementsCount($selector_type, $selector, $count);
}
/**
* Asserts that text appears in an element after a wait.
*
* @param string $selector
* The CSS selector of the element to check.
* @param string $text
* The text that should appear in the element.
* @param int $timeout
* Timeout in milliseconds, defaults to 10000.
*
* @todo replace with whatever gets added in
* https://www.drupal.org/node/3061852
*/
protected function waitForElementTextContains($selector, $text, $timeout = 10000) {
$element = $this->assertSession()->waitForElement('css', "$selector:contains('$text')", $timeout);
$this->assertNotEmpty($element);
}
/**
* Waits for the specified selector and returns it if not empty.
*
* @param string $selector
* The selector engine name. See ElementInterface::findAll() for the
* supported selectors.
* @param string|array $locator
* The selector locator.
* @param int $timeout
* Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement
* The page element node if found. If not found, the test fails.
*
* @todo replace with whatever gets added in
* https://www.drupal.org/node/3061852
*/
protected function assertElementExistsAfterWait($selector, $locator, $timeout = 10000) {
$element = $this->assertSession()->waitForElement($selector, $locator, $timeout);
$this->assertNotEmpty($element);
return $element;
}
/**
* Gets the menu of available media types.
*
* @return \Behat\Mink\Element\NodeElement
* The menu of available media types.
*/
protected function getTypesMenu() {
return $this->assertSession()
->elementExists('css', '.js-media-library-menu');
}
/**
* Clicks a media type tab and waits for it to appear.
*/
protected function switchToMediaType($type) {
$link = $this->assertSession()
->elementExists('named', ['link', "Type $type"], $this->getTypesMenu());
if ($link->hasClass('active')) {
// There is nothing to do as the type is already active.
return;
}
$link->click();
$result = $link->waitFor(10, function ($link) {
/** @var \Behat\Mink\Element\NodeElement $link */
return $link->hasClass('active');
});
$this->assertNotEmpty($result);
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$this->assertSession()->assertWaitOnAjaxRequest();
}
/**
* Checks for the existence of a field on page after wait.
*
* @param string $field
* The field to find.
* @param int $timeout
* Timeout in milliseconds, defaults to 10000.
*
* @return \Behat\Mink\Element\NodeElement|null
* The element if found, otherwise null.
*
* @todo replace with whatever gets added in
* https://www.drupal.org/node/3061852
*/
protected function waitForFieldExists($field, $timeout = 10000) {
$assert_session = $this->assertSession();
$assert_session->waitForField($field, $timeout);
return $assert_session->fieldExists($field);
}
/**
* Waits for a file field to exist before uploading.
*/
public function addMediaFileToField($locator, $path) {
$page = $this->getSession()->getPage();
$this->waitForFieldExists($locator);
$page->attachFileToField($locator, $path);
}
/**
* Clicks "Save and select||insert" button and waits for AJAX completion.
*
* @param string $operation
* The final word of the button to be clicked.
*/
protected function saveAnd($operation) {
$this->assertElementExistsAfterWait('css', '.ui-dialog-buttonpane')->pressButton("Save and $operation");
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$this->assertSession()->assertWaitOnAjaxRequest();
}
/**
* Clicks "Save" button and waits for AJAX completion.
*
* @param bool $expect_errors
* Whether validation errors are expected after the "Save" button is
* pressed. Defaults to FALSE.
*/
protected function pressSaveButton($expect_errors = FALSE) {
$buttons = $this->assertElementExistsAfterWait('css', '.ui-dialog-buttonpane');
$buttons->pressButton('Save');
// If no validation errors are expected, wait for the "Insert selected"
// button to return.
if (!$expect_errors) {
$result = $buttons->waitFor(10, function ($buttons) {
/** @var \Behat\Mink\Element\NodeElement $buttons */
return $buttons->findButton('Insert selected');
});
$this->assertNotEmpty($result);
}
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$this->assertSession()->assertWaitOnAjaxRequest();
}
/**
* Clicks a button that opens a media widget and confirms it is open.
*
* @param string $field_name
* The machine name of the field for which to open the media library.
* @param string $after_open_selector
* The selector to look for after the button is clicked.
*
* @return \Behat\Mink\Element\NodeElement
* The NodeElement found via $after_open_selector.
*/
protected function openMediaLibraryForField($field_name, $after_open_selector = '.js-media-library-menu') {
$this->assertElementExistsAfterWait('css', "#$field_name-media-library-wrapper.js-media-library-widget")
->pressButton('Add media');
$this->waitForText('Add or select media');
// Assert that the grid display is visible and the links to toggle between
// the grid and table displays are present.
$this->assertMediaLibraryGrid();
$assert_session = $this->assertSession();
$assert_session->linkExists('Grid');
$assert_session->linkExists('Table');
// The "select all" checkbox should never be present in the modal.
$assert_session->elementNotExists('css', '.media-library-select-all');
return $this->assertElementExistsAfterWait('css', $after_open_selector);
}
/**
* Gets the "Additional selected media" area after adding new media.
*
* @param bool $open
* Whether or not to open the area before returning it. Defaults to TRUE.
*
* @return \Behat\Mink\Element\NodeElement
* The "additional selected media" area.
*/
protected function getSelectionArea($open = TRUE) {
$summary = $this->assertElementExistsAfterWait('css', 'summary:contains("Additional selected media")');
if ($open) {
$summary->click();
}
return $summary->getParent();
}
/**
* Asserts a media item was added, but not yet saved.
*
* @param int $index
* (optional) The index of the media item, if multiple items can be added at
* once. Defaults to 0.
*/
protected function assertMediaAdded($index = 0) {
$selector = '.js-media-library-add-form-added-media';
// Assert that focus is shifted to the new media items.
$this->assertJsCondition('jQuery("' . $selector . '").is(":focus")');
$assert_session = $this->assertSession();
$assert_session->pageTextMatches('/The media items? ha(s|ve) been created but ha(s|ve) not yet been saved. Fill in any required fields and save to add (it|them) to the media library./');
$assert_session->elementAttributeContains('css', $selector, 'aria-label', 'Added media items');
$fields = $this->assertElementExistsAfterWait('css', '[data-drupal-selector="edit-media-' . $index . '-fields"]');
$assert_session->elementNotExists('css', '.js-media-library-menu');
// Assert extraneous components were removed in
// FileUploadForm::hideExtraSourceFieldComponents().
$assert_session->elementNotExists('css', '[data-drupal-selector$="preview"]', $fields);
$assert_session->buttonNotExists('Remove', $fields);
$assert_session->elementNotExists('css', '[data-drupal-selector$="filename"]', $fields);
}
/**
* Asserts that media was not added, i.e. due to a validation error.
*/
protected function assertNoMediaAdded() {
// Assert the focus is shifted to the first tabbable element of the add
// form, which should be the source field.
$this->assertJsCondition('jQuery(tabbable.tabbable(document.getElementById("media-library-add-form-wrapper"))[0]).is(":focus")');
$this->assertSession()
->elementNotExists('css', '[data-drupal-selector="edit-media-0-fields"]');
$this->getTypesMenu();
}
/**
* Presses the modal's "Insert selected" button.
*
* @param string $expected_announcement
* (optional) The expected screen reader announcement once the modal is
* closed.
* @param bool $should_close
* (optional) TRUE if we expect the modal to be successfully closed.
*
* @todo Consider requiring screen reader assertion every time "Insert
* selected" is pressed in
* https://www.drupal.org/project/drupal/issues/3087227.
*/
protected function pressInsertSelected($expected_announcement = NULL, bool $should_close = TRUE) {
$this->assertSession()
->elementExists('css', '.ui-dialog-buttonpane')
->pressButton('Insert selected');
if ($should_close) {
$this->waitForNoText('Add or select media');
}
if ($expected_announcement) {
$this->waitForText($expected_announcement);
}
}
/**
* Gets all available media item checkboxes.
*
* @return \Behat\Mink\Element\NodeElement[]
* The available checkboxes.
*/
protected function getCheckboxes() {
return $this->getSession()
->getPage()
->findAll('css', '.js-media-library-view .js-click-to-select-checkbox input');
}
/**
* Selects an item in the media library modal.
*
* @param int $index
* The zero-based index of the item to select.
* @param string $expected_selected_count
* (optional) The expected text of the selection counter.
*/
protected function selectMediaItem($index, $expected_selected_count = NULL) {
$checkboxes = $this->getCheckboxes();
$this->assertGreaterThan($index, count($checkboxes));
$checkboxes[$index]->check();
if ($expected_selected_count) {
$this->assertSelectedMediaCount($expected_selected_count);
}
}
/**
* De-selects an item in the media library modal.
*
* @param int $index
* The zero-based index of the item to unselect.
*/
protected function deselectMediaItem(int $index): void {
$checkboxes = $this->getCheckboxes();
$this->assertGreaterThan($index, count($checkboxes));
$checkboxes[$index]->uncheck();
}
/**
* Switches to the grid display of the widget view.
*/
protected function switchToMediaLibraryGrid() {
$this->getSession()->getPage()->clickLink('Grid');
// Assert the display change is correctly announced for screen readers.
$this->assertAnnounceContains('Loading grid view.');
$this->assertAnnounceContains('Changed to grid view.');
$this->assertMediaLibraryGrid();
}
/**
* Switches to the table display of the widget view.
*/
protected function switchToMediaLibraryTable() {
hold_test_response(TRUE);
$this->getSession()->getPage()->clickLink('Table');
// Assert the display change is correctly announced for screen readers.
$this->assertAnnounceContains('Loading table view.');
hold_test_response(FALSE);
$this->assertAnnounceContains('Changed to table view.');
$this->assertMediaLibraryTable();
}
/**
* Asserts that the grid display of the widget view is visible.
*/
protected function assertMediaLibraryGrid() {
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()
->elementExists('css', '.js-media-library-view[data-view-display-id="widget"]');
}
/**
* Asserts that the table display of the widget view is visible.
*/
protected function assertMediaLibraryTable() {
$this->assertSession()
->elementExists('css', '.js-media-library-view[data-view-display-id="widget_table"]');
}
/**
* Asserts the current text of the selected item counter.
*
* @param string $text
* The expected text of the counter.
*/
protected function assertSelectedMediaCount($text) {
$selected_count = $this->assertSession()
->elementExists('css', '.js-media-library-selected-count');
$this->assertSame('status', $selected_count->getAttribute('role'));
$this->assertSame('polite', $selected_count->getAttribute('aria-live'));
$this->assertSame('true', $selected_count->getAttribute('aria-atomic'));
$this->assertSame($text, $selected_count->getText());
}
/**
* Checks for inclusion of text in #drupal-live-announce.
*
* @param string $expected_message
* The text expected to be present in #drupal-live-announce.
*
* @internal
*/
protected function assertAnnounceContains(string $expected_message): void {
$assert_session = $this->assertSession();
$this->assertNotEmpty($assert_session->waitForElement('css', "#drupal-live-announce:contains('$expected_message')"));
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
/**
* Tests the grid-style media overview page.
*
* @group media_library
*/
class MediaOverviewTest extends MediaLibraryTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a few example media items for use in selection.
$this->createMediaItems([
'type_one' => [
'Horse',
'Bear',
'Cat',
'Dog',
],
'type_two' => [
'Crocodile',
'Lizard',
'Snake',
'Turtle',
],
]);
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('local_actions_block');
$user = $this->drupalCreateUser([
'access media overview',
'create media',
'delete any media',
'update any media',
]);
$this->drupalLogin($user);
}
/**
* Tests that the Media Library's administration page works as expected.
*/
public function testAdministrationPage(): void {
$session = $this->getSession();
$page = $session->getPage();
$assert_session = $this->assertSession();
// Visit the administration page.
$this->drupalGet('admin/content/media');
// There should be links to both the grid and table displays.
$assert_session->linkExists('Grid');
$assert_session->linkExists('Table');
// We should see the table view and a link to add media.
$assert_session->elementExists('css', '[data-drupal-selector="views-form-media-media-page-list"] table');
$assert_session->linkExists('Add media');
// Go to the grid display for the rest of the test.
$page->clickLink('Grid');
$assert_session->addressEquals('admin/content/media-grid');
// Verify that the "Add media" link is present.
$assert_session->linkExists('Add media');
// Verify that media from two separate types is present.
$assert_session->pageTextContains('Dog');
$assert_session->pageTextContains('Turtle');
// Verify that the media name does not contain a link. The selector is
// tricky, so start by asserting ".js-media-library-item-preview + div"
// can select a div containing a media name.
$assert_session->elementExists('css', '.js-media-library-item-preview + div:contains("Dog")');
$assert_session->elementExists('css', '.js-media-library-item-preview + div:contains("Turtle")');
$assert_session->elementNotExists('css', '.js-media-library-item-preview + div a');
// Verify that there are links to edit and delete media items.
$assert_session->linkExists('Edit Dog');
$assert_session->linkExists('Delete Turtle');
// Test that users can filter by type.
$page->selectFieldOption('Media type', 'Type One');
$page->pressButton('Apply filters');
$this->waitForNoText('Turtle');
$assert_session->pageTextContains('Dog');
$page->selectFieldOption('Media type', 'Type Two');
$page->pressButton('Apply filters');
$this->waitForText('Turtle');
$assert_session->pageTextNotContains('Dog');
// Test that selecting elements as a part of bulk operations works.
$page->selectFieldOption('Media type', '- Any -');
$assert_session->elementExists('css', '#views-exposed-form-media-library-page')->submit();
$this->waitForText('Dog');
// Select the "Delete media" action.
$page->selectFieldOption('Action', 'Delete media');
$this->waitForText('Dog');
// This tests that anchor tags clicked inside the preview are suppressed.
$this->getSession()->executeScript('jQuery(".js-click-to-select-trigger a")[4].click()');
$this->submitForm([], 'Apply to selected items');
$assert_session->pageTextContains('Dog');
$assert_session->pageTextNotContains('Cat');
// For reasons that are not clear, deleting media items by pressing the
// "Delete" button can fail (the button is found, but never actually pressed
// by the Mink driver). This workaround allows the delete form to be
// submitted.
$assert_session->elementExists('css', 'form')->submit();
$assert_session->pageTextNotContains('Dog');
$assert_session->pageTextContains('Cat');
// Test the 'Select all media' checkbox and assert that it makes the
// expected announcements.
$select_all = $this->waitForFieldExists('Select all media');
$select_all->check();
$this->waitForText('All 7 items selected');
$select_all->uncheck();
$this->waitForText('Zero items selected');
$select_all->check();
$page->selectFieldOption('Action', 'media_delete_action');
$this->submitForm([], 'Apply to selected items');
// For reasons that are not clear, deleting media items by pressing the
// "Delete" button can fail (the button is found, but never actually pressed
// by the Mink driver). This workaround allows the delete form to be
// submitted.
$assert_session->elementExists('css', 'form')->submit();
$assert_session->pageTextNotContains('Cat');
$assert_session->pageTextNotContains('Turtle');
$assert_session->pageTextNotContains('Snake');
// Test empty text.
$assert_session->pageTextContains('No media available.');
}
}

View File

@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\file\Entity\File;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\media\Entity\Media;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests media library for translatable media.
*
* @group media_library
*/
class TranslationsTest extends WebDriverTestBase {
use EntityReferenceFieldCreationTrait;
use MediaTypeCreationTrait;
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'field',
'media',
'media_library',
'node',
'views',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create some languages.
foreach (['nl', 'es'] as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
// Create an image media type and article node type.
$this->createMediaType('image', ['id' => 'image']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Make the media translatable and ensure the change is picked up.
\Drupal::service('content_translation.manager')->setEnabled('media', 'image', TRUE);
// Create a media reference field on articles.
$this->createEntityReferenceField(
'node',
'article',
'field_media',
'Media',
'media',
'default',
['target_bundles' => ['image']],
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
);
// Add the media field to the form display.
\Drupal::service('entity_display.repository')->getFormDisplay('node', 'article')
->setComponent('field_media', ['type' => 'media_library_widget'])
->save();
// Create a file to user for our images.
$image = File::create([
'uri' => $this->getTestFiles('image')[0]->uri,
]);
$image->setPermanent();
$image->save();
// Create a translated and untranslated media item in each language.
// cSpell:disable
$media_items = [
['nl' => 'Eekhoorn', 'es' => 'Ardilla'],
['es' => 'Zorro', 'nl' => 'Vos'],
['nl' => 'Hert'],
['es' => 'Tejón'],
];
// cSpell:enable
foreach ($media_items as $translations) {
$default_langcode = key($translations);
$default_name = array_shift($translations);
$media = Media::create([
'name' => $default_name,
'bundle' => 'image',
'field_media_image' => $image,
'langcode' => $default_langcode,
]);
foreach ($translations as $langcode => $name) {
$media->addTranslation($langcode, ['name' => $name]);
}
$media->save();
}
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'access media overview',
'edit own article content',
'create article content',
'administer media',
]);
$this->drupalLogin($user);
}
/**
* Tests the media library widget shows all media only once.
*/
public function testMediaLibraryTranslations(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// All translations should be shown in the administration overview,
// regardless of the interface language.
$this->drupalGet('nl/admin/content/media-grid');
$assert_session->elementsCount('css', '.js-media-library-item', 6);
$media_items = $page->findAll('css', '.js-media-library-item-preview + div');
$media_names = [];
foreach ($media_items as $media_item) {
$media_names[] = $media_item->getText();
}
sort($media_names);
// cSpell:disable-next-line
$this->assertSame(['Ardilla', 'Eekhoorn', 'Hert', 'Tejón', 'Vos', 'Zorro'], $media_names);
$this->drupalGet('es/admin/content/media-grid');
$assert_session->elementsCount('css', '.js-media-library-item', 6);
$media_items = $page->findAll('css', '.js-media-library-item-preview + div');
$media_names = [];
foreach ($media_items as $media_item) {
$media_names[] = $media_item->getText();
}
sort($media_names);
// cSpell:disable-next-line
$this->assertSame(['Ardilla', 'Eekhoorn', 'Hert', 'Tejón', 'Vos', 'Zorro'], $media_names);
// All media should only be shown once, and should be shown in the interface
// language.
$this->drupalGet('nl/node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->waitForText('Add or select media');
$assert_session->elementsCount('css', '.js-media-library-item', 4);
$media_items = $page->findAll('css', '.js-media-library-item-preview + div');
$media_names = [];
foreach ($media_items as $media_item) {
$media_names[] = $media_item->getText();
}
sort($media_names);
// cSpell:disable-next-line
$this->assertSame(['Eekhoorn', 'Hert', 'Tejón', 'Vos'], $media_names);
$this->drupalGet('es/node/add/article');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_media"]')->click();
$assert_session->waitForText('Add or select media');
$assert_session->elementsCount('css', '.js-media-library-item', 4);
$media_items = $page->findAll('css', '.js-media-library-item-preview + div');
$media_names = [];
foreach ($media_items as $media_item) {
$media_names[] = $media_item->getText();
}
sort($media_names);
// cSpell:disable-next-line
$this->assertSame(['Ardilla', 'Hert', 'Tejón', 'Zorro'], $media_names);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
/**
* Tests Media Library's integration with Views UI.
*
* @group media_library
*/
class ViewsUiIntegrationTest extends MediaLibraryTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['views_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a few example media items for use in selection.
$this->createMediaItems([
'type_one' => [
'Horse',
'Bear',
'Cat',
'Dog',
],
'type_two' => [
'Crocodile',
'Lizard',
'Snake',
'Turtle',
],
]);
$account = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($account);
}
/**
* Tests that the integration with Views works correctly.
*/
public function testViewsAdmin(): void {
$page = $this->getSession()->getPage();
// Assert that the widget can be seen and that there are 8 items.
$this->drupalGet('/admin/structure/views/view/media_library/edit/widget');
$this->waitForElementsCount('css', '.js-media-library-item', 8);
// Assert that filtering works in live preview.
$page->find('css', '.js-media-library-view')->fillField('name', 'snake');
$page->find('css', '.js-media-library-view')->pressButton('Apply filters');
$this->waitForElementsCount('css', '.js-media-library-item', 1);
// Test the same routine but in the view for the table widget.
$this->drupalGet('/admin/structure/views/view/media_library/edit/widget_table');
$this->waitForElementsCount('css', '.js-media-library-item', 8);
// Assert that filtering works in live preview.
$page->find('css', '.js-media-library-view')->fillField('name', 'snake');
$page->find('css', '.js-media-library-view')->pressButton('Apply filters');
$this->waitForElementsCount('css', '.js-media-library-item', 1);
// We cannot test clicking the 'Insert selected' button in either view
// because we expect an AJAX error, which would always throw an exception
// on ::tearDown even if we try to catch it here. If there is an API for
// marking certain elements 'unsuitable for previewing', we could test that
// here.
// @see https://www.drupal.org/project/drupal/issues/3060852
}
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\media\Entity\Media;
use Drupal\media_library\MediaLibraryState;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the media library UI access.
*
* @group media_library
*/
class WidgetAccessTest extends MediaLibraryTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that the widget access works as expected.
*/
public function testWidgetAccess(): void {
$assert_session = $this->assertSession();
$session = $this->getSession();
$this->createMediaItems([
'type_one' => ['Horse', 'Bear'],
]);
$account = $this->drupalCreateUser(['create basic_page content']);
$this->drupalLogin($account);
// Assert users can not select media items they do not have access to.
$unpublished_media = Media::create([
'name' => 'Mosquito',
'bundle' => 'type_one',
'field_media_test' => 'Mosquito',
'status' => FALSE,
]);
$unpublished_media->save();
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
// Set the hidden value and trigger the mousedown event on the button via
// JavaScript since the field and button are hidden.
$session->executeScript("jQuery('[data-media-library-widget-value=\"field_unlimited_media\"]').val('1,2,{$unpublished_media->id()}')");
$session->executeScript("jQuery('[data-media-library-widget-update=\"field_unlimited_media\"]').trigger('mousedown')");
$this->assertElementExistsAfterWait('css', '.js-media-library-item');
// Assert the published items are selected and the unpublished item is not
// selected.
$assert_session->pageTextContains('Horse');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextNotContains('Mosquito');
$this->drupalLogout();
$role = Role::load(RoleInterface::ANONYMOUS_ID);
$role->revokePermission('view media');
$role->save();
// Create a working state.
$allowed_types = ['type_one', 'type_two', 'type_three', 'type_four'];
// The opener parameters are not relevant to the test, but the opener
// expects them to be there or it will deny access.
$state = MediaLibraryState::create('media_library.opener.field_widget', $allowed_types, 'type_three', 2, [
'entity_type_id' => 'node',
'bundle' => 'basic_page',
'field_name' => 'field_unlimited_media',
]);
$url_options = ['query' => $state->all()];
// Verify that unprivileged users can't access the widget view.
$this->drupalGet('admin/content/media-widget', $url_options);
$assert_session->responseContains('Access denied');
$this->drupalGet('admin/content/media-widget-table', $url_options);
$assert_session->responseContains('Access denied');
$this->drupalGet('media-library', $url_options);
$assert_session->responseContains('Access denied');
// Allow users with 'view media' permission to access the media library view
// and controller. Since we are using the node entity type in the state
// object, ensure the user also has permission to work with those.
$this->grantPermissions($role, [
'create basic_page content',
'view media',
]);
$this->drupalGet('admin/content/media-widget', $url_options);
$assert_session->elementExists('css', '.js-media-library-view');
$this->drupalGet('admin/content/media-widget-table', $url_options);
$assert_session->elementExists('css', '.js-media-library-view');
$this->drupalGet('media-library', $url_options);
$assert_session->elementExists('css', '.js-media-library-view');
// Assert the user does not have access to the media add form if the user
// does not have the 'create media' permission.
$assert_session->fieldNotExists('files[upload][]');
// Assert users can not access the widget displays of the media library view
// without a valid media library state.
$this->drupalGet('admin/content/media-widget');
$assert_session->responseContains('Access denied');
$this->drupalGet('admin/content/media-widget-table');
$assert_session->responseContains('Access denied');
$this->drupalGet('media-library');
$assert_session->responseContains('Access denied');
// Assert users with the 'create media' permission can access the media add
// form.
$this->grantPermissions($role, [
'create media',
]);
$this->drupalGet('media-library', $url_options);
$assert_session->elementExists('css', '.js-media-library-view');
$assert_session->fieldExists('Add files');
// Assert the media library can not be accessed if the required state
// parameters are changed without changing the hash.
$this->drupalGet('media-library', [
'query' => array_merge($url_options['query'], ['media_library_opener_id' => 'fail']),
]);
$assert_session->responseContains('Access denied');
$this->drupalGet('media-library', [
'query' => array_merge($url_options['query'], ['media_library_allowed_types' => ['type_one', 'type_two']]),
]);
$assert_session->responseContains('Access denied');
$this->drupalGet('media-library', [
'query' => array_merge($url_options['query'], ['media_library_selected_type' => 'type_one']),
]);
$assert_session->responseContains('Access denied');
$this->drupalGet('media-library', [
'query' => array_merge($url_options['query'], ['media_library_remaining' => 3]),
]);
$assert_session->responseContains('Access denied');
$this->drupalGet('media-library', [
'query' => array_merge($url_options['query'], ['hash' => 'fail']),
]);
$assert_session->responseContains('Access denied');
}
/**
* Tests the widget with a required field that the user can't access.
*/
public function testRequiredFieldNoAccess(): void {
// Make field_single_media_type required.
$fieldConfig = FieldConfig::loadByName('node', 'basic_page', 'field_single_media_type');
assert($fieldConfig instanceof FieldConfig);
$fieldConfig->setRequired(TRUE)
->save();
// Deny access to the field.
\Drupal::state()->set('media_library_test_entity_field_access_deny_fields', ['field_single_media_type']);
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create type_one media',
'view media',
]);
$this->drupalLogin($user);
$this->drupalGet('node/add/basic_page');
$this->assertSession()->elementNotExists('css', '.field--name-field-single-media-type');
$this->submitForm([
'title[0][value]' => $this->randomMachineName(),
], 'Save');
$this->assertSession()->elementNotExists('css', '.messages--error');
$this->assertSession()->pageTextNotContains('Single media type field is required.');
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests that the widget works as expected for anonymous users.
*
* @group media_library
*/
class WidgetAnonymousTest extends MediaLibraryTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a few example media items for use in selection.
$this->createMediaItems([
'type_one' => [
'Cat',
'Dog',
],
]);
// Allow the anonymous user to create pages and view media.
$role = Role::load(RoleInterface::ANONYMOUS_ID);
$this->grantPermissions($role, [
'access content',
'create basic_page content',
'view media',
]);
}
/**
* Tests that the widget works as expected for anonymous users.
*/
public function testWidgetAnonymous(): void {
$assert_session = $this->assertSession();
// Allow the anonymous user to create pages and view media.
$role = Role::load(RoleInterface::ANONYMOUS_ID);
$this->grantPermissions($role, [
'access content',
'create basic_page content',
'view media',
]);
// Ensure the widget works as an anonymous user.
$this->drupalGet('node/add/basic_page');
// Add to the unlimited cardinality field.
$this->openMediaLibraryForField('field_unlimited_media');
// Select the first media item (should be Dog).
$this->selectMediaItem(0);
$this->pressInsertSelected('Added one media item.');
// Ensure that the selection completed successfully.
$this->waitForText('Dog');
$assert_session->fieldNotExists('Weight');
// Add to the unlimited cardinality field.
$this->openMediaLibraryForField('field_unlimited_media');
// Select the second media item (should be Cat).
$this->selectMediaItem(1);
$this->pressInsertSelected('Added one media item.');
// Ensure that the selection completed successfully.
$this->waitForText('Cat');
// Save the form.
$assert_session->elementExists('css', '.js-media-library-widget-toggle-weight')->click();
$this->submitForm([
'title[0][value]' => 'My page',
'field_unlimited_media[selection][0][weight]' => '0',
'field_unlimited_media[selection][1][weight]' => '1',
], 'Save');
$assert_session->pageTextContains('Basic Page My page has been created');
$assert_session->pageTextContains('Dog');
}
}

View File

@@ -0,0 +1,440 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\media\Entity\Media;
use Drupal\media_test_oembed\Controller\ResourceController;
use Drupal\Tests\media\Traits\OEmbedTestTrait;
// cspell:ignore Drupalin Hustlin Schipulcon
/**
* Tests that oEmbed media can be added in the Media library's widget.
*
* @group media_library
*/
class WidgetOEmbedTest extends MediaLibraryTestBase {
use OEmbedTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['media_test_oembed'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->lockHttpClientToFixtures();
$this->hijackProviderEndpoints();
// Create a user who can use the Media library.
$user = $this->drupalCreateUser([
'access content',
'create basic_page content',
'view media',
'create media',
'administer media',
]);
$this->drupalLogin($user);
}
/**
* Tests that oEmbed media can be added in the Media library's widget.
*/
public function testWidgetOEmbed(): void {
$this->hijackProviderEndpoints();
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$youtube_title = "Everyday I'm Drupalin' Drupal Rap (Rick Ross - Hustlin)";
$youtube_url = 'https://www.youtube.com/watch?v=PWjcqE3QKBg';
$vimeo_title = "Drupal Rap Video - Schipulcon09";
$vimeo_url = 'https://vimeo.com/7073899';
ResourceController::setResourceUrl($youtube_url, $this->getFixturesDirectory() . '/video_youtube.json');
ResourceController::setResourceUrl($vimeo_url, $this->getFixturesDirectory() . '/video_vimeo.json');
ResourceController::setResource404('https://www.youtube.com/watch?v=PWjcqE3QKBg1');
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
// Add to the unlimited media field.
$this->openMediaLibraryForField('field_unlimited_media');
// Assert the default tab for media type one does not have an oEmbed form.
$assert_session->fieldNotExists('Add Type Five via URL');
// Assert other media types don't have the oEmbed form fields.
$this->switchToMediaType('Three');
$assert_session->fieldNotExists('Add Type Five via URL');
// Assert we can add an oEmbed video to media type five.
$this->switchToMediaType('Five');
$page->fillField('Add Type Five via URL', $youtube_url);
$assert_session->pageTextContains('Allowed providers: YouTube, Vimeo.');
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('The media item has been created but has not yet been saved.');
// There is no other selected media and this is not the advanced ui.
// Assert that the Additional selected media element does not appear.
$assert_session->pageTextNotContains('Additional selected media');
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-selection"]');
// Assert the name field contains the remote video title.
$assert_session->fieldValueEquals('Name', $youtube_title);
$this->pressSaveButton();
$this->waitForText('Add Type Five via URL');
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains($youtube_title);
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
// Assert the created oEmbed video is correctly added to the widget.
$this->pressInsertSelected('Added one media item.');
$this->waitForText($youtube_title);
// Open the media library again for the unlimited field and go to the tab
// for media type five.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
// Assert the video is available on the tab.
$assert_session->pageTextContains($youtube_title);
// Assert we can only add supported URLs.
$page->fillField('Add Type Five via URL', 'https://www.youtube.com/');
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('No matching provider found.');
// Assert we can not add a video ID that doesn't exist. We need to use a
// video ID that will not be filtered by the regex, because otherwise the
// message 'No matching provider found.' will be returned.
$page->fillField('Add Type Five via URL', 'https://www.youtube.com/watch?v=PWjcqE3QKBg1');
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('Could not retrieve the oEmbed resource.');
// Select a media item to check if the selection is persisted when adding
// new items.
$checkbox = $page->findField("Select $youtube_title");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->pageTextContains('1 item selected');
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
// Assert we can add a oEmbed video with a custom name.
$page->fillField('Add Type Five via URL', $youtube_url);
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('The media item has been created but has not yet been saved.');
$page->fillField('Name', 'Custom video title');
// The non-advanced ui should not show the Additional selected media.
$assert_session->pageTextNotContains('Additional selected media');
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-selection"]');
$this->pressSaveButton();
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains('Custom video title');
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
// Assert the item that was selected before uploading the file is still
// selected.
$assert_session->pageTextContains('2 items selected');
$assert_session->checkboxChecked("Select Custom video title");
$assert_session->checkboxChecked("Select $youtube_title");
$assert_session->hiddenFieldValueEquals('current_selection', implode(',', [$selected_item_id, $added_media_id]));
$selected_checkboxes = [];
foreach ($this->getCheckboxes() as $checkbox) {
if ($checkbox->isChecked()) {
$selected_checkboxes[] = $checkbox->getAttribute('value');
}
}
$this->assertCount(2, $selected_checkboxes);
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added 2 media items.');
$this->waitForText('Custom video title');
// Assert we can directly insert added oEmbed media in the widget.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
$page->fillField('Add Type Five via URL', $vimeo_url);
$page->pressButton('Add');
$this->waitForText('The media item has been created but has not yet been saved.');
$this->pressSaveButton();
$this->waitForText('Add or select media');
$this->pressInsertSelected();
$this->waitForText($vimeo_title);
// Assert we can remove selected items from the selection area in the oEmbed
// form.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
$checkbox = $page->findField("Select $vimeo_title");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
$page->fillField('Add Type Five via URL', $youtube_url);
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('The media item has been created but has not yet been saved');
$page->fillField('Name', 'Another video');
$this->pressSaveButton();
$page->uncheckField("media_library_select_form[$selected_item_id]");
$this->waitForText('1 item selected');
$this->pressInsertSelected('Added one media item.');
$this->waitForText('Another video');
// Assert removing an added oEmbed media item before save works as expected.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
$page->fillField('Add Type Five via URL', $youtube_url);
$page->pressButton('Add');
// Assert the media item fields are shown and the vertical tabs are no
// longer shown.
$this->assertMediaAdded();
// Press the 'Remove button' and assert the user is sent back to the media
// library.
$page->pressButton('media-0-remove-button');
// Assert the remove message is shown.
$this->waitForText("The media item $youtube_title has been removed.");
$this->assertNoMediaAdded();
}
/**
* Tests that oEmbed media can be added in the widget's advanced UI.
*
* @todo Merge this with testWidgetOEmbed() in
* https://www.drupal.org/project/drupal/issues/3087227
*/
public function testWidgetOEmbedAdvancedUi(): void {
$this->config('media_library.settings')->set('advanced_ui', TRUE)->save();
$this->hijackProviderEndpoints();
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$youtube_title = "Everyday I'm Drupalin' Drupal Rap (Rick Ross - Hustlin)";
$youtube_url = 'https://www.youtube.com/watch?v=PWjcqE3QKBg';
$vimeo_title = "Drupal Rap Video - Schipulcon09";
$vimeo_url = 'https://vimeo.com/7073899';
ResourceController::setResourceUrl($youtube_url, $this->getFixturesDirectory() . '/video_youtube.json');
ResourceController::setResourceUrl($vimeo_url, $this->getFixturesDirectory() . '/video_vimeo.json');
ResourceController::setResource404('https://www.youtube.com/watch?v=PWjcqE3QKBg1');
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
// Add to the unlimited media field.
$this->openMediaLibraryForField('field_unlimited_media');
// Assert the default tab for media type one does not have an oEmbed form.
$assert_session->fieldNotExists('Add Type Five via URL');
// Assert other media types don't have the oEmbed form fields.
$this->switchToMediaType('Three');
$assert_session->fieldNotExists('Add Type Five via URL');
// Assert we can add an oEmbed video to media type five.
$this->switchToMediaType('Five');
$page->fillField('Add Type Five via URL', $youtube_url);
$assert_session->pageTextContains('Allowed providers: YouTube, Vimeo.');
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('The media item has been created but has not yet been saved.');
// Assert that Additional selected media does not appear.
$assert_session->pageTextNotContains('Additional selected media');
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-selection"]');
// Assert the name field contains the remote video title.
$assert_session->fieldValueEquals('Name', $youtube_title);
$this->saveAnd('select');
$this->waitForText('Add Type Five via URL');
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains($youtube_title);
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
// Assert the created oEmbed video is correctly added to the widget.
$this->pressInsertSelected('Added one media item.');
$this->waitForText($youtube_title);
// Open the media library again for the unlimited field and go to the tab
// for media type five.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
// Assert the video is available on the tab.
$assert_session->pageTextContains($youtube_title);
// Assert we can only add supported URLs.
$page->fillField('Add Type Five via URL', 'https://www.youtube.com/');
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('No matching provider found.');
// Assert we can not add a video ID that doesn't exist. We need to use a
// video ID that will not be filtered by the regex, because otherwise the
// message 'No matching provider found.' will be returned.
$page->fillField('Add Type Five via URL', 'https://www.youtube.com/watch?v=PWjcqE3QKBg1');
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('Could not retrieve the oEmbed resource.');
// Select a media item to check if the selection is persisted when adding
// new items.
$checkbox = $page->findField("Select $youtube_title");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->pageTextContains('1 item selected');
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
// Assert we can add a oEmbed video with a custom name.
$page->fillField('Add Type Five via URL', $youtube_url);
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('The media item has been created but has not yet been saved.');
// The advanced ui should show the Additional selected media.
$assert_session->pageTextContains('Additional selected media');
$assert_session->elementExists('css', '[data-drupal-selector="edit-selection"]');
$page->fillField('Name', 'Custom video title');
$assert_session->checkboxChecked("Select $youtube_title", $this->getSelectionArea());
$this->saveAnd('select');
$this->waitForNoText('Save and select');
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains('Custom video title');
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
// Assert the item that was selected before uploading the file is still
// selected.
$assert_session->pageTextContains('2 items selected');
$assert_session->checkboxChecked("Select Custom video title");
$assert_session->checkboxChecked("Select $youtube_title");
$assert_session->hiddenFieldValueEquals('current_selection', implode(',', [$selected_item_id, $added_media_id]));
$selected_checkboxes = [];
foreach ($this->getCheckboxes() as $checkbox) {
if ($checkbox->isChecked()) {
$selected_checkboxes[] = $checkbox->getAttribute('value');
}
}
$this->assertCount(2, $selected_checkboxes);
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added 2 media items.');
$this->waitForText('Custom video title');
// Assert we can directly insert added oEmbed media in the widget.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
$page->fillField('Add Type Five via URL', $vimeo_url);
$page->pressButton('Add');
$this->waitForText('The media item has been created but has not yet been saved.');
$this->saveAnd('insert');
$this->waitForText('Added one media item.');
$this->waitForNoText('Add or select media');
$this->waitForText($vimeo_title);
// Assert we can remove selected items from the selection area in the oEmbed
// form.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
$checkbox = $page->findField("Select $vimeo_title");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
$page->fillField('Add Type Five via URL', $youtube_url);
$page->pressButton('Add');
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$this->waitForText('The media item has been created but has not yet been saved');
$page->fillField('Name', 'Another video');
$selection_area = $this->getSelectionArea();
$assert_session->checkboxChecked("Select $vimeo_title", $selection_area);
$page->uncheckField("Select $vimeo_title");
$assert_session->hiddenFieldValueEquals('current_selection', '');
// Close the details element so that clicking the Save and select works.
// @todo Fix dialog or test so this is not necessary to prevent random
// fails. https://www.drupal.org/project/drupal/issues/3055648
$selection_area->find('css', 'summary')->click();
$this->saveAnd('select');
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
$this->waitForText('1 item selected');
$assert_session->checkboxChecked('Select Another video');
$assert_session->checkboxNotChecked("Select $vimeo_title");
$assert_session->hiddenFieldValueEquals('current_selection', $added_media_id);
$this->pressInsertSelected('Added one media item.');
$this->waitForText('Another video');
// Assert removing an added oEmbed media item before save works as expected.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Five');
$page->fillField('Add Type Five via URL', $youtube_url);
$page->pressButton('Add');
// Assert the media item fields are shown and the vertical tabs are no
// longer shown.
$this->assertMediaAdded();
// Press the 'Remove button' and assert the user is sent back to the media
// library.
$page->pressButton('media-0-remove-button');
// Assert the remove message is shown.
$this->waitForText("The media item $youtube_title has been removed.");
$this->assertNoMediaAdded();
}
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests that uploads in the 'media_library_widget' works as expected.
*
* @group media_library
*
* @todo This test will occasionally fail with SQLite until
* https://www.drupal.org/node/3066447 is addressed.
*/
class WidgetOverflowTest extends MediaLibraryTestBase {
use TestFileCreationTrait;
/**
* The test image file.
*
* @var \Drupal\file\Entity\File
*/
protected $image;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
foreach ($this->getTestFiles('image') as $image) {
$extension = pathinfo($image->filename, PATHINFO_EXTENSION);
if ($extension === 'png') {
$this->image = $image;
}
}
if (!isset($this->image)) {
$this->fail('Expected test files not present.');
}
// Create a user that can only add media of type four.
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create type_one media',
'create type_three media',
'view media',
]);
$this->drupalLogin($user);
}
/**
* Uploads multiple test images into the media library.
*
* @param int $number
* The number of images to upload.
*/
private function uploadFiles(int $number): void {
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = $this->container->get('file_system');
// Create a list of new files to upload.
$filenames = $remote_paths = [];
for ($i = 0; $i < $number; $i++) {
$path = $file_system->copy($this->image->uri, 'public://');
$path = $file_system->realpath($path);
$this->assertNotEmpty($path);
$this->assertFileExists($path);
$filenames[] = $file_system->basename($path);
$remote_paths[] = $this->getSession()
->getDriver()
->uploadFileAndGetRemoteFilePath($path);
}
$page = $this->getSession()->getPage();
$page->fillField('Add files', implode("\n", $remote_paths));
$this->assertMediaAdded();
$assert_session = $this->assertSession();
foreach ($filenames as $i => $filename) {
$assert_session->fieldValueEquals("media[$i][fields][name][0][value]", $filename);
$page->fillField("media[$i][fields][field_media_test_image][0][alt]", $filename);
}
}
/**
* Tests that the Media Library constrains the number of selected items.
*
* @param string|null $selected_operation
* The operation of the button to click. For example, if this is "insert",
* the "Save and insert" button will be pressed. If NULL, the "Save" button
* will be pressed.
*
* @dataProvider providerWidgetOverflow
*/
public function testWidgetOverflow(?string $selected_operation): void {
// If we want to press the "Save and insert" or "Save and select" buttons,
// we need to enable the advanced UI.
if ($selected_operation) {
$this->config('media_library.settings')->set('advanced_ui', TRUE)->save();
}
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('node/add/basic_page');
// Upload 5 files into a media field that only allows 2.
$this->openMediaLibraryForField('field_twin_media');
$this->uploadFiles(5);
// Save the media items and ensure that the user is warned that they have
// selected too many items.
if ($selected_operation) {
$this->saveAnd($selected_operation);
}
else {
$this->pressSaveButton();
}
$this->waitForElementTextContains('.messages--warning', 'There are currently 5 items selected. The maximum number of items for the field is 2. Remove 3 items from the selection.');
// If the user tries to insert the selected items anyway, they should get
// an error.
$this->pressInsertSelected(NULL, FALSE);
$this->waitForElementTextContains('.messages--error', 'There are currently 5 items selected. The maximum number of items for the field is 2. Remove 3 items from the selection.');
$assert_session->elementNotExists('css', '.messages--warning');
// Once the extra items are deselected, all should be well.
$this->deselectMediaItem(2);
$this->deselectMediaItem(3);
$this->deselectMediaItem(4);
$this->pressInsertSelected('Added 2 media items.');
}
/**
* Tests that unlimited fields' selection count is not constrained.
*
* @param string|null $selected_operation
* The operation of the button to click. For example, if this is "insert",
* the "Save and insert" button will be pressed. If NULL, the "Save" button
* will be pressed.
*
* @dataProvider providerWidgetOverflow
*/
public function testUnlimitedCardinality(?string $selected_operation): void {
if ($selected_operation) {
$this->config('media_library.settings')->set('advanced_ui', TRUE)->save();
}
$assert_session = $this->assertSession();
// Visit a node create page and open the media library.
$this->drupalGet('node/add/basic_page');
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
$this->uploadFiles(5);
if ($selected_operation) {
$this->saveAnd($selected_operation);
}
else {
$this->pressSaveButton();
}
if ($selected_operation !== 'insert') {
$this->pressInsertSelected();
}
// There should not be any warnings or errors.
$assert_session->elementNotExists('css', '.messages--error');
$assert_session->elementNotExists('css', '.messages--warning');
$this->waitForText('Added 5 media items.');
}
/**
* Data provider for ::testWidgetOverflow() and ::testUnlimitedCardinality().
*
* @return array[]
* Sets of arguments to pass to the test method.
*/
public static function providerWidgetOverflow(): array {
return [
'Save' => [NULL],
'Save and insert' => ['insert'],
'Save and select' => ['select'],
];
}
}

View File

@@ -0,0 +1,763 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\media\Entity\Media;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests that uploads in the 'media_library_widget' works as expected.
*
* @group media_library
*
* @todo This test will occasionally fail with SQLite until
* https://www.drupal.org/node/3066447 is addressed.
*/
class WidgetUploadTest extends MediaLibraryTestBase {
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that uploads in the 'media_library_widget' works as expected.
*/
public function testWidgetUpload(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$driver = $this->getSession()->getDriver();
foreach ($this->getTestFiles('image') as $image) {
$extension = pathinfo($image->filename, PATHINFO_EXTENSION);
if ($extension === 'png') {
$png_image = $image;
}
elseif ($extension === 'jpg') {
$jpg_image = $image;
}
}
if (!isset($png_image) || !isset($jpg_image)) {
$this->fail('Expected test files not present.');
}
// Create a user that can only add media of type four.
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create type_one media',
'create type_four media',
'view media',
]);
$this->drupalLogin($user);
// Visit a node create page and open the media library.
$this->drupalGet('node/add/basic_page');
$this->openMediaLibraryForField('field_twin_media');
// Assert the upload form is not visible for default tab type_three without
// the proper permissions.
$assert_session->elementNotExists('css', '.js-media-library-add-form');
// Assert the upload form is not visible for the non-file based media type
// type_one.
$this->switchToMediaType('One');
$assert_session->elementNotExists('css', '.js-media-library-add-form');
// Assert the upload form is visible for type_four.
$this->switchToMediaType('Four');
$assert_session->fieldExists('Add files');
$assert_session->pageTextContains('Maximum 2 files.');
// Create a user that can create media for all media types.
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create media',
'view media',
]);
$this->drupalLogin($user);
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
$file_storage = $this->container->get('entity_type.manager')->getStorage('file');
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = $this->container->get('file_system');
// Add to the twin media field.
$this->openMediaLibraryForField('field_twin_media');
// Assert the upload form is now visible for default tab type_three.
$assert_session->elementExists('css', '.js-media-library-add-form');
$assert_session->fieldExists('Add files');
// Assert we can upload a file to the default tab type_three.
$assert_session->elementNotExists('css', '.js-media-library-add-form[data-input]');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
$this->assertMediaAdded();
$assert_session->elementExists('css', '.js-media-library-add-form[data-input]');
// We do not have pre-selected items, so the container should not be added
// to the form.
$assert_session->pageTextNotContains('Additional selected media');
// Files are temporary until the form is saved.
$files = $file_storage->loadMultiple();
$file = array_pop($files);
$this->assertSame('public://type-three-dir', $file_system->dirname($file->getFileUri()));
$this->assertTrue($file->isTemporary());
// Assert the revision_log_message field is not shown.
$upload_form = $assert_session->elementExists('css', '.js-media-library-add-form');
$assert_session->fieldNotExists('Revision log message', $upload_form);
// Assert the name field contains the filename and the alt text is required.
$assert_session->fieldValueEquals('Name', $png_image->filename);
$this->pressSaveButton(TRUE);
$this->waitForText('Alternative text field is required');
$page->fillField('Alternative text', $this->randomString());
$this->pressSaveButton();
$this->assertJsCondition('jQuery("input[name=\'media_library_select_form[1]\']").is(":focus")');
// The file should be permanent now.
$files = $file_storage->loadMultiple();
$file = array_pop($files);
$this->assertFalse($file->isTemporary());
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains($png_image->filename);
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
$assert_session->pageTextContains('1 of 2 items selected');
$assert_session->hiddenFieldValueEquals('current_selection', $added_media_id);
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added one media item.');
$this->waitForText($png_image->filename);
// Remove the item.
$assert_session->elementTextContains('css', '.field--name-field-twin-media', $png_image->filename);
$assert_session->elementExists('css', '.field--name-field-twin-media')->pressButton('Remove');
$this->waitForElementTextContains('#drupal-live-announce', $png_image->filename . ' has been removed');
$assert_session->elementTextNotContains('css', '.field--name-field-twin-media', $png_image->filename);
$this->openMediaLibraryForField('field_twin_media');
$this->switchToMediaType('Three');
$png_uri_2 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_2));
$this->waitForFieldExists('Alternative text')->setValue($this->randomString());
$this->pressSaveButton();
$this->pressInsertSelected('Added one media item.');
$this->waitForText($file_system->basename($png_uri_2));
// Also make sure that we can upload to the unlimited cardinality field.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
// Select a media item to check if the selection is persisted when adding
// new items.
$existing_media_name = $file_system->basename($png_uri_2);
$checkbox = $page->findField("Select $existing_media_name");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->pageTextContains('1 item selected');
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
$png_uri_3 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_3));
$this->waitForText('The media item has been created but has not yet been saved.');
$page->fillField('Name', 'Unlimited Cardinality Image');
$page->fillField('Alternative text', $this->randomString());
$this->pressSaveButton();
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_name = $added_media->label();
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains('Unlimited Cardinality Image');
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
// Assert the item that was selected before uploading the file is still
// selected.
$assert_session->pageTextContains('2 items selected');
$assert_session->checkboxChecked("Select $added_media_name");
$assert_session->checkboxChecked("Select $existing_media_name");
$assert_session->hiddenFieldValueEquals('current_selection', implode(',', [$selected_item_id, $added_media_id]));
$selected_checkboxes = [];
foreach ($this->getCheckboxes() as $checkbox) {
if ($checkbox->isChecked()) {
$selected_checkboxes[] = $checkbox->getAttribute('value');
}
}
$this->assertCount(2, $selected_checkboxes);
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added 2 media items.');
$this->waitForText('Unlimited Cardinality Image');
// Assert we can now only upload one more media item.
$this->openMediaLibraryForField('field_twin_media');
$this->switchToMediaType('Four');
// We set the multiple to FALSE if only one file can be uploaded
$this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple'));
$assert_session->pageTextContains('One file only.');
$choose_files = $assert_session->elementExists('css', '.form-managed-file');
$choose_files->hasButton('Choose file');
$this->assertFalse($choose_files->hasButton('Choose files'));
// Assert media type four should only allow jpg files by trying a png file
// first.
$png_uri_4 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add file', $file_system->realpath($png_uri_4));
$this->waitForText('Only files with the following extensions are allowed');
// Assert that jpg files are accepted by type four.
$jpg_uri_2 = $file_system->copy($jpg_image->uri, 'public://');
$this->addMediaFileToField('Add file', $file_system->realpath($jpg_uri_2));
$this->waitForFieldExists('Alternative text')->setValue($this->randomString());
// The type_four media type has another optional image field.
$assert_session->pageTextContains('Extra Image');
$jpg_uri_3 = $file_system->copy($jpg_image->uri, 'public://');
$this->addMediaFileToField('Extra Image', $this->container->get('file_system')->realpath($jpg_uri_3));
$this->waitForText($file_system->basename($jpg_uri_3));
// Ensure that the extra image was uploaded to the correct directory.
$files = $file_storage->loadMultiple();
$file = array_pop($files);
$this->assertSame('public://type-four-extra-dir', $file_system->dirname($file->getFileUri()));
$this->pressSaveButton();
// Ensure the media item was saved to the library and automatically
// selected.
$this->waitForText('Add or select media');
$this->waitForText($file_system->basename($jpg_uri_2));
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added one media item.');
$assert_session->pageTextContains($file_system->basename($jpg_uri_2));
// Assert we can also remove selected items from the selection area in the
// upload form.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
$checkbox = $page->findField("Select $existing_media_name");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
$this->assertTrue($assert_session->fieldExists('Add files')->hasAttribute('multiple'));
$png_uri_5 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_5));
// assertWaitOnAjaxRequest() required for input "id" attributes to
// consistently match their label's "for" attribute.
$assert_session->assertWaitOnAjaxRequest();
$page->fillField('Alternative text', $this->randomString());
$this->pressSaveButton();
$page->uncheckField('media_library_select_form[2]');
$this->waitForText('1 item selected');
$this->waitForText("Select $existing_media_name");
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_name = $added_media->label();
$added_media_id = $added_media->id();
$assert_session->pageTextContains('1 item selected');
$assert_session->checkboxChecked("Select $added_media_name");
$assert_session->checkboxNotChecked("Select $existing_media_name");
$assert_session->hiddenFieldValueEquals('current_selection', $added_media_id);
$this->pressInsertSelected('Added one media item.');
$this->waitForText($file_system->basename($png_uri_5));
// Assert removing an uploaded media item before save works as expected.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
// Assert the media item fields are shown and the vertical tabs are no
// longer shown.
$this->waitForFieldExists('Alternative text');
$this->assertMediaAdded();
// Press the 'Remove button' and assert the user is sent back to the media
// library.
$page->pressButton('media-0-remove-button');
// Assert the remove message is shown.
$this->waitForText("The media item $png_image->filename has been removed.");
// Assert the focus is shifted to the first tabbable element of the add
// form, which should be the source field.
$this->assertNoMediaAdded();
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert uploading multiple files.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
// Assert the existing items are remembered when adding and removing media.
$checkbox = $page->findField("Select $existing_media_name");
$checkbox->click();
// Assert we can add multiple files.
$this->assertTrue($assert_session->fieldExists('Add files')->hasAttribute('multiple'));
// Create a list of new files to upload.
$filenames = [];
$remote_paths = [];
foreach (range(1, 4) as $i) {
$path = $file_system->copy($png_image->uri, 'public://');
$filenames[] = $file_system->basename($path);
$remote_paths[] = $driver->uploadFileAndGetRemoteFilePath($file_system->realpath($path));
}
$page->findField('Add files')->setValue(implode("\n", $remote_paths));
// Assert the media item fields are shown and the vertical tabs are no
// longer shown.
$this->assertMediaAdded();
// Assert all files have been added.
$assert_session->fieldValueEquals('media[0][fields][name][0][value]', $filenames[0]);
$assert_session->fieldValueEquals('media[1][fields][name][0][value]', $filenames[1]);
$assert_session->fieldValueEquals('media[2][fields][name][0][value]', $filenames[2]);
$assert_session->fieldValueEquals('media[3][fields][name][0][value]', $filenames[3]);
// Set alt texts for items 1 and 2, leave the alt text empty for items 3
// and 4 to assert the field validation does not stop users from removing
// items.
$page->fillField('media[0][fields][field_media_test_image][0][alt]', $filenames[0]);
$page->fillField('media[1][fields][field_media_test_image][0][alt]', $filenames[1]);
// Assert the file is available in the file storage.
$files = $file_storage->loadByProperties(['filename' => $filenames[1]]);
$this->assertCount(1, $files);
$file_1_uri = reset($files)->getFileUri();
// Remove the second file and assert the focus is shifted to the container
// of the next media item and field values are still correct.
$page->pressButton('media-1-remove-button');
$this->assertTrue($assert_session->waitForText('The media item ' . $filenames[1] . ' has been removed.'));
$this->assertJsCondition('jQuery("[data-media-library-added-delta=2]").is(":focus")');
// Assert the file was deleted.
$this->assertEmpty($file_storage->loadByProperties(['filename' => $filenames[1]]));
$this->assertFileDoesNotExist($file_1_uri);
// When a file is already in usage, it should not be deleted. To test,
// let's add a usage for $filenames[3] (now in the third position).
$files = $file_storage->loadByProperties(['filename' => $filenames[3]]);
$this->assertCount(1, $files);
$target_file = reset($files);
Media::create([
'bundle' => 'type_three',
'name' => 'Disturbing',
'field_media_test_image' => [
['target_id' => $target_file->id()],
],
])->save();
// Remove $filenames[3] (now in the third position) and assert the focus is
// shifted to the container of the previous media item and field values are
// still correct.
$page->pressButton('media-3-remove-button');
$this->assertTrue($assert_session->waitForText('The media item ' . $filenames[3] . ' has been removed.'));
// Assert the file was not deleted, due to being in use elsewhere.
$this->assertNotEmpty($file_storage->loadByProperties(['filename' => $filenames[3]]));
$this->assertFileExists($target_file->getFileUri());
// The second media item should be removed (this has the delta 1 since we
// start counting from 0).
$assert_session->elementNotExists('css', '[data-media-library-added-delta=1]');
$media_item_one = $assert_session->elementExists('css', '[data-media-library-added-delta=0]');
$assert_session->fieldValueEquals('Name', $filenames[0], $media_item_one);
$assert_session->fieldValueEquals('Alternative text', $filenames[0], $media_item_one);
$media_item_three = $assert_session->elementExists('css', '[data-media-library-added-delta=2]');
$assert_session->fieldValueEquals('Name', $filenames[2], $media_item_three);
$assert_session->fieldValueEquals('Alternative text', '', $media_item_three);
}
/**
* Tests that uploads in the widget's advanced UI works as expected.
*
* @todo Merge this with testWidgetUpload() in
* https://www.drupal.org/project/drupal/issues/3087227
*/
public function testWidgetUploadAdvancedUi(): void {
$this->config('media_library.settings')->set('advanced_ui', TRUE)->save();
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$driver = $this->getSession()->getDriver();
foreach ($this->getTestFiles('image') as $image) {
$extension = pathinfo($image->filename, PATHINFO_EXTENSION);
if ($extension === 'png') {
$png_image = $image;
}
elseif ($extension === 'jpg') {
$jpg_image = $image;
}
}
if (!isset($png_image) || !isset($jpg_image)) {
$this->fail('Expected test files not present.');
}
// Create a user that can only add media of type four.
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create type_one media',
'create type_four media',
'view media',
]);
$this->drupalLogin($user);
// Visit a node create page and open the media library.
$this->drupalGet('node/add/basic_page');
$this->openMediaLibraryForField('field_twin_media');
// Assert the upload form is not visible for default tab type_three without
// the proper permissions.
$assert_session->elementNotExists('css', '.js-media-library-add-form');
// Assert the upload form is not visible for the non-file based media type
// type_one.
$this->switchToMediaType('One');
$assert_session->elementNotExists('css', '.js-media-library-add-form');
// Assert the upload form is visible for type_four.
$this->switchToMediaType('Four');
$assert_session->fieldExists('Add files');
$assert_session->pageTextContains('Maximum 2 files.');
// Create a user that can create media for all media types.
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create media',
'view media',
]);
$this->drupalLogin($user);
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
$file_storage = $this->container->get('entity_type.manager')->getStorage('file');
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = $this->container->get('file_system');
// Add to the twin media field.
$this->openMediaLibraryForField('field_twin_media');
// Assert the upload form is now visible for default tab type_three.
$assert_session->elementExists('css', '.js-media-library-add-form');
$assert_session->fieldExists('Add files');
// Assert we can upload a file to the default tab type_three.
$assert_session->elementNotExists('css', '.js-media-library-add-form[data-input]');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
$this->assertMediaAdded();
$assert_session->elementExists('css', '.js-media-library-add-form[data-input]');
// We do not have a pre-selected items, so the container should not be added
// to the form.
$assert_session->elementNotExists('css', 'details summary:contains(Additional selected media)');
// Files are temporary until the form is saved.
$files = $file_storage->loadMultiple();
$file = array_pop($files);
$this->assertSame('public://type-three-dir', $file_system->dirname($file->getFileUri()));
$this->assertTrue($file->isTemporary());
// Assert the revision_log_message field is not shown.
$upload_form = $assert_session->elementExists('css', '.js-media-library-add-form');
$assert_session->fieldNotExists('Revision log message', $upload_form);
// Assert the name field contains the filename and the alt text is required.
$assert_session->fieldValueEquals('Name', $png_image->filename);
$this->saveAnd('select');
$this->waitForText('Alternative text field is required');
$page->fillField('Alternative text', $this->randomString());
$this->saveAnd('select');
$this->assertJsCondition('jQuery("input[name=\'media_library_select_form[1]\']").is(":focus")');
// The file should be permanent now.
$files = $file_storage->loadMultiple();
$file = array_pop($files);
$this->assertFalse($file->isTemporary());
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains($png_image->filename);
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
$assert_session->pageTextContains('1 of 2 items selected');
$assert_session->hiddenFieldValueEquals('current_selection', $added_media_id);
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added one media item.');
$this->waitForText($png_image->filename);
// Remove the item.
$assert_session->elementTextContains('css', '.field--name-field-twin-media', $png_image->filename);
$assert_session->elementExists('css', '.field--name-field-twin-media')->pressButton('Remove');
$this->waitForElementTextContains('#drupal-live-announce', $png_image->filename . ' has been removed');
$assert_session->elementTextNotContains('css', '.field--name-field-twin-media', $png_image->filename);
$this->openMediaLibraryForField('field_twin_media');
$this->switchToMediaType('Three');
$png_uri_2 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_2));
$this->waitForFieldExists('Alternative text')->setValue($this->randomString());
// Assert we can also directly insert uploaded files in the widget.
$this->saveAnd('insert');
$this->waitForText('Added one media item.');
$this->waitForNoText('Add or select media');
$this->waitForText($file_system->basename($png_uri_2));
// Also make sure that we can upload to the unlimited cardinality field.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
// Select a media item to check if the selection is persisted when adding
// new items.
$existing_media_name = $file_system->basename($png_uri_2);
$checkbox = $page->findField("Select $existing_media_name");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->pageTextContains('1 item selected');
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
$png_uri_3 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_3));
$this->waitForText('The media item has been created but has not yet been saved.');
$assert_session->checkboxChecked("Select $existing_media_name");
$page->fillField('Name', 'Unlimited Cardinality Image');
$page->fillField('Alternative text', $this->randomString());
$this->saveAnd('select');
$this->waitForNoText('Save and select');
// Load the created media item.
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_name = $added_media->label();
$added_media_id = $added_media->id();
// Ensure the media item was saved to the library and automatically
// selected. The added media items should be in the first position of the
// add form.
$assert_session->pageTextContains('Add or select media');
$assert_session->pageTextContains('Unlimited Cardinality Image');
$assert_session->fieldValueEquals("media_library_select_form[$added_media_id]", $added_media_id);
$assert_session->checkboxChecked("media_library_select_form[$added_media_id]");
// Assert the item that was selected before uploading the file is still
// selected.
$assert_session->pageTextContains('2 items selected');
$assert_session->checkboxChecked("Select $added_media_name");
$assert_session->checkboxChecked("Select $existing_media_name");
$assert_session->hiddenFieldValueEquals('current_selection', implode(',', [$selected_item_id, $added_media_id]));
$selected_checkboxes = [];
foreach ($this->getCheckboxes() as $checkbox) {
if ($checkbox->isChecked()) {
$selected_checkboxes[] = $checkbox->getAttribute('value');
}
}
$this->assertCount(2, $selected_checkboxes);
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added 2 media items.');
$this->waitForText('Unlimited Cardinality Image');
// Assert we can now only upload one more media item.
$this->openMediaLibraryForField('field_twin_media');
$this->switchToMediaType('Four');
// We set the multiple to FALSE if only one file can be uploaded
$this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple'));
$assert_session->pageTextContains('One file only.');
$choose_files = $assert_session->elementExists('css', '.form-managed-file');
$choose_files->hasButton('Choose file');
$this->assertFalse($choose_files->hasButton('Choose files'));
// Assert media type four should only allow jpg files by trying a png file
// first.
$png_uri_4 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add file', $file_system->realpath($png_uri_4));
$this->waitForText('Only files with the following extensions are allowed');
// Assert that jpg files are accepted by type four.
$jpg_uri_2 = $file_system->copy($jpg_image->uri, 'public://');
$this->addMediaFileToField('Add file', $file_system->realpath($jpg_uri_2));
$this->waitForFieldExists('Alternative text')->setValue($this->randomString());
// The type_four media type has another optional image field.
$assert_session->pageTextContains('Extra Image');
$jpg_uri_3 = $file_system->copy($jpg_image->uri, 'public://');
$this->addMediaFileToField('Extra Image', $this->container->get('file_system')->realpath($jpg_uri_3));
$this->waitForText($file_system->basename($jpg_uri_3));
// Ensure that the extra image was uploaded to the correct directory.
$files = $file_storage->loadMultiple();
$file = array_pop($files);
$this->assertSame('public://type-four-extra-dir', $file_system->dirname($file->getFileUri()));
$this->saveAnd('select');
// Ensure the media item was saved to the library and automatically
// selected.
$this->waitForText('Add or select media');
$this->waitForText($file_system->basename($jpg_uri_2));
// Ensure the created item is added in the widget.
$this->pressInsertSelected('Added one media item.');
$assert_session->pageTextContains($file_system->basename($jpg_uri_2));
// Assert users can not select media items they do not have access to.
$unpublished_media = Media::create([
'name' => 'Mosquito',
'bundle' => 'type_one',
'field_media_test' => 'Mosquito',
'status' => FALSE,
]);
$unpublished_media->save();
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
// Set the hidden field with the current selection via JavaScript and upload
// a file.
$this->getSession()->executeScript("jQuery('.js-media-library-add-form-current-selection').val('1,2,{$unpublished_media->id()}')");
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_3));
$this->assertMediaAdded();
// Assert the pre-selected items are shown.
$this->getSelectionArea();
// Assert the published items are selected and the unpublished item is not
// selected.
$this->waitForText(Media::load(1)->label());
$this->waitForText(Media::load(2)->label());
$assert_session->pageTextNotContains('Mosquito');
$page->find('css', '.ui-dialog-titlebar-close')->click();
// Assert we can also remove selected items from the selection area in the
// upload form.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
$checkbox = $page->findField("Select $existing_media_name");
$selected_item_id = $checkbox->getAttribute('value');
$checkbox->click();
$assert_session->hiddenFieldValueEquals('current_selection', $selected_item_id);
$this->assertTrue($assert_session->fieldExists('Add files')->hasAttribute('multiple'));
$png_uri_5 = $file_system->copy($png_image->uri, 'public://');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_5));
$this->assertMediaAdded();
$page->fillField('Alternative text', $this->randomString());
// Assert the pre-selected items are shown.
$selection_area = $this->getSelectionArea();
$assert_session->checkboxChecked("Select $existing_media_name", $selection_area);
$selection_area->uncheckField("Select $existing_media_name");
$page->waitFor(10, function () use ($page) {
return $page->find('hidden_field_selector', ['hidden_field', 'current_selection'])->getValue() === '';
});
// Close the details element so that clicking the Save and select works.
// @todo Fix dialog or test so this is not necessary to prevent random
// fails. https://www.drupal.org/project/drupal/issues/3055648
$selection_area->find('css', 'summary')->click();
$this->saveAnd('select');
$this->waitForText("Select $existing_media_name");
$media_items = Media::loadMultiple();
$added_media = array_pop($media_items);
$added_media_name = $added_media->label();
$added_media_id = $added_media->id();
$assert_session->pageTextContains('1 item selected');
$assert_session->checkboxChecked("Select $added_media_name");
$assert_session->checkboxNotChecked("Select $existing_media_name");
$assert_session->hiddenFieldValueEquals('current_selection', $added_media_id);
$this->pressInsertSelected('Added one media item.');
$this->waitForText($file_system->basename($png_uri_5));
// Assert removing an uploaded media item before save works as expected.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
$this->addMediaFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
// Assert the media item fields are shown and the vertical tabs are no
// longer shown.
$this->assertMediaAdded();
// Press the 'Remove button' and assert the user is sent back to the media
// library.
$page->pressButton('media-0-remove-button');
// Assert the remove message is shown.
$this->waitForText("The media item $png_image->filename has been removed.");
$this->assertNoMediaAdded();
$assert_session->elementExists('css', '.ui-dialog-titlebar-close')->click();
// Assert uploading multiple files.
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
// Assert the existing items are remembered when adding and removing media.
$checkbox = $page->findField("Select $existing_media_name");
$checkbox->click();
// Assert we can add multiple files.
$this->assertTrue($assert_session->fieldExists('Add files')->hasAttribute('multiple'));
// Create a list of new files to upload.
$filenames = [];
$remote_paths = [];
foreach (range(1, 4) as $i) {
$path = $file_system->copy($png_image->uri, 'public://');
$filenames[] = $file_system->basename($path);
$remote_paths[] = $driver->uploadFileAndGetRemoteFilePath($file_system->realpath($path));
}
$page->findField('Add files')->setValue(implode("\n", $remote_paths));
// Assert the media item fields are shown and the vertical tabs are no
// longer shown.
$this->assertMediaAdded();
// Assert all files have been added.
$assert_session->fieldValueEquals('media[0][fields][name][0][value]', $filenames[0]);
$assert_session->fieldValueEquals('media[1][fields][name][0][value]', $filenames[1]);
$assert_session->fieldValueEquals('media[2][fields][name][0][value]', $filenames[2]);
$assert_session->fieldValueEquals('media[3][fields][name][0][value]', $filenames[3]);
// Assert the pre-selected items are shown.
$assert_session->checkboxChecked("Select $existing_media_name", $this->getSelectionArea());
// Set alt texts for items 1 and 2, leave the alt text empty for items 3
// and 4 to assert the field validation does not stop users from removing
// items.
$page->fillField('media[0][fields][field_media_test_image][0][alt]', $filenames[0]);
$page->fillField('media[1][fields][field_media_test_image][0][alt]', $filenames[1]);
// Assert the file is available in the file storage.
$files = $file_storage->loadByProperties(['filename' => $filenames[1]]);
$this->assertCount(1, $files);
$file_1_uri = reset($files)->getFileUri();
// Remove the second file and assert the focus is shifted to the container
// of the next media item and field values are still correct.
$page->pressButton('media-1-remove-button');
$this->assertJsCondition('jQuery("[data-media-library-added-delta=2]").is(":focus")');
$assert_session->pageTextContains('The media item ' . $filenames[1] . ' has been removed.');
// Assert the file was deleted.
$this->assertEmpty($file_storage->loadByProperties(['filename' => $filenames[1]]));
$this->assertFileDoesNotExist($file_1_uri);
// When a file is already in usage, it should not be deleted. To test,
// let's add a usage for $filenames[3] (now in the third position).
$files = $file_storage->loadByProperties(['filename' => $filenames[3]]);
$this->assertCount(1, $files);
$target_file = reset($files);
Media::create([
'bundle' => 'type_three',
'name' => 'Disturbing',
'field_media_test_image' => [
['target_id' => $target_file->id()],
],
])->save();
// Remove $filenames[3] (now in the third position) and assert the focus is
// shifted to the container of the previous media item and field values are
// still correct.
$page->pressButton('media-3-remove-button');
$this->assertTrue($assert_session->waitForText('The media item ' . $filenames[3] . ' has been removed.'));
// Assert the file was not deleted, due to being in use elsewhere.
$this->assertNotEmpty($file_storage->loadByProperties(['filename' => $filenames[3]]));
$this->assertFileExists($target_file->getFileUri());
// The second media item should be removed (this has the delta 1 since we
// start counting from 0).
$assert_session->elementNotExists('css', '[data-media-library-added-delta=1]');
$media_item_one = $assert_session->elementExists('css', '[data-media-library-added-delta=0]');
$assert_session->fieldValueEquals('Name', $filenames[0], $media_item_one);
$assert_session->fieldValueEquals('Alternative text', $filenames[0], $media_item_one);
$media_item_three = $assert_session->elementExists('css', '[data-media-library-added-delta=2]');
$assert_session->fieldValueEquals('Name', $filenames[2], $media_item_three);
$assert_session->fieldValueEquals('Alternative text', '', $media_item_three);
// Assert the pre-selected items are still shown.
$assert_session->checkboxChecked("Select $existing_media_name", $this->getSelectionArea());
// Remove the last file and assert the focus is shifted to the container
// of the first media item and field values are still correct.
$page->pressButton('media-2-remove-button');
$this->assertTrue($assert_session->waitForText('The media item ' . $filenames[2] . ' has been removed.'));
$this->assertJsCondition('jQuery("[data-media-library-added-delta=0]").is(":focus")');
$assert_session->pageTextContains('The media item ' . $filenames[2] . ' has been removed.');
$assert_session->elementNotExists('css', '[data-media-library-added-delta=1]');
$assert_session->elementNotExists('css', '[data-media-library-added-delta=2]');
$media_item_one = $assert_session->elementExists('css', '[data-media-library-added-delta=0]');
$assert_session->fieldValueEquals('Name', $filenames[0], $media_item_one);
$assert_session->fieldValueEquals('Alternative text', $filenames[0], $media_item_one);
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
/**
* Tests the views in the media library widget.
*
* @group media_library
*/
class WidgetViewsTest extends MediaLibraryTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a few example media items for use in selection.
$this->createMediaItems([
'type_one' => [
'Horse',
'Bear',
'Cat',
'Dog',
'Goat',
'Sheep',
'Pig',
'Cow',
'Chicken',
'Duck',
'Donkey',
'Llama',
'Mouse',
'Goldfish',
'Rabbit',
'Turkey',
'Dove',
'Giraffe',
'Tiger',
'Hamster',
'Parrot',
'Monkey',
'Koala',
'Panda',
'Kangaroo',
],
'type_two' => [
'Crocodile',
'Lizard',
'Snake',
'Turtle',
],
]);
// Create a user who can use the Media library.
$user = $this->drupalCreateUser([
'access content',
'create basic_page content',
'view media',
'create media',
]);
$this->drupalLogin($user);
}
/**
* Tests that the views in the Media library's widget work as expected.
*/
public function testWidgetViews(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('node/add/basic_page');
$this->openMediaLibraryForField('field_unlimited_media');
// Assert the 'Apply filter' button is not moved to the button pane.
$button_pane = $assert_session->elementExists('css', '.ui-dialog-buttonpane');
$assert_session->buttonExists('Insert selected', $button_pane);
$assert_session->buttonNotExists('Apply filters', $button_pane);
// Assert the pager works as expected.
// An active pager item is not linked and contains "Page #" as text.
$assert_session->elementTextContains('css', '.js-media-library-view .js-pager__items > li:nth-of-type(1)', 'Page 1');
$assert_session->elementNotExists('css', '.js-media-library-view .js-pager__items > li:nth-of-type(1) a');
$assert_session->elementExists('css', '.js-media-library-view .js-pager__items > li:nth-of-type(2) a');
$this->assertCount(24, $this->getCheckboxes());
$page->clickLink('Next page');
$this->waitForElementTextContains('.js-media-library-view .js-pager__items > li:nth-of-type(2)', 'Page 2');
$assert_session->elementExists('css', '.js-media-library-view .js-pager__items > li:nth-of-type(1) a');
$assert_session->elementNotExists('css', '.js-media-library-view .js-pager__items > li:nth-of-type(2) a');
$this->assertCount(1, $this->getCheckboxes());
$page->clickLink('Previous page');
$this->waitForElementTextContains('.js-media-library-view .js-pager__items > li:nth-of-type(1)', 'Page 1');
$this->assertCount(24, $this->getCheckboxes());
$page->checkField('Select Bear');
$this->pressInsertSelected('Added one media item.');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextNotContains('Cat');
$assert_session->pageTextNotContains('Turtle');
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaLibraryTable();
// Assert the 'Apply filter' button is not moved to the button pane.
$assert_session->buttonExists('Insert selected', $button_pane);
$assert_session->buttonNotExists('Apply filters', $button_pane);
$assert_session->pageTextContains('Dog');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextNotContains('Turtle');
// Assert the exposed filters can be applied and page is reset from second
// page.
$page->clickLink('Next page');
$this->waitForElementTextContains('.js-media-library-view .js-pager__items > li:nth-of-type(2)', 'Page 2');
$page->fillField('Name', 'Bear');
$page->pressButton('Apply filters');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Dog');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextNotContains('Turtle');
// Test clearing the filters.
$page->fillField('Name', '');
$page->pressButton('Apply filters');
$assert_session->waitForLink('Next page');
$page->clickLink('Next page');
$this->waitForElementTextContains('.js-media-library-view .js-pager__items > li:nth-of-type(2)', 'Page 2');
// Assert the exposed filters are persisted when changing display.
$page->fillField('Name', 'Dog');
$page->pressButton('Apply filters');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContains('Dog');
$assert_session->pageTextNotContains('Crocodile');
$assert_session->pageTextNotContains('Turtle');
$page->checkField('Select Dog');
$assert_session->linkExists('Table');
$this->switchToMediaLibraryGrid();
$this->assertSame('Dog', $page->findField('Name')->getValue());
$assert_session->pageTextContains('Dog');
$assert_session->pageTextNotContains('Crocodile');
$assert_session->pageTextNotContains('Turtle');
$assert_session->linkExists('Grid');
$this->switchToMediaLibraryTable();
// Select the item.
$this->pressInsertSelected('Added one media item.');
// Ensure that the selection completed successfully.
$assert_session->pageTextContains('Dog');
$assert_session->pageTextContains('Bear');
$assert_session->pageTextNotContains('Turtle');
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
/**
* Tests the media library widget when no media types are available.
*
* @group media_library
*/
class WidgetWithoutTypesTest extends MediaLibraryTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that the widget works as expected when media types are deleted.
*/
public function testWidgetWithoutMediaTypes(): void {
$assert_session = $this->assertSession();
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create media',
'view media',
]);
$this->drupalLogin($user);
$default_message = 'There are no allowed media types configured for this field. Contact the site administrator.';
$this->drupalGet('node/add/basic_page');
// Assert a properly configured field does not show a message.
$assert_session->elementTextNotContains('css', '.field--name-field-twin-media', 'There are no allowed media types configured for this field.');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_twin_media"]');
// Assert that the message is shown when the target_bundles setting for the
// entity reference field is an empty array. No types are allowed in this
// case.
$assert_session->elementTextContains('css', '.field--name-field-empty-types-media', $default_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_empty_types_media"]');
// Assert that the message is not shown when the target_bundles setting for
// the entity reference field is null. All types are allowed in this case.
$assert_session->elementTextNotContains('css', '.field--name-field-null-types-media', 'There are no allowed media types configured for this field.');
$assert_session->elementExists('css', '.js-media-library-open-button[name^="field_null_types_media"]');
// Delete all media and media types.
$entity_type_manager = \Drupal::entityTypeManager();
$media_storage = $entity_type_manager->getStorage('media');
$media_type_storage = $entity_type_manager->getStorage('media_type');
$media_storage->delete($media_storage->loadMultiple());
$media_type_storage->delete($media_type_storage->loadMultiple());
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
// Assert a properly configured field now shows a message.
$assert_session->elementTextContains('css', '.field--name-field-twin-media', $default_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_twin_media"]');
// Assert that the message is shown when the target_bundles setting for the
// entity reference field is an empty array.
$assert_session->elementTextContains('css', '.field--name-field-empty-types-media', $default_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_empty_types_media"]');
// Assert that the message is shown when the target_bundles setting for
// the entity reference field is null.
$assert_session->elementTextContains('css', '.field--name-field-null-types-media', $default_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_null_types_media"]');
// Assert a different message is shown when the user is allowed to
// administer the fields.
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'view media',
'administer node fields',
]);
$this->drupalLogin($user);
$route_bundle_params = FieldUI::getRouteBundleParameter(\Drupal::entityTypeManager()->getDefinition('node'), 'basic_page');
$field_twin_url = new Url('entity.field_config.node_field_edit_form', [
'field_config' => 'node.basic_page.field_twin_media',
] + $route_bundle_params);
$field_twin_message = 'There are no allowed media types configured for this field. <a href="' . $field_twin_url->toString() . '">Edit the field settings</a> to select the allowed media types.';
$field_empty_types_url = new Url('entity.field_config.node_field_edit_form', [
'field_config' => 'node.basic_page.field_empty_types_media',
] + $route_bundle_params);
$field_empty_types_message = 'There are no allowed media types configured for this field. <a href="' . $field_empty_types_url->toString() . '">Edit the field settings</a> to select the allowed media types.';
$field_null_types_url = new Url('entity.field_config.node_field_edit_form', [
'field_config' => 'node.basic_page.field_null_types_media',
] + $route_bundle_params);
$field_null_types_message = 'There are no allowed media types configured for this field. <a href="' . $field_null_types_url->toString() . '">Edit the field settings</a> to select the allowed media types.';
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
// Assert a properly configured field still shows a message.
$assert_session->elementContains('css', '.field--name-field-twin-media', $field_twin_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_twin_media"]');
// Assert that the message is shown when the target_bundles setting for the
// entity reference field is an empty array.
$assert_session->elementContains('css', '.field--name-field-empty-types-media', $field_empty_types_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_empty_types_media"]');
// Assert that the message is shown when the target_bundles setting for the
// entity reference field is null.
$assert_session->elementContains('css', '.field--name-field-null-types-media', $field_null_types_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_null_types_media"]');
// Assert the messages are also shown in the default value section of the
// field edit form.
$this->drupalGet($field_empty_types_url);
$assert_session->elementContains('css', '.field--name-field-empty-types-media', $field_empty_types_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_empty_types_media"]');
$this->drupalGet($field_null_types_url);
$assert_session->elementContains('css', '.field--name-field-null-types-media', $field_null_types_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_null_types_media"]');
// Uninstall the Field UI and check if the link is removed from the message.
\Drupal::service('module_installer')->uninstall(['field_ui']);
// Visit a node create page.
$this->drupalGet('node/add/basic_page');
$field_ui_uninstalled_message = 'There are no allowed media types configured for this field. Contact the site administrator.';
// Assert the link is now longer part of the message.
$assert_session->elementNotExists('named', ['link', 'Edit the field settings']);
// Assert a properly configured field still shows a message.
$assert_session->elementContains('css', '.field--name-field-twin-media', $field_ui_uninstalled_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_twin_media"]');
// Assert that the message is shown when the target_bundles setting for the
// entity reference field is an empty array.
$assert_session->elementContains('css', '.field--name-field-empty-types-media', $field_ui_uninstalled_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_empty_types_media"]');
// Assert that the message is shown when the target_bundles setting for the
// entity reference field is null.
$assert_session->elementContains('css', '.field--name-field-null-types-media', $field_ui_uninstalled_message);
$assert_session->elementNotExists('css', '.js-media-library-open-button[name^="field_null_types_media"]');
}
}

View File

@@ -0,0 +1,426 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Kernel;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\entity_test\Entity\EntityTestWithBundle;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\media_library\MediaLibraryState;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\views\Views;
/**
* Tests the media library access.
*
* @group media_library
*/
class MediaLibraryAccessTest extends KernelTestBase {
use UserCreationTrait;
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'media',
'media_library',
'media_library_test',
'filter',
'file',
'field',
'image',
'system',
'views',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', 'file_usage');
$this->installEntitySchema('entity_test_with_bundle');
$this->installEntitySchema('filter_format');
$this->installEntitySchema('media');
$this->installConfig([
'field',
'system',
'file',
'image',
'media',
'media_library',
]);
EntityTestBundle::create(['id' => 'test'])->save();
$field_storage = FieldStorageConfig::create([
'type' => 'entity_reference',
'field_name' => 'field_test_media',
'entity_type' => 'entity_test_with_bundle',
'settings' => [
'target_type' => 'media',
],
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'test',
])->save();
// Create an account with special UID 1.
$this->createUser([]);
}
/**
* Tests that the field widget opener respects entity creation permissions.
*/
public function testFieldWidgetEntityCreateAccess(): void {
/** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */
$ui_builder = $this->container->get('media_library.ui_builder');
// Create a media library state to test access.
$state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [
'entity_type_id' => 'entity_test_with_bundle',
'bundle' => 'test',
'field_name' => 'field_test_media',
]);
$access_result = $ui_builder->checkAccess($this->createUser(), $state);
$this->assertAccess($access_result, FALSE, "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create test entity_test_with_bundle entities'.", [], ['url.query_args', 'user.permissions']);
// Create a user with the appropriate permissions and assert that access is
// granted.
$account = $this->createUser([
'create test entity_test_with_bundle entities',
'view media',
]);
$access_result = $ui_builder->checkAccess($account, $state);
$this->assertAccess($access_result, TRUE, NULL, Views::getView('media_library')->storage->getCacheTags(), ['url.query_args', 'user.permissions']);
}
/**
* @covers \Drupal\media_library\MediaLibraryEditorOpener::checkAccess
*
* @param bool $media_embed_enabled
* Whether to test with media_embed filter enabled on the text format.
* @param bool $can_use_format
* Whether the logged in user is allowed to use the text format.
*
* @dataProvider editorOpenerAccessProvider
*/
public function testEditorOpenerAccess($media_embed_enabled, $can_use_format): void {
$format = $this->container
->get('entity_type.manager')
->getStorage('filter_format')->create([
'format' => $this->randomMachineName(),
'name' => $this->randomString(),
'filters' => [
'media_embed' => ['status' => $media_embed_enabled],
],
]);
$format->save();
$permissions = [
'access media overview',
'view media',
];
if ($can_use_format) {
$permissions[] = $format->getPermissionName();
}
$state = MediaLibraryState::create(
'media_library.opener.editor',
['image'],
'image',
1,
['filter_format_id' => $format->id()]
);
$access_result = $this->container
->get('media_library.ui_builder')
->checkAccess($this->createUser($permissions), $state);
if ($media_embed_enabled && $can_use_format) {
$this->assertAccess($access_result, TRUE, NULL, Views::getView('media_library')->storage->getCacheTags(), ['user.permissions']);
}
else {
$this->assertAccess($access_result, FALSE, NULL, [], ['user.permissions']);
}
}
/**
* Data provider for ::testEditorOpenerAccess.
*/
public static function editorOpenerAccessProvider() {
return [
'media_embed filter enabled' => [
TRUE,
TRUE,
],
'media_embed filter disabled' => [
FALSE,
TRUE,
],
'media_embed filter enabled, user not allowed to use text format' => [
TRUE,
FALSE,
],
];
}
/**
* Tests that the field widget opener respects entity-specific access.
*/
public function testFieldWidgetEntityEditAccess(): void {
/** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */
$ui_builder = $this->container->get('media_library.ui_builder');
$forbidden_entity = EntityTestWithBundle::create([
'type' => 'test',
// This label will automatically cause an access denial.
// @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
'name' => 'forbid_access',
]);
$forbidden_entity->save();
// Create a media library state to test access.
$state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [
'entity_type_id' => $forbidden_entity->getEntityTypeId(),
'bundle' => $forbidden_entity->bundle(),
'field_name' => 'field_test_media',
'entity_id' => $forbidden_entity->id(),
]);
$access_result = $ui_builder->checkAccess($this->createUser(), $state);
$this->assertAccess($access_result, FALSE, NULL, [], ['url.query_args']);
$neutral_entity = EntityTestWithBundle::create([
'type' => 'test',
// This label will result in neutral access.
// @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
'name' => $this->randomString(),
]);
$neutral_entity->save();
$parameters = $state->getOpenerParameters();
$parameters['entity_id'] = $neutral_entity->id();
$state = MediaLibraryState::create(
$state->getOpenerId(),
$state->getAllowedTypeIds(),
$state->getSelectedTypeId(),
$state->getAvailableSlots(),
$parameters
);
$access_result = $ui_builder->checkAccess($this->createUser(), $state);
$this->assertTrue($access_result->isNeutral());
$this->assertAccess($access_result, FALSE, NULL, [], ['url.query_args', 'user.permissions']);
// Give the user permission to edit the entity and assert that access is
// granted.
$account = $this->createUser([
'administer entity_test content',
'view media',
]);
$access_result = $ui_builder->checkAccess($account, $state);
$this->assertAccess($access_result, TRUE, NULL, Views::getView('media_library')->storage->getCacheTags(), ['url.query_args', 'user.permissions']);
}
/**
* Data provider for ::testFieldWidgetEntityFieldAccess().
*
* @return array[]
* Sets of arguments to pass to the test method.
*/
public static function providerFieldWidgetEntityFieldAccess(): array {
return [
['entity_reference'],
['entity_reference_subclass'],
];
}
/**
* Tests that the field widget opener respects entity field-level access.
*
* @param string $field_type
* The field type.
*
* @dataProvider providerFieldWidgetEntityFieldAccess
*/
public function testFieldWidgetEntityFieldAccess(string $field_type): void {
$field_storage = FieldStorageConfig::create([
'type' => $field_type,
'entity_type' => 'entity_test_with_bundle',
// The media_library_test module will deny access to this field.
// @see media_library_test_entity_field_access()
'field_name' => 'field_media_no_access',
'settings' => [
'target_type' => 'media',
],
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'test',
])->save();
/** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */
$ui_builder = $this->container->get('media_library.ui_builder');
// Create an account with administrative access to the test entity type,
// so that we can be certain that field access is checked.
$account = $this->createUser(['administer entity_test content']);
// Test that access is denied even without an entity to work with.
$state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [
'entity_type_id' => 'entity_test_with_bundle',
'bundle' => 'test',
'field_name' => $field_storage->getName(),
]);
$access_result = $ui_builder->checkAccess($account, $state);
$this->assertAccess($access_result, FALSE, 'Field access denied by test module', [], ['url.query_args', 'user.permissions']);
// Assert that field access is also checked with a real entity.
$entity = EntityTestWithBundle::create([
'type' => 'test',
'name' => $this->randomString(),
]);
$entity->save();
$parameters = $state->getOpenerParameters();
$parameters['entity_id'] = $entity->id();
$state = MediaLibraryState::create(
$state->getOpenerId(),
$state->getAllowedTypeIds(),
$state->getSelectedTypeId(),
$state->getAvailableSlots(),
$parameters
);
$access_result = $ui_builder->checkAccess($account, $state);
$this->assertAccess($access_result, FALSE, 'Field access denied by test module', [], ['url.query_args', 'user.permissions']);
}
/**
* Tests that media library access respects the media_library view.
*/
public function testViewAccess(): void {
/** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */
$ui_builder = $this->container->get('media_library.ui_builder');
// Create a media library state to test access.
$state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [
'entity_type_id' => 'entity_test_with_bundle',
'bundle' => 'test',
'field_name' => 'field_test_media',
]);
// Create a clone of the view so we can reset the original later.
$view_original = clone Views::getView('media_library');
// Create our test users. Both have permission to create entity_test content
// so that we can specifically test Views-related access checking.
// @see ::testEntityCreateAccess()
$forbidden_account = $this->createUser([
'create test entity_test_with_bundle entities',
]);
$allowed_account = $this->createUser([
'create test entity_test_with_bundle entities',
'view media',
]);
// Assert the 'view media' permission is needed to access the library and
// validate the cache dependencies.
$access_result = $ui_builder->checkAccess($forbidden_account, $state);
$this->assertAccess($access_result, FALSE, "The 'view media' permission is required.", $view_original->storage->getCacheTags(), ['url.query_args', 'user.permissions']);
// Assert that the media library access is denied when the view widget
// display is deleted.
$view_storage = Views::getView('media_library')->storage;
$displays = $view_storage->get('display');
unset($displays['widget']);
$view_storage->set('display', $displays);
$view_storage->save();
$access_result = $ui_builder->checkAccess($allowed_account, $state);
$this->assertAccess($access_result, FALSE, 'The media library widget display does not exist.', $view_original->storage->getCacheTags());
// Restore the original view and assert that the media library controller
// works again.
$view_original->storage->save();
$access_result = $ui_builder->checkAccess($allowed_account, $state);
$this->assertAccess($access_result, TRUE, NULL, $view_original->storage->getCacheTags(), ['url.query_args', 'user.permissions']);
// Assert that the media library access is denied when the entire media
// library view is deleted.
Views::getView('media_library')->storage->delete();
$access_result = $ui_builder->checkAccess($allowed_account, $state);
$this->assertAccess($access_result, FALSE, 'The media library view does not exist.');
}
/**
* Tests that the media library respects arbitrary access to the add form.
*/
public function testAddFormAccess(): void {
// Access is denied if the media library is trying to create media whose
// type name is 'deny_access'. Also create a second media type that we *can*
// add, so we can be certain that the add form is otherwise visible.
// @see media_library_test_media_create_access()
$media_types = [
$this->createMediaType('image', ['id' => 'deny_access'])->id(),
$this->createMediaType('image')->id(),
];
$account = $this->createUser(['create media']);
$this->setCurrentUser($account);
/** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */
$ui_builder = $this->container->get('media_library.ui_builder');
$state = MediaLibraryState::create('test', $media_types, $media_types[0], 1);
$build = $ui_builder->buildUi($state);
$this->assertEmpty($build['content']['form']);
$state = MediaLibraryState::create('test', $media_types, $media_types[1], 1);
$build = $ui_builder->buildUi($state);
$this->assertNotEmpty($build['content']['form']);
}
/**
* Asserts various aspects of an access result.
*
* @param \Drupal\Core\Access\AccessResult $access_result
* The access result.
* @param bool $is_allowed
* The expected access status.
* @param string $expected_reason
* (optional) The expected reason attached to the access result.
* @param string[] $expected_cache_tags
* (optional) The expected cache tags attached to the access result.
* @param string[] $expected_cache_contexts
* (optional) The expected cache contexts attached to the access result.
*/
private function assertAccess(AccessResult $access_result, bool $is_allowed, ?string $expected_reason = NULL, array $expected_cache_tags = [], array $expected_cache_contexts = []): void {
$this->assertSame($is_allowed, $access_result->isAllowed());
if ($access_result instanceof AccessResultReasonInterface && isset($expected_reason)) {
$this->assertSame($expected_reason, $access_result->getReason());
}
$this->assertEqualsCanonicalizing($expected_cache_tags, $access_result->getCacheTags());
$this->assertEqualsCanonicalizing($expected_cache_contexts, $access_result->getCacheContexts());
}
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Kernel;
use Drupal\Core\Form\FormState;
use Drupal\KernelTests\KernelTestBase;
use Drupal\media_library\Form\FileUploadForm;
use Drupal\media_library\Form\OEmbedForm;
use Drupal\media_library\MediaLibraryState;
use Drupal\media_library_form_overwrite_test\Form\TestAddForm;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests the media library add form.
*
* @group media_library
*/
class MediaLibraryAddFormTest extends KernelTestBase {
use MediaTypeCreationTrait;
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'media',
'media_library',
'file',
'field',
'filter',
'image',
'system',
'views',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', 'file_usage');
$this->installEntitySchema('media');
$this->installConfig([
'field',
'system',
'file',
'image',
'media',
'media_library',
]);
// Create an account with special UID 1.
$this->createUser([]);
$this->createMediaType('image', ['id' => 'image']);
$this->createMediaType('oembed:video', ['id' => 'remote_video']);
}
/**
* Tests the media library add form.
*/
public function testMediaTypeAddForm(): void {
$entity_type_manager = \Drupal::entityTypeManager();
$image = $entity_type_manager->getStorage('media_type')->load('image');
$remote_video = $entity_type_manager->getStorage('media_type')->load('remote_video');
$image_source_definition = $image->getSource()->getPluginDefinition();
$remote_video_source_definition = $remote_video->getSource()->getPluginDefinition();
// Assert the form class is added to the media source.
$this->assertSame(FileUploadForm::class, $image_source_definition['forms']['media_library_add']);
$this->assertSame(OEmbedForm::class, $remote_video_source_definition['forms']['media_library_add']);
// Assert the media library UI does not contains the add form when the user
// does not have access.
$this->assertEmpty($this->buildLibraryUi('image')['content']['form']);
$this->assertEmpty($this->buildLibraryUi('remote_video')['content']['form']);
// Create a user that has access to create the image media type but not the
// remote video media type.
$this->setCurrentUser($this->createUser([
'create image media',
]));
// Assert the media library UI only contains the add form for the image
// media type.
$this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['container']['upload']['#type']);
$this->assertEmpty($this->buildLibraryUi('remote_video')['content']['form']);
// Create a user that has access to create both media types.
$this->setCurrentUser($this->createUser([
'create image media',
'create remote_video media',
]));
// Assert the media library UI only contains the add form for both media
// types.
$this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['container']['upload']['#type']);
$this->assertSame('url', $this->buildLibraryUi('remote_video')['content']['form']['container']['url']['#type']);
}
/**
* Build the media library UI for a selected type.
*
* @param string $selected_type_id
* The selected media type ID.
*
* @return array
* The render array for the media library.
*/
protected function buildLibraryUi($selected_type_id) {
$state = MediaLibraryState::create('test', ['image', 'remote_video'], $selected_type_id, -1);
return \Drupal::service('media_library.ui_builder')->buildUi($state);
}
/**
* Tests the validation of the library state in the media library add form.
*/
public function testFormStateValidation(): void {
$form_state = new FormState();
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The media library state is not present in the form state.');
\Drupal::formBuilder()->buildForm(FileUploadForm::class, $form_state);
}
/**
* Tests the validation of the selected type in the media library add form.
*/
public function testSelectedTypeValidation(): void {
$state = MediaLibraryState::create('test', ['image', 'remote_video', 'header_image'], 'header_image', -1);
$form_state = new FormState();
$form_state->set('media_library_state', $state);
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("The 'header_image' media type does not exist.");
\Drupal::formBuilder()->buildForm(FileUploadForm::class, $form_state);
}
/**
* Tests overwriting of the add form.
*/
public function testDifferentAddForm(): void {
$this->enableModules(['media_library_form_overwrite_test']);
$entity_type_manager = \Drupal::entityTypeManager();
$image = $entity_type_manager->getStorage('media_type')->load('image');
$image_source_definition = $image->getSource()->getPluginDefinition();
// Assert the overwritten form class is set to the media source.
$this->assertSame(TestAddForm::class, $image_source_definition['forms']['media_library_add']);
}
}

View File

@@ -0,0 +1,400 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\media_library\MediaLibraryState;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Tests the media library state value object.
*
* @group media_library
*
* @coversDefaultClass \Drupal\media_library\MediaLibraryState
*/
class MediaLibraryStateTest extends KernelTestBase {
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'media',
'media_library',
'file',
'field',
'image',
'system',
'views',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', 'file_usage');
$this->installEntitySchema('media');
$this->installConfig([
'field',
'system',
'file',
'image',
'media',
'media_library',
]);
// Create some media types to validate against.
$this->createMediaType('file', ['id' => 'document']);
$this->createMediaType('image', ['id' => 'image']);
$this->createMediaType('video_file', ['id' => 'video']);
}
/**
* Tests the media library state methods.
*/
public function testMethods(): void {
$opener_id = 'test';
$allowed_media_type_ids = ['document', 'image'];
$selected_media_type_id = 'image';
$remaining_slots = 2;
$state = MediaLibraryState::create($opener_id, $allowed_media_type_ids, $selected_media_type_id, $remaining_slots);
$this->assertSame($opener_id, $state->getOpenerId());
$this->assertSame($allowed_media_type_ids, $state->getAllowedTypeIds());
$this->assertSame($selected_media_type_id, $state->getSelectedTypeId());
$this->assertSame($remaining_slots, $state->getAvailableSlots());
$this->assertTrue($state->hasSlotsAvailable());
$state = MediaLibraryState::create($opener_id, $allowed_media_type_ids, $selected_media_type_id, 0);
$this->assertFalse($state->hasSlotsAvailable());
}
/**
* Tests the media library state creation.
*
* @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 string $exception_message
* The expected exception message.
*
* @covers ::create
* @dataProvider providerCreate
*/
public function testCreate($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots, $exception_message = ''): void {
if ($exception_message) {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage($exception_message);
}
$state = MediaLibraryState::create($opener_id, $allowed_media_type_ids, $selected_type_id, $remaining_slots);
$this->assertInstanceOf(MediaLibraryState::class, $state);
// Ensure that the state object carries cache metadata.
$this->assertInstanceOf(CacheableDependencyInterface::class, $state);
$this->assertSame(['url.query_args'], $state->getCacheContexts());
$this->assertSame(Cache::PERMANENT, $state->getCacheMaxAge());
}
/**
* Data provider for testCreate().
*
* @return array
* The data sets to test.
*/
public static function providerCreate() {
$test_data = [];
// Assert no exception is thrown when we add the parameters as expected.
$test_data['valid parameters'] = [
'test',
['document', 'image'],
'image',
2,
];
// Assert an exception is thrown when the opener ID parameter is empty.
$test_data['empty opener ID'] = [
'',
['document', 'image'],
'image',
2,
'The opener ID parameter is required and must be a string.',
];
// Assert an exception is thrown when the opener ID parameter is not a
// valid string.
$test_data['integer opener ID'] = [
1,
['document', 'image'],
'image',
2,
'The opener ID parameter is required and must be a string.',
];
$test_data['boolean opener ID'] = [
TRUE,
['document', 'image'],
'image',
2,
'The opener ID parameter is required and must be a string.',
];
$test_data['spaces opener ID'] = [
' ',
['document', 'image'],
'image',
2,
'The opener ID parameter is required and must be a string.',
];
// Assert an exception is thrown when the allowed types parameter is empty.
$test_data['empty allowed types'] = [
'test',
[],
'image',
2,
'The allowed types parameter is required and must be an array of strings.',
];
// It is not possible to assert a non-array allowed types parameter, since
// that would throw a TypeError which is not a subclass of Exception.
// Continue asserting an exception is thrown when the allowed types
// parameter contains elements that are not a valid string.
$test_data['integer in allowed types'] = [
'test',
[1, 'image'],
'image',
2,
'The allowed types parameter is required and must be an array of strings.',
];
$test_data['boolean in allowed types'] = [
'test',
[TRUE, 'image'],
'image',
2,
'The allowed types parameter is required and must be an array of strings.',
];
$test_data['spaces in allowed types'] = [
'test',
[' ', 'image'],
'image',
2,
'The allowed types parameter is required and must be an array of strings.',
];
// Assert an exception is thrown when the selected type parameter is empty.
$test_data['empty selected type'] = [
'test',
['document', 'image'],
'',
2,
'The selected type parameter is required and must be a string.',
];
// Assert an exception is thrown when the selected type parameter is not a
// valid string.
$test_data['numeric selected type'] = [
'test',
['document', 'image'],
1,
2,
'The selected type parameter is required and must be a string.',
];
$test_data['boolean selected type'] = [
'test',
['document', 'image'],
TRUE,
2,
'The selected type parameter is required and must be a string.',
];
$test_data['spaces selected type'] = [
'test',
['document', 'image'],
' ',
2,
'The selected type parameter is required and must be a string.',
];
// Assert an exception is thrown when the selected type parameter is not in
// the list of allowed types.
$test_data['non-present selected type'] = [
'test',
['document', 'image'],
'video',
2,
'The selected type parameter must be present in the list of allowed types.',
];
// Assert an exception is thrown when the remaining slots parameter is
// empty.
$test_data['empty remaining slots'] = [
'test',
['document', 'image'],
'image',
'',
'The remaining slots parameter is required and must be numeric.',
];
// Assert an exception is thrown when the remaining slots parameter is
// not numeric.
$test_data['string remaining slots'] = [
'test',
['document', 'image'],
'image',
'fail',
'The remaining slots parameter is required and must be numeric.',
];
$test_data['boolean remaining slots'] = [
'test',
['document', 'image'],
'image',
TRUE,
'The remaining slots parameter is required and must be numeric.',
];
return $test_data;
}
/**
* Tests the hash validation when the state is created from a request.
*
* @param array $query_overrides
* The query parameters to override.
* @param bool $exception_expected
* Whether an AccessDeniedHttpException is expected or not.
*
* @covers ::fromRequest
* @dataProvider providerFromRequest
*/
public function testFromRequest(array $query_overrides, $exception_expected): void {
// Override the query parameters and verify an exception is thrown when
// required state parameters are changed.
$query = MediaLibraryState::create('test', ['file', 'image'], 'image', 2)->all();
$query = array_merge($query, $query_overrides);
if ($exception_expected) {
$this->expectException(BadRequestHttpException::class);
$this->expectExceptionMessage("Invalid media library parameters specified.");
}
$state = MediaLibraryState::fromRequest(new Request($query));
$this->assertInstanceOf(MediaLibraryState::class, $state);
}
/**
* @covers ::fromRequest
*/
public function testFromRequestQueryLess(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The opener ID parameter is required and must be a string.');
$state = MediaLibraryState::fromRequest(new Request());
$this->assertInstanceOf(MediaLibraryState::class, $state);
}
/**
* Data provider for testFromRequest().
*
* @return array
* The data sets to test.
*/
public static function providerFromRequest() {
$test_data = [];
// Assert no exception is thrown when we use valid state parameters.
$test_data['valid parameters'] = [
[],
FALSE,
];
// Assert no exception is thrown when we override all query parameters with
// the same data.
$test_data['changed with same values'] = [
[
'media_library_opener_id' => 'test',
'media_library_allowed_types' => ['file', 'image'],
'media_library_selected_type' => 'image',
'media_library_remaining' => 2,
],
FALSE,
];
// Assert an exception is thrown when we change the opener ID parameter.
$test_data['changed opener ID'] = [
['media_library_opener_id' => 'fail'],
TRUE,
];
// Assert an exception is thrown when we change the allowed types parameter.
$test_data['changed allowed types'] = [
['media_library_allowed_types' => ['audio', 'image']],
TRUE,
];
// Assert an exception is thrown when we change the selected type parameter.
$test_data['changed selected type'] = [
['media_library_selected_type' => 'file'],
TRUE,
];
// Assert an exception is thrown when we change the remaining slots
// parameter.
$test_data['changed remaining'] = [
['media_library_remaining' => 4],
TRUE,
];
// Assert an exception is thrown when we change the actual hash.
$test_data['changed hash'] = [
['hash' => 'fail'],
TRUE,
];
return $test_data;
}
/**
* @covers ::getOpenerParameters
*/
public function testOpenerParameters(): void {
$state = MediaLibraryState::create('test', ['file'], 'file', -1, [
'foo' => 'baz',
]);
$this->assertSame(['foo' => 'baz'], $state->getOpenerParameters());
}
/**
* Tests that hash is unaffected by allowed media type order.
*/
public function testHashUnaffectedByMediaTypeOrder(): void {
$state1 = MediaLibraryState::create('test', ['file', 'image'], 'image', 2);
$state2 = MediaLibraryState::create('test', ['image', 'file'], 'image', 2);
$this->assertSame($state1->getHash(), $state2->getHash());
}
/**
* Tests that hash is unaffected by opener parameter order.
*/
public function testHashUnaffectedByOpenerParamOrder(): void {
$state1 = MediaLibraryState::create('test', ['file'], 'file', -1, [
'foo' => 'baz',
'baz' => 'foo',
]);
$state2 = MediaLibraryState::create('test', ['file'], 'file', -1, [
'baz' => 'foo',
'foo' => 'baz',
]);
$this->assertSame($state1->getHash(), $state2->getHash());
}
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Kernel;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormState;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\KernelTests\KernelTestBase;
use Drupal\media\Entity\MediaType;
use Drupal\media_library\MediaLibraryState;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests the media library widget.
*
* @coversDefaultClass \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget
* @group media_library
*/
class MediaLibraryWidgetTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'media',
'media_library',
'field',
'filter',
'image',
'system',
'views',
'user',
'entity_test',
];
/**
* An admin user.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* The base field definition.
*
* @var \Drupal\Core\Field\BaseFieldDefinition
*/
protected BaseFieldDefinition $baseField;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->baseField = BaseFieldDefinition::create('entity_reference')
->setName('media')
->setSetting('target_type', 'media')
->setSetting('handler_settings', ['target_bundles' => ['test_type' => 'test_type']]);
$this->container->get('state')->set('entity_test.additional_base_field_definitions', [
'media' => $this->baseField,
]);
$this->container->get('state')->set('entity_test_rev.additional_base_field_definitions', [
'media' => $this->baseField,
]);
$this->installEntitySchema('entity_test');
$this->installEntitySchema('entity_test_rev');
$this->installEntitySchema('user');
$this->installConfig([
'system',
'image',
'media',
'media_library',
]);
MediaType::create([
'id' => 'test_type',
'label' => 'Test type',
'source' => 'image',
])->save();
// Create user 1 so the test user doesn't bypass access control.
$this->createUser();
$this->adminUser = $this->createUser([
'administer entity_test content',
'view media',
]);
}
/**
* Test the media library widget access.
*/
public function testWidgetAccess(): void {
$entity = EntityTest::create([
'name' => 'sample entity',
]);
$entity->save();
$element = $this->buildWidgetForm($entity);
$this->assertMediaLibraryStateAccess(TRUE, $this->adminUser, $element['open_button']['#media_library_state']);
}
/**
* Test the media library widget access with a revisionable entity type.
*/
public function testRevisionableWidgetAccess(): void {
$allowed_revision = EntityTestRev::create([
'name' => 'allowed_access',
]);
$allowed_revision->save();
$denied_revision = clone $allowed_revision;
$denied_revision->setNewRevision();
$denied_revision->name = 'forbid_access';
$denied_revision->save();
$element = $this->buildWidgetForm($allowed_revision);
$this->assertMediaLibraryStateAccess(TRUE, $this->adminUser, $element['open_button']['#media_library_state']);
$element = $this->buildWidgetForm($denied_revision);
$this->assertMediaLibraryStateAccess(FALSE, $this->adminUser, $element['open_button']['#media_library_state']);
}
/**
* Assert if the given user has access to the given state.
*
* @param bool $access
* The access result to assert.
* @param \Drupal\Core\Session\AccountInterface $user
* The user account.
* @param \Drupal\media_library\MediaLibraryState $state
* The media library state.
*
* @throws \Exception
*
* @internal
*/
protected function assertMediaLibraryStateAccess(bool $access, AccountInterface $user, MediaLibraryState $state): void {
$ui_builder = $this->container->get('media_library.ui_builder');
$access_result = $ui_builder->checkAccess($user, $state);
$this->assertEquals($access, $access_result->isAllowed());
}
/**
* Build the media library widget form.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to build the form for.
*
* @return array
* A built form array of the media library widget.
*/
protected function buildWidgetForm($entity) {
$form = [
'#parents' => [],
];
return $this->container->get('plugin.manager.field.widget')->createInstance('media_library_widget', [
'field_definition' => $this->baseField,
'settings' => [],
'third_party_settings' => [],
])->formElement($entity->media, 0, ['#description' => ''], $form, new FormState());
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\media_library\Unit;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\media_library\Plugin\views\field\MediaLibrarySelectForm;
use Drupal\Tests\UnitTestCase;
use Drupal\views\Entity\View;
use Drupal\views\Plugin\views\display\DefaultDisplay;
use Drupal\views\Plugin\ViewsPluginManager;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\HttpFoundation\Request;
/**
* @coversDefaultClass \Drupal\media_library\Plugin\views\field\MediaLibrarySelectForm
* @group media_library
*/
class MediaLibrarySelectFormTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
parent::tearDown();
$container = new ContainerBuilder();
\Drupal::setContainer($container);
}
/**
* @covers ::viewsForm
*/
public function testViewsForm(): void {
$row = new ResultRow();
$field = $this->getMockBuilder(MediaLibrarySelectForm::class)
->onlyMethods(['getEntity'])
->disableOriginalConstructor()
->getMock();
$field->expects($this->any())
->method('getEntity')
->willReturn(NULL);
$container = new ContainerBuilder();
$container->set('string_translation', $this->createMock(TranslationInterface::class));
\Drupal::setContainer($container);
$request = $this->getMockBuilder(Request::class)
->disableOriginalConstructor()
->getMock();
$request->query = new InputBag();
$view = $this->getMockBuilder(ViewExecutable::class)
->onlyMethods(['getRequest', 'initStyle', 'getDisplay'])
->disableOriginalConstructor()
->getMock();
$view->expects($this->any())
->method('getRequest')
->willReturn($request);
$view->expects($this->any())
->method('initStyle')
->willReturn(TRUE);
$display = $this->getMockBuilder(DefaultDisplay::class)
->disableOriginalConstructor()
->getMock();
$display->display['id'] = 'foo';
$view->expects($this->any())
->method('getDisplay')
->willReturn($display);
$view_entity = $this->getMockBuilder(View::class)
->disableOriginalConstructor()
->getMock();
$view_entity->expects($this->any())
->method('get')
->willReturn([]);
$view->storage = $view_entity;
$display_manager = $this->getMockBuilder(ViewsPluginManager::class)
->disableOriginalConstructor()
->getMock();
$display = $this->getMockBuilder(DefaultDisplay::class)
->disableOriginalConstructor()
->getMock();
$display_manager->expects($this->any())
->method('createInstance')
->willReturn($display);
$container->set('plugin.manager.views.display', $display_manager);
\Drupal::setContainer($container);
$form_state = $this->createMock(FormStateInterface::class);
$view->result = [$row];
$field->view = $view;
$field->options = ['id' => 'bar'];
$form = [];
$field->viewsForm($form, $form_state);
$this->assertNotEmpty($form);
$this->assertNotEmpty($field->view->result);
$this->assertIsArray($form[$field->options['id']][0]);
$this->assertEmpty($form[$field->options['id']][0]);
}
}