Skip to content

Instantly share code, notes, and snippets.

@xurizaemon
Last active December 19, 2019 22:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xurizaemon/70e407352e8b7a4ad3f2c56d6761776c to your computer and use it in GitHub Desktop.
Save xurizaemon/70e407352e8b7a4ad3f2c56d6761776c to your computer and use it in GitHub Desktop.
8.6.x backports for SA-CORE-2019-09 SA-CORE-2019-10 SA-CORE-2019-11 SA-CORE-2019-12
commit 96a625132a34cdf092bc9d19afde3f96eb0687d0
Author: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Wed Dec 18 18:55:10 2019 +1000
SA-CORE-2019-009 by mcdruid, larowlan, Heine, alexpott, xjm, DamienMcKenna, dsnopek, catch, greggles
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index aed688d8be..f380d7ef12 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -433,10 +433,9 @@ function install_begin_request($class_loader, &$install_state) {
}
$GLOBALS['conf']['container_service_providers']['InstallerConfigOverride'] = 'Drupal\Core\Installer\ConfigOverride';
- // Only allow dumping the container once the hash salt has been created. Note,
- // InstallerKernel::createFromRequest() is not used because Settings is
+ // Note, InstallerKernel::createFromRequest() is not used because Settings is
// already initialized.
- $kernel = new InstallerKernel($environment, $class_loader, (bool) Settings::get('hash_salt', FALSE));
+ $kernel = new InstallerKernel($environment, $class_loader, FALSE);
$kernel::bootEnvironment();
$kernel->setSitePath($site_path);
$kernel->boot();
diff --git a/core/lib/Drupal/Core/Installer/InstallerKernel.php b/core/lib/Drupal/Core/Installer/InstallerKernel.php
index adeb5c53ee..4b5b6d19a8 100644
--- a/core/lib/Drupal/Core/Installer/InstallerKernel.php
+++ b/core/lib/Drupal/Core/Installer/InstallerKernel.php
@@ -15,6 +15,8 @@ class InstallerKernel extends DrupalKernel {
protected function initializeContainer() {
// Always force a container rebuild.
$this->containerNeedsRebuild = TRUE;
+ // Ensure the InstallerKernel's container is not dumped.
+ $this->allowDumping = FALSE;
$container = parent::initializeContainer();
return $container;
}
diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
index cfd0f2f796..629c6f4230 100644
--- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
+++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
@@ -398,4 +398,12 @@ public function getCacheableResponseWithCustomCacheControl() {
return new CacheableResponse('Foo', 200, ['Cache-Control' => 'bar']);
}
+ /**
+ * Use a plain Symfony response object to output the current install_profile.
+ */
+ public function getInstallProfile() {
+ $install_profile = \Drupal::installProfile() ?: 'NONE';
+ return new Response('install_profile: ' . $install_profile);
+ }
+
}
diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml
index b47174faac..521e7b2376 100644
--- a/core/modules/system/tests/modules/system_test/system_test.routing.yml
+++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml
@@ -204,3 +204,10 @@ system_test.custom_cache_control:
_controller: '\Drupal\system_test\Controller\SystemTestController::getCacheableResponseWithCustomCacheControl'
requirements:
_access: 'TRUE'
+
+system_test.install_profile:
+ path: '/system-test/get-install-profile'
+ defaults:
+ _controller: '\Drupal\system_test\Controller\SystemTestController::getInstallProfile'
+ requirements:
+ _access: 'TRUE'
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerPostInstallTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerPostInstallTest.php
new file mode 100644
index 0000000000..80e2840fa8
--- /dev/null
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerPostInstallTest.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\FunctionalTests\Installer;
+
+/**
+ * Tests re-visiting the installer after a successful installation.
+ *
+ * @group Installer
+ */
+class InstallerPostInstallTest extends InstallerTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $profile = 'minimal';
+
+ /**
+ * Confirms that visiting the installer does not break things post-install.
+ */
+ public function testVisitInstallerPostInstall() {
+ \Drupal::service('module_installer')->install(['system_test']);
+ // Clear caches to ensure that system_test's routes are available.
+ $this->resetAll();
+ // Confirm that the install_profile is correct.
+ $this->drupalGet('/system-test/get-install-profile');
+ $this->assertText('minimal');
+ // Make an anonymous visit to the installer
+ $this->drupalLogout();
+ $this->visitInstaller();
+ // Ensure that the install profile is still correct.
+ $this->drupalGet('/system-test/get-install-profile');
+ $this->assertText('minimal');
+ }
+
+}
commit 63e42e9a15ae8a0158d294dc88bdcae1299c29a8
Author: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Wed Dec 18 18:55:10 2019 +1000
SA-CORE-2019-010 by larowlan, greggles, mlhess, kim.pepper, alexpott, dww, xjm, David_Rothstein
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 58a2b8e1a9..6ac6d89001 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -955,7 +955,7 @@ function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $va
$values = [
'uid' => $user->id(),
'status' => 0,
- 'filename' => $file_info->getClientOriginalName(),
+ 'filename' => trim($file_info->getClientOriginalName(), '.'),
'uri' => $file_info->getRealPath(),
'filesize' => $file_info->getSize(),
];
diff --git a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php
index 94d00ef9f2..fe11e125ee 100644
--- a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php
+++ b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php
@@ -2,6 +2,8 @@
namespace Drupal\Tests\file\Functional;
+use Drupal\file\Entity\File;
+
/**
* Tests the 'managed_file' element type.
*
@@ -151,6 +153,21 @@ public function testManagedFileRemoved() {
$this->assertRaw('The file referenced by the Managed <em>file &amp; butter</em> field does not exist.');
}
+ /**
+ * Tests file names have leading . removed.
+ */
+ public function testFileNameTrim() {
+ file_put_contents('public://.leading-period.txt', $this->randomString(32));
+ $last_fid_prior = $this->getLastFileId();
+ $this->drupalPostForm('file/test/0/0/0', [
+ 'files[file]' => \Drupal::service('file_system')->realpath('public://.leading-period.txt'),
+ ], t('Save'));
+ $next_fid = $this->getLastFileId();
+ $this->assertGreaterThan($last_fid_prior, $next_fid);
+ $file = File::load($next_fid);
+ $this->assertEquals('leading-period.txt', $file->getFilename());
+ }
+
/**
* Ensure a file entity can be saved when the file does not exist on disk.
*/
commit f0b92ac00dcccb40fd07abe49de799f82ac33a03
Author: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Wed Dec 18 18:55:11 2019 +1000
SA-CORE-2019-011 by phenaproxima, xjm, amateescu, effulgentsia, greggles, seanB, larowlan
diff --git a/core/modules/media_library/src/Form/AddFormBase.php b/core/modules/media_library/src/Form/AddFormBase.php
new file mode 100644
index 0000000000..1445d36699
--- /dev/null
+++ b/core/modules/media_library/src/Form/AddFormBase.php
@@ -0,0 +1,842 @@
+<?php
+
+namespace Drupal\media_library\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Ajax\InvokeCommand;
+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\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+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.
+ *
+ * @internal
+ * Media Library is an experimental module and its internal code may be
+ * subject to change in minor releases. External code should not instantiate
+ * or extend this class.
+ */
+abstract class AddFormBase extends FormBase {
+
+ /**
+ * 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 a 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 = NULL) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->libraryUiBuilder = $library_ui_builder;
+ $this->viewBuilder = $this->entityTypeManager->getViewBuilder('media');
+ if (!$opener_resolver) {
+ @trigger_error('The media_library.opener_resolver service must be passed to AddFormBase::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
+ $opener_resolver = \Drupal::service('media_library.opener_resolver');
+ }
+ $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 getFormId() {
+ 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" class="media-library-add-form-wrapper">';
+ $form['#suffix'] = '</div>';
+ $form['#attached']['library'][] = 'media_library/style';
+
+ // 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'] = [
+ 'media-library-add-form',
+ 'js-media-library-add-form',
+ ];
+
+ $added_media = $this->getAddedMediaItems($form_state);
+ if (empty($added_media)) {
+ $form['#attributes']['class'][] = 'media-library-add-form--without-input';
+ $form = $this->buildInputElement($form, $form_state);
+ }
+ else {
+ $form['#attributes']['class'][] = 'media-library-add-form--with-input';
+
+ $form['media'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => [
+ 'media-library-add-form__added-media',
+ ],
+ 'aria-label' => $this->t('Added media items'),
+ 'role' => 'list',
+ // 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',
+ ],
+ ];
+
+ $form['media']['description'] = [
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => $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.'),
+ '#attributes' => [
+ 'class' => [
+ 'media-library-add-form__description',
+ ],
+ ],
+ ];
+
+ 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 = isset($form['#parents']) ? $form['#parents'] : [];
+ $id_suffix = $parents ? '-' . implode('-', $parents) : '';
+
+ $element = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => [
+ 'media-library-add-form__media',
+ ],
+ 'aria-label' => $media->getName(),
+ 'role' => 'listitem',
+ // 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,
+ '#attributes' => [
+ 'class' => [
+ 'media-library-add-form__preview',
+ ],
+ ],
+ ],
+ 'fields' => [
+ '#type' => 'container',
+ '#weight' => 20,
+ '#attributes' => [
+ 'class' => [
+ 'media-library-add-form__fields',
+ ],
+ ],
+ // 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' => [
+ 'class' => ['media-library-add-form__remove-button'],
+ '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);
+
+ // We hide the preview of the uploaded file in the image widget with CSS.
+ // @todo Improve hiding file widget elements in
+ // https://www.drupal.org/project/drupal/issues/2987921
+ $source_field_name = $this->getSourceFieldName($media->bundle->entity);
+ if (isset($element['fields'][$source_field_name])) {
+ $element['fields'][$source_field_name]['#attributes']['class'][] = 'media-library-add-form__source-field';
+ }
+ // 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;
+ }
+
+ /**
+ * 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) {
+ return [];
+ }
+
+ $selection = [
+ '#type' => 'details',
+ '#open' => FALSE,
+ '#title' => $this->t('Additional selected media'),
+ '#attributes' => [
+ 'class' => [
+ 'media-library-add-form__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 [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => [
+ 'media-library-item',
+ 'media-library-item--grid',
+ 'media-library-item--small',
+ '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) {
+ return [
+ '#type' => 'actions',
+ 'save_select' => [
+ '#type' => 'submit',
+ '#button_type' => 'primary',
+ '#value' => $this->t('Save and select'),
+ '#ajax' => [
+ 'callback' => '::updateLibrary',
+ 'wrapper' => 'media-library-add-form-wrapper',
+ ],
+ ],
+ 'save_insert' => [
+ '#type' => 'submit',
+ '#value' => $this->t('Save and insert'),
+ '#ajax' => [
+ 'callback' => '::updateWidget',
+ 'wrapper' => 'media-library-add-form-wrapper',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * 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 InvokeCommand('#media-library-add-form-wrapper :tabbable', 'focus'));
+ }
+ // 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(".media-library-add-form__media[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('.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));
+
+ $response = new AjaxResponse();
+ $response->addCommand(new UpdateSelectionCommand($media_ids));
+ $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
+ 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 a 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.
+ $current_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);
+ return $this->openerResolver->get($state)
+ ->getSelectionResponse($state, $current_media_ids)
+ ->addCommand(new CloseDialogCommand());
+ }
+
+ /**
+ * 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);
+ }
+
+}
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index d6a90314df..5cda7943e1 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -135,6 +135,284 @@ public function testAdministrationPage() {
$assert_session->linkExists('Add media');
}
+ /**
+ * Tests that the widget works as expected when media types are deleted.
+ */
+ public function testWidgetWithoutMediaTypes() {
+ $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. Please 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', '.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', '.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', '.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', '.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', '.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', '.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', '.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', '.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', '.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', '.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', '.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. Edit the field settings to select the allowed media types.';
+
+ // 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', '.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', '.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', '.media-library-open-button[name^="field_null_types_media"]');
+ }
+
+ /**
+ * Tests that the integration with Views works correctly.
+ */
+ public function testViewsAdmin() {
+ $assert_session = $this->assertSession();
+ $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');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->elementsCount('css', '.media-library-item', 8);
+
+ // Assert that filtering works in live preview.
+ $page->find('css', '.media-library-view .view-filters')->fillField('name', 'snake');
+ $page->find('css', '.media-library-view .view-filters')->pressButton('Apply filters');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->elementsCount('css', '.media-library-item', 1);
+
+ // Test the same routine but in the view for the table wiget.
+ $this->drupalGet('/admin/structure/views/view/media_library/edit/widget_table');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->elementsCount('css', '.media-library-item', 8);
+
+ // Assert that filtering works in live preview.
+ $page->find('css', '.media-library-view .view-filters')->fillField('name', 'snake');
+ $page->find('css', '.media-library-view .view-filters')->pressButton('Apply filters');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->elementsCount('css', '.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
+ }
+
+ /**
+ * Tests that the widget access works as expected.
+ */
+ public function testWidgetAccess() {
+ $assert_session = $this->assertSession();
+
+ // 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();
+ $unpublished_media_id = $unpublished_media->id();
+ // 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.
+ $this->getSession()->executeScript("jQuery('[data-media-library-widget-value=\"field_unlimited_media\"]').val('1,2,$unpublished_media_id')");
+ $this->getSession()->executeScript("jQuery('[data-media-library-widget-update=\"field_unlimited_media\"]').trigger('mousedown')");
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.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', '.view-media-library');
+ $this->drupalGet('admin/content/media-widget-table', $url_options);
+ $assert_session->elementExists('css', '.view-media-library');
+ $this->drupalGet('media-library', $url_options);
+ $assert_session->elementExists('css', '.view-media-library');
+ // 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', '.view-media-library');
+ $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 that the Media library's widget works as expected.
*/
@@ -435,6 +713,255 @@ public function testWidgetUpload() {
$files = $file_storage->loadMultiple();
$file = array_pop($files);
$this->assertSame('public://type-four-extra-dir', $file_system->dirname($file->getFileUri()));
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save and select');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Ensure the media item was saved to the library and automatically
+ // selected.
+ $assert_session->pageTextContains('Add or select media');
+ $assert_session->pageTextContains($file_system->basename($jpg_uri_2));
+ // Ensure the created item is added in the widget.
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
+ $this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextNotContains('Add or select media');
+ $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();
+ $unpublished_media_id = $unpublished_media->id();
+ $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->clickLink('Type Three');
+ $assert_session->assertWaitOnAjaxRequest();
+ // 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')");
+ $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_3));
+ $assert_session->assertWaitOnAjaxRequest();
+ // Assert the pre-selected items are shown.
+ $selection_area = $assert_session->elementExists('css', '.media-library-add-form__selected-media');
+ $assert_session->elementExists('css', 'summary', $selection_area)->click();
+ // 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');
+ $page->find('css', '.ui-dialog-titlebar-close')->click();
+
+ // Assert we can also remove selected items from the selection area in the
+ // upload form.
+ $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextContains('Add or select media');
+ $page->clickLink('Type Three');
+ $assert_session->assertWaitOnAjaxRequest();
+ $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://');
+ $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_uri_5));
+ $assert_session->assertWaitOnAjaxRequest();
+ // Assert the pre-selected items are shown.
+ $selection_area = $assert_session->elementExists('css', '.media-library-add-form__selected-media');
+ $assert_session->elementExists('css', 'summary', $selection_area)->click();
+ $assert_session->checkboxChecked("Select $existing_media_name", $selection_area);
+ $page->uncheckField("Select $existing_media_name");
+ $page->fillField('Alternative text', $this->randomString());
+ $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
+ $this->click('details.media-library-add-form__selected-media summary');
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save and select');
+ $assert_session->assertWaitOnAjaxRequest();
+ $media_items = Media::loadMultiple();
+ $added_media = array_pop($media_items);
+ $added_media_name = $added_media->label();
+ $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());
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
+ $this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextNotContains('Add or select media');
+ $assert_session->pageTextContains($file_system->basename($png_uri_5));
+
+ // Assert removing an uploaded media item before save works as expected.
+ $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextContains('Add or select media');
+ $page->clickLink('Type Three');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
+ $assert_session->assertWaitOnAjaxRequest();
+ // Assert the focus is shifted to the added media items.
+ $this->assertJsCondition('jQuery(".media-library-add-form__added-media").is(":focus")');
+ // Assert the media item fields are shown and the vertical tabs are no
+ // longer shown.
+ $assert_session->elementExists('css', '.media-library-add-form__fields');
+ $assert_session->elementNotExists('css', '.media-library-menu');
+ // Press the 'Remove button' and assert the user is sent back to the media
+ // library.
+ $assert_session->elementExists('css', '.media-library-add-form__remove-button')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ // Assert the remove message is shown.
+ $assert_session->pageTextContains("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->assertJsCondition('jQuery("#media-library-add-form-wrapper :tabbable").is(":focus")');
+ $assert_session->elementNotExists('css', '.media-library-add-form__fields');
+ $assert_session->elementExists('css', '.media-library-menu');
+ $page->find('css', '.ui-dialog-titlebar-close')->click();
+
+ // Assert uploading multiple files.
+ $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextContains('Add or select media');
+ $page->clickLink('Type Three');
+ $assert_session->assertWaitOnAjaxRequest();
+ // 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, 3) 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_session->assertWaitOnAjaxRequest();
+ // Assert the media item fields are shown and the vertical tabs are no
+ // longer shown.
+ $assert_session->elementExists('css', '.media-library-add-form__fields');
+ $assert_session->elementNotExists('css', '.media-library-menu');
+ // 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 the pre-selected items are shown.
+ $selection_area = $assert_session->elementExists('css', '.media-library-add-form__selected-media');
+ $assert_session->elementExists('css', 'summary', $selection_area)->click();
+ $assert_session->checkboxChecked("Select $existing_media_name", $selection_area);
+ // Set alt texts for items 1 and 2, leave the alt text empty for item 3 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]);
+ // 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(".media-library-add-form__media[data-media-library-added-delta=2]").is(":focus")');
+ $assert_session->pageTextContains('The media item ' . $filenames[1] . ' has been removed.');
+ // The second media item should be removed (this has the delta 1 since we
+ // start counting from 0).
+ $assert_session->elementNotExists('css', '.media-library-add-form__media[data-media-library-added-delta=1]');
+ $media_item_one = $assert_session->elementExists('css', '.media-library-add-form__media[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', '.media-library-add-form__media[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.
+ $selection_area = $assert_session->elementExists('css', '.media-library-add-form__selected-media');
+ $assert_session->elementExists('css', 'summary', $selection_area)->click();
+ $assert_session->checkboxChecked("Select $existing_media_name", $selection_area);
+ // 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->assertJsCondition('jQuery(".media-library-add-form__media[data-media-library-added-delta=0]").is(":focus")');
+ $assert_session->pageTextContains('The media item ' . $filenames[2] . ' has been removed.');
+ $assert_session->elementNotExists('css', '.media-library-add-form__media[data-media-library-added-delta=1]');
+ $assert_session->elementNotExists('css', '.media-library-add-form__media[data-media-library-added-delta=2]');
+ $media_item_one = $assert_session->elementExists('css', '.media-library-add-form__media[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);
+ }
+
+ /**
+ * Tests that oEmbed media can be added in the Media library's widget.
+ */
+ public function testWidgetOEmbed() {
+ $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.
+ $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextContains('Add or select 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.
+ $page->clickLink('Type Three');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->fieldNotExists('Add Type Five via URL');
+
+ // Assert we can add an oEmbed video to media type five.
+ $page->clickLink('Type Five');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->fillField('Add Type Five via URL', $youtube_url);
+ $assert_session->pageTextContains('Allowed providers: YouTube, Vimeo.');
+ $page->pressButton('Add');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Assert the name field contains the remote video title.
+ $assert_session->fieldValueEquals('Name', $youtube_title);
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save and select');
+ $assert_session->assertWaitOnAjaxRequest();
+
+ // Load the created media item.
+ $media_items = Media::loadMultiple();
+ $added_media = array_pop($media_items);
+
+ // 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[0]', $added_media->id());
+ $assert_session->checkboxChecked('media_library_select_form[0]');
+
+ // Assert the created oEmbed video is correctly added to the widget.
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
+ $this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextNotContains('Add or select media');
+ $assert_session->pageTextContains($youtube_title);
+
+ // Open the media library again for the unlimited field and go to the tab
+ // for media type five.
+ $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextContains('Add or select media');
+ $page->clickLink('Type Five');
+ $assert_session->assertWaitOnAjaxRequest();
+
+ // Assert the video is available on the tab.
+ $assert_session->pageTextContains($youtube_title);
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save');
$assert_session->assertWaitOnAjaxRequest();
commit 4abdf6e3f915897b6364f9d2e544e6ffa01d2f46
Author: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Wed Dec 18 18:55:11 2019 +1000
SA-CORE-2019-012 by samuel.mortenson, larowlan, pwolanin, Sam152, Jasu_M, David_Rothstein, michieltcs, Ayesh, alexpott, xjm, vijaycs85, mcdruid
diff --git a/core/lib/Drupal/Core/Archiver/Tar.php b/core/lib/Drupal/Core/Archiver/Tar.php
index 3b33dddfe4..ecc62abd8f 100644
--- a/core/lib/Drupal/Core/Archiver/Tar.php
+++ b/core/lib/Drupal/Core/Archiver/Tar.php
@@ -54,10 +54,10 @@ public function remove($file_path) {
*/
public function extract($path, array $files = []) {
if ($files) {
- $this->tar->extractList($files, $path);
+ $this->tar->extractList($files, $path, '', FALSE, FALSE);
}
else {
- $this->tar->extract($path);
+ $this->tar->extract($path, FALSE, FALSE);
}
return $this;
diff --git a/core/modules/config/src/Form/ConfigImportForm.php b/core/modules/config/src/Form/ConfigImportForm.php
index 28852f24ea..c07288ecb4 100644
--- a/core/modules/config/src/Form/ConfigImportForm.php
+++ b/core/modules/config/src/Form/ConfigImportForm.php
@@ -99,7 +99,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($archiver->listContent() as $file) {
$files[] = $file['filename'];
}
- $archiver->extractList($files, config_get_config_directory(CONFIG_SYNC_DIRECTORY));
+ $archiver->extractList($files, config_get_config_directory(CONFIG_SYNC_DIRECTORY), '', FALSE, FALSE);
$this->messenger()->addStatus($this->t('Your configuration files were successfully uploaded and are ready for import.'));
$form_state->setRedirect('config.sync');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment