Skip to content

Commit a3a298f

Browse files
authored
Merge pull request DSpace#3087 from DSpace/backport-3062-to-dspace-7_x
[Port dspace-7_x] Made legacy bitstream URLs redirect with 301 status code
2 parents 975e9fd + c332600 commit a3a298f

7 files changed

Lines changed: 248 additions & 201 deletions

src/app/bitstream-page/bitstream-page-routing.module.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ResourcePolicyCreateComponent } from '../shared/resource-policies/creat
88
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
99
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
1010
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
11-
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
11+
import { legacyBitstreamURLRedirectGuard } from './legacy-bitstream-url-redirect.guard';
1212
import { BitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver';
1313
import { BitstreamBreadcrumbsService } from '../core/breadcrumbs/bitstream-breadcrumbs.service';
1414
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
@@ -27,17 +27,13 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
2727
// Resolve XMLUI bitstream download URLs
2828
path: 'handle/:prefix/:suffix/:filename',
2929
component: BitstreamDownloadPageComponent,
30-
resolve: {
31-
bitstream: LegacyBitstreamUrlResolver
32-
},
30+
canActivate: [legacyBitstreamURLRedirectGuard],
3331
},
3432
{
3533
// Resolve JSPUI bitstream download URLs
3634
path: ':prefix/:suffix/:sequence_id/:filename',
3735
component: BitstreamDownloadPageComponent,
38-
resolve: {
39-
bitstream: LegacyBitstreamUrlResolver
40-
},
36+
canActivate: [legacyBitstreamURLRedirectGuard],
4137
},
4238
{
4339
// Resolve angular bitstream download URLs
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { cold } from 'jasmine-marbles';
2+
import { EMPTY } from 'rxjs';
3+
4+
import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths';
5+
import { BitstreamDataService } from '../core/data/bitstream-data.service';
6+
import { RemoteData } from '../core/data/remote-data';
7+
import { RequestEntryState } from '../core/data/request-entry-state.model';
8+
import { BrowserHardRedirectService } from '../core/services/browser-hard-redirect.service';
9+
import { HardRedirectService } from '../core/services/hard-redirect.service';
10+
import { Bitstream } from '../core/shared/bitstream.model';
11+
import { RouterStub } from '../shared/testing/router.stub';
12+
import { legacyBitstreamURLRedirectGuard } from './legacy-bitstream-url-redirect.guard';
13+
14+
describe('legacyBitstreamURLRedirectGuard', () => {
15+
let resolver: any;
16+
let bitstreamDataService: BitstreamDataService;
17+
let remoteDataMocks: { [type: string]: RemoteData<any> };
18+
let route;
19+
let state;
20+
let hardRedirectService: HardRedirectService;
21+
let router: RouterStub;
22+
23+
let bitstream: Bitstream;
24+
25+
beforeEach(() => {
26+
route = {
27+
params: {},
28+
queryParams: {}
29+
};
30+
router = new RouterStub();
31+
hardRedirectService = new BrowserHardRedirectService(window.location);
32+
state = {};
33+
bitstream = Object.assign(new Bitstream(), {
34+
uuid: 'bitstream-id',
35+
});
36+
remoteDataMocks = {
37+
RequestPending: new RemoteData(undefined, 0, 0, RequestEntryState.RequestPending, undefined, undefined, undefined),
38+
ResponsePending: new RemoteData(undefined, 0, 0, RequestEntryState.ResponsePending, undefined, undefined, undefined),
39+
Success: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, bitstream, 200),
40+
NoContent: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, undefined, 204),
41+
Error: new RemoteData(0, 0, 0, RequestEntryState.Error, 'Internal server error', undefined, 500),
42+
};
43+
bitstreamDataService = {
44+
findByItemHandle: () => undefined
45+
} as any;
46+
resolver = legacyBitstreamURLRedirectGuard;
47+
});
48+
49+
describe(`resolve`, () => {
50+
describe(`For JSPUI-style URLs`, () => {
51+
beforeEach(() => {
52+
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
53+
route = Object.assign({}, route, {
54+
params: {
55+
prefix: '123456789',
56+
suffix: '1234',
57+
filename: 'some-file.pdf',
58+
sequence_id: '5'
59+
}
60+
});
61+
});
62+
it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => {
63+
resolver(route, state, bitstreamDataService, hardRedirectService, router);
64+
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
65+
`${route.params.prefix}/${route.params.suffix}`,
66+
route.params.sequence_id,
67+
route.params.filename
68+
);
69+
});
70+
});
71+
72+
describe(`For XMLUI-style URLs`, () => {
73+
describe(`when there is a sequenceId query parameter`, () => {
74+
beforeEach(() => {
75+
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
76+
route = Object.assign({}, route, {
77+
params: {
78+
prefix: '123456789',
79+
suffix: '1234',
80+
filename: 'some-file.pdf',
81+
},
82+
queryParams: {
83+
sequenceId: '5'
84+
}
85+
});
86+
});
87+
it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => {
88+
resolver(route, state, bitstreamDataService, hardRedirectService, router);
89+
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
90+
`${route.params.prefix}/${route.params.suffix}`,
91+
route.queryParams.sequenceId,
92+
route.params.filename
93+
);
94+
});
95+
});
96+
describe(`when there's no sequenceId query parameter`, () => {
97+
beforeEach(() => {
98+
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
99+
route = Object.assign({}, route, {
100+
params: {
101+
prefix: '123456789',
102+
suffix: '1234',
103+
filename: 'some-file.pdf',
104+
},
105+
});
106+
});
107+
it(`should call findByItemHandle with the handle, and filename from the route`, () => {
108+
resolver(route, state, bitstreamDataService, hardRedirectService, router);
109+
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
110+
`${route.params.prefix}/${route.params.suffix}`,
111+
undefined,
112+
route.params.filename
113+
);
114+
});
115+
});
116+
});
117+
describe('should return and complete after the RemoteData has...', () => {
118+
it('...failed', () => {
119+
spyOn(router, 'createUrlTree').and.callThrough();
120+
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
121+
a: remoteDataMocks.RequestPending,
122+
b: remoteDataMocks.ResponsePending,
123+
c: remoteDataMocks.Error,
124+
}));
125+
resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => {
126+
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
127+
expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
128+
});
129+
});
130+
131+
it('...succeeded without content', () => {
132+
spyOn(router, 'createUrlTree').and.callThrough();
133+
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
134+
a: remoteDataMocks.RequestPending,
135+
b: remoteDataMocks.ResponsePending,
136+
c: remoteDataMocks.NoContent,
137+
}));
138+
resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => {
139+
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
140+
expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
141+
});
142+
});
143+
144+
it('...succeeded', () => {
145+
spyOn(hardRedirectService, 'redirect');
146+
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
147+
a: remoteDataMocks.RequestPending,
148+
b: remoteDataMocks.ResponsePending,
149+
c: remoteDataMocks.Success,
150+
}));
151+
resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => {
152+
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
153+
expect(hardRedirectService.redirect).toHaveBeenCalledWith(new URL(`/bitstreams/${bitstream.uuid}/download`, window.location.origin).href, 301);
154+
});
155+
});
156+
});
157+
});
158+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { inject } from '@angular/core';
2+
import {
3+
ActivatedRouteSnapshot,
4+
CanActivateFn,
5+
Router,
6+
RouterStateSnapshot,
7+
UrlTree,
8+
} from '@angular/router';
9+
import { Observable } from 'rxjs';
10+
import { map } from 'rxjs/operators';
11+
12+
import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths';
13+
import { BitstreamDataService } from '../core/data/bitstream-data.service';
14+
import { RemoteData } from '../core/data/remote-data';
15+
import { HardRedirectService } from '../core/services/hard-redirect.service';
16+
import { Bitstream } from '../core/shared/bitstream.model';
17+
import { getFirstCompletedRemoteData } from '../core/shared/operators';
18+
import { hasNoValue } from '../shared/empty.util';
19+
20+
/**
21+
* Redirects to a bitstream based on the handle of the item, and the sequence id or the filename of the
22+
* bitstream. In production mode the status code will also be set the status code to 301 marking it as a permanent URL
23+
* redirect for bots.
24+
*
25+
* @returns Observable<UrlTree> Returns a URL to redirect the user to the new URL format
26+
*/
27+
export const legacyBitstreamURLRedirectGuard: CanActivateFn = (
28+
route: ActivatedRouteSnapshot,
29+
state: RouterStateSnapshot,
30+
bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
31+
serverHardRedirectService: HardRedirectService = inject(HardRedirectService),
32+
router: Router = inject(Router),
33+
): Observable<UrlTree | boolean> => {
34+
const prefix = route.params.prefix;
35+
const suffix = route.params.suffix;
36+
const filename = route.params.filename;
37+
let sequenceId = route.params.sequence_id;
38+
if (hasNoValue(sequenceId)) {
39+
sequenceId = route.queryParams.sequenceId;
40+
}
41+
return bitstreamDataService.findByItemHandle(
42+
`${prefix}/${suffix}`,
43+
sequenceId,
44+
filename,
45+
).pipe(
46+
getFirstCompletedRemoteData(),
47+
map((rd: RemoteData<Bitstream>) => {
48+
if (rd.hasSucceeded && !rd.hasNoContent) {
49+
serverHardRedirectService.redirect(new URL(`/bitstreams/${rd.payload.uuid}/download`, serverHardRedirectService.getCurrentOrigin()).href, 301);
50+
return false;
51+
} else {
52+
return router.createUrlTree([PAGE_NOT_FOUND_PATH]);
53+
}
54+
})
55+
);
56+
};

src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts

Lines changed: 0 additions & 145 deletions
This file was deleted.

0 commit comments

Comments
 (0)