Skip to content

Commit f7bef6a

Browse files
Merge remote-tracking branch 'gitHub/main' into task/main/DURACOM-445
2 parents c7fdbdc + 201b238 commit f7bef6a

File tree

103 files changed

+5110
-454
lines changed

Some content is hidden

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

103 files changed

+5110
-454
lines changed

config/config.example.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ form:
164164
validatorMap:
165165
required: required
166166
regex: pattern
167+
# If true it enables the button "Duplicate" inside inline form groups.
168+
# The button will give the possibility to duplicate the whole form section copying the metadata values as well.
169+
showInlineGroupDuplicateButton: false
167170

168171
# Notification settings
169172
notifications:
@@ -202,6 +205,10 @@ submission:
202205
# default configuration
203206
- name: default
204207
style: ''
208+
# Icons that should remain visible even when no authority value is present for the metadata field.
209+
# This is useful for fields where you want to display an icon regardless of whether the value has an authority link.
210+
# Example: ['fas fa-user'] will show the user icon for author fields even without authority data.
211+
iconsVisibleWithNoAuthority: ['fas fa-user']
205212
authority:
206213
confidence:
207214
# NOTE: example of configuration
@@ -708,6 +715,28 @@ layout:
708715
default:
709716
icon: fas fa-project-diagram
710717
style: text-success
718+
# If true the download link in item page will be rendered as an advanced attachment, the view can be then configured with the layout.advancedAttachmentRendering config
719+
showDownloadLinkAsAttachment: false
720+
# Configuration for advanced attachment rendering in item pages. This controls how files are displayed when showDownloadLinkAsAttachment is enabled.
721+
# Defines which metadata/attributes to display for bitstream attachments.
722+
advancedAttachmentRendering:
723+
# Metadata and attributes to display for each attachment
724+
metadata:
725+
- name: dc.title
726+
type: metadata
727+
truncatable: false
728+
- name: dc.type
729+
type: metadata
730+
truncatable: false
731+
- name: dc.description
732+
type: metadata
733+
truncatable: true
734+
- name: size
735+
type: attribute
736+
- name: format
737+
type: attribute
738+
- name: checksum
739+
type: attribute
711740

712741
# Configuration for customization of search results
713742
searchResults:
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { PLATFORM_ID } from '@angular/core';
2+
import {
3+
TestBed,
4+
waitForAsync,
5+
} from '@angular/core/testing';
6+
import { Router } from '@angular/router';
7+
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
8+
import { getForbiddenRoute } from '@dspace/core/router/core-routing-paths';
9+
import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
10+
import { Store } from '@ngrx/store';
11+
import { of } from 'rxjs';
12+
13+
import { AuthService } from '../core/auth/auth.service';
14+
import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service';
15+
import { ObjectCacheService } from '../core/cache/object-cache.service';
16+
import { BitstreamDataService } from '../core/data/bitstream-data.service';
17+
import { BitstreamFormatDataService } from '../core/data/bitstream-format-data.service';
18+
import { DSOChangeAnalyzer } from '../core/data/dso-change-analyzer.service';
19+
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
20+
import { SignpostingDataService } from '../core/data/signposting-data.service';
21+
import { HardRedirectService } from '../core/services/hard-redirect.service';
22+
import { ServerResponseService } from '../core/services/server-response.service';
23+
import {
24+
NativeWindowRef,
25+
NativeWindowService,
26+
} from '../core/services/window.service';
27+
import { Bitstream } from '../core/shared/bitstream.model';
28+
import { FileService } from '../core/shared/file.service';
29+
import { HALEndpointService } from '../core/shared/hal-endpoint.service';
30+
import { UUIDService } from '../core/shared/uuid.service';
31+
import { bitstreamDownloadRedirectGuard } from './bitstream-download-redirect.guard';
32+
33+
describe('BitstreamDownloadRedirectGuard', () => {
34+
let resolver: any;
35+
36+
let authService: AuthService;
37+
let authorizationService: AuthorizationDataService;
38+
let bitstreamDataService: BitstreamDataService;
39+
let fileService: FileService;
40+
let halEndpointService: HALEndpointService;
41+
let hardRedirectService: HardRedirectService;
42+
let remoteDataBuildService: RemoteDataBuildService;
43+
let uuidService: UUIDService;
44+
let objectCacheService: ObjectCacheService;
45+
let router: Router;
46+
let store: Store;
47+
let bitstream: Bitstream;
48+
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
49+
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
50+
51+
let route = {
52+
params: {},
53+
queryParams: {},
54+
};
55+
let state = {};
56+
57+
const mocklink = {
58+
href: 'http://test.org',
59+
rel: 'test',
60+
type: 'test',
61+
};
62+
63+
const mocklink2 = {
64+
href: 'http://test2.org',
65+
rel: 'test',
66+
type: 'test',
67+
};
68+
69+
function init() {
70+
authService = jasmine.createSpyObj('authService', {
71+
isAuthenticated: of(true),
72+
setRedirectUrl: {},
73+
});
74+
authorizationService = jasmine.createSpyObj('authorizationSerivice', {
75+
isAuthorized: of(true),
76+
});
77+
78+
fileService = jasmine.createSpyObj('fileService', {
79+
retrieveFileDownloadLink: of('content-url-with-headers'),
80+
});
81+
82+
hardRedirectService = jasmine.createSpyObj('fileService', {
83+
redirect: {},
84+
});
85+
86+
halEndpointService = jasmine.createSpyObj('halEndpointService', {
87+
getEndpoint: of('https://rest.api/core'),
88+
});
89+
90+
remoteDataBuildService = jasmine.createSpyObj('remoteDataBuildService', {
91+
buildSingle: of(new Bitstream()),
92+
});
93+
94+
uuidService = jasmine.createSpyObj('uuidService', {
95+
generate: 'test-id',
96+
});
97+
98+
bitstream = Object.assign(new Bitstream(), {
99+
uuid: 'bitstreamUuid',
100+
_links: {
101+
content: { href: 'bitstream-content-link' },
102+
self: { href: 'bitstream-self-link' },
103+
},
104+
});
105+
106+
router = jasmine.createSpyObj('router', ['navigateByUrl', 'createUrlTree']);
107+
108+
store = jasmine.createSpyObj('store', {
109+
dispatch: {},
110+
pipe: of(true),
111+
});
112+
113+
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
114+
setHeader: jasmine.createSpy('setHeader'),
115+
});
116+
117+
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
118+
getLinks: of([mocklink, mocklink2]),
119+
});
120+
121+
objectCacheService = jasmine.createSpyObj('objectCacheService', {
122+
getByHref: of(null),
123+
});
124+
125+
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
126+
findById: createSuccessfulRemoteDataObject$(Object.assign(new Bitstream(), {
127+
_links: {
128+
content: { href: 'bitstream-content-link' },
129+
self: { href: 'bitstream-self-link' },
130+
},
131+
})),
132+
});
133+
134+
resolver = bitstreamDownloadRedirectGuard;
135+
}
136+
137+
function initTestbed() {
138+
TestBed.configureTestingModule({
139+
providers: [
140+
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
141+
{ provide: Router, useValue: router },
142+
{ provide: AuthorizationDataService, useValue: authorizationService },
143+
{ provide: AuthService, useValue: authService },
144+
{ provide: FileService, useValue: fileService },
145+
{ provide: HardRedirectService, useValue: hardRedirectService },
146+
{ provide: ServerResponseService, useValue: serverResponseService },
147+
{ provide: SignpostingDataService, useValue: signpostingDataService },
148+
{ provide: ObjectCacheService, useValue: objectCacheService },
149+
{ provide: PLATFORM_ID, useValue: 'server' },
150+
{ provide: UUIDService, useValue: uuidService },
151+
{ provide: Store, useValue: store },
152+
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
153+
{ provide: HALEndpointService, useValue: halEndpointService },
154+
{ provide: DSOChangeAnalyzer, useValue: {} },
155+
{ provide: BitstreamFormatDataService, useValue: {} },
156+
{ provide: NotificationsService, useValue: {} },
157+
{ provide: BitstreamDataService, useValue: bitstreamDataService },
158+
],
159+
});
160+
}
161+
162+
describe('bitstream retrieval', () => {
163+
describe('when the user is authorized and not logged in', () => {
164+
beforeEach(() => {
165+
init();
166+
(authService.isAuthenticated as jasmine.Spy).and.returnValue(of(false));
167+
initTestbed();
168+
});
169+
it('should redirect to the content link', waitForAsync(() => {
170+
TestBed.runInInjectionContext(() => {
171+
resolver(route, state).subscribe(() => {
172+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link', null, true);
173+
},
174+
);
175+
});
176+
}));
177+
});
178+
describe('when the user is authorized and logged in', () => {
179+
beforeEach(() => {
180+
init();
181+
initTestbed();
182+
});
183+
it('should redirect to an updated content link', waitForAsync(() => {
184+
TestBed.runInInjectionContext(() => {
185+
resolver(route, state).subscribe(() => {
186+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers', null, true);
187+
});
188+
});
189+
}));
190+
});
191+
describe('when the user is not authorized and logged in', () => {
192+
beforeEach(() => {
193+
init();
194+
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(of(false));
195+
initTestbed();
196+
});
197+
it('should navigate to the forbidden route', waitForAsync(() => {
198+
TestBed.runInInjectionContext(() => {
199+
resolver(route, state).subscribe(() => {
200+
expect(router.createUrlTree).toHaveBeenCalledWith([getForbiddenRoute()]);
201+
});
202+
});
203+
}));
204+
});
205+
describe('when the user is not authorized and not logged in', () => {
206+
beforeEach(() => {
207+
init();
208+
(authService.isAuthenticated as jasmine.Spy).and.returnValue(of(false));
209+
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(of(false));
210+
initTestbed();
211+
});
212+
it('should navigate to the login page', waitForAsync(() => {
213+
214+
TestBed.runInInjectionContext(() => {
215+
resolver(route, state).subscribe(() => {
216+
expect(authService.setRedirectUrl).toHaveBeenCalled();
217+
expect(router.createUrlTree).toHaveBeenCalledWith(['login']);
218+
});
219+
});
220+
}));
221+
});
222+
});
223+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { inject } from '@angular/core';
2+
import {
3+
ActivatedRouteSnapshot,
4+
CanActivateFn,
5+
Router,
6+
RouterStateSnapshot,
7+
UrlTree,
8+
} from '@angular/router';
9+
import { getForbiddenRoute } from '@dspace/core/router/core-routing-paths';
10+
import {
11+
combineLatest,
12+
Observable,
13+
of,
14+
} from 'rxjs';
15+
import {
16+
filter,
17+
map,
18+
switchMap,
19+
take,
20+
} from 'rxjs/operators';
21+
22+
import { AuthService } from '../core/auth/auth.service';
23+
import { BitstreamDataService } from '../core/data/bitstream-data.service';
24+
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
25+
import { FeatureID } from '../core/data/feature-authorization/feature-id';
26+
import { RemoteData } from '../core/data/remote-data';
27+
import { HardRedirectService } from '../core/services/hard-redirect.service';
28+
import { redirectOn4xx } from '../core/shared/authorized.operators';
29+
import {
30+
Bitstream,
31+
BITSTREAM_PAGE_LINKS_TO_FOLLOW,
32+
} from '../core/shared/bitstream.model';
33+
import { FileService } from '../core/shared/file.service';
34+
import { getFirstCompletedRemoteData } from '../core/shared/operators';
35+
import {
36+
hasValue,
37+
isNotEmpty,
38+
} from '../utils/empty.util';
39+
40+
/**
41+
* Guard that handles bitstream download authorization and redirection logic.
42+
* This guard intercepts bitstream download requests and performs the following checks and actions:
43+
*
44+
* 1. **Retrieves the bitstream** by ID from the route parameters
45+
* 2. **Checks authorization** using the CanDownload feature permission
46+
* 3. **Determines authentication status** of the current user
47+
* 4. **Handles different scenarios**:
48+
* - **Authorized + Logged in**: Retrieves a secure download link and redirects to it
49+
* - **Authorized + Not logged in + No access token**: Direct redirect to bitstream content URL
50+
* - **Not authorized + Has access token**: Redirect to content URL with access token appended
51+
* - **Not authorized + Logged in**: Redirect to forbidden page
52+
* - **Not authorized + Not logged in**: Store current URL and redirect to login page
53+
*
54+
* @param route - The activated route snapshot containing the bitstream ID and optional access token
55+
* @param state - The router state snapshot
56+
* @param bitstreamDataService - Service for retrieving bitstream data
57+
* @param authorizationService - Service for checking download authorization
58+
* @param auth - Service for authentication operations
59+
* @param fileService - Service for retrieving secure file download links
60+
* @param hardRedirectService - Service for performing hard redirects to download URLs
61+
* @param router - Angular router for navigation
62+
* @returns Observable that emits a UrlTree for navigation or boolean to allow/prevent route activation
63+
*/
64+
export const bitstreamDownloadRedirectGuard: CanActivateFn = (
65+
route: ActivatedRouteSnapshot,
66+
state: RouterStateSnapshot,
67+
bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
68+
authorizationService: AuthorizationDataService = inject(AuthorizationDataService),
69+
auth: AuthService = inject(AuthService),
70+
fileService: FileService = inject(FileService),
71+
hardRedirectService: HardRedirectService = inject(HardRedirectService),
72+
router: Router = inject(Router),
73+
): Observable<UrlTree | boolean> => {
74+
75+
const bitstreamId = route.params.id;
76+
const accessToken: string = route.queryParams.accessToken;
77+
78+
return bitstreamDataService.findById(bitstreamId, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW).pipe(
79+
getFirstCompletedRemoteData(),
80+
redirectOn4xx(router, auth),
81+
switchMap((rd: RemoteData<Bitstream>) => {
82+
if (rd.hasSucceeded && !rd.hasNoContent) {
83+
const bitstream = rd.payload;
84+
const isAuthorized$ = authorizationService.isAuthorized(FeatureID.CanDownload, bitstream.self);
85+
const isLoggedIn$ = auth.isAuthenticated();
86+
return combineLatest([isAuthorized$, isLoggedIn$, of(bitstream)]);
87+
} else {
88+
return of([false, false, null]);
89+
}
90+
}),
91+
filter(([isAuthorized, isLoggedIn, bitstream]: [boolean, boolean, Bitstream]) => hasValue(isAuthorized) && hasValue(isLoggedIn)),
92+
take(1),
93+
switchMap(([isAuthorized, isLoggedIn, bitstream]: [boolean, boolean, Bitstream]) => {
94+
if (isAuthorized && isLoggedIn) {
95+
return fileService.retrieveFileDownloadLink(bitstream._links.content.href).pipe(
96+
filter((fileLink) => hasValue(fileLink)),
97+
take(1),
98+
map((fileLink) => {
99+
return [isAuthorized, isLoggedIn, bitstream, fileLink];
100+
}));
101+
} else {
102+
return of([isAuthorized, isLoggedIn, bitstream, '']);
103+
}
104+
}),
105+
map(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => {
106+
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
107+
hardRedirectService.redirect(fileLink, null, true);
108+
return false;
109+
} else if (isAuthorized && !isLoggedIn && !hasValue(accessToken)) {
110+
hardRedirectService.redirect(bitstream._links.content.href, null, true);
111+
return false;
112+
} else if (!isAuthorized) {
113+
if (hasValue(accessToken)) {
114+
hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken, null, true);
115+
return false;
116+
} else if (isLoggedIn) {
117+
return router.createUrlTree([getForbiddenRoute()]);
118+
} else if (!isLoggedIn) {
119+
auth.setRedirectUrl(router.url);
120+
return router.createUrlTree(['login']);
121+
}
122+
}
123+
}),
124+
);
125+
};

0 commit comments

Comments
 (0)