Merge pull request #861 from atmire/Cookie-preferences

Cookie consent preferences
This commit is contained in:
Tim Donohue
2020-09-23 15:14:24 -05:00
committed by GitHub
17 changed files with 876 additions and 39 deletions

View File

@@ -97,6 +97,7 @@
"json5": "^2.1.0",
"jsonschema": "1.2.2",
"jwt-decode": "^2.2.0",
"klaro": "^0.6.3",
"moment": "^2.22.1",
"morgan": "^1.9.1",
"ng-mocks": "^8.1.0",

View File

@@ -1,11 +1,11 @@
import { delay, map, distinctUntilChanged } from 'rxjs/operators';
import { delay, map, distinctUntilChanged, filter, take } from 'rxjs/operators';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
HostListener,
Inject,
OnInit,
OnInit, Optional,
ViewEncapsulation
} from '@angular/core';
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
@@ -31,8 +31,8 @@ import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
import { environment } from '../environments/environment';
import { models } from './core/core.module';
import { LocaleService } from './core/locale/locale.service';
export const LANG_COOKIE = 'language_cookie';
import { hasValue } from './shared/empty.util';
import { KlaroService } from './shared/cookies/klaro.service';
@Component({
selector: 'ds-app',
@@ -69,8 +69,10 @@ export class AppComponent implements OnInit, AfterViewInit {
private cssService: CSSVariableService,
private menuService: MenuService,
private windowService: HostWindowService,
private localeService: LocaleService
private localeService: LocaleService,
@Optional() private cookiesService: KlaroService
) {
/* Use models object so all decorators are actually called */
this.models = models;
// Load all the languages that are defined as active from the config file
@@ -91,6 +93,7 @@ export class AppComponent implements OnInit, AfterViewInit {
console.info(environment);
}
this.storeCSSVariables();
}
ngOnInit() {
@@ -98,6 +101,12 @@ export class AppComponent implements OnInit, AfterViewInit {
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged()
);
this.isNotAuthBlocking$
.pipe(
filter((notBlocking: boolean) => notBlocking),
take(1)
).subscribe(() => this.initializeKlaro());
const env: string = environment.production ? 'Production' : 'Development';
const color: string = environment.production ? 'red' : 'green';
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
@@ -158,4 +167,9 @@ export class AppComponent implements OnInit, AfterViewInit {
);
}
private initializeKlaro() {
if (hasValue(this.cookiesService)) {
this.cookiesService.initialize()
}
}
}

View File

@@ -382,6 +382,14 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
);
}
createPatchFromCache(object: T): Observable<Operation[]> {
const oldVersion$ = this.findByHref(object._links.self.href);
return oldVersion$.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((oldVersion: T) => this.comparator.diff(oldVersion, object)));
}
/**
* Send a PUT request for the specified object
*
@@ -410,18 +418,16 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
* @param {DSpaceObject} object The given object
*/
update(object: T): Observable<RemoteData<T>> {
const oldVersion$ = this.findByHref(object._links.self.href);
return oldVersion$.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
mergeMap((oldVersion: T) => {
const operations = this.comparator.diff(oldVersion, object);
return this.createPatchFromCache(object)
.pipe(
mergeMap((operations: Operation[]) => {
if (isNotEmpty(operations)) {
this.objectCache.addPatch(object._links.self.href, operations);
}
return this.findByHref(object._links.self.href);
}
));
)
);
}
/**

View File

@@ -10,7 +10,7 @@ import { Observable, of as observableOf, combineLatest } from 'rxjs';
import { map, take, flatMap } from 'rxjs/operators';
import { NativeWindowService, NativeWindowRef } from '../services/window.service';
export const LANG_COOKIE = 'language_cookie';
export const LANG_COOKIE = 'dsLanguage';
/**
* This enum defines the possible origin of the languages

View File

@@ -184,4 +184,25 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
setMetadata(key: string, language?: string, ...values: string[]) {
const mdValues: MetadataValue[] = values.map((value: string, index: number) => {
const md = new MetadataValue();
md.value = value;
md.authority = null;
md.confidence = -1;
md.language = language || null;
md.place = index;
return md;
});
if (hasNoValue(this.metadata)) {
this.metadata = Object.create({});
}
this.metadata[key] = mdValues;
}
removeMetadata(key: string) {
delete this.metadata[key];
}
}

View File

@@ -6,5 +6,16 @@
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
<a href="http://www.duraspace.org/">{{ 'footer.link.duraspace' | translate}}</a>
</p>
<ul class="list-unstyled small d-flex justify-content-center mb-0 text-secondary">
<li>
<a href="#" (click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
</li>
<li>
<a routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
</li>
<li>
<a routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
</li>
</ul>
</div>
</footer>

View File

@@ -8,11 +8,33 @@ $footer-logo-height: 55px;
border-top: $footer-border;
text-align: center;
padding: $footer-padding;
padding-bottom: $spacer;
p {
margin: 0;
}
img {
height: $footer-logo-height;
}
ul {
padding-top: $spacer * 0.5;
li {
display: inline-flex;
a {
padding: 0 $spacer/2;
color: inherit
}
&:not(:last-child) {
&:after {
content: '';
border-right: 1px map-get($theme-colors, secondary) solid;
}
}
}
}
}

View File

@@ -1,4 +1,6 @@
import { Component } from '@angular/core';
import { Component, Optional } from '@angular/core';
import { hasValue } from '../shared/empty.util';
import { KlaroService } from '../shared/cookies/klaro.service';
@Component({
selector: 'ds-footer',
@@ -6,7 +8,15 @@ import { Component } from '@angular/core';
templateUrl: 'footer.component.html'
})
export class FooterComponent {
dateObj: number = Date.now();
constructor(@Optional() private cookies: KlaroService) {
}
showCookieSettings() {
if (hasValue(this.cookies)) {
this.cookies.showSettings();
}
return false;
}
}

View File

@@ -0,0 +1,237 @@
import { TestBed } from '@angular/core/testing';
import { BrowserKlaroService, COOKIE_MDFIELD } from './browser-klaro.service';
import { getMockTranslateService } from '../mocks/translate.service.mock';
import { of as observableOf } from 'rxjs'
import { RestResponse } from '../../core/cache/response.models';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { TranslateService } from '@ngx-translate/core';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { CookieService } from '../../core/services/cookie.service';
import { getTestScheduler } from 'jasmine-marbles';
import { MetadataValue } from '../../core/shared/metadata.models';
import { cloneDeep } from 'lodash';
describe('BrowserKlaroService', () => {
let translateService;
let ePersonService;
let authService;
let cookieService;
let user;
let service: BrowserKlaroService;
let mockConfig;
let appName;
let purpose;
let testKey;
beforeEach(() => {
user = new EPerson();
translateService = getMockTranslateService();
ePersonService = jasmine.createSpyObj('ePersonService', {
createPatchFromCache: observableOf([]),
patch: observableOf(new RestResponse(true, 200, 'Ok'))
});
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true),
getAuthenticatedUserFromStore: observableOf(user)
});
cookieService = jasmine.createSpyObj('cookieService', {
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
set: () => {
/* empty */
}
});
TestBed.configureTestingModule({
providers: [
BrowserKlaroService,
{
provide: TranslateService,
useValue: translateService
},
{
provide: EPersonDataService,
useValue: ePersonService,
},
{
provide: AuthService,
useValue: authService
},
{
provide: CookieService,
useValue: cookieService
}
]
});
service = TestBed.get(BrowserKlaroService);
appName = 'testName';
purpose = 'test purpose';
testKey = 'this.is.a.fake.message.key';
mockConfig = {
translations: {
en: {
purposes: {},
test: {
testeritis: testKey
}
}
},
apps: [{
name: appName,
purposes: [purpose]
}],
};
service.klaroConfig = mockConfig;
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('initialize with user', () => {
beforeEach(() => {
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
translateService.get.and.returnValue(observableOf('loading...'));
spyOn(service, 'addAppMessages');
spyOn((service as any), 'initializeUser');
spyOn(service, 'translateConfiguration');
});
it('to call the initialize user method and other methods', () => {
service.initialize();
expect((service as any).initializeUser).toHaveBeenCalledWith(user);
expect(service.addAppMessages).toHaveBeenCalled();
expect(service.translateConfiguration).toHaveBeenCalled();
});
});
describe('to not call the initialize user method, but the other methods', () => {
beforeEach(() => {
spyOn((service as any), 'getUser$').and.returnValue(observableOf(undefined));
translateService.get.and.returnValue(observableOf('loading...'));
spyOn(service, 'addAppMessages');
spyOn((service as any), 'initializeUser');
spyOn(service, 'translateConfiguration');
});
it('to call all ', () => {
service.initialize();
expect((service as any).initializeUser).not.toHaveBeenCalledWith(user);
expect(service.addAppMessages).toHaveBeenCalled();
expect(service.translateConfiguration).toHaveBeenCalled();
});
});
it('addAppMessages', () => {
service.addAppMessages();
expect(mockConfig.translations.en[appName]).toBeDefined();
expect(mockConfig.translations.en.purposes[purpose]).toBeDefined();
});
it('translateConfiguration', () => {
service.translateConfiguration();
expect((service as any).translateService.instant).toHaveBeenCalledWith(testKey);
});
describe('initializeUser when there is a metadata field value', () => {
beforeEach(() => {
user.setMetadata(COOKIE_MDFIELD, undefined, '{}');
spyOn(service, 'restoreSettingsForUsers');
});
it('initializeUser', () => {
(service as any).initializeUser(user);
expect(service.restoreSettingsForUsers).toHaveBeenCalledWith(user);
});
});
describe('initializeUser when there is no metadata field value but there is an anonymous cookie', () => {
const cookie = '{test: \'testt\'}';
beforeEach(() => {
(service as any).cookieService.get.and.returnValue(cookie);
spyOn(service, 'updateSettingsForUsers');
});
it('initializeUser', () => {
(service as any).initializeUser(user);
expect((service as any).cookieService.set).toHaveBeenCalledWith(service.getStorageName(user.uuid), cookie)
expect(service.updateSettingsForUsers).toHaveBeenCalledWith(user);
});
});
describe('getUser$ when there is no one authenticated', () => {
beforeEach(() => {
(service as any).authService.isAuthenticated.and.returnValue(observableOf(false));
});
it('should return undefined', () => {
getTestScheduler().expectObservable((service as any).getUser$()).toBe('(a|)', { a: undefined });
});
});
describe('getUser$ when there someone is authenticated', () => {
beforeEach(() => {
(service as any).authService.isAuthenticated.and.returnValue(observableOf(true));
(service as any).authService.getAuthenticatedUserFromStore.and.returnValue(observableOf(user));
});
it('should return the user', () => {
getTestScheduler().expectObservable((service as any).getUser$()).toBe('(a|)', { a: user });
});
});
describe('getSettingsForUser', () => {
const cookieConsentString = '{test: \'testt\'}';
beforeEach(() => {
user.metadata = {};
user.metadata[COOKIE_MDFIELD] = [Object.assign(new MetadataValue(), { value: cookieConsentString })];
spyOn(JSON, 'parse');
});
it('should return the cookie consents object', () => {
service.getSettingsForUser(user);
expect(JSON.parse).toHaveBeenCalledWith(cookieConsentString);
});
});
describe('setSettingsForUser when there are changes', () => {
const cookieConsent = { test: 'testt' };
const cookieConsentString = '{test: \'testt\'}';
const operation = { op: 'add', path: 'metadata/dc.agreements.cookie', value: cookieConsentString };
let updatedUser;
beforeEach(() => {
updatedUser = cloneDeep(user);
spyOn(updatedUser, 'setMetadata');
spyOn(JSON, 'stringify').and.returnValue(cookieConsentString);
ePersonService.createPatchFromCache.and.returnValue(observableOf([operation]))
});
it('should call patch on the data service', () => {
service.setSettingsForUser(updatedUser, cookieConsent);
expect(updatedUser.setMetadata).toHaveBeenCalledWith(COOKIE_MDFIELD, undefined, cookieConsentString);
expect(ePersonService.patch).toHaveBeenCalledWith(updatedUser, [operation])
});
});
describe('setSettingsForUser when there are no changes', () => {
const cookieConsent = { test: 'testt' };
const cookieConsentString = '{test: \'testt\'}';
let updatedUser;
beforeEach(() => {
updatedUser = cloneDeep(user);
spyOn(updatedUser, 'setMetadata');
spyOn(JSON, 'stringify').and.returnValue(cookieConsentString);
ePersonService.createPatchFromCache.and.returnValue(observableOf([]))
});
it('should not call patch on the data service', () => {
service.setSettingsForUser(updatedUser, cookieConsent);
expect(updatedUser.setMetadata).toHaveBeenCalledWith(COOKIE_MDFIELD, undefined, cookieConsentString);
expect(ePersonService.patch).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,257 @@
import { Injectable } from '@angular/core';
import * as Klaro from 'klaro'
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { TranslateService } from '@ngx-translate/core';
import { environment } from '../../../environments/environment';
import { switchMap, take } from 'rxjs/operators';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { KlaroService } from './klaro.service';
import { hasValue, isNotEmpty } from '../empty.util';
import { CookieService } from '../../core/services/cookie.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { cloneDeep, debounce } from 'lodash';
import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration';
import { Operation } from 'fast-json-patch';
/**
* Metadata field to store a user's cookie consent preferences in
*/
export const COOKIE_MDFIELD = 'dspace.agreements.cookies';
/**
* Prefix key for app title messages
*/
const cookieNameMessagePrefix = 'cookies.consent.app.title.';
/**
* Prefix key for app description messages
*/
const cookieDescriptionMessagePrefix = 'cookies.consent.app.description.';
/**
* Prefix key for app purpose messages
*/
const cookiePurposeMessagePrefix = 'cookies.consent.purpose.';
/**
* Update request debounce in ms
*/
const updateDebounce = 300;
/**
* Browser implementation for the KlaroService, representing a service for handling Klaro consent preferences and UI
*/
@Injectable()
export class BrowserKlaroService extends KlaroService {
/**
* Initial Klaro configuration
*/
klaroConfig = klaroConfiguration;
constructor(
private translateService: TranslateService,
private authService: AuthService,
private ePersonService: EPersonDataService,
private cookieService: CookieService) {
super();
}
/**
* Initializes the service:
* - Retrieves the current authenticated user
* - Checks if the translation service is ready
* - Initialize configuration for users
* - Add and translate klaro configuration messages
*/
initialize() {
this.translateService.setDefaultLang(environment.defaultLanguage);
const user$: Observable<EPerson> = this.getUser$();
const translationServiceReady$ = this.translateService.get('loading.default').pipe(take(1));
observableCombineLatest(user$, translationServiceReady$)
.subscribe(([user, translation]: [EPerson, string]) => {
user = cloneDeep(user);
if (hasValue(user)) {
this.initializeUser(user);
}
/**
* Add all message keys for apps and purposes
*/
this.addAppMessages();
/**
* Subscribe on a message to make sure the translation service is ready
* Translate all keys in the translation section of the configuration
* Show the configuration if the configuration has not been confirmed
*/
this.translateConfiguration();
Klaro.renderKlaro(this.klaroConfig, false);
Klaro.initialize();
});
}
/**
* Initialize configuration for the logged in user
* @param user The authenticated user
*/
private initializeUser(user: EPerson) {
this.klaroConfig.callback = debounce((consent, app) => this.updateSettingsForUsers(user), updateDebounce);
this.klaroConfig.storageName = this.getStorageName(user.uuid);
const anonCookie = this.cookieService.get(ANONYMOUS_STORAGE_NAME_KLARO);
if (hasValue(this.getSettingsForUser(user))) {
this.restoreSettingsForUsers(user);
} else if (hasValue(anonCookie)) {
this.cookieService.set(this.getStorageName(user.uuid), anonCookie);
this.updateSettingsForUsers(user);
}
}
/**
* Retrieves the currently logged in user
* Returns undefined when no one is logged in
*/
private getUser$() {
return this.authService.isAuthenticated()
.pipe(
take(1),
switchMap((loggedIn: boolean) => {
if (loggedIn) {
return this.authService.getAuthenticatedUserFromStore();
}
return observableOf(undefined);
}),
take(1)
);
}
/**
* Create a title translation key
* @param title
*/
private getTitleTranslation(title: string) {
return cookieNameMessagePrefix + title;
}
/**
* Create a description translation key
* @param description
*/
private getDescriptionTranslation(description: string) {
return cookieDescriptionMessagePrefix + description;
}
/**
* Create a purpose translation key
* @param purpose
*/
private getPurposeTranslation(purpose: string) {
return cookiePurposeMessagePrefix + purpose;
}
/**
* Show the cookie consent form
*/
showSettings() {
Klaro.show(this.klaroConfig);
}
/**
* Add message keys for all apps and purposes
*/
addAppMessages() {
this.klaroConfig.apps.forEach((app) => {
this.klaroConfig.translations.en[app.name] = { title: this.getTitleTranslation(app.name), description: this.getDescriptionTranslation(app.name) };
app.purposes.forEach((purpose) => {
this.klaroConfig.translations.en.purposes[purpose] = this.getPurposeTranslation(purpose);
})
});
}
/**
* Translate the translation section from the Klaro configuration
*/
translateConfiguration() {
/**
* Make sure the fallback language is english
*/
this.translateService.setDefaultLang(environment.defaultLanguage);
this.translate(this.klaroConfig.translations.en);
}
/**
* Translate string values in an object
* @param object The object containing translation keys
*/
private translate(object) {
if (typeof (object) === 'string') {
return this.translateService.instant(object);
}
Object.entries(object).forEach(([key, value]: [string, any]) => {
object[key] = this.translate(value);
});
return object;
}
/**
* Retrieves the stored Klaro consent settings for a user
* @param user The user to resolve the consent for
*/
getSettingsForUser(user: EPerson) {
const mdValue = user.firstMetadataValue(COOKIE_MDFIELD);
return hasValue(mdValue) ? JSON.parse(mdValue) : undefined;
}
/**
* Stores the Klaro consent settings for a user in a metadata field
* @param user The user to save the settings for
* @param config The consent settings for the user to save
*/
setSettingsForUser(user: EPerson, config: object) {
if (isNotEmpty(config)) {
user.setMetadata(COOKIE_MDFIELD, undefined, JSON.stringify(config));
} else {
user.removeMetadata(COOKIE_MDFIELD);
}
this.ePersonService.createPatchFromCache(user)
.pipe(
take(1),
switchMap((operations: Operation[]) => {
if (isNotEmpty(operations)) {
return this.ePersonService.patch(user, operations)
}
return observableOf(undefined)
}
)
).subscribe();
}
/**
* Restores the users consent settings cookie based on the user's stored consent settings
* @param user The user to save the settings for
*/
restoreSettingsForUsers(user: EPerson) {
this.cookieService.set(this.getStorageName(user.uuid), this.getSettingsForUser(user));
}
/**
* Stores the consent settings for a user based on the current cookie for this user
* @param user
*/
updateSettingsForUsers(user: EPerson) {
this.setSettingsForUser(user, this.cookieService.get(this.getStorageName(user.uuid)))
}
/**
* Create the storage name for klaro cookies based on the user's identifier
* @param identifier The user's uuid
*/
getStorageName(identifier: string) {
return 'klaro-' + identifier
}
}

View File

@@ -0,0 +1,159 @@
import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
import { LANG_COOKIE } from '../../core/locale/locale.service';
/**
* Cookie for has_agreed_end_user
*/
export const HAS_AGREED_END_USER = 'dsHasAgreedEndUser';
/**
* Storage name used to store klaro cookie
*/
export const ANONYMOUS_STORAGE_NAME_KLARO = 'klaro-anonymous';
/**
* Klaro configuration
* For more information see https://kiprotect.com/docs/klaro/annotated-config
*/
export const klaroConfiguration: any = {
storageName: ANONYMOUS_STORAGE_NAME_KLARO,
privacyPolicy: '/info/privacy',
/*
Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in
the consent notice. We strongly advise against using this under most
circumstances, as it keeps the user from customizing his/her consent choices.
*/
hideLearnMore: false,
/*
Setting 'acceptAll' to 'true' will show an "accept all" button in the notice and
modal, which will enable all third-party apps if the user clicks on it. If set
to 'false', there will be an "accept" button that will only enable the apps that
are enabled in the consent modal.
*/
acceptAll: true,
/*
You can also set a custom expiration time for the Klaro cookie. By default, it
will expire after 30 days. Only relevant if 'storageMethod' is set to 'cookie'.
*/
cookieExpiresAfterDays: 365,
htmlTexts: true,
/*
You can overwrite existing translations and add translations for your app
descriptions and purposes. See `src/translations/` for a full list of
translations that can be overwritten:
https://github.com/KIProtect/klaro/tree/master/src/translations
*/
translations: {
en: {
acceptAll: 'cookies.consent.accept-all',
acceptSelected: 'cookies.consent.accept-selected',
app: {
optOut: {
description: 'cookies.consent.app.opt-out.description',
title: 'cookies.consent.app.opt-out.title'
},
purpose: 'cookies.consent.app.purpose',
purposes: 'cookies.consent.app.purposes',
required: {
description: 'cookies.consent.app.required.description',
title: 'cookies.consent.app.required.title'
}
},
close: 'cookies.consent.close',
decline: 'cookies.consent.decline',
changeDescription: 'cookies.consent.update',
consentNotice: {
description: 'cookies.consent.content-notice.description',
learnMore: 'cookies.consent.content-notice.learnMore'
},
consentModal: {
description: 'cookies.consent.content-modal.description',
privacyPolicy: {
name: 'cookies.consent.content-modal.privacy-policy.name',
text: 'cookies.consent.content-modal.privacy-policy.text'
},
title: 'cookies.consent.content-modal.title'
},
purposes: {}
}
},
apps: [
{
name: 'authentication',
purposes: ['functional'],
required: true,
cookies: [
TOKENITEM,
IMPERSONATING_COOKIE,
REDIRECT_COOKIE
]
},
{
name: 'preferences',
purposes: ['functional'],
required: true,
cookies: [
LANG_COOKIE
]
},
{
name: 'acknowledgement',
purposes: ['functional'],
required: true,
cookies: [
[/^klaro-.+$/],
HAS_AGREED_END_USER
]
},
{
name: 'google-analytics',
purposes: ['statistical'],
required: false,
cookies: [
// /*
// you an either only provide a cookie name or regular expression (regex) or a list
// consisting of a name or regex, a path and a cookie domain. Providing a path and
// domain is necessary if you have apps that set cookies for a path that is not
// "/", or a domain that is not the current domain. If you do not set these values
// properly, the cookie can't be deleted by Klaro, as there is no way to access the
// path or domain of a cookie in JS. Notice that it is not possible to delete
// cookies that were set on a third-party domain, or cookies that have the HTTPOnly
// attribute: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#new-
// cookie_domain
// */
//
// /*
// This rule will match cookies that contain the string '_pk_' and that are set on
// the path '/' and the domain 'klaro.kiprotect.com'
// */
[/^_ga.?$/],
[/^_gid$/],
//
// /*
// Same as above, only for the 'localhost' domain
// */
// [/^_pk_.*$/, '/', 'localhost'],
//
// /*
// This rule will match all cookies named 'piwik_ignore' that are set on the path
// '/' on the current domain
// */
// 'piwik_ignore',
],
/*
If 'onlyOnce' is set to 'true', the app will only be executed once regardless
how often the user toggles it on and off. This is relevant e.g. for tracking
scripts that would generate new page view events every time Klaro disables and
re-enables them due to a consent change by the user.
*/
onlyOnce: true,
},
],
};

View File

@@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
/**
* Abstract class representing a service for handling Klaro consent preferences and UI
*/
@Injectable()
export abstract class KlaroService {
/**
* Initializes the service
*/
abstract initialize();
/**
* Shows a the dialog with the current consent preferences
*/
abstract showSettings();
}

View File

@@ -3,6 +3,7 @@ import { TranslateService } from '@ngx-translate/core';
export function getMockTranslateService(): TranslateService {
return jasmine.createSpyObj('translateService', {
get: jasmine.createSpy('get'),
instant: jasmine.createSpy('instant')
instant: jasmine.createSpy('instant'),
setDefaultLang: jasmine.createSpy('setDefaultLang')
});
}

View File

@@ -937,6 +937,68 @@
"cookies.consent.accept-all": "Accept all",
"cookies.consent.accept-selected": "Accept selected",
"cookies.consent.app.opt-out.description": "This app is loaded by default (but you can opt out)",
"cookies.consent.app.opt-out.title": "(opt-out)",
"cookies.consent.app.purpose": "purpose",
"cookies.consent.app.required.description": "This application is always required",
"cookies.consent.app.required.title": "(always required)",
"cookies.consent.update": "There were changes since your last visit, please update your consent.",
"cookies.consent.close": "Close",
"cookies.consent.decline": "Decline",
"cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>. <br/> To learn more, please read our {privacyPolicy}.",
"cookies.consent.content-notice.learnMore": "Customize",
"cookies.consent.content-modal.description": "Here you can see and customize the information that we collect about you.",
"cookies.consent.content-modal.privacy-policy.name": "privacy policy",
"cookies.consent.content-modal.privacy-policy.text": "To learn more, please read our {privacyPolicy}.",
"cookies.consent.content-modal.title": "Information that we collect",
"cookies.consent.app.title.authentication": "Authentication",
"cookies.consent.app.description.authentication": "Required for signing you in",
"cookies.consent.app.title.preferences": "Preferences",
"cookies.consent.app.description.preferences": "Required for saving your preferences",
"cookies.consent.app.title.acknowledgement": "Acknowledgement",
"cookies.consent.app.description.acknowledgement": "Required for saving your acknowledgements and consents",
"cookies.consent.app.title.google-analytics": "Google Analytics",
"cookies.consent.app.description.google-analytics": "Allows us to track statistical data",
"cookies.consent.purpose.functional": "Functional",
"cookies.consent.purpose.statistical": "Statistical",
"curation-task.task.checklinks.label": "Check Links in Metadata",
"curation-task.task.noop.label": "NOOP",
@@ -1051,6 +1113,13 @@
"footer.link.duraspace": "DuraSpace",
"footer.link.cookies": "Cookie settings",
"footer.link.privacy-policy": "Privacy policy",
"footer.link.end-user-agreement":"End User Agreement",
"forgot-email.form.header": "Forgot Password",

View File

@@ -21,6 +21,8 @@ import { AuthService } from '../../app/core/auth/auth.service';
import { Angulartics2RouterlessModule } from 'angulartics2/routerlessmodule';
import { SubmissionService } from '../../app/submission/submission.service';
import { StatisticsModule } from '../../app/statistics/statistics.module';
import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service';
import { KlaroService } from '../../app/shared/cookies/klaro.service';
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
import {
BrowserHardRedirectService,
@@ -80,6 +82,10 @@ export function getRequest(transferState: TransferState): any {
provide: CookieService,
useClass: ClientCookieService
},
{
provide: KlaroService,
useClass: BrowserKlaroService
},
{
provide: SubmissionService,
useClass: SubmissionService

View File

@@ -25,7 +25,8 @@ module.exports = {
module: {
noParse: /polyfills-.*\.js/,
rules: [
{ test: /\.ts$/, loader: 'ts-loader',
{
test: /\.ts$/, loader: 'ts-loader',
options: {
configFile: "tsconfig.server.json"
} },

View File

@@ -6211,6 +6211,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
klaro@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/klaro/-/klaro-0.6.3.tgz#b2aaa810d17f073c9a1b5eab618a9f13d5f40fa3"
integrity sha512-rRP37FaJaHHSScHIe3YUdMZJ1asxOF5+C/RMrFB2RzhAUfGVMM5/GiucECM3Si1lhW2LL0xGVymE8JhYZl2Bjg==
last-call-webpack-plugin@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"