Browse Source

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

pull/457/head
Nelson Hart 11 years ago
parent
commit
283a396204
  1. 6
      includes/admin.form.inc
  2. 2
      includes/breadcrumb.inc
  3. 157
      includes/datastream.inc
  4. 18
      islandora.api.php
  5. 1
      islandora.info
  6. 42
      islandora.module
  7. 160
      tests/datastream_cache.test

6
includes/admin.form.inc

@ -55,6 +55,12 @@ function islandora_repository_admin(array $form, array &$form_state) {
'#description' => t('The PID of the Root Collection Object'),
'#required' => TRUE,
),
'islandora_use_datastream_cache_headers' => array(
'#type' => 'checkbox',
'#title' => t('Generate/parse datastream HTTP cache headers'),
'#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_namespace' => array(
'#type' => 'fieldset',

2
includes/breadcrumb.inc

@ -27,6 +27,8 @@
function islandora_get_breadcrumbs($object) {
$breadcrumbs = islandora_get_breadcrumbs_recursive($object->id, $object->repository);
array_pop($breadcrumbs);
$context = 'islandora';
drupal_alter('islandora_breadcrumbs', $breadcrumbs, $context);
return $breadcrumbs;
}

157
includes/datastream.inc

@ -46,8 +46,6 @@ function islandora_view_datastream(AbstractDatastream $datastream, $download = F
}
}
header_remove('Cache-Control');
header_remove('Expires');
header('Content-type: ' . $datastream->mimetype);
if ($datastream->controlGroup == 'M' || $datastream->controlGroup == 'X') {
header('Content-length: ' . $datastream->size);
@ -59,13 +57,164 @@ function islandora_view_datastream(AbstractDatastream $datastream, $download = F
$filename = $datastream->label . '.' . $extension;
header("Content-Disposition: attachment; filename=\"$filename\"");
}
$cache_check = islandora_view_datastream_cache_check($datastream);
if ($cache_check !== 200) {
if ($cache_check === 304) {
header('HTTP/1.1 304 Not Modified');
}
elseif ($cache_check === 412) {
header('HTTP/1.0 412 Precondition Failed');
}
}
islandora_view_datastream_set_cache_headers($datastream);
drupal_page_is_cacheable(FALSE);
// Try not to load the file into PHP memory!
ob_end_flush();
$datastream->getContent('php://output');
// Close and flush ALL the output buffers!
while (@ob_end_flush()) {
};
// New content needed.
if ($cache_check === 200) {
$datastream->getContent('php://output');
}
exit();
}
/**
* Parse "etags" from HTTP If-Match or If-None-Match headers.
*
* Parses from the CSV-like struture supported by HTTP headers into an array,
* so `"asdf", "fdsa", W/"2132"` should become an array containing the strings:
* - asdf
* - fdsa
* - 2132
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
*
* @param string $header_value
* The value from the headers.
*
* @return array
* An array containing all the etags present.
*/
function islandora_parse_http_match_headers($header_value) {
$matches = array();
// Match the CSV-like structure supported by the HTTP headers.
$count = preg_match_all('/(((W\/)?("?)(\*|.+?)\4)(, +)?)/', $header_value, $matches);
// The fifth sub-expression/group is which will contain the etags.
return $matches[5];
}
/**
* Validate cache headers.
*
* @param AbstractDatastream $datastream
* The datastream for which to check the request headers against.
*
* @return int
* An integer representing the HTTP response code. One of:
* - 200: Proceed as normal. (Full download).
* - 304: Resource hasn't changed; pass cache validation.
* - 412: Resource has changed; fail cache validation.
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
*/
function islandora_view_datastream_cache_check(AbstractDatastream $datastream) {
if (!variable_get('islandora_use_datastream_cache_headers', TRUE)) {
return 200;
}
// Let's assume that if we get here, we'll be able to complete the request.
$return = 200;
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$modified_since = DateTime::createFromFormat('D, d M Y H:i:s e', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ($datastream->createdDate->getTimestamp() - $modified_since->getTimestamp() > 0) {
// Changed!
return $return;
}
else {
$return = 304;
}
}
if ($return === 200 && isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
$unmodified_since = DateTime::createFromFormat('D, d M Y H:i:s e', $_SERVER['HTTP_IF_UNMODIFIED_SINCE']);
if ($datastream->createdDate->getTimestamp() !== $unmodified_since->getTimestamp()) {
// Changed!
$return = 412;
}
else {
return $return;
}
}
// Only consider Etags we have provided.
if (isset($datastream->checksum)) {
$tags = array();
foreach ($datastream as $offset => $version) {
if (isset($version->checksum)) {
$tags[$offset] = $version->checksum;
}
}
if ($return === 200 && isset($_SERVER['HTTP_IF_MATCH'])) {
$request_tags = islandora_parse_http_match_headers($_SERVER['HTTP_IF_MATCH']);
if (in_array('*', $request_tags) || count(array_intersect($tags, $request_tags)) > 0) {
// There's a match... Let things go ahead.
return $return;
}
else {
$return = 412;
}
}
if (in_array($return, array(200, 304), TRUE) && isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
$request_tags = islandora_parse_http_match_headers($_SERVER['HTTP_IF_NONE_MATCH']);
if (in_array('*', $request_tags) || count(array_intersect($tags, $request_tags)) > 0) {
$return = 304;
}
else {
$return = 200;
}
}
}
return $return;
}
/**
* Set various HTTP headers for caching.
*
* @param AbstractDatastream $datastream
* The datastream being viewed/downloaded.
*/
function islandora_view_datastream_set_cache_headers(AbstractDatastream $datastream) {
if (variable_get('islandora_use_datastream_cache_headers', TRUE)) {
// Force cache revalidation.
header('Expires: Sun, 19 Nov 1978 05:00:00 GMT');
$cache_control = array();
if ($datastream->parent->repository->api->connection->username == 'anonymous') {
$cache_control[] = 'public';
}
else {
$cache_control[] = 'private';
}
$cache_control[] = 'must-revalidate';
$cache_control[] = 'max-age=0';
header('Cache-Control: ' . implode(', ', $cache_control));
header('Last-Modified: ' . $datastream->createdDate->format('D, d M Y H:i:s \G\M\T'));
if (isset($datastream->checksum)) {
header("Etag: \"{$datastream->checksum}\"");
}
}
else {
header_remove('Cache-Control');
header_remove('Expires');
}
}
/**
* Get the human readable size of the given datastream.
*

18
islandora.api.php

@ -86,7 +86,7 @@ function hook_CMODEL_PID_islandora_view_object_alter(&$object, &$rendered) {
}
/**
* Generate an object's management display.
* Generate an object's datastreams management display.
*
* @param AbstractObject $object
* A Tuque FedoraObject
@ -98,7 +98,7 @@ function hook_islandora_edit_object($object) {
}
/**
* Generate an object's management display for the given content model.
* Generate an object's datastreams management display based on content model.
*
* Content models PIDs have colons and hyphens changed to underscores, to
* create the hook name.
@ -113,7 +113,7 @@ function hook_CMODEL_PID_islandora_edit_object($object) {
}
/**
* Allow management display output to be altered.
* Allow datastreams management display output to be altered.
*
* @param AbstractObject $object
* A Tuque FedoraObject
@ -648,3 +648,15 @@ function hook_islandora_derivative() {
function hook_CMODEL_PID_islandora_derivative() {
}
/**
* Alters breadcrumbs used on Solr search results and within Islandora views.
*
* @param array $breadcrumbs
* Breadcrumbs array to be altered by reference. Each element is markup.
* @param string $context
* Where the alter is originating from for distinguishing.
*/
function hook_islandora_breadcrumbs_alter(&$breadcrumbs, $context) {
}

1
islandora.info

@ -18,5 +18,6 @@ files[] = tests/ingest.test
files[] = tests/hooked_access.test
files[] = tests/islandora_manage_permissions.test
files[] = tests/datastream_versions.test
files[] = tests/datastream_cache.test
files[] = tests/derivatives.test
php = 5.3

42
islandora.module

@ -119,13 +119,22 @@ function islandora_menu() {
'access callback' => 'islandora_object_access_callback',
'access arguments' => array(ISLANDORA_VIEW_OBJECTS, 2),
);
$items['islandora/object/%islandora_object/print'] = array(
$items['islandora/object/%islandora_object/print_object'] = array(
'page callback' => 'islandora_printer_object',
'page arguments' => array(2),
'type' => MENU_NORMAL_ITEM,
'access callback' => 'islandora_object_access_callback',
'access callback' => 'islandora_object_access',
'access arguments' => array(ISLANDORA_VIEW_OBJECTS, 2),
);
$items['islandora/object/%islandora_object/print'] = array(
'title' => 'Print Object',
'page callback' => 'islandora_print_object',
'page arguments' => array(2),
'type' => MENU_CALLBACK,
'access callback' => 'islandora_object_access',
'access arguments' => array(ISLANDORA_VIEW_OBJECTS, 2),
'load arguments' => array(2),
);
$items['islandora/object/%islandora_object/view'] = array(
'title' => 'View',
'type' => MENU_DEFAULT_LOCAL_TASK,
@ -151,13 +160,11 @@ function islandora_menu() {
ISLANDORA_INGEST,
), 2),
);
$items['islandora/object/%islandora_object/manage/overview'] = array(
'title' => 'Overview',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -20,
);
$items['islandora/object/%islandora_object/manage/datastreams'] = array(
'title' => 'Datastreams',
'type' => MENU_LOCAL_TASK,
@ -172,7 +179,6 @@ function islandora_menu() {
), 2),
'weight' => -10,
);
$items['islandora/object/%islandora_object/manage/properties'] = array(
'title' => 'Properties',
'page callback' => 'drupal_get_form',
@ -297,15 +303,6 @@ function islandora_menu() {
'access arguments' => array(ISLANDORA_VIEW_DATASTREAM_HISTORY, 4),
'load arguments' => array(2),
);
$items['islandora/object/%islandora_object/printer'] = array(
'title' => 'Print Object',
'page callback' => 'islandora_print_object',
'page arguments' => array(2),
'type' => MENU_CALLBACK,
'access callback' => 'islandora_object_access',
'access arguments' => array(FEDORA_VIEW_OBJECTS, 2),
'load arguments' => array(2),
);
$items['islandora/object/%islandora_object/download_clip'] = array(
'page callback' => 'islandora_download_clip',
'page arguments' => array(2),
@ -350,12 +347,6 @@ function islandora_theme() {
'variables' => array('islandora_object' => NULL),
),
// Default edit page.
'islandora_default_print' => array(
'file' => 'theme/theme.inc',
'template' => 'theme/islandora-object-print',
'variables' => array('islandora_object' => NULL),
),
// Default edit page.
'islandora_default_edit' => array(
'file' => 'theme/theme.inc',
'template' => 'theme/islandora-object-edit',
@ -366,8 +357,13 @@ function islandora_theme() {
'file' => 'includes/solution_packs.inc',
'render element' => 'form',
),
// Print object view.
// Print used by the clipper.
'islandora_object_print' => array(
'file' => 'theme/theme.inc',
'variables' => array('object' => NULL, 'content' => array()),
),
// Print object view, prints islandora objects.
'islandora_object_print_object' => array(
'file' => 'theme/theme.inc',
'template' => 'theme/islandora-object-print',
'variables' => array(
@ -866,7 +862,7 @@ function islandora_view_object(AbstractObject $object) {
drupal_add_js(array(
'islandora' => array(
'print_link' => 'islandora/object/' . $object->id . '/print')),
'print_link' => 'islandora/object/' . $object->id . '/print_object')),
array('type' => 'setting'));
drupal_add_js($path . '/js/add_print.js');
@ -999,7 +995,7 @@ function islandora_default_islandora_printer_object($object, $alter) {
}
$variables = isset($dc_object) ? $dc_object->asArray() : array();
$output = theme('islandora_object_print', array(
$output = theme('islandora_object_print_object', array(
'object' => $object,
'dc_array' => $variables,
'islandora_content' => $alter));

160
tests/datastream_cache.test

@ -0,0 +1,160 @@
<?php
/**
* @file
* Tests to verify the cache headers we provide.
*/
class IslandoraDatastreamCacheTestCase extends IslandoraWebTestCase {
/**
* Gets info to display to describe this test.
*
* @see IslandoraWebTestCase::getInfo()
*/
public static function getInfo() {
return array(
'name' => 'Datastream Cache Headers',
'description' => 'Check our headers work as we expect them to.',
'group' => 'Islandora',
);
}
/**
* Creates an admin user and a connection to a fedora repository.
*
* @see IslandoraWebTestCase::setUp()
*/
public function setUp() {
parent::setUp();
$this->repository = $this->admin->repository;
$this->purgeTestObjects();
}
/**
* Free any objects/resources created for this test.
*
* @see IslandoraWebTestCase::tearDown()
*/
public function tearDown() {
$this->purgeTestObjects();
parent::tearDown();
}
/**
* Purge any objects created by the test's in this class.
*/
public function purgeTestObjects() {
$objects = array(
'test:test',
);
foreach ($objects as $object) {
try {
$object = $this->repository->getObject($object);
$this->repository->purgeObject($object->id);
}
catch (Exception $e) {
// Meh... Either it didn't exist or the purge failed.
}
}
}
/**
* Create our test object.
*/
protected function createTestObject() {
$object = $this->repository->constructObject('test:test');
$object->label = 'Test object';
$object->models = 'test:model';
$datastream = $object->constructDatastream('asdf', 'M');
$datastream->label = 'datastream of doom';
$datastream->mimetype = 'text/plain';
$datastream->content = 'And then things happened.';
$datastream->checksumType = 'SHA-1';
$object->ingestDatastream($datastream);
$this->repository->ingestObject($object);
return $object;
}
/**
* Test HTTP cache headers.
*/
public function testCacheHeaders() {
$object = $this->createTestObject();
$datastream = $object['asdf'];
$user = $this->drupalCreateUser(array(ISLANDORA_VIEW_OBJECTS));
$this->drupalLogin($user);
// Test If-Modified-Since.
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
'If-Modified-Since: ' . $datastream->createdDate->format('D, d M Y H:i:s \G\M\T'),
));
$this->assertResponse(304);
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
'If-Modified-Since: ' . $datastream->createdDate->sub(new DateInterval('P1M'))->format('D, d M Y H:i:s \G\M\T'),
));
$this->assertResponse(200);
// Test If-Unmodified-Since.
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
'If-Unmodified-Since: ' . $datastream->createdDate->format('D, d M Y H:i:s \G\M\T'),
));
$this->assertResponse(200);
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
'If-Unmodified-Since: ' . $datastream->createdDate->sub(new DateInterval('P1M'))->format('D, d M Y H:i:s \G\M\T'),
));
$this->assertResponse(412);
// Test If-Match.
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
format_string('If-Match: "!checksum"', array(
'!checksum' => $datastream->checksum,
)),
));
$this->assertResponse(200);
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
format_string('If-Match: "!checksum"', array(
'!checksum' => 'dont-match' . $datastream->checksum,
)),
));
$this->assertResponse(412);
// Test If-None-Match.
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
format_string('If-None-Match: "!checksum"', array(
'!checksum' => $datastream->checksum,
)),
));
$this->assertResponse(304);
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
format_string('If-None-Match: "!checksum"', array(
'!checksum' => 'dont-match' . $datastream->checksum,
)),
));
$this->assertResponse(200);
// Test combination of If-None-Match and If-Modified-Since
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
'If-Modified-Since: ' . $datastream->createdDate->format('D, d M Y H:i:s \G\M\T'),
format_string('If-None-Match: "!checksum"', array(
'!checksum' => $datastream->checksum,
)),
));
$this->assertResponse(304);
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
'If-Modified-Since: ' . $datastream->createdDate->format('D, d M Y H:i:s \G\M\T'),
format_string('If-None-Match: "!checksum"', array(
'!checksum' => 'dont-match' . $datastream->checksum,
)),
));
$this->assertResponse(200);
$result = $this->drupalGet("islandora/object/{$object->id}/datastream/{$datastream->id}/view", array(), array(
'If-Modified-Since: ' . $datastream->createdDate->sub(new DateInterval('P1M'))->format('D, d M Y H:i:s \G\M\T'),
format_string('If-None-Match: "!checksum"', array(
'!checksum' => $datastream->checksum,
)),
));
$this->assertResponse(200);
}
}
Loading…
Cancel
Save