mirador
Version:
An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.
226 lines (196 loc) • 7.62 kB
JavaScript
import { all, call, put, select, takeEvery } from 'redux-saga/effects';
import { Utils } from 'manifesto.js';
import normalizeUrl from 'normalize-url';
import ActionTypes from '../actions/action-types';
import {
receiveManifest,
receiveManifestFailure,
receiveInfoResponse,
receiveInfoResponseFailure,
receiveDegradedInfoResponse,
receiveSearch,
receiveSearchFailure,
receiveAnnotation,
receiveAnnotationFailure,
} from '../actions';
import { getManifests, getRequestsConfig, getAccessTokens, selectInfoResponse } from '../selectors';
/** */
function fetchWrapper(url, options, { success, degraded, failure }) {
return fetch(url, options)
.then((response) =>
response
.json()
.then((json) => {
if (response.status === 401) return (degraded || success)({ json, response });
if (response.ok) return success({ json, response });
return failure({ error: response.statusText, json, response });
})
.catch((error) => failure({ error, response })),
)
.catch((error) => failure({ error }));
}
/** */
function* fetchIiifResource(url, options, { success, degraded, failure }) {
const { preprocessors = [], postprocessors = [] } = yield select(getRequestsConfig);
try {
const reqOptions = preprocessors.reduce((acc, f) => f(url, acc) || acc, options);
let action = yield call(fetchWrapper, url, reqOptions, { degraded, failure, success });
action = postprocessors.reduce((acc, f) => f(url, acc) || acc, action);
return action;
} catch (error) {
return failure({ error });
}
}
/** */
function* fetchIiifResourceWithAuth(url, iiifResource, options, { degraded, failure, success }) {
const urlOptions = { ...options };
let tokenServiceId;
// If we have a requested IIIF resource (say, the image description from the manifest)
// we can optimistically try an appropriate access token.
//
// TODO: there might be multiple applicable access token services
if (iiifResource) {
const tokenService = yield call(getAccessTokenService, iiifResource);
tokenServiceId = tokenService && tokenService.id;
if (tokenService && tokenService.json) {
urlOptions.headers = {
Authorization: `Bearer ${tokenService.json.accessToken}`,
...options.headers,
};
}
}
const { error, json, response } = yield call(fetchIiifResource, url, urlOptions, {
failure: (arg) => arg,
success: (arg) => arg,
});
// Hard error either requesting the resource or deserializing the JSON.
if (error) {
yield put(
failure({
error,
json,
response,
tokenServiceId,
}),
);
return;
}
const id = json['@id'] || json.id;
if (response.ok) {
if (
normalizeUrl(id, { stripAuthentication: false }) ===
normalizeUrl(url.replace(/info\.json$/, ''), { stripAuthentication: false })
) {
yield put(success({ json, response, tokenServiceId }));
return;
}
} else if (response.status !== 401) {
yield put(
failure({
error,
json,
response,
tokenServiceId,
}),
);
return;
}
// Start attempting some IIIF Auth;
// First, the IIIF resource we were given may not be authoritative; check if
// it suggests a different access token service and re-enter the auth workflow
const authoritativeTokenService = yield call(getAccessTokenService, json);
if (authoritativeTokenService && authoritativeTokenService.id !== tokenServiceId) {
yield call(fetchIiifResourceWithAuth, url, json, options, { degraded, failure, success });
return;
}
// Record the response (potentially kicking off other auth flows)
yield put((degraded || success)({ json, response, tokenServiceId }));
}
/** */
export function* fetchManifest({ manifestId }) {
const callbacks = {
failure: ({ error, json, response }) => receiveManifestFailure(manifestId, typeof error === 'object' ? String(error) : error),
success: ({ json, response }) => receiveManifest(manifestId, json),
};
const dispatch = yield call(fetchIiifResource, manifestId, {}, callbacks);
yield put(dispatch);
}
/** @private */
function* getAccessTokenService(resource) {
const manifestoCompatibleResource = resource && resource.__jsonld ? resource : { ...resource, options: {} };
const services = Utils.getServices(manifestoCompatibleResource).filter((s) =>
s.getProfile().match(/http:\/\/iiif.io\/api\/auth\//),
);
if (services.length === 0) return undefined;
const accessTokens = yield select(getAccessTokens);
if (!accessTokens) return undefined;
for (let i = 0; i < services.length; i += 1) {
const authService = services[i];
const accessTokenService =
Utils.getService(authService, 'http://iiif.io/api/auth/1/token') ||
Utils.getService(authService, 'http://iiif.io/api/auth/0/token');
const token = accessTokenService && accessTokens[accessTokenService.id];
if (token && token.json) return token;
}
return undefined;
}
/** @private */
export function* fetchInfoResponse({ imageResource, infoId, windowId }) {
let iiifResource = imageResource;
if (!iiifResource) {
iiifResource = yield select(selectInfoResponse, { infoId });
}
const callbacks = {
degraded: ({ json, response, tokenServiceId }) =>
receiveDegradedInfoResponse(infoId, json, response.ok, tokenServiceId, windowId),
failure: ({ error, json, response, tokenServiceId }) => receiveInfoResponseFailure(infoId, error, tokenServiceId),
success: ({ json, response, tokenServiceId }) => receiveInfoResponse(infoId, json, response.ok, tokenServiceId),
};
yield call(fetchIiifResourceWithAuth, `${infoId.replace(/\/$/, '')}/info.json`, iiifResource, {}, callbacks);
}
/** @private */
export function* fetchSearchResponse({ windowId, companionWindowId, query, searchId }) {
const callbacks = {
failure: ({ error, json, response }) => receiveSearchFailure(windowId, companionWindowId, searchId, error),
success: ({ json, response }) => receiveSearch(windowId, companionWindowId, searchId, json),
};
const dispatch = yield call(fetchIiifResource, searchId, {}, callbacks);
yield put(dispatch);
}
/** @private */
export function* fetchAnnotation({ targetId, annotationId }) {
const callbacks = {
failure: ({ error, json, response }) => receiveAnnotationFailure(targetId, annotationId, error),
success: ({ json, response }) => receiveAnnotation(targetId, annotationId, json),
};
const dispatch = yield call(fetchIiifResource, annotationId, {}, callbacks);
yield put(dispatch);
}
/** */
export function* fetchResourceManifest({ manifestId, manifestJson }) {
if (manifestJson) {
yield put(receiveManifest(manifestId, manifestJson));
return;
}
if (!manifestId) return;
const manifests = yield select(getManifests) || {};
if (!manifests[manifestId]) yield* fetchManifest({ manifestId });
}
/** */
export function* fetchManifests(...manifestIds) {
const manifests = yield select(getManifests);
for (let i = 0; i < manifestIds.length; i += 1) {
const manifestId = manifestIds[i];
if (!manifests[manifestId]) yield call(fetchManifest, { manifestId });
}
}
/** */
export default function* iiifSaga() {
yield all([
takeEvery(ActionTypes.REQUEST_MANIFEST, fetchManifest),
takeEvery(ActionTypes.REQUEST_INFO_RESPONSE, fetchInfoResponse),
takeEvery(ActionTypes.REQUEST_SEARCH, fetchSearchResponse),
takeEvery(ActionTypes.REQUEST_ANNOTATION, fetchAnnotation),
takeEvery(ActionTypes.ADD_RESOURCE, fetchResourceManifest),
]);
}