diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 985851c321..6114f13511 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -3,7 +3,7 @@ import { Meta, Title } from '@angular/platform-browser'; import { NavigationEnd, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, of } from 'rxjs'; +import { Observable, of as observableOf, of } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Item } from '../shared/item.model'; @@ -23,6 +23,7 @@ import { DSONameService } from '../breadcrumbs/dso-name.service'; import { HardRedirectService } from '../services/hard-redirect.service'; import { getMockStore } from '@ngrx/store/testing'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; describe('MetadataService', () => { let metadataService: MetadataService; @@ -38,6 +39,7 @@ describe('MetadataService', () => { let rootService: RootDataService; let translateService: TranslateService; let hardRedirectService: HardRedirectService; + let authorizationService: AuthorizationDataService; let router: Router; let store; @@ -76,6 +78,9 @@ describe('MetadataService', () => { hardRedirectService = jasmine.createSpyObj( { getCurrentOrigin: 'https://request.org', }); + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) + }); // @ts-ignore store = getMockStore({ initialState }); @@ -92,7 +97,8 @@ describe('MetadataService', () => { undefined, rootService, store, - hardRedirectService + hardRedirectService, + authorizationService ); }); @@ -300,6 +306,24 @@ describe('MetadataService', () => { }); })); + describe('bitstream not download allowed', () => { + it('should not have citation_pdf_url', fakeAsync(() => { + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3])); + (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); + + (metadataService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + } + } + }); + tick(); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_pdf_url' })); + })); + + }); + describe('no primary Bitstream', () => { it('should link to first and only Bitstream regardless of format', fakeAsync(() => { (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3])); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 1c6946b0d3..0569115b59 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -18,7 +18,11 @@ import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bitstream } from '../shared/bitstream.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, + getDownloadableBitstream +} from '../shared/operators'; import { RootDataService } from '../data/root-data.service'; import { getBitstreamDownloadRoute } from '../../app-routing-paths'; import { BundleDataService } from '../data/bundle-data.service'; @@ -32,6 +36,7 @@ import { createSelector, select, Store } from '@ngrx/store'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { coreSelector } from '../core.selectors'; import { CoreState } from '../core.reducers'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; /** * The base selector function to select the metaTag section in the store @@ -82,6 +87,7 @@ export class MetadataService { private rootService: RootDataService, private store: Store, private hardRedirectService: HardRedirectService, + private authorizationService: AuthorizationDataService ) { } @@ -296,7 +302,6 @@ export class MetadataService { ).pipe( getFirstSucceededRemoteDataPayload(), switchMap((bundle: Bundle) => - // First try the primary bitstream bundle.primaryBitstream.pipe( getFirstCompletedRemoteData(), @@ -307,13 +312,14 @@ export class MetadataService { return null; } }), + getDownloadableBitstream(this.authorizationService), // return the bundle as well so we can use it again if there's no primary bitstream map((bitstream: Bitstream) => [bundle, bitstream]) ) ), switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => { if (hasValue(primaryBitstream)) { - // If there was a primary bitstream, emit its link + // If there was a downloadable primary bitstream, emit its link return [getBitstreamDownloadRoute(primaryBitstream)]; } else { // Otherwise consider the regular bitstreams in the bundle @@ -321,8 +327,8 @@ export class MetadataService { getFirstCompletedRemoteData(), switchMap((bitstreamRd: RemoteData>) => { if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) { - // If there's only one bitstream in the bundle, emit its link - return [getBitstreamDownloadRoute(bitstreamRd.payload.page[0])]; + // If there's only one bitstream in the bundle, emit its link if its downloadable + return this.getBitLinkIfDownloadable(bitstreamRd.payload.page[0], bitstreamRd); } else { // Otherwise check all bitstreams to see if one matches the format whitelist return this.getFirstAllowedFormatBitstreamLink(bitstreamRd); @@ -342,6 +348,20 @@ export class MetadataService { } } + getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData>): Observable { + return observableOf(bitstream).pipe( + getDownloadableBitstream(this.authorizationService), + switchMap((bit: Bitstream) => { + if (hasValue(bit)) { + return [getBitstreamDownloadRoute(bit)]; + } else { + // Otherwise check all bitstreams to see if one matches the format whitelist + return this.getFirstAllowedFormatBitstreamLink(bitstreamRd); + } + }) + ); + } + /** * For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream with a MIME type * included in {@linkcode CITATION_PDF_URL_MIMETYPES} @@ -388,9 +408,14 @@ export class MetadataService { // for the link at the end map((format: BitstreamFormat) => [bitstream, format]) )), - // Filter out only pairs with whitelisted formats - filter(([, format]: [Bitstream, BitstreamFormat]) => - hasValue(format) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)), + // Check if bitstream downloadable + switchMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe( + getDownloadableBitstream(this.authorizationService), + map((bit: Bitstream) => [bit, format]) + )), + // Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check + filter(([bitstream, format]: [Bitstream, BitstreamFormat]) => + hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)), // We only need 1 take(1), // Emit the link of the match diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index ea2a0283eb..bc24489f08 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,5 +1,5 @@ import { Router, UrlTree } from '@angular/router'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { debounceTime, filter, @@ -27,6 +27,9 @@ import { getForbiddenRoute, getPageNotFoundRoute } from '../../app-routing-paths import { getEndUserAgreementPath } from '../../info/info-routing-paths'; import { AuthService } from '../auth/auth.service'; import { InjectionToken } from '@angular/core'; +import { Bitstream } from './bitstream.model'; +import { FeatureID } from '../data/feature-authorization/feature-id'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<(dueTime: number) => (source: Observable) => Observable>('debounceTime', { providedIn: 'root', @@ -355,3 +358,21 @@ export const metadataFieldsToString = () => return fieldSchemaArray.map((fieldSchema: { field: MetadataField, schema: MetadataSchema }) => fieldSchema.schema.prefix + '.' + fieldSchema.field.toString()); }) ); + +/** + * Operator to check if the given bitstream is downloadable + */ +export const getDownloadableBitstream = (authService: AuthorizationDataService) => + (source: Observable): Observable => + source.pipe( + switchMap((bit: Bitstream) => { + if (hasValue(bit)) { + return authService.isAuthorized(FeatureID.CanDownload, bit.self).pipe( + map((canDownload: boolean) => { + return canDownload ? bit : null; + })); + } else { + return observableOf(null); + } + }) + );