Browse Source

Merge branch '7.x' of github.com:islandora/islandora into 7.x-ISLANDORA-1042

pull/545/head
nruest 10 years ago
parent
commit
6e64814200
  1. 1
      .travis.yml
  2. 62
      includes/add_datastream.form.inc
  3. 13
      includes/admin.form.inc
  4. 65
      includes/breadcrumb.inc
  5. 7
      includes/datastream.inc
  6. 26
      includes/datastream.version.inc
  7. 39
      includes/derivatives.inc
  8. 10
      includes/ingest.form.inc
  9. 3
      includes/mime_detect.inc
  10. 88
      includes/mimetype.utils.inc
  11. 1
      includes/object.entity_controller.inc
  12. 2
      includes/object_properties.form.inc
  13. 2
      includes/regenerate_derivatives.form.inc
  14. 50
      includes/tuque_wrapper.inc
  15. 16
      includes/utilities.inc
  16. 7
      islandora.info
  17. 1
      islandora.install
  18. 174
      islandora.module
  19. 443
      islandora.rules.inc
  20. 77
      js/spinner.js
  21. 9
      tests/datastream_validator_tests.test
  22. 123
      tests/includes/datastream_validators.inc
  23. 76
      tests/includes/islandora_unit_test_case.inc
  24. 415
      tests/includes/islandora_web_test_case.inc
  25. 153
      tests/includes/test_utility_abstraction.inc
  26. 358
      tests/includes/utilities.inc
  27. 604
      tests/islandora_web_test_case.inc
  28. 30
      tests/scripts/travis_setup.sh

1
.travis.yml

@ -2,6 +2,7 @@ language: php
php:
- 5.3.3
- 5.4
- 5.5
branches:
only:
- /^7.x/

62
includes/add_datastream.form.inc

@ -27,8 +27,8 @@ function islandora_add_datastream_form(array $form, array &$form_state, Abstract
// @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']);
$datastream_requirements = islandora_get_missing_datastreams_requirements($object);
$unused_datastreams = array_keys($datastream_requirements);
$unused_datastreams = "'" . implode("', '", $unused_datastreams) . "'";
$upload_size = min((int) ini_get('post_max_size'), (int) ini_get('upload_max_filesize'));
return array(
@ -38,7 +38,7 @@ function islandora_add_datastream_form(array $form, array &$form_state, Abstract
),
'dsid_fieldset' => array(
'#type' => 'fieldset',
'#title' => 'Add a datastream',
'#title' => t('Add a Datastream'),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'dsid' => array(
@ -60,7 +60,7 @@ function islandora_add_datastream_form(array $form, array &$form_state, Abstract
'#required' => TRUE,
'#size' => 64,
'#maxlength' => 64,
'#description' => t('A human-readable label'),
'#description' => t('A human-readable label.'),
'#type' => 'textfield',
'#element_validate' => array('islandora_add_datastream_form_field_does_not_contain_a_forward_slash'),
),
@ -69,10 +69,11 @@ function islandora_add_datastream_form(array $form, array &$form_state, Abstract
'#required' => TRUE,
'#title' => t('Upload Document'),
'#size' => 48,
'#description' => t('Select a file to upload.<br/>Files must be less than <b>@size MB.</b>', array('@size' => $upload_size)),
'#description' => t('Select a file to upload.<br/>Files must be less than <strong>@size MB.</strong>', array('@size' => $upload_size)),
'#default_value' => isset($form_state['values']['files']) ? $form_state['values']['files'] : NULL,
'#upload_location' => 'temporary://',
'#upload_location' => file_default_scheme() . '://',
'#upload_validators' => array(
// Disable default file_validate_extensions; we need direct control.
'file_validate_extensions' => array(NULL),
// Assume its specified in MB.
'file_validate_size' => array($upload_size * 1024 * 1024),
@ -136,6 +137,30 @@ function islandora_add_datastream_form_field_is_valid_dsid(array $element, array
}
}
/**
* Validation callback for islandora_add_datastream_form.
*
* Checks if the given datastream can accept the given MIME type.
*/
function islandora_add_datastream_form_validate(array $form, array &$form_state) {
module_load_include('inc', 'islandora', 'includes/mimetype.utils');
$extensions = islandora_get_extensions_for_datastream(
$form_state['object'],
$form_state['values']['dsid']
);
$file = file_load($form_state['values']['file']);
// Only validate extensions if mimes defined in ds-composite.
if ($file && $extensions) {
$errors = file_validate_extensions($file, implode(' ', $extensions));
if (count($errors) > 0) {
form_set_error(
'file',
t("!error (for the set DSID)", array('!error' => $errors[0]))
);
}
}
}
/**
* Checks if the given form field contains a "/" character.
*
@ -152,31 +177,6 @@ function islandora_add_datastream_form_field_does_not_contain_a_forward_slash(ar
}
}
/**
* Checks if the given datastream requires the upload to be a certain MIME type.
*
* @param array $form
* The Drupal form.
* @param array $form_state
* The Drupal form state.
*/
function islandora_add_datastream_form_validate(array $form, array &$form_state) {
$file = file_load($form_state['values']['file']);
$dsid = $form_state['values']['dsid'];
if (isset($form_state['datastream_requirements'][$dsid]) && $file) {
$allowed_types = $form_state['datastream_requirements'][$dsid]['mime'];
$mime_detect = new MimeDetect();
$allowed_extensions = array();
foreach ($allowed_types as $mime) {
$allowed_extensions = array_merge($allowed_extensions, $mime_detect->getValidExtensions($mime));
}
$errors = file_validate_extensions($file, implode(' ', $allowed_extensions));
if (count($errors) > 0) {
form_set_error('file', $errors[0]);
}
}
}
/**
* Adds the new datastream based on the submitted values.
*

13
includes/admin.form.inc

@ -61,6 +61,19 @@ function islandora_repository_admin(array $form, array &$form_state) {
'#description' => t('HTTP caching can reduce network traffic, by allowing clients to used cached copies.'),
'#default_value' => variable_get('islandora_use_datastream_cache_headers', TRUE),
),
'islandora_defer_derivatives_on_ingest' => array(
'#type' => 'checkbox',
'#title' => t('Defer derivative generation during ingest'),
'#description' => t('Prevent derivatives from running during ingest,
useful if derivatives are to be created by an external service.'),
'#default_value' => variable_get('islandora_defer_derivatives_on_ingest', FALSE),
),
'islandora_show_print_option' => array(
'#type' => 'checkbox',
'#title' => t('Show print option on objects'),
'#description' => t('Displays an extra print tab, allowing an object to be printed'),
'#default_value' => variable_get('islandora_show_print_option', FALSE),
),
),
'islandora_namespace' => array(
'#type' => 'fieldset',

65
includes/breadcrumb.inc

@ -77,37 +77,50 @@ function islandora_get_breadcrumbs_recursive($pid, FedoraRepository $repository,
);
}
else {
$query_string = 'select $parentObject $title $content from <#ri>
where (
<info:fedora/' . $pid . '> <fedora-model:label> $title
and $parentObject <fedora-model:hasModel> $content
and (
<info:fedora/' . $pid . '> <fedora-rels-ext:isMemberOfCollection> $parentObject
or <info:fedora/' . $pid . '> <fedora-rels-ext:isMemberOf> $parentObject
or <info:fedora/' . $pid . '> <fedora-rels-ext:isPartOf> $parentObject
)
and $parentObject <fedora-model:state> <info:fedora/fedora-system:def/model#Active>
)
minus $content <mulgara:is> <info:fedora/fedora-system:FedoraObject-3.0>
minus $parentObject <mulgara:is> <info:fedora/' . $pid . '>
order by $title desc';
$results = $repository->ri->itqlQuery($query_string);
if (count($results) > 0 && $context['level'] > 0) {
$parent = $results[0]['parentObject']['value'];
$this_title = $results[0]['title']['value'];
$query = <<<EOQ
PREFIX islandora-rels-ext: <http://islandora.ca/ontology/relsext#>
PREFIX fedora-rels-ext: <info:fedora/fedora-system:def/relations-external#>
SELECT DISTINCT ?object ?label
FROM <#ri>
WHERE {
<info:fedora/!pid> ?collection_predicate ?object;
<fedora-model:label> ?label .
?object <fedora-model:state> <fedora-model:Active>
.
!optionals
!filters
}
EOQ;
if (empty($this_title)) {
$this_title = t('-');
$query_optionals = (array) module_invoke('islandora_xacml_api', 'islandora_basic_collection_get_query_optionals', 'view');
$query_filters = (array) module_invoke('islandora_xacml_api', 'islandora_basic_collection_get_query_filters');
$filter_map = function ($filter) {
return "FILTER($filter)";
};
$query_filters[] = "sameTerm(?collection_predicate, <fedora-rels-ext:isMemberOfCollection>) || sameTerm(?collection_predicate, <fedora-rels-ext:isMemberOf>)";
$query = format_string($query, array(
'!optionals' => !empty($query_optionals) ? ('OPTIONAL {{' . implode('} UNION {', $query_optionals) . '}}') : '',
'!filters' => implode(' ', array_map($filter_map, $query_filters)),
));
$query = format_string($query, array(
'!pid' => $pid,
));
$results = $repository->ri->sparqlQuery($query, 'unlimited');
if (count($results) > 0 && $context['level'] > 0) {
$parent = $results[0]['object']['value'];
$this_label = $results[0]['label']['value'];
if (empty($this_label)) {
$this_label = t('-');
}
$context['level']--;
return array_merge(
islandora_get_breadcrumbs_recursive($parent, $repository, $context),
array(
l($this_title, "islandora/object/$pid"),
)
);
return array_merge(islandora_get_breadcrumbs_recursive($parent, $repository, $context), array(
l($this_label, "islandora/object/$pid"),
));
}
else {
// Add an non-link, as we don't know how to get back to the root, and

7
includes/datastream.inc

@ -30,6 +30,7 @@ function islandora_download_datastream(AbstractDatastream $datastream) {
* The version of the datastream to display
*/
function islandora_view_datastream(AbstractDatastream $datastream, $download = FALSE, $version = NULL) {
module_load_include('inc', 'islandora', 'includes/mimetype.utils');
// XXX: Certain features of the Devel module rely on the use of "shutdown
// handlers", such as query logging... The problem is that they might blindly
// add additional output which will break things if what is actually being
@ -45,16 +46,13 @@ function islandora_view_datastream(AbstractDatastream $datastream, $download = F
return drupal_not_found();
}
}
header('Content-type: ' . $datastream->mimetype);
if ($datastream->controlGroup == 'M' || $datastream->controlGroup == 'X') {
header('Content-length: ' . $datastream->size);
}
if ($download) {
// Browsers will not append all extensions.
$mime_detect = new MimeDetect();
$extension = '.' . $mime_detect->getExtension($datastream->mimetype);
$extension = '.' . islandora_get_extension_for_mimetype($datastream->mimetype);
// Prevent adding on a duplicate extension.
$label = $datastream->label;
$extension_length = strlen($extension);
@ -65,7 +63,6 @@ function islandora_view_datastream(AbstractDatastream $datastream, $download = F
if ($duplicate_extension_position === FALSE) {
$filename .= $extension;
}
header("Content-Disposition: attachment; filename=\"$filename\"");
}

26
includes/datastream.version.inc

@ -275,35 +275,29 @@ function islandora_get_object_extensions(AbstractObject $object) {
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');
module_load_include('inc', 'islandora', 'includes/mimetype.utils');
$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);
}
}
$extensions = islandora_get_extensions_for_datastream($object, $datastream->id);
$valid_extensions = implode(' ', $extensions);
$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"),
'#title' => t("Update Datastream"),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'dsid' => array(
'#type' => 'markup',
'#markup' => "<div>DSID: <b>$datastream->id</b></div>",
'#markup' => t("<div>DSID: <strong>@dsid</strong></div>", array('@dsid' => $datastream->id)),
),
'label' => array(
'#type' => 'markup',
'#markup' => "<div>Label: <b>$datastream->label</b></div>",
'#markup' => t("<div>Label: <strong>@label</strong></div>", array('@label' => $datastream->label)),
),
'file' => array(
'#type' => 'managed_file',
@ -311,9 +305,9 @@ function islandora_datastream_version_replace_form($form, &$form_state, Abstract
'#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_location' => file_default_scheme() . '://',
'#upload_validators' => array(
'file_validate_extensions' => $ext,
'file_validate_extensions' => array($valid_extensions),
// Assume its specified in MB.
'file_validate_size' => array($upload_size * 1024 * 1024),
),
@ -342,7 +336,9 @@ function islandora_datastream_version_replace_form_submit($form, &$form_state) {
$file = file_load($form_state['values']['file']);
try {
$ds = $object[$form_state['dsid']];
$ds->mimetype = $file->filemime;
if ($ds->mimetype != $file->filemime) {
$ds->mimetype = $file->filemime;
}
$path = drupal_realpath($file->uri);
$ds->setContentFromFile($path);
file_delete($file);

39
includes/derivatives.inc

@ -4,6 +4,9 @@
* Defines functions used when constructing derivatives.
*/
// Relations.
define('ISLANDORA_DEFER_DERIVATIVES_FLAG', 'deferDerivatives');
/**
* Decides which derivative function to call and runs it.
*
@ -245,3 +248,39 @@ function islandora_filter_derivatives($hooks, $options, AbstractObject $object)
$hooks = array_filter($hooks, $filter_function);
return $hooks;
}
/**
* Set the defer derivatives flag on an object.
*/
function islandora_set_defer_derivatives_flag(AbstractObject $object) {
$object->relationships->add(
ISLANDORA_RELS_EXT_URI,
ISLANDORA_DEFER_DERIVATIVES_FLAG,
'true',
RELS_TYPE_PLAIN_LITERAL
);
}
/**
* Get the defer derivatives flag on an object.
*/
function islandora_get_defer_derivatives_flag(AbstractObject $object) {
return $object->relationships->get(
ISLANDORA_RELS_EXT_URI,
ISLANDORA_DEFER_DERIVATIVES_FLAG,
'true',
RELS_TYPE_PLAIN_LITERAL
);
}
/**
* Remove the defer derivatives flag on an object.
*/
function islandora_remove_defer_derivatives_flag(AbstractObject $object) {
$object->relationships->remove(
ISLANDORA_RELS_EXT_URI,
ISLANDORA_DEFER_DERIVATIVES_FLAG,
'true',
RELS_TYPE_PLAIN_LITERAL
);
}

10
includes/ingest.form.inc

@ -469,6 +469,14 @@ function islandora_ingest_form_stepify(array $form, array &$form_state, array $s
);
$form['prev'] = $first_form_step ? NULL : islandora_ingest_form_previous_button($form_state);
$form['next'] = $last_form_step ? islandora_ingest_form_ingest_button($form_state) : islandora_ingest_form_next_button($form_state);
// Duplicate next button and hide it at the top of the form so that the form
// always triggers "next" if the user submits the form with the enter key.
if (!is_null($form['prev'])) {
$form['hidden_next'] = $form['next'];
$form['hidden_next']['#weight'] = -99;
$form['hidden_next']['#prefix'] = '<div style="display:none;">';
$form['hidden_next']['#suffix'] = '</div>';
}
// Allow for a hook_form_FORM_ID_alter().
drupal_alter(array('form_' . $step['form_id'], 'form'), $form, $form_state, $step['form_id']);
return $form;
@ -769,7 +777,7 @@ function islandora_ingest_form_submit(array $form, array &$form_state) {
WATCHDOG_ERROR
);
drupal_set_message(
t('A problem occured while ingesting "@label" (ID: @pid), please notifiy the administrator.',
t('A problem occured while ingesting "@label" (ID: @pid), please notify the administrator.',
array('@label' => $object->label, '@pid' => $object->id)),
'error'
);

3
includes/mime_detect.inc

@ -134,7 +134,6 @@ class MimeDetect {
'xhtml' => 'application/xhtml+xml',
'xsl' => 'text/xsl',
'xslt' => 'text/xsl',
'xml' => 'text/xml',
'csv' => 'text/csv',
'tsv' => 'text/tab-separated-values',
'txt' => 'text/plain',
@ -223,10 +222,12 @@ class MimeDetect {
"tar" => "application/x-tar",
"gtar" => "application/x-gtar",
"zip" => "application/x-zip",
"dat" => "application/octet-stream",
// others:
'bin' => 'application/octet-stream',
// Web Archives:
"warc" => "application/warc",
"json" => "application/json",
);
protected $protectedFileExtensions;
protected $extensionExceptions = array(

88
includes/mimetype.utils.inc

@ -0,0 +1,88 @@
<?php
/**
* @file
* Mimetype specific utility functions.
*/
/**
* Retrieve the correct file extension for a give mimetype.
*
* @param string $mimetype
* The mimetype whose extension is required.
*
* @return string
* The extension mapped to the given mimetype. Defaults to 'bin'.
*/
function islandora_get_extension_for_mimetype($mimetype) {
// file.mimetypes.inc is a part of Drupal core, however is not
// automatically loaded. Manually require it.
require_once DRUPAL_ROOT . "/includes/file.mimetypes.inc";
$extension = 'bin';
$mimetype_mapping = file_mimetype_mapping();
$extension_index = array_search($mimetype, $mimetype_mapping['mimetypes']);
if ($extension_index !== FALSE) {
$mime_array_flipped = array_reverse($mimetype_mapping['extensions']);
$extension = array_search($extension_index, $mime_array_flipped);
}
// We can only have one mapping in drupal for 'xml'.
if ($mimetype == "text/xml") {
return "xml";
}
return $extension;
}
/**
* Retrieve all file extensions for a give mimetype.
*
* @param string $mimetype
* The mimetype whose extensions are required.
*
* @return array
* All known legal extensions.
*/
function islandora_get_extensions_for_mimetype($mimetype) {
// file.mimetypes.inc is a part of Drupal core, however is not
// automatically loaded. Manually require it.
require_once DRUPAL_ROOT . "/includes/file.mimetypes.inc";
$mimetype_mapping = file_mimetype_mapping();
$index = array_search($mimetype, $mimetype_mapping['mimetypes']);
$extensions = array();
if ($index !== FALSE) {
$extensions = array_keys($mimetype_mapping['extensions'], $index);
}
// We can only have one mapping in drupal for 'xml'.
if ($mimetype == "text/xml") {
$extensions[] = 'xml';
}
return $extensions;
}
/**
* Extensions for the mimes accepted by a datastream.
*
* @param AbstractObject $object
* Object to check for extensions.
* @param string $dsid
* Datastream ID to check for extensions.
*
* @return string[]
* Extensions for the mimes accepted by the datastream ID on the object.
*/
function islandora_get_extensions_for_datastream(AbstractObject $object, $dsid) {
module_load_include('inc', 'islandora', 'includes/utilities');
$datastream_mime_map = islandora_get_datastreams_requirements_from_models($object->models);
$mimes = isset($datastream_mime_map[$dsid]) ? $datastream_mime_map[$dsid]['mime'] : array();
if (isset($object[$dsid])) {
$current_mime = $object[$dsid]->mimetype;
if (!in_array($current_mime, $mimes)) {
$mimes[] = $current_mime;
}
}
$extensions = array();
foreach ($mimes as $mime) {
$extensions = array_merge($extensions, islandora_get_extensions_for_mimetype($mime));
}
return array_unique($extensions);
}

1
includes/object.entity_controller.inc

@ -30,6 +30,7 @@ class IslandoraObjectEntityController implements DrupalEntityControllerInterface
*/
public function load($ids = array(), $conditions = array()) {
if (!empty($conditions)) {
// TODO: Allow loading by specifying IDs in the condition.
throw new Exception('Conditions not implemented.');
}

2
includes/object_properties.form.inc

@ -31,7 +31,7 @@ function islandora_object_properties_form(array $form, array &$form_state, Abstr
module_load_include('inc', 'islandora', 'includes/derivatives');
$hooks = islandora_invoke_hook_list(ISLANDORA_DERVIATIVE_CREATION_HOOK, $object->models, array($object));
$hooks = islandora_filter_derivatives($hooks, array('force' => TRUE), $object);
if (count($hooks) > 1) {
if (count($hooks) >= 1) {
$regenerate_derivatives_access = TRUE;
}
}

2
includes/regenerate_derivatives.form.inc

@ -95,6 +95,7 @@ function islandora_regenerate_object_derivatives_form_submit(array $form, array
*/
function islandora_regenerate_object_derivatives_batch(AbstractObject $object) {
module_load_include('inc', 'islandora', 'includes/derivatives');
islandora_remove_defer_derivatives_flag($object);
return array(
'title' => t('Regenerating all derivatives for @label', array('@label' => $object->label)),
'operations' => islandora_do_batch_derivatives($object, array('force' => TRUE)),
@ -117,6 +118,7 @@ function islandora_regenerate_object_derivatives_batch(AbstractObject $object) {
*/
function islandora_regenerate_datastream_derivative_batch(AbstractDatastream $datastream) {
module_load_include('inc', 'islandora', 'includes/derivatives');
islandora_remove_defer_derivatives_flag($datastream->parent);
return array(
'title' => t('Regenerating derivatives for the @dsid datastream', array('@dsid' => $datastream->id)),
'operations' => islandora_do_batch_derivatives($datastream->parent, array(

50
includes/tuque_wrapper.inc

@ -115,10 +115,6 @@ 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) {
@ -166,16 +162,21 @@ class IslandoraFedoraObject extends FedoraObject {
public function __set($name, $value) {
parent::__set($name, $value);
// 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);
// 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);
}
}
}
@ -430,16 +431,21 @@ class IslandoraFedoraDatastream extends FedoraDatastream {
public function __set($name, $value) {
parent::__set($name, $value);
// 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);
// 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);
}
}
}

16
includes/utilities.inc

@ -72,6 +72,9 @@ function islandora_temp_file_entry($file_uri, $mime = NULL) {
if (isset($mime)) {
$file->filemime = $mime;
}
else {
$file->filemime = file_get_mimetype($file_uri);
}
}
$file->status = 0;
return file_save($file);
@ -188,12 +191,21 @@ function islandora_describe_repository($url = NULL) {
*/
function islandora_invoke_hook_list($hook, array $refinements, array $args) {
$return = array();
foreach (islandora_build_hook_list($hook, $refinements) as $hook) {
array_unshift($args, $hook);
foreach (islandora_build_hook_list($hook, $refinements) as $refined_hook) {
array_unshift($args, $refined_hook);
$result = call_user_func_array('module_invoke_all', $args);
$return = array_merge_recursive($return, $result);
array_shift($args);
}
if (module_exists('rules')) {
$event_info = rules_get_event_info($hook);
if (isset($event_info['module'])) {
$parameters = $event_info['variables'];
$rule_args = array_slice($args, 0, count($parameters));
array_unshift($rule_args, $hook);
$result = call_user_func_array('rules_invoke_event', $rule_args);
}
}
return $return;
}

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,6 +26,5 @@ files[] = tests/islandora_manage_permissions.test
files[] = tests/datastream_versions.test
files[] = tests/datastream_cache.test
files[] = tests/derivatives.test
files[] = tests/islandora_manage_temp_file.test
files[] = tests/datastream_validator_tests.test
php = 5.3

1
islandora.install

@ -46,6 +46,7 @@ function islandora_uninstall() {
// Add new variables to clean up.
$variables = array(
'islandora_ds_replace_exclude_enforced',
'islandora_defer_derivatives_on_ingest',
);
array_walk($variables, 'variable_del');
}

174
islandora.module

@ -147,7 +147,7 @@ function islandora_menu() {
'page callback' => 'islandora_printer_object',
'page arguments' => array(2),
'type' => MENU_LOCAL_TASK,
'access callback' => 'islandora_object_access',
'access callback' => 'islandora_print_object_access',
'access arguments' => array(ISLANDORA_VIEW_OBJECTS, 2),
);
$items['islandora/object/%islandora_object/print'] = array(
@ -663,6 +663,31 @@ function islandora_print_object(AbstractObject $object) {
return theme('islandora_object_print', array('object' => $object));
}
/**
* View print tab access.
*
* Configurable option in islandora configuration.
*
* @param string $op
* String identifying an operation to check. Should correspond to a
* permission declared via hook_permission().
* @param AbstractObject $object
* An object to check for permissions.
*
* @return bool
* TRUE if at least one implementation of hook_islandora_object_access()
* returned TRUE, and no implementation return FALSE; FALSE otherwise, or
* FALSE if 'islandora_show_print_option' is not selected in islandora
* configuraton.
*/
function islandora_print_object_access($op, $object) {
if (!variable_get('islandora_show_print_option', FALSE)) {
return FALSE;
}
$access = islandora_object_access($op, $object);
return $access;
}
/**
* Implements hook_forms().
*/
@ -1042,6 +1067,7 @@ function islandora_view_object(AbstractObject $object) {
function islandora_printer_object(AbstractObject $object) {
$output = array();
$temp_arr = array();
// Dispatch print hook.
foreach (islandora_build_hook_list(ISLANDORA_PRINT_HOOK, $object->models) as $hook) {
$temp = module_invoke_all($hook, $object);
@ -1483,6 +1509,14 @@ function islandora_entity_info() {
'label' => 'label',
),
);
$entities['islandora_datastream'] = array(
'label' => t('Islandora Datastream'),
'fieldable' => FALSE,
'entity keys' => array(
'id' => 'id',
'label' => 'label',
),
);
return $entities;
}
@ -1495,39 +1529,79 @@ function islandora_entity_info() {
function islandora_entity_property_info() {
$info = array();
$p = &$info['islandora_object']['properties'];
$object_properties = &$info['islandora_object']['properties'];
$datastream_properties = &$info['islandora_datastream']['properties'];
$p['id'] = array(
$object_properties['id'] = array(
'type' => 'text',
'label' => t('ID'),
'description' => t('The identifier of the object.'),
);
$p['label'] = array(
$object_properties['label'] = array(
'type' => 'text',
'label' => t('Object Label'),
'description' => t('The label of the object.'),
'setter callback' => 'islandora_entity_set_property',
);
$p['owner'] = array(
$object_properties['owner'] = array(
'type' => 'text',
'label' => t('Object Owner'),
'description' => t('The name of the owner of the object.'),
'setter callback' => 'islandora_entity_set_property',
);
$p['state'] = array(
$object_properties['state'] = array(
'type' => 'text',
'label' => t('Object State'),
'description' => t('An initial representing the state of the object.'),
'setter callback' => 'islandora_entity_set_property',
);
$p['models'] = array(
$object_properties['models'] = array(
'type' => 'list<text>',
'label' => t('Content Models'),
'description' => t('The list of content models which the object has.'),
'setter callback' => 'islandora_entity_set_property',
);
$p['createdDate'] = array(
$object_properties['createdDate'] = array(
'type' => 'text',
'label' => t('Created Date'),
'description' => t('When the object was created.'),
);
$datastream_properties['id'] = array(
'type' => 'text',
'label' => t('ID'),
'description' => t('The identifier of the datastream.'),
);
$datastream_properties['state'] = array(
'type' => 'text',
'label' => t('Datastream State'),
'description' => t('An initial representing the state of the datastream.'),
'setter callback' => 'islandora_entity_set_property',
);
$datastream_properties['label'] = array(
'type' => 'text',
'label' => t('Datastream Label'),
'description' => t('The label of the datastream.'),
'setter callback' => 'islandora_entity_set_property',
);
$datastream_properties['mimetype'] = array(
'type' => 'text',
'label' => t('MIME type'),
'description' => t('Content type of the datastream.'),
'setter callback' => 'islandora_entity_set_property',
);
$datastream_properties['parent'] = array(
'type' => 'islandora_object',
'label' => t('Object'),
'description' => t('The Tuque object on which this datastream exists.'),
);
$datastream_properties['content'] = array(
'type' => 'text',
'label' => t('Datastream content'),
'description' => t('The contents of the datastream; only useful when the content is textual.'),
'setter callback' => 'islandora_entity_set_property',
);
return $info;
}
@ -1536,9 +1610,12 @@ function islandora_entity_property_info() {
*/
function islandora_download_clip(AbstractObject $object) {
if (isset($_GET['clip'])) {
$is_https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
$http_protocol = $is_https ? 'https' : 'http';
$url = $http_protocol . '://' . $_SERVER['HTTP_HOST'] . $_GET['clip'];
$url = $_GET['clip'];
if (!preg_match('/^https?:\/\//', $url)) {
$is_https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
$http_protocol = $is_https ? 'https' : 'http';
$url = $http_protocol . '://' . $_SERVER['HTTP_HOST'] . $url;
}
$filename = $object->label;
header("Content-Disposition: attachment; filename=\"{$filename}.jpg\"");
header("Content-type: image/jpeg");
@ -1562,6 +1639,7 @@ function islandora_download_clip(AbstractObject $object) {
function islandora_file_mimetype_mapping_alter(&$mapping) {
$mime_detect = new MimeDetect();
$types = $mime_detect->getMimeTypes();
$diff = array_diff_key($types, $mapping['extensions']);
foreach ($diff as $ext => $mime) {
$mapping['mimetypes'][] = $mime;
@ -1691,6 +1769,10 @@ function islandora_islandora_basic_collection_get_query_filters() {
*/
function islandora_islandora_object_ingested(AbstractObject $object) {
module_load_include('inc', 'islandora', 'includes/derivatives');
// Defer derivatives if necessary.
if (islandora_get_defer_derivatives_flag($object)) {
return;
}
islandora_run_derivatives($object, NULL);
}
@ -1702,6 +1784,10 @@ function islandora_islandora_object_ingested(AbstractObject $object) {
*/
function islandora_islandora_datastream_ingested(AbstractObject $object, AbstractDatastream $datastream) {
module_load_include('inc', 'islandora', 'includes/derivatives');
// Defer derivatives if necessary.
if (islandora_get_defer_derivatives_flag($object)) {
return;
}
islandora_run_derivatives($object, $datastream->id);
}
@ -1726,10 +1812,29 @@ function islandora_islandora_datastream_modified(AbstractObject $object, Abstrac
*/
function islandora_form_simpletest_test_form_alter(array &$form) {
module_load_include('inc', 'simpletest', 'simpletest.pages');
module_load_include('inc', 'islandora', 'tests/includes/test_utility_abstraction');
$configuration = IslandoraTestUtilityClass::getTestConfiguration();
$filter_path = $configuration['drupal_filter_file'];
$filter_status = is_writable($filter_path);
if ($filter_status) {
$filter_status_message = theme_image(array('path' => 'misc/watchdog-ok.png', 'attributes' => array())) . " ";
$filter_status_message .= t("Drupal filter at <b>!filter_path</b> is writable by the server.", array(
'!filter_path' => $filter_path,
));
}
else {
$filter_status_message = theme_image(array('path' => 'misc/watchdog-error.png', 'attributes' => array())) . " ";
$filter_status_message .= t("Drupal filter at <b>!filter_path</b> is not writable by the server. Please make sure your webserver has permission to write to the Drupal filter. If the path given is incorrect, you will need to change it in your server's test config file, located in the Islandora module's 'tests' folder as test_config.ini or default.test_config.ini.", array(
'!filter_path' => $filter_path,
));
}
$form['tests'] = array(
'#type' => 'fieldset',
'#title' => t('Tests'),
'#description' => t('Select the test(s) or test group(s) you would like to run, and click <em>Run tests</em>.<br/><br/>NOTE: Tests in groups prefixed with <em>Islandora</em> generally require a configuration file, found in the Islandora module "tests" folder, as well as the use of the Islandora Drupal filter. Before any tests are run, please ensure that your web server has the appropriate permissions to alter the drupal_filter.xml file in your Fedora config folder. Your original Islandora Drupal filter configuration will NOT be overwritten by tests; HOWEVER, if PHP exits abnormally before the filter is reset, please use the "Repair Drupal Filter" button below.'),
'#description' => t("Select the test(s) or test group(s) you would like to run, and click <em>Run tests</em>.<p>NOTE: Tests in groups prefixed with <em>Islandora</em> generally require a configuration file, found in the Islandora module 'tests' folder, as well as the use of the Islandora Drupal filter. Before any tests are run, please ensure that your web server has the appropriate permissions to alter the drupal-filter.xml file in your Fedora config folder. Your original Islandora Drupal filter configuration will NOT be overwritten by tests; HOWEVER, if PHP exits abnormally before the filter is reset, please use the 'Repair Drupal Filter' button below.</p><p><em>Drupal Filter Write Status:</em> !filter_status_message</p>", array(
'!filter_status_message' => $filter_status_message,
)),
);
$form['tests']['table'] = array(
@ -1749,6 +1854,9 @@ function islandora_form_simpletest_test_form_alter(array &$form) {
'#title' => filter_xss($info['name']),
'#description' => filter_xss($info['description']),
);
if (is_subclass_of($class, 'IslandoraWebTestCase', TRUE) && !$filter_status) {
$form['tests']['table'][$group][$class]['#disabled'] = TRUE;
}
}
}
@ -1773,20 +1881,17 @@ function islandora_form_simpletest_test_form_alter(array &$form) {
}
/**
* Submit handler for islandora_form_simpletest_test_form_alter().
* Removes simpletest entries from the Drupal filter.
*/
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;
}
@ -1843,7 +1948,7 @@ 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 &&
if (isset($hook['destination_dsid']) && $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;
@ -1903,6 +2008,8 @@ function islandora_menu_local_tasks_alter(&$data, $router_item, $root_path) {
if ($tab['#link']['path'] == 'islandora/object/%/print_object') {
if ($root_path == 'islandora/object/%') {
$islandora_path = drupal_get_path('module', 'islandora');
$tab['#prefix'] = '<li>';
$tab['#suffix'] = '</li>';
$tab['#theme'] = 'link';
$tab['#text'] = theme('image', array(
'path' => "$islandora_path/images/print-icon.png",
@ -1926,3 +2033,26 @@ function islandora_menu_local_tasks_alter(&$data, $router_item, $root_path) {
}
}
}
/**
* Implements hook_islandora_object_alter().
*/
function islandora_islandora_object_alter(AbstractObject $object, array &$context) {
// Prevent derivative creation during ingest if var is set.
if ($context['action'] == 'ingest' && variable_get('islandora_defer_derivatives_on_ingest', FALSE)) {
module_load_include('inc', 'islandora', 'includes/derivatives');
islandora_set_defer_derivatives_flag($object);
}
}
/**
* Set property on an entity.
*
* Drupal's usual entity_property_verbatim_set() does not work, as tries to use
* ArrayAccess stuff instead of setting properties directly.
*/
function islandora_entity_set_property(&$data, $name, $value, $langcode) {
if (is_object($data)) {
$data->$name = $value;
}
}

443
islandora.rules.inc

@ -21,6 +21,76 @@ function islandora_rules_event_info() {
),
),
),
'islandora_datastream_ingested' => array(
'group' => t('Islandora'),
'label' => t('Datastream ingested'),
'variables' => array(
'object' => array(
'type' => 'islandora_object',
'label' => t('The ingested object'),
'description' => t('A Tuque object for the Fedora object on which the datastream exists, as an entity.'),
),
'datastream' => array(
'type' => 'islandora_datastream',
'label' => t('Datastream ID'),
'description' => t('The ID of the ingested datastream.'),
),
),
),
'islandora_object_modified' => array(
'group' => t('Islandora'),
'label' => t('Object modified'),
'variables' => array(
'object' => array(
'type' => 'islandora_object',
'label' => t('The modified object'),
'description' => t('A Tuque object for the modified Fedora object, as an entity.'),
),
),
),
'islandora_datastream_modified' => array(
'group' => t('Islandora'),
'label' => t('Datastream modified'),
'variables' => array(
'object' => array(
'type' => 'islandora_object',
'label' => t('The modified object'),
'description' => t('A Tuque object for the Fedora object on which the datastream exists, as an entity.'),
),
'datastream' => array(
'type' => 'islandora_datastream',
'label' => t('Datastream'),
'description' => t('The modified datastream.'),
),
),
),
'islandora_object_purged' => array(
'group' => t('Islandora'),
'label' => t('Object purged'),
'variables' => array(
'object' => array(
'type' => 'text',
'label' => t('Object ID'),
'description' => t('The ID of the purged object.'),
),
),
),
'islandora_datastream_purged' => array(
'group' => t('Islandora'),
'label' => t('Datastream purged'),
'variables' => array(
'object' => array(
'type' => 'islandora_object',
'label' => t('Object'),
'description' => t('A Tuque object for the Fedora object on which the datastream existed, as an entity.'),
),
'datastream' => array(
'type' => 'text',
'label' => t('Datastream ID'),
'description' => t('The identifier of the purged datastream.'),
),
),
),
);
}
@ -60,6 +130,35 @@ function islandora_rules_relationship_parameter_array() {
);
}
/**
* Helper function; get default parameters for the XPath condition and action.
*/
function islandora_rules_base_xpath_parameters() {
return array(
'object' => array(
'type' => 'islandora_object',
'label' => t('Object'),
'description' => t('The object containing the datastream to check.'),
),
'datastream_id' => array(
'type' => 'text',
'label' => t('Datastream'),
'description' => t('The identifier of the XML datastream to check.'),
),
'xpath' => array(
'type' => 'text',
'label' => t('XPath'),
'description' => t('An XPath to evaluate.'),
),
'xpath_namespaces' => array(
'type' => 'taxonomy_vocabulary',
'label' => t('XPath Namespace Taxonomy'),
'description' => t('A flat taxonomy of which the terms are namespace prefixes and the description contains the URI for the namespace.'),
),
);
}
/**
* Implements hook_rules_condition_info().
*/
@ -71,6 +170,27 @@ function islandora_rules_condition_info() {
'group' => t('Islandora'),
'parameter' => islandora_rules_relationship_parameter_array(),
);
$cond['islandora_object_has_datastream'] = array(
'label' => t('Check object for existence of a datastream'),
'group' => t('Islandora'),
'parameter' => array(
'object' => array(
'type' => 'islandora_object',
'label' => t('Object'),
'description' => t('The object containing the datastream to check.'),
),
'datastream_id' => array(
'type' => 'text',
'label' => t('Datastream'),
'description' => t('The identifier of the datastream to check.'),
),
),
);
$cond['islandora_rules_datastream_has_xpath'] = array(
'label' => t('Check for an XPath match in an XML datastream'),
'group' => t('Islandora'),
'parameter' => islandora_rules_base_xpath_parameters(),
);
return $cond;
}
@ -92,8 +212,132 @@ function islandora_rules_action_info() {
'group' => t('Islandora'),
'parameter' => islandora_rules_relationship_parameter_array(),
);
$cond['islandora_rules_datastream_load'] = array(
'label' => t('Load a datastream from an object.'),
'group' => t('Islandora'),
'parameter' => array(
'object' => array(
'type' => 'islandora_object',
'label' => t('Object'),
'description' => t('A Tuque object for the Fedora object from which to load the datastream, as an entity.'),
),
'datastream_id' => array(
'type' => 'text',
'label' => t('Datastream ID'),
'description' => t('A string containing the identity of the datastream to load from the object.'),
),
),
'provides' => array(
'datastream' => array(
'type' => 'islandora_datastream',
'label' => t('Loaded datastream instance'),
),
),
);
$cond['islandora_rules_datastream_load_domxpath'] = array(
'label' => t('Load a DOMXPath for a given XML.'),
'group' => t('Islandora DOMXPath'),
'parameter' => array(
'datastream' => array(
'type' => 'text',
'label' => t('XML'),
'description' => t('A string containing the XML to load.'),
),
),
'provides' => array(
'islandora_domxpath' => array(
'type' => 'islandora_domxpath',
'label' => t('Loaded DOMXPath instance'),
),
),
);
$cond['islandora_rules_datastream_load_xpath'] = array(
'label' => t('Load a DOMXPath from a datastream.'),
'group' => t('Islandora DOMXPath'),
'parameter' => array(
'datastream' => array(
'type' => 'islandora_datastream',
'label' => t('Datastream'),
'description' => t('A datastream containing the XML to load.'),
),
),
'provides' => array(
'islandora_domxpath' => array(
'type' => 'islandora_domxpath',
'label' => t('Loaded DOMXPath instance'),
),
),
);
$cond['islandora_rules_datastream_load_namespace_vocab'] = array(
'label' => t('Register namespaces on a DOMXPath instance.'),
'group' => t('Islandora DOMXPath'),
'parameter' => array(
'value' => array(
'type' => 'islandora_domxpath',
'label' => t('DOMXPath instance'),
'description' => t('The DOMXPath instance on which to register the namespaces.'),
),
'xpath_namespaces' => array(
'type' => 'taxonomy_vocabulary',
'label' => t('XPath Namespace Taxonomy'),
'description' => t('A flat taxonomy of which the terms are namespace prefixes and the description contains the URI for the namespace.'),
),
),
);
$cond['islandora_rules_datastream_query_xpath'] = array(
'label' => t('Query nodes from DOMXPath instance.'),
'group' => t('Islandora DOMXPath'),
'parameter' => array(
'xpath' => array(
'type' => 'islandora_domxpath',
'label' => t('DOMXPath instance'),
'description' => t('The DOMXPath instance on which to perform the query.'),
),
'query' => array(
'type' => 'text',
'label' => t('XPath query'),
'description' => t('The XPath query to perform.'),
),
'context_node' => array(
'type' => 'islandora_domnode',
'label' => t('Context Node'),
'description' => t('If provided, the query will be performed relative to the provided node.'),
'optional' => TRUE,
'default value' => NULL,
'allow null' => TRUE,
),
),
'provides' => array(
'nodes' => array(
'type' => 'list<islandora_domnode>',
'label' => t('Queried DOMNode elements'),
),
),
);
$cond['islandora_rules_datastream_set_xpath'] = array(
'label' => t('Set value in elements matched by an XPath in an XML datastream'),
'group' => t('Islandora'),
'parameter' => islandora_rules_base_xpath_parameters() + array(
'value' => array(
'type' => 'text',
'label' => t('Value'),
'description' => t('The value to set in the XML on elements matched by the XPath.'),
),
),
);
return $cond;
}
/**
* Rules action callback; grab a datastream from an object.
*/
function islandora_rules_datastream_load(AbstractObject $object, $datastream_id) {
return array('datastream' => $object[$datastream_id]);
}
/**
* Checks that there is a relationship match on the given object.
*
@ -133,3 +377,202 @@ function islandora_object_remove_relationship($sub, $pred_uri, $pred, $object, $
function islandora_object_add_relationship($sub, $pred_uri, $pred, $object, $type) {
$sub->relationships->add($pred_uri, $pred, $object, $type);
}
/**
* Rules Action callback; instantiate a DOMXPath with some XML.
*/
function islandora_rules_datastream_load_domxpath($string) {
$doc = new DOMDocument();
$doc->loadXML($string);
$xpath = new DOMXPath($doc);
return array('islandora_domxpath' => $xpath);
}
/**
* Rules Action callback; load namespaces onto a DOMXPath instance.
*
* @param DOMXPath $xpath
* A DOMXPath instance.
* @param object $xpath_vocab
* A loaded Drupal taxonomy vocabulary object, in which terms are understood
* to be namespace prefixes and descriptions are the namespace URIs.
*/
function islandora_rules_datastream_load_namespace_vocab($xpath, $xpath_vocab) {
foreach (taxonomy_get_tree($xpath_vocab->vid, 0, 1, FALSE) as $term) {
$xpath->registerNamespace($term->name, $term->description);
}
}
/**
* Rules XPath helper; grab the datastream content and build a DOMXPath.
*/
function islandora_rules_datastream_load_xpath(AbstractDatastream $datastream, $xpath_vocab) {
$result = islandora_rules_datastream_load_domxpath($datastream->content, $xpath_vocab);
islandora_rules_datastream_load_namespace_vocab($result['islandora_domxpath'], $xpath_vocab);
return $result;
}
/**
* Rules Condition callback; test that an XPath returns a non-empty result set.
*/
function islandora_rules_datastream_has_xpath(AbstractObject $object, $datastream_id, $search_xpath, $xpath_vocab) {
$datastream = $object[$datastream_id];
$xpath = islandora_rules_datastream_load_xpath($datastream, $xpath_vocab);
$result = $xpath['islandora_domxpath']->query($search_xpath);
return $result->length > 0;
}
/**
* Rules Action callback; set the value of all matched nodes.
*/
function islandora_rules_datastream_set_xpath(AbstractObject $object, $datastream_id, $search_xpath, $xpath_vocab, $value) {
$datastream = $object[$datastream_id];
$xpath = islandora_rules_datastream_load_xpath($datastream, $xpath_vocab);
$result = $xpath['islandora_domxpath']->query($search_xpath);
foreach ($result as $node) {
$node->nodeValue = $value;
}
$datastream->content = $xpath['islandora_domxpath']->document->saveXML();
}
/**
* Implements hook_rules_data_info().
*/
function islandora_rules_data_info() {
return array(
'islandora_domxpath' => array(
'label' => t('DOMXPath instance'),
'group' => t('Islandora'),
'property info' => array(
'content' => array(
'type' => 'text',
'label' => t('XML Content'),
'computed' => TRUE,
'getter callback' => 'islandora_rules_get_domxpath_document_content',
'token type' => 'string',
),
),
'wrap' => TRUE,
),
'islandora_domnode' => array(
'label' => t('DOMNode instance'),
'group' => t('Islandora'),
'property info' => array(
'node_value' => array(
'type' => 'text',
'label' => t('Node value'),
'computed' => TRUE,
'getter callback' => 'islandora_rules_property_get',
'setter callback' => 'islandora_rules_property_set',
'property' => 'nodeValue',
'token type' => 'string',
),
'text_content' => array(
'type' => 'text',
'label' => t('Text content'),
'computed' => TRUE,
'getter callback' => 'islandora_rules_property_get',
'property' => 'textContent',
'token type' => 'string',
),
),
'wrap' => TRUE,
),
'islandora_domelement' => array(
'label' => t('DOMElement instance'),
'group' => t('Islandora'),
'parent' => 'islandora_domnode',
'wrap' => TRUE,
),
);
}
/**
* Property setter helper; set a property on an object.
*
* In Rules, properties can contain lowercase and numeric characters. Since
* we want to refer to "nodeValue", we have to step around the Rules constraint.
*
* @param object $data
* The object on which to set the property, described by $info['property'].
* @param array $options
* An array of options... Not sure how it's used? Not touched by us, in any
* case. :P
* @param string $name
* The name of the property to set, as used by Rules.
* @param string $type
* The type of object on which the property is being set, as used by Rules.
* @param array $info
* An associative array describing this property. In addition to that
* required by Rules/the Entity API, should contain:
* - property: A string indicate the actual property on the $data we wish to
* set.
*/
function islandora_rules_property_get($data, array $options, $name, $type, $info) {
return $data->$info['property'];
}
/**
* Property setter helper; set a property on an object.
*
* In Rules, properties can contain lowercase and numeric characters. Since
* we want to refer to "nodeValue", we have to step around the Rules constraint.
*
* @param object $data
* The object on which to set the property, described by $info['property'].
* @param string $name
* The name of the property to set, as used by Rules.
* @param mixed $value
* The value to set on the property.
* @param string $langcode
* A string indicating the language being set, or NULL.
* @param string $type
* The type of object on which the property is being set, as used by Rules.
* @param array $info
* An associative array describing this property. In addition to that
* required by Rules/the Entity API, should contain:
* - property: A string indicate the actual property on the $data we wish to
* set.
*/
function islandora_rules_property_set(&$data, $name, $value, $langcode, $type, $info) {
$data->$info['property'] = $value;
}
/**
* Rules property "get" callback; get XML contained in a DOMXPath instance.
*
* @param DOMXPath $xpath
* A DOMXPath instance.
*
* @return string
* The XML contained inside of the DOMXPath instance.
*/
function islandora_rules_get_domxpath_document_content(DOMXPath $xpath) {
return $xpath->document->saveXML();
}
/**
* Rules action callback; perform a query on a DOMXPath instance.
*
* @param DOMXPath $xpath
* A DOMXPath instance.
* @param string $query
* An XPath query.
* @param DOMNode $context_node
* An optional DOMNode. If provided, the query will be performed relative to
* the given node.
*
* @return array
* An array containing:
* - nodes: An array containing the results of the query.
*/
function islandora_rules_datastream_query_xpath(DOMXPath $xpath, $query, DOMNode $context_node = NULL) {
return array('nodes' => iterator_to_array($xpath->query($query, $context_node)));
}
/**
* Rules condition callback; check for the datastream on an object.
*/
function islandora_object_has_datastream(AbstractObject $object, $datastream_id) {
return isset($object[$datastream_id]);
}

77
js/spinner.js

@ -1,9 +1,11 @@
/*jshint browser: true, devel:true*/
/*global jQuery, Drupal, Spinner*/
/**
* @file
* Triggers the display of a spinning icon when the form is submitted.
*/
(function ($) {
'use strict';
Drupal.behaviors.spinner = {
attach: function(context, settings) {
// Store what triggered the submit.
@ -15,44 +17,53 @@
// On enter the first submit button is assumed as is most often the
// case and this is part of the HTML 5 specification, although some
// Browsers may choose the button with the lowest tab-index.
if (event.which == 13) {
if (event.which === 13) {
$(this).data('clicked', $(':submit', this).first());
}
});
});
for (var base in settings.spinner) {
var id = '#' + base;
$(id, context).once('spinner', function () {
var spinner = new Spinner(settings.spinner[base].opts);
$(id).parents('form').one('submit', function(event) {
if ($(this).data('clicked').is(id)) {
event.preventDefault();
// Add Message.
var message = $('<div/>').text(settings.spinner[base].message);
$(id).after(message);
// Make UI changes.
spinner.spin(this);
$('#edit-next').hide();
$('#edit-prev').hide();
// Submit the form after a set timeout, this handles problems with
// safari, in that safari submit's immediately..
if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
$(':submit').attr('disabled', 'disabled');
$.each(settings.spinner, function (base, value) {
var id = '#' + base,
message = $('<div/>').text(settings.spinner[base].message);
if (id !== '#edit-hidden-next') {
$(id, context).once('spinner', function () {
var spinner = new Spinner(settings.spinner[base].opts);
$(id).parents('form').submit(function (event) {
// If some other widget is preventing form submission we should
// not attempt to submit at this time.
if (event.isDefaultPrevented()) {
return;
}
setTimeout(function() {
// Allow for the button to be clicked, then click it then
// prevent the default behavoir.
$(id).removeAttr('disabled')
.click()
.click(function(event) {
event.preventDefault();
});
}, 500);
}
return true;
if ($(this).data('clicked').is(id) && $(this).data('submitted') === undefined) {
event.preventDefault();
// Prevent this from being entered a second time.
$(this).data('submitted', true);
// Add Message.
$(id).after(message);
// Make UI changes.
spinner.spin(this);
$('#edit-next').hide();
$('#edit-prev').hide();
// Submit the form after a set timeout, this handles problems with
// safari, in that safari submits immediately..
if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
$(':submit').attr('disabled', 'disabled');
}
setTimeout(function () {
// Allow for the button to be clicked, then click it then
// prevent the default behaviour.
$(id).removeAttr('disabled')
.click()
.click(function (event) {
event.preventDefault();
});
}, 500);
}
return true;
});
});
});
}
}
});
}
};
})(jQuery);

9
tests/datastream_validator_tests.test

@ -4,8 +4,6 @@
* Tests for things that test tests. Madness.
*/
include 'datastream_validators.inc';
/**
* A test DatastreamValidator for the DatastreamValidatorResultTestCase.
*/
@ -62,7 +60,7 @@ class DatastreamValidatorResultTestCase extends IslandoraWebTestCase {
* Generates a generic DatastreamValidatorResult and grabs its properties.
*/
public function testDatastreamValidatorResult() {
$result = new DatastreamValidatorResult(TRUE, 'true', array());
$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');
@ -111,10 +109,11 @@ class DatastreamValidatorResultTestCase extends IslandoraWebTestCase {
$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'] === 18, "Correct pass line number returned (actual: {$first_caller['line']}; expected: 9).", 'Islandora');
$this->assertTrue($second_caller['line'] === 25, "Correct fail line number returned (actual: {$second_caller['line']}; expected: 13).", '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');
}
}
}
/**

123
tests/datastream_validators.inc → tests/includes/datastream_validators.inc

@ -28,7 +28,7 @@ function islandora_hex2int($hex) {
drupal_set_message(t('String passed to islandora_hex2int() contains non-hexidecimal characters.'), 'error');
return FALSE;
}
if (!strlen($hex) === 4 || !strlen($hex) === 8) {
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;
}
@ -44,84 +44,11 @@ function islandora_hex2int($hex) {
}
}
/**
* A result from a datastream validator; $type defines TRUE/FALSE as pass/fail.
*/
class DatastreamValidatorResult {
/**
* 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 a DatastreamValidatorResult.
*
* @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 datastream validators.
*
* Classes extended from DatastreamValidator don't require much to be useful.
* They accept an IslandoraFedoraObject and a DSID to perform assertions on;
* 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
@ -137,12 +64,12 @@ class DatastreamValidatorResult {
* all the test results using getPasses() and getFails() and transforms those
* into something that DrupalWebTestCase can use.
*/
abstract class DatastreamValidator {
abstract class DatastreamValidator extends IslandoraTestUtilityClass {
/**
* The IslandoraFedoraObject containing the datastream to test.
* The Fedora object containing the datastream to test.
*
* @var IslandoraFedoraObject
* @var IslandoraFedoraObject|FedoraObject
*/
public $object;
@ -160,15 +87,6 @@ abstract class DatastreamValidator {
*/
public $datastreamContent;
/**
* An array of DatastreamValidatorResults.
*
* These should be generated using $this->addResult.
*
* @var DatastreamValidatorResult[]
*/
public $results = array();
/**
* An array of additional required parameters.
*
@ -179,14 +97,14 @@ abstract class DatastreamValidator {
/**
* Constructs a DatastreamValidator.
*
* @param IslandoraFedoraObject $object
* @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(IslandoraFedoraObject $object, $datastream, array $params = array()) {
public function __construct($object, $datastream, array $params = array()) {
$this->object = $object;
$this->datastream = $datastream;
$this->params = $params;
@ -210,18 +128,23 @@ abstract class DatastreamValidator {
* fails to $this->passes and/or $this->fails.
*/
public function runValidators() {
$methods = get_class_methods($this);
foreach ($methods as $method) {
if (substr($method, 0, 6) === 'assert') {
$this->$method();
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 DatastreamValidatorResults.
* Returns an array of IslandoraTestUtilityResults.
*
* @return DatastreamValidatorResult[]
* @return IslandoraTestUtilityResult[]
* The results.
*/
public function getResults() {
@ -240,7 +163,7 @@ abstract class DatastreamValidator {
* The message to put in the result.
*/
public function addResult($type, $message) {
$result = new DatastreamValidatorResult($type, $message, $this->getAssertionCall());
$result = new IslandoraTestUtilityResult($type, $message, $this->getAssertionCall());
$this->results[] = $result;
}
@ -254,7 +177,7 @@ abstract class DatastreamValidator {
* @return array
* Array representing the true caller.
*/
protected function getAssertionCall() {
public function getAssertionCall() {
$backtrace = debug_backtrace();
// While the current caller's function starts with 'assert', and another one
@ -420,7 +343,7 @@ class TextDatastreamValidator extends DatastreamValidator {
/**
* Constructor override; blow up if we don't have our two values.
*/
public function __construct(IslandoraFedoraObject $object, $datastream, array $params = array()) {
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.');
}
@ -463,14 +386,14 @@ class WAVDatastreamValidator extends DatastreamValidator {
/**
* We need a special constructor here to get the hex datastream content.
*
* @param IslandoraFedoraObject $object
* @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(IslandoraFedoraObject $object, $datastream, array $params = array()) {
public function __construct($object, $datastream, array $params = array()) {
parent::__construct($object, $datastream, $params);
$this->datastreamContent = bin2hex($this->datastreamContent);
}

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();
}
}

415
tests/includes/islandora_web_test_case.inc

@ -0,0 +1,415 @@
<?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;
/**
* Instantiates an IslandoraWebTestCase with a configuration.
*
* @throws Exception
* If the required test config file is not found.
*/
public function __construct($test_id = NULL) {
$this->configuration = IslandoraTestUtilityClass::getTestConfiguration();
parent::__construct($test_id);
}
/**
* Defers to IslandoraTestUtilities for missing methods.
*
* @param string $method
* The method being called.
* @param array $args
* The arguments for that method.
*
* @return bool
* TRUE if the result was a pass, or FALSE otherwise.
*/
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());
}
}
/**
* Run all tests in this class.
*
* Attempts to figure out if the Drupal filter is writable before running any
* tests.
*
* @see DrupalWebTestCase::run()
*/
public function run(array $methods = array()) {
// Determine if the Drupal filter is writable so we know if we can proceed.
if (is_writable($this->configuration['drupal_filter_file'])) {
// Set up the SimpleTest environment.
simpletest_verbose(NULL, variable_get('file_public_path', conf_path() . '/files'), get_class($this));
$this->httpauth_method = variable_get('simpletest_httpauth_method', CURLAUTH_BASIC);
$username = variable_get('simpletest_httpauth_username', NULL);
$password = variable_get('simpletest_httpauth_password', NULL);
if ($username && $password) {
$this->httpauth_credentials = $username . ':' . $password;
}
set_error_handler(array($this, 'errorHandler'));
$class = get_class($this);
// Iterate through all the methods in this class, unless a specific list
// of methods to run was passed.
$class_methods = get_class_methods($class);
if ($methods) {
$class_methods = array_intersect($class_methods, $methods);
}
foreach ($class_methods as $method) {
// If the current method starts with "test", run it - it's a test.
if (strtolower(substr($method, 0, 4)) == 'test') {
// Insert a fail record. This will be deleted on completion to ensure
// that testing completed.
$method_info = new ReflectionMethod($class, $method);
$caller = array(
'file' => $method_info->getFileName(),
'line' => $method_info->getStartLine(),
'function' => $class . '->' . $method . '()',
);
$completion_check_id = DrupalTestCase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller);
try {
$this->setUp();
$this->$method();
}
catch (Exception $e) {
$this->exceptionHandler($e);
}
$this->tearDown();
// Remove the completion check record.
DrupalTestCase::deleteAssert($completion_check_id);
}
}
}
// If the Drupal filter is not writable, skip testing and error out.
else {
$method_info = new ReflectionMethod($this, 'run');
$class = get_class($this);
set_error_handler(array($this, 'errorHandler'));
$caller = array(
'file' => $method_info->getFileName(),
'line' => $method_info->getStartLine(),
'function' => $class . '->run()',
);
$this->assert(FALSE, "Unable to proceed; the Drupal filter is not writable by the server.", "Completion check", $caller);
}
drupal_get_messages();
restore_error_handler();
}
/**
* 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');
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);
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.
*
* @return bool
* If the deletion fails, return FALSE.
*/
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();
}

358
tests/includes/utilities.inc

@ -0,0 +1,358 @@
<?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.
* 'string' - A string to set the datastream from; overrides 'path'.
* 'control_group' - The single-letter control group identifier.
* 'mimetype' - The datastream's mimetype.
*
* @return bool|AbstractObject
* FALSE if the object ingest failed, or the object 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['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'];
}
if (isset($datastream['string'])) {
$new_datastream->setContentFromString($datastream['string']);
}
else {
$path = isset($datastream['path']) ? $datastream['path'] : drupal_get_path('module', 'islandora') . '/tests/fixtures/test.jpg';
$new_datastream->setContentFromFile($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);
}
}

604
tests/islandora_web_test_case.inc

@ -1,604 +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.
*
* 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 (!is_object($object)) {
$this->fail("Datastream validation failed; Object passed in is invalid.", 'Islandora');
return;
}
module_load_include('inc', 'islandora', 'tests/datastream_validators');
foreach ($datastreams as $datastream) {
// Let's give them conventional names.
$dsid = $datastream[0];
$prefix = $datastream[1];
$params = array();
if (isset($datastream[2])) {
$params = $datastream[2];
}
// Legacy tests were created before the CamelCase conventions of the class
// system now in place. So, we need to automagically seek out prefixes
// that start with a lower-case letter and convert them to the proper
// format (rather than fixing every single legacy test).
if (ctype_lower(substr($prefix, 0, 1))) {
// Handle the case where the prefix is "image".
if ($prefix === 'image') {
$prefix = 'Image';
}
// Handle the case where the prefix is "text".
elseif ($prefix === 'text') {
$prefix = 'Text';
}
// All other cases involve just converting everything to caps.
else {
$prefix = strtoupper($prefix);
}
}
// 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->assert($result->getType(), $result->getMessage(), 'Islandora', $result->getCaller());
}
}
else {
$this->fail("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');
}
}
}
/**
* 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, 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()) {
$tuque = islandora_get_tuque_connection($this->admin);
$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'])) {
try {
$object->models = (array) $properties['models'];
}
catch (Exception $e) {
$this->fail("Encountered an exception when trying to add content models to {$object->id}: $e");
return FALSE;
}
}
$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;
}
}
/**
* 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->assertRaw("<div class=\"messages error\">", $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->assertNoRaw("<div class=\"messages error\">", $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->assertRaw("<div class=\"messages warning\">", $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->assertNoRaw("<div class=\"messages error\">", $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);
}
}

30
tests/scripts/travis_setup.sh

@ -1,12 +1,12 @@
#!/bin/bash
mysql -u root -e 'create database drupal;'
mysql -u root -e "create database fedora;"
mysql -u root -e "GRANT ALL PRIVILEGES ON fedora.* To 'fedora'@'localhost' IDENTIFIED BY 'fedora';"
mysql -u root -e "GRANT ALL PRIVILEGES ON drupal.* To 'drupal'@'localhost' IDENTIFIED BY 'drupal';"
cd $HOME
git clone git://github.com/Islandora/tuque.git
git clone -b $FEDORA_VERSION git://github.com/Islandora/islandora_tomcat.git
wget http://alpha.library.yorku.ca/islandora_tomcat.$FEDORA_VERSION.tar.gz
tar xf islandora_tomcat.$FEDORA_VERSION.tar.gz
cd islandora_tomcat
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"
@ -17,27 +17,29 @@ pear upgrade --force Console_Getopt
pear upgrade --force pear
pear channel-discover pear.drush.org
# "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/ \;
wget http://alpha.library.yorku.ca/drush-6.3.tar.gz
tar xf drush-6.3.tar.gz
sudo mv drush-6.3 /opt/
sudo ln -s /opt/drush-6.3/drush /usr/bin/drush
wget http://alpha.library.yorku.ca/PHP_CodeSniffer-1.5.6.tgz
pear install PHP_CodeSniffer-1.5.6.tgz
wget http://alpha.library.yorku.ca/phpcpd.phar
sudo mv phpcpd.phar /usr/local/bin/phpcpd
sudo chmod +x /usr/local/bin/phpcpd
phpenv rehash
drush dl --yes drupal
cd drupal-*
drush si minimal --db-url=mysql://drupal:drupal@localhost/drupal --yes
# 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 &>/dev/null &
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
ln -s $HOME/tuque sites/all/libraries/tuque
drush dl --yes coder
drush dl --yes potx
drush dl --yes coder-7.x-2.4
drush dl --yes potx-7.x-1.0
drush en --yes coder_review
drush en --yes simpletest
drush en --yes potx

Loading…
Cancel
Save