diff --git a/islandora.routing.yml b/islandora.routing.yml index 58dea1a2..5ca9e348 100644 --- a/islandora.routing.yml +++ b/islandora.routing.yml @@ -57,3 +57,11 @@ islandora.media_source_put_to_node: _custom_access: '\Drupal\islandora\Controller\MediaSourceController::putToNodeAccess' options: _auth: ['basic_auth', 'cookie', 'jwt_auth'] + +islandora.confirm_delete_media_and_file: + path: '/media/delete_with_files' + defaults: + _form: 'Drupal\islandora\Form\ConfirmDeleteMediaAndFile' + requirements: + _permission: 'administer media+delete any media' + diff --git a/src/Form/ConfirmDeleteMediaAndFile.php b/src/Form/ConfirmDeleteMediaAndFile.php new file mode 100644 index 00000000..85b01209 --- /dev/null +++ b/src/Form/ConfirmDeleteMediaAndFile.php @@ -0,0 +1,182 @@ +currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + $this->tempStore = $temp_store_factory->get('media_and_file_delete_confirm'); + $this->messenger = $messenger; + $this->mediaSourceService = $media_source_service; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('tempstore.private'), + $container->get('messenger'), + $container->get('islandora.media_source_service'), + $container->get('logger.channel.islandora')); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'media_and_file_delete_confirm_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->formatPlural(count($this->selection), + 'Are you sure you want to delete this media and associated files?', + 'Are you sure you want to delete these media and associated files?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.media.collection'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) { + return parent::buildForm($form, $form_state, 'media'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Similar to parent::submitForm(), but let's blend in the related files and + // optimize based on the fact that we know we're working with media. + $total_count = 0; + $delete_media = []; + $delete_media_translations = []; + $delete_files = []; + $inaccessible_entities = []; + $media_storage = $this->entityTypeManager->getStorage('media'); + $file_storage = $this->entityTypeManager->getStorage('file'); + $media = $media_storage->loadMultiple(array_keys($this->selection)); + foreach ($this->selection as $id => $selected_langcodes) { + $entity = $media[$id]; + if (!$entity->access('delete', $this->currentUser)) { + $inaccessible_entities[] = $entity; + continue; + } + // Check for files. + $source_field = $this->mediaSourceService->getSourceFieldName($entity->bundle()); + foreach ($entity->get($source_field)->referencedEntities() as $file) { + if (!$file->access('delete', $this->currentUser)) { + $inaccessible_entities[] = $file; + continue; + } + $delete_files[$file->id()] = $file; + $total_count++; + } + foreach ($selected_langcodes as $langcode) { + // We're only working with media, which are translatable. + $entity = $entity->getTranslation($langcode); + if ($entity->isDefaultTranslation()) { + $delete_media[$id] = $entity; + unset($delete_media_translations[$id]); + $total_count += count($entity->getTranslationLanguages()); + } + elseif (!isset($delete_media[$id])) { + $delete_media_translations[$id][] = $entity; + } + } + } + if ($delete_media) { + $media_storage->delete($delete_media); + foreach ($delete_media as $entity) { + $this->logger->notice('The media %label has been deleted.', [ + '%label' => $entity->label(), + ]); + } + } + if ($delete_files) { + $file_storage->delete($delete_files); + foreach ($delete_files as $entity) { + $this->logger->notice('The file %label has been deleted.', [ + '%label' => $entity->label(), + ]); + } + } + if ($delete_media_translations) { + foreach ($delete_media_translations as $id => $translations) { + $entity = $media[$id]; + foreach ($translations as $translation) { + $entity->removeTranslation($translation->language()->getId()); + } + $entity->save(); + foreach ($translations as $translation) { + $this->logger->notice('The media %label @language translation has been deleted', [ + '%label' => $entity->label(), + '@language' => $translation->language()->getName(), + ]); + } + $total_count += count($translations); + } + } + if ($total_count) { + $this->messenger->addStatus($this->getDeletedMessage($total_count)); + } + if ($inaccessible_entities) { + $this->messenger->addWarning($this->getInaccessibleMessage(count($inaccessible_entities))); + } + $this->tempStore->delete($this->currentUser->id()); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/src/Plugin/Action/DeleteMediaAndFile.php b/src/Plugin/Action/DeleteMediaAndFile.php index 2e9d1df1..d92d39b8 100644 --- a/src/Plugin/Action/DeleteMediaAndFile.php +++ b/src/Plugin/Action/DeleteMediaAndFile.php @@ -2,13 +2,10 @@ namespace Drupal\islandora\Plugin\Action; -use Drupal\Core\Action\ActionBase; -use Drupal\Core\Database\Connection; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Action\Plugin\Action\DeleteAction; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\islandora\MediaSource\MediaSourceService; -use Psr\Log\LoggerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\TempStore\PrivateTempStoreFactory; /** * Deletes a media and its source file. @@ -16,105 +13,35 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * @Action( * id = "delete_media_and_file", * label = @Translation("Delete media and file"), - * type = "media" + * type = "media", + * confirm_form_route_name = "islandora.confirm_delete_media_and_file" * ) */ -class DeleteMediaAndFile extends ActionBase implements ContainerFactoryPluginInterface { - - /** - * Media source service. - * - * @var \Drupal\islandora\MediaSource\MediaSourceService - */ - protected $mediaSourceService; - - /** - * Database connection. - * - * @var \Drupal\Core\Database\Connection - */ - protected $connection; - - /** - * Logger. - * - * @var Psr\Log\LoggerInterface - */ - protected $logger; - - /** - * Constructor. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin ID for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source_service - * Media source service. - * @param \Drupal\Core\Database\Connection $connection - * Database connection. - * @param Psr\Log\LoggerInterface $logger - * Logger. - */ - public function __construct( - array $configuration, - $plugin_id, - $plugin_definition, - MediaSourceService $media_source_service, - Connection $connection, - LoggerInterface $logger - ) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->mediaSourceService = $media_source_service; - $this->connection = $connection; - $this->logger = $logger; - } +class DeleteMediaAndFile extends DeleteAction { /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('islandora.media_source_service'), - $container->get('database'), - $container->get('logger.channel.islandora') - ); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) { + $this->currentUser = $current_user; + $this->tempStore = $temp_store_factory->get('media_and_file_delete_confirm'); + $this->entityTypeManager = $entity_type_manager; + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; } /** * {@inheritdoc} */ - public function execute($entity = NULL) { - if (!$entity) { - return; - } + public function executeMultiple(array $entities) { - $transaction = $this->connection->startTransaction(); - - try { - // Delete all the source files and then the media. - $source_field = $this->mediaSourceService->getSourceFieldName($entity->bundle()); - foreach ($entity->get($source_field)->referencedEntities() as $file) { - $file->delete(); - } - $entity->delete(); - } - catch (\Exception $e) { - $transaction->rollBack(); - $this->logger->error("Cannot delete media and its files. Rolling back transaction: @msg", ["@msg" => $e->getMessage()]); + $selection = []; + foreach ($entities as $entity) { + $langcode = $entity->language()->getId(); + $selection[$entity->id()][$langcode] = $langcode; } - } - - /** - * {@inheritdoc} - */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - return $object->access('delete', $account, $return_as_object); + $this->tempStore->set("{$this->currentUser->id()}:media", $selection); } } diff --git a/tests/src/Functional/DeleteMediaTest.php b/tests/src/Functional/DeleteMediaTest.php index 03192194..f112c700 100644 --- a/tests/src/Functional/DeleteMediaTest.php +++ b/tests/src/Functional/DeleteMediaTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\islandora\Functional; +use Drupal\views\Views; + /** * Tests the DeleteMedia and DeleteMediaAndFile actions. * @@ -9,6 +11,18 @@ namespace Drupal\Tests\islandora\Functional; */ class DeleteMediaTest extends IslandoraFunctionalTestBase { + /** + * Modules to be enabled. + * + * @var array + */ + public static $modules = [ + 'media_test_views', + 'context_ui', + 'field_ui', + 'islandora', + ]; + /** * Media. * @@ -23,6 +37,13 @@ class DeleteMediaTest extends IslandoraFunctionalTestBase { */ protected $file; + /** + * User account. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $account; + /** * {@inheritdoc} */ @@ -30,9 +51,9 @@ class DeleteMediaTest extends IslandoraFunctionalTestBase { parent::setUp(); // Create a test user. - $account = $this->createUser(['create media']); + $this->account = $this->createUser(['create media', 'delete any media']); - list($this->file, $this->media) = $this->makeMediaAndFile($account); + list($this->file, $this->media) = $this->makeMediaAndFile($this->account); } /** @@ -41,12 +62,31 @@ class DeleteMediaTest extends IslandoraFunctionalTestBase { * @covers \Drupal\islandora\Plugin\Action\DeleteMediaAndFile::execute */ public function testDeleteMediaAndFile() { - $action = $this->container->get('entity_type.manager')->getStorage('action')->load('delete_media_and_file'); + $this->drupalLogin($this->account); + $session = $this->getSession(); + $page = $session->getPage(); $mid = $this->media->id(); $fid = $this->file->id(); - $action->execute([$this->media]); + // Ensure the media is in the test view. + $view = Views::getView('test_media_bulk_form'); + $view->execute(); + $this->assertSame($view->total_rows, 1); + + $this->drupalGet('test-media-bulk-form'); + + // Check that the option exists. + $this->assertSession()->optionExists('action', 'delete_media_and_file'); + + // Run the bulk action. + $page->checkField('media_bulk_form[0]'); + $page->selectFieldOption('action', 'delete_media_and_file'); + $page->pressButton('Apply to selected items'); + $this->assertSession()->pageTextContains('Are you sure you want to delete this media and associated files?'); + $page->pressButton('Delete'); + // Should assert that a media and file were deleted. + $this->assertSession()->pageTextContains('Deleted 2 items.'); // Attempt to reload the entities. // Both media and file should be gone.