Merge pull request #1597 from atmire/w2p-90263_issue-8205_no-embargoed-files-on-google-scholar-meta-tag

Fix for embargoed file links on Google Scholar Meta Tag "citation_pdf_url"
This commit is contained in:
Tim Donohue
2022-04-19 17:12:40 -05:00
committed by GitHub
3 changed files with 81 additions and 11 deletions

View File

@@ -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]));

View File

@@ -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<CoreState>,
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<PaginatedList<Bitstream>>) => {
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<PaginatedList<Bitstream>>): Observable<string> {
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

View File

@@ -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<<T>(dueTime: number) => (source: Observable<T>) => Observable<T>>('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<Bitstream>): Observable<Bitstream | null> =>
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);
}
})
);