diff --git a/README b/README index 1a3479e8..c9784a95 100644 --- a/README +++ b/README @@ -38,6 +38,14 @@ INSTALLATION CONFIGURATION ------------- +The islandora_drupal_filter passes the username of 'anonymous' through to +Fedora for unauthenticated Drupal Users. A user with the name of 'anonymous' +may have XACML policies applied to them that are meant to be applied to Drupal +users that are not logged in or vice-versa. This is a potential security issue +that can be plugged by creating a user named 'anonymous' and restricting access +to the account. + +Drupal's cron will can be ran to remove expired authentication tokens. CUSTOMIZATION ------------- diff --git a/includes/islandora_authtokens.inc b/includes/islandora_authtokens.inc new file mode 100644 index 00000000..f7861a0b --- /dev/null +++ b/includes/islandora_authtokens.inc @@ -0,0 +1,122 @@ + 5.3. + // We might be safe in this case because mt_rand should never be using + // the same seed, but this is still more secure. + $token = hash("sha256", mt_rand() . $time); + + $id = db_insert("islandora_authtokens")->fields( + array( + 'token' => $token, + 'uid' => $user->uid, + 'pid' => $pid, + 'dsid' => $dsid, + 'time' => $time, + 'remaining_uses' => $uses, + ))->execute(); + + return $token; +} + +/** + * Submit a token to islandora for authentication. Supply islandora with the + * token and the object/datastream it is for and you will receive access if + * authentication passes. Tokens can only be redeemed in a short window after + * their creation. + * + * @param string $pid + * The PID of the object to retrieve. + * @param string $dsid + * The datastream id to retrieve. + * @param string $token + * The registered token that allows access to this object. + * + * @return mixed + * The user credentials for access if the token validation passes, + * FALSE otherwise + */ +function islandora_validate_object_token($pid, $dsid, $token) { + // Check for database token. + $time = time(); + $query = db_select('islandora_authtokens', 'tokens'); + $query->join('users', 'u', 'tokens.uid = u.uid'); + // The results will look like user objects. + $result = $query + ->fields('u', array('uid', 'name', 'pass')) + ->fields('tokens', array('remaining_uses')) + ->condition('token', $token, '=') + ->condition('pid', $pid, '=') + ->condition('dsid', $dsid, '=') + ->condition('time', $time, '<=') + ->condition('time', $time - TOKEN_TIMEOUT, '>') + ->execute() + ->fetchAll(); + if ($result) { + $remaining_uses = $result[0]->remaining_uses; + $remaining_uses--; + // Remove the authentication token so it can't be used again. + if ($remaining_uses == 0) { + db_delete("islandora_authtokens") + ->condition('token', $token, '=') + ->condition('pid', $pid, '=') + ->condition('dsid', $dsid, '=') + ->execute(); + } + // Decrement authentication token uses. + else { + db_update("islandora_authtokens") + ->fields(array('remaining_uses' => $remaining_uses)) + ->condition('token', $token, '=') + ->condition('pid', $pid, '=') + ->condition('dsid', $dsid, '=') + ->execute(); + } + unset($result[0]->remaining_uses); + return $result[0]; + } + else { + return FALSE; + } +} + +/** + * Will remove any expired authentication tokens. + */ +function islandora_remove_expired_tokens() { + $time = time(); + db_delete("islandora_authtokens") + ->condition('time', $time - TOKEN_TIMEOUT, '<') + ->execute(); +} diff --git a/includes/solution_packs.inc b/includes/solution_packs.inc index d6fab446..21e22619 100644 --- a/includes/solution_packs.inc +++ b/includes/solution_packs.inc @@ -9,157 +9,143 @@ /** * Solution pack admin page callback. + * + * @return string + * The html repersentation of all solution pack forms for required objects. */ function islandora_solution_packs_admin() { module_load_include('inc', 'islandora', 'includes/utilities'); - drupal_add_css(drupal_get_path('module', 'islandora') . '/css/islandora.admin.css'); if (!islandora_describe_repository()) { - $message = t('Could not connect to the repository. Please check the settings on the ' . - 'Islandora configuration page.', - array('@config_url' => url('admin/islandora/configure'))); - drupal_set_message($message, 'error'); + islandora_display_repository_inaccessible_message(); + return ''; } - $enabled_solution_packs = module_invoke_all('islandora_required_objects'); + + $connection = islandora_get_tuque_connection(); + drupal_add_css(drupal_get_path('module', 'islandora') . '/css/islandora.admin.css'); $output = ''; + $enabled_solution_packs = module_invoke_all('islandora_required_objects', $connection); foreach ($enabled_solution_packs as $solution_pack_module => $solution_pack_info) { - $objects = array(); - foreach ($solution_pack_info as $field => $value) { - switch ($field) { - case 'title': - $solution_pack_name = $value; - break; - case 'objects': - $objects = $value; - break; - } - } - $form_array = drupal_get_form('islandora_solution_pack_form_' . $solution_pack_module, $solution_pack_module, $solution_pack_name, $objects); - $output .= drupal_render($form_array); + // @todo We should probably get the title of the solution pack from the + // systems table for consistency in the interface. + $solution_pack_name = $solution_pack_info['title']; + $objects = array_filter($solution_pack_info['objects']); + $form = drupal_get_form('islandora_solution_pack_form_' . $solution_pack_module, $solution_pack_module, $solution_pack_name, $objects); + $output .= drupal_render($form); } return $output; } /** - * Solution pack admin page + * A solution pack form for the given module, it lists all the given objects and + * their status, allowing the user to re-ingest them. + * + * @param array $form + * The Drupal form definition. + * @param array $form_state + * The Drupal form state. + * @param string $solution_pack_module + * The module which requires the given objects. + * @param string $solution_pack_name + * The name of the solution pack to display to the users. + * @param array $objects + * An array of NewFedoraObjects which describe the state in which objects + * should exist. + * + * @return array + * The Drupal form definition. */ -function islandora_solution_pack_form($form, &$form_state, $solution_pack_module, $solution_pack_name, $objects = array()) { - global $base_url; - $needs_update = FALSE; - $needs_install = FALSE; - $could_not_connect = FALSE; - $form = array(); - - $form['solution_pack'] = array( - '#type' => 'fieldset', - '#collapsible' => FALSE, - '#collapsed' => FALSE, - '#attributes' => array('class' => array('islandora-solution-pack-fieldset')), - ); - - // adding values - $form['solution_pack']['solution_pack_module'] = array( - '#type' => 'value', - '#value' => $solution_pack_module, - ); - $form['solution_pack']['solution_pack_name'] = array( - '#type' => 'value', - '#value' => $solution_pack_name, +function islandora_solution_pack_form(array $form, array &$form_state, $solution_pack_module, $solution_pack_name, $objects = array()) { + // The order is important in terms of severity of the status, where higher + // index indicates the status is more serious, this will be used to determine + // what messages get displayed to the user. + $ok_image = theme_image(array('path' => 'misc/watchdog-ok.png', 'attributes' => array())); + $warning_image = theme_image(array('path' => 'misc/watchdog-warning.png', 'attributes' => array())); + $status_info = array( + 'up_to_date' => array( + 'solution_pack' => t('All required objects are installed and up-to-date.'), + 'image' => $ok_image, + 'button' => t("Force reinstall objects"), + ), + 'modified_datastream' => array( + 'solution_pack' => t('Some objects must be reinstalled. See objects list for details.'), + 'image' => $warning_image, + 'button' => t("Reinstall objects") + ), + 'out_of_date' => array( + 'solution_pack' => t('Some objects must be reinstalled. See objects list for details.'), + 'image' => $warning_image, + 'button' => t("Reinstall objects") + ), + 'missing_datastream' => array( + 'solution_pack' => t('Some objects must be reinstalled. See objects list for details.'), + 'image' => $warning_image, + 'button' => t("Reinstall objects") + ), + 'missing' => array( + 'solution_pack' => t( 'Some objects are missing and must be installed. See objects list for details.'), + 'image' => $warning_image, + 'button' => t("Install objects") + ), ); - $form['solution_pack']['objects'] = array( - '#type' => 'value', - '#value' => $objects, - ); - - $table_header = array(t('Label'), t('PID'), t('Status')); + $status_severities = array_keys($status_info); + $solution_pack_status_severity = array_search('up_to_date', $status_severities); $table_rows = array(); - foreach ($objects as $object) { - $datastreams = NULL; - if (isset($object['pid'])) { - $pid = $object['pid']; - $table_row = array(); - $object_status = islandora_check_object_status($object); - switch ($object_status) { - case 'up_to_date': - $object_status = t('Up-to-date'); - break; - case 'missing': - $object_status = t('Missing'); - $needs_install = TRUE; - break; - case 'missing_datastream': - $object_status = t('Missing datastream'); - $needs_update = TRUE; - break; - case 'out_of_date': - $object_status = t('Out-of-date'); - $needs_update = TRUE; - break; - case 'could_not_connect': - $object_status = t('Could not connect'); - $could_not_connect = TRUE; - break; - } - if ($needs_install OR $could_not_connect) { - $label = $object['label'] ? $object['label'] : ''; - } - else { - $label = $object['label'] ? l($object['label'], $base_url . '/islandora/object/' . $pid) : ''; - } - $table_row[] = $label; - $table_row[] = $pid; - $table_row[] = $object_status; - $table_rows[] = $table_row; - } - } - - // title - if (!$form_state['submitted']) { - $form['solution_pack']['solution_pack_label'] = array( - '#markup' => filter_xss($solution_pack_name), - '#prefix' => '

', - '#suffix' => '

', - ); - - $form['solution_pack']['install_status'] = array( - '#markup' => '' . t('Object status:') . ' ', - '#prefix' => '
', - '#suffix' => '
', - ); - if (!$needs_install AND !$needs_update AND !$could_not_connect) { - $form['solution_pack']['install_status']['#markup'] .= ' ' . theme('image', array('path' => 'misc/watchdog-ok.png')) . ' ' . t('All required objects are installed and up-to-date.'); - $submit_button_text = t("Force reinstall objects"); - } - elseif ($needs_install) { - $form['solution_pack']['install_status']['#markup'] .= ' ' . theme('image', array('path' => 'misc/watchdog-warning.png')) . ' ' . t('Some objects are missing and must be installed. See objects list for details.'); - $submit_button_text = t("Install objects"); - } - elseif ($needs_update) { - $form['solution_pack']['install_status']['#markup'] .= ' ' . theme('image', array('path' => 'misc/watchdog-warning.png')) . ' ' . t('Some objects must be reinstalled. See objects list for details.'); - $submit_button_text = t("Reinstall objects"); - } - elseif ($could_not_connect) { - $form['solution_pack']['install_status']['#markup'] .= ' ' . theme('image', array('path' => 'misc/watchdog-error.png')) . ' ' . t('Could not connect to the repository.'); - $submit_button_text = ''; - } - - $form['solution_pack']['table'] = array( - '#type' => 'item', - '#markup' => theme('table', array('header' => $table_header, 'rows' => $table_rows)), - ); + $object_status = islandora_check_object_status($object); + $object_status_info = $status_info[$object_status['status']]; + $object_status_severity = array_search($object_status['status'], $status_severities); + // The solution pack status severity will be the highest severity of the objects. + $solution_pack_status_severity = max($solution_pack_status_severity, $object_status_severity); + $exists = $object_status['status'] != 'missing'; + $label = $exists ? l($object->label, "islandora/object/{$object->id}") : $object->label; + $status_msg = "{$object_status_info['image']} {$object_status['status_friendly']}"; + $table_rows[] = array($label, $object->id, $status_msg); } - - if (!$could_not_connect) { - $form['solution_pack']['submit'] = array( - '#value' => $submit_button_text, - '#type' => 'submit', - '#name' => $solution_pack_module, - '#attributes' => array('class' => array('islandora-solution-pack-submit')), - '#weight' => 40, - ); - $form['solution_pack']['#submit'] = array('islandora_solution_pack_form_submit'); - } - return $form; + $solution_pack_status = $status_severities[$solution_pack_status_severity]; + $solution_pack_status_info = $status_info[$solution_pack_status]; + return array( + 'solution_pack' => array( + '#type' => 'fieldset', + '#collapsible' => FALSE, + '#collapsed' => FALSE, + '#attributes' => array('class' => array('islandora-solution-pack-fieldset')), + 'solution_pack_module' => array( + '#type' => 'value', + '#value' => $solution_pack_module, + ), + 'solution_pack_name' => array( + '#type' => 'value', + '#value' => $solution_pack_name, + ), + 'objects' => array( + '#type' => 'value', + '#value' => $objects, + ), + 'solution_pack_label' => array( + '#markup' => $solution_pack_name, + '#prefix' => '

', + '#suffix' => '

', + ), + 'install_status' => array( + '#markup' => t('Object status: !image !status', array( + '!image' => $solution_pack_status_info['image'], + '!status' => $solution_pack_status_info['solution_pack'] + )), + '#prefix' => '
', + '#suffix' => '
', + ), + 'table' => array( + '#type' => 'item', + '#markup' => theme('table', array('header' => array(t('Label'), t('PID'), t('Status')), 'rows' => $table_rows)) + ), + 'submit' => array( + '#type' => 'submit', + '#name' => $solution_pack_module, + '#value' => $solution_pack_status_info['button'], + '#attributes' => array('class' => array('islandora-solution-pack-submit')), + ) + ) + ); } /** @@ -167,349 +153,279 @@ function islandora_solution_pack_form($form, &$form_state, $solution_pack_modul * * @param array $form * The form submitted. - * @param array_reference $form_state + * @param array $form_state * The state of the form submited. */ -function islandora_solution_pack_form_submit($form, &$form_state) { +function islandora_solution_pack_form_submit(array $form, array &$form_state) { $solution_pack_module = $form_state['values']['solution_pack_module']; - $solution_pack_name = $form_state['values']['solution_pack_name']; $objects = $form_state['values']['objects']; - $batch = array( - 'title' => t('Installing / updating solution pack objects'), + 'title' => t('Installing / Updating solution pack objects'), 'file' => drupal_get_path('module', 'islandora') . '/includes/solution_packs.inc', 'operations' => array(), ); - foreach ($objects as $object) { - // Add this object to the batch job queue. - $batch['operations'][] = array('islandora_batch_reingest_object', array($object)); + $batch['operations'][] = array('islandora_solution_pack_batch_operation_reingest_object', array($object)); } - batch_set($batch); - // Hook to let solution pack objects be modified. // Not using module_invoke so solution packs can be expanded by other modules. + // @todo shouldn't we send the object list along as well? module_invoke_all('islandora_postprocess_solution_pack', $solution_pack_module); +} +/** + * Batch operation used by the solution pack forms to ingest/reingest required + * object(s) + * + * @param NewFedoraObject $object + * The object to ingest/reingest. + * @param array $context + * The context of this batch operation. + */ +function islandora_solution_pack_batch_operation_reingest_object(NewFedoraObject $object, array &$context) { + $deleted = FALSE; + $existing_object = islandora_object_load($object->id); + if ($existing_object) { + $deleted = islandora_delete_object($existing_object); + $purged = $deleted && $existing_object == NULL; + if (!$purged) { + $object_link = l($existing_object->label, "islandora/object/{$existing_object->id}"); + drupal_set_message(t('Failed to purge existing object !object_link.', array( + '!object_link' => $object_link, + )), 'error'); + // Failed to purge don't attempt to ingest. + return; + } + } + // Object was deleted or did not exist. + $pid = $object->id; + $label = $object->label; + $action = $deleted ? 'reinstalled' : 'installed'; + $object_link = l($label, "islandora/object/{$pid}"); + $object = islandora_add_object($object); + $msg = $object ? "Successfully $action !object_link." : "Failed to $action @label identified by @pid."; + $status = $object ? 'status' : 'error'; + drupal_set_message(t($msg, array( + '@pid' => $pid, + '@label' => $label, + '!object_link' => $object_link + )), $status); } /** - * Batch reingest object(s) + * This is to be called from the solution pack's hook_install() + * and hook_uninstall() functions. It provides a convient way to have a + * solution pack's required objects ingested at install time. * - * @param array $object - * @param type $context - * @return type + * @param string $module_name + * The name of the module that is calling this function in its + * install/unistall hooks. + * @param string $op + * The operation to perform, either install or uninstall. + * + * @todo Implement hook_modules_installed/hook_modules_uninstalled instead of + * calling this function directly. + * @todo Remove the second parameter and have two seperate functions. */ -function islandora_batch_reingest_object($object_model, &$context) { +function islandora_install_solution_pack($module, $op = 'install') { + if ($op == 'uninstall') { + islandora_uninstall_solution_pack($module); + return; + } + module_load_include('module', 'islandora', 'islandora'); module_load_include('inc', 'islandora', 'includes/utilities'); - global $base_url; - $connection = islandora_get_tuque_connection(); - if (!$connection) { + module_load_include('module', $module, $module); + $info_file = drupal_get_path('module', $module) . "/{$module}.info"; + $info_array = drupal_parse_info_file($info_file); + $module_name = $info_array['name']; + $admin_link = l(t('Solution Pack admin'), 'admin/islandora/solution_packs'); + $config_link = l(t('Islandora configuration'), 'admin/islandora/configure'); + if (!islandora_describe_repository()) { + $msg = '@module: Did not install any objects. Could not connect to the '; + $msg .= 'repository. Please check the settings on the !config_link page '; + $msg .= 'and install the required objects manually on the !admin_link page.'; + drupal_set_message(st($msg, array( + '@module' => $module_name, + '!config_link' => $config_link, + '@admin_link' => $admin_link + )), 'error'); return; } - if (!empty($object_model) && is_array($object_model)) { - $pid = $object_model['pid']; - if (!islandora_is_valid_pid($pid)) { - return NULL; - } - - // purge object - // check if object already exits - $object_query = $connection->api->a->findObjects('query', 'pid=' . $pid); - $reinstall = FALSE; - if (!empty($object_query['results'])) { - $object = islandora_object_load($pid); - if (isset($object)) { - islandora_delete_object($object); - } - $reinstall = TRUE; + $connection = islandora_get_tuque_connection(); + $required_objects = module_invoke($module, 'islandora_required_objects', $connection); + $objects = $required_objects[$module]['objects']; + $status_messages = array( + 'up_to_date' => 'The object already exists and is up-to-date', + 'missing_datastream' => 'The object already exists but is missing a datastream. Please reinstall the object on the !admin_link page', + 'out_of_date' => 'The object already exists but is out-of-date. Please update the object on the !admin_link page', + 'modified_datastream' => 'The object already exists but datastreams are modified. Please reinstall the object on the !admin_link page', + ); + foreach ($objects as $object) { + $query = $connection->api->a->findObjects('query', 'pid=' . $object->id); + $already_exists = !empty($query['results']); + $label = $object->label; + $object_link = l($label, "islandora/object/{$object->id}"); + if ($already_exists) { + $object_status = islandora_check_object_status($object); + $status_msg = $status_messages[$object_status['status']]; + drupal_set_message(st("@module: Did not install !object_link. $status_msg.", array( + '@module' => $module_name, + '!object_link' => $object_link, + '!admin_link' => $admin_link, + )), 'warning'); } - - // build and ingest new object - try { - $object = islandora_solution_pack_add_object($object_model); - $object_name = $object->label; - if ($reinstall) { - drupal_set_message(t('Successfully reinstalled @object_name.', array('@object_name' => $object_name, '@pid' => $pid))); + else { + $object = islandora_add_object($object); + if ($object) { + drupal_set_message(t('@module: Successfully installed. !object_link.', array( + '@module' => $module_name, + '!object_link' => $object_link, + )), 'status'); } else { - drupal_set_message(t('Successfully installed @object_name.', array('@object_name' => $object_name, '@pid' => $pid))); + drupal_set_message(t('@module: Failed to install. @label.', array( + '@module' => $module_name, + '@label' => $label, + )), 'warning'); } } - catch (Exception $e) { - drupal_set_message(t('Installation of object @pid failed', array('@pid' => $pid)), 'error'); - } } } - /** - * Callback function that can be called from the solution pack's hook_install() and hook_uninstall() functions. + * Uninstalls the given solution pack. + * + * @param string $module + * The solution pack to uninstall. * - * @TODO: add documentation + * @todo Implement hook_modules_uninstalled instead of calling this function + * directly for each solution pack. */ -function islandora_install_solution_pack($module_name = NULL, $op = 'install') { - // check if a module name is given. // @TODO: check module name for existance - if (!empty($module_name)) { - module_load_include('module', 'islandora', 'islandora'); - module_load_include('inc', 'islandora', 'includes/utilities'); - module_load_include('module', $module_name, $module_name); - - // set globals - global $base_url; - global $user; - - // set variables - $sp_admin = url($base_url . '/admin/islandora/solution_packs'); - $config_url = url('admin/islandora/configure'); - - // get module info - $info_file = drupal_get_path('module', $module_name) . '/' . $module_name . '.info'; - $info_array = drupal_parse_info_file($info_file); - $module_label = $info_array['name']; - - // check connection - $url = variable_get('islandora_base_url', 'http://localhost:8080/fedora'); - $info = islandora_describe_repository($url); - if (!$info) { - // operation - switch ($op) { - case 'install': - drupal_set_message(st('@module_label: Did not install any objects. Could not connect to the repository. Please check the settings on the Islandora configuration page and install the required objects manually on the solution pack admin page.', array('@module_label' => $module_label, '@config_url' => $config_url, '@sp_url' => $sp_admin)), 'error'); - break; - - case 'uninstall': - drupal_set_message(st('@module_label: Did not uninstall any objects. Could not connect to the repository. Please check the settings on the Islandora configuration page and uninstall the required objects manually if necessary.', array('@module_label' => $module_label, '@config_url' => $config_url)), 'error'); - break; - } - return; - } - - $connection = islandora_get_tuque_connection(); - // get object models - $enabled_solution_packs = module_invoke_all('islandora_required_objects'); - $islandora_required_objects = $module_name . '_islandora_required_objects'; - $required_objects = $islandora_required_objects(); - $objects = $required_objects[$module_name]['objects']; - - // loop over object models - foreach ($objects as $object) { - // set variables - $pid = $object['pid']; - $label = isset($object['label']) ? $object['label'] : st('Object'); - // check if object already exists - $query = $connection->api->a->findObjects('query', 'pid=' . $pid); - // object url - $object_url = url($base_url . '/islandora/object/' . $pid); - - // operation: install or uninstall - switch ($op) { - case 'install': - // if object exists, don't re-ingest - if (!empty($query['results'])) { - // check object status - $object_status = islandora_check_object_status($object); - // set messages - switch ($object_status) { - case 'up_to_date': - drupal_set_message(st('@module_label: did not install @label. The object already exists and is up-to-date.', array('@module_label' => $module_label, '@label' => $label, '@pid' => $pid, '@object_url' => $object_url))); - break; - case 'missing_datastream': - drupal_set_message(st('@module_label: did not install @label. The object already exists but is missing a datastream. Please reinstall the object on the solution pack admin page.', array('@module_label' => $module_label, '@label' => $label, '@pid' => $pid, '@objecturl' => $object_url, '@sp_admin' => $sp_admin)), 'warning'); - break; - case 'out_of_date': - drupal_set_message(st('@module_label: did not install @label. The object already exists but is out-of-date. Please update the object on the solution pack admin page.', array('@module_label' => $module_label, '@label' => $label, '@pid' => $pid, '@object_url' => $object_url, '@sp_admin' => $sp_admin)), 'warning'); - break; - } - } - else { - islandora_solution_pack_add_object($object); - drupal_set_message(st('@module_label: installed @label object.', array('@module_label' => $module_label, '@label' => $label, '@pid' => $pid, '@object_url' => $object_url))); - } - break; - - case 'uninstall': - // if object exists, set message - if (!empty($query['results'])) { - $object_url = url($base_url . '/islandora/object/' . $pid); - drupal_set_message(st('@module_label: did not remove @label. It may be used by other sites.', array('@pid' => $pid, '@object_url' => $object_url, '@label' => $label, '@module_label' => $module_label)), 'warning'); - } - break; - } - } +function islandora_uninstall_solution_pack($module) { + module_load_include('module', 'islandora', 'islandora'); + module_load_include('inc', 'islandora', 'includes/utilities'); + module_load_include('module', $module, $module); + $config_link = l(t('Islandora configuration'), 'admin/islandora/configure'); + $info_file = drupal_get_path('module', $module) . "/{$module}.info"; + $info_array = drupal_parse_info_file($info_file); + $module_name = $info_array['name']; + if (!islandora_describe_repository()) { + $msg = '@module: Did not uninstall any objects. Could not connect to the '; + $msg .= 'repository. Please check the settings on the !config_link page '; + $msg .= 'and uninstall the required objects manually if necessary.'; + drupal_set_message(st($msg, array( + '@module' => $module_name, + '!config_link' => $config_link + )), 'error'); + return; + } + $connection = islandora_get_tuque_connection(); + $required_objects = module_invoke($module, 'islandora_required_objects', $connection); + $objects = $required_objects[$module]['objects']; + $existing_objects = array_filter($objects, function($o) use($connection) { + $param = "pid={$o->id}"; + $query = $connection->api->a->findObjects('query', $param); + return !empty($query['results']); + } + ); + foreach ($existing_objects as $object) { + $msg = '@module: Did not remove !object_link. It may be used by other sites.'; + $object_link = l($object->label, "islandora/object/{$object->id}"); + drupal_set_message(st($msg, array( + '!object_link' => $object_link, + '@module' => $module_name + )), 'warning'); } } - /** * Function to check the status of an object against an object model array. * - * @param array $object_model - * an array describing an object + * @param NewFedoraObject $object_definition + * A new fedora object that defines what the object should contain. + * * @return string * Returns one of the following values: * up_to_date, missing, missing_datastream or out_of_date * You can perform an appropriate action based on this value. - * Returns FALSE if the array is empty * * @see islandora_solution_pack_form() * @see islandora_install_solution_pack() - * @todo: Should this function live in islandora.module so it can be called - * easier without having to include the solution_packs.inc file? */ -function islandora_check_object_status($object_model = array()) { - if (!empty($object_model)) { - // set variables - $pid = $object_model['pid']; - $object_status = 'up_to_date'; - - // table row - $table_row = array(); - - // check connection - module_load_include('inc', 'islandora', 'includes/utilities'); - $url = variable_get('islandora_base_url', 'http://localhost:8080/fedora'); - $info = islandora_describe_repository($url); - if (!$info) { - $object_status = 'could_not_connect'; - } - else { - - // load object - $object = islandora_object_load($pid); - // check if object exists - if (!$object) { - $object_status = 'missing'; - } - else { - // object defined with single datastream file - // @TODO: should dsversion be mandatory for the check to valid? - if (isset($object_model['dsid']) && isset($object_model['datastream_file']) && isset($object_model['dsversion'])) { - $datastreams = array( - array( - 'dsid' => $object_model['dsid'], - 'datastream_file' => $object_model['datastream_file'], - 'dsversion' => $object_model['dsversion'], - ), - ); - } - // object defined with multiple datastreams (using an array) - elseif (!empty($object_model['datastreams'])) { - $datastreams = $object_model['datastreams']; - } - if (!empty($datastreams) && is_array($datastreams)) { - // loop over defined datastreams - foreach ($datastreams as $ds) { - $ds_id = $ds['dsid']; - // check if defined datastream exists in the object - if (!$object[$ds_id]) { - $object_status = 'missing_datastream'; - break; - } - elseif (isset($ds['dsversion'])) { - // Check if the datastream is versioned and needs updating. - $installed_version = islandora_get_islandora_datastream_version($object, $ds['dsid']); - $available_version = islandora_get_islandora_datastream_version(NULL, NULL, $ds['datastream_file']); - - if ($available_version > $installed_version) { - $object_status = 'out_of_date'; - break; - } - } - } - } - } - } - return $object_status; +function islandora_check_object_status(NewFedoraObject $object_definition) { + $existing_object = islandora_object_load($object_definition->id); + if (!$existing_object) { + return array('status' => 'missing', 'status_friendly' => t('Missing')); } - else { - return FALSE; + + $existing_datastreams = array_keys(iterator_to_array($existing_object)); + $expected_datastreams = array_keys(iterator_to_array($object_definition)); + $datastream_diff = array_diff($expected_datastreams, $existing_datastreams); + if (!empty($datastream_diff)) { + $status_friendly = format_plural(count($datastream_diff), 'Missing Datastream: %dsids.', 'Missing Datastreams: %dsids.', array('%dsids' => implode(', ', $datastream_diff))); + return array('status' => 'missing_datastream', 'status_friendly' => $status_friendly, 'data' => $datastream_diff); } -} -/** - * Converts the given definition into an object and add's it to the repository. - * - * @param array $object_definition - * An associative array containing the necessary parameters to create the - * desired object: - * - pid: The PID with which the object will be created. - * - label: An optional label to apply to the object. - * - datastreams: Same as the "datastreams" array accepted by - * islandora_prepare_new_object(). - * - cmodel: Either an array of content models as accepted by - * islandora_preprare_new_object(), or a single content model PID to add - * to the object. - * - parent: Either an array of parents, or a single parent PID to which to - * relate to; uses isMemberOfCollection by default. - * - relationships: An array of relationships as accepted by - * islandora_prepare_new_object(). - * - * @return FedoraObject - * The newly created object. - */ -function islandora_solution_pack_add_object(array $object_definition) { - $object = islandora_solution_pack_prepare_new_object($object_definition); - return islandora_add_object($object); -} + $is_xml_datastream = function($ds) { return $ds->mimetype == 'text/xml'; }; + $xml_datastreams = array_filter(iterator_to_array($object_definition), $is_xml_datastream); + $out_of_date_datastreams = array(); + foreach ($xml_datastreams as $ds) { + $installed_version = islandora_get_islandora_datastream_version($existing_object, $ds->id); + $available_version = islandora_get_islandora_datastream_version($object_definition, $ds->id); + if ($available_version > $installed_version) { + $out_of_date_datastreams[] = $ds->id; + } + } -/** - * Prepares a new object based on the solution pack style of declaring them as arrays. - * - * @param array $object_definition - * An associative array containing the necessary parameters to create the - * desired object: - * - pid: The PID with which the object will be created. - * - label: An optional label to apply to the object. - * - datastreams: Same as the "datastreams" array accepted by - * islandora_prepare_new_object(). - * - cmodel: Either an array of content models as accepted by - * islandora_prepare_new_object(), or a single content model PID to add - * to the object. - * - parent: Either an array of parents, or a single parent PID to which to - * relate to; uses isMemberOfCollection by default. - * - relationships: An array of relationships as accepted by - * islandora_prepare_new_object(). - * - * @return NewFedoraObject - * An NewFedoraObject which has been initalized with the given properties. - */ -function islandora_solution_pack_prepare_new_object(array $object_definition) { - module_load_include('inc', 'islandora', 'includes/utilities'); - $namespace = $object_definition['pid']; - $label = !empty($object_definition['label']) ? $object_definition['label'] : NULL; - $datastreams = array(); - if (!empty($object_definition['datastreams']) AND is_array($object_definition['datastreams'])) { - $datastreams = $object_definition['datastreams']; + if (count($out_of_date_datastreams)) { + $status_friendly = format_plural(count($out_of_date_datastreams), 'Datastream out of date: %dsids.', 'Datastreams out of date: %dsids.', array('%dsids' => implode(', ', $out_of_date_datastreams))); + return array('status' => 'out_of_date', 'status_friendly' => $status_friendly, 'data' => $out_of_date_datastreams); } - $content_models = array(); - if (!empty($object_definition['cmodel'])) { - if (is_array($object_definition['cmodel'])) { - $content_models = $object_definition['cmodel']; + + // This is a pretty heavy function, but I'm not sure a better way. If we have + // performance trouble, we should maybe remove this. + $modified_datastreams = array(); + foreach ($object_definition as $ds) { + if ($ds->mimetype == 'text/xml' + || $ds->mimetype == 'application/rdf+xml' + || $ds->mimetype == 'application/xml') { + // If the datastream is XML we use the domdocument C14N cannonicalization + // function to test if they are equal, because the strings likely won't + // be equal as Fedora does some XML mangling. In order for C14N to work + // we need to replace the info:fedora namespace, as C14N hates it. + // C14N also doesn't normalize whitespace at the end of lines and Fedora + // may add some whitespace on some lines. + $object_definition_dom = new DOMDocument(); + $object_definition_dom->preserveWhiteSpace = FALSE; + $object_definition_dom->loadXML(str_replace('info:', 'http://', $ds->content)); + $object_actual_dom = new DOMDocument(); + $object_actual_dom->preserveWhiteSpace = FALSE; + $object_actual_dom->loadXML(str_replace('info:', 'http://', $existing_object[$ds->id]->content)); + + // Fedora changes the xml structure so we need to cannonize it. + if ($object_actual_dom->C14N() != $object_definition_dom->C14N()) { + $modified_datastreams[] = $ds->id; + } } else { - $content_models[] = $object_definition['cmodel']; - } - } - $relationships = array(); - if (!empty($object_definition['parent']) AND !is_array($object_definition['parent'])) { - $relationships[] = array('relationship' => 'isMemberOfCollection', 'pid' => $object_definition['parent']); - } - if (!empty($object_definition['parents']) AND is_array($object_definition['parents'])) { - foreach ($object_definition['parents'] as $parent) { - $relationships[] = array('relationship' => 'isMemberOfCollection', 'pid' => $parent); + $object_definition_hash = md5($ds->content); + $object_actual_hash = md5($existing_object[$ds->id]->content); + if ($object_definition_hash != $object_actual_hash) { + $modified_datastreams[] = $ds->id;; + } } } - if (!empty($object_definition['relationships']) AND is_array($object_definition['relationships'])) { - foreach ($object_definition['relationships'] as $relationship) { - $relationships[] = array('relationship' => $relationship['relationship'], 'pid' => $relationship['pid']); - } + if (count($modified_datastreams)) { + $status_friendly = format_plural(count($modified_datastreams), 'Modified Datastream: %dsids.', 'Modified Datastreams: %dsids.', array('%dsids' => implode(', ', $modified_datastreams))); + return array('status' => 'modified_datastream', 'data' => $modified_datastreams, 'status_friendly' => $status_friendly); } - return islandora_prepare_new_object($namespace, $label, $datastreams, $content_models, $relationships); -} + // If not anything else we can assume its up to date. + return array('status' => 'up_to_date', 'status_friendly' => t('Up-to-date')); +} + /** * @defgroup viewer-functions * @{ diff --git a/includes/utilities.inc b/includes/utilities.inc index 537c5dd8..55ef7562 100644 --- a/includes/utilities.inc +++ b/includes/utilities.inc @@ -413,3 +413,17 @@ function islandora_prepare_new_object($namespace = NULL, $label = NULL, $datastr } return $object; } + +/** + * Displays the repository is inaccessible message + * + * Use anywhere we want to ensure a consitent error message when the repository + * is not accessible. + */ +function islandora_display_repository_inaccessible_message() { + $text = t('Islandora configuration'); + $link = l($text, 'admin/islandora/configure', array('attributes' => array('title' => $text))); + $message = t('Could not connect to the repository. Please check the settings on the !link page.', + array('!link' => $link)); + drupal_set_message($message, 'error', FALSE); +} diff --git a/islandora.install b/islandora.install index 88549ce5..549f7e14 100644 --- a/islandora.install +++ b/islandora.install @@ -1,15 +1,13 @@ 'The hub for all islandora authentication tokens', + 'fields' => array( + 'id' => array( + 'description' => 'key', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'token' => array( + 'description' => 'a unique identifier for this token', + 'type' => 'varchar', + 'length' => 64, + ), + 'remaining_uses' => array( + 'description' => 'How many uses until this should be removed', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'uid' => array( + 'description' => 'the user id that requested this token', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'time' => array( + 'description' => 'when this token was created', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'pid' => array( + 'description' => 'the pid of the object this token unlocks', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ), + 'dsid' => array( + 'description' => 'the datasteram id of the object this token unlocks', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + ), + 'unique keys' => array( + 'id' => array('id'), + ), + 'primary key' => array('id'), + ); + return $schema; +} + +/** + * Implements hook_update_N(). + * + * Add the required table for handling authentication tokens. + * This is the first instance that has this table. + */ +function islandora_update_7001(&$sandbox) { + drupal_install_schema('islandora'); + $t = get_t(); + return $t("Islandora database updates complete"); +} diff --git a/islandora.module b/islandora.module index 2b553da8..5e6fd7cf 100644 --- a/islandora.module +++ b/islandora.module @@ -161,8 +161,10 @@ function islandora_menu() { 'access arguments' => array(FEDORA_VIEW, 2, 4), 'load arguments' => array(2), ); - $items['islandora/object/%islandora_object/datastream/%islandora_datastream/view'] = array( + // This menu item uses token authentication in islandora_tokened_object. + $items['islandora/object/%islandora_tokened_object/datastream/%islandora_tokened_datastream/view'] = array( 'title' => 'View datastream', + 'load arguments' => array('%map'), 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['islandora/object/%islandora_object/datastream/%islandora_datastream/download'] = array( @@ -311,6 +313,12 @@ function islandora_forms($form_id) { */ function islandora_object_access_callback($perm, $object = NULL) { module_load_include('inc', 'islandora', 'includes/utilities'); + + if (!$object && !islandora_describe_repository()) { + islandora_display_repository_inaccessible_message(); + return FALSE; + } + return user_access($perm) && is_object($object) && islandora_namespace_accessible($object->id); } @@ -518,6 +526,55 @@ function islandora_object_load($object_id) { return NULL; } +/** + * A helper function to get a connection and return an object using a token + * for authentication. + * + * @param string $object_id + * The PID of an object in the menu path identified by + * '%islandora_tokened_object'. + * @param array $map + * Used to extract the Fedora object's DSID at $map[4]. + * + * @return FedoraObject + * A token authenticated object. @see islandora_object_load + */ +function islandora_tokened_object_load($object_id, $map) { + if (array_key_exists('token', $_GET)) { + $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING); + if ($token) { + module_load_include('inc', 'islandora', 'includes/islandora_authtokens'); + $token_user = islandora_validate_object_token($object_id, $map[4], $token); + islandora_get_tuque_connection($user = $token_user); + } + } + return islandora_object_load($object_id); +} + +/** + * This datastream load must take in arguments in a different + * order than the usual islandora_datastream_load. This is because + * the function islandora_tokened_object_load needs DSID. It uses + * the path %map to avoid duplicate parameters. The menu system + * passes 'load arguments' to both islandora_tokened_object_load + * and this function and the first parameter is positional with the token. + * An alternative: + * islandora_tokened_object_load(PID, DSID, PID) + * islandora_tokened_datastream_load(DSID, DSID, PID) + * + * @param mixed $datastream_id + * %islandora_tokened_datastream @see islandora_datastream_load + * @param array $map + * Used to extract the Fedora object's PID at $map[2]. + * + * @return FedoraDatastream + * A datastream from Fedora. + * @see islandora_datastream_load + */ +function islandora_tokened_datastream_load($datastream_id, $map) { + return islandora_datastream_load($datastream_id, $map[2]); +} + /** * A helper function to get an datastream specified as '%islandora_datastream' * for the object specified in the menu path as '%islandora_object'. @@ -526,23 +583,23 @@ function islandora_object_load($object_id) { * drupal_access_denied() when appropriate. * * @param string $datastream_id - * The dsid of the datastream specified as '%islandora_datastream' to fetch + * The DSID of the datastream specified as '%islandora_datastream' to fetch * from the given object in the menu path identified by '%islandora_object'. * - * $param string $object_id - * The object to load the datastream from. + * @param mixed $object_id + * The object to load the datastream from. This can be a Fedora PID or + * an instantiated IslandoraFedoraObject as it implements __toString() + * returning the PID. * * @return FedoraDatastream * If the given datastream ID exists then this returns a FedoraDatastream * object, otherwise it returns NULL which triggers drupal_page_not_found(). */ function islandora_datastream_load($datastream_id, $object_id) { - $object = islandora_object_load($object_id); - + $object = is_object($object_id) ? $object_id : islandora_object_load($object_id); if (!$object) { return NULL; } - return $object[$datastream_id]; } @@ -578,35 +635,32 @@ function islandora_get_islandora_datastream_version($item = NULL, $dsid = NULL, /** * Implements hook_islandora_required_objects(). */ -function islandora_islandora_required_objects() { +function islandora_islandora_required_objects(IslandoraTuque $connection) { $module_path = drupal_get_path('module', 'islandora'); + // Root Collection + $root_collection = $connection->repository->constructObject('islandora:root'); + $root_collection->owner = 'fedoraAdmin'; + $root_collection->label = 'Top-level Collection'; + $root_collection->models = 'islandora:collectionCModel'; + // Collection Policy Datastream + $datastream = $root_collection->constructDatastream('COLLECTION_POLICY', 'X'); + $datastream->label = 'Collection policy'; + $datastream->mimetype = 'text/xml'; + $datastream->setContentFromFile("$module_path/xml/islandora_collection_policy.xml", FALSE); + $root_collection->ingestDatastream($datastream); + // TN Datastream + $datastream = $root_collection->constructDatastream('TN', 'M'); + $datastream->label = 'Thumbnail'; + $datastream->mimetype = 'image/png'; + $datastream->setContentFromFile("$module_path/images/folder.png", FALSE); + $root_collection->ingestDatastream($datastream); return array( 'islandora' => array( 'title' => 'Islandora', 'objects' => array( - array( - 'pid' => 'islandora:root', - 'label' => 'Top-level collection', - 'cmodel' => 'islandora:collectionCModel', - 'datastreams' => array( - array( - 'dsid' => 'COLLECTION_POLICY', - 'label' => 'Collection policy', - 'mimetype' => 'text/xml', - 'control_group' => 'X', - 'datastream_file' => "$module_path/xml/islandora_collection_policy.xml", - ), - array( - 'dsid' => 'TN', - 'label' => 'Thumbnail', - 'mimetype' => 'image/png', - 'control_group' => 'M', - 'datastream_file' => "$module_path/images/folder.png", - ), - ), - ), - ), - ), + $root_collection + ) + ) ); } @@ -818,3 +872,13 @@ function islandora_post_delete_datastream(FedoraObject $object, $datastream_id) module_invoke_all($hook, $object, $datastream_id); } } + +/** + * Implements hook_cron() + * + * Removes expired authentication tokens. + */ +function islandora_cron() { + module_load_include('inc', 'islandora', 'includes/islandora_authtokens'); + islandora_remove_expired_tokens(); +}