diff --git a/src/state/sagas/auth.js b/src/state/sagas/auth.js index 7cd2eafd3..3fe5dd81d 100644 --- a/src/state/sagas/auth.js +++ b/src/state/sagas/auth.js @@ -5,7 +5,7 @@ import { Utils } from 'manifesto.js'; import flatten from 'lodash/flatten'; import ActionTypes from '../actions/action-types'; import MiradorCanvas from '../../lib/MiradorCanvas'; -import { getTokenService } from '../../lib/getServices'; +import { getTokenService, getProbeService } from '../../lib/getServices'; import { addAuthenticationRequest, resolveAuthenticationRequest, @@ -14,13 +14,14 @@ import { } from '../actions'; import { selectInfoResponses, + selectProbeResponses, getVisibleCanvases, getWindows, getConfig, getAuth, getAccessTokens, } from '../selectors'; -import { fetchInfoResponse } from './iiif'; +import { fetchInfoResponse, fetchProbeResponse } from './iiif'; /** */ export function* refetchInfoResponsesOnLogout({ tokenServiceId }) { @@ -70,6 +71,54 @@ export function* refetchInfoResponses({ serviceId }) { })); } +/** */ +export function* refetchProbeResponsesOnLogout({ tokenServiceId }) { + // delay logout actions to give the cookie service a chance to invalidate our cookies + // before we reinitialize openseadragon and rerequest images. + + yield delay(2000); + yield call(refetchProbeResponses, { serviceId: tokenServiceId }); +} + +/** + * Figure out what probe responses could have used the access token service and: + * - refetch, if they are currently visible + * - throw them out (and lazy re-fetch) otherwise + */ +export function* refetchProbeResponses({ serviceId }) { + const windows = yield select(getWindows); + + const canvases = yield all( + Object.keys(windows).map(windowId => select(getVisibleCanvases, { windowId })), + ); + + const visibleProbeServiceIds = flatten(flatten(canvases).map((canvas) => { + const miradorCanvas = new MiradorCanvas(canvas); + return miradorCanvas.imageResources.filter((r) => getProbeService(r)).map((r) => getProbeService(r)); + })); + + const probeResponses = yield select(selectProbeResponses); + /** */ + const haveThisTokenService = probeResponse => { + const services = Utils.getServices(probeResponse); + return services.some(e => { + const probeTokenService = getTokenService(e); + return probeTokenService && probeTokenService.id === serviceId; + }); + }; + + const obsoleteProbeResponses = Object.values(probeResponses).filter( + i => i.json && haveThisTokenService(i.json), + ); + + yield all(obsoleteProbeResponses.map(({ id: probeId }) => { + if (visibleProbeServiceIds.includes(probeId)) { + return call(fetchProbeResponse, { probeId }); + } + return put({ probeId, type: ActionTypes.REMOVE_PROBE_RESPONSE }); + })); +} + /** try to start any non-interactive auth flows */ export function* doAuthWorkflow({ infoJson, windowId }) { const auths = yield select(getAuth); @@ -152,9 +201,13 @@ export function* invalidateInvalidAuth({ serviceId }) { export default function* authSaga() { yield all([ takeEvery(ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE, rerequestOnAccessTokenFailure), + takeEvery(ActionTypes.RECEIVE_DEGRADED_PROBE_RESPONSE, rerequestOnAccessTokenFailure), takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE, invalidateInvalidAuth), takeEvery(ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE, doAuthWorkflow), + takeEvery(ActionTypes.RECEIVE_DEGRADED_PROBE_RESPONSE, doAuthWorkflow), takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN, refetchInfoResponses), + takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN, refetchProbeResponses), takeEvery(ActionTypes.RESET_AUTHENTICATION_STATE, refetchInfoResponsesOnLogout), + takeEvery(ActionTypes.RESET_AUTHENTICATION_STATE, refetchProbeResponsesOnLogout), ]); } diff --git a/src/state/sagas/iiif.js b/src/state/sagas/iiif.js index d7a1f7115..59d44ba4c 100644 --- a/src/state/sagas/iiif.js +++ b/src/state/sagas/iiif.js @@ -1,7 +1,6 @@ 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 { @@ -9,13 +8,16 @@ import { receiveInfoResponseFailure, receiveDegradedInfoResponse, receiveSearch, receiveSearchFailure, receiveAnnotation, receiveAnnotationFailure, + receiveProbeResponse, receiveProbeResponseFailure, + receiveDegradedProbeResponse, } from '../actions'; -import { getTokenService } from '../../lib/getServices'; +import { anyAuthServices, getTokenService } from '../../lib/getServices'; import { getManifests, getRequestsConfig, getAccessTokens, selectInfoResponse, + selectProbeResponse, } from '../selectors'; /** */ @@ -80,10 +82,13 @@ function* fetchIiifResourceWithAuth(url, iiifResource, options, { degraded, fail const id = json['@id'] || json.id; if (response.ok) { - if (normalizeUrl(id, { stripAuthentication: false }) + if (id && normalizeUrl(id, { stripAuthentication: false }) === normalizeUrl(url.replace(/info\.json$/, ''), { stripAuthentication: false })) { - yield put(success({ json, response, tokenServiceId })); - return; + if (!json.substitute) { + // substitute indicates the Auth2 equivalent of a degraded response, should fall through + yield put(success({ json, response, tokenServiceId })); + return; + } } } else if (response.status !== 401) { yield put(failure({ @@ -121,7 +126,7 @@ 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\//)); + const services = anyAuthServices(manifestoCompatibleResource); if (services.length === 0) return undefined; const accessTokens = yield select(getAccessTokens); @@ -161,6 +166,30 @@ export function* fetchInfoResponse({ imageResource, infoId, windowId }) { yield call(fetchIiifResourceWithAuth, `${infoId.replace(/\/$/, '')}/info.json`, iiifResource, {}, callbacks); } +/** @private */ +export function* fetchProbeResponse({ resource, probeId, windowId }) { + let iiifResource = resource; + if (!iiifResource) { + iiifResource = yield select(selectProbeResponse, { probeId }); + } + + const callbacks = { + degraded: ({ + json, response, tokenServiceId, + }) => receiveDegradedProbeResponse(probeId, json, response.ok, tokenServiceId, windowId), + failure: ({ + error, json, response, tokenServiceId, + }) => ( + receiveProbeResponseFailure(probeId, error, tokenServiceId) + ), + success: ({ + json, response, tokenServiceId, + }) => receiveProbeResponse(probeId, json, response.ok, tokenServiceId), + }; + + yield call(fetchIiifResourceWithAuth, probeId, iiifResource, {}, callbacks); +} + /** @private */ export function* fetchSearchResponse({ windowId, companionWindowId, query, searchId, @@ -215,6 +244,7 @@ export default function* iiifSaga() { yield all([ takeEvery(ActionTypes.REQUEST_MANIFEST, fetchManifest), takeEvery(ActionTypes.REQUEST_INFO_RESPONSE, fetchInfoResponse), + takeEvery(ActionTypes.REQUEST_PROBE_RESPONSE, fetchProbeResponse), takeEvery(ActionTypes.REQUEST_SEARCH, fetchSearchResponse), takeEvery(ActionTypes.REQUEST_ANNOTATION, fetchAnnotation), takeEvery(ActionTypes.ADD_RESOURCE, fetchResourceManifest), diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js index bfcd275aa..f0468d560 100644 --- a/src/state/sagas/windows.js +++ b/src/state/sagas/windows.js @@ -4,6 +4,7 @@ import { import ActionTypes from '../actions/action-types'; import MiradorManifest from '../../lib/MiradorManifest'; import MiradorCanvas from '../../lib/MiradorCanvas'; +import { getProbeService } from '../../lib/getServices'; import { setContentSearchCurrentAnnotation, selectAnnotation, @@ -13,6 +14,7 @@ import { fetchSearch, receiveManifest, fetchInfoResponse, + fetchProbeResponse, showCollectionDialog, } from '../actions'; import { @@ -27,6 +29,7 @@ import { getElasticLayout, getCanvases, selectInfoResponses, + selectProbeResponses, getWindowConfig, } from '../selectors'; import { fetchManifests } from './iiif'; @@ -250,6 +253,21 @@ export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowI })); } +/** Fetch probe responses for the visible canvases */ +export function* fetchProbeResponses({ visibleCanvases: visibleCanvasIds, windowId }) { + const canvases = yield select(getCanvases, { windowId }); + const probeResponses = yield select(selectProbeResponses); + const visibleCanvases = (canvases || []).filter(c => visibleCanvasIds.includes(c.id)); + + yield all(visibleCanvases.map((canvas) => { + const miradorCanvas = new MiradorCanvas(canvas); + return all(miradorCanvas.imageResources.filter((r) => getProbeService(r)).map(resource => ( + !probeResponses[getProbeService(resource).id] + && put(fetchProbeResponse({ resource, windowId })) + )).filter(Boolean)); + })); +} + /** */ export function* determineAndShowCollectionDialog(manifestId, windowId) { const manifestoInstance = yield select(getManifestoInstance, { manifestId }); @@ -266,6 +284,7 @@ export default function* windowsSaga() { takeEvery(ActionTypes.UPDATE_WINDOW, setCanvasOnNewSequence), takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas), takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses), + takeEvery(ActionTypes.SET_CANVAS, fetchProbeResponses), takeEvery(ActionTypes.UPDATE_COMPANION_WINDOW, fetchCollectionManifests), takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases), takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult), diff --git a/src/state/selectors/auth.js b/src/state/selectors/auth.js index d38ab255b..2fde29e4c 100644 --- a/src/state/selectors/auth.js +++ b/src/state/selectors/auth.js @@ -1,13 +1,14 @@ import { createSelector } from 'reselect'; import { Utils } from 'manifesto.js'; import flatten from 'lodash/flatten'; +import { anyProbeServices } from '../../lib/getServices'; import { - audioResourcesFrom, filterByTypes, textResourcesFrom, videoResourcesFrom, + audioResourcesFrom, iiifImageResourcesFrom, textResourcesFrom, videoResourcesFrom, } from '../../lib/typeFilters'; import MiradorCanvas from '../../lib/MiradorCanvas'; import { miradorSlice } from './utils'; import { getConfig } from './config'; -import { getVisibleCanvases, selectInfoResponses } from './canvases'; +import { getVisibleCanvases, selectInfoResponses, selectProbeResponses } from './canvases'; export const getAuthProfiles = createSelector( [ @@ -26,19 +27,19 @@ export const selectCurrentAuthServices = createSelector( [ getVisibleCanvases, selectInfoResponses, + selectProbeResponses, getAuthProfiles, getAuth, (state, { iiifResources }) => iiifResources, ], - (canvases, infoResponses = {}, serviceProfiles, auth, iiifResources) => { + (canvases, infoResponses = {}, probeResponses = {}, serviceProfiles, auth, iiifResources) => { let currentAuthResources = iiifResources; if (!currentAuthResources && canvases) { currentAuthResources = flatten(canvases.map(c => { const miradorCanvas = new MiradorCanvas(c); - const images = miradorCanvas.iiifImageResources; - - return images.map(i => { + const canvasResources = miradorCanvas.imageResources; + const authResources = iiifImageResourcesFrom(canvasResources).map(i => { const iiifImageService = i.getServices()[0]; const infoResponse = infoResponses[iiifImageService.id]; @@ -48,14 +49,7 @@ export const selectCurrentAuthServices = createSelector( return iiifImageService; }); - })); - } - - if (currentAuthResources.length === 0 && canvases) { - currentAuthResources = flatten(canvases.map(c => { - const miradorCanvas = new MiradorCanvas(c); - const canvasResources = miradorCanvas.imageResources; - return videoResourcesFrom(canvasResources) + return authResources.concat(videoResourcesFrom(canvasResources)) .concat(audioResourcesFrom(canvasResources)) .concat(textResourcesFrom(canvasResources)); })); @@ -67,7 +61,7 @@ export const selectCurrentAuthServices = createSelector( const currentAuthServices = currentAuthResources.map(resource => { let lastAttemptedService; const resourceServices = Utils.getServices(resource); - const probeServices = filterByTypes(resourceServices, 'AuthProbeService2'); + const probeServices = anyProbeServices(resource); const probeServiceServices = flatten(probeServices.map(p => Utils.getServices(p))); for (const authProfile of serviceProfiles) { @@ -91,7 +85,6 @@ export const selectCurrentAuthServices = createSelector( if (service && !h[service.id]) { h[service.id] = service; // eslint-disable-line no-param-reassign } - return h; }, {})); }, diff --git a/src/state/selectors/canvases.js b/src/state/selectors/canvases.js index 7cfa0c044..f7751687a 100644 --- a/src/state/selectors/canvases.js +++ b/src/state/selectors/canvases.js @@ -1,15 +1,21 @@ import { createSelector } from 'reselect'; import flatten from 'lodash/flatten'; +import { Resource } from 'manifesto.js'; import CanvasGroupings from '../../lib/CanvasGroupings'; import MiradorCanvas from '../../lib/MiradorCanvas'; import { miradorSlice } from './utils'; import { getWindow } from './getters'; import { getSequence } from './sequences'; import { getWindowViewType } from './windows'; +import { getProbeService } from '../../lib/getServices'; +import { anyImageServices } from '../../lib/typeFilters'; /** */ export const selectInfoResponses = state => miradorSlice(state).infoResponses; +/** */ +export const selectProbeResponses = state => miradorSlice(state).probeResponses; + export const getCanvases = createSelector( [getSequence], sequence => (sequence && sequence.getCanvases()) || [], @@ -175,21 +181,42 @@ export const getCanvasDescription = createSelector( canvas => canvas && canvas.getProperty('description'), ); +/** */ +const probeReplacements = (resources, probeResponses) => { + if (!probeResponses) return resources; + + return resources.map((r) => { + const probeService = getProbeService(r); + const probeServiceId = probeService && probeService.id; + const probeResponse = probeServiceId && probeResponses[probeServiceId]; + if (!probeResponse || probeResponse.isFetching) return r; + + const probeContentUrl = probeResponse.json && (probeResponse.json.location || probeResponse.json.substitute); + const probeReplacedProperties = {}; + if (probeContentUrl) { + probeReplacedProperties.id = probeContentUrl; + if (probeResponse.json.format) probeReplacedProperties.format = probeResponse.json.format; + } + return new Resource({ ...r.__jsonld, ...probeReplacedProperties }, r.options); + }); +}; + export const getVisibleCanvasNonTiledResources = createSelector( [ getVisibleCanvases, ], canvases => flatten(canvases .map(canvas => new MiradorCanvas(canvas).imageResources)) - .filter(resource => resource.getServices().length < 1), + .filter(resource => anyImageServices(resource).length < 1), ); export const getVisibleCanvasVideoResources = createSelector( [ getVisibleCanvases, + selectProbeResponses, ], - canvases => flatten(canvases - .map(canvas => new MiradorCanvas(canvas).videoResources)), + (canvases, probeResponses) => flatten(canvases + .map(canvas => probeReplacements(new MiradorCanvas(canvas).videoResources, probeResponses))), ); export const getVisibleCanvasCaptions = createSelector( @@ -207,9 +234,10 @@ export const getVisibleCanvasCaptions = createSelector( export const getVisibleCanvasAudioResources = createSelector( [ getVisibleCanvases, + selectProbeResponses, ], - canvases => flatten(canvases - .map(canvas => new MiradorCanvas(canvas).audioResources)), + (canvases, probeResponses) => flatten(canvases + .map(canvas => probeReplacements(new MiradorCanvas(canvas).audioResources, probeResponses))), ); export const selectInfoResponse = createSelector( @@ -233,3 +261,26 @@ export const selectInfoResponse = createSelector( && infoResponses[iiifServiceId]; }, ); + +export const selectProbeResponse = createSelector( + [ + (state, { probeId }) => probeId, + getCanvas, + selectProbeResponses, + ], + (probeId, canvas, probeResponses) => { + let probeServiceId = probeId; + + if (!probeServiceId) { + if (!canvas) return undefined; + const miradorCanvas = new MiradorCanvas(canvas); + const contentResource = miradorCanvas.imageResources[0]; + const probeService = getProbeService(contentResource); + probeServiceId = probeService && probeService.id; + } + + return probeServiceId && probeResponses[probeServiceId] + && !probeResponses[probeServiceId].isFetching + && probeResponses[probeServiceId]; + }, +);