Repository: ProjectMirador/mirador (js)

Issue:
Implementation of fetchInfoResponse is not complient with iiif
Like @stephenwf mentioned in https://github.com/ProjectMirador/mirador/issues/3552#issuecomment-1112335225

----

I think the code mentioned:

```
/**
 * fetchInfoResponse - action creator
 *
 * @param  {String} infoId
 * @memberof ActionCreators
 */
export function fetchInfoResponse({ imageId, imageResource, windowId }) {
  const imageService = imageResource && imageResource.getServices()[0];
  const infoId = (imageId || imageService.id);
  return requestInfoResponse(infoId, imageService, windowId);
}
```

is still prone to bugs. A completely valid Image resource for example, could break:

```
{
  "id": "https://example.org/path/image.jpg",
  "type": "Image",
  "service": [
     { "type": "Service", "profile": "my-dimensions", " .... " },
     { "type": "ImageService3", "profile": "level2", "...." }
  ]
}
```

As the code is not looking for an image service from the list, just the first service:

`imageResource.getServices()[0]`

I think Manifesto just surfaced the bug.


----

Pull request description:
Handle multiple IIIF resource services; fixes #3553

Patch:
diff --git a/src/lib/CanvasWorld.js b/src/lib/CanvasWorld.js
index e2865ca5..d506d9f3 100644
--- a/src/lib/CanvasWorld.js
+++ b/src/lib/CanvasWorld.js
@@ -1,5 +1,5 @@
 import normalizeUrl from 'normalize-url';
-import MiradorCanvas from './MiradorCanvas';
+import { getIiifResourceImageService } from './iiif';
 
 /**
  * CanvasWorld
@@ -141,7 +141,7 @@ export default class CanvasWorld {
     if (!miradorCanvas) return undefined;
     return miradorCanvas.imageResources
       .find(r => (
-        normalizeUrl(r.getServices()[0].id, { stripAuthentication: false })
+        normalizeUrl(getIiifResourceImageService(r).id, { stripAuthentication: false })
         === normalizeUrl(infoResponseId, { stripAuthentication: false })));
   }
 
diff --git a/src/lib/MiradorCanvas.js b/src/lib/MiradorCanvas.js
index 6d367cf9..c34958e7 100644
--- a/src/lib/MiradorCanvas.js
+++ b/src/lib/MiradorCanvas.js
@@ -1,6 +1,8 @@
 import flatten from 'lodash/flatten';
 import flattenDeep from 'lodash/flattenDeep';
 import { Canvas, AnnotationPage, Annotation } from 'manifesto.js';
+import { getIiifResourceImageService } from './iiif';
+
 /**
  * MiradorCanvas - adds additional, testable logic around Manifesto's Canvas
  * https://iiif-commons.github.io/manifesto/classes/_canvas_.manifesto.canvas.html
@@ -189,13 +191,12 @@ export default class MiradorCanvas {
 
   /** */
   get iiifImageResources() {
-    return this.imageResources
-      .filter(r => r && r.getServices()[0] && r.getServices()[0].id);
+    return this.imageResources.filter(r => r && getIiifResourceImageService(r)?.id);
   }
 
   /** */
   get imageServiceIds() {
-    return this.iiifImageResources.map(r => r.getServices()[0].id);
+    return this.iiifImageResources.map(r => r && getIiifResourceImageService(r)?.id);
   }
 
   /**
diff --git a/src/lib/iiif.js b/src/lib/iiif.js
new file mode 100644
index 00000000..a8f76f5e
--- /dev/null
+++ b/src/lib/iiif.js
@@ -0,0 +1,7 @@
+import { Utils } from 'manifesto.js';
+
+/** @returns an IIIF image service for the given resource */
+export function getIiifResourceImageService(resource) {
+  return resource.getServices().find(service => (
+    (service.getProfile && Utils.isImageProfile(service.getProfile() || '')) || Utils.isImageServiceType(service.getIIIFResourceType() || '')));
+}
diff --git a/src/state/actions/infoResponse.js b/src/state/actions/infoResponse.js
index b990a9c8..915a6706 100644
--- a/src/state/actions/infoResponse.js
+++ b/src/state/actions/infoResponse.js
@@ -1,3 +1,4 @@
+import { getIiifResourceImageService } from '../../lib/iiif';
 import ActionTypes from './action-types';
 
 /**
@@ -73,7 +74,7 @@ export function receiveInfoResponseFailure(infoId, error, tokenServiceId) {
  * @memberof ActionCreators
  */
 export function fetchInfoResponse({ imageId, imageResource, windowId }) {
-  const imageService = imageResource && imageResource.getServices()[0];
+  const imageService = imageResource && getIiifResourceImageService(imageResource);
   const infoId = (imageId || imageService.id);
   return requestInfoResponse(infoId, imageService, windowId);
 }
diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js
index a1d7669c..f8dd591c 100644
--- a/src/state/sagas/windows.js
+++ b/src/state/sagas/windows.js
@@ -2,8 +2,6 @@ import {
   all, call, put, select, takeEvery,
 } from 'redux-saga/effects';
 import ActionTypes from '../actions/action-types';
-import MiradorManifest from '../../lib/MiradorManifest';
-import MiradorCanvas from '../../lib/MiradorCanvas';
 import {
   setContentSearchCurrentAnnotation,
   selectAnnotation,
@@ -32,6 +30,7 @@ import {
   getMiradorManifestWrapper,
 } from '../selectors';
 import { fetchManifests } from './iiif';
+import { getIiifResourceImageService } from '../../lib/iiif';
 
 /** */
 export function* fetchWindowManifest(action) {
@@ -248,7 +247,7 @@ export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowI
   yield all(visibleCanvases.map((canvas) => {
     const miradorCanvas = getMiradorCanvas(canvas);
     return all(miradorCanvas.iiifImageResources.map(imageResource => (
-      !infoResponses[imageResource.getServices()[0].id]
+      !infoResponses[getIiifResourceImageService(imageResource)?.id]
         && put(fetchInfoResponse({ imageResource, windowId }))
     )).filter(Boolean));
   }));
diff --git a/src/state/selectors/auth.js b/src/state/selectors/auth.js
index 5f4f73c0..d24e02ef 100644
--- a/src/state/selectors/auth.js
+++ b/src/state/selectors/auth.js
@@ -1,10 +1,11 @@
 import { createSelector } from 'reselect';
-import { Utils } from 'manifesto.js';
 import flatten from 'lodash/flatten';
+import { Utils } from 'manifesto.js';
 import { miradorSlice, EMPTY_ARRAY, EMPTY_OBJECT } from './utils';
 import { getConfig } from './config';
 import { getVisibleCanvases, selectInfoResponses } from './canvases';
 import { getMiradorCanvasWrapper } from './wrappers';
+import { getIiifResourceImageService } from '../../lib/iiif';
 
 /**
  * Returns the authentification profile from the configuration
@@ -56,7 +57,7 @@ export const selectCurrentAuthServices = createSelector(
         const images = miradorCanvas.iiifImageResources;
 
         return images.map(i => {
-          const iiifImageService = i.getServices()[0];
+          const iiifImageService = getIiifResourceImageService(i);
 
           const infoResponse = infoResponses[iiifImageService.id];
           if (infoResponse && infoResponse.json) {
diff --git a/src/state/selectors/canvases.js b/src/state/selectors/canvases.js
index a727ef86..b213f3a5 100644
--- a/src/state/selectors/canvases.js
+++ b/src/state/selectors/canvases.js
@@ -1,6 +1,7 @@
 import { createSelector } from 'reselect';
 import flatten from 'lodash/flatten';
 import CanvasGroupings from '../../lib/CanvasGroupings';
+import { getIiifResourceImageService } from '../../lib/iiif';
 import { getMiradorCanvasWrapper } from './wrappers';
 import { miradorSlice, EMPTY_ARRAY } from './utils';
 import { getWindow } from './getters';
@@ -215,7 +216,7 @@ export const getVisibleCanvasNonTiledResources = createSelector(
   ],
   (canvases, getMiradorCanvas) => flatten(canvases
     .map(canvas => getMiradorCanvas(canvas).imageResources))
-    .filter(resource => resource.getServices().length < 1),
+    .filter(resource => !getIiifResourceImageService(resource)),
 );
 
 /**
@@ -304,7 +305,7 @@ export const selectInfoResponse = createSelector(
     if (!infoId) {
       if (!canvas) return undefined;
       const image = getMiradorCanvas(canvas).iiifImageResources[0];
-      iiifServiceId = image && image.getServices()[0].id;
+      iiifServiceId = image && getIiifResourceImageService(image)?.id;
     }
 
     return iiifServiceId && infoResponses[iiifServiceId]


Test patch:
diff --git a/__tests__/src/actions/infoResponse.test.js b/__tests__/src/actions/infoResponse.test.js
index f7c20c3d..be101d4d 100644
--- a/__tests__/src/actions/infoResponse.test.js
+++ b/__tests__/src/actions/infoResponse.test.js
@@ -30,9 +30,8 @@ describe('infoResponse actions', () => {
   describe('fetchInfoResponse', () => {
     describe('success response', () => {
       it('dispatches the REQUEST_INFO_RESPONSE action', () => {
-        const imageResource = { getServices: () => ['service'] };
-        expect(actions.fetchInfoResponse({ imageId: 'someUrl', imageResource })).toEqual({
-          imageResource: 'service',
+        const imageResource = { getServices: () => [{ getIIIFResourceType: () => 'ImageService2', getProfile: () => '' }] };
+        expect(actions.fetchInfoResponse({ imageId: 'someUrl', imageResource })).toMatchObject({
           infoId: 'someUrl',
           type: 'mirador/REQUEST_INFO_RESPONSE',
         });
diff --git a/__tests__/src/lib/iiif.test.js b/__tests__/src/lib/iiif.test.js
new file mode 100644
index 00000000..6c343b1a
--- /dev/null
+++ b/__tests__/src/lib/iiif.test.js
@@ -0,0 +1,16 @@
+import { Service } from 'manifesto.js';
+import { getIiifResourceImageService } from '../../../src/lib/iiif';
+
+describe('IIIF utils', () => {
+  it('extracts the IIIF image service from the resource', () => {
+    const subject = {
+      getServices: () => [
+        new Service({ id: 'some-service', type: 'SearchService1' }),
+        new Service({ id: 'image-service', type: 'ImageService2' }),
+      ],
+    };
+    const service = getIiifResourceImageService(subject);
+
+    expect(service.id).toEqual('image-service');
+  });
+});