Skip to content

Commit e230110

Browse files
committed
Merge branch 'dspace-cris-2023_02_x' into ux-plus-2023_02_x
# Conflicts: # src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts # src/app/item-page/mirador-viewer/mirador-viewer.component.ts
2 parents 51c7c34 + 682b900 commit e230110

60 files changed

Lines changed: 563 additions & 118 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

server.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser');
6565
// Set path fir IIIF viewer.
6666
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
6767

68+
const miradorHtml = join(IIIF_VIEWER, '/mirador/index.html');
69+
6870
const indexHtml = join(DIST_FOLDER, 'index.html');
6971

7072
const cookieParser = require('cookie-parser');
@@ -86,8 +88,10 @@ const _window = domino.createWindow(indexHtml);
8688
// The REST server base URL
8789
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
8890

91+
const IIIF_ALLOWED_ORIGINS = environment.rest.allowedOrigins || [];
92+
8993
// Assign the DOM window and document objects to the global object
90-
(_window as any).screen = {deviceXDPI: 0, logicalXDPI: 0};
94+
(_window as any).screen = { deviceXDPI: 0, logicalXDPI: 0 };
9195
(global as any).window = _window;
9296
(global as any).document = _window.document;
9397
(global as any).navigator = _window.navigator;
@@ -231,6 +235,35 @@ export function app() {
231235
*/
232236
router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
233237

238+
/*
239+
* Adapt headers to allow embedding of IIIF viewer in authorized pages
240+
*/
241+
server.get('/iiif/mirador/index.html', (req, res) => {
242+
const referer = req.headers.referer;
243+
244+
if (referer && !referer.startsWith('/')) {
245+
try {
246+
const origin = new URL(referer).origin;
247+
if (IIIF_ALLOWED_ORIGINS.includes(origin)) {
248+
console.info('Found allowed origin, setting headers for IIIF viewer');
249+
// CORS header
250+
res.setHeader('Access-Control-Allow-Origin', origin);
251+
// CSP for iframe embedding
252+
res.setHeader('Content-Security-Policy', `frame-ancestors ${origin};`);
253+
console.info('Headers have been set ', res.getHeader('Access-Control-Allow-Origin'), res.getHeader('Content-Security-Policy'));
254+
}
255+
} catch (error) {
256+
console.error('An error occurred setting security headers in response:', error.message);
257+
}
258+
}
259+
260+
res.sendFile(miradorHtml, (err) => {
261+
if (err) {
262+
res.status(500).send('Internal Server Error');
263+
}
264+
});
265+
});
266+
234267
/**
235268
* Checking server status
236269
*/
@@ -286,6 +319,11 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
286319
originUrl: environment.ui.baseUrl,
287320
requestUrl: req.originalUrl,
288321
}, (err, data) => {
322+
323+
if (res.writableEnded || res.headersSent || res.finished) {
324+
return;
325+
}
326+
289327
if (hasNoValue(err) && hasValue(data)) {
290328
// Replace REST URL with UI URL
291329
if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
@@ -644,10 +682,10 @@ function start() {
644682
* The callback function to serve client health check requests
645683
*/
646684
function clientHealthCheck(req, res) {
647-
const isServerHealthy = true;
648-
if (isServerHealthy) {
649-
res.status(200).json({ status: 'UP' });
650-
}
685+
const isServerHealthy = true;
686+
if (isServerHealthy) {
687+
res.status(200).json({ status: 'UP' });
688+
}
651689
}
652690

653691
/*
@@ -665,6 +703,8 @@ function healthCheck(req, res) {
665703
});
666704
});
667705
}
706+
707+
668708
// Webpack will replace 'require' with '__webpack_require__'
669709
// '__non_webpack_require__' is a proxy to Node 'require'
670710
// The below code is to ensure that the server is run only when not requiring the bundle.

src/app/bitstream-page/bitstream-download-redirect.guard.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ describe('BitstreamDownloadRedirectGuard', () => {
162162
it('should redirect to the content link', waitForAsync(() => {
163163
TestBed.runInInjectionContext(() => {
164164
resolver(route, state).subscribe(() => {
165-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
166-
}
165+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link', null, true);
166+
},
167167
);
168168
});
169169
}));
@@ -176,7 +176,7 @@ describe('BitstreamDownloadRedirectGuard', () => {
176176
it('should redirect to an updated content link', waitForAsync(() => {
177177
TestBed.runInInjectionContext(() => {
178178
resolver(route, state).subscribe(() => {
179-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
179+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers', null, true);
180180
});
181181
});
182182
}));

src/app/bitstream-page/bitstream-download-redirect.guard.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const bitstreamDownloadRedirectGuard: CanActivateFn = (
3535
): Observable<UrlTree | boolean> => {
3636

3737
const bitstreamId = route.params.id;
38+
const accessToken: string = route.queryParams.accessToken;
3839

3940
return bitstreamDataService.findById(bitstreamId, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW).pipe(
4041
getFirstCompletedRemoteData(),
@@ -65,16 +66,23 @@ export const bitstreamDownloadRedirectGuard: CanActivateFn = (
6566
}),
6667
map(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => {
6768
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
68-
hardRedirectService.redirect(fileLink);
69+
hardRedirectService.redirect(fileLink, null, true);
6970
return false;
70-
} else if (isAuthorized && !isLoggedIn) {
71-
hardRedirectService.redirect(bitstream._links.content.href);
71+
} else if (isAuthorized && !isLoggedIn && !hasValue(accessToken)) {
72+
hardRedirectService.redirect(bitstream._links.content.href, null, true);
7273
return false;
73-
} else if (!isAuthorized && isLoggedIn) {
74-
return router.createUrlTree([getForbiddenRoute()]);
75-
} else if (!isAuthorized && !isLoggedIn) {
76-
auth.setRedirectUrl(router.url);
77-
return router.createUrlTree(['login']);
74+
} else if (!isAuthorized) {
75+
// Either we have an access token, or we are logged in, or we are not logged in.
76+
// For now, the access token does not care if we are logged in or not.
77+
if (hasValue(accessToken)) {
78+
hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken, null, true);
79+
return false;
80+
} else if (isLoggedIn) {
81+
return router.createUrlTree([getForbiddenRoute()]);
82+
} else if (!isLoggedIn) {
83+
auth.setRedirectUrl(router.url);
84+
return router.createUrlTree(['login']);
85+
}
7886
}
7987
})
8088
);

src/app/core/auth/server-auth-request.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { map } from 'rxjs/operators';
1818
import { Observable } from 'rxjs';
1919
import { XSRFService } from '../xsrf/xsrf.service';
20+
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
2021

2122
/**
2223
* Server side version of the service to send authentication requests
@@ -41,8 +42,8 @@ export class ServerAuthRequestService extends AuthRequestService {
4142
* @protected
4243
*/
4344
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
44-
// First do a call to the root endpoint in order to get an XSRF token
45-
return this.httpClient.get(this.halService.getRootHref(), { observe: 'response' }).pipe(
45+
// First do a call to the csrf endpoint in order to get an XSRF token
46+
return this.httpClient.get(new RESTURLCombiner('/security/csrf').toString(), { observe: 'response' }).pipe(
4647
// retrieve the XSRF token from the response header
4748
map((response: HttpResponse<any>) => response.headers.get(XSRF_RESPONSE_HEADER)),
4849
map((xsrfToken: string) => {

src/app/core/data/bitstream-data.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
174174
const searchParams = [];
175175
searchParams.push(new RequestParam('handle', handle));
176176
if (hasValue(sequenceId)) {
177-
searchParams.push(new RequestParam('sequenceId', sequenceId));
177+
searchParams.push(new RequestParam('sequence', sequenceId));
178178
}
179179
if (hasValue(filename)) {
180180
searchParams.push(new RequestParam('filename', filename));

src/app/core/data/feature-authorization/feature-id.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ export enum FeatureID {
3939
EPersonForgotPassword = 'epersonForgotPassword',
4040
ShowClaimItem = 'showClaimItem',
4141
CanCorrectItem = 'canCorrectItem',
42+
CanViewInWorkflowSinceStatistics = 'canViewInWorkflowSinceStatistics',
4243
}

src/app/core/services/hard-redirect.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export abstract class HardRedirectService {
1313
* the page to redirect to
1414
* @param statusCode
1515
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
16+
* @param shouldSetCorsHeader
17+
* optional to prevent CORS error on redirect
1618
*/
17-
abstract redirect(url: string, statusCode?: number);
19+
abstract redirect(url: string, statusCode?: number, shouldSetCorsHeader?: boolean);
1820

1921
/**
2022
* Get the current route, with query params included

src/app/core/services/server-hard-redirect.service.spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ describe('ServerHardRedirectService', () => {
88
const mockRequest = jasmine.createSpyObj(['get']);
99
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
1010

11-
let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
11+
const serverResponseService = jasmine.createSpyObj('ServerResponseService', {
12+
setHeader: jasmine.createSpy('setHeader'),
13+
});
14+
15+
let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse, serverResponseService);
1216
const origin = 'https://test-host.com:4000';
1317

1418
beforeEach(() => {
1519
mockRequest.protocol = 'https';
20+
mockRequest.path = '/bitstreams/test-uuid/download';
1621
mockRequest.headers = {
1722
host: 'test-host.com:4000',
1823
};
@@ -76,7 +81,7 @@ describe('ServerHardRedirectService', () => {
7681
ssrBaseUrl: 'https://private-url:4000/server',
7782
baseUrl: 'https://public-url/server',
7883
} } };
79-
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse);
84+
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse, serverResponseService);
8085

8186
beforeEach(() => {
8287
service.redirect(redirect);
@@ -88,4 +93,21 @@ describe('ServerHardRedirectService', () => {
8893
});
8994
});
9095

96+
describe('Should add cors header on download path', () => {
97+
const redirect = 'https://private-url:4000/server/api/bitstreams/uuid';
98+
const environmentWithSSRUrl: any = { ...environment, ...{ ...environment.rest, rest: {
99+
ssrBaseUrl: 'https://private-url:4000/server',
100+
baseUrl: 'https://public-url/server',
101+
} } };
102+
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse, serverResponseService);
103+
104+
beforeEach(() => {
105+
service.redirect(redirect, null, true);
106+
});
107+
108+
it('should set header', () => {
109+
expect(serverResponseService.setHeader).toHaveBeenCalled();
110+
});
111+
});
112+
91113
});

src/app/core/services/server-hard-redirect.service.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
44
import { HardRedirectService } from './hard-redirect.service';
55
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
66
import { isNotEmpty } from '../../shared/empty.util';
7+
import { ServerResponseService } from './server-response.service';
78

89
/**
910
* Service for performing hard redirects within the server app module
@@ -15,6 +16,7 @@ export class ServerHardRedirectService extends HardRedirectService {
1516
@Inject(APP_CONFIG) protected appConfig: AppConfig,
1617
@Inject(REQUEST) protected req: Request,
1718
@Inject(RESPONSE) protected res: Response,
19+
private responseService: ServerResponseService,
1820
) {
1921
super();
2022
}
@@ -26,8 +28,9 @@ export class ServerHardRedirectService extends HardRedirectService {
2628
* the page to redirect to
2729
* @param statusCode
2830
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
31+
* @param shouldSetCorsHeader
2932
*/
30-
redirect(url: string, statusCode?: number) {
33+
redirect(url: string, statusCode?: number, shouldSetCorsHeader?: boolean) {
3134
if (url === this.req.url) {
3235
return;
3336
}
@@ -57,6 +60,10 @@ export class ServerHardRedirectService extends HardRedirectService {
5760
status = 302;
5861
}
5962

63+
if (shouldSetCorsHeader) {
64+
this.setCorsHeader();
65+
}
66+
6067
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
6168

6269
this.res.redirect(status, redirectUrl);
@@ -83,4 +90,12 @@ export class ServerHardRedirectService extends HardRedirectService {
8390
getCurrentOrigin(): string {
8491
return this.req.protocol + '://' + this.req.headers.host;
8592
}
93+
94+
/**
95+
* Set CORS header to allow embedding of redirected content.
96+
* The actual security header will be set by the rest
97+
*/
98+
setCorsHeader() {
99+
this.responseService.setHeader('Access-Control-Allow-Origin', '*');
100+
}
86101
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<p class="full-text-op">{{'iiifviewer.fullscreen.notice' | translate}}</p>
22
<p *ngIf="!isViewerAvailable" id="viewer-message">{{viewerMessage}}</p>
3-
<iframe title="Mirador Viewer" allowtransparency="true" *ngIf="isViewerAvailable" [src]="iframeViewerUrl | async" id="mirador-viewer"></iframe>
3+
<ng-container *ngVar="(iframeViewerUrl | async) as iframeUrl">
4+
<iframe title="Mirador Viewer" allowtransparency="true" *ngIf="isViewerAvailable && iframeUrl" [src]="(iframeUrl) | dsSafeUrl" id="mirador-viewer"></iframe>
5+
</ng-container>
46

0 commit comments

Comments
 (0)