Browse Source

Merge branch '7.x' of github.com:Islandora/islandora into 7.x

pull/415/head
Alan Stanley 11 years ago
parent
commit
0cc10ba3eb
  1. 37
      includes/ingest.form.inc
  2. 16
      includes/utilities.inc
  3. 96
      islandora.module
  4. 21
      js/spin/LICENSE.txt
  5. 11
      js/spin/README.md
  6. 1
      js/spin/spin.min.js
  7. 58
      js/spinner.js
  8. 473
      tests/datastream_validators.inc
  9. BIN
      tests/fixtures/test.jpg
  10. 161
      tests/islandora_web_test_case.inc

37
includes/ingest.form.inc

@ -703,11 +703,48 @@ function islandora_ingest_form_ingest_button(array &$form_state) {
'#type' => 'submit',
'#name' => 'ingest',
'#value' => t('Ingest'),
'#process' => array('islandora_ingest_form_ingest_button_process'),
'#validate' => $validate,
'#submit' => $submit,
);
}
/**
* Process hook for the ingest button, adds the please wait spinning icon.
*/
function islandora_ingest_form_ingest_button_process(array $element) {
$settings['spinner'][$element['#id']] = array(
'message' => t('Please be patient while the the page loads.'),
'options' => array(
'lines' => 10,
'length' => 20,
'width' => 10,
'radius' => 30,
'corners' => 1,
'rotate' => 0,
'direction' => 1,
'color' => '#000',
'speed' => 1,
'trail' => 60,
'shadow' => FALSE,
'hwaccel' => FALSE,
'className' => 'spinner',
'zIndex' => 2000000000,
'top' => 'auto',
'left' => 'auto',
),
);
drupal_add_js($settings, 'setting');
$islandora_path = drupal_get_path('module', 'islandora');
$element['#attached'] = array(
'js' => array(
"$islandora_path/js/spin/spin.min.js",
"$islandora_path/js/spinner.js",
),
);
return $element;
}
/**
* The submit handler for the ingest form.
*

16
includes/utilities.inc

@ -908,3 +908,19 @@ function islandora_as_renderable_array(&$markup_array) {
}
unset($value);
}
/**
* Sanitizes an input string to be valid XML.
*
* @param string $input
* An input string.
* @param string $replacement
* What we are replacing invalid characters with, defaults to ''.
*
* @return string
* The sanitized string.
*/
function islandora_sanitize_input_for_valid_xml($input, $replacement = '') {
$input = preg_replace('/[^\x9\xA\xD\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', $replacement, $input);
return $input;
}

96
islandora.module

@ -105,7 +105,7 @@ function islandora_menu() {
'page callback' => 'drupal_get_form',
'page arguments' => array('islandora_metadata_display_form'),
'file' => 'includes/metadata.inc',
'access arguments' => array('adminisiter site configuration'),
'access arguments' => array('administer site configuration'),
);
$items['admin/islandora/solution_packs'] = array(
'title' => 'Solution packs',
@ -1639,6 +1639,100 @@ function islandora_islandora_datastream_modified(AbstractObject $object, Abstrac
}
/**
* Implements hook_form_simpletest_test_form_alter().
*/
function islandora_form_simpletest_test_form_alter(array &$form) {
module_load_include('inc', 'simpletest', 'simpletest.pages');
$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.'),
);
$form['tests']['table'] = array(
'#theme' => 'simpletest_test_table',
);
// Generate the list of tests arranged by group.
$groups = simpletest_test_get_all();
foreach ($groups as $group => $tests) {
$form['tests']['table'][$group] = array(
'#collapsed' => TRUE,
);
foreach ($tests as $class => $info) {
$form['tests']['table'][$group][$class] = array(
'#type' => 'checkbox',
'#title' => filter_xss($info['name']),
'#description' => filter_xss($info['description']),
);
}
}
// Operation buttons.
$form['tests']['op'] = array(
'#type' => 'submit',
'#value' => t('Run tests'),
);
$form['reset'] = array(
'#type' => 'fieldset',
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#title' => t('Repair Islandora Drupal Filter'),
'#description' => t('Attempts to repair the Islandora Drupal filter if a test that alters the Drupal filter has crashed. This is also intended for developers when creating tests built using the IslandoraWebTestCase class.'),
);
$form['reset']['op'] = array(
'#type' => 'submit',
'#value' => t('Repair Drupal Filter'),
'#submit' => array('islandora_repair_drupal_filter'),
);
}
/**
* Submit handler for islandora_form_simpletest_test_form_alter().
*/
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");
}
else {
drupal_set_message(t('Required default.test_config.ini/test_config.ini file not found'), 'error');
return FALSE;
}
// Xpath to the filter 'sql' elements.
$drupal_filter_dom = new DOMDocument();
$drupal_filter_dom->loadXML(file_get_contents($configuration['drupal_filter_file']));
$drupal_filter_xpath = new DOMXPath($drupal_filter_dom);
// Blow out the simpletest stuff.
$entries = 0;
foreach ($drupal_filter_xpath->query('//sql') as $sql) {
if (strpos($sql->nodeValue, 'simpletest') !== FALSE) {
$parent = $sql->parentNode;
$root = $parent->parentNode;
$parent->removeChild($sql);
$root->removeChild($parent);
$entries++;
}
}
file_put_contents($configuration['drupal_filter_file'], $drupal_filter_dom->saveXML());
if ($entries == 0) {
drupal_set_message(t("No simpletest entries were found in the Drupal filter."));
}
else {
drupal_set_message(format_plural($entries, "Removed 1 simpletest entry from the Drupal filter.", "Removed @count simpletest entries from the Drupal filter."));
}
}
/**
* Implements hook_islandora_metadata_display_info().
*/
function islandora_islandora_metadata_display_info() {

21
js/spin/LICENSE.txt

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2011 Felix Gnass [fgnass at neteye dot de]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

11
js/spin/README.md

@ -0,0 +1,11 @@
CONTENTS OF THIS FILE
---------------------
* summary
SUMMARY
-------
This directory contains the spin.js library: http://fgnass.github.io/spin.js/
downloaded from http://fgnass.github.io/spin.js/dist/spin.min.js on Sept 27th
2013. This project is under version control: https://github.com/fgnass/spin.js.

1
js/spin/spin.min.js vendored

@ -0,0 +1 @@
(function(t,e){if(typeof exports=="object")module.exports=e();else if(typeof define=="function"&&define.amd)define(e);else t.Spinner=e()})(this,function(){"use strict";var t=["webkit","Moz","ms","O"],e={},i;function o(t,e){var i=document.createElement(t||"div"),o;for(o in e)i[o]=e[o];return i}function n(t){for(var e=1,i=arguments.length;e<i;e++)t.appendChild(arguments[e]);return t}var r=function(){var t=o("style",{type:"text/css"});n(document.getElementsByTagName("head")[0],t);return t.sheet||t.styleSheet}();function s(t,o,n,s){var a=["opacity",o,~~(t*100),n,s].join("-"),f=.01+n/s*100,l=Math.max(1-(1-t)/o*(100-f),t),u=i.substring(0,i.indexOf("Animation")).toLowerCase(),d=u&&"-"+u+"-"||"";if(!e[a]){r.insertRule("@"+d+"keyframes "+a+"{"+"0%{opacity:"+l+"}"+f+"%{opacity:"+t+"}"+(f+.01)+"%{opacity:1}"+(f+o)%100+"%{opacity:"+t+"}"+"100%{opacity:"+l+"}"+"}",r.cssRules.length);e[a]=1}return a}function a(e,i){var o=e.style,n,r;i=i.charAt(0).toUpperCase()+i.slice(1);for(r=0;r<t.length;r++){n=t[r]+i;if(o[n]!==undefined)return n}if(o[i]!==undefined)return i}function f(t,e){for(var i in e)t.style[a(t,i)||i]=e[i];return t}function l(t){for(var e=1;e<arguments.length;e++){var i=arguments[e];for(var o in i)if(t[o]===undefined)t[o]=i[o]}return t}function u(t){var e={x:t.offsetLeft,y:t.offsetTop};while(t=t.offsetParent)e.x+=t.offsetLeft,e.y+=t.offsetTop;return e}function d(t,e){return typeof t=="string"?t:t[e%t.length]}var p={lines:12,length:7,width:5,radius:10,rotate:0,corners:1,color:"#000",direction:1,speed:1,trail:100,opacity:1/4,fps:20,zIndex:2e9,className:"spinner",top:"auto",left:"auto",position:"relative"};function c(t){if(typeof this=="undefined")return new c(t);this.opts=l(t||{},c.defaults,p)}c.defaults={};l(c.prototype,{spin:function(t){this.stop();var e=this,n=e.opts,r=e.el=f(o(0,{className:n.className}),{position:n.position,width:0,zIndex:n.zIndex}),s=n.radius+n.length+n.width,a,l;if(t){t.insertBefore(r,t.firstChild||null);l=u(t);a=u(r);f(r,{left:(n.left=="auto"?l.x-a.x+(t.offsetWidth>>1):parseInt(n.left,10)+s)+"px",top:(n.top=="auto"?l.y-a.y+(t.offsetHeight>>1):parseInt(n.top,10)+s)+"px"})}r.setAttribute("role","progressbar");e.lines(r,e.opts);if(!i){var d=0,p=(n.lines-1)*(1-n.direction)/2,c,h=n.fps,m=h/n.speed,y=(1-n.opacity)/(m*n.trail/100),g=m/n.lines;(function v(){d++;for(var t=0;t<n.lines;t++){c=Math.max(1-(d+(n.lines-t)*g)%m*y,n.opacity);e.opacity(r,t*n.direction+p,c,n)}e.timeout=e.el&&setTimeout(v,~~(1e3/h))})()}return e},stop:function(){var t=this.el;if(t){clearTimeout(this.timeout);if(t.parentNode)t.parentNode.removeChild(t);this.el=undefined}return this},lines:function(t,e){var r=0,a=(e.lines-1)*(1-e.direction)/2,l;function u(t,i){return f(o(),{position:"absolute",width:e.length+e.width+"px",height:e.width+"px",background:t,boxShadow:i,transformOrigin:"left",transform:"rotate("+~~(360/e.lines*r+e.rotate)+"deg) translate("+e.radius+"px"+",0)",borderRadius:(e.corners*e.width>>1)+"px"})}for(;r<e.lines;r++){l=f(o(),{position:"absolute",top:1+~(e.width/2)+"px",transform:e.hwaccel?"translate3d(0,0,0)":"",opacity:e.opacity,animation:i&&s(e.opacity,e.trail,a+r*e.direction,e.lines)+" "+1/e.speed+"s linear infinite"});if(e.shadow)n(l,f(u("#000","0 0 4px "+"#000"),{top:2+"px"}));n(t,n(l,u(d(e.color,r),"0 0 1px rgba(0,0,0,.1)")))}return t},opacity:function(t,e,i){if(e<t.childNodes.length)t.childNodes[e].style.opacity=i}});function h(){function t(t,e){return o("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',e)}r.addRule(".spin-vml","behavior:url(#default#VML)");c.prototype.lines=function(e,i){var o=i.length+i.width,r=2*o;function s(){return f(t("group",{coordsize:r+" "+r,coordorigin:-o+" "+-o}),{width:r,height:r})}var a=-(i.width+i.length)*2+"px",l=f(s(),{position:"absolute",top:a,left:a}),u;function p(e,r,a){n(l,n(f(s(),{rotation:360/i.lines*e+"deg",left:~~r}),n(f(t("roundrect",{arcsize:i.corners}),{width:o,height:i.width,left:i.radius,top:-i.width>>1,filter:a}),t("fill",{color:d(i.color,e),opacity:i.opacity}),t("stroke",{opacity:0}))))}if(i.shadow)for(u=1;u<=i.lines;u++)p(u,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(u=1;u<=i.lines;u++)p(u);return n(e,l)};c.prototype.opacity=function(t,e,i,o){var n=t.firstChild;o=o.shadow&&o.lines||0;if(n&&e+o<n.childNodes.length){n=n.childNodes[e+o];n=n&&n.firstChild;n=n&&n.firstChild;if(n)n.opacity=i}}}var m=f(o("group"),{behavior:"url(#default#VML)"});if(!a(m,"transform")&&m.adj)h();else i=a(m,"animation");return c});

58
js/spinner.js

@ -0,0 +1,58 @@
/**
* @file
* Triggers the display of a spinning icon when the form is submitted.
*/
(function ($) {
Drupal.behaviors.spinner = {
attach: function(context, settings) {
// Store what triggered the submit.
$('form').once('submit-resolver', function() {
$(this).click(function(event) {
$(this).data('clicked', $(event.target));
});
$(this).keypress(function(event) {
// 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) {
$(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');
}
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;
});
});
}
}
};
})(jQuery);

473
tests/datastream_validators.inc

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

BIN
tests/fixtures/test.jpg vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

161
tests/islandora_web_test_case.inc

@ -74,15 +74,22 @@ class IslandoraWebTestCase extends DrupalWebTestCase {
$connection_info = Database::getConnectionInfo('default');
$drupal_filter_dom = new DomDocument();
$drupal_filter_dom->loadXML($this->originalDrupalFilterContent);
$drupal_filter_xpath = new DOMXPath($drupal_filter_dom);
$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'];
$results = $drupal_filter_xpath->query("/FilterDrupal_Connection/connection[@server='$server' and @dbname='$dbname' and @user='$user' and @password='$password' and @port='$port']/sql");
$results->item(0)->nodeValue = "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=?;";
$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());
}
@ -181,29 +188,37 @@ class IslandoraWebTestCase extends DrupalWebTestCase {
}
/**
* Asserts that an object's given datastreams are common-type image files.
* Attempts to validate an array of datastreams, generally via binary checks.
*
* Uses PHPGD to run the assertion check. This means that only certain kinds
* of image files can be checked. Please check the documentation for the PHPGD
* imagecreatefromstring() function to determine what filetypes are valid.
* These functions exist in, and can be added to, datastream_validators.inc,
* which is found in this folder.
*
* @param AbstractObject $object
* The PID of the object.
* @param array $datastreams
* An array of datastreams to check.
* $param AbstractObject $object
* The object to load datastreams from.
* $param array $datastreams
* An array of paired DSIDs, validate function names, and optional params.
*/
public function assertImageDatastreams($object, array $datastreams) {
public function validateDatastreams($object, array $datastreams) {
if (!is_object($object)) {
$this->fail("Failed. Object passed in is invalid.", 'Islandora');
}
else {
module_load_include('inc', 'islandora', 'tests/datastream_validators');
foreach ($datastreams as $datastream) {
$datastream_string = $object[$datastream]->content;
if (!imagecreatefromstring($datastream_string)) {
$this->fail("Image datastream {$datastream} is either invalid or corrupt.", 'Islandora');
if (isset($object[$datastream[0]])) {
$function = 'islandora_validate_' . $datastream[1] . '_datastream';
if (function_exists($function)) {
if (isset($datastream[2])) {
$results = $function($object, $datastream[0], $datastream[2]);
}
else {
$results = $function($object, $datastream[0]);
}
foreach ($results as $result) {
$this->assertTrue($result[0], $result[1], 'Islandora');
}
}
else {
$this->pass("Image datastream {$datastream} is valid.", 'Islandora');
$this->fail("No {$datastream[0]} validation function exists for the {$datastream[1]} datastream.", 'Islandora');
}
}
}
@ -240,10 +255,16 @@ class IslandoraWebTestCase extends DrupalWebTestCase {
* @param string $button
* The label of the first 'Delete' button
*/
public function deleteObject($pid, $button = 'Delete') {
public function deleteObject($pid, $button = NULL) {
$path = 'islandora/object/' . $pid . '/manage/properties';
$edit = array();
if (isset($button)) {
$this->drupalPost($path, $edit, $button);
}
else {
$object = islandora_object_load($pid);
$this->drupalPost($path, $edit, "Permanently remove '{$object->label}' from repository");
}
$this->drupalPost($this->url, $edit, t('Delete'));
$object = islandora_object_load($pid);
@ -252,34 +273,98 @@ class IslandoraWebTestCase extends DrupalWebTestCase {
}
/**
* Reverses a hex string and converts it to a little-endian-formatted integer.
* Constructs and ingests a Fedora object and datastream(s) via tuque.
*
* This is useful for running checks on values that appear in the binary
* of a datastream. Returns FALSE if the hex value contains non-hex characters
* or if the string would not return a 16- or 32-bit formatted big-endian
* signed integer.
* All keys inside the parameter arrays for this function are optional. it
* can be run simply by calling $this->ingestConstructedObject();.
*
* @param string $hex
* The hex value being converted.
* @param array $properties
* An array containing object information using these keys:
* 'label' - The object label; randomized if not set.
* 'pid' - 'namespace:pid', or just 'namespace' to generate the suffix.
* 'models' - An array that can contain multiple content model PIDs.
* 'owner' - The object's owner.
* '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|int
* FALSE or the integer value that is converted.
* @return bool|array
* FALSE if the object ingest failed, or the object array if successful.
*/
public function convertHexToInt($hex) {
public function ingestConstructedObject(array $properties = array(), array $datastreams = array()) {
module_load_include('inc', 'islandora', 'includes/tuque');
$tuque = new IslandoraTuque();
$repository = $tuque->repository;
if (!isset($properties['pid'])) {
$properties['pid'] = "islandora";
}
$object = $repository->constructObject($properties['pid']);
// A couple of quick string checks.
if (!ctype_xdigit($hex)) {
$this->fail('String passed to convertHexToInt() contains non-hexidecimal characters.', 'PHP');
return FALSE;
// Set the object properties before ingesting it.
if (isset($properties['label'])) {
$object->label = $properties['label'];
}
if (!strlen($hex) === 4 || !strlen($hex) === 8) {
$this->fail('String passed to convertHexToInt() cannot create a 16- or 32-bit little-endian signed integer', 'PHP');
else {
$properties['label'] = $this->randomName(16);
$object->label = $properties['label'];
}
if (isset($properties['owner'])) {
$object->owner = $properties['owner'];
}
if (isset($properties['models']) && is_array($properties['models'])) {
foreach ($properties['models'] as $model) {
$object->relationships->add(FEDORA_MODEL_URI, 'hasModel', $model);
}
}
elseif (isset($properties['models']) && !is_array($properties['models'])) {
$this->fail(t("'models' key of properties variable is not an array. Content model(s) will not be set."), 'Islandora');
}
$repository->ingestObject($object);
if (!$object) {
$this->fail(t("Failed to ingest object."), 'Islandora');
return FALSE;
}
else {
$this->pass(t("Ingested object %object", array('%object' => $object->id)), 'Islandora');
}
// The actual conversion.
$reverse_hex = implode('', array_reverse(str_split($hex, 2)));
$int = hexdec($reverse_hex);
return $int;
// 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;
}
}

Loading…
Cancel
Save