diff --git a/includes/orphaned_objects.inc b/includes/orphaned_objects.inc new file mode 100644 index 00000000..502177f9 --- /dev/null +++ b/includes/orphaned_objects.inc @@ -0,0 +1,275 @@ + 'item', + '#markup' => format_plural(count($form_state['pids_to_delete']), + 'Are you sure you want to delete this object? This action cannot be reversed.', + 'Are you sure you want to delete these @count objects? This action cannot be reversed.'), + ); + if (count($pids) <= 10) { + $form['pids_to_delete'] = array( + '#type' => 'markup', + '#theme' => 'item_list', + '#list_type' => 'ol', + ); + $options = array('attributes' => array('target' => '_blank')); + foreach ($pids as $pid) { + $form['pids_to_delete']['#items'][] = l($pid, "/islandora/object/{$pid}", $options); + } + } + $form['confirm_submit'] = array( + '#type' => 'submit', + '#value' => t('Confirm'), + '#weight' => 2, + '#submit' => array('islandora_manage_orphaned_objects_confirm_submit'), + ); + $form['cancel_submit'] = array( + '#type' => 'submit', + '#value' => t('Cancel'), + '#weight' => 3, + ); + } + else { + drupal_set_message(t('This page lists objects that have at least one parent, according to their RELS-EXT, that does not + exist in the Fedora repository. These orphans might exist due to a failed batch ingest, their parents being deleted, + or a variety of other reasons. Some of these orphans may exist intentionally. + Please be cautious when deleting, as this action is irreversible.'), 'warning'); + $orphaned_objects = islandora_get_orphaned_objects(); + $rows = array(); + foreach ($orphaned_objects as $orphaned_object) { + $pid = $orphaned_object['object']['value']; + if (islandora_namespace_accessible($pid)) { + $rows[$pid] = array(l($orphaned_object['title']['value'] . " (" . $pid . ")", "islandora/object/$pid")); + } + } + ksort($rows); + $form['management_table'] = array( + '#type' => 'tableselect', + '#header' => array(t('Object')), + '#options' => $rows, + '#attributes' => array(), + '#empty' => t('No orphaned objects were found.'), + ); + if (!empty($rows)) { + $form['submit_selected'] = array( + '#type' => 'submit', + '#name' => 'islandora-orphaned-objects-submit-selected', + '#validate' => array('islandora_delete_selected_orphaned_objects_validate'), + '#submit' => array('islandora_delete_orphaned_objects_submit'), + '#value' => t('Delete Selected'), + ); + $form['submit_all'] = array( + '#type' => 'submit', + '#name' => 'islandora-orphaned-objects-submit-all', + '#submit' => array('islandora_delete_orphaned_objects_submit'), + '#value' => t('Delete All'), + ); + } + } + return $form; +} + +/** + * Validation for the Islandora Orphaned Objects management form. + * + * @param array $form + * An array representing a form within Drupal. + * @param array $form_state + * An array containing the Drupal form state. + */ +function islandora_delete_selected_orphaned_objects_validate($form, $form_state) { + $selected = array_filter($form_state['values']['management_table']); + if (empty($selected)) { + form_error($form['management_table'], t('At least one object must be selected to delete!')); + } +} +/** + * Submit handler for the delete buttons in the workflow management form. + * + * @param array $form + * An array representing a form within Drupal. + * @param array $form_state + * An array containing the Drupal form state. + */ +function islandora_delete_orphaned_objects_submit(&$form, &$form_state) { + if ($form_state['triggering_element']['#name'] == 'islandora-orphaned-objects-submit-selected') { + $selected = array_keys(array_filter($form_state['values']['management_table'])); + } + else { + $selected = array_keys($form_state['values']['management_table']); + } + $form_state['pids_to_delete'] = $selected; + // Rebuild to show the confirm form. + $form_state['rebuild'] = TRUE; + $form_state['show_confirm'] = TRUE; +} +/** + * Submit handler for the workflow management confirm form. + * + * @param array $form + * An array representing a form within Drupal. + * @param array $form_state + * An array containing the Drupal form state. + */ +function islandora_manage_orphaned_objects_confirm_submit($form, &$form_state) { + $batch = islandora_delete_orphaned_objects_create_batch($form_state['pids_to_delete']); + batch_set($batch); +} + +/** + * Query for orphaned objects. + * + * @return array + * An array containing the results of the orphaned objects queries. + */ +function islandora_get_orphaned_objects() { + $connection = islandora_get_tuque_connection(); + // SPARQL: get orphaned objects, exclude any with a living parent. + $object_query = << +SELECT DISTINCT ?object ?title +WHERE { + ?object ; + ?p ?otherobject . + ?object ?title; + OPTIONAL { + ?otherobject ?model . + } . + FILTER (!bound(?model)) + + # Filter by "parent" relationships - isMemberOf, isMemberOfCollection, isPageOf. + FILTER (?p = || ?p = || ?p = ) + +# Exclude objects with live parents - isMemberOf, isMemberOfCollection, isPageOf. + OPTIONAL { + {?object ?liveparent } + UNION {?object ?liveparent } + UNION { ?object ?liveparent } . + ?liveparent . + } + !optionals + !filters + FILTER (!bound(?liveparent)) +} ORDER BY ?object +EOQ; + + $optionals = (array) module_invoke('islandora_xacml_api', 'islandora_basic_collection_get_query_optionals', 'view'); + $filter_modules = array( + 'islandora_xacml_api', + 'islandora', + ); + $filters = array(); + foreach ($filter_modules as $module) { + $filters = array_merge_recursive($filters, (array) module_invoke($module, 'islandora_basic_collection_get_query_filters', 'view')); + } + $filter_map = function ($filter) { + return "FILTER($filter)"; + }; + // Use separate queries for different object types. + $sparql_query_objects = format_string($object_query, array( + '!optionals' => !empty($optionals) ? ('OPTIONAL {{' . implode('} UNION {', $optionals) . '}}') : '', + '!filters' => !empty($filters) ? implode(' ', array_map($filter_map, $filters)) : '', + )); + $results = $connection->repository->ri->sparqlQuery($sparql_query_objects); + return $results; +} + +/** + * Constructs the batch that will go out and delete objects. + * + * @param array $pids + * The array of pids to be deleted. + * + * @return array + * An array detailing the batch that is about to be run. + */ +function islandora_delete_orphaned_objects_create_batch($pids) { + // Set up a batch operation. + $batch = array( + 'operations' => array( + array('islandora_delete_orphaned_objects_batch_operation', array($pids)), + ), + 'title' => t('Deleting the selected objects...'), + 'init_message' => t('Preparing to delete objects.'), + 'progress_message' => t('Time elapsed: @elapsed
Estimated time remaining @estimate.'), + 'error_message' => t('An error has occurred.'), + 'finished' => 'islandora_delete_orphaned_objects_batch_finished', + 'file' => drupal_get_path('module', 'islandora') . '/includes/orphaned_objects.inc', + ); + return $batch; +} +/** + * Constructs and performs the deleting batch operation. + * + * @param array $pids + * An array of pids to be deleted. + * @param array $context + * The context of the Drupal batch. + */ +function islandora_delete_orphaned_objects_batch_operation($pids, &$context) { + if (empty($context['sandbox'])) { + $context['sandbox'] = array(); + $context['sandbox']['progress'] = 0; + $context['sandbox']['pids'] = $pids; + $context['sandbox']['total'] = count($pids); + $context['results']['success'] = array(); + } + if (!empty($context['sandbox']['pids'])) { + $target_pid = array_pop($context['sandbox']['pids']); + $target_object = islandora_object_load($target_pid); + $context['message'] = t('Deleting @label (@pid) (@current of @total)...', array( + '@label' => $target_object->label, + '@pid' => $target_pid, + '@current' => $context['sandbox']['progress'], + '@total' => $context['sandbox']['total'], + )); + islandora_delete_object($target_object); + $object_check = islandora_object_load($target_pid); + if ($object_check) { + drupal_set_message(t('Could not delete ' . $target_pid . '. You may not have permission to manage this object.'), 'error'); + } + else { + $context['results']['success'][] = $target_pid; + } + $context['sandbox']['progress']++; + } + $context['finished'] = ($context['sandbox']['total'] == 0) ? 1 : ($context['sandbox']['progress'] / $context['sandbox']['total']); +} +/** + * Finished function for the orphaned objects delete batch. + * + * @param bool $success + * Whether the batch was successful or not. + * @param array $results + * An array containing the results of the batch operations. + * @param array $operations + * The operations array that was used in the batch. + */ +function islandora_delete_orphaned_objects_batch_finished($success, $results, $operations) { + if ($success) { + $message = format_plural(count($results['success']), 'One object deleted.', '@count objects deleted.'); + } + else { + $message = t('Finished with an error.'); + } + drupal_set_message($message); +} diff --git a/islandora.module b/islandora.module index 3349902c..f9276404 100644 --- a/islandora.module +++ b/islandora.module @@ -39,6 +39,7 @@ define('ISLANDORA_MANAGE_DELETED_OBJECTS', 'manage deleted objects'); define('ISLANDORA_REVERT_DATASTREAM', 'revert to old datastream'); define('ISLANDORA_REGENERATE_DERIVATIVES', 'regenerate derivatives for an object'); define('ISLANDORA_REPLACE_DATASTREAM_CONTENT', 'replace a datastream with new content, preserving version history'); +define('ISLANDORA_MANAGE_ORPHANED_OBJECTS', 'view and delete a list of orphaned objects'); // Hooks. define('ISLANDORA_VIEW_HOOK', 'islandora_view_object'); @@ -397,7 +398,14 @@ function islandora_menu() { 'type' => MENU_NORMAL_ITEM, 'access arguments' => array(ISLANDORA_MANAGE_DELETED_OBJECTS), ); - + $items['admin/reports/orphaned_objects/list'] = array( + 'title' => 'Orphaned Islandora objects', + 'description' => 'List of orphaned Islandora objects.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('islandora_manage_orphaned_objects_form'), + 'access arguments' => array(ISLANDORA_MANAGE_ORPHANED_OBJECTS), + 'file' => 'includes/orphaned_objects.inc', + ); $items['admin/islandora/restore/manage'] = array( 'description' => 'Restore or permanantly remove objects with Deleted status', 'title' => 'Manage Deleted Objects', @@ -624,6 +632,10 @@ function islandora_permission() { 'title' => t('Replace datastreams'), 'description' => t('Add new datastream content as latest version.'), ), + ISLANDORA_MANAGE_ORPHANED_OBJECTS => array( + 'title' => t('Manage orphaned Islandora objects'), + 'description' => t('View a list of orphaned Islandora objects.'), + ), ); if (variable_get('islandora_deny_inactive_and_deleted', FALSE)) { $permissions[ISLANDORA_ACCESS_INACTIVE_AND_DELETED_OBJECTS] = array(