Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { RESTRICTED_ACCESS_MODULE_PATH } from '../../app-routing-paths';
// End UMD Customization
import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
Expand Down Expand Up @@ -97,16 +98,16 @@ describe('BitstreamDownloadPageComponent', () => {
self: { href: 'bitstream-self-link' },
},
});

activatedRoute = {
data: observableOf({
bitstream: createSuccessfulRemoteDataObject(
bitstream,
),
bitstream: createSuccessfulRemoteDataObject(bitstream),
}),
params: observableOf({
id: 'testid',
}),
queryParams: observableOf({
accessToken: undefined,
}),
};

router = jasmine.createSpyObj('router', ['navigateByUrl']);
Expand All @@ -118,7 +119,10 @@ describe('BitstreamDownloadPageComponent', () => {
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
getLinks: observableOf([mocklink, mocklink2]),
});
matomoService = jasmine.createSpyObj('MatomoService', ['appendVisitorId']);
matomoService = jasmine.createSpyObj('MatomoService', {
appendVisitorId: observableOf(''),
isMatomoEnabled$: observableOf(true),
});
matomoService.appendVisitorId.and.callFake((link) => observableOf(link));
}

Expand All @@ -138,6 +142,7 @@ describe('BitstreamDownloadPageComponent', () => {
{ provide: PLATFORM_ID, useValue: 'server' },
{ provide: Location, useValue: location },
{ provide: DSONameService, useValue: dsoNameService },
{ provide: ConfigurationDataService, useValue: {} },
],
})
.compileComponents();
Expand Down Expand Up @@ -240,4 +245,42 @@ describe('BitstreamDownloadPageComponent', () => {
// End UMD Customization
});
});

describe('when Matomo is enabled', () => {
beforeEach(waitForAsync(() => {
init();
(matomoService.appendVisitorId as jasmine.Spy).and.callFake((link) => observableOf(link + '?visitorId=12345'));
initTestbed();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BitstreamDownloadPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should append visitor ID to the file link', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(matomoService.appendVisitorId).toHaveBeenCalledWith('content-url-with-headers');
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers?visitorId=12345');
});
}));
});

describe('when Matomo is not enabled', () => {
beforeEach(waitForAsync(() => {
init();
(matomoService.isMatomoEnabled$ as jasmine.Spy).and.returnValue(observableOf(false));
initTestbed();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BitstreamDownloadPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should not append visitor ID to the file link', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(matomoService.appendVisitorId).not.toHaveBeenCalled();
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
});
}));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import {
import {
Component,
Inject,
inject,
OnInit,
PLATFORM_ID,
} from '@angular/core';
import {
ActivatedRoute,
Params,
Router,
} from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
Expand All @@ -35,6 +37,7 @@ import { RESTRICTED_ACCESS_MODULE_PATH } from '../../app-routing-paths';
// End UMD Customization
import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { RemoteData } from '../../core/data/remote-data';
Expand Down Expand Up @@ -69,6 +72,8 @@ export class BitstreamDownloadPageComponent implements OnInit {
bitstream$: Observable<Bitstream>;
bitstreamRD$: Observable<RemoteData<Bitstream>>;

configService = inject(ConfigurationDataService);

constructor(
private route: ActivatedRoute,
protected router: Router,
Expand All @@ -91,6 +96,10 @@ export class BitstreamDownloadPageComponent implements OnInit {
}

ngOnInit(): void {
const accessToken$: Observable<string> = this.route.queryParams.pipe(
map((queryParams: Params) => queryParams?.accessToken || null),
take(1),
);

this.bitstreamRD$ = this.route.data.pipe(
map((data) => data.bitstream));
Expand All @@ -104,45 +113,58 @@ export class BitstreamDownloadPageComponent implements OnInit {
switchMap((bitstream: Bitstream) => {
const isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(bitstream) ? bitstream.self : undefined);
const isLoggedIn$ = this.auth.isAuthenticated();
return observableCombineLatest([isAuthorized$, isLoggedIn$, observableOf(bitstream)]);
const isMatomoEnabled$ = this.matomoService.isMatomoEnabled$();
return observableCombineLatest([isAuthorized$, isLoggedIn$, isMatomoEnabled$, accessToken$, observableOf(bitstream)]);
}),
filter(([isAuthorized, isLoggedIn, bitstream]: [boolean, boolean, Bitstream]) => hasValue(isAuthorized) && hasValue(isLoggedIn)),
filter(([isAuthorized, isLoggedIn, isMatomoEnabled, accessToken, bitstream]: [boolean, boolean, boolean, string, Bitstream]) => (hasValue(isAuthorized) && hasValue(isLoggedIn)) || hasValue(accessToken)),
take(1),
switchMap(([isAuthorized, isLoggedIn, bitstream]: [boolean, boolean, Bitstream]) => {
switchMap(([isAuthorized, isLoggedIn, isMatomoEnabled, accessToken, bitstream]: [boolean, boolean, boolean, string, Bitstream]) => {
if (isAuthorized && isLoggedIn) {
return this.fileService.retrieveFileDownloadLink(bitstream._links.content.href).pipe(
filter((fileLink) => hasValue(fileLink)),
take(1),
map((fileLink) => {
return [isAuthorized, isLoggedIn, bitstream, fileLink];
return [isAuthorized, isLoggedIn, isMatomoEnabled, bitstream, fileLink];
}));
} else if (hasValue(accessToken)) {
return [[isAuthorized, !isLoggedIn, isMatomoEnabled, bitstream, '', accessToken]];
} else {
return [[isAuthorized, isLoggedIn, bitstream, bitstream._links.content.href]];
return [[isAuthorized, isLoggedIn, isMatomoEnabled, bitstream, bitstream._links.content.href]];
}
}),
switchMap(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) =>
this.matomoService.appendVisitorId(fileLink)
.pipe(
map((fileLinkWithVisitorId) => [isAuthorized, isLoggedIn, bitstream, fileLinkWithVisitorId]),
),
),
).subscribe(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => {
switchMap(([isAuthorized, isLoggedIn, isMatomoEnabled, bitstream, fileLink, accessToken]: [boolean, boolean, boolean, Bitstream, string, string]) => {
if (isMatomoEnabled) {
return this.matomoService.appendVisitorId(fileLink).pipe(
map((fileLinkWithVisitorId) => [isAuthorized, isLoggedIn, bitstream, fileLinkWithVisitorId, accessToken]),
);
}
return observableOf([isAuthorized, isLoggedIn, bitstream, fileLink, accessToken]);
}),
).subscribe(([isAuthorized, isLoggedIn, bitstream, fileLink, accessToken]: [boolean, boolean, Bitstream, string, string]) => {
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
this.hardRedirectService.redirect(fileLink);
} else if (isAuthorized && !isLoggedIn) {
} else if (isAuthorized && !isLoggedIn && !hasValue(accessToken)) {
this.hardRedirectService.redirect(fileLink);
// UMD Customization
} else if (!isAuthorized && isLoggedIn) {
// This can happen due to a "Campus" group IP restrictions
void this.router.navigateByUrl(
`${RESTRICTED_ACCESS_MODULE_PATH}/${bitstream.uuid}`,
{ replaceUrl: true },
);
} else if (!isAuthorized && !isLoggedIn) {
void this.router.navigateByUrl(`${RESTRICTED_ACCESS_MODULE_PATH}/${bitstream.uuid}`,
{ replaceUrl: true },
);
// End UMD Customization
} else if (!isAuthorized) {
// Either we have an access token, or we are logged in, or we are not logged in.
// For now, the access token does not care if we are logged in or not.
if (hasValue(accessToken)) {
this.hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken);
} else if (isLoggedIn) {
// UMD Customization
// This can happen due to a "Campus" group IP restrictions
void this.router.navigateByUrl(
`${RESTRICTED_ACCESS_MODULE_PATH}/${bitstream.uuid}`,
{ replaceUrl: true },
);
// End UMD Customization
} else if (!isLoggedIn) {
// UMD Customization
void this.router.navigateByUrl(`${RESTRICTED_ACCESS_MODULE_PATH}/${bitstream.uuid}`,
{ replaceUrl: true },
);
// End UMD Customization
}
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/footer/footer.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class FooterComponent implements OnInit {
}

showCookieSettings() {
if (hasValue(this.cookies)) {
if (hasValue(this.cookies) && this.cookies.showSettings instanceof Function) {
this.cookies.showSettings();
}
return false;
Expand Down
3 changes: 2 additions & 1 deletion src/app/shared/cookies/browser-klaro.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { EPerson } from '../../core/eperson/models/eperson.model';
import { CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
import { CookieService } from '../../core/services/cookie.service';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { MATOMO_ENABLED } from '../../statistics/matomo.service';
import {
hasValue,
isEmpty,
Expand Down Expand Up @@ -86,7 +87,7 @@ export class BrowserKlaroService extends KlaroService {

private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics';

private readonly MATOMO_ENABLED = 'matomo.enabled';
private readonly MATOMO_ENABLED = MATOMO_ENABLED;

/**
* Initial Klaro configuration
Expand Down
2 changes: 1 addition & 1 deletion src/app/shared/cookies/klaro.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
/**
* Abstract class representing a service for handling Klaro consent preferences and UI
*/
@Injectable()
@Injectable({ providedIn: 'root' })
export abstract class KlaroService {
/**
* Initializes the service
Expand Down
54 changes: 49 additions & 5 deletions src/app/statistics/matomo.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
Injector,
runInInjectionContext,
} from '@angular/core';
import {
fakeAsync,
TestBed,
Expand All @@ -23,6 +27,7 @@ import {
createSuccessfulRemoteDataObject$,
} from '../shared/remote-data.utils';
import {
MATOMO_ENABLED,
MATOMO_SITE_ID,
MATOMO_TRACKER_URL,
MatomoService,
Expand Down Expand Up @@ -52,6 +57,7 @@ describe('MatomoService', () => {
{ provide: KlaroService, useValue: klaroService },
{ provide: NativeWindowService, useValue: nativeWindowService },
{ provide: ConfigurationDataService, useValue: configService },
{ provide: Injector, useValue: TestBed },
],
});

Expand All @@ -69,11 +75,13 @@ describe('MatomoService', () => {
});

it('should call setConsentGiven when consent is true', () => {
service.matomoTracker = matomoTracker;
service.changeMatomoConsent(true);
expect(matomoTracker.setConsentGiven).toHaveBeenCalled();
});

it('should call forgetConsentGiven when consent is false', () => {
service.matomoTracker = matomoTracker;
service.changeMatomoConsent(false);
expect(matomoTracker.forgetConsentGiven).toHaveBeenCalled();
});
Expand All @@ -84,10 +92,16 @@ describe('MatomoService', () => {
configService.findByPropertyName.withArgs(MATOMO_TRACKER_URL).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['http://matomo'] })),
);
configService.findByPropertyName.withArgs(MATOMO_ENABLED).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['true'] })),
);
configService.findByPropertyName.withArgs(MATOMO_SITE_ID).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { values: ['1'] })));
klaroService.getSavedPreferences.and.returnValue(of({ matomo: true }));
service.init();

runInInjectionContext(TestBed, () => {
service.init();
});

expect(matomoTracker.setConsentGiven).toHaveBeenCalled();
expect(matomoInitializer.initializeTracker).toHaveBeenCalledWith({
Expand All @@ -96,36 +110,66 @@ describe('MatomoService', () => {
});
});

it('should initialize tracker with REST configuration correct parameters in production', () => {
it('should initialize tracker with REST configuration correct parameters in production', fakeAsync(() => {
environment.production = true;
environment.matomo = { trackerUrl: '' };
configService.findByPropertyName.withArgs(MATOMO_TRACKER_URL).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['http://matomo'] })),
);
configService.findByPropertyName.withArgs(MATOMO_ENABLED).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['true'] })),
);
configService.findByPropertyName.withArgs(MATOMO_SITE_ID).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { values: ['1'] })));
klaroService.getSavedPreferences.and.returnValue(of({ matomo: true }));

service.init();
runInInjectionContext(TestBed, () => {
service.init();
});

tick();

expect(matomoTracker.setConsentGiven).toHaveBeenCalled();
expect(matomoInitializer.initializeTracker).toHaveBeenCalledWith({
siteId: '1',
trackerUrl: 'http://matomo',
});
});
}));

it('should not initialize tracker if not in production', () => {
environment.production = false;

service.init();
runInInjectionContext(TestBed, () => {
service.init();
});

expect(matomoInitializer.initializeTracker).not.toHaveBeenCalled();
});

it('should not initialize tracker if matomo is disabled', () => {
environment.production = true;
environment.matomo = { trackerUrl: '' };
configService.findByPropertyName.withArgs(MATOMO_TRACKER_URL).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['http://example.com'] })),
);
configService.findByPropertyName.withArgs(MATOMO_ENABLED).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['false'] })),
);
configService.findByPropertyName.withArgs(MATOMO_SITE_ID).and.returnValue(
createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { values: ['1'] })));
klaroService.getSavedPreferences.and.returnValue(of({ matomo: true }));

runInInjectionContext(TestBed, () => {
service.init();
});

expect(matomoInitializer.initializeTracker).not.toHaveBeenCalled();
});

describe('with visitorId set', () => {
beforeEach(() => {
matomoTracker.getVisitorId.and.returnValue(Promise.resolve('12345'));
service.matomoTracker = matomoTracker;
});

it('should add trackerId parameter', fakeAsync(() => {
Expand Down
Loading