mimetype); if ($datastream->controlGroup == 'M' || $datastream->controlGroup == 'X') { header('Content-length: ' . $datastream->size); } if ($download) { // Browsers will not append all extensions. $extension = '.' . islandora_get_extension_for_mimetype($datastream->mimetype); // Prevent adding on a duplicate extension. $label = $datastream->label; $extension_length = strlen($extension); $duplicate_extension_position = strlen($label) > $extension_length ? strripos($label, $extension, -$extension_length) : FALSE; $filename = $label; if ($duplicate_extension_position === FALSE) { $filename .= $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); // New content needed. if ($cache_check === 200) { // We need to see if the chunking is being requested. This will mainly // happen with iOS video requests as they do not support any other way // to receive content for playback. $chunk_headers = FALSE; if (isset($_SERVER['HTTP_RANGE'])) { // Set headers specific to chunking. $chunk_headers = islandora_view_datastream_set_chunk_headers($datastream); } // Try not to load the file into PHP memory! // Close and flush ALL the output buffers! while (@ob_end_flush()) { }; if (isset($_SERVER['HTTP_RANGE'])) { if ($chunk_headers) { islandora_view_datastream_deliver_chunks($datastream, $chunk_headers); } } else { $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. * * @param AbstractDatastream $datastream * The datastream to check. * * @return string * A human readable size of the given datastream, or '-' if the size could not * be determined. */ function islandora_datastream_get_human_readable_size(AbstractDatastream $datastream) { module_load_include('inc', 'islandora', 'includes/utilities'); $size_is_calculatable = $datastream->controlGroup == 'M' || $datastream->controlGroup == 'X'; return $size_is_calculatable ? islandora_convert_bytes_to_human_readable($datastream->size) : '-'; } /** * Get either the 'view' or 'download' url for the given datastream if possible. * * @param AbstractDatastream $datastream * The datastream to generated the url to. * @param string $type * One of: * - download * - view * @param int $version * (Optional) The version of the datastream to get a URL for. * * @return string * either the 'view' or 'download' url for the given datastream. */ function islandora_datastream_get_url(AbstractDatastream $datastream, $type = 'download', $version = NULL) { if ($version === NULL) { $link = "islandora/object/{$datastream->parent->id}/datastream/{$datastream->id}/$type"; } else { $link = "islandora/object/{$datastream->parent->id}/datastream/{$datastream->id}/version/$version/$type"; $datastream = $datastream[$version]; } if ($datastream->controlGroup == 'R') { return $datastream->url; } else { return $link; } } /** * Display the edit datastream page. * * @param AbstractDatastream $datastream * The datastream to edit. */ function islandora_edit_datastream(AbstractDatastream $datastream) { $edit_registry = module_invoke_all('islandora_edit_datastream_registry', $datastream->parent, $datastream); $context = array( 'datastream_parent' => $datastream->parent, 'datastream' => $datastream, 'original_edit_registry' => $edit_registry, ); drupal_alter(ISLANDORA_EDIT_DATASTREAM_MODIFY_REGISTRY_HOOK, $edit_registry, $context); $edit_count = count($edit_registry); switch ($edit_count) { case 0: // No edit implementations. drupal_set_message(t('There are no edit methods specified for this datastream.')); drupal_goto("islandora/object/{$datastream->parent->id}/manage/datastreams"); break; case 1: // One registry implementation, go there. $edit_registry = reset($edit_registry); drupal_goto($edit_registry['url']); break; default: // Multiple edit routes registered. return islandora_edit_datastream_registry_render($edit_registry); } } /** * Displays links to all the edit datastream registry items. * * @param array $edit_registry * A list of 'islandora_edit_datastream_registry' values. * * @return array * A Drupal renderable array containing the "edit" markup. */ function islandora_edit_datastream_registry_render(array $edit_registry) { $markup = ''; foreach ($edit_registry as $edit_route) { $markup .= l($edit_route['name'], $edit_route['url']) . '
'; } return array( '#type' => 'markup', '#markup' => $markup, ); } /** * Set the headers for the chunking of our content. * * @param AbstractDatastream $datastream * An AbstractDatastream representing a datastream on a Fedora object. * * @return bool * TRUE if there are chunks to be returned, FALSE otherwise. */ function islandora_view_datastream_set_chunk_headers(AbstractDatastream $datastream) { $file_uri = islandora_view_datastream_retrieve_file_uri($datastream); // The meat of this has been taken from: // http://mobiforge.com/design-development/content-delivery-mobile-devices. $size = filesize($file_uri); $length = $size; $start = 0; $end = $size - 1; header("Accept-Ranges: 0-$length"); if (isset($_SERVER['HTTP_RANGE'])) { $c_start = $start; $c_end = $end; // Extract the range string. list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); // Make sure the client hasn't sent us a multibyte range. if (strpos($range, ',') !== FALSE) { // Not a valid range, notify the client. header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $start-$end/$size"); exit; } // If the range starts with an '-' we start from the beginning. If not, we // forward the file pointer and make sure to get the end byte if specified. if (strpos($range, '-') === 0) { // The n-number of the last bytes is requested. $c_start = $size - substr($range, 1); } else { $range = explode('-', $range); $c_start = $range[0]; $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; } /* Check the range and make sure it's treated according to the specs. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html */ // End bytes can not be larger than $end. $c_end = ($c_end > $end) ? $end : $c_end; // Validate the requested range and return an error if it's not correct. if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $start-$end/$size"); exit; } $start = $c_start; $end = $c_end; // Calculate new content length. $length = $end - $start + 1; header('HTTP/1.1 206 Partial Content'); } // Notify the client the byte range we'll be outputting. header("Content-Range: bytes $start-$end/$size"); header("Content-Length: $length"); return array( 'start' => $start, 'end' => $end, ); } /** * Deliver back the specified chunks of a file. * * @param AbstractDatastream $datastream * An AbstractDatastream representing a datastream on a Fedora object. * @param array $params * An associate array containing the start and ending chunk bytes. */ function islandora_view_datastream_deliver_chunks(AbstractDatastream $datastream, $params) { $file_uri = islandora_view_datastream_retrieve_file_uri($datastream); // The meat of this has been taken from: // http://mobiforge.com/design-development/content-delivery-mobile-devices. $fp = @fopen($file_uri, 'rb'); fseek($fp, $params['start']); // Start buffered download. $buffer = 1024 * 8; while (!feof($fp) && ($p = ftell($fp)) <= $params['end']) { if ($p + $buffer > $params['end']) { // In case we're only outputting a chunk, make sure we don't read past the // length. $buffer = $params['end'] - $p + 1; } // Reset time limit for big files. set_time_limit(0); echo fread($fp, $buffer); } fclose($fp); } /** * Creates/returns the file URI for the content of a datastream for chunking. * * @param AbstractDatastream $datastream * An AbstractDatastream representing a datastream on a Fedora object. * * @return string * The URI of the file. */ function islandora_view_datastream_retrieve_file_uri(AbstractDatastream $datastream) { module_load_include('inc', 'islandora', 'includes/mimetype.utils'); $extension = islandora_get_extension_for_mimetype($datastream->mimetype); $file_uri = 'temporary://chunk_' . $datastream->parent->id . '_' . $datastream->id . '_' . $datastream->createdDate->getTimestamp() . '.' . $extension; if (!file_exists($file_uri)) { $file = new stdClass(); $file->uri = $file_uri; $file->filename = drupal_basename($file_uri); $file->filemime = $datastream->mimeType; $file->status = 0; $datastream->getContent($file_uri); file_save($file); } return $file_uri; }