diff --git a/islandora_mirador.module b/islandora_mirador.module index c1a8bbe..f381f47 100644 --- a/islandora_mirador.module +++ b/islandora_mirador.module @@ -35,6 +35,11 @@ function islandora_mirador_theme() { function template_preprocess_mirador(&$variables) { $variables['mirador_view_id'] = Html::getUniqueId($variables['mirador_view_id']); + if (!empty(\Drupal::hasService('jwt.authentication.jwt'))) { + $variables['#attached']['drupalSettings']['token'] = \Drupal::service('jwt.authentication.jwt')->generateToken(); + } + + /** * @var \Drupal\islandora_mirador\IslandoraMiradorPluginManager */ diff --git a/islandora_mirador.routing.yml b/islandora_mirador.routing.yml index 9d27632..e07167d 100644 --- a/islandora_mirador.routing.yml +++ b/islandora_mirador.routing.yml @@ -5,3 +5,9 @@ islandora_mirador.miradorconfig: _title: 'Mirador Settings' requirements: _permission: 'administer site configuration' +islandora_mirador.service_worker: + path: '/islandora_mirador_service_worker' + defaults: + _controller: '\Drupal\islandora_mirador\Controller\ServiceWorkerController::serve' + requirements: + _permission: 'access content' diff --git a/js/mirador_viewer.js b/js/mirador_viewer.js index c520e40..07727ed 100644 --- a/js/mirador_viewer.js +++ b/js/mirador_viewer.js @@ -16,12 +16,55 @@ Drupal.IslandoraMirador.instances = Drupal.IslandoraMirador.instances || {} Object.entries(settings.mirador.viewers).forEach(entry => { const [base, values] = entry; - once('mirador-viewer', base, context).forEach(() => - // save the mirador instance so other modules can interact - // with the store/actions at e.g. Drupal.IslandoraMirador.instances["#mirador-xyz"].store + once('mirador-viewer', base, context, settings).forEach(() => { + if (settings.token !== undefined) { + values["resourceHeaders"] = { + 'Authorization': 'Bearer '+ settings.token, + 'token': settings.token + }; + values["requestPipeline"] = [ + (url, options) => ({ ...options, headers: { + "Accept": 'application/ld+json;profile="http://iiif.io/api/presentation/3/context.json"', + 'Authorization': 'Bearer '+ settings.token, + 'token': settings.token + }}) + ]; + values["osdConfig"] = { + "loadTilesWithAjax": true, + "ajaxHeaders": { + 'Authorization': 'Bearer '+ settings.token, + 'token': settings.token + } + }; + values["requests"] = { + preprocessors: [ // Functions that receive HTTP requests and manipulate them (e.g. to add headers) + // rewrite all info.json requests to add the text/json request header + (url, options) => (url.match('info.json') && { ...options, headers: { + 'Authorization': 'Bearer '+ settings.token, + 'token': settings.token + }}) + ], + }; + } Drupal.IslandoraMirador.instances[base] = Mirador.viewer(values, window.miradorPlugins || {}) - ); + }); }); + if (settings.token !== undefined) { + if ('serviceWorker' in navigator) { + // The Mirador viewer uses img tags for thumbnails so thumbnail image requests + // do not have authorization or token headers. Attach them using a service worker. + window.addEventListener('load', () => { + navigator.serviceWorker + .register('/islandora_mirador_service_worker?token=' + settings.token, { scope: '/' }) + .then(registration => { + console.log('ServiceWorker registration successful with scope: ', registration.scope); + }) + .catch(err => { + console.log('ServiceWorker registration failed: ', err); + }); + }); + } + } }, detach: function (context, settings) { Object.entries(settings.mirador.viewers).forEach(entry => { diff --git a/js/service_worker.js b/js/service_worker.js new file mode 100755 index 0000000..3299cb0 --- /dev/null +++ b/js/service_worker.js @@ -0,0 +1,21 @@ +self.addEventListener('activate', function (event) { + console.log('Service Worker: claiming control...'); + return self.clients.claim(); +}); + +self.addEventListener('fetch', function (event) { + if (event.request.destination === "image" && new URL(event.request.url).pathname.startsWith('/cantaloupe/iiif/') && new URL(location).searchParams.has('token')) { + console.log('Service Worker: fetching...'); + var token = new URL(location).searchParams.get('token'); + event.respondWith( + fetch(event.request, { + headers: { + 'Authorization': 'Bearer ' + token, + 'token': token + }, + mode: "cors", + credentials: "include" + }) + ); + } +}); diff --git a/scripts/delegates.rb b/scripts/delegates.rb new file mode 100755 index 0000000..95dd2d5 --- /dev/null +++ b/scripts/delegates.rb @@ -0,0 +1,249 @@ +## +# Sample Ruby delegate script containing stubs and documentation for all +# available delegate methods. See the user manual for more information. +# +# The application will create an instance of this class early in the request +# cycle and dispose of it at the end of the request cycle. Instances don't need +# to be thread-safe, but sharing information across instances (requests) +# **does** need to be done thread-safely. +# +# This version of the script works with Cantaloupe version 4, and not earlier +# versions. Likewise, earlier versions of the script are not compatible with +# Cantaloupe 4. +# +class CustomDelegate + + ## + # Attribute for the request context, which is a hash containing information + # about the current request. + # + # This attribute will be set by the server before any other methods are + # called. Methods can access its keys like: + # + # ``` + # identifier = context['identifier'] + # ``` + # + # The hash will contain the following keys in response to all requests: + # + # * `client_ip` [String] Client IP address. + # * `cookies` [Hash] Hash of cookie name-value pairs. + # * `identifier` [String] Image identifier. + # * `request_headers` [Hash] Hash of header name-value pairs. + # * `request_uri` [String] Public request URI. + # * `scale_constraint` [Array] Two-element array with scale + # constraint numerator at position 0 and denominator at + # position 1. + # + # It will contain the following additional string keys in response to image + # requests: + # + # * `full_size` [Hash] Hash with `width` and `height` + # keys corresponding to the pixel dimensions of the + # source image. + # * `operations` [Array>] Array of operations in + # order of application. Only operations that are not + # no-ops will be included. Every hash contains a `class` + # key corresponding to the operation class name, which + # will be one of the `e.i.l.c.operation.Operation` + # implementations. + # * `output_format` [String] Output format media (MIME) type. + # * `resulting_size` [Hash] Hash with `width` and `height` + # keys corresponding to the pixel dimensions of the + # resulting image after all operations have been applied. + # + # @return [Hash] Request context. + # + attr_accessor :context + + ## + # Returns authorization status for the current request. Will be called upon + # all requests to all public endpoints. + # + # Implementations should assume that the underlying resource is available, + # and not try to check for it. + # + # Possible return values: + # + # 1. Boolean true/false, indicating whether the request is fully authorized + # or not. If false, the client will receive a 403 Forbidden response. + # 2. Hash with a `status_code` key. + # a. If it corresponds to an integer from 200-299, the request is + # authorized. + # b. If it corresponds to an integer from 300-399: + # i. If the hash also contains a `location` key corresponding to a + # URI string, the request will be redirected to that URI using + # that code. + # ii. If the hash also contains `scale_numerator` and + # `scale_denominator` keys, the request will be + # redirected using that code to a virtual reduced-scale version of + # the source image. + # c. If it corresponds to 401, the hash must include a `challenge` key + # corresponding to a WWW-Authenticate header value. + # + # @param options [Hash] Empty hash. + # @return [Boolean,Hash] See above. + # + def authorize(options = {}) + true + end + + ## + # Used to add additional keys to an information JSON response. See the + # [Image API specification](http://iiif.io/api/image/2.1/#image-information). + # + # @param options [Hash] Empty hash. + # @return [Hash] Hash that will be merged into an IIIF Image API 2.x + # information response. Return an empty hash to add nothing. + # + def extra_iiif2_information_response_keys(options = {}) +=begin + Example: + { + 'attribution' => 'Copyright My Great Organization. All rights '\ + 'reserved.', + 'license' => 'http://example.org/license.html', + 'logo' => 'http://example.org/logo.png', + 'service' => { + '@context' => 'http://iiif.io/api/annex/services/physdim/1/context.json', + 'profile' => 'http://iiif.io/api/annex/services/physdim', + 'physicalScale' => 0.0025, + 'physicalUnits' => 'in' + } + } +=end + {} + end + + ## + # Tells the server which source to use for the given identifier. + # + # @param options [Hash] Empty hash. + # @return [String] Source name. + # + def source(options = {}) + end + + ## + # N.B.: this method should not try to perform authorization. `authorize()` + # should be used instead. + # + # @param options [Hash] Empty hash. + # @return [String,nil] Blob key of the image corresponding to the given + # identifier, or nil if not found. + # + def azurestoragesource_blob_key(options = {}) + end + + ## + # N.B.: this method should not try to perform authorization. `authorize()` + # should be used instead. + # + # @param options [Hash] Empty hash. + # @return [String,nil] Absolute pathname of the image corresponding to the + # given identifier, or nil if not found. + # + def filesystemsource_pathname(options = {}) + end + + ## + # Returns one of the following: + # + # 1. String URI + # 2. Hash with the following keys: + # * `uri` [String] (required) + # * `username` [String] For HTTP Basic authentication (optional). + # * `secret` [String] For HTTP Basic authentication (optional). + # * `headers` [Hash] Hash of request headers (optional). + # 3. nil if not found. + # + # N.B.: this method should not try to perform authorization. `authorize()` + # should be used instead. + # + # @param options [Hash] Empty hash. + # @return See above. + # + def httpsource_resource_info(options = {}) + return { "uri" => context['identifier'], "headers" => { "Authorization" => context['request_headers']['authorization'] } } + end + + ## + # N.B.: this method should not try to perform authorization. `authorize()` + # should be used instead. + # + # @param options [Hash] Empty hash. + # @return [String] Identifier of the image corresponding to the given + # identifier in the database. + # + def jdbcsource_database_identifier(options = {}) + end + + ## + # Returns either the media (MIME) type of an image, or an SQL statement that + # can be used to retrieve it, if it is stored in the database. In the latter + # case, the "SELECT" and "FROM" clauses should be in uppercase in order to + # be autodetected. If nil is returned, the media type will be inferred some + # other way, such as by identifier extension or magic bytes. + # + # @param options [Hash] Empty hash. + # @return [String, nil] + # + def jdbcsource_media_type(options = {}) + end + + ## + # @param options [Hash] Empty hash. + # @return [String] SQL statement that selects the BLOB corresponding to the + # value returned by `jdbcsource_database_identifier()`. + # + def jdbcsource_lookup_sql(options = {}) + end + + ## + # N.B.: this method should not try to perform authorization. `authorize()` + # should be used instead. + # + # @param options [Hash] Empty hash. + # @return [Hash,nil] Hash containing `bucket` and `key` keys; + # or nil if not found. + # + def s3source_object_info(options = {}) + end + + ## + # Tells the server what overlay, if any, to apply to an image in response + # to a request. Will be called upon all image requests to any endpoint if + # overlays are enabled and the overlay strategy is set to `ScriptStrategy` + # in the application configuration. + # + # N.B.: When a string overlay is too large or long to fit entirely within + # the image, it won't be drawn. Consider breaking long strings with LFs (\n). + # + # @param options [Hash] Empty hash. + # @return [Hash,nil] For image overlays, a hash with `image`, + # `position`, and `inset` keys. For string overlays, a hash with + # `background_color`, `color`, `font`, `font_min_size`, `font_size`, + # `font_weight`, `glyph_spacing`,`inset`, `position`, `string`, + # `stroke_color`, and `stroke_width` keys. + # Return nil for no overlay. + # + def overlay(options = {}) + puts "overlay" + end + + ## + # Tells the server what regions of an image to redact in response to a + # particular request. Will be called upon all image requests to any endpoint + # if redactions are enabled in the application configuration. + # + # @param options [Hash] Empty hash. + # @return [Array>] Array of hashes, each with `x`, `y`, + # `width`, and `height` keys; or an empty array if no redactions are + # to be applied. + # + def redactions(options = {}) + puts "redactions" + [] + end + +end diff --git a/src/Controller/ServiceWorkerController.php b/src/Controller/ServiceWorkerController.php new file mode 100644 index 0000000..fb677b6 --- /dev/null +++ b/src/Controller/ServiceWorkerController.php @@ -0,0 +1,58 @@ +extensionPathResolver = $extension_path_resolver; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('extension.path.resolver') + ); + } + + /** + * Adds headers to the HTTP response. + */ + public function serve(Request $request) { + $file_str = $this->extensionPathResolver->getPath('module', 'islandora_mirador') . '/js/service_worker.js'; + if (file_exists($file_str)) { + $response = new BinaryFileResponse($file_str, 200); + $response->headers->set('Content-Type', 'application/javascript'); + // Allow same origin service worker. + $response->headers->set('Service-Worker-Allowed', '/'); + return $response; + } + throw new NotFoundHttpException(); + } + +}