diff --git a/config/install/islandora.settings.yml b/config/install/islandora.settings.yml index 8fb25fb8..179d4213 100644 --- a/config/install/islandora.settings.yml +++ b/config/install/islandora.settings.yml @@ -1,4 +1,5 @@ broker_url: 'tcp://localhost:61613' jwt_expiry: '+2 hour' gemini_url: '' +delete_media_and_files: TRUE gemini_pseudo_bundles: [] diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index e85d5739..86b65fd0 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -14,6 +14,9 @@ islandora.settings: jwt_expiry: type: string label: 'How long JWTs should last before expiring.' + delete_media_and_files: + type: boolean + label: 'Node Delete with Media and Files' upload_form_location: type: string label: 'Upload Form Location' @@ -166,7 +169,7 @@ condition.plugin.node_had_namespace: pid_field: type: ignore label: 'PID field' - + field.formatter.settings.islandora_image: type: field.formatter.settings.image label: 'Islandora image field display format settings' diff --git a/css/islandora.css b/css/islandora.css new file mode 100644 index 00000000..696587ac --- /dev/null +++ b/css/islandora.css @@ -0,0 +1,3 @@ +.container .islandora-media-items { + margin: 0; +} diff --git a/islandora.libraries.yml b/islandora.libraries.yml new file mode 100644 index 00000000..840dc294 --- /dev/null +++ b/islandora.libraries.yml @@ -0,0 +1,5 @@ +islandora: + version: VERSION + css: + theme: + css/islandora.css: {} diff --git a/islandora.module b/islandora.module index 9814820e..6f7a15d2 100644 --- a/islandora.module +++ b/islandora.module @@ -27,6 +27,8 @@ use Drupal\file\FileInterface; use Drupal\taxonomy\TermInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\serialization\Normalizer\CacheableNormalizerInterface; +use Drupal\Core\Entity\EntityForm; +use Drupal\file\Entity\File; /** * Implements hook_help(). @@ -331,6 +333,197 @@ function islandora_form_alter(&$form, FormStateInterface $form_state, $form_id) } } } + + $form_object = $form_state->getFormObject(); + $utils = \Drupal::service('islandora.utils'); + $config = \Drupal::config('islandora.settings')->get('delete_media_and_files'); + + if ($config == 1 && $form_object instanceof EntityForm) { + $entity = $form_object->getEntity(); + + if ($entity->getEntityTypeId() == "node" && $utils->isIslandoraType($entity->getEntityTypeId(), $entity->bundle()) && strpos($form['#form_id'], 'delete_form') !== FALSE) { + $medias = $utils->getMedia($form_state->getFormObject()->getEntity()); + if (count($medias) != 0) { + $form['delete_associated_content'] = [ + '#type' => 'checkbox', + '#title' => t('Delete all associated medias and nodes'), + ]; + + $media_list = []; + + foreach ($medias as $media) { + $media_list[] = $media->getName(); + } + + $form['container'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + ':input[name="delete_associated_content"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['container']['media_items'] = [ + '#theme' => 'item_list', + '#type' => 'ul', + '#items' => $media_list, + '#attributes' => ['class' => ['islandora-media-items']], + '#wrapper_attributes' => ['class' => ['container']], + '#attached' => [ + 'library' => [ + 'islandora/islandora', + ], + ], + ]; + + $form['actions']['submit']['#submit'][] = 'islandora_object_delete_form_submit'; + return $form; + } + } + } + + return $form; +} + +/** + * Implements a submit handler for the delete form. + */ +function islandora_object_delete_form_submit($form, FormStateInterface $form_state) { + + $result = $form_state->getValues('delete_associated_content'); + $utils = \Drupal::service('islandora.utils'); + + if ($result['delete_associated_content'] == 1) { + + $node = $form_state->getFormObject()->getEntity(); + $medias = $utils->getMedia($node); + $media_list = []; + + $entity_field_manager = \Drupal::service('entity_field.manager'); + $current_user = \Drupal::currentUser(); + $logger = \Drupal::logger('logger.channel.islandora'); + $messenger = \Drupal::messenger(); + + $delete_media = []; + $media_translations = []; + $media_files = []; + $entity_protected_medias = []; + $inaccessible_entities = []; + + foreach ($medias as $id => $media) { + $lang = $media->language()->getId(); + $selected_langcodes[$lang] = $lang; + + if (!$media->access('delete', $current_user)) { + $inaccessible_entities[] = $media; + continue; + } + // Check for files. + $fields = $entity_field_manager->getFieldDefinitions('media', $media->bundle()); + foreach ($fields as $field) { + $type = $field->getType(); + if ($type == 'file' || $type == 'image') { + $target_id = $media->get($field->getName())->target_id; + $file = File::load($target_id); + if ($file) { + if (!$file->access('delete', $current_user)) { + $inaccessible_entities[] = $file; + continue; + } + $media_files[$id][$file->id()] = $file; + } + } + } + + foreach ($selected_langcodes as $langcode) { + // We're only working with media, which are translatable. + $entity = $media->getTranslation($langcode); + if ($entity->isDefaultTranslation()) { + $delete_media[$id] = $entity; + unset($media_translations[$id]); + } + elseif (!isset($delete_media[$id])) { + $media_translations[$id][] = $entity; + } + } + } + + if ($delete_media) { + foreach ($delete_media as $id => $media) { + try { + $media->delete(); + $media_list[] = $id; + $logger->notice('The media %label has been deleted.', [ + '%label' => $media->label(), + ]); + } + catch (Exception $e) { + $entity_protected_medias[] = $id; + } + } + } + + $delete_files = array_filter($media_files, function ($media) use ($entity_protected_medias) { + return !in_array($media, $entity_protected_medias); + }, ARRAY_FILTER_USE_KEY); + + if ($delete_files) { + foreach ($delete_files as $files_array) { + foreach ($files_array as $file) { + $file->delete(); + $logger->notice('The file %label has been deleted.', [ + '%label' => $file->label(), + ]); + } + } + } + + $delete_media_translations = array_filter($media_translations, function ($media) use ($entity_protected_medias) { + return !in_array($media, $entity_protected_medias); + }, ARRAY_FILTER_USE_KEY); + + if ($delete_media_translations) { + foreach ($delete_media_translations as $id => $translations) { + $media = $medias[$id]; + foreach ($translations as $translation) { + $media->removeTranslation($translation->language()->getId()); + } + $media->save(); + foreach ($translations as $translation) { + $logger->notice('The media %label @language translation has been deleted', [ + '%label' => $media->label(), + '@language' => $translation->language()->getName(), + ]); + } + } + } + + if ($inaccessible_entities) { + $messenger->addWarning("@count items have not been deleted because you do not have the necessary permissions.", [ + '@count' => count($inaccessible_entities), + ]); + } + + $build = [ + 'heading' => [ + '#type' => 'html_tag', + '#tag' => 'div', + '#value' => t("The repository item @node and @media", [ + '@node' => $node->getTitle(), + '@media' => \Drupal::translation()->formatPlural( + count($media_list), 'the media with the id @media has been deleted.', + 'the medias with the ids @media have been deleted.', + ['@media' => implode(", ", $media_list)], + ), + ]), + ], + ]; + + $message = \Drupal::service('renderer')->renderPlain($build); + $messenger->deleteByType('status'); + $messenger->addStatus($message); + } } /** diff --git a/islandora.post_update.php b/islandora.post_update.php new file mode 100644 index 00000000..0a29e56e --- /dev/null +++ b/islandora.post_update.php @@ -0,0 +1,16 @@ +getEditable('islandora.settings'); + $config->set('delete_media_and_files', TRUE); + $config->save(TRUE); +} diff --git a/src/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php index 5049ef16..90e0b420 100644 --- a/src/Form/IslandoraSettingsForm.php +++ b/src/Form/IslandoraSettingsForm.php @@ -42,6 +42,7 @@ class IslandoraSettingsForm extends ConfigFormBase { 'year', ]; const GEMINI_PSEUDO_FIELD = 'field_gemini_uri'; + const NODE_DELETE_MEDIA_AND_FILES = 'delete_media_and_files'; /** * To list the available bundle types. @@ -201,6 +202,14 @@ class IslandoraSettingsForm extends ConfigFormBase { $fedora_url = NULL; } + $form[self::NODE_DELETE_MEDIA_AND_FILES] = [ + '#type' => 'checkbox', + '#title' => $this->t('Node Delete with Media and Files'), + '#description' => $this->t('Adds a checkbox in the "Delete" tab of islandora objects to delete media and files associated with the object.' + ), + '#default_value' => (bool) $config->get(self::NODE_DELETE_MEDIA_AND_FILES), + ]; + $form[self::FEDORA_URL] = [ '#type' => 'textfield', '#title' => $this->t('Fedora URL'), @@ -351,6 +360,7 @@ class IslandoraSettingsForm extends ConfigFormBase { ->set(self::UPLOAD_FORM_LOCATION, $form_state->getValue(self::UPLOAD_FORM_LOCATION)) ->set(self::UPLOAD_FORM_ALLOWED_MIMETYPES, $form_state->getValue(self::UPLOAD_FORM_ALLOWED_MIMETYPES)) ->set(self::GEMINI_PSEUDO, $new_pseudo_types) + ->set(self::NODE_DELETE_MEDIA_AND_FILES, $form_state->getValue(self::NODE_DELETE_MEDIA_AND_FILES)) ->save(); parent::submitForm($form, $form_state); diff --git a/tests/src/Functional/DeleteNodeWithMediaAndFile.php b/tests/src/Functional/DeleteNodeWithMediaAndFile.php new file mode 100644 index 00000000..40e469c5 --- /dev/null +++ b/tests/src/Functional/DeleteNodeWithMediaAndFile.php @@ -0,0 +1,104 @@ +drupalCreateUser([ + 'delete any media', + 'create media', + 'view media', + 'bypass node access', + 'access files overview', + 'administer site configuration', + ]); + $this->drupalLogin($account); + + $assert_session = $this->assertSession(); + + $testImageMediaType = $this->createMediaType('image', ['id' => 'test_image_media_type']); + $testImageMediaType->save(); + + $this->createEntityReferenceField('media', $testImageMediaType->id(), 'field_media_of', 'Media Of', 'node', 'default', [], 2); + + $node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => 'test_type', + 'title' => 'node', + ]); + $node->save(); + + // Make an image for the Media. + $file = $this->container->get('entity_type.manager')->getStorage('file')->create([ + 'uid' => $account->id(), + 'uri' => "public://test.jpeg", + 'filename' => "test.jpeg", + 'filemime' => "image/jpeg", + 'status' => FILE_STATUS_PERMANENT, + ]); + $file->save(); + + $this->drupalGet("node/1/delete"); + $assert_session->pageTextNotContains('Delete all associated medias and nodes'); + + // Make the media, and associate it with the image and node. + $media1 = $this->container->get('entity_type.manager')->getStorage('media')->create([ + 'bundle' => $testImageMediaType->id(), + 'name' => 'Media1', + 'field_media_image' => + [ + 'target_id' => $file->id(), + 'alt' => 'Some Alt', + 'title' => 'Some Title', + ], + 'field_media_of' => ['target_id' => $node->id()], + ]); + $media1->save(); + + $media2 = $this->container->get('entity_type.manager')->getStorage('media')->create([ + 'bundle' => $testImageMediaType->id(), + 'name' => 'Media2', + 'field_media_image' => + [ + 'target_id' => $file->id(), + 'alt' => 'Some Alt', + 'title' => 'Some Title', + ], + 'field_media_of' => ['target_id' => $node->id()], + ]); + $media2->save(); + + $this->drupalGet("admin/config/islandora/core"); + $assert_session->pageTextContains('Node Delete with Media and Files'); + \Drupal::configFactory()->getEditable('islandora.settings')->set('delete_media_and_files', TRUE)->save(); + + $delete = ['delete_associated_content' => TRUE]; + + $this->drupalGet("node/1/delete"); + $assert_session->pageTextContains('Media1'); + $assert_session->pageTextContains('Media2'); + $this->submitForm($delete, 'Delete'); + + $assert_session->pageTextContains($media1->id()); + $assert_session->pageTextContains($media2->id()); + + $this->drupalGet("media/1/delete"); + $assert_session->pageTextContains('Page not found'); + + $this->drupalGet("media/2/delete"); + $assert_session->pageTextContains('Page not found'); + + $this->drupalGet("/admin/content/files"); + $assert_session->pageTextNotContains('test.jpeg'); + + } + +}