Browse Source

resolved conflict

pull/547/head
Alan Stanley 10 years ago
parent
commit
95ca398d11
  1. 16
      .travis.yml
  2. 91
      README.md
  3. 9
      includes/add_datastream.form.inc
  4. 14
      includes/admin.form.inc
  5. 7
      includes/authtokens.inc
  6. 76
      includes/datastream.inc
  7. 134
      includes/datastream.version.inc
  8. 58
      includes/derivatives.inc
  9. 20
      includes/ingest.form.inc
  10. 8
      includes/manage_deleted_objects.inc
  11. 38
      includes/mime_detect.inc
  12. 10
      includes/regenerate_derivatives.form.inc
  13. 18
      includes/solution_packs.inc
  14. 174
      includes/tuque_wrapper.inc
  15. 114
      includes/utilities.inc
  16. 94
      islandora.api.php
  17. 5
      islandora.drush.inc
  18. 7
      islandora.info
  19. 5
      islandora.install
  20. 190
      islandora.module
  21. 23
      islandora.rules.inc
  22. 26
      islandora.rules_defaults.inc
  23. 18
      js/add_print.js
  24. 31
      policies/permit-apim-to-authenticated-user.xml
  25. 25
      policies/permit-getDatastream-unrestricted.xml
  26. 25
      policies/permit-getDatastreamHistory-unrestricted.xml
  27. 31
      policies/permit-upload-to-authenticated-user.xml
  28. 41
      tests/README.md
  29. 276
      tests/datastream_validator_tests.test
  30. 473
      tests/datastream_validators.inc
  31. 4
      tests/hooks.test
  32. 704
      tests/includes/datastream_validators.inc
  33. 76
      tests/includes/islandora_unit_test_case.inc
  34. 332
      tests/includes/islandora_web_test_case.inc
  35. 153
      tests/includes/test_utility_abstraction.inc
  36. 354
      tests/includes/utilities.inc
  37. 2
      tests/islandora_hooked_access_test.module
  38. 8
      tests/islandora_hooks_test.module
  39. 83
      tests/islandora_manage_temp_file.test
  40. 445
      tests/islandora_web_test_case.inc
  41. 22
      tests/scripts/travis_after_failure.sh
  42. 24
      tests/scripts/travis_setup.sh
  43. 4
      theme/islandora-dublin-core-display.tpl.php
  44. 48
      theme/islandora-object.tpl.php
  45. 39
      theme/theme.inc

16
.travis.yml

@ -2,13 +2,19 @@ language: php
php:
- 5.3.3
- 5.4
- 5.5
branches:
only:
- /^7.x/
env:
- FEDORA_VERSION="3.5"
- FEDORA_VERSION="3.6.2"
- FEDORA_VERSION="3.7.0"
matrix:
- FEDORA_VERSION="3.5"
- FEDORA_VERSION="3.6.2"
- FEDORA_VERSION="3.7.0"
global:
# This key is unique to the Islandora/islandora repository; logging will
# fail on forked repositories unless a new unique key is generated for them.
- secure: "nTv2Zb/qKlECK+xE5ahbfXI9ZZbf2ZMd796q7oPlTxUwvu6nomHnUOjJATl6tq2cj23PyJ89Jlgl5cMZ5h0QMUzYpN5hPyY6oCJxWgFamFaE3bv5E/rBd1f6WVTJW7S4UKn8Mr9R2PrX+ZxQZGVIigAfR8VfhQuP8PcuO5eMLBk="
before_install:
- export ISLANDORA_DIR=$TRAVIS_BUILD_DIR
- $TRAVIS_BUILD_DIR/tests/scripts/travis_setup.sh
@ -19,3 +25,7 @@ script:
- drush coder-review --reviews=production,security,style,i18n,potx,sniffer islandora
- phpcpd --names *.module,*.inc,*.test sites/all/modules/islandora
- drush test-run --uri=http://localhost:8081 Islandora
after_failure:
- $ISLANDORA_DIR/tests/scripts/travis_after_failure.sh
notifications:
irc: "irc.freenode.org#islandora"

91
README.md

@ -1,81 +1,70 @@
BUILD STATUS
------------
Current build status:
[![Build Status](https://travis-ci.org/Islandora/islandora.png?branch=7.x)](https://travis-ci.org/Islandora/islandora)
# Islandora [![Build Status](https://travis-ci.org/Islandora/islandora.png?branch=7.x)](https://travis-ci.org/Islandora/islandora)
CI Server:
http://jenkins.discoverygarden.ca
SUMMARY
-------
## Introduction
Islandora Fedora Repository Module
For installation and customization instructions please see the documentation
and the DuraSpace Wiki:
For installation and customization instructions please see the [documentation and the DuraSpace Wiki](https://wiki.duraspace.org/display/ISLANDORA/Islandora).
All bugs, feature requests and improvement suggestions are tracked at the [DuraSpace JIRA](https://jira.duraspace.org/browse/ISLANDORA).
https://wiki.duraspace.org/display/ISLANDORA/Islandora
## Requirements
All bugs, feature requests and improvement suggestions are tracked at the
DuraSpace JIRA:
This module requires the following modules/libraries:
https://jira.duraspace.org/browse/ISLANDORA
* [Tuque](https://github.com/islandora/tuque)
REQUIREMENTS
------------
The Tuque library must be installed to use Islandora. It can be found here:
http://github.com/Islandora/tuque
It is expected to be in one of two paths:
- sites/all/libraries/tuque (libraries directory may need to be created)
- islandora_folder/libraries/tuque
Tuque is expected to be in one of two paths:
OPTIONAL REQUIREMENTS
---------------------
* sites/all/libraries/tuque (libraries directory may need to be created)
* islandora_folder/libraries/tuque
If you want to support languages other than English download and enable
[String Translation](https://drupal.org/project/i18n), And follow our
[guide](wiki/Multilingual-Support) for setting up additional languges.
More detailed requirements are outlined in the [Installing the Islandora Module](https://wiki.duraspace.org/display/ISLANDORA712/Installing+the+Islandora+Module) chapter of the documentation.
### Optional Requirements
INSTALLATION
------------
If you want to support languages other than English download and enable [String Translation](https://drupal.org/project/i18n), and follow our [guide](wiki/Multilingual-Support) for setting up additional languges.
Before installing Islandora the XACML policies located in the policies folder
should be copied into the Fedora global XACML policies folder. This will allow
"authenticated users" in Drupal to access Fedora API-M functions.
## Installation
Before installing Islandora the XACML policies located [here](https://github.com/Islandora/islandora-xacml-policies) should be copied into the Fedora global XACML policies folder. This will allow "authenticated users" in Drupal to access Fedora API-M functions. It is to be noted that the `permit-upload-to-anonymous-user.xml` and `permit-apim-to-anonymous-user.xml` files do not need to be present unless requirements for anonymous ingesting are present.
You will also have to remove some default policies if you want full functionality as well.
Remove deny-purge-datastream-if-active-or-inactive.xml to allow for purging of datastream versions.
CONFIGURATION
-------------
More detailed information can be found in the 'Set XACML Policies' in the [Installing Fedora](https://wiki.duraspace.org/display/ISLANDORA713/Installing+Fedora) chapter of the documentation.
## 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.
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.
Drupal's cron can be run to remove expired authentication tokens.
CUSTOMIZATION
-------------
### Customization
[Customize ingest forms](http://github.com/Islandora/islandora/wiki/Multi-paged-Ingest-Forms)
TROUBLESHOOTING
---------------
## Troubleshooting/Issues
Having problems or solved a problem? Check out the Islandora google groups for a solution.
* [Islandora Group](https://groups.google.com/forum/?hl=en&fromgroups#!forum/islandora)
* [Islandora Dev Group](https://groups.google.com/forum/?hl=en&fromgroups#!forum/islandora-dev)
## Maintainers/Sponsors
Current maintainers:
* [Nick Ruest](https://github.com/ruebot)
F.A.Q.
------
## Development
If you would like to contribute to this module, please check out our helpful [Documentation for Developers](https://github.com/Islandora/islandora/wiki#wiki-documentation-for-developers) info, as well as our [Developers](http://islandora.ca/developers) section on the Islandora.ca site.
The tests for this module will not run through Drupal’s UI. They will work using Drush, which works around Drupal’s batch API.
CONTACT
-------
## License
[GPLv3](http://www.gnu.org/licenses/gpl-3.0.txt)
SPONSORS
--------

9
includes/add_datastream.form.inc

@ -22,6 +22,10 @@ function islandora_add_datastream_form(array $form, array &$form_state, Abstract
module_load_include('inc', 'islandora', 'includes/content_model');
module_load_include('inc', 'islandora', 'includes/utilities');
form_load_include($form_state, 'inc', 'islandora', 'includes/add_datastream.form');
$form_state['object_id'] = $object->id;
// @deprecated Storing objects in $form_state is asking for a bad time...
// Causes issues with derivative generation when we try to use it.
$form_state['object'] = $object;
$form_state['datastream_requirements'] = islandora_get_missing_datastreams_requirements($object);
$unused_datastreams = array_keys($form_state['datastream_requirements']);
@ -93,7 +97,8 @@ function islandora_add_datastream_form(array $form, array &$form_state, Abstract
* The Drupal form.
*/
function islandora_add_datastream_form_field_is_not_an_existing_datastream_id(array $element, array &$form_state, array $form) {
if (isset($form_state['object'][$element['#value']])) {
$object = islandora_object_load($form_state['object_id']);
if (isset($object[$element['#value']])) {
form_error($element, t("@title already exists in the object.", array('@title' => $element['#title'])));
}
}
@ -183,7 +188,7 @@ function islandora_add_datastream_form_validate(array $form, array &$form_state)
* The Drupal form state.
*/
function islandora_add_datastream_form_submit(array $form, array &$form_state) {
$object = $form_state['object'];
$object = islandora_object_load($form_state['object_id']);
$form_state['redirect'] = "islandora/object/{$object->id}";
$file = file_load($form_state['values']['file']);
try {

14
includes/admin.form.inc

@ -27,7 +27,7 @@ function islandora_repository_admin(array $form, array &$form_state) {
'#type' => 'vertical_tabs',
'islandora_general' => array(
'#type' => 'fieldset',
'#title' => t('General Configuarion'),
'#title' => t('General Configuration'),
'wrapper' => array(
'#prefix' => '<div id="islandora-url">',
'#suffix' => '</div>',
@ -72,7 +72,7 @@ function islandora_repository_admin(array $form, array &$form_state) {
'#default_value' => $restrict_namespaces,
),
'islandora_pids_allowed' => array(
'#type' => 'textfield',
'#type' => 'textarea',
'#title' => t('PID namespaces allowed in this Drupal install'),
'#description' => t('A list of PID namespaces, separated by spaces, that users are permitted to access from this Drupal installation. <br /> This could be more than a simple namespace, e.g. <b>demo:mydemos</b>. <br /> The namespace <b>islandora:</b> is reserved, and is always allowed.'),
'#default_value' => variable_get('islandora_pids_allowed', 'default: demo: changeme: ilives: islandora-book: books: newspapers: '),
@ -83,6 +83,16 @@ function islandora_repository_admin(array $form, array &$form_state) {
),
),
),
'islandora_ds_replace_exclude' => array(
'#type' => 'fieldset',
'#title' => t('Excluded DSID'),
'islandora_ds_replace_exclude_enforced' => array(
'#type' => 'textfield',
'#title' => t('Enforce DSID restrictions'),
'#description' => t("A comma seperated list, allowing administrator to restrict user's access to replace a versionable datastreams latest version"),
'#default_value' => variable_get('islandora_ds_replace_exclude_enforced', 'RELS-EXT,RELS-INT'),
),
),
),
);
return system_settings_form($form);

7
includes/authtokens.inc

@ -36,12 +36,7 @@ define('ISLANDORA_AUTHTOKEN_TOKEN_TIMEOUT', 300);
function islandora_get_object_token($pid, $dsid, $uses = 1) {
global $user;
$time = time();
// The function mt_rand is not considered cryptographically secure
// and openssl_rando_pseudo_bytes() is only available in PHP > 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);
$token = bin2hex(drupal_random_bytes(32));
$id = db_insert("islandora_authtokens")->fields(
array(
'token' => $token,

76
includes/datastream.inc

@ -54,7 +54,6 @@ function islandora_view_datastream(AbstractDatastream $datastream, $download = F
// Browsers will not append all extensions.
$mime_detect = new MimeDetect();
$extension = '.' . $mime_detect->getExtension($datastream->mimetype);
// Prevent adding on a duplicate extension.
$label = $datastream->label;
$extension_length = strlen($extension);
@ -65,7 +64,6 @@ function islandora_view_datastream(AbstractDatastream $datastream, $download = F
if ($duplicate_extension_position === FALSE) {
$filename .= $extension;
}
header("Content-Disposition: attachment; filename=\"$filename\"");
}
@ -290,42 +288,6 @@ function islandora_datastream_get_url(AbstractDatastream $datastream, $type = 'd
}
}
/**
* Gets the delete link.
*
* @param AbstractDatastream $datastream
* The datastream to generated the url to.
*
* @return string
* Markup containing the link to the confirm form to delete the datastream.
*/
function islandora_datastream_get_delete_link(AbstractDatastream $datastream) {
$message = islandora_deprecated('7.x-1.2', 'Use the "islandora_datastream_delete_link" theme implementation.');
trigger_error(filter_xss($message), E_USER_DEPRECATED);
return theme('islandora_datastream_delete_link', array(
'datastream' => $datastream,
));
}
/**
* Gets the edit link.
*
* @param AbstractDatastream $datastream
* The datastream to generated the url to.
*
* @return string
* Markup containing the link to edit the datastream.
*/
function islandora_datastream_edit_get_link(AbstractDatastream $datastream) {
$message = islandora_deprecated('7.x-1.2', 'Use the "islandora_datastream_edit_link" theme implementation.');
trigger_error(filter_xss($message), E_USER_DEPRECATED);
return theme('islandora_datastream_edit_link', array(
'datastream' => $datastream,
));
}
/**
* Display the edit datastream page.
*
@ -373,44 +335,6 @@ function islandora_edit_datastream_registry_render(array $edit_registry) {
);
}
/**
* Get markup for a download link.
*
* @param AbstractDatastream $datastream
* The datastream for which to generate a link.
*
* @return string
* Either the link markup if the user has access or an empty string if the
* user is not allowed to see the given datastream.
*/
function islandora_datastream_get_download_link(AbstractDatastream $datastream) {
$message = islandora_deprecated('7.x-1.2', 'Use the "islandora_datastream_download_link" theme implementation.');
trigger_error(filter_xss($message), E_USER_DEPRECATED);
return theme('islandora_datastream_download_link', array(
'datastream' => $datastream,
));
}
/**
* Get markup for a view link.
*
* @param AbstractDatastream $datastream
* The datastream for which to generate a link.
*
* @return string
* Either the link markup if the user has access or a string containing the
* datastream ID if the user is not allowed to see the given datastream.
*/
function islandora_datastream_get_view_link(AbstractDatastream $datastream) {
$message = islandora_deprecated('7.x-1.2', 'Use the "islandora_datastream_view_link" theme implementation.');
trigger_error(filter_xss($message), E_USER_DEPRECATED);
return theme('islandora_datastream_view_link', array(
'datastream' => $datastream,
));
}
/**
* Set the headers for the chunking of our content.
*

134
includes/datastream.version.inc

@ -231,10 +231,142 @@ function islandora_revert_datastream_version_form_submit(array $form, array &$fo
$form_state['redirect'] = "islandora/object/{$islandora_object->id}/datastream/{$datastream_to_revert->id}/version";
}
/**
* Process available dsids, mime and extensions for a given object.
*
* @param AbstractObject $object
* The FedoraObject to process available extensions
*
* @return array()
* An associative array, merged from calls to
* islandora_get_datastreams_requirements_from_content_models()
* and an objects dsid's.
*/
function islandora_get_object_extensions(AbstractObject $object) {
$extensions = islandora_get_datastreams_requirements_from_models($object->models);
foreach ($object as $datastream) {
// Could be a datastream not associated in a content model,
// such as user added.
if (!isset($extensions[$datastream->id])) {
// Add the extensions manually.
$extensions[$datastream->id] = array(
'id' => $datastream->id,
'optional' => TRUE,
'mime' => array($datastream->mimeType),
);
}
}
return $extensions;
}
/**
* The admin replace datastream form.
*
* @param array $form
* The Drupal form.
* @param array $form_state
* The Drupal form state.
* @param AbstractDatastream $datastream
* The datastream to be updated.
*
* @return array
* The drupal form definition.
*/
function islandora_datastream_version_replace_form($form, &$form_state, AbstractDatastream $datastream) {
module_load_include('inc', 'islandora', 'includes/content_model');
module_load_include('inc', 'islandora', 'includes/utilities');
$object = islandora_object_load($datastream->parent->id);
$form_state['object_id'] = $object->id;
$form_state['dsid'] = $datastream->id;
$form_state['object'] = $object;
$datastream_mime_map = islandora_get_object_extensions($object);
$mime_detect = new MimeDetect();
$ext = array();
if (isset($datastream_mime_map[$datastream->id])) {
foreach ($datastream_mime_map[$datastream->id]['mime'] as $key => $value) {
$extensions = $mime_detect->getValidExtensions($value);
$ext = array_merge($ext, $extensions);
}
}
$valid_extensions = implode(' ', $ext);
$upload_size = min((int) ini_get('post_max_size'), (int) ini_get('upload_max_filesize'));
return array(
'dsid_fieldset' => array(
'#type' => 'fieldset',
'#title' => t("Update datastream with latest version"),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'dsid' => array(
'#type' => 'markup',
'#markup' => "<div>DSID: <b>$datastream->id</b></div>",
),
'label' => array(
'#type' => 'markup',
'#markup' => "<div>Label: <b>$datastream->label</b></div>",
),
'file' => array(
'#type' => 'managed_file',
'#required' => TRUE,
'#title' => t('Upload Document'),
'#size' => 64,
'#description' => t('Select a file to upload.<br/>Files must be less than <strong>@size MB.</strong>', array('@size' => $upload_size)),
'#upload_location' => 'temporary://',
'#upload_validators' => array(
'file_validate_extensions' => array($valid_extensions),
// Assume its specified in MB.
'file_validate_size' => array($upload_size * 1024 * 1024),
),
),
'submit' => array(
'#type' => 'submit',
'#value' => t('Add Contents'),
),
),
);
}
/**
* Submit handler for the replace datastream form.
*
* Adds a new datastream version as latest.
*
* @param array $form
* The Drupal form.
* @param array $form_state
* The Drupal form state.
*/
function islandora_datastream_version_replace_form_submit($form, &$form_state) {
$object = islandora_object_load($form_state['object_id']);
$form_state['redirect'] = "islandora/object/{$object->id}";
$file = file_load($form_state['values']['file']);
try {
$ds = $object[$form_state['dsid']];
$ds->mimetype = $file->filemime;
$path = drupal_realpath($file->uri);
$ds->setContentFromFile($path);
file_delete($file);
}
catch (exception $e) {
drupal_set_message(t('An error occurred during datastream updates. See watchlog for more information.'), 'error');
watchdog('islandora',
'Failed to add new versionable datastream.<br/>code: @code<br/>message: @msg',
array(
'@code' => $e->getCode(),
'@msg' => $e->getMessage(),
),
WATCHDOG_ERROR
);
file_delete($file);
return;
}
drupal_set_message(t("Successfully Updated Datastream"));
}
/**
* Gets Audit datastream values from foxml.
*
* @param String $pid
* @param string $pid
* PID of parent object
*
* @return array

58
includes/derivatives.inc

@ -4,6 +4,48 @@
* Defines functions used when constructing derivatives.
*/
/**
* Decides which derivative function to call and runs it.
*
* @param AbstractObject $object
* The object to run the derivative function for.
* @param string $dsid
* The DSID to run the derivative function for.
*/
function islandora_run_derivatives(AbstractObject $object, $dsid) {
$batch_array = batch_get();
if (empty($batch_array)) {
$logging_results = islandora_do_derivatives($object, array(
'source_dsid' => $dsid,
));
islandora_derivative_logging($logging_results);
}
else {
$operations = islandora_do_batch_derivatives(
$object,
array(
'source_dsid' => $dsid,
)
);
if ($operations) {
batch_set(
// Title won't show for batch in a batch.
array(
'init_message' => t('Preparing derivatives for @label', array('@label' => $object->label)),
'error_message' => t('An error occured creating derivatives.'),
'progress_message' => t(
'Creating derivatives for @label <br/>Time elapsed: @elapsed <br/>
Estimated time remaining @estimate.',
array('@label' => $object->label)
),
'file' => drupal_get_path('module', 'islandora') . '/includes/regenerate_derivatives.form.inc',
'operations' => $operations,
)
);
}
}
}
/**
* Kicks off derivative functions based upon hooks and conditions.
*
@ -49,7 +91,7 @@ function islandora_do_derivatives(AbstractObject $object, array $options) {
}
foreach ($hook['function'] as $function) {
if (function_exists($function)) {
$logging = call_user_func($function, $object, $options['force']);
$logging = call_user_func($function, $object, $options['force'], $hook);
if (!empty($logging)) {
$results[] = $logging;
}
@ -142,11 +184,15 @@ function islandora_do_batch_derivatives(AbstractObject $object, array $options)
$file = $hook['file'];
}
foreach ($hook['function'] as $function) {
$operations[] = array('islandora_derivative_perform_batch_operation', array(
$function,
$file,
$object->id,
$options['force']),
$operations[] = array(
'islandora_derivative_perform_batch_operation',
array(
$function,
$file,
$object->id,
$options['force'],
$hook,
),
);
}
}

20
includes/ingest.form.inc

@ -467,12 +467,14 @@ function islandora_ingest_form_undo_consecutive_callback_steps(array $form, arra
* The step which the undo callback is being called on.
*/
function islandora_ingest_form_undo_callback_step(array $form, array &$form_state, array $step) {
$args = array(&$form_state);
$args = isset($step['undo_function']['args']) ? array_merge($args, $step['undo_function']['args']) : $args;
if (isset($step['undo_function']['file'])) {
require_once drupal_get_path('module', $step['module']) . "/" . $step['undo_function']['file'];
if (isset($step['undo_function'])) {
$args = array(&$form_state);
$args = isset($step['undo_function']['args']) ? array_merge($args, $step['undo_function']['args']) : $args;
if (isset($step['undo_function']['file'])) {
require_once drupal_get_path('module', $step['module']) . "/" . $step['undo_function']['file'];
}
call_user_func_array($step['undo_function']['function'], $args);
}
call_user_func_array($step['undo_function']['function'], $args);
}
/**
@ -704,7 +706,11 @@ function islandora_ingest_form_ingest_button(array &$form_state) {
$validate_callback = $form_id . '_validate';
$validate = function_exists($validate_callback) ? array($validate_callback) : NULL;
$submit_callback = $form_id . '_submit';
$submit = function_exists($submit_callback) ? array($submit_callback, 'islandora_ingest_form_submit') : array('islandora_ingest_form_submit');
$submit = function_exists($submit_callback) ? array(
$submit_callback,
'islandora_ingest_form_pre_submit',
'islandora_ingest_form_submit',
) : array('islandora_ingest_form_pre_submit', 'islandora_ingest_form_submit');
return array(
'#type' => 'submit',
'#name' => 'ingest',
@ -769,7 +775,7 @@ function islandora_ingest_form_submit(array $form, array &$form_state) {
islandora_ingest_form_execute_consecutive_callback_steps($form, $form_state, $step);
}
// Ingest the objects.
$set_redirect = TRUE;
$set_redirect = isset($form_state['redirect']) ? FALSE : TRUE;
foreach ($form_state['islandora']['objects'] as &$object) {
try {
islandora_add_object($object);

8
includes/manage_deleted_objects.inc

@ -287,7 +287,7 @@ function islandora_get_contentmodels_with_deleted_members() {
/**
* Restores deleted object.
*
* @param String $pid
* @param string $pid
* PID of object to be restored
*/
function islandora_restore_object_by_pid($pid) {
@ -298,7 +298,7 @@ function islandora_restore_object_by_pid($pid) {
/**
* Purges deleted object.
*
* @param String $pid
* @param string $pid
* PID of object to be restored
*/
function islandora_purge_object_by_pid($pid) {
@ -314,7 +314,7 @@ function islandora_purge_object_by_pid($pid) {
* @param int $offset
* offset to be added to search
*
* @return String
* @return string
* Sparql query
*/
function islandora_get_deleted_query($content_models, $offset = 0) {
@ -332,7 +332,7 @@ function islandora_get_deleted_query($content_models, $offset = 0) {
$unions .= "UNION {?subject fm:hasModel <info:fedora/$contentmodel>;
fm:state fm:Deleted;
fm:hasModel ?object;
}
}
";
}
$optional = "OPTIONAL{?subject fm:label ?label}";

38
includes/mime_detect.inc

@ -132,8 +132,9 @@ class MimeDetect {
'wbxml' => 'application/vnd.wap.wbxml',
'xht' => 'application/xhtml+xml',
'xhtml' => 'application/xhtml+xml',
'xsl' => 'text/xml',
'xml' => 'text/xml',
'xsl' => 'text/xsl',
'xslt' => 'text/xsl',
'xml' => 'application/xml',
'csv' => 'text/csv',
'tsv' => 'text/tab-separated-values',
'txt' => 'text/plain',
@ -184,6 +185,37 @@ class MimeDetect {
"ogg" => "audio/ogg",
"flac" => "audio/x-flac",
"wav" => "audio/vnd.wave",
// Chemical:
// MDL Molfile.
"mol" => "chemical/x-mdl-molfile",
// XYZ format.
"xyz" => "chemical/x-xyz",
// PDB.
"pdb" => "chemical/x-pdb",
// ChemDraw CDX.
'cdx' => 'chemical/x-cdx',
// ChemDraw 3D.
"c3d" => "chemical/x-chem3d",
// ChemDraw file.
"chm" => "chemical/x-chemdraw",
// Crystallographic Information File.
"cif" => "chemical/x-cif",
// Chemical Markup Language.
"cml" => "chemical/x-cml",
// GAMESS Input.
"inp" => "chemical/x-gamess-input",
// GAMESS Output.
"gam" => "chemical/x-gamess-output",
// Gaussian Cube.
"cub" => "chemical/x-gaussian-cube",
// Gaussian 98/03 Cartesian Input.
"gau" => "chemical/x-gaussian-input",
// JCAMP Spectroscopic Data Exchange Format.
"jdx" => "chemical/x-jcamp-dx",
// OpenDX Grid.
"dx" => "chemical/x-jcamp-dx",
// MOPAC Cartesian.
"mop" => "chemical/x-mopac-input",
// Compressed formats:
// (note: http://svn.cleancode.org/svn/email/trunk/mime.types)
"tgz" => "application/x-gzip",
@ -193,6 +225,8 @@ class MimeDetect {
"zip" => "application/x-zip",
// others:
'bin' => 'application/octet-stream',
// Web Archives:
"warc" => "application/warc",
);
protected $protectedFileExtensions;
protected $extensionExceptions = array(

10
includes/regenerate_derivatives.form.inc

@ -99,7 +99,7 @@ function islandora_regenerate_object_derivatives_batch(AbstractObject $object) {
'title' => t('Regenerating all derivatives for @label', array('@label' => $object->label)),
'operations' => islandora_do_batch_derivatives($object, array('force' => TRUE)),
'init_message' => t('Preparing to regenerate derivatives...'),
'progress_message' => t('Time elapsed: @elapsed <br/>Estimated time remaning @estimate.'),
'progress_message' => t('Time elapsed: @elapsed <br/>Estimated time remaining @estimate.'),
'error_message' => t('An error has occurred.'),
'file' => drupal_get_path('module', 'islandora') . '/includes/regenerate_derivatives.form.inc',
'finished' => 'islandora_regenerate_derivative_batch_finished',
@ -124,7 +124,7 @@ function islandora_regenerate_datastream_derivative_batch(AbstractDatastream $da
'destination_dsid' => $datastream->id,
)),
'init_message' => t('Preparing to regenerate derivatives...'),
'progress_message' => t('Time elapsed: @elapsed <br/>Estimated time remaning @estimate.'),
'progress_message' => t('Time elapsed: @elapsed <br/>Estimated time remaining @estimate.'),
'error_message' => t('An error has occurred.'),
'file' => drupal_get_path('module', 'islandora') . '/includes/regenerate_derivatives.form.inc',
'finished' => 'islandora_regenerate_derivative_batch_finished',
@ -142,15 +142,17 @@ function islandora_regenerate_datastream_derivative_batch(AbstractDatastream $da
* The pid of the object we are performing.
* @param bool $force
* Whether we are forcing derivative regeneration or not.
* @param array $hook
* The derivative definition.
* @param array $context
* The context of the current batch operation.
*/
function islandora_derivative_perform_batch_operation($function, $file, $pid, $force, &$context) {
function islandora_derivative_perform_batch_operation($function, $file, $pid, $force, $hook, &$context) {
if ($file) {
require_once $file;
}
if (function_exists($function)) {
$logging = call_user_func($function, islandora_object_load($pid), $force);
$logging = call_user_func($function, islandora_object_load($pid), $force, $hook);
if (!empty($logging)) {
$context['results']['logging'][] = $logging;
}

18
includes/solution_packs.inc

@ -256,7 +256,7 @@ function islandora_solution_pack_form_submit(array $form, array &$form_state) {
* the batch.
* @param array $not_checked
* The object that will bot be install.
*
*
* @return array
* An array defining a batch which can be passed on to batch_set().
*/
@ -367,7 +367,7 @@ function islandora_install_solution_pack($module, $op = 'install', $force = FALS
$t = get_t();
// Some general replacements.
$admin_link = l($t('Solution Pack admin'), 'admin/islandora/solution_packs');
$admin_link = l($t('Solution Pack admin'), 'admin/islandora/solution_pack_config');
$config_link = l($t('Islandora configuration'), 'admin/islandora/configure');
$t_params = array(
'@module' => $module,
@ -712,9 +712,8 @@ function islandora_viewers_form($variable_id = NULL, $mimetype = NULL, $model =
* given, than any viewer that supports either the give $mimetype or $model will
* be listed.
*
* @param string $mimetype
* Specify a mimetype to return only viewers that support this certain
* mimetype.
* @param array $mimetype
* List of mimetypes that the viewer supports.
* @param string $content_model
* Specify a content model to return only viewers that support the content
* model.
@ -722,14 +721,19 @@ function islandora_viewers_form($variable_id = NULL, $mimetype = NULL, $model =
* @return array
* Viewer definitions, or FALSE if none are found.
*/
function islandora_get_viewers($mimetype = NULL, $content_model = NULL) {
function islandora_get_viewers($mimetype = array(), $content_model = NULL) {
$viewers = array();
$defined_viewers = module_invoke_all('islandora_viewer_info');
if (!is_array($mimetype)) {
$mimetype = array($mimetype);
}
// Filter viewers by MIME type.
foreach ($defined_viewers as $key => $value) {
$value['mimetype'] = isset($value['mimetype']) ? $value['mimetype'] : array();
$value['model'] = isset($value['model']) ? $value['model'] : array();
if (in_array($mimetype, $value['mimetype']) OR in_array($content_model, $value['model'])) {
if (array_intersect($mimetype, $value['mimetype']) OR in_array($content_model, $value['model'])) {
$viewers[$key] = $value;
}
}

174
includes/tuque_wrapper.inc

@ -115,6 +115,10 @@ class IslandoraFedoraRepository extends FedoraRepository {
foreach ($object as $dsid => $datastream) {
islandora_invoke_datastream_hooks(ISLANDORA_DATASTREAM_INGESTED_HOOK, $object->models, $dsid, $object, $datastream);
}
// Fire of event if rules is enabled.
if (module_exists('rules')) {
rules_invoke_event('islandora_object_ingested', $object);
}
return $ret;
}
catch (Exception $e) {
@ -140,6 +144,46 @@ class IslandoraFedoraObject extends FedoraObject {
protected $fedoraDatastreamClass = 'IslandoraFedoraDatastream';
protected $fedoraRelsExtClass = 'IslandoraFedoraRelsExt';
/**
* Magical magic, to allow recursive modifications.
*
* So... Magic functions in PHP are not re-entrant... Meaning that if you
* have something which tries to call __set on an object anywhere later in
* the callstack after it has already been called, it will not call the
* magic method again; instead, it will set the property on the object
* proper. Here, we detect the property being set on the object proper, and
* restore the magic functionality as long as it keeps getting set...
*
* Not necessary to try to account for this in Tuque proper, as Tuque itself
* does not have a mechanism to trigger modifications resulting from other
* modifications.
*
* @param string $name
* The name of the property being set.
* @param mixed $value
* The value to which the property should be set.
*/
public function __set($name, $value) {
parent::__set($name, $value);
// Recursion only matters for magic properties... "Plain" properties cannot
// call other code in order to start recursing, and in fact we would get
// stuck looping with a "plain" property.
if ($this->propertyIsMagical($name)) {
// XXX: Due to the structure of the code, we cannot use property_exists()
// (because many of the properties are declared in the class, and the
// magic triggers due them being NULLed), nor can we use isset() (because
// it is implemented as another magic function...).
$vars = get_object_vars($this);
while (isset($vars[$name])) {
$new_value = $this->$name;
unset($this->$name);
parent::__set($name, $new_value);
$vars = get_object_vars($this);
}
}
}
/**
* Ingest the given datastream.
*
@ -169,6 +213,30 @@ class IslandoraFedoraObject extends FedoraObject {
throw $e;
}
}
/**
* Inherits.
*
* Calls parent and invokes object modified and deleted(/purged) hooks.
*
* @see FedoraObject::modifyObject()
*/
protected function modifyObject($params) {
try {
parent::modifyObject($params);
islandora_invoke_object_hooks(ISLANDORA_OBJECT_MODIFIED_HOOK, $this->models, $this);
if ($this->state == 'D') {
islandora_invoke_object_hooks(ISLANDORA_OBJECT_PURGED_HOOK, $this->models, $this->id);
}
}
catch (Exception $e) {
watchdog('islandora', 'Failed to modify object: @pid</br>code: @code<br/>message: @msg', array(
'@pid' => $this->id,
'@code' => $e->getCode(),
'@msg' => $e->getMessage()), WATCHDOG_ERROR);
throw $e;
}
}
}
class IslandoraRepositoryConnection extends RepositoryConnection {}
@ -215,25 +283,10 @@ class IslandoraFedoraApiM extends FedoraApiM {
if (isset($params['lastModifiedDate'])) {
$params['lastModifiedDate'] = (string) $object[$dsid]->createdDate;
}
try {
if ($context['block']) {
throw new Exception('Modify Datastream was blocked.');
}
$ret = parent::modifyDatastream($pid, $dsid, $params);
islandora_invoke_datastream_hooks(ISLANDORA_DATASTREAM_MODIFIED_HOOK, $object->models, $dsid, $object, $datastream);
if (isset($params['dsState']) && $params['dsState'] == 'D') {
islandora_invoke_datastream_hooks(ISLANDORA_DATASTREAM_PURGED_HOOK, $object->models, $dsid, $object, $dsid);
}
return $ret;
}
catch (Exception $e) {
watchdog('islandora', 'Failed to modify datastream @dsid from @pid</br>code: @code<br/>message: @msg', array(
'@pid' => $pid,
'@dsid' => $dsid,
'@code' => $e->getCode(),
'@msg' => $e->getMessage()), WATCHDOG_ERROR);
throw $e;
if ($context['block']) {
throw new Exception('Modify Datastream was blocked.');
}
return parent::modifyDatastream($pid, $dsid, $params);
}
/**
@ -250,24 +303,10 @@ class IslandoraFedoraApiM extends FedoraApiM {
);
islandora_alter_object($object, $context);
$params = $context['params'];
try {
if ($context['block']) {
throw new Exception('Modify Object was blocked.');
}
$ret = parent::modifyObject($pid, $params);
islandora_invoke_object_hooks(ISLANDORA_OBJECT_MODIFIED_HOOK, $object->models, $object);
if (isset($params['state']) && $params['state'] == 'D') {
islandora_invoke_object_hooks(ISLANDORA_OBJECT_PURGED_HOOK, $object->models, $object->id);
}
return $ret;
}
catch (Exception $e) {
watchdog('islandora', 'Failed to modify object: @pid</br>code: @code<br/>message: @msg', array(
'@pid' => $pid,
'@code' => $e->getCode(),
'@msg' => $e->getMessage()), WATCHDOG_ERROR);
throw $e;
if ($context['block']) {
throw new Exception('Modify Object was blocked.');
}
return parent::modifyObject($pid, $params);
}
/**
@ -373,6 +412,71 @@ class IslandoraNewFedoraDatastream extends NewFedoraDatastream {
class IslandoraFedoraDatastream extends FedoraDatastream {
protected $fedoraRelsIntClass = 'IslandoraFedoraRelsInt';
protected $fedoraDatastreamVersionClass = 'IslandoraFedoraDatastreamVersion';
/**
* Magical magic, to allow recursive modifications.
*
* So... Magic functions in PHP are not re-entrant... Meaning that if you
* have something which tries to call __set on an object anywhere later in
* the callstack after it has already been called, it will not call the
* magic method again; instead, it will set the property on the object
* proper. Here, we detect the property being set on the object proper, and
* restore the magic functionality as long as it keeps getting set...
*
* Not necessary to try to account for this in Tuque proper, as Tuque itself
* does not have a mechanism to trigger modifications resulting from other
* modifications.
*
* @param string $name
* The name of the property being set.
* @param mixed $value
* The value to which the property should be set.
*/
public function __set($name, $value) {
parent::__set($name, $value);
// Recursion only matters for magic properties... "Plain" properties cannot
// call other code in order to start recursing, and in fact we would get
// stuck looping with a "plain" property.
if ($this->propertyIsMagical($name)) {
// XXX: Due to the structure of the code, we cannot use property_exists()
// (because many of the properties are declared in the class, and the
// magic triggers due them being NULLed), nor can we use isset() (because
// it is implemented as another magic function...).
$vars = get_object_vars($this);
while (isset($vars[$name])) {
$new_value = $this->$name;
unset($this->$name);
parent::__set($name, $new_value);
$vars = get_object_vars($this);
}
}
}
/**
* Inherits.
*
* Calls parent and invokes modified and purged hooks.
*
* @see FedoraDatastream::modifyDatastream()
*/
protected function modifyDatastream(array $args) {
try {
parent::modifyDatastream($args);
islandora_invoke_datastream_hooks(ISLANDORA_DATASTREAM_MODIFIED_HOOK, $this->parent->models, $this->id, $this->parent, $this);
if ($this->state == 'D') {
islandora_invoke_datastream_hooks(ISLANDORA_DATASTREAM_PURGED_HOOK, $this->parent->models, $this->id, $this->parent, $this->id);
}
}
catch (Exception $e) {
watchdog('islandora', 'Failed to modify datastream @dsid from @pid</br>code: @code<br/>message: @msg', array(
'@pid' => $this->parent->id,
'@dsid' => $this->id,
'@code' => $e->getCode(),
'@msg' => $e->getMessage()), WATCHDOG_ERROR);
throw $e;
}
}
}
class IslandoraFedoraDatastreamVersion extends FedoraDatastreamVersion {

114
includes/utilities.inc

@ -30,22 +30,53 @@ function islandora_convert_bytes_to_human_readable($bytes, $precision = 2) {
return $bytes . ' B';
}
elseif (($bytes >= $kilobyte) && ($bytes < $megabyte)) {
return round($bytes / $kilobyte, $precision) . ' KB';
return round($bytes / $kilobyte, $precision) . ' KiB';
}
elseif (($bytes >= $megabyte) && ($bytes < $gigabyte)) {
return round($bytes / $megabyte, $precision) . ' MB';
return round($bytes / $megabyte, $precision) . ' MiB';
}
elseif (($bytes >= $gigabyte) && ($bytes < $terabyte)) {
return round($bytes / $gigabyte, $precision) . ' GB';
return round($bytes / $gigabyte, $precision) . ' GiB';
}
elseif ($bytes >= $terabyte) {
return round($bytes / $terabyte, $precision) . ' TB';
return round($bytes / $terabyte, $precision) . ' TiB';
}
else {
return $bytes . ' B';
}
}
/**
* Add a file as managed if is not already.
*
* @param string $file_uri
* The given file URI location.
*
* @return object
* The file, as returned from file_save().
*/
function islandora_temp_file_entry($file_uri, $mime = NULL) {
$query = new EntityFieldQuery();
$result = $query
->entityCondition('entity_type', 'file')
->propertyCondition('uri', $file_uri)
->execute();
if (isset($result['file'])) {
$fid = current($result['file'])->fid;
$file = file_load($fid);
}
else {
$file = new stdClass();
$file->uri = $file_uri;
$file->filename = drupal_basename($file_uri);
if (isset($mime)) {
$file->filemime = $mime;
}
}
$file->status = 0;
return file_save($file);
}
/**
* Creates a label for control group symbols.
*/
@ -414,8 +445,9 @@ function islandora_get_datastreams_requirements_from_content_model(AbstractObjec
/**
* Prepare an ingestable object.
*
* @param string $namespace
* The namespace in which the PID for the new object will be created.
* @param string $name_source
* Either a pid or namespace in which the PID for the new object will be
* created.
* @param string $label
* An optional label to apply to the object.
* @param array $datastreams
@ -440,10 +472,10 @@ function islandora_get_datastreams_requirements_from_content_model(AbstractObjec
* @return NewFedoraObject
* An ingestable NewFedoraObject.
*/
function islandora_prepare_new_object($namespace = NULL, $label = NULL, $datastreams = array(), $content_models = array(), $relationships = array()) {
function islandora_prepare_new_object($name_source = NULL, $label = NULL, $datastreams = array(), $content_models = array(), $relationships = array()) {
global $user;
$tuque = islandora_get_tuque_connection();
$object = isset($namespace) ? $tuque->repository->constructObject($namespace) : new IslandoraNewFedoraObject(NULL, $tuque->repository);
$object = isset($name_source) ? $tuque->repository->constructObject($name_source) : new IslandoraNewFedoraObject(NULL, $tuque->repository);
$object->owner = isset($user->name) ? $user->name : $object->owner;
$object->label = isset($label) ? $label : $object->label;
foreach ($content_models as $content_model) {
@ -486,10 +518,12 @@ function islandora_prepare_new_object($namespace = NULL, $label = NULL, $datastr
// set datastream_file by url for the given scheme. Https (SSL) can
// cause this to fail, and trigger an output log in watchdog.
$datastream_file = url($ds['datastream_file'], array('absolute' => TRUE));
watchdog('islandora',
'Attempting to ingest %file in islandora_prepare_new_object(), but it does not appear to be readable. We will pass the path through url(), and pass along as such.',
array('%file' => $datastream_file),
WATCHDOG_WARNING);
watchdog(
'islandora',
'Attempting to ingest %file in islandora_prepare_new_object(), but it does not appear to be readable. We will pass the path through url(), and pass along as such.',
array('%file' => $datastream_file),
WATCHDOG_WARNING
);
}
}
@ -528,7 +562,7 @@ function islandora_display_repository_inaccessible_message() {
$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(check_plain($message), 'error', FALSE);
drupal_set_message(filter_xss($message), 'error', FALSE);
}
@ -615,54 +649,6 @@ function islandora_system_settings_form_default_value($name, $default_value, arr
return isset($form_state['values'][$name]) ? $form_state['values'][$name] : variable_get($name, $default_value);
}
/**
* Returns basic information about DS-COMPOSITE-MODEL.
*
* @deprecated
* The pre-existing--and more flexible--
* islandora_get_datastreams_requirements_from_content_model() should be
* preferred, as it addresses the case where a stream can be allowed to have
* one of a set of mimetypes (this functions appears to only return the last
* declared mimetype for a given datastream).
*
* @param string $pid
* The PID of content model containing DS_COMP stream.
*
* @return array
* An associative array mapping datastream IDs to an associative array
* representing the parsed DS-COMPOSITE-MODEL of the form:
* - DSID: A string containing the datastream ID.
* - "mimetype": A string containing the last mimetype declared for the
* given datastream ID.
* - "optional": An optional boolean indicating that the given datastream
* is optional.
*/
function islandora_get_comp_ds_mappings($pid) {
$message = islandora_deprecated('7.x-1.2', t('Refactor to use the more flexible islandora_get_datastreams_requirements_from_content_model().'));
trigger_error(filter_xss($message), E_USER_DEPRECATED);
$cm_object = islandora_object_load($pid);
if (!isset($cm_object) || !isset($cm_object['DS-COMPOSITE-MODEL'])) {
return FALSE;
}
$datastream = $cm_object['DS-COMPOSITE-MODEL'];
$ds_comp_stream = $datastream->content;
$sxml = new SimpleXMLElement($ds_comp_stream);
$mappings = array();
foreach ($sxml->dsTypeModel as $ds) {
$dsid = (string) $ds['ID'];
$optional = (string) $ds['optional'];
foreach ($ds->form as $form) {
$mime = (string) $form['MIME'];
if ($optional) {
$mappings[$dsid]['optional'] = $optional;
}
$mappings[$dsid]['mimetype'] = $mime;
}
}
return $mappings;
}
/**
* Checks that the given/current account has all the given permissions.
*
@ -859,9 +845,9 @@ function islandora_deprecated($release, $solution = NULL) {
assert($bt[0]['function'] == __FUNCTION__);
$function = $bt[1]['function'];
$message = t('@function() has been deprecated. As of @release, please update your code before the next release.', array(
'@function' => $function,
'@release' => $release,
));
'@function' => $function,
'@release' => $release,
));
if (isset($solution)) {
$message .= "<br/>\n" . $solution;
}

94
islandora.api.php

@ -57,7 +57,7 @@ function hook_islandora_view_print_object($object) {
* @return array
* An array whose values are markup.
*/
function hook_CMODEL_PID_islandora_view_object($object) {
function hook_cmodel_pid_islandora_view_object($object) {
}
@ -82,7 +82,7 @@ function hook_islandora_view_object_alter(&$object, &$rendered) {
* @param array $rendered
* The array of rendered views.
*/
function hook_CMODEL_PID_islandora_view_object_alter(&$object, &$rendered) {
function hook_cmodel_pid_islandora_view_object_alter(&$object, &$rendered) {
}
/**
@ -109,7 +109,7 @@ function hook_islandora_edit_object($object) {
* @return array
* An array whose values are markup.
*/
function hook_CMODEL_PID_islandora_edit_object($object) {
function hook_cmodel_pid_islandora_edit_object($object) {
}
/**
@ -160,7 +160,7 @@ function hook_islandora_object_alter(AbstractObject $object, array &$context) {
*
* @see hook_islandora_object_alter()
*/
function hook_CMODEL_PID_islandora_object_alter(AbstractObject $object, array &$context) {
function hook_cmodel_pid_islandora_object_alter(AbstractObject $object, array &$context) {
}
/**
@ -209,7 +209,7 @@ function hook_islandora_datastream_alter(AbstractObject $object, AbstractDatastr
*
* @see hook_islandora_datastream_alter()
*/
function hook_CMODEL_PID_DSID_islandora_datastream_alter(AbstractObject $object, AbstractDatastream $datastream, array &$context) {
function hook_cmodel_pid_dsid_islandora_datastream_alter(AbstractObject $object, AbstractDatastream $datastream, array &$context) {
}
/**
@ -233,7 +233,7 @@ function hook_islandora_object_ingested(AbstractObject $object) {
*
* @see hook_islandora_object_ingested()
*/
function hook_CMODEL_PID_islandora_object_ingested(AbstractObject $object) {
function hook_cmodel_pid_islandora_object_ingested(AbstractObject $object) {
}
/**
@ -258,7 +258,7 @@ function hook_islandora_object_modified(AbstractObject $object) {
*
* @see hook_islandora_object_modified()
*/
function hook_CMODEL_PID_islandora_object_modified(AbstractObject $object) {
function hook_cmodel_pid_islandora_object_modified(AbstractObject $object) {
}
/**
@ -278,7 +278,7 @@ function hook_islandora_object_purged($pid) {
*
* @see hook_islandora_object_purged()
*/
function hook_CMODEL_PID_islandora_object_purged($pid) {
function hook_cmodel_pid_islandora_object_purged($pid) {
}
/**
@ -303,7 +303,7 @@ function hook_islandora_datastream_ingested(AbstractObject $object, AbstractData
*
* @see hook_islandora_object_ingested()
*/
function hook_CMODEL_PID_DSID_islandora_datastream_ingested(AbstractObject $object, AbstractDatastream $datastream) {
function hook_cmodel_pid_dsid_islandora_datastream_ingested(AbstractObject $object, AbstractDatastream $datastream) {
}
/**
@ -330,7 +330,7 @@ function hook_islandora_datastream_modified(AbstractObject $object, AbstractData
*
* @see hook_islandora_datastream_modified()
*/
function hook_CMODEL_PID_islandora_datastream_modified(AbstractObject $object, AbstractDatastream $datastream) {
function hook_cmodel_pid_islandora_datastream_modified(AbstractObject $object, AbstractDatastream $datastream) {
}
/**
@ -352,7 +352,7 @@ function hook_islandora_datastream_purged(AbstractObject $object, $dsid) {
*
* @see hook_islandora_datastream_purged()
*/
function hook_CMODEL_PID_islandora_datastream_purged(AbstractObject $object, $dsid) {
function hook_cmodel_pid_islandora_datastream_purged(AbstractObject $object, $dsid) {
}
/**
@ -423,13 +423,13 @@ function hook_islandora_undeletable_datastreams(array $models) {
* - form_id: The form building function to call to get the form structure
* for this step.
* - args: An array of arguments to pass to the form building function.
* Required "callback" type specific parameters:
* - do_function: An associate array including:
* "Callback" type specific parameters:
* - do_function: A required associative array including:
* - 'function': The callback function to be called.
* - 'args': An array of arguments to pass to the callback function.
* - 'file': A file to include (relative to the module's path, including
* the file's extension).
* - undo_function: An associate array including:
* - undo_function: An optional associative array including:
* - 'function': The callback function to be called to reverse the
* executed action in the ingest steps.
* - 'args': An array of arguments to pass to the callback function.
@ -466,6 +466,19 @@ function hook_islandora_ingest_steps(array $form_state) {
),
);
}
/**
* Alter the generated ingest steps.
*
* @param array $steps
* An array of steps as generated by hook_islandora_ingest_steps().
*
* @param array $form_state
* An array containing the Drupal form_state.
*/
function hook_islandora_ingest_steps_alter(array &$steps, array &$form_state) {
}
/**
* Content model specific version of hook_islandora_ingest_steps().
*
@ -476,7 +489,19 @@ function hook_islandora_ingest_steps(array $form_state) {
*
* @see hook_islandora_ingest_steps()
*/
function hook_CMODEL_PID_islandora_ingest_steps(array $form_state) {
function hook_cmodel_pid_islandora_ingest_steps(array $form_state) {
}
/**
* Alter the generated ingest steps for the given content model.
*
* @param array $steps
* An array of steps as generated by hook_islandora_ingest_steps().
*
* @param array $form_state
* An array containing the Drupal form_state.
*/
function hook_cmodel_pid_islandora_ingest_steps_alter(array &$steps, array &$form_state) {
}
/**
@ -515,7 +540,7 @@ function hook_islandora_object_access($op, $object, $user) {
*
* @see hook_islandora_object_access()
*/
function hook_CMODEL_PID_islandora_object_access($op, $object, $user) {
function hook_cmodel_pid_islandora_object_access($op, $object, $user) {
}
/**
@ -554,7 +579,7 @@ function hook_islandora_datastream_access($op, $object, $user) {
*
* @see hook_islandora_datastream_access()
*/
function hook_CMODEL_PID_islandora_datastream_access($op, $object, $user) {
function hook_cmodel_pid_islandora_datastream_access($op, $object, $user) {
}
/**
@ -569,7 +594,7 @@ function hook_islandora_overview_object(AbstractObject $object) {
*
* Content model specific.
*/
function hook_CMODEL_PID_islandora_overview_object(AbstractObject $object) {
function hook_cmodel_pid_islandora_overview_object(AbstractObject $object) {
return drupal_render(drupal_get_form('some_form', $object));
}
/**
@ -584,17 +609,19 @@ function hook_islandora_overview_object_alter(AbstractObject &$object, &$output)
*
* Content model specific.
*/
function hook_CMODEL_PID_islandora_overview_object_alter(AbstractObject &$object, &$output) {
function hook_cmodel_pid_islandora_overview_object_alter(AbstractObject &$object, &$output) {
$output = $output . drupal_render(drupal_get_form('some_form', $object));
}
/*
/**
* Defines derivative functions to be executed based on certain conditions.
*
* @param AbstractObject $object
* Object to which derivatives will be added
* This hook fires when an object/datastream is ingested or a datastream is
* modified.
* modified. It may also be called to discover the datastream derivative
* hierarchy.
*
* @param AbstractObject $object
* Optional object to which derivatives will be added
*
* @return array
* An array containing an entry for each derivative to be created. Each entry
@ -602,21 +629,23 @@ function hook_CMODEL_PID_islandora_overview_object_alter(AbstractObject &$object
* - force: Bool denoting whether we are forcing the generation of
* derivatives.
* - source_dsid: (Optional) String of the datastream id we are generating
* from or NULL if it's the object itself.
* from or NULL if it's the object itself. Does not impact function
* ordering.
* - destination_dsid: (Optional) String of the datastream id that is being
* created. To be used in the UI.
* created. To be used in the UI. Does not impact function ordering.
* - weight: A string denoting the weight of the function. This value is
* sorted upon to run functions in order.
* - function: An array of function(s) to be ran when constructing
* derivatives. Functions that are defined to be called for derivation
* creation must have the following structure:
* module_name_derivative_creation_function($object, $force = FALSE)
* module_name_derivative_creation_function($object, $force = FALSE, $hook)
* These functions must return an array in the structure of:
* - success: Bool denoting whether the operation was successful.
* - messages: An array structure containing:
* - messages: An array structure containing zero or more array's with the
* following fields:
* - message: A string passed through t() describing the
* outcome of the operation.
* - message_sub: (Optional) Substitutions to be passed along to t() or
* - message_sub: (Optional) A substitution array as acceptable by t() or
* watchdog.
* - type: A string denoting whether the output is to be
* drupal_set_messaged (dsm) or watchdogged (watchdog).
@ -626,7 +655,7 @@ function hook_CMODEL_PID_islandora_overview_object_alter(AbstractObject &$object
* - file: A string denoting the path to the file where the function
* is being called from.
*/
function hook_islandora_derivative(AbstractObject $object) {
function hook_islandora_derivative(AbstractObject $object = NULL) {
$derivatives[] = array(
'source_dsid' => 'OBJ',
@ -665,12 +694,11 @@ function hook_islandora_derivative(AbstractObject $object) {
*
* @see hook_islandora_derivative()
*/
function hook_CMODEL_PID_islandora_derivative() {
function hook_cmodel_pid_islandora_derivative() {
}
/**
* Retrieves PIDS of related objects for property updating.
*
* @param AbstractObject $object
@ -679,7 +707,7 @@ function hook_CMODEL_PID_islandora_derivative() {
function hook_islandora_update_related_objects_properties(AbstractObject $object) {
$related_objects = get_all_children_siblings_and_friends($object);
$pids_to_return = array();
foreach($related_objects as $related_object) {
foreach ($related_objects as $related_object) {
$pids_to_return[] = $related_object->id;
}
return $pids_to_return;
@ -716,7 +744,7 @@ function hook_islandora_breadcrumbs_alter(&$breadcrumbs, $context, $object = NUL
* wishes to default back to the Dublin Core display for the current object.
* -configuration (Optional): A path to the administration page for the
* metadata display.
*
* @see islandora_retrieve_metadata_markup()
*/
function hook_islandora_metadata_display_info() {

5
islandora.drush.inc

@ -176,8 +176,7 @@ function drush_islandora_solution_pack_install_content_models() {
$info = islandora_solution_packs_get_required_objects($module);
$objects_to_add = array();
foreach ($info['objects'] as $key => $candidate) {
$object = islandora_object_load($candidate);
if ($object && in_array('fedora-system:ContentModel-3.0', $object->models)) {
if (in_array('fedora-system:ContentModel-3.0', $candidate->models)) {
$objects_to_add[] = $candidate;
}
}
@ -194,7 +193,7 @@ function drush_islandora_solution_pack_install_content_models() {
$new_object = islandora_add_object($object_to_add);
$verb = $deleted ? dt("Replaced") : dt("Added");
if ($new_object) {
drush_log("$verb " . $object_to_add->id . " - " . $object_to_add->label);
drush_print("$verb " . $object_to_add->id . " - " . $object_to_add->label);
}
}
}

7
islandora.info

@ -13,7 +13,11 @@ files[] = includes/dublin_core.inc
files[] = includes/tuque.inc
files[] = includes/tuque_wrapper.inc
files[] = includes/object.entity_controller.inc
files[] = tests/islandora_web_test_case.inc
files[] = tests/includes/datastream_validators.inc
files[] = tests/includes/islandora_web_test_case.inc
files[] = tests/includes/islandora_unit_test_case.inc
files[] = tests/includes/utilities.inc
files[] = tests/includes/test_utility_abstraction.inc
files[] = tests/authtokens.test
files[] = tests/hooks.test
files[] = tests/ingest.test
@ -22,4 +26,5 @@ files[] = tests/islandora_manage_permissions.test
files[] = tests/datastream_versions.test
files[] = tests/datastream_cache.test
files[] = tests/derivatives.test
files[] = tests/datastream_validator_tests.test
php = 5.3

5
islandora.install

@ -43,6 +43,11 @@ function islandora_install() {
function islandora_uninstall() {
module_load_include('inc', 'islandora', 'includes/solution_packs');
islandora_install_solution_pack('islandora', 'uninstall');
// Add new variables to clean up.
$variables = array(
'islandora_ds_replace_exclude_enforced',
);
array_walk($variables, 'variable_del');
}
/**

190
islandora.module

@ -37,7 +37,7 @@ define('ISLANDORA_VIEW_DATASTREAM_HISTORY', 'view old datastream versions');
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');
// Hooks.
define('ISLANDORA_VIEW_HOOK', 'islandora_view_object');
@ -64,19 +64,6 @@ define('ISLANDORA_DERVIATIVE_CREATION_HOOK', 'islandora_derivative');
define('ISLANDORA_CONTENT_MODELS_AUTOCOMPLETE', 'islandora/autocomplete/content-models');
define('ISLANDORA_MIME_TYPES_AUTOCOMPLETE', 'islandora/autocomplete/mime-types');
/**
* @deprecated Constants.
*/
// @codingStandardsIgnoreStart
define('DS_COMP_STREAM', ISLANDORA_DS_COMP_STREAM);
define('FEDORA_VIEW_OBJECTS', ISLANDORA_VIEW_OBJECTS);
define('FEDORA_METADATA_EDIT', ISLANDORA_METADATA_EDIT);
define('FEDORA_ADD_DS', ISLANDORA_ADD_DS);
define('FEDORA_INGEST', ISLANDORA_INGEST);
define('FEDORA_PURGE', ISLANDORA_PURGE);
define('FEDORA_MANAGE_PROPERTIES', ISLANDORA_MANAGE_PROPERTIES);
// @codingStandardsIgnoreEnd
/**
* Implements hook_menu().
*
@ -109,13 +96,35 @@ function islandora_menu() {
'file' => 'includes/metadata.inc',
'access arguments' => array('administer site configuration'),
);
$items['admin/islandora/solution_packs'] = array(
'title' => 'Solution packs',
$items['admin/islandora/solution_pack_config'] = array(
'title' => 'Solution pack configuration',
'description' => 'Configure Islandora solution packs.',
'access callback' => 'islandora_find_package',
'access arguments' => array('Islandora Solution Packs'),
'type' => MENU_NORMAL_ITEM,
);
$items['admin/islandora/islandora_viewers'] = array(
'title' => 'Islandora Viewers',
'description' => 'Configure custom viewers.',
'access callback' => 'islandora_find_package',
'access arguments' => array('Islandora Viewers'),
'type' => MENU_NORMAL_ITEM,
);
$items['admin/islandora/tools'] = array(
'title' => 'Islandora Utility Modules',
'description' => 'Configure Islandora utility modules.',
'access callback' => 'islandora_find_package',
'access arguments' => array('Islandora Tools'),
'type' => MENU_NORMAL_ITEM,
);
$items['admin/islandora/solution_pack_config/solution_packs'] = array(
'title' => 'Solution packs required objects',
'description' => 'Install content models and collections required by installed solution packs.',
'page callback' => 'islandora_solution_packs_admin',
'access arguments' => array(ISLANDORA_ADD_DS),
'file' => 'includes/solution_packs.inc',
'type' => MENU_NORMAL_ITEM,
'weight' => -1,
);
$items['islandora'] = array(
'title' => 'Islandora Repository',
@ -133,9 +142,11 @@ function islandora_menu() {
'access arguments' => array(ISLANDORA_VIEW_OBJECTS, 2),
);
$items['islandora/object/%islandora_object/print_object'] = array(
'title' => 'Print Object',
'weight' => 20,
'page callback' => 'islandora_printer_object',
'page arguments' => array(2),
'type' => MENU_NORMAL_ITEM,
'type' => MENU_LOCAL_TASK,
'access callback' => 'islandora_object_access',
'access arguments' => array(ISLANDORA_VIEW_OBJECTS, 2),
);
@ -295,6 +306,16 @@ function islandora_menu() {
'access arguments' => array(ISLANDORA_VIEW_DATASTREAM_HISTORY, 4),
'load arguments' => array(2),
);
$items['islandora/object/%islandora_object/datastream/%islandora_datastream/replace'] = array(
'title' => 'Replace Datastream',
'page arguments' => array('islandora_datastream_version_replace_form', 4),
'page callback' => 'drupal_get_form',
'file' => 'includes/datastream.version.inc',
'type' => MENU_CALLBACK,
'access callback' => 'islandora_datastream_access',
'access arguments' => array(ISLANDORA_REPLACE_DATASTREAM_CONTENT, 4),
'load arguments' => array(2),
);
$items['islandora/object/%islandora_object/datastream/%islandora_datastream/version/%/delete'] = array(
'title' => 'Delete datastream version',
'page arguments' => array('islandora_delete_datastream_version_form', 4, 6),
@ -494,6 +515,10 @@ function islandora_theme() {
'file' => 'theme/theme.inc',
'variables' => array('datastream' => NULL),
),
'islandora_datastream_replace_link' => array(
'file' => 'theme/theme.inc',
'variables' => array('datastream' => NULL),
),
'islandora_dublin_core_display' => array(
'file' => 'theme/theme.inc',
'template' => 'theme/islandora-dublin-core-display',
@ -572,6 +597,10 @@ function islandora_permission() {
'title' => t('Regenerate derivatives'),
'description' => t('Regenerate derivatives for an object or per datastream.'),
),
ISLANDORA_REPLACE_DATASTREAM_CONTENT => array(
'title' => t('Replace datastreams'),
'description' => t('Add new datastream content as latest version.'),
),
);
}
@ -677,6 +706,7 @@ function islandora_forms($form_id) {
* otherwise.
*/
function islandora_user_access($object_or_datastream, array $permissions, $content_models = array(), $access_any = TRUE, $user = NULL) {
module_load_include('inc', 'islandora', 'includes/utilities');
$is_repository_accessible = &drupal_static(__FUNCTION__);
@ -880,7 +910,8 @@ function islandora_object_manage_access_callback($perms, $object = NULL) {
function islandora_manage_overview_object(AbstractObject $object) {
module_load_include('inc', 'islandora', 'includes/utilities');
$output = array();
foreach (islandora_build_hook_list(ISLANDORA_OVERVIEW_HOOK, $object->models) as $hook) {
$hooks = islandora_build_hook_list(ISLANDORA_OVERVIEW_HOOK, $object->models);
foreach ($hooks as $hook) {
$temp = module_invoke_all($hook, $object);
islandora_as_renderable_array($temp);
if (!empty($temp)) {
@ -892,7 +923,7 @@ function islandora_manage_overview_object(AbstractObject $object) {
$output = islandora_default_islandora_manage_overview_object($object);
}
arsort($output);
drupal_alter(ISLANDORA_OVERVIEW_HOOK, $object, $output);
drupal_alter($hooks, $object, $output);
islandora_as_renderable_array($output);
return $output;
}
@ -995,17 +1026,6 @@ function islandora_view_object(AbstractObject $object) {
module_load_include('inc', 'islandora', 'includes/breadcrumb');
module_load_include('inc', 'islandora', 'includes/utilities');
// Add the print button via JavaScript.
$path = drupal_get_path('module', 'islandora');
drupal_add_js(array(
'islandora' => array(
'print_img' => $path . '/images/print-icon.png'),
), array(
'type' => 'setting'));
drupal_add_js(array('islandora' => array('print_link' => 'islandora/object/' . $object->id . '/print_object')), array('type' => 'setting'));
drupal_add_js($path . '/js/add_print.js');
drupal_set_title($object->label);
drupal_set_breadcrumb(islandora_get_breadcrumbs($object));
@ -1530,6 +1550,11 @@ function islandora_entity_property_info() {
'label' => t('Content Models'),
'description' => t('The list of content models which the object has.'),
);
$p['createdDate'] = array(
'type' => 'text',
'label' => t('Created Date'),
'description' => t('When the object was created.'),
);
return $info;
}
@ -1681,7 +1706,7 @@ function islandora_islandora_basic_collection_get_query_filters() {
if ($enforced) {
$namespace_array = islandora_get_allowed_namespaces();
$namespace_sparql = implode('|', $namespace_array);
return format_string('regex(str(?object), "info:fedora/(!namespaces):")', array('!namespaces' => $namespace_sparql));
return array('islandora_namespace_restrictions' => format_string('regex(str(?object), "info:fedora/(!namespaces):")', array('!namespaces' => $namespace_sparql)));
}
}
@ -1694,10 +1719,7 @@ function islandora_islandora_basic_collection_get_query_filters() {
*/
function islandora_islandora_object_ingested(AbstractObject $object) {
module_load_include('inc', 'islandora', 'includes/derivatives');
$logging_results = islandora_do_derivatives($object, array(
'source_dsid' => NULL,
));
islandora_derivative_logging($logging_results);
islandora_run_derivatives($object, NULL);
}
/**
@ -1708,10 +1730,7 @@ function islandora_islandora_object_ingested(AbstractObject $object) {
*/
function islandora_islandora_datastream_ingested(AbstractObject $object, AbstractDatastream $datastream) {
module_load_include('inc', 'islandora', 'includes/derivatives');
$logging_results = islandora_do_derivatives($object, array(
'source_dsid' => $datastream->id,
));
islandora_derivative_logging($logging_results);
islandora_run_derivatives($object, $datastream->id);
}
/**
@ -1787,15 +1806,12 @@ function islandora_form_simpletest_test_form_alter(array &$form) {
function islandora_repair_drupal_filter() {
// Grab the config.
$path = drupal_get_path('module', 'islandora');
if (file_exists("$path/tests/test_config.ini")) {
$configuration = parse_ini_file("$path/tests/test_config.ini");
}
elseif (file_exists("$path/tests/default.test_config.ini")) {
$configuration = parse_ini_file("$path/tests/default.test_config.ini");
module_load_include('inc', 'islandora', 'tests/utilities/test_utility_abstraction');
try {
$configuration = IslandoraTestUtilityClass::getTestConfiguration();
}
else {
drupal_set_message(t('Required default.test_config.ini/test_config.ini file not found'), 'error');
catch (Exception $e) {
drupal_set_message(t("Error parsing test configuration: %e", array('%e' => $e)), 'error');
return FALSE;
}
@ -1824,9 +1840,9 @@ function islandora_repair_drupal_filter() {
}
}
/**
* Implements hook_islandora_metadata_display_info().
*/
/**
* Implements hook_islandora_metadata_display_info().
*/
function islandora_islandora_metadata_display_info() {
return array(
'dublin_core' => array(
@ -1852,7 +1868,8 @@ function islandora_islandora_datastream_access($op, AbstractDatastream $datastre
$hooks = islandora_invoke_hook_list(ISLANDORA_DERVIATIVE_CREATION_HOOK, $object->models, array($object));
$hooks = islandora_filter_derivatives($hooks, array('force' => TRUE), $object);
foreach ($hooks as $hook) {
if ($hook['destination_dsid'] == $datastream->id && islandora_datastream_access(ISLANDORA_VIEW_OBJECTS, $object[$hook['source_dsid']], $user)) {
if ($hook['destination_dsid'] == $datastream->id &&
(is_null($hook['source_dsid']) || islandora_datastream_access(ISLANDORA_VIEW_OBJECTS, $object[$hook['source_dsid']], $user))) {
$applicable_hook = TRUE;
break;
}
@ -1863,3 +1880,74 @@ function islandora_islandora_datastream_access($op, AbstractDatastream $datastre
}
return $result;
}
/**
* Access for submenu items.
*
* @param string $package_name
* Name of the package
*
* @return bool
* Access granted
*/
function islandora_find_package($package_name) {
$results = system_get_info('module');
$found = FALSE;
foreach ($results as $name => $values) {
if ($values['package'] == $package_name) {
$found = TRUE;
break;
}
}
return $found && user_access('administer site configuration');
}
/**
* Helper function for ingest steps and batches.
*
* When batches within batches are triggered within ingest steps
* the submit handler becomes out of scope. When it becomes time to execute the
* submit the function cannot be found and fails. This pre-submit is to
* get around this problem.
*
* @see https://www.drupal.org/node/2300367
*
* @see https://www.drupal.org/node/1300928
*/
function islandora_ingest_form_pre_submit($form, &$form_state) {
module_load_include('inc', 'islandora', 'includes/ingest.form');
}
/**
* Implements hook_menu_local_tasks_alter().
*/
function islandora_menu_local_tasks_alter(&$data, $router_item, $root_path) {
if (strpos($root_path, 'islandora/object/%') === 0) {
if (isset($data['tabs'][0]['output'])) {
foreach ($data['tabs'][0]['output'] as $key => &$tab) {
if ($tab['#link']['path'] == 'islandora/object/%/print_object') {
if ($root_path == 'islandora/object/%') {
$islandora_path = drupal_get_path('module', 'islandora');
$tab['#theme'] = 'link';
$tab['#text'] = theme('image', array(
'path' => "$islandora_path/images/print-icon.png",
'alt' => t('Print Object'),
'attributes' => array(
'id' => 'print_btn',
),
));
$tab['#path'] = "{$router_item['href']}/print_object";
$tab['#options'] = array(
'attributes' => array(),
'html' => TRUE,
);
}
else {
unset($data['tabs'][0]['output'][$key]);
break;
}
}
}
}
}
}

23
islandora.rules.inc

@ -5,11 +5,30 @@
* Does rule type stuff,
*/
/**
* Implements hook_rules_event_info().
*/
function islandora_rules_event_info() {
return array(
'islandora_object_ingested' => array(
'group' => t('Islandora'),
'label' => t('Object ingested'),
'variables' => array(
'object' => array(
'type' => 'islandora_object',
'label' => t('The ingested object'),
'description' => t('A Tuque object for the ingested Fedora object, as an entity.'),
),
),
),
);
}
/**
* Helper function to get reused "parameter" array.
*/
function islandora_rules_relationship_parameter_array() {
$to_return = array(
return array(
'subject' => array(
'type' => 'islandora_object',
'label' => t('Subject'),
@ -39,8 +58,6 @@ function islandora_rules_relationship_parameter_array() {
'default value' => 0,
),
);
return $to_return;
}
/**

26
islandora.rules_defaults.inc

@ -0,0 +1,26 @@
<?php
/**
* @file
* Implementation of Rules hooks, giving default configurations.
*/
/**
* Implements hook_default_rules_configuration().
*/
function islandora_default_rules_configuration() {
$rule = rules_reaction_rule();
$rule->label = 'E-mail admin';
$rule->active = FALSE;
$rule
->event('islandora_object_ingested')
->action(
'mail',
array(
'to' => '[site:mail]',
'subject' => '[[site:name]] "[object:label]" has been ingested',
'message' => '[object:label] has been ingested as [object:id].',
)
);
return array('islandora_object_ingested_notify_admin' => $rule);
}

18
js/add_print.js

@ -1,18 +0,0 @@
/**
* @file
* JavaScript file responsable for the print button behaviour.
*
* The print button is added automatically to every view, as metadata
* can be printed from every object.
*
*/
(function ($) {
$(document).ready(function() {
$('.tabs .primary').append('<img id="print_btn" title="Print" src="' + Drupal.settings.basePath + Drupal.settings.islandora.print_img + '"></img>');
$('#print_btn').css("cursor","pointer");
$('#print_btn').click(function() {
window.location=Drupal.settings.basePath + Drupal.settings.islandora.print_link;
});
});
})(jQuery);

31
policies/permit-apim-to-authenticated-user.xml

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Policy xmlns="urn:oasis:names:tc:xacml:1.0:policy"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
PolicyId="permit-apim-to-authenticated-user"
RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:first-applicable">
<Description>note that other policies may provide exceptions to this broad policy. This policy assumes api-m users have to be authenticated</Description>
<Target>
<Subjects>
<Subject>
<SubjectMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">authenticated user</AttributeValue>
<SubjectAttributeDesignator AttributeId="fedoraRole" MustBePresent="false"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</SubjectMatch>
</Subject>
</Subjects>
<Resources>
<AnyResource/>
</Resources>
<Actions>
<Action>
<ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">urn:fedora:names:fedora:2.1:action:api-m</AttributeValue>
<ActionAttributeDesignator DataType="http://www.w3.org/2001/XMLSchema#string"
AttributeId="urn:fedora:names:fedora:2.1:action:api"/>
</ActionMatch>
</Action>
</Actions>
</Target>
<Rule RuleId="1" Effect="Permit"/>
</Policy>

25
policies/permit-getDatastream-unrestricted.xml

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Policy xmlns="urn:oasis:names:tc:xacml:1.0:policy"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
PolicyId="permit-getDatastreamHistory-to-authenticated"
RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:first-applicable">
<Description>Note that other policies may provide exceptions to this broad policy. This policy assumes api-m users have to be authenticated</Description>
<Target>
<Subjects>
<AnySubject/>
</Subjects>
<Resources>
<AnyResource/>
</Resources>
<Actions>
<Action>
<ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">urn:fedora:names:fedora:2.1:action:id-getDatastream</AttributeValue>
<ActionAttributeDesignator DataType="http://www.w3.org/2001/XMLSchema#string"
AttributeId="urn:fedora:names:fedora:2.1:action:id"/>
</ActionMatch>
</Action>
</Actions>
</Target>
<Rule RuleId="1" Effect="Permit"/>
</Policy>

25
policies/permit-getDatastreamHistory-unrestricted.xml

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Policy xmlns="urn:oasis:names:tc:xacml:1.0:policy"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
PolicyId="permit-getDatastreamHistory-to-authenticated"
RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:first-applicable">
<Description>Note that other policies may provide exceptions to this broad policy. This policy assumes api-m users have to be authenticated</Description>
<Target>
<Subjects>
<AnySubject/>
</Subjects>
<Resources>
<AnyResource/>
</Resources>
<Actions>
<Action>
<ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">urn:fedora:names:fedora:2.1:action:id-getDatastreamHistory</AttributeValue>
<ActionAttributeDesignator DataType="http://www.w3.org/2001/XMLSchema#string"
AttributeId="urn:fedora:names:fedora:2.1:action:id"/>
</ActionMatch>
</Action>
</Actions>
</Target>
<Rule RuleId="1" Effect="Permit"/>
</Policy>

31
policies/permit-upload-to-authenticated-user.xml

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Policy xmlns="urn:oasis:names:tc:xacml:1.0:policy"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
PolicyId="permit-upload-to-authenticated-user"
RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:first-applicable">
<Description></Description>
<Target>
<Subjects>
<Subject>
<SubjectMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">authenticated user</AttributeValue>
<SubjectAttributeDesignator AttributeId="fedoraRole" MustBePresent="false"
DataType="http://www.w3.org/2001/XMLSchema#string"/>
</SubjectMatch>
</Subject>
</Subjects>
<Resources>
<AnyResource/>
</Resources>
<Actions>
<Action>
<ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
<AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">urn:fedora:names:fedora:2.1:action:id-upload</AttributeValue>
<ActionAttributeDesignator DataType="http://www.w3.org/2001/XMLSchema#string"
AttributeId="urn:fedora:names:fedora:2.1:action:id"/>
</ActionMatch>
</Action>
</Actions>
</Target>
<Rule RuleId="1" Effect="Permit"/>
</Policy>

41
tests/README.md

@ -1,4 +1,43 @@
You can define your own configurations specific to your enviroment by copying
OVERVIEW
********
You can define your own configurations specific to your environment by copying
default.test_config.ini to test_config.ini, making your changes in the copied
file. These test need write access to the system's $FEDORA_HOME/server/config
directory as well as the filter-drupal.xml file.
DATASTREAM VALIDATION TESTS
***************************
The datastream validator included in the Islandora testing suite is able to
generate tests procedurally based on the files in the folder
'fixtures/datastream_validator_files'. By default, this folder is empty.
The unit tests for the validator pull the name of the file (before the
extension) and use that to instantiate the correct ______DatastreamValidator
class to test that file against (e.g. Image.jpg spins up an instance of the
ImageDatastreamValidator class and checks the results).
You can test against multiple different encodings of the same filetype by giving
each file a different set of extensions, e.g. MP3.vbr.mp3 and MP3.sbr.mp3 both
test against the MP3 datastream validator, even though both are encoded
differently.
For classes that require the third parameter (e.g. the TextDatastreamValidator),
place an additional name.extension.ini file in the datastream_validator_files
folder (e.g. the Text.txt would be paired with Text.txt.ini). This .ini file
should be structured like a PHP .ini file (e.g. according to the format used by
http://php.net/parse_ini_file).The generated test will parse the .ini
file as an array and pass it on to the third parameter.
The following prefixes are currently available for use:
- Image (jpg, png, gif, and other filetypes recognized by PHPGD)
- TIFF
- JP2
- PDF
- Text (requires a configured .ini)
- WAV
- MP3
- MP4
- OGG (asserts OGG video; use an .ini with an 'audio' key to test audio only)
- MKV

276
tests/datastream_validator_tests.test

@ -0,0 +1,276 @@
<?php
/**
* @file
* Tests for things that test tests. Madness.
*/
/**
* A test DatastreamValidator for the DatastreamValidatorResultTestCase.
*/
class TestDatastreamValidator extends DatastreamValidator {
/**
* Assertion that adds a TRUE result.
*/
protected function assertSomethingSuccessfully() {
$this->addResult(TRUE, 'yay you did it', $this->getAssertionCall());
}
/**
* Assertion that adds a FALSE result.
*/
protected function assertSomethingFailed() {
$this->addResult(FALSE, 'boo you failed', $this->getAssertionCall());
}
}
/**
* Tests the ability of DatastreamValidators to produce correct results.
*/
class DatastreamValidatorResultTestCase extends IslandoraWebTestCase {
/**
* Returns the info for this test case.
*
* @see DrupalWebTestCase::getInfo()
*/
public static function getInfo() {
return array(
'name' => 'Datastream Validator Result Tests',
'description' => 'Unit tests for datastream validation result functionality.',
'group' => 'Islandora',
);
}
/**
* Sets up the test.
*
* @see DrupalWebTestCase::setUp()
*/
public function setUp() {
parent::setUp();
$user = $this->drupalCreateUser(array_keys(module_invoke_all('permission')));
$this->drupalLogin($user);
$object = $this->ingestConstructedObject();
$validator = new TestDatastreamValidator($object, 'DC');
$this->validator_results = $validator->getResults();
}
/**
* Generates a generic DatastreamValidatorResult and grabs its properties.
*/
public function testDatastreamValidatorResult() {
$result = new IslandoraTestUtilityResult(TRUE, 'true', array());
$this->assertEqual($result->getMessage(), 'true', "Result message generated correctly.", 'Islandora');
$this->assertEqual($result->getType(), TRUE, "Result type generated correctly.", 'Islandora');
$this->assertEqual($result->getCaller(), array(), "Result caller generated correctly.", 'Islandora');
}
/**
* Gets the results of TestDatastreamValidator and confirms only 2 exist.
*/
public function testTestDatastreamValidatorResultCount() {
$this->assertTrue(isset($this->validator_results[0]), "First of two expected results found.", 'Islandora');
$this->assertTrue(isset($this->validator_results[1]), "Second of two expected results found.", 'Islandora');
$this->assertFalse(isset($this->validator_results[2]), "No other unexpected results found.", 'Islandora');
}
/**
* Confirms the messages from TestDatastreamValidator.
*/
public function testTestDatastreamValidatorMessages() {
if (isset($this->validator_results[0]) && isset($this->validator_results[1])) {
$this->assertEqual($this->validator_results[0]->getMessage(), 'yay you did it', "Appropriate pass message returned.", 'Islandora');
$this->assertEqual($this->validator_results[1]->getMessage(), 'boo you failed', "Appropriate fail message returned.", 'Islandora');
}
}
/**
* Confirms the types from TestDatastreamValidator.
*/
public function testTestDatastreamValidatorTypes() {
if (isset($this->validator_results[0]) && isset($this->validator_results[1])) {
$this->assertEqual($this->validator_results[0]->getType(), TRUE, "Appropriate pass type of TRUE returned.", 'Islandora');
$this->assertEqual($this->validator_results[1]->getType(), FALSE, "Appropriate fail type of FALSE returned.", 'Islandora');
}
}
/**
* Confirms the useful information from TestDatastreamValidator.
*/
public function testTestDatastreamValidatorCallers() {
if (isset($this->validator_results[0]) && isset($this->validator_results[1])) {
// Grab the callers.
$first_caller = $this->validator_results[0]->getCaller();
$second_caller = $this->validator_results[1]->getCaller();
// Assert the 'file' key.
$this->assertTrue(substr($first_caller['file'], -48) === '/islandora/tests/datastream_validator_tests.test', "Appropriate pass caller file returned.", 'Islandora');
$this->assertTrue(substr($first_caller['file'], -48) === substr($second_caller['file'], -48), "Fail caller file matches the pass caller file.", 'Islandora');
$this->assertTrue($first_caller['function'] === 'TestDatastreamValidator->assertSomethingSuccessfully()', "Correct pass caller function returned (actual: {$first_caller['function']}; expected: TestDatastreamValidator->assertSomethingSuccessfully()).", 'Islandora');
$this->assertTrue($second_caller['function'] === 'TestDatastreamValidator->assertSomethingFailed()', "Correct fail caller function returned (actual: {$second_caller['function']}; expected: TestDatastreamValidator->assertSomethingFailed()).", 'Islandora');
$this->assertTrue($first_caller['line'] === 16, "Correct pass line number returned (actual: {$first_caller['line']}; expected: 16).", 'Islandora');
$this->assertTrue($second_caller['line'] === 23, "Correct fail line number returned (actual: {$second_caller['line']}; expected: 23).", 'Islandora');
}
}
}
/**
* Procedurally generated tests for DatastreamValidators.
*/
class PrefixDatastreamValidatorTestCase extends IslandoraWebTestCase {
/**
* The path to the datastream validator files.
*
* @var string
*/
protected $path;
/**
* Returns the info for this test case.
*
* @see DrupalWebTestCase::getInfo()
*/
public static function getInfo() {
return array(
'name' => 'Datastream File Validation Tests',
'description' => 'Tests each file in the islandora/tests/fixtures/datastream_validator_files folder against the appropriate DatastreamValidator class (see the README.md in the tests folder for details).',
'group' => 'Islandora',
);
}
/**
* Sets up the test.
*
* @see DrupalWebTestCase::setUp()
*/
public function setUp() {
parent::setUp();
$this->path = DRUPAL_ROOT . drupal_get_path('module', 'islandora') . "/tests/fixtures/datastream_validator_files/";
}
/**
* Confirms that a DatastreamValidator class exists for given filename.
*
* @param string $filename
* The file to grab the DatastreamValidator prefix from.
*
* @return bool
* TRUE if such a class exists; FALSE otherwise.
*/
protected function confirmValidatorClass($filename) {
$prefix = $this->getPrefix($filename);
if (!class_exists("{$prefix}DatastreamValidator")) {
$this->fail("No such DatastreamValidator exists for the prefix $prefix (filename: $filename).");
return FALSE;
}
return TRUE;
}
/**
* Confirms that a file exists at the given path.
*
* Bundled with lovely return values and fail messages.
*
* @param string $path
* The path to the file.
*
* @return bool
* TRUE if the file exists, FALSE otherwise.
*/
protected function confirmValidatorFile($path) {
if (!file_exists($path)) {
$this->fail("No such file exists at path $path.");
return FALSE;
}
return TRUE;
}
/**
* Gets the intended prefix from a filename.
*
* Uses the portion of the filename before the first period.
*
* @param string $filename
* The filename to get the prefix for.
*
* @return string
* The intended prefix.
*/
protected function getPrefix($filename) {
return array_shift(explode('.', $filename));
}
/**
* Create an object with a datastream generated from the given filename.
*
* @param string $filename
* The filename to use when adding a datastream.
*
* @return IslandoraFedoraObject
* The created object.
*/
protected function createObjectWithDatastream($filename) {
$prefix = $this->getPrefix($filename);
if (!$this->confirmValidatorClass($filename) && !$this->confirmValidatorFile($this->path . $filename)) {
return FALSE;
}
$datastreams = array(array(
'dsid' => $prefix,
'path' => $this->path . $filename,
'control_group' => 'M',
),
);
$object = $this->ingestConstructedObject(array(), $datastreams);
return $object;
}
/**
* Procedurally test each file in $this->path against a datastream validator.
*
* Check the README.md in this folder for details.
*/
public function testDatastreamValidators() {
// Grab everything in the validator files folder except for .ini files.
$files = array_intersect(glob("{$this->path}/*"), glob("{$this->path}/*.ini"));
foreach ($files as $file) {
$prefix = $this->getPrefix($file);
// If createObjectWithDatastream fails, we don't want to continue. We'll
// task it with returning fail messages rather than do so here.
$object = $this->createObjectWithDatastream($file);
if ($object !== FALSE) {
// Generate an appropriate validator.
$validator_name = "{$prefix}DatastreamValidator";
// If the file is paired with an .ini, use it as the third param.
if (file_exists("{$this->path}$file.ini")) {
$validator = new $validator_name($object, $prefix, parse_ini_file("{$this->path}$file.ini"));
}
else {
$validator = new $validator_name($object, $prefix);
}
// Get the results, check for fails.
$results = $validator->getResults();
$fails = FALSE;
foreach ($results as $result) {
if (!$result->getType()) {
$fails = TRUE;
$caller = $result->getCaller();
$this->fail("Failed to validate the test file $file against the assertion {$caller['function']}.");
}
}
// If there were no fails, say that the file passed validation.
if (!$fails) {
$this->pass("Test file $file validated successfully.");
}
}
}
}
}

473
tests/datastream_validators.inc

@ -1,473 +0,0 @@
<?php
/**
* @file
* Assertions for various datastream types.
*
* For a datastream validator to work correctly with IslandoraWebTestCase::
* validateDatastreams(), it needs to return an array of results, each entry of
* which contains two values: first, TRUE or FALSE, depending on whether or not
* that particular result passed or failed, and second, a string containing a
* message to accompany the result.
*
* It also should contain three parameters, all of which may use any label, but
* must be organized in the following order:
* $object - an object that the datastream can be loaded from.
* $datastream - a DSID to pull from $object.
* $optional_params - a parameter for any data the function requires.
*
* When IslandoraWebTestCase::validateDatastreams() is called, it is passed an
* array of datastreams, each of which is itself an array containing the DSID of
* the datastream, the middle of the function name (image, pdf, tiff, etc.), and
* (optional) data to be passed to that third parameter.
*/
/**
* A function to pass assertions to and receive results from.
*
* @param bool $assertion
* The if/then statement to validate against.
* @param array $results
* An array of results to append the generated result to.
* @param string $pass
* A message to return if the assertion turns up true.
* @param string $fail
* An optional message to return if the assertion turns up false.
* If left empty, the $pass message will be returned.
*
* @return array
* A result that can be made useful in the validation functions below.
*/
function islandora_assert_valid($assertion, $results, $pass, $fail = NULL) {
if ($assertion) {
$result = array(TRUE, $pass);
}
else {
if (isset($fail)) {
$result = array(FALSE, $fail);
}
else {
$result = array(FALSE, $pass);
}
}
array_push($results, $result);
return $results;
}
/**
* Converts a hexidecimal string to an integer.
*
* This is useful for running checks on values that appear in the binary
* of a datastream. Returns FALSE if the hex value contains non-hex characters
* or if the string would not return a 16- or 32-bit formatted big-endian
* signed integer.
*
* @param string $hex
* The hexidecimal string.
*
* @return bool|int
* FALSE on failure, or the integer on success.
*/
function islandora_hex2int($hex) {
// A couple of quick string checks.
if (!ctype_xdigit($hex)) {
drupal_set_message(t('String passed to islandora_hex2int() contains non-hexidecimal characters.'), 'error');
return FALSE;
}
if (!strlen($hex) === 4 || !strlen($hex) === 8) {
drupal_set_message(t('String passed to islandora_hex2int() cannot create a 16- or 32-bit little-endian signed integer'), 'error');
return FALSE;
}
// The actual conversion.
try {
$reverse_hex = implode('', array_reverse(str_split($hex, 2)));
$int = hexdec($reverse_hex);
return $int;
}
catch (Exception $e) {
throw new Exception('An error occurred during the conversion of hexidecimal to integer.', 0, $e);
}
}
/**
* Asserts that an object's given datastreams are common-type image files.
*
* Uses PHPGD to run the assertion check. This means that only certain kinds
* of image files can be checked. Please check the documentation for the PHPGD
* imagecreatefromstring() function to determine what filetypes are valid.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID to check that corresponds to a PHPGD-valid image datastream.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_image_datastream($object, $datastream) {
$datastream_string = $object[$datastream]->content;
$results = array();
$pass = "Image datastream {$datastream} is valid.";
$fail = "Image datastream {$datastream} is either invalid or corrupt.";
$results = islandora_assert_valid(imagecreatefromstring($datastream_string), $results, $pass, $fail);
return $results;
}
/**
* Asserts the validity of any .tif/.tiff datastream.
*
* Does not use the islandora_assert_valid() function, as this is not a simple
* true/false.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID to check that corresponds to a .tif/.tiff datastream.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_tiff_datastream($object, $datastream) {
$datastream_string = $object[$datastream]->content;
$datastream_header_hex = substr(bin2hex($datastream_string), 0, 8);
$results = array();
if ($datastream_header_hex == "49492a00") {
// In this case, the ingested TIFF is designated as using the "Intel
// byte-order" (e.g. little-endian) by starting with the characters "II"
// (repeated so that byte order does not yet need to be significant).
// The number that follows is '42' in little-endian hex, a number of
// 'deep philosophical significance' to the TIFF format creators.
array_push($results, array(TRUE, "{$datastream} datastream asserts that it is a valid Intel-byte-orderded TIF/TIFF file."));
}
elseif ($datastream_header_hex == "4d4d002a") {
// In this case, the ingested TIFF is designated as using the "Motorola
// byte-order" (e.g. big-endian) by starting with the characters "MM"
// instead. 42 follows once again, this time in big-endian hex.
array_push($results, array(TRUE, "{$datastream} datastream asserts that it is a valid Motorola-byte-ordered TIF/TIFF file."));
}
else {
array_push($results, array(FALSE, "{$datastream} datastream does not assert that it is a valid TIF/TIFF file."));
}
return $results;
}
/**
* Asserts the validity of any .jp2 datastream.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID to check that corresponds to a .jp2 datastream.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_jp2_datastream($object, $datastream) {
$datastream_hex = bin2hex($object[$datastream]->content);
$results = array();
// JP2 files begin with an offset header at the second 32-bit integer,
// 0x6A502020. This header is in all .jp2s, and we check for it here.
$pass = "{$datastream} datastream begins correctly with the appropriate .jp2 header.";
$fail = "{$datastream} datastream does not begin with the appropriate .jp2 header.";
$results = islandora_assert_valid(substr($datastream_hex, 8, 8) == '6a502020', $results, $pass, $fail);
// JP2 files have their codestream capped with a marker, 0xFFD9. We're
// just checking for it here to see if the .jp2 encoder finished okay.
$pass = "{$datastream} datastream ends correctly with the appropriate .jp2 marker.";
$fail = "{$datastream} datastream does not end with a .jp2 marker; derivative generation was likely interrupted.";
$results = islandora_assert_valid(substr($datastream_hex, strlen($datastream_hex) - 4, 4) == 'ffd9', $results, $pass, $fail);
return $results;
}
/**
* Asserts the validity of any .pdf datastream.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID to check that corresponds to a .pdf datastream.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_pdf_datastream($object, $datastream) {
$pdf = $object[$datastream]->content;
$pdf_version = substr($pdf, 5, 3);
$results = array();
$pass = "{$datastream} datastream asserts that it is a valid PDF file using PDF version {$pdf_version}";
$fail = "{$datastream} datastream binary header appears to be corrupt and missing a valid PDF signature.";
$results = islandora_assert_valid(substr($pdf, 0, 5) == '%PDF-', $results, $pass, $fail);
$pdf_streams = substr_count(bin2hex($pdf), '0a73747265616d0a');
$pass = "{$datastream} datastream reports the existence of {$pdf_streams} PDF streams. Note that an extremely low number could still indicate corruption.";
$fail = "{$datastream} datastream contains zero PDF streams, and is likely not a PDF file.";
$results = islandora_assert_valid($pdf_streams, $results, $pass, $fail);
$pass = "{$datastream} datastream reports the existence of the closing 'EOF' tag required at the end of PDFs";
$fail = "{$datastream} datastream does not contain the closing 'EOF' tag. If this is the only PDF validation that failed, it is likely that derivative generation was interrupted.";
$results = islandora_assert_valid(strpos(bin2hex($pdf), '0a2525454f460a'), $results, $pass, $fail);
return $results;
}
/**
* Asserts that a string of text shows up inside a datastream.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID to check that corresponds to a datastream containing text.
* @param array $text
* An array of strings/the number of times it should appear in the datastream.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_text_datastream($object, $datastream, array $text) {
$results = array();
$content = $object[$datastream]->content;
$string_count = substr_count($content, $text[0]);
$pass = "{$datastream} datastream contains the word(s) '{$text[0]}' repeated {$string_count} time(s) (expected: {$text[1]}).";
$fail = "{$datastream} datastream contains the word(s) '{$text[0]}' repeated {$string_count} time(s) (expected: {$text[1]}).";
$results = islandora_assert_valid($string_count == $text[1], $results, $pass, $fail);
return $results;
}
/**
* Asserts the validity of any .wav datastraeam.
*
* WAV files contain a rigidly detailed header that contains all sorts of fun
* information we can use to validate things against other things. So, we check
* rigorously that the header contains properly constructed data by looking to
* see if certain values are at their expected byte offset. We also compare
* declared chunk sizes against actual sizes. If any of these are off, WAV
* players will fail to function.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID to check that corresponds to a datastream generated via OCR or HOCR.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_wav_datastream($object, $datastream) {
$results = array();
$wav = bin2hex($object['OBJ']->content);
$wav_subchunk2size = islandora_hex2int(substr($wav, 80, 8));
$wav_samplerate = islandora_hex2int(substr($wav, 48, 8));
$wav_numchannels = islandora_hex2int(substr($wav, 44, 4));
$wav_bytespersample = islandora_hex2int(substr($wav, 68, 4)) / 8;
$wav_numsamples = strlen(substr($wav, 88)) / $wav_numchannels / $wav_bytespersample / 2;
$magic_number = str_split(substr($wav, 0, 24), 8);
$pass = "Header of the {$datastream} datastream contains correct file signature";
$fail = "Header of the {$datastream} datastream contains corrupt file signature";
$results = islandora_assert_valid($magic_number[0] = '52494646' && $magic_number[2] = '57415645', $results, $pass, $fail);
$pass = "{$datastream} datastream chunksize in WAV header is correct";
$fail = "{$datastream} datastream chunksize in WAV header does not match actual chunksize.";
$results = islandora_assert_valid(islandora_hex2int(substr($wav, 8, 8)) === 36 + $wav_subchunk2size, $results, $pass, $fail);
$pass = "{$datastream} datastream contains a 'fmt' subchunk.";
$fail = "{$datastream} datastream is missing the required 'fmt' subchunk.";
$results = islandora_assert_valid(substr($wav, 24, 8) === '666d7420', $results, $pass, $fail);
$pass = "{$datastream} datastream byterate in the WAV header is correct.";
$fail = "{$datastream} datastream byterate in the WAV header does not match actual calculated byterate.";
$results = islandora_assert_valid(islandora_hex2int(substr($wav, 56, 8)) === $wav_samplerate * $wav_numchannels * $wav_bytespersample, $results, $pass, $fail);
$pass = "{$datastream} datastream block alignment is set correctly.";
$fail = "{$datastream} datastream block alignment is off.";
$results = islandora_assert_valid(islandora_hex2int(substr($wav, 64, 4)) === $wav_numchannels * $wav_bytespersample, $results, $pass, $fail);
$pass = "{$datastream} datastream contains 'data' subchunk.";
$fail = "{$datastream} datastream is missing the 'data' subchunk.";
$results = islandora_assert_valid(substr($wav, 72, 8) === '64617461', $results, $pass, $fail);
$pass = "{$datastream} datastream 'data' chunk is the correct size.";
$fail = "{$datastream} datastream 'data' chunk is sized incorrectly.";
$results = islandora_assert_valid($wav_subchunk2size === $wav_numsamples * $wav_numchannels * $wav_bytespersample, $results, $pass, $fail);
return $results;
}
/**
* Asserts the validity of any .mp3 datastream.
*
* Our default setup tries to create an MP3 using VBR, but we do some extra
* checks in case someone turns that off. If the header contains the characters
* 'Xing', it is flagged as VBR, and we can do an in-depth check on each of the
* VBR settings. Otherwise, we look for the basic MP3 signature 'fffa' or 'fffb'
* at the start of the binary.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID of a datastream corresponding to an mp3 file.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_mp3_datastream($object, $datastream) {
$results = array();
$mp3 = bin2hex($object[$datastream]->content);
$mp3_size = strlen($mp3) / 2;
// Looks to see if VBR was set properly by LAME. If so, MATH TIME!
if (strpos($mp3, '58696e67')) {
$mp3_vbrheader = substr($mp3, strpos($mp3, '58696e67'), 240);
// Check the field flags. VBR-formatted MP3 files contain a 32-bit
// integer (stored as $mp3_flag_value) that is a combination of four
// bits, each one indicating the on-off status of a VBR setting, via
// logical OR. Rather than disassembling this value into individual
// bits, we use the algorithm "if (binary_total+bit_value*2)/bit_value*2
// is greater than or equal to bit_value, that bit is turned on" to find
// the status of each bit, so we know whether to offset the rest.
$mp3_field_offset = array(0, 0, 0);
$mp3_flag_value = hexdec(substr($mp3_vbrheader, 8, 8));
// We can't use the first flag, but we still need to offset the rest.
if (($mp3_flag_value + 1) % 2 == 0) {
$mp3_field_offset[0] += 8;
$mp3_field_offset[1] += 8;
$mp3_field_offset[2] += 8;
}
// The second flag leads us to filesize data, which we can verify.
if (($mp3_flag_value + 4) % 4 > 1) {
$mp3_field_bytes = hexdec(substr($mp3_vbrheader, $mp3_field_offset[0] + 16, 8));
$pass = "{$datastream} datastream reported filesize of {$mp3_size} bytes matches size field value of {$mp3_field_bytes}";
$fail = "{$datastream} datastream reported filesize of {$mp3_size} bytes does not match size field value of {$mp3_field_bytes}";
$results = islandora_assert_valid($mp3_size == $mp3_field_bytes, $results, $pass, $fail);
$mp3_field_offset[1] += 8;
$mp3_field_offset[2] += 8;
}
// We can't use the third flag for anything either.
if (($mp3_flag_value + 8) % 8 > 3) {
$mp3_field_offset[2] += 200;
}
// The fourth flag leads us to VBR quality data, which we can validate.
if ($mp3_flag_value > 7) {
$mp3_field_quality = hexdec(substr($mp3_vbrheader, $mp3_field_offset[2] + 16, 8));
$pass = "{$datastream} datastream reports valid VBR quality of {$mp3_field_quality} (expected: between 0-100)";
$fail = "{$datastream} datastream reports invalid VBR quality of {$mp3_field_quality} (expected: between 0-100)";
$results = islandora_assert_valid($mp3_field_quality <= 100 && $mp3_field_quality >= 0, $results, $pass, $fail);
}
}
// Otherwise, just forget everything and check the file signature.
elseif (strpos($mp3, '58696e67') == FALSE && substr($mp3, 0, 4) == 'fffa') {
$results = array(array(TRUE, "{$datastream} datastream is encoded as a valid MPEG-1 Layer 3 file with CRC protection"));
}
elseif (strpos($mp3, '58696e67') == FALSE && substr($mp3, 0, 4) == 'fffb') {
$results = array(array(TRUE, "{$datastream} datastream is encoded as a valid unprotected MPEG-1 Layer 3 file"));
}
else {
$results = array(array(FALSE, "{$datastream} datastream is corrupt and does not identify as a valid MP3."));
}
return $results;
}
/**
* Attempts to validate an .mp4 datastream.
*
* MP4 files are a subset of the ISO file format specification, and as such need
* to contain a 64-bit declaration of type within the first eight eight bytes of
* the file. This declaration is comprised of the characters 'ftyp', followed by
* a four-character filetype code. Below, we look for 'ftyp', and then pass the
* filetype code to the test message.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID of a datastream corresponding to an mp4 file.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_mp4_datastream($object, $datastream) {
$results = array();
$mp4 = $object[$datastream]->content;
if (strpos($mp4, 'ftyp')) {
$mp4_ftyp = substr(strpos($mp4, 'ftyp'), 4, 4);
}
$pass = "{$datastream} datastream asserts that it is a valid ISO-formatted video file using ftyp {$mp4_ftyp}";
$fail = "{$datastream} datastream is not a valid ISO-formatted video";
$results = islandora_assert_valid(strpos($mp4, 'ftyp'), $results, $pass, $fail);
return $results;
}
/**
* Attempts to validate an .ogg/ogv datastream using Vorbis and Theora encoding.
*
* OGG files are made up of several 'pages' of OGG data, each prefaced with an
* OGG marker - the letters 'OggS'. The file header also contains information on
* what encoders were used to create the file. Here, we're looking for at least
* one OGG page, and confirming that the file asserts the Theora and Vorbis
* codecs were used to create the file.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID of a datastream corresponding to an ogg file.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_ogg_datastream($object, $datastream) {
$results = array();
$ogg = $object[$datastream]->content;
$ogg_pages = substr_count($ogg, 'OggS');
$pass = "{$datastream} datastream asserts that it contains {$ogg_pages} Ogg pages (even a very small file should contain several).";
$fail = "{$datastream} datastream contains no Ogg pages.";
$results = islandora_assert_valid(substr_count($ogg, 'OggS'), $results, $pass, $fail);
$pass = "{$datastream} datastream asserts that it contains Theora-encoded video data.";
$fail = "{$datastream} datastream contains no marker indicating the presence of Theora-encoded video data.";
$results = islandora_assert_valid(substr_count($ogg, 'theora'), $results, $pass, $fail);
$pass = "{$datastream} datastream asserts that it contains Vorbis-encoded audio data";
$fail = "{$datastream} datastream contains no marker indicating the presence of Vorbis-encoded audio data.";
$results = islandora_assert_valid(substr_count($ogg, 'vorbis'), $results, $pass, $fail);
return $results;
}
/**
* Attempts to validate an .mkv datastream.
*
* There's not much we can do to check an MKV file, since the format is really,
* really loose. We do know a couple of things though - first, since MKV is an
* EBML format, the first four characters will always be the same. Since they're
* non-standard characters, we're looking at their hex values instead. And
* second, we know that the file will contain the declaration 'matroska' soon
* after. We could look for this in the binary, but we already have the hex-
* translated version, so we just look for 'matroska' in hex.
*
* @param AbstractObject $object
* The PID of the object.
* @param string $datastream
* A DSID of a datastream corresponding to an MKV file.
*
* @return array
* A series of TRUE(pass)/FALSE(fail) results paired with result messages.
*/
function islandora_validate_mkv_datastream($object, $datastream) {
$results = array();
$mkv = bin2hex($object[$datastream]->content);
$pass = "{$datastream} datastream asserts that it is an EBML-formatted file";
$fail = "{$datastream} datastream is not an EBML-formatted file.";
$results = islandora_assert_valid(substr($mkv, 0, 8) == '1a45dfa3', $results, $pass, $fail);
$pass = "{$datastream} datastream asserts that its EBML DocType is Matroska";
$fail = "{$datastream} datastream does not contain a Matroska EBML DocType marker.";
$results = islandora_assert_valid(substr_count($mkv, '6d6174726f736b61') == 1, $results, $pass, $fail);
return $results;
}

4
tests/hooks.test

@ -109,9 +109,11 @@ class IslandoraHooksTestCase extends IslandoraWebTestCase {
$this->repository->ingestObject($object);
$_SESSION['islandora_hooks']['hook'][ISLANDORA_OBJECT_MODIFIED_HOOK] = FALSE;
$_SESSION['islandora_hooks']['alter'][ISLANDORA_OBJECT_MODIFIED_HOOK] = FALSE;
$_SESSION['islandora_hooks']['iteration'][ISLANDORA_OBJECT_MODIFIED_HOOK] = 0;
$object->label = "New Label!";
$this->assert($_SESSION['islandora_hooks']['alter'][ISLANDORA_OBJECT_MODIFIED_HOOK], 'Called "hook_islandora_object_alter" when modifying via set magic functions.');
$this->assert($_SESSION['islandora_hooks']['hook'][ISLANDORA_OBJECT_MODIFIED_HOOK], 'Called ISLANDORA_OBJECT_MODIFIED_HOOK when modifying via set magic functions.');
$this->assertEqual('New Label! + 3', $object->label, 'Re-entered ISLANDORA_OBJECT_MODIFIED_HOOK when modifying via set magic functions.');
// Test blocking the modification.
try {
@ -191,10 +193,12 @@ class IslandoraHooksTestCase extends IslandoraWebTestCase {
$ds = $object->constructDatastream('TEST');
$object->ingestDatastream($ds);
$_SESSION['islandora_hooks']['hook'][ISLANDORA_DATASTREAM_MODIFIED_HOOK] = FALSE;
$_SESSION['islandora_hooks']['iteration'][ISLANDORA_DATASTREAM_MODIFIED_HOOK] = 0;
$_SESSION['islandora_hooks']['alter'][ISLANDORA_DATASTREAM_MODIFIED_HOOK] = FALSE;
$ds->label = "New Label!";
$this->assert($_SESSION['islandora_hooks']['alter'][ISLANDORA_DATASTREAM_MODIFIED_HOOK], 'Called "hook_islandora_datastream_alter" when modifying via set magic functions.');
$this->assert($_SESSION['islandora_hooks']['hook'][ISLANDORA_DATASTREAM_MODIFIED_HOOK], 'Called ISLANDORA_DATASTREAM_MODIFIED_HOOK when modifying via set magic functions.');
$this->assertEqual('New Label! + 3', $ds->label, 'Re-entered ISLANDORA_DATASTREAM_MODIFIED_HOOK when modifying via set magic functions.');
// Test blocking modifying.
try {

704
tests/includes/datastream_validators.inc

@ -0,0 +1,704 @@
<?php
/**
* @file
* Classes and functions for datastream validation.
*/
/**
* Converts a hexidecimal string to an integer.
*
* This is useful for running checks on values that appear in the binary
* of a datastream. Returns FALSE if the hex value contains non-hex characters
* or if the string would not return a 16- or 32-bit formatted big-endian
* signed integer.
*
* @param string $hex
* The hexidecimal string.
*
* @throws Exception
* if something horrible happens during the actual conversion.
*
* @return bool|int
* FALSE on failure, or the integer on success.
*/
function islandora_hex2int($hex) {
// A couple of quick string checks.
if (!ctype_xdigit($hex)) {
drupal_set_message(t('String passed to islandora_hex2int() contains non-hexidecimal characters.'), 'error');
return FALSE;
}
if (!(strlen($hex) == 4 || strlen($hex) == 8)) {
drupal_set_message(t('String passed to islandora_hex2int() cannot create a 16- or 32-bit little-endian signed integer'), 'error');
return FALSE;
}
// The actual conversion.
try {
$reverse_hex = implode('', array_reverse(str_split($hex, 2)));
$int = hexdec($reverse_hex);
return $int;
}
catch (Exception $e) {
throw new Exception('An error occurred during the conversion of hexidecimal to integer.', 0, $e);
}
}
/**
* Abstraction for datastream validators.
*
* Classes extended from DatastreamValidator don't require much to be useful.
* They accept a Fedora object and a DSID to perform assertions on;
* all you have to do is place a series of functions inside the extended class
* using the naming convention assertThing(); each of these functions should
* ideally assert one thing and one thing only (for simplicity's sake), and
* should generate either a pass or a fail message by calling addPass() or
* addFail(). That's it, really; they don't have to return any values, as
* addPass() and addFail() just add messages to the overall pass/fail array.
*
* As long as you use those rules and naming conventions, all the magic is done
* when you instantiate the new datastream validator object.
*
* The IslandoraWebTestCase::assertDatastreams() function accepts paired DSIDs
* and datastream validator names in order to do the rest of the work. It grabs
* all the test results using getPasses() and getFails() and transforms those
* into something that DrupalWebTestCase can use.
*/
abstract class DatastreamValidator extends IslandoraTestUtilityClass {
/**
* The Fedora object containing the datastream to test.
*
* @var IslandoraFedoraObject|FedoraObject
*/
public $object;
/**
* The DSID of the string to test.
*
* @var string
*/
public $datastream;
/**
* The content of the datastream.
*
* @var string[]
*/
public $datastreamContent;
/**
* An array of additional required parameters.
*
* @var array
*/
public $params = array();
/**
* Constructs a DatastreamValidator.
*
* @param IslandoraFedoraObject|FedoraObject $object
* The object to grab the datastream from.
* @param string $datastream
* The DSID of the datastream itself.
* @param array $params
* An extra array of parameters the validator might need.
*/
public function __construct($object, $datastream, array $params = array()) {
$this->object = $object;
$this->datastream = $datastream;
$this->params = $params;
if ($object[$datastream]) {
$this->datastreamContent = $object[$datastream]->content;
}
else {
drupal_set_message(t("Error grabbing content from datastream %datastream in object %id", array(
'%datastream' => $datastream,
'%id' => $object->id,
)), 'error');
}
}
/**
* Helper function to run all the validators in a class.
*
* On DatastreamValidator::__construct(), this looks for any functions
* within the class beginning in "assert" and runs them. In all current cases
* (and realistically in all future cases), this adds one or more passes or
* fails to $this->passes and/or $this->fails.
*/
public function runValidators() {
if ($this->object[$this->datastream]) {
$methods = get_class_methods($this);
foreach ($methods as $method) {
if (substr($method, 0, 6) === 'assert') {
$this->$method();
}
}
}
else {
$this->addResult(FALSE, "Unable to load the requested datastream {$this->datastream} from object {$this->object->id}.");
}
}
/**
* Returns an array of IslandoraTestUtilityResults.
*
* @return IslandoraTestUtilityResult[]
* The results.
*/
public function getResults() {
if (empty($this->results)) {
$this->runValidators();
}
return $this->results;
}
/**
* Adds a result to $this->results.
*
* @param bool $type
* The type of result (TRUE for pass, FALSE for fail).
* @param string $message
* The message to put in the result.
*/
public function addResult($type, $message) {
$result = new IslandoraTestUtilityResult($type, $message, $this->getAssertionCall());
$this->results[] = $result;
}
/**
* Cycles through backtrace until the first non-assertion method is found.
*
* This is a manipulated version of DrupalWebTestCase::getAssertionCall().
* We use it here so that we can pass back assertion calls from
* DatastreamValidator assertions instead of less useful TestCase functions.
*
* @return array
* Array representing the true caller.
*/
public function getAssertionCall() {
$backtrace = debug_backtrace();
// While the current caller's function starts with 'assert', and another one
// exists after this function, keep poppin' em off.
while (substr($backtrace[1]['function'], 0, 6) !== 'assert' && isset($backtrace[2])) {
array_shift($backtrace);
}
return _drupal_get_last_caller($backtrace);
}
}
/**
* Asserts that an object's given datastreams are common-type image files.
*
* Uses PHPGD to run the assertion check. This means that only certain kinds
* of image files can be checked. Please check the documentation for the PHPGD
* imagecreatefromstring() function to determine what filetypes are valid.
*/
class ImageDatastreamValidator extends DatastreamValidator {
/**
* Asserts the validity of an image using PHPGD.
*/
protected function assertImageGeneration() {
$assertion = imagecreatefromstring($this->datastreamContent) !== FALSE;
$pass = "Image datastream {$this->datastream} is valid.";
$fail = "Image datastream {$this->datastream} is either invalid or corrupt.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}
/**
* Asserts the validity of any .tif/.tiff datastream.
*/
class TIFFDatastreamValidator extends DatastreamValidator {
/**
* Asserts that the TIFF contains an appropriate header.
*/
public function assertTIFFHeaderHex() {
$datastream_header_hex = self::getTIFFHeaderHex();
if ($datastream_header_hex == "49492a00") {
// In this case, the ingested TIFF is designated as using the "Intel
// byte-order" (i.e. little-endian) by starting with the characters "II"
// (repeated so that byte order does not yet need to be significant).
// The number that follows is '42' in little-endian hex, a number of
// 'deep philosophical significance' to the TIFF format creators.'
$this->addResult(TRUE, "{$this->datastream} datastream asserts that it is a valid Intel-byte-orderded TIF/TIFF file.");
}
elseif ($datastream_header_hex == "4d4d002a") {
// In this case, the ingested TIFF is designated as using the "Motorola
// byte-order" (i.e. big-endian) by starting with the characters "MM"
// instead. 42 follows once again, this time in big-endian hex.
$this->addResult(TRUE, "{$this->datastream} datastream asserts that it is a valid Motorola-byte-ordered TIF/TIFF file.");
}
else {
$this->addResult(FALSE, "{$this->datastream} datastream does not assert that it is a valid TIF/TIFF file.");
}
}
/**
* Grabs the first 8 characters from the TIFF datastream's hex.
*
* @return string
* The ... thing I just wrote up there.
*/
protected function getTIFFHeaderHex() {
return substr(bin2hex($this->datastreamContent), 0, 8);
}
}
/**
* Asserts the validity of a JP2 datastream.
*/
class JP2DatastreamValidator extends DatastreamValidator {
/**
* Asserts the hex values at the head of the JP2 file.
*
* JP2 files begin with an offset header at the second 32-bit integer,
* 0x6A502020. This header is in all .jp2s, and we check for it here.
*/
protected function assertJP2Header() {
$assertion = substr(bin2hex($this->datastreamContent), 8, 8) == '6a502020';
$pass = "Datastream {$this->datastream} contains the appropriate JP2 header.";
$fail = "Datastream {$this->datastream} does not contain the appropriate JP2 header.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts the marker at the end of the JP2 file.
*
* JP2 files have their codestream capped with a marker, 0xFFD9. We're just
* checking for it here to see if the .jp2 encoder finished okay.
*/
protected function assertJP2Marker() {
$assertion = substr(bin2hex($this->datastreamContent), strlen(bin2hex($this->datastreamContent)) - 4, 4) == 'ffd9';
$pass = "Datastream {$this->datastream} contains the appropriate JP2 ending marker.";
$fail = "Datastream {$this->datastream} does not contain the appropriate JP2 ending marker. If this is the only JP2 validator that failed, it is likely that derivative generation was interrupted.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}
/**
* Asserts the validity of a PDF datastream.
*/
class PDFDatastreamValidator extends DatastreamValidator {
/**
* Validates the PDF signature.
*/
protected function assertPDFSignature() {
$assertion = substr($this->datastreamContent, 0, 5) == '%PDF-';
$pdf_version = substr($this->datastreamContent, 5, 3);
$pass = "{$this->datastream} datastream asserts that it is a valid PDF file using PDF version {$pdf_version}";
$fail = "{$this->datastream} datastream binary header appears to be corrupt and missing a valid PDF signature.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Counts the number of signatures in this PDF file and asserts there are any.
*/
protected function assertPDFStreamCount() {
$pdf_stream_count = substr_count(bin2hex($this->datastreamContent), '0a73747265616d0a');
$assertion = $pdf_stream_count !== 0;
$pass = "{$this->datastream} datastream reports the existence of {$pdf_stream_count} PDF streams. Note that an extremely low number could still indicate corruption.";
$fail = "{$this->datastream} datastream contains zero PDF streams, and is likely not a PDF file.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Validates the PDF closing tag.
*
* @return bool
* TRUE if it was present; FALSE otherwise.
*/
protected function assertPDFClosingTag() {
$assertion = strpos(bin2hex($this->datastreamContent), '0a2525454f460a') == TRUE;
$pass = "{$this->datastream} datastream reports the existence of the closing 'EOF' tag required at the end of PDFs";
$fail = "{$this->datastream} datastream does not contain the closing 'EOF' tag. If this is the only PDF validation that failed, it is likely that derivative generation was interrupted.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}
/**
* Validates the number of times a string occurs in a datastream.
*
* Requires $this->params to be set to an array containing two keys - the first
* is the string we're looking to find in the datastream, and the second is an
* integer representing the number of times it should appear in the datastream.
*/
class TextDatastreamValidator extends DatastreamValidator {
/**
* Constructor override; blow up if we don't have our two values.
*/
public function __construct($object, $datastream, array $params = array()) {
if (count($params) < 2) {
throw new InvalidArgumentException('$params must contain at least two values to instantiate a TextDatastreamValidator.');
}
parent::__construct($object, $datastream, $params);
}
/**
* Asserts that the string given appears the correct number of times.
*/
protected function assertTextStringCount() {
$string_count = self::getTextStringCount();
list($string, $expected) = $this->params;
$assertion = $string_count === $expected;
$this->addResult($assertion, "{$this->datastream} datastream contains the word(s) '{$string}' repeated {$string_count} time(s) (expected: {$expected}).");
}
/**
* The number of times key [0] in $this->params appears in the datastream.
*
* @return int
* That count I just mentioned up there.
*/
protected function getTextStringCount() {
return substr_count($this->datastreamContent, $this->params[0]);
}
}
/**
* Asserts the validity a WAV datastream.
*
* WAV files contain a rigidly detailed header that contains all sorts of fun
* information we can use to validate things against other things. So, we check
* rigorously that the header contains properly constructed data by looking to
* see if certain values are at their expected byte offset. We also compare
* declared chunk sizes against actual sizes. If any of these are off, WAV
* players will fail to function.
*/
class WAVDatastreamValidator extends DatastreamValidator {
/**
* We need a special constructor here to get the hex datastream content.
*
* @param IslandoraFedoraObject|FedoraObject $object
* The object to grab the datastream from.
* @param string $datastream
* The DSID of the datastream itself.
* @param array $params
* An extra array of parameters the validator might need.
*/
public function __construct($object, $datastream, array $params = array()) {
parent::__construct($object, $datastream, $params);
$this->datastreamContent = bin2hex($this->datastreamContent);
}
/**
* Asserts that the datastream contains a valid WAV signature.
*/
protected function assertWAVSignature() {
$signatures = str_split(substr($this->datastreamContent, 0, 24), 8);
$assertion = $signatures[0] == '52494646' && $signatures[2] == '57415645';
$pass = "Header of the {$this->datastream} datastream contains a valid file signature.";
$fail = "Header of the {$this->datastream} datastream contains corrupt file signature.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts that the chunksize in the header is correct.
*/
protected function assertWAVChunkSize() {
$assertion = islandora_hex2int(substr($this->datastreamContent, 8, 8)) === 36 + self::getDataSubChunkSize();
$pass = "{$this->datastream} datastream chunksize in WAV header is correct";
$fail = "{$this->datastream} datastream chunksize in WAV header does not match actual chunksize.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts that the datastream contains a 'fmt' subchunk.
*/
protected function assertWAVFmtSubChunk() {
$assertion = substr($this->datastreamContent, 24, 8) === '666d7420';
$pass = "{$this->datastream} datastream contains a 'fmt' subchunk.";
$fail = "{$this->datastream} datastream is missing the required 'fmt' subchunk.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts that the byterate reported by the WAV header is valid.
*/
protected function assertWAVByteRate() {
$wav_samplerate = islandora_hex2int(substr($this->datastreamContent, 48, 8));
$assertion = islandora_hex2int(substr($this->datastreamContent, 56, 8)) === $wav_samplerate * self::getNumChannels() * self::getBytesPerSample();
$pass = "{$this->datastream} datastream byterate in the WAV header is correct.";
$fail = "{$this->datastream} datastream byterate in the WAV header does not match actual calculated byterate.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts that the block alignment is correct.
*/
protected function assertWAVBlockAlignment() {
$assertion = islandora_hex2int(substr($this->datastreamContent, 64, 4)) === self::getNumChannels() * self::getBytesPerSample();
$pass = "{$this->datastream} datastream block alignment is set correctly.";
$fail = "{$this->datastream} datastream block alignment is off.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts the existence of a 'data' subchunk.
*
* Also asserts that the subchunk size is correct.
*/
protected function assertWAVDataSubChunk() {
if (substr($this->datastreamContent, 72, 8) !== '64617461') {
$this->addResult(FALSE, "{$this->datastream} datastream is missing the 'data' subchunk.");
return;
}
else {
$this->addResult(TRUE, "{$this->datastream} datastream contains 'data' subchunk.");
$wav_numsamples = strlen(substr($this->datastreamContent, 88)) / self::getNumChannels() / self::getBytesPerSample() / 2;
$assertion = self::getDataSubChunkSize() === $wav_numsamples * self::getNumChannels() * self::getBytesPerSample();
$pass = "{$this->datastream} datastream 'data' chunk is the correct size.";
$fail = "{$this->datastream} datastream 'data' chunk is sized incorrectly.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}
/**
* Gets the number of channels reported by the WAV header.
*
* @return int
* The number of channels reported by the datastream header.
*/
protected function getNumChannels() {
return islandora_hex2int(substr($this->datastreamContent, 44, 4));
}
/**
* Gets the reported number of byte rates per sample.
*
* @return int
* The number of bytes per sample reported by the datastream header.
*/
protected function getBytesPerSample() {
return islandora_hex2int(substr($this->datastreamContent, 68, 4)) / 8;
}
/**
* Gets the size of the 'data' subchunk.
*
* @return int
* The size of the 'data' subchunk.
*/
protected function getDataSubChunkSize() {
return islandora_hex2int(substr($this->datastreamContent, 80, 8));
}
}
/**
* Asserts the validity of any .mp3 datastream.
*
* Our default setup tries to create an MP3 using VBR, but we do some extra
* checks in case someone turns that off. If the header contains the characters
* 'Xing', it is flagged as VBR, and we can do an in-depth check on each of the
* VBR settings. Otherwise, we look for the basic MP3 signature 'fffa' or 'fffb'
* at the start of the binary.
*/
class MP3DatastreamValidator extends DatastreamValidator {
/**
* Asserts the validity of the MP3.
*
* The MP3 file format is a bit of a mess; the entire makeup of the file
* depends on whether it uses variable bit rate or static bit rate. So, I'm
* breaking my own rules here and using a single assert function so that I
* can handle the weird logic.
*/
protected function assertValidMP3() {
$this->datastreamContent = bin2hex($this->datastreamContent);
// If it's not a VBR MP3, we don't have to check much, so let's get that
// out of the way first before we go doing a bunch of potentially pointless
// math. Check to see if the VBR flag (58696e67) isn't there.
if (strpos($this->datastreamContent, '58696e67') == FALSE && substr($this->datastreamContent, 0, 4) == 'fffa') {
$this->addResult(TRUE, "{$this->datastream} datastream is encoded as a valid MPEG-1 Layer 3 file with CRC protection");
return;
}
if (strpos($this->datastreamContent, '58696e67') == FALSE && substr($this->datastreamContent, 0, 4) == 'fffb') {
$this->addResult(TRUE, "{$this->datastream} datastream is encoded as a valid unprotected MPEG-1 Layer 3 file");
return;
}
// And what if the flag IS set?
if (strpos($this->datastreamContent, '58696e67')) {
// Check the field flags. VBR-formatted MP3 files contain a 32-bit
// integer (stored as $mp3_flag_value) that is a combination of four
// bits, each one indicating the on-off status of a VBR setting, via
// logical OR. Rather than disassembling this value into individual
// bits, we use the algorithm "if (binary_total+bit_value*2)/bit_value*2
// is greater than or equal to bit_value, that bit is turned on" to find
// the status of each bit, so we know whether to offset the rest.
$mp3_field_offset = array(0, 0, 0);
$mp3_vbrheader = substr($this->datastreamContent, strpos($this->datastreamContent, '58696e67'), 240);
$mp3_flag_value = hexdec(substr($mp3_vbrheader, 8, 8));
// We can't use the first flag, but we still need to offset the rest.
if (($mp3_flag_value + 1) % 2 == 0) {
$mp3_field_offset[0] += 8;
$mp3_field_offset[1] += 8;
$mp3_field_offset[2] += 8;
}
// The second flag leads us to filesize data, which we can verify.
if (($mp3_flag_value + 4) % 4 > 1) {
$mp3_field_bytes = hexdec(substr($mp3_vbrheader, $mp3_field_offset[0] + 16, 8));
$mp3_size = strlen($this->datastreamContent) / 2;
$assertion = $mp3_size == $mp3_field_bytes;
$pass = "{$this->datastream} datastream reported filesize of {$mp3_size} bytes matches size field value of {$mp3_field_bytes}";
$fail = "{$this->datastream} datastream reported filesize of {$mp3_size} bytes does not match size field value of {$mp3_field_bytes}";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
$mp3_field_offset[1] += 8;
$mp3_field_offset[2] += 8;
}
// We can't use the third flag for anything, but we still have to offset.
if (($mp3_flag_value + 8) % 8 > 3) {
$mp3_field_offset[2] += 200;
}
// The fourth flag leads us to VBR quality data, which we can validate.
if ($mp3_flag_value > 7) {
$mp3_field_quality = hexdec(substr($mp3_vbrheader, $mp3_field_offset[2] + 16, 8));
$assertion = $mp3_field_quality <= 100 && $mp3_field_quality >= 0;
$pass = "{$this->datastream} datastream reports valid VBR quality of {$mp3_field_quality} (expected: between 0-100)";
$fail = "{$this->datastream} datastream reports invalid VBR quality of {$mp3_field_quality} (expected: between 0-100)";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}
// If none of that works out, fail.
else {
$this->addResult(FALSE, "{$this->datastream} datastream is corrupt and does not identify as a valid MP3.");
}
}
}
/**
* Attempts to validate an .mp4 datastream.
*
* MP4 files are a subset of the ISO file format specification, and as such need
* to contain a 64-bit declaration of type within the first eight eight bytes of
* the file. This declaration is comprised of the characters 'ftyp', followed by
* a four-character filetype code. Here, we look for 'ftyp', and then pass the
* filetype code to the test message.
*/
class MP4DatastreamValidator extends DatastreamValidator {
/**
* Asserts that the datastream is ISO-formatted video.
*/
protected function assertISOVideo() {
$mp4_ftyp = substr(strpos($this->datastreamContent, 'ftyp'), 4, 4);
$assertion = strpos($this->datastreamContent, 'ftyp') !== 0;
$pass = "{$this->datastream} datastream asserts that it is a valid ISO-formatted video file using ftyp {$mp4_ftyp}";
$fail = "{$this->datastream} datastream is not a valid ISO-formatted video";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}
/**
* Attempts to validate an .ogg/ogv datastream using Vorbis and Theora encoding.
*
* OGG files are made up of several 'pages' of OGG data, each prefaced with an
* OGG marker - the letters 'OggS'. The file header also contains information on
* what encoders were used to create the file. Here, we're looking for at least
* one OGG page, and confirming that the file asserts the Theora and Vorbis
* codecs were used to create the file.
*/
class OGGDatastreamValidator extends DatastreamValidator {
/**
* Asserts that the datastream contains ogg pages.
*/
protected function assertOGGPages() {
$ogg_pages = substr_count($this->datastreamContent, 'OggS');
$assertion = $ogg_pages !== 0;
$pass = "{$this->datastream} datastream asserts that it contains {$ogg_pages} Ogg pages (even a very small file should contain several).";
$fail = "{$this->datastream} datastream contains no Ogg pages.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts that the datastream contains Theora-encoded video.
*/
protected function assertTheoraVideo() {
$assertion = substr_count($this->datastreamContent, 'theora') !== 0;
$pass = "{$this->datastream} datastream asserts that it contains Theora-encoded video data.";
$fail = "{$this->datastream} datastream contains no marker indicating the presence of Theora-encoded video data.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts that the datastream contains Vorbis-encoded audio.
*/
protected function assertVorbisAudio() {
$assertion = substr_count($this->datastreamContent, 'vorbis') !== 0;
$pass = "{$this->datastream} datastream asserts that it contains Vorbis-encoded audio data";
$fail = "{$this->datastream} datastream contains no marker indicating the presence of Vorbis-encoded audio data.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}
/**
* Attempts to validate an .mkv datastream.
*
* There's not much we can do to check an MKV file, since the format is really,
* really loose. We do know a couple of things though - first, since MKV is an
* EBML format, the first four characters will always be the same. Since they're
* non-standard characters, we're looking at their hex values instead. And
* second, we know that the file will contain the declaration 'matroska' soon
* after.
*/
class MKVDatastreamValidator extends DatastreamValidator {
/**
* Asserts that the datastream is an EBML-format file.
*/
protected function assertEBMLFormat() {
$assertion = substr(bin2hex($this->datastreamContent), 0, 8) == '1a45dfa3';
$pass = "{$this->datastream} datastream asserts that it is an EBML-formatted file";
$fail = "{$this->datastream} datastream is not an EBML-formatted file.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
/**
* Asserts that the datastream contains a matroska marker.
*/
protected function assertMatroskaMarker() {
$assertion = substr_count($this->datastreamContent, 'matroska') == 1;
$pass = "{$this->datastream} datastream asserts that its EBML DocType is Matroska";
$fail = "{$this->datastream} datastream does not contain a Matroska EBML DocType marker.";
$message = $assertion ? $pass : $fail;
$this->addResult($assertion, $message);
}
}

76
tests/includes/islandora_unit_test_case.inc

@ -0,0 +1,76 @@
<?php
/**
* @file
* Islandora extensions for DrupalUnitTestCase.
*/
class IslandoraUnitTestCase extends DrupalUnitTestCase {
/**
* By default, deleteUserCreatedObjects() runs on each tearDown() step.
*
* @var bool
*/
protected $deleteObjectsOnTeardown = TRUE;
/**
* Defers to IslandoraTestUtilities for missing methods.
*
* @param string $method
* The method being called.
* @param array $args
* The arguments for that method.
*/
public function __call($method, $args) {
module_load_include('inc', 'islandora', 'tests/includes/utilities');
$params = array('db_access' => FALSE);
$utilities = new IslandoraTestUtilities($this->configuration, $params);
if (!method_exists($utilities, $method)) {
$caller = $this->getAssertionCall();
throw new BadMethodCallException("Exception: undefined method $method in {$caller['file']}, line {$caller['line']}.");
}
$result = call_user_func_array(array(&$utilities, $method), $args);
$this->parseUtilityResults($utilities);
return $result;
}
/**
* Parses utility results and passes them to the test results as an assertion.
*
* @param IslandoraTestUtilities $utility
* An instance of IslandoraTestUtilities with populated results.
*/
public function parseUtilityResults($utility) {
foreach ($utility->getResults() as $result) {
$this->assert($result->getType(), $result->getMessage(), 'Islandora', $result->getCaller());
}
}
/**
* Sets up the Drupal filter to access this test Drupal instances database.
*
* @see DrupalWebTestCase::setUp()
*/
public function setUp() {
parent::setUp();
// It's possible test are running before class autoloading.
module_load_include('inc', 'islandora', 'includes/tuque');
$this->configuration = IslandoraTestUtilityClass::getTestConfiguration();
$this->connection = new RepositoryConnection($this->configuration['fedora_url'], $this->configuration['admin_user'], $this->configuration['admin_pass']);
$api = new FedoraApi($this->connection);
$this->repository = new FedoraRepository($api, new SimpleCache());
}
/**
* Frees any allocated resources.
*
* @see DrupalWebTestCase::tearDown()
*/
public function tearDown() {
unset($this->configuration);
parent::tearDown();
}
}

332
tests/includes/islandora_web_test_case.inc

@ -0,0 +1,332 @@
<?php
/**
* @file
* Defines the class IslandoraWebTestCase, which allows tests to access Fedora.
*/
class IslandoraWebTestCase extends DrupalWebTestCase {
/**
* An array of users that may be created over the course of a test.
*
* @var array
*/
protected $users = array();
/**
* By default, deleteUserCreatedObjects() runs on each tearDown() step.
*
* @var bool
*/
protected $deleteObjectsOnTeardown = TRUE;
/**
* Defers to IslandoraTestUtilities for missing methods.
*
* @param string $method
* The method being called.
* @param array $args
* The arguments for that method.
*/
public function __call($method, $args) {
module_load_include('inc', 'islandora', 'tests/includes/utilities');
$params = array(
'logged_in_user' => $this->loggedInUser,
'db_access' => TRUE,
);
$utilities = new IslandoraTestUtilities($this->configuration, $params);
if (!method_exists($utilities, $method)) {
$caller = $this->getAssertionCall();
throw new BadMethodCallException("Exception: undefined method $method in {$caller['file']}, line {$caller['line']}.");
}
$result = call_user_func_array(array(&$utilities, $method), $args);
$this->parseUtilityResults($utilities);
return $result;
}
/**
* Parses utility results and passes them to the test results as an assertion.
*
* @param IslandoraTestUtilities $utility
* An instance of IslandoraTestUtilities with populated results.
*/
public function parseUtilityResults($utility) {
foreach ($utility->getResults() as $result) {
$this->assert($result->getType(), $result->getMessage(), 'Islandora', $result->getCaller());
}
}
/**
* Sets up the web test case.
*
* @see DrupalWebTestCase::setUp()
*/
public function setUp() {
$args = func_get_args();
$args = (isset($args[0]) && is_array($args[0])) ? $args[0] : $args;
// Always enable islandora.
$args[] = 'islandora';
parent::setUp($args);
// It's possible test are running before class autoloading.
module_load_include('inc', 'islandora', 'includes/tuque');
module_load_include('inc', 'islandora', 'includes/tuque_wrapper');
module_load_include('inc', 'islandora', 'tests/includes/utilities');
$this->configuration = IslandoraTestUtilityClass::getTestConfiguration();
if ($this->configuration['use_drupal_filter']) {
$this->setUpDrupalFilter();
}
$this->admin = $this->createAdminUser();
}
/**
* Creates the a full fedora admin user with a repository connection.
*/
protected function createAdminUser() {
$roles = user_roles();
$index = array_search('administrator', $roles);
$user = $this->drupalCreateUser();
$user->roles[$index] = 'administrator';
$user->name = $this->configuration['admin_user'];
$user->pass = $this->configuration['admin_pass'];
$user = user_save($user);
$url = variable_get('islandora_base_url', $this->configuration['fedora_url']);
$connection = islandora_get_tuque_connection($user, $url);
$user->repository = $connection->repository;
return $user;
}
/**
* Logs in the given user, handles the special case where the user is admin.
*
* @see DrupalWebTestCase::drupalLogin()
*/
protected function drupalLogin(stdClass $account) {
if ($account->uid == $this->admin->uid) {
// Create password for Drupal.
$edit = array('pass' => user_password());
$account = user_save($account, $edit);
// Raw password is used to login.
$account->pass_raw = $edit['pass'];
// We must login before changing the password for fedora.
parent::drupalLogin($account);
$account->name = $this->configuration['admin_user'];
$account->pass = $this->configuration['admin_pass'];
// Save the fedora admin credentials for later GET/POST requests.
$account = user_save($account);
}
else {
parent::drupalLogin($account);
$this->users[] = $account->name;
}
}
/**
* Restores the original Drupal filter, frees any allocated resources.
*
* To safeguard against leaving test objects in the repository, tearDown()
* calls deleteUserCreatedObjects() every time by default. This feature can be
* toggled by setting $this->deleteObjectsOnTeardown to TRUE or FALSE.
*
* @see DrupalWebTestCase::tearDown()
*/
public function tearDown() {
if ($this->deleteObjectsOnTeardown) {
foreach ($this->users as $user) {
$this->deleteUserCreatedObjects($user);
}
}
if ($this->configuration['use_drupal_filter']) {
islandora_repair_drupal_filter();
}
unset($this->admin);
unset($this->configuration);
parent::tearDown();
}
/**
* Gets a tuque object from a path.
*
* @param string $path
* A full or partial path to an islandora object.
*
* @return AbstractObject
* The pid of the object or FALSE if a PID is not found.
*/
public function getObjectFromPath($path) {
$path_parts = explode('/', $path);
$array_length = count($path_parts);
for ($i = 0; $i < $array_length; $i++) {
if ($path_parts[$i] == 'islandora' && isset($path_parts[$i + 1]) && $path_parts[$i + 1] == 'object') {
if (isset($path_parts[$i + 2])) {
return islandora_object_load(urldecode($path_parts[$i + 2]));
}
}
}
$this->fail("Failed to parse path: $path.");
return FALSE;
}
/**
* Deletes an object using the PID. This does the deletion using the UI.
*
* @param string $pid
* The PID of the collection to be deleted
* @param string $button
* The label of the first 'Delete' button
* @param bool $safety
* If TRUE, this will only delete objects owned by users in $this->users.
*/
public function deleteObject($pid, $button = NULL, $safety = TRUE) {
$object = islandora_object_load($pid);
if (!$safety || in_array($object->owner, $this->users)) {
$path = "islandora/object/$pid/manage/properties";
if (isset($button)) {
$this->drupalPost($path, array(), $button);
}
else {
$object = islandora_object_load($pid);
$this->drupalPost($path, array(), "Permanently remove '{$object->label}' from repository");
}
$this->drupalPost($this->url, array(), t('Delete'));
$this->drupalGet("islandora/object/$pid");
$this->assertResponse(404, "Object $pid successfully deleted.");
}
else {
$this->fail("Cannot delete object {$pid}; it is owned by non-test user {$object->owner}, and this function was called with the safety on.");
return FALSE;
}
}
/**
* These are a few quick helper functions to fill in a gap in the standard
* DrupalWebTestCase where no function exists to test for the simple existence
* or non-existence of an error.
*/
/**
* Asserts that an error is found in $this->content.
*
* @param string $message
* The message to pass on to the results.
* @param string $group
* The group to place the result in.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function assertError($message = '', $group = 'Other') {
if (!$message) {
$message = "Error found on current page when error was expected.";
}
return $this->assertFieldByXPath('//div[contains(@class, "message") and contains(@class, "error")]', NULL, $message, $group);
}
/**
* Asserts that no error is found in $this->content.
*
* @param string $message
* The message to pass on to the results.
* @param string $group
* The group to place the result in.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function assertNoError($message = '', $group = 'Other') {
if (!$message) {
$message = "No error found on current page when no error was expected.";
}
return $this->assertNoFieldByXPath('//div[contains(@class, "message") and contains(@class, "error")]', NULL, $message, $group);
}
/**
* Asserts that a warning is found in $this->content.
*
* @param string $message
* The message to pass on to the results.
* @param string $group
* The group to place the result in.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function assertWarning($message = '', $group = 'Other') {
if (!$message) {
$message = "Warning found on current page when warning was expected.";
}
return $this->assertFieldByXPath('//div[contains(@class, "message") and contains(@class, "warning")]', NULL, $message, $group);
}
/**
* Asserts that no warning is found in $this->content.
*
* @param string $message
* The message to pass on to the results.
* @param string $group
* The group to place the result in.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function assertNoWarning($message = '', $group = 'Other') {
if (!$message) {
$message = "No warning found on current page when no warning was expected.";
}
return $this->assertNoFieldByXPath('//div[contains(@class, "message") and contains(@class, "warning")]', NULL, $message, $group);
}
/**
* Makes a drupalPost() request, using the form submit button's ID.
*
* Because drupalPost() is silly and doesn't let us choose which button we'd
* like to select if multiple buttons have the same value, this function
* allows us to select whatever button we'd like based on the ID instead.
*
* This is done via the absolutely hilarious method of fudging the actual
* button labels that don't have the ID we want, so that the only one left
* with the correct label is the one with the right ID.
*
* @see DrupalWebTestCase::drupalPost()
*
* @param string $path
* Location of the post form.
* @param array $edit
* Field data in an associative array.
* @param string $submit
* Value of the submit button whose click is to be emulated.
* @param string $id
* ID of the submit button whose click is to be emulated.
* @param array $options
* Options to be forwarded to url().
* @param array $headers
* An array containing additional HTTP request headers, each formatted as
* "name: value".
* @param null $form_html_id
* (optional) HTML ID of the form to be submitted.
* @param null $extra_post
* (optional) A string of additional data to append to the POST submission.
*
* @return bool|string
* The content from the POST request's curlExec, or FALSE on fail.
*/
public function drupalPostByID($path, $edit, $submit, $id, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) {
$buttons = $this->xpath("//input[@type=\"submit\" and @value=\"{$submit}\"]");
if (empty($buttons)) {
$this->fail("No buttons found on the page with value '$submit'");
return FALSE;
}
$i = 0;
foreach ($buttons as $button) {
if ($button['id'] !== $id) {
$button['value'] .= "_$i";
$i++;
}
}
return $this->drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post);
}
}

153
tests/includes/test_utility_abstraction.inc

@ -0,0 +1,153 @@
<?php
/**
* @file
* Abstraction n' stuff for the test utilities and results.
*/
/**
* A result from a utility method; $type defines TRUE/FALSE as pass/fail.
*/
class IslandoraTestUtilityResult {
/**
* The message for this result.
*
* @var string
*/
protected $message;
/**
* The caller for this result.
*
* @var array
*/
protected $caller;
/**
* The type of result this is - TRUE for pass, FALSE for fail.
*
* @var bool
*/
protected $type;
/**
* Constructs an IslandoraTestUtilityResult.
*
* @param bool $type
* Whether this result should indicate a pass (TRUE) or fail (FALSE).
* @param string $message
* The message that will be used by this result.
* @param array $caller
* The caller for this result.
*/
public function __construct($type, $message, array $caller) {
$this->message = $message;
$this->caller = $caller;
$this->type = $type;
}
/**
* Get the message for this result.
*
* @return string
* The message for this result.
*/
public function getMessage() {
return $this->message;
}
/**
* Get the caller for this result.
*
* @return string
* The caller for this result.
*/
public function getCaller() {
return $this->caller;
}
/**
* Get the type of result.
*
* @return bool
* The type of pass (TRUE for pass, FALSE for fail).
*/
public function getType() {
return $this->type;
}
}
/**
* Abstraction for test utility classes.
*
* This is to be implemented in any class that wants to have test utility
* functionality (i.e. that wants to pass back results to tests). Check out the
* datastream validator class for a good example of how this is implemented.
*
* Test utility classes should store per-installation configuration options in
* a test_config.ini file, contained in the islandora/tests folder. A function
* is included with the abstraction to parse the configuration file.
*/
abstract class IslandoraTestUtilityClass {
/**
* An array of IslandoraTestUtilityResults.
*
* These should be generated using $this->addResult.
*
* @var IslandoraTestUtilityResult[]
*/
public $results = array();
/**
* Parses and returns the settings from the test configuration file.
*
* If no install specific test_config.ini file is found, it will use the
* assumed default configs found in default.test_config.ini.
*
* @return array
* The test configuration.
*
* @see parse_ini_file()
*/
public static function getTestConfiguration() {
$path = drupal_get_path('module', 'islandora');
if (file_exists("$path/tests/test_config.ini")) {
return parse_ini_file("$path/tests/test_config.ini");
}
elseif (file_exists("$path/tests/default.test_config.ini")) {
return parse_ini_file("$path/tests/default.test_config.ini");
}
throw new Exception('Required default.test_config.ini/test_config.ini file not found');
}
/**
* Returns an array of IslandoraTestUtilityResults.
*
* The particular testing implementation you are using should use this to
* parse results from utilities and pass them through.
*
* @return IslandoraTestUtilityResult[]
* The results.
*/
abstract public function getResults();
/**
* Adds a result to $this->results.
*
* @param bool $type
* The type of result (TRUE for pass, FALSE for fail).
* @param string $message
* The message to put in the result.
*/
abstract public function addResult($type, $message);
/**
* Gets the caller of the method that passed a result.
*
* @return array
* Array representing the true caller.
*/
abstract public function getAssertionCall();
}

354
tests/includes/utilities.inc

@ -0,0 +1,354 @@
<?php
/**
* @file
* Utilities classes for simpletest.
*
* These utilities are shared between the Islandora Web and Unit test cases and
* can potentially be used for future testing implementations, as they pass back
* a consistent style of result.
*
* Check the implementations in IslandoraUnitTestCase and IslandoraWebTestCase
* for some examples of how this is done in a Drupal context; in the future,
* more methods for result-passing could potentially be added to this without
* breaking existing implementations.
*/
class IslandoraTestUtilities extends IslandoraTestUtilityClass {
protected $configuration;
protected $params;
public $results = array();
protected $repository;
/**
* Constructs an IslandoraTestUtilities object.
*
* @param array $configuration
* The parsed test configuration.
* @param array $params
* Any additional parameters the method called may need to function.
*/
public function __construct($configuration, array $params = array()) {
$this->configuration = $configuration;
$this->params = $params;
$connection = new RepositoryConnection($this->configuration['fedora_url'], $this->configuration['admin_user'], $this->configuration['admin_pass']);
$api = new FedoraApi($connection);
$this->repository = new FedoraRepository($api, new SimpleCache());
}
/**
* Sets up a drupal filter that can read from the tests users table.
*/
public function setUpDrupalFilter() {
$original_drupal_filter_content = file_get_contents($this->configuration['drupal_filter_file']);
$connection_info = Database::getConnectionInfo('default');
$drupal_filter_dom = new DomDocument();
$drupal_filter_dom->loadXML($original_drupal_filter_content);
$prefix = $connection_info['default']['prefix']['default'];
$filter_drupal_connection_node = $drupal_filter_dom->getElementsByTagName('FilterDrupal_Connection')->item(0);
$first_connection_node = $drupal_filter_dom->getElementsByTagName('connection')->item(0);
$connection_node = $filter_drupal_connection_node->insertBefore($drupal_filter_dom->createElement('connection'), $first_connection_node);
$connection_node->setAttribute('server', $connection_info['default']['host']);
$connection_node->setAttribute('dbname', $connection_info['default']['database']);
$connection_node->setAttribute('user', $connection_info['default']['username']);
$connection_node->setAttribute('password', $connection_info['default']['password']);
$connection_node->setAttribute('port', $connection_info['default']['port'] ? $connection_info['default']['port'] : '3306');
$sql_node = $drupal_filter_dom->createElement('sql', "SELECT DISTINCT u.uid AS userid, u.name AS Name, u.pass AS Pass, r.name AS Role FROM ({$prefix}users u LEFT JOIN {$prefix}users_roles ON u.uid={$prefix}users_roles.uid) LEFT JOIN {$prefix}role r ON r.rid={$prefix}users_roles.rid WHERE u.name=? AND u.pass=?;");
$connection_node->appendChild($sql_node);
file_put_contents($this->configuration['drupal_filter_file'], $drupal_filter_dom->saveXML());
}
/**
* Returns an array of IslandoraTestUtilityResults.
*
* @return IslandoraTestUtilityResult[]
* The results.
*/
public function getResults() {
return $this->results;
}
/**
* Adds a result to $this->results.
*
* @param bool $type
* The type of result (TRUE for pass, FALSE for fail).
* @param string $message
* The message to put in the result.
*/
public function addResult($type, $message) {
$result = new IslandoraTestUtilityResult($type, $message, $this->getAssertionCall());
$this->results[] = $result;
}
/**
* Cycles through backtrace until the first non-assertion method is found.
*
* This is a manipulated version of DrupalWebTestCase::getAssertionCall().
* We use it here so that we can pass back assertion calls from
* DatastreamValidator assertions instead of less useful TestCase functions.
*
* @return array
* Array representing the true caller.
*/
public function getAssertionCall() {
$backtrace = debug_backtrace();
array_shift($backtrace);
return _drupal_get_last_caller($backtrace);
}
/**
* Asserts that the given datastreams exist correctly on the object.
*
* @param AbstractObject $object
* The object to check.
* @param array $datastreams
* An array of strings containing datastream names.
*
* @return bool
* TRUE on success, FALSE on fail.
*/
public function assertDatastreams($object, array $datastreams) {
if (!self::assertFedoraObject($object)) {
$this->addResult(FALSE, "Failed. Object passed in is invalid.", 'Islandora');
}
else {
$missing_datastreams = array_diff_key(array_flip($datastreams), $object->repository->api->a->listDatastreams($object->id));
if (!empty($missing_datastreams)) {
$this->addResult(FALSE, "Failed to find datastream(s) " . implode(', ', array_flip($missing_datastreams)) . " in object {$object->id}.");
return FALSE;
}
$this->addResult(TRUE, "Found required datastream(s) in object {$object->id}");
return TRUE;
}
}
/**
* Asserts that the given datastreams do not exist on the object.
*
* @param AbstractObject $object
* The object to check.
* @param array $datastreams
* An array of datastreams to confirm not present.
*/
public function assertNoDatastreams($object, array $datastreams) {
if (!self::assertFedoraObject($object)) {
$this->addResult(FALSE, "Failed. Object passed in is invalid.", 'Islandora');
return;
}
$found_datastreams = array_intersect_key(array_flip($datastreams), $object->repository->api->a->listDatastreams($object->id));
if (!empty($found_datastreams)) {
$this->addResult(FALSE, "Found unwanted datastream(s)" . implode(', ', array_flip($found_datastreams)) . " in object {$object->id}.");
return FALSE;
}
$this->addResult(TRUE, "Unwanted datastream(s) not found in object {$object->id}");
return TRUE;
}
/**
* Attempts to validate an array of datastreams, generally via binary checks.
*
* Datastream validation classes exist in, and can be added to, the file
* 'datastream_validators.inc', which is found in this folder. Datastream
* validator classes use the naming convention 'PrefixDatastreamValidator',
* and that 'Prefix' is what this function uses to determine what class to
* instantiate.
*
* $param IslandoraFedoraObject $object
* The object to load datastreams from.
* $param array $datastreams
* An array of arrays that pair DSIDs, DatastreamValidator class prefixes,
* and optional params. You can check some of the existing implementations
* for examples.
*/
public function validateDatastreams($object, array $datastreams) {
if (!self::assertFedoraObject($object)) {
$this->addResult(FALSE, "Datastream validation failed; Object passed in is invalid.", 'Islandora');
return;
}
module_load_include('inc', 'islandora', 'tests/includes/datastream_validators');
foreach ($datastreams as $datastream) {
// XXX: The "+ array (2 => array())" is to allow the value to be optional.
list($dsid, $prefix, $params) = $datastream + array(2 => array());
// Instantiate the appropriate class, and grab the results.
$class_name = "{$prefix}DatastreamValidator";
if (class_exists($class_name)) {
$validator = new $class_name($object, $dsid, $params);
foreach ($validator->getResults() as $result) {
$this->addResult($result->getType(), $result->getMessage());
}
}
else {
$this->addResult(FALSE, "No DatastreamValidator class was found with the name '$class_name'; are you sure the prefix given to IslandoraWebTestCase->validateDatastreams() was entered correctly, or that such a validator exists?", 'Islandora');
}
}
}
/**
* Constructs and ingests a Fedora object and datastream(s) via tuque.
*
* All keys inside the parameter arrays for this function are optional. it
* can be run simply by calling the method with no arguments.
*
* If your test case supports logged in Drupal users, IslandoraTestUtilities
* can be instantiated with $params['logged_in_user'] as that user object, and
* this method will set the owner of the ingested object as that user by
* default.
*
* @param array $properties
* An array containing object information using these keys:
* 'label' - The object label; randomized if not set.
* 'pid' - 'namespace:pid', or just 'namespace' to generate the suffix.
* 'models' - An array that can contain multiple content model PIDs, or a
* string containing a single content model PID.
* 'owner' - The object's owner. Defaults to the currently logged-in user,
* if available. It is recommended to set this to a value that can be found
* in $this->users; otherwise, this object will have to be manually deleted.
* 'parent' - The PID of the parent collection.
* @param array $datastreams
* An array containing zero or more datastream arrays that use the keys:
* 'dsid' - the datastream ID; randomized if not set.
* 'path' - The path to the file to use; defaults to fixtures/test.jpg.
* 'control_group' - The single-letter control group identifier.
* 'mimetype' - The datastream's mimetype.
*
* @return bool|array
* FALSE if the object ingest failed, or the object array if successful.
*/
public function ingestConstructedObject(array $properties = array(), array $datastreams = array()) {
if (!isset($properties['pid'])) {
$properties['pid'] = "islandora";
}
$object = $this->repository->constructObject($properties['pid']);
// Set the object properties before ingesting it.
if (isset($properties['label'])) {
$object->label = $properties['label'];
}
else {
$properties['label'] = DrupalUnitTestCase::randomName();
$object->label = $properties['label'];
}
if (isset($properties['owner'])) {
$object->owner = $properties['owner'];
}
elseif (isset($this->params['logged_in_user'])) {
$object->owner = $this->params['logged_in_user']->name;
}
if (isset($properties['models'])) {
try {
$object->models = (array) $properties['models'];
}
catch (Exception $e) {
$this->addResult(FALSE, "Encountered an exception when trying to add content models to {$object->id}: $e");
return FALSE;
}
}
// Chuck in some datastreams.
if (!empty($datastreams)) {
foreach ($datastreams as $datastream) {
if (!isset($datastream['dsid'])) {
$datastream['dsid'] = DrupalUnitTestCase::randomName();
}
if (!isset($datastream['path'])) {
$datastream['path'] = drupal_get_path('module', 'islandora') . '/tests/fixtures/test.jpg';
}
if (!isset($datastream['control_group'])) {
$new_datastream = $object->constructDatastream($datastream['dsid']);
}
else {
$new_datastream = $object->constructDatastream($datastream['dsid'], $datastream['control_group']);
}
$new_datastream->label = $datastream['dsid'];
if (isset($datastream['mimetype'])) {
$new_datastream->mimetype = $datastream['mimetype'];
}
$new_datastream->setContentFromFile($datastream['path']);
$object->ingestDatastream($new_datastream);
}
}
$this->repository->ingestObject($object);
if (!$object) {
$this->addResult(FALSE, t("Failed to ingest object."), 'Islandora');
return FALSE;
}
else {
$this->addResult(TRUE, t("Ingested object %object", array('%object' => $object->id)), 'Islandora');
}
// Add a parent relationship, if necessary.
if (isset($properties['parent'])) {
$object->relationships->add(FEDORA_RELS_EXT_URI, 'isMemberOfCollection', $properties['parent']);
}
return $object;
}
/**
* Deletes all objects created by the given user.
*
* To safeguard against leaving test objects in the repository, this is called
* each time DrupalTestCase::run() calls tearDown(). This feature can be
* toggled by setting $this->deleteObjectsOnTeardown to TRUE or FALSE.
*
* @param object $username
* The user whose objects we'd like to remove.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function deleteUserCreatedObjects($username) {
if ($username === $this->configuration['admin_user']) {
$this->addResult(FALSE, "This function will under no circumstance attempt deletion of all objects owned by the configured Fedora admin user ({$this->configuration['admin_user']}), as this could irreparably damage the repository.", 'Islandora');
return FALSE;
}
$query = <<<QUERY
SELECT ?object FROM <#ri> WHERE
{
?object <fedora-model:ownerId> "$username"
}
QUERY;
$objects = $this->repository->ri->sparqlQuery($query);
foreach ($objects as $object) {
$loaded_object = islandora_object_load($object['object']['value']);
$this->repository->api->m->purgeObject($loaded_object->id);
if (islandora_object_load($object['object']['value'])) {
$this->addResult(TRUE, "Object {$object['object']['value']} successfully removed from repository.");
return TRUE;
}
$this->addResult(FALSE, "Unable to remove object {$object['object']['value']} from the repository.");
return FALSE;
}
}
/**
* Asserts that an object is a FedoraObject or an IslandoraFedoraObject.
*
* @param object $object
* The object to assess.
*
* @return bool
* TRUE if it is either of those object types, or FALSE otherwise.
*/
public static function assertFedoraObject($object) {
return ($object instanceof FedoraObject);
}
}

2
tests/islandora_hooked_access_test.module

@ -23,7 +23,7 @@ function islandora_hooked_access_test_islandora_object_access($op, $object, $use
* Implements hook_islandora_datastream_access().
*/
function islandora_hooked_access_test_islandora_datastream_access($op, $datastream, $user) {
if ($op == FEDORA_PURGE) {
if ($op == ISLANDORA_PURGE) {
return FALSE;
}
if (isset($_SESSION['islandora_hooked_access_test']) && $_SESSION['islandora_hooked_access_test'] === func_get_args()) {

8
tests/islandora_hooks_test.module

@ -112,6 +112,10 @@ function islandora_hooks_test_islandora_object_ingested(AbstractObject $object)
function islandora_hooks_test_islandora_object_modified(AbstractObject $object) {
if ($object->id == 'test:testModifiedObjectHook') {
$_SESSION['islandora_hooks']['hook'][ISLANDORA_OBJECT_MODIFIED_HOOK] = TRUE;
if ($_SESSION['islandora_hooks']['iteration'][ISLANDORA_OBJECT_MODIFIED_HOOK]++ < 3) {
$new_label = 'New Label! + ' . $_SESSION['islandora_hooks']['iteration'][ISLANDORA_OBJECT_MODIFIED_HOOK];
$object->label = $new_label;
}
}
}
@ -139,6 +143,10 @@ function islandora_hooks_test_islandora_datastream_ingested(AbstractObject $obje
function islandora_hooks_test_islandora_datastream_modified(AbstractObject $object, AbstractDatastream $datastream) {
if ($object->id == 'test:testModifiedDatastreamHook' && $datastream->id == "TEST") {
$_SESSION['islandora_hooks']['hook'][ISLANDORA_DATASTREAM_MODIFIED_HOOK] = TRUE;
if ($_SESSION['islandora_hooks']['iteration'][ISLANDORA_DATASTREAM_MODIFIED_HOOK]++ < 3) {
$new_label = 'New Label! + ' . $_SESSION['islandora_hooks']['iteration'][ISLANDORA_DATASTREAM_MODIFIED_HOOK];
$datastream->label = $new_label;
}
}
}

83
tests/islandora_manage_temp_file.test

@ -0,0 +1,83 @@
<?php
/**
* @file
* Tests for our islandora_temp_file_entry() function.
*/
class IslandoraManageTempfileTestCase extends IslandoraWebTestCase {
/**
* Gets info to display to describe this test.
*
* @see IslandoraWebTestCase::getInfo()
*/
public static function getInfo() {
return array(
'name' => 'Islandora Managed Tempfile Interface',
'description' => 'Ensure that our managed tempfile interface returns appropriate results.',
'group' => 'Islandora',
);
}
/**
* Creates an admin user and a connection to a fedora repository.
*
* @see IslandoraWebTestCase::setUp()
*/
public function setUp() {
parent::setUp('islandora');
$this->tempUri = file_create_filename('temp.txt', 'temporary://');
$this->publicUri = file_create_filename('temp.txt', 'public://');
}
/**
* Free any objects/resources created for this test.
*
* @see IslandoraWebTestCase::tearDown()
*/
public function tearDown() {
parent::tearDown();
file_unmanaged_delete($this->tempUri);
file_unmanaged_delete($this->publicUri);
}
/**
* Existing files are made temporary.
*/
public function testExistingFile() {
$temp_file = file_save_data('blah', $this->tempUri, FILE_EXISTS_REPLACE);
$public_file = file_save_data('blah', $this->publicUri, FILE_EXISTS_REPLACE);
$this->existingFileHelper($temp_file);
$this->existingFileHelper($public_file);
}
/**
* Helper function; ensure file is permanent (as file_save_data() creates).
*/
protected function existingFileHelper($file_object) {
$this->assertEqual($file_object->status & FILE_STATUS_PERMANENT, FILE_STATUS_PERMANENT, 'Existing file is permanent.');
$this->baseFileHelper($file_object->uri);
$this->assertTrue(file_delete($file_object));
}
/**
* Helper function; ensure our function produces an temporary file object.
*/
protected function baseFileHelper($file_uri) {
$temp_file = islandora_temp_file_entry($file_uri);
$this->assertNotEqual($temp_file->status & FILE_STATUS_PERMANENT, FILE_STATUS_PERMANENT, 'File has been made temporary.');
}
/**
* Unmanaged files start being managed.
*/
public function testNewFileUri() {
file_put_contents($this->tempUri, 'test');
file_put_contents($this->publicUri, 'test');
$this->baseFileHelper($this->tempUri);
$this->baseFileHelper($this->publicUri);
}
}

445
tests/islandora_web_test_case.inc

@ -1,445 +0,0 @@
<?php
/**
* @file
* Defines the class IslandoraWebTestCase, which allows tests to access Fedora.
*/
class IslandoraWebTestCase extends DrupalWebTestCase {
/**
* An array of users that may be created over the course of a test.
*
* @var array
*/
protected $users = array();
/**
* By default, deleteUserCreatedObjects() runs on each tearDown() step.
*/
protected $deleteObjectsOnTeardown = TRUE;
/**
* Sets up the Drupal filter to access this test Drupal instances database.
*
* @see DrupalWebTestCase::setUp()
*/
public function setUp() {
$args = func_get_args();
$args = (isset($args[0]) && is_array($args[0])) ? $args[0] : $args;
// Always enable islandora.
$args[] = 'islandora';
parent::setUp($args);
// Its possible test are running before class autoloading.
module_load_include('inc', 'islandora', 'includes/tuque');
module_load_include('inc', 'islandora', 'includes/tuque_wrapper');
$this->configuration = $this->getTestConfiguration();
if ($this->configuration['use_drupal_filter']) {
$this->backUpDrupalFilter();
$this->setUpDrupalFilter();
}
$this->admin = $this->createAdminUser();
}
/**
* Parses and returns the settings from the test configuration file.
*
* If no install specific test_config.ini file is found, it will use the
* assumed default configs found in default.test_config.ini.
*
* @return array
* The test configuration.
*
* @see parse_ini_file()
*/
protected function getTestConfiguration() {
$path = drupal_get_path('module', 'islandora');
if (file_exists("$path/tests/test_config.ini")) {
$this->pass('Using custom test configuration.');
return parse_ini_file("$path/tests/test_config.ini");
}
elseif (file_exists("$path/tests/default.test_config.ini")) {
$this->pass('Using default test configuration.');
return parse_ini_file("$path/tests/default.test_config.ini");
}
throw new Exception('Required default.test_config.ini/test_config.ini file not found');
}
/**
* Stores the content of the Drupal Filter for later restoration.
*/
protected function backUpDrupalFilter() {
if (file_exists($this->configuration['drupal_filter_file'])) {
$this->originalDrupalFilterContent = file_get_contents($this->configuration['drupal_filter_file']);
}
else {
throw new Exception('Failed to find the required Drupal Filter configuration file.');
}
}
/**
* Sets up a drupal filter that can read for the tests users table.
*/
protected function setUpDrupalFilter() {
$connection_info = Database::getConnectionInfo('default');
$drupal_filter_dom = new DomDocument();
$drupal_filter_dom->loadXML($this->originalDrupalFilterContent);
$server = $connection_info['default']['host'];
$dbname = $connection_info['default']['database'];
$user = $connection_info['default']['username'];
$password = $connection_info['default']['password'];
$port = $connection_info['default']['port'] ? $connection_info['default']['port'] : '3306';
$prefix = $connection_info['default']['prefix']['default'];
$filter_drupal_connection_node = $drupal_filter_dom->getElementsByTagName('FilterDrupal_Connection')->item(0);
$first_connection_node = $drupal_filter_dom->getElementsByTagName('connection')->item(0);
$connection_node = $filter_drupal_connection_node->insertBefore($drupal_filter_dom->createElement('connection'), $first_connection_node);
$connection_node->setAttributeNode(new DOMAttr('server', $server));
$connection_node->setAttributeNode(new DOMAttr('dbname', $dbname));
$connection_node->setAttributeNode(new DOMAttr('user', $user));
$connection_node->setAttributeNode(new DOMAttr('password', $password));
$connection_node->setAttributeNode(new DOMAttr('port', $port));
$sql_node = $connection_node->appendChild(new DOMElement('sql'));
$sql_node->appendChild($drupal_filter_dom->createTextNode("SELECT DISTINCT u.uid AS userid, u.name AS Name, u.pass AS Pass, r.name AS Role FROM ({$prefix}users u LEFT JOIN {$prefix}users_roles ON u.uid={$prefix}users_roles.uid) LEFT JOIN {$prefix}role r ON r.rid={$prefix}users_roles.rid WHERE u.name=? AND u.pass=?;"));
file_put_contents($this->configuration['drupal_filter_file'], $drupal_filter_dom->saveXML());
}
/**
* Creates the a full fedora admin user with a repository connection.
*/
protected function createAdminUser() {
$roles = user_roles();
$index = array_search('administrator', $roles);
$user = $this->drupalCreateUser();
$user->roles[$index] = 'administrator';
$user->name = $this->configuration['admin_user'];
$user->pass = $this->configuration['admin_pass'];
$user = user_save($user);
$url = variable_get('islandora_base_url', $this->configuration['fedora_url']);
$connection = islandora_get_tuque_connection($user, $url);
$user->repository = $connection->repository;
return $user;
}
/**
* Logs in the given user, handles the special case where the user is admin.
*
* @see DrupalWebTestCase::drupalLogin()
*/
protected function drupalLogin(stdClass $account) {
if ($account->uid == $this->admin->uid) {
// Create password for Drupal.
$edit = array('pass' => user_password());
$account = user_save($account, $edit);
// Raw password is used to login.
$account->pass_raw = $edit['pass'];
// We must login before changing the password for fedora.
parent::drupalLogin($account);
$account->name = $this->configuration['admin_user'];
$account->pass = $this->configuration['admin_pass'];
// Save the fedora admin credentials for later GET/POST requests.
$account = user_save($account);
}
else {
parent::drupalLogin($account);
$this->users[] = $account->name;
}
}
/**
* Stores the content of the Drupal Filter for later restoration.
*/
protected function restoreDrupalFilter() {
$file = $this->configuration['drupal_filter_file'];
if (isset($this->originalDrupalFilterContent)) {
file_put_contents($file, $this->originalDrupalFilterContent);
}
elseif (file_exists($file)) {
// Remove if there was never an original.
drupal_unlink($file);
}
}
/**
* Restores the original Drupal filter, frees any allocated resources.
*
* To safeguard against leaving test objects in the repository, tearDown()
* calls deleteUserCreatedObjects() every time by default. This feature can be
* toggled by setting $this->deleteObjectsOnTeardown to TRUE or FALSE.
*
* @see DrupalWebTestCase::tearDown()
*/
public function tearDown() {
if ($this->deleteObjectsOnTeardown) {
foreach ($this->users as $user) {
$this->deleteUserCreatedObjects($user);
}
}
if ($this->configuration['use_drupal_filter']) {
$this->restoreDrupalFilter();
}
unset($this->admin);
unset($this->configuration);
parent::tearDown();
}
/**
* Asserts that the given datastreams exist correctly on the object.
*
* @param AbstractObject $object
* The PID of the object
* @param array $datastreams
* An array of strings containing datastream names
*
* @return bool
* TRUE on success, FALSE on fail.
*/
public function assertDatastreams($object, array $datastreams) {
if (!is_object($object)) {
$this->fail("Failed. Object passed in is invalid.", 'Islandora');
}
else {
$missing_datastreams = array_diff_key(array_flip($datastreams), $this->admin->repository->api->a->listDatastreams($object->id));
if (!empty($missing_datastreams)) {
$this->fail("Failed to find datastream(s) " . implode(', ', array_flip($missing_datastreams)) . " in object {$object->id}.");
return FALSE;
}
$this->pass("Found required datastream(s) in object {$object->id}");
return TRUE;
}
}
/**
* Attempts to validate an array of datastreams, generally via binary checks.
*
* These functions exist in, and can be added to, datastream_validators.inc,
* which is found in this folder.
*
* $param AbstractObject $object
* The object to load datastreams from.
* $param array $datastreams
* An array of paired DSIDs, validate function names, and optional params.
*/
public function validateDatastreams($object, array $datastreams) {
if (!is_object($object)) {
$this->fail("Failed. Object passed in is invalid.", 'Islandora');
}
module_load_include('inc', 'islandora', 'tests/datastream_validators');
foreach ($datastreams as $datastream) {
if (isset($object[$datastream[0]])) {
$function = 'islandora_validate_' . $datastream[1] . '_datastream';
if (function_exists($function)) {
if (isset($datastream[2])) {
$results = $function($object, $datastream[0], $datastream[2]);
}
else {
$results = $function($object, $datastream[0]);
}
foreach ($results as $result) {
$this->assertTrue($result[0], $result[1], 'Islandora');
}
}
else {
$this->fail("No {$datastream[0]} validation function exists for the {$datastream[1]} datastream.", 'Islandora');
}
}
}
}
/**
* Gets a tuque object from a path.
*
* @param string $path
* A full or partial path to an islandora object.
*
* @return AbstractObject
* The pid of the object or FALSE if a PID is not found.
*/
public function getObjectFromPath($path) {
$path_parts = explode('/', $path);
$array_length = count($path_parts);
for ($i = 0; $i < $array_length; $i++) {
if ($path_parts[$i] == 'islandora' && isset($path_parts[$i + 1]) && $path_parts[$i + 1] == 'object') {
if (isset($path_parts[$i + 2])) {
return islandora_object_load(urldecode($path_parts[$i + 2]));
}
}
}
$this->fail("Failed to parse path: $path.");
return FALSE;
}
/**
* Deletes an object using the PID. This does the deletion using the UI.
*
* @param string $pid
* The PID of the collection to be deleted
* @param string $button
* The label of the first 'Delete' button
* @param bool $safety
* If TRUE, this will only delete objects owned by users in $this->users.
*/
public function deleteObject($pid, $button = NULL, $safety = TRUE) {
$object = islandora_object_load($pid);
if (!$safety || in_array($object->owner, $this->users)) {
$path = "islandora/object/$pid/manage/properties";
if (isset($button)) {
$this->drupalPost($path, array(), $button);
}
else {
$object = islandora_object_load($pid);
$this->drupalPost($path, array(), "Permanently remove '{$object->label}' from repository");
}
$this->drupalPost($this->url, array(), t('Delete'));
$this->drupalGet("islandora/object/$pid");
$this->assertResponse(404, "Object $pid successfully deleted.");
}
else {
$this->fail("Cannot delete object {$pid}; it is owned by non-test user {$object->owner}, and this function was called with the safety on.");
return FALSE;
}
}
/**
* Constructs and ingests a Fedora object and datastream(s) via tuque.
*
* All keys inside the parameter arrays for this function are optional. it
* can be run simply by calling $this->ingestConstructedObject();.
*
* @param array $properties
* An array containing object information using these keys:
* 'label' - The object label; randomized if not set.
* 'pid' - 'namespace:pid', or just 'namespace' to generate the suffix.
* 'models' - An array that can contain multiple content model PIDs.
* 'owner' - The object's owner. Defaults to the currently logged-in user,
* if available. It is recommended to set this to a value that can be found
* in $this->users; otherwise, this object will have to be manually deleted.
* 'parent' - The PID of the parent collection.
* @param array $datastreams
* An array containing zero or more datastream arrays that use the keys:
* 'dsid' - the datastream ID; randomized if not set.
* 'path' - The path to the file to use; defaults to fixtures/test.jpg.
* 'control_group' - The single-letter control group identifier.
* 'mimetype' - The datastream's mimetype.
*
* @return bool|array
* FALSE if the object ingest failed, or the object array if successful.
*/
public function ingestConstructedObject(array $properties = array(), array $datastreams = array()) {
module_load_include('inc', 'islandora', 'includes/tuque');
$tuque = new IslandoraTuque();
$repository = $tuque->repository;
if (!isset($properties['pid'])) {
$properties['pid'] = "islandora";
}
$object = $repository->constructObject($properties['pid']);
// Set the object properties before ingesting it.
if (isset($properties['label'])) {
$object->label = $properties['label'];
}
else {
$properties['label'] = $this->randomName(16);
$object->label = $properties['label'];
}
if (isset($properties['owner'])) {
$object->owner = $properties['owner'];
}
elseif ($this->loggedInUser !== FALSE) {
$object->owner = $this->loggedInUser->name;
}
if (isset($properties['models']) && is_array($properties['models'])) {
foreach ($properties['models'] as $model) {
$object->relationships->add(FEDORA_MODEL_URI, 'hasModel', $model);
}
}
elseif (isset($properties['models']) && !is_array($properties['models'])) {
$this->fail(t("'models' key of properties variable is not an array. Content model(s) will not be set."), 'Islandora');
}
$repository->ingestObject($object);
if (!$object) {
$this->fail(t("Failed to ingest object."), 'Islandora');
return FALSE;
}
else {
$this->pass(t("Ingested object %object", array('%object' => $object->id)), 'Islandora');
}
// Chuck in some datastreams.
if (!empty($datastreams)) {
foreach ($datastreams as $datastream) {
if (!isset($datastream['dsid'])) {
$datastream['dsid'] = $this->randomName(8);
}
if (!isset($datastream['path'])) {
$datastream['path'] = drupal_get_path('module', 'islandora') . '/tests/fixtures/test.jpg';
}
if (!isset($datastream['control_group'])) {
$new_datastream = $object->constructDatastream($datastream['dsid']);
}
else {
$new_datastream = $object->constructDatastream($datastream['dsid'], $datastream['control_group']);
}
$new_datastream->label = $datastream['dsid'];
if (isset($datastream['mimetype'])) {
$new_datastream->mimetype = $datastream['mimetype'];
}
$new_datastream->setContentFromFile($datastream['path']);
$object->ingestDatastream($new_datastream);
}
}
// Add a parent relationship, if necessary.
if (isset($properties['parent'])) {
$object->relationships->add(FEDORA_RELS_EXT_URI, 'isMemberOfCollection', $properties['parent']);
}
return $object;
}
/**
* Deletes all objects created by the given user.
*
* To safeguard against leaving test objects in the repository, this is called
* each time DrupalTestCase::run() calls tearDown(). This feature can be
* toggled by setting $this->deleteObjectsOnTeardown to TRUE or FALSE.
*
* @param object $username
* The user whose objects we'd like to remove.
*
* @return bool
* TRUE on success, FALSE on failure.
*/
public function deleteUserCreatedObjects($username) {
if ($username === $this->configuration['admin_user']) {
$this->fail("This function will under no circumstance attempt deletion of all objects owned by the configured Fedora admin user ({$this->configuration['admin_user']}), as this could irreparably damage the repository.", 'Islandora');
return FALSE;
}
$query = <<<QUERY
SELECT ?object FROM <#ri> WHERE
{
?object <fedora-model:ownerId> "$username"
}
QUERY;
$objects = $this->admin->repository->ri->sparqlQuery($query);
foreach ($objects as $object) {
$loaded_object = islandora_object_load($object['object']['value']);
islandora_delete_object($loaded_object);
if ($this->assertFalse(islandora_object_load($object['object']['value']), "Object {$object['object']['value']} successfully removed from repository.", 'Islandora')) {
return FALSE;
}
return TRUE;
}
}
}

22
tests/scripts/travis_after_failure.sh

@ -0,0 +1,22 @@
#!/bin/bash
# Get the end portion of the TRAVIS_REPO_SLUG for the branch name.
IFS=/ read -a DELIMITED_SLUG <<< "$TRAVIS_REPO_SLUG"
export CURRENT_REPO=${DELIMITED_SLUG[1]}
git config user.name --global islandora-logger
git config user.email --global noreply@islandora.ca
# Git business
export VERBOSE_DIR = $HOME/sites/default/files/simpletest/verbose
cd $HOME
git clone https://islandora-logger:$LOGGER_PW@github.com/Islandora/islandora_travis_logs.git
cd islandora_travis_logs
git checkout -B $CURRENT_REPO
# Out with the old, in with the new
git rm $HOME/islandora_travis_logs/*.*
cp $VERBOSE_DIR/*.* $HOME/islandora_travis_logs
git add -A
git commit -m "Job: $TRAVIS_JOB_NUMBER Commit: $TRAVIS_COMMIT"
git push origin $CURRENT_REPO

24
tests/scripts/travis_setup.sh

@ -8,25 +8,31 @@ cd $HOME
git clone git://github.com/Islandora/tuque.git
git clone -b $FEDORA_VERSION git://github.com/Islandora/islandora_tomcat.git
cd islandora_tomcat
git fsck
export CATALINA_HOME='.'
export JAVA_OPTS="-Xms1024m -Xmx1024m -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -Djavax.net.ssl.trustStore=$CATALINA_HOME/fedora/server/truststore -Djavax.net.ssl.trustStorePassword=tomcat"
./bin/startup.sh
cd $HOME
pear channel-discover pear.drush.org
pear upgrade --force Console_Getopt
pear upgrade --force pear
pear upgrade-all
pear channel-discover pear.drush.org
pear channel-discover pear.drush.org
pear channel-discover pear.phpqatools.org
pear channel-discover pear.netpirates.net
pear install pear/PHP_CodeSniffer-1.4.8
pear install pear.phpunit.de/phpcpd
pear install drush/drush-5.9.0
# "prefer-source" required due to SSL shenanigans on the Travis boxes...
composer global require --prefer-source 'squizlabs/php_codesniffer=*' 'sebastian/phpcpd=*' 'drush/drush:6.3.0'
# Because we can't add to the PATH here and this file is used in many repos,
# let's just throw symlinks in.
find $HOME/.composer/vendor/bin -executable \! -type d -exec sudo ln -s {} /usr/local/sbin/ \;
phpenv rehash
drush dl --yes drupal
cd drupal-*
drush si minimal --db-url=mysql://drupal:drupal@localhost/drupal --yes
drush runserver --php-cgi=$HOME/.phpenv/shims/php-cgi localhost:8081 &>/dev/null &
# Needs to make things from Composer be available (PHP CS, primarily)
sudo chmod a+w sites/default/settings.php
echo "include_once '$HOME/.composer/vendor/autoload.php';" >> sites/default/settings.php
sudo chmod a-w sites/default/settings.php
drush runserver --php-cgi=$HOME/.phpenv/shims/php-cgi localhost:8081 &>/tmp/drush_webserver.log &
ln -s $ISLANDORA_DIR sites/all/modules/islandora
mv sites/all/modules/islandora/tests/travis.test_config.ini sites/all/modules/islandora/tests/test_config.ini
mkdir sites/all/libraries
@ -38,4 +44,6 @@ drush en --yes simpletest
drush en --yes potx
drush en --user=1 --yes islandora
drush cc all
# The shebang in this file is a bogeyman that is haunting the web test cases.
rm /home/travis/.phpenv/rbenv.d/exec/hhvm-switcher.bash
sleep 20

4
theme/islandora-dublin-core-display.tpl.php

@ -17,10 +17,10 @@
<fieldset <?php $print ? print('class="islandora islandora-metadata"') : print('class="islandora islandora-metadata collapsible collapsed"');?>>
<legend><span class="fieldset-legend"><?php print t('Details'); ?></span></legend>
<div class="fieldset-wrapper">
<dl xmlns:dcterms="http://purl.org/dc/terms/" class="islandora-inline-metadata islandora-metadata-fields">
<dl xmlns:dcterms="http://purl.org/dc/terms/" class="islandora-inline-metadata islandora-metadata-fields islandora-object-fields">
<?php $row_field = 0; ?>
<?php foreach($dc_array as $key => $value): ?>
<dt property="<?php print $value['dcterms']; ?>" content="<?php print filter_xss($value['value']); ?>" class="<?php print $value['class']; ?><?php print $row_field == 0 ? ' first' : ''; ?>">
<dt property="<?php print $value['dcterms']; ?>" content="<?php print filter_xss(htmlspecialchars($value['value'], ENT_QUOTES, 'UTF-8')); ?>" class="<?php print $value['class']; ?><?php print $row_field == 0 ? ' first' : ''; ?>">
<?php print filter_xss($value['label']); ?>
</dt>
<dd class="<?php print $value['class']; ?><?php print $row_field == 0 ? ' first' : ''; ?>">

48
theme/islandora-object.tpl.php

@ -60,34 +60,18 @@
?>
<div class="islandora-object islandora">
<h2><?php print t('Details'); ?></h2>
<dl class="islandora-object-tn">
<dt>
<?php if (isset($variables['islandora_thumbnail_url'])): ?>
<img src="<?php print $variables['islandora_thumbnail_url']; ?>"/></dt>
<?php endif; ?>
<dd></dd>
</dl>
<dl class="islandora-inline-metadata islandora-object-fields">
<?php $row_field = 0; ?>
<?php foreach ($dc_array as $key => $value): ?>
<dt class="<?php print $value['class']; ?><?php print $row_field == 0 ? ' first' : ''; ?>">
<?php print $value['label']; ?>
</dt>
<dd class="<?php print $value['class']; ?><?php print $row_field == 0 ? ' first' : ''; ?>">
<?php print $value['value']; ?>
</dd>
<?php $row_field++; ?>
<?php endforeach; ?>
<?php if ($parent_collections): ?>
<dt>Collections</dt>
<dd>
<?php foreach ($parent_collections as $collection): ?>
<div><?php print l($collection->label, "islandora/object/{$collection->id}"); ?></div>
<?php endforeach; ?>
</dd>
<?php endif; ?>
<?php if (isset($variables['islandora_thumbnail_url'])): ?>
<dl class="islandora-object-tn">
<dt>
<img src="<?php print $variables['islandora_thumbnail_url']; ?>"/>
</dt>
</dl>
<?php endif; ?>
<div class="islandora-default-metadata">
<?php print $description; ?>
<?php print $metadata; ?>
</div>
</div>
<fieldset class="collapsible collapsed" style="display: block; clear:both">
<legend><span class="fieldset-legend"><?php print t('File details'); ?></span></legend>
@ -112,3 +96,13 @@
</table>
</div>
</fieldset>
<?php if ($parent_collections): ?>
<div>
<h2><?php print t('In collections'); ?></h2>
<ul>
<?php foreach ($parent_collections as $collection): ?>
<li><?php print l($collection->label, "islandora/object/{$collection->id}"); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>

39
theme/theme.inc

@ -25,7 +25,7 @@ function islandora_preprocess_islandora_default_edit(array &$variables) {
$header[] = array('data' => t('Versions'));
}
$header[] = array('data' => t('Operations'), 'colspan' => '4');
$header[] = array('data' => t('Operations'), 'colspan' => '5');
$table_attributes = array('class' => array('manage-datastreams'));
$rows = array();
@ -60,6 +60,13 @@ function islandora_preprocess_islandora_default_edit(array &$variables) {
'datastream' => $ds,
)),
);
// Add new datastream content as the lastest version.
$row[] = array(
'class' => 'datastream-replace',
'data' => theme('islandora_datastream_replace_link', array(
'datastream' => $ds,
)),
);
}
$row[] = array(
'class' => 'datastream-download',
@ -109,6 +116,7 @@ function islandora_preprocess_islandora_default(&$variables) {
$islandora_object = $variables['islandora_object'];
module_load_include('inc', 'islandora', 'includes/utilities');
module_load_include('inc', 'islandora', 'includes/datastream');
module_load_include('inc', 'islandora', 'includes/metadata');
$variables['parent_collections'] = islandora_get_parents_from_rels_ext($islandora_object);
@ -140,8 +148,13 @@ function islandora_preprocess_islandora_default(&$variables) {
$dc_object = DublinCore::importFromXMLString($islandora_object['DC']->content);
$dc_array = $dc_object->asArray();
}
// We should eventually remove the DC object and dc_array code as it only
// exists to not break legacy implementations.
$variables['dc_array'] = isset($dc_array) ? $dc_array : array();
$variables['islandora_dublin_core'] = isset($dc_object) ? $dc_object : NULL;
$variables['metadata'] = islandora_retrieve_metadata_markup($islandora_object, TRUE);
$variables['description'] = islandora_retrieve_description_markup($islandora_object);
$variables['islandora_object_label'] = $islandora_object->label;
if (isset($islandora_object['TN']) && islandora_datastream_access(ISLANDORA_VIEW_OBJECTS, $islandora_object['TN'])) {
$variables['islandora_thumbnail_url'] = url("islandora/object/{$islandora_object->id}/datastream/TN/view");
@ -190,7 +203,6 @@ function theme_islandora_object_print(array &$variables) {
* Implements hook_preprocess_theme().
*/
function islandora_preprocess_islandora_objects(array &$variables) {
module_load_include('inc', 'islandora_paged_content', 'includes/utilities');
$display = (empty($_GET['display'])) ? 'grid' : $_GET['display'];
$grid_display = $display == 'grid';
$list_display = !$grid_display;
@ -263,7 +275,7 @@ function islandora_preprocess_islandora_objects(array &$variables) {
*/
function theme_islandora_datastream_download_link(array $vars) {
$datastream = $vars['datastream'];
module_load_include('inc', 'islandora', 'includes/utilities');
module_load_include('inc', 'islandora', 'includes/datastream');
$label = t('download');
return islandora_datastream_access(ISLANDORA_VIEW_OBJECTS, $datastream) ?
@ -430,6 +442,27 @@ function theme_islandora_datastream_version_link(array $vars) {
}
}
/**
* Renders a link to take you to the datastream add latest version page.
*
* @param array $vars
* An array containing:
* - datastream: An AbstractDatastream to generate a new version.
*
* @return string
* Markup.
*/
function theme_islandora_datastream_replace_link(array $vars) {
$datastream = $vars['datastream'];
if (islandora_datastream_access(ISLANDORA_REPLACE_DATASTREAM_CONTENT, $datastream)) {
$var_string = variable_get("islandora_ds_replace_exclude_enforced", "RELS-EXT,RELS-INT");
$replace_exclude = explode(",", $var_string);
if (!in_array($datastream->id, $replace_exclude)) {
return l(t('replace'), "islandora/object/{$datastream->parent->id}/datastream/{$datastream->id}/replace");
}
}
}
/**
* Renders a link that will re-create derivatives for a datastream.
*

Loading…
Cancel
Save