From 472154c0df032a55ea4585eca3b1422bbba1feef Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 21 Aug 2020 17:50:43 +0200 Subject: [PATCH] 72635: Cookie preferences first committ --- package.json | 1 + src/app/app.component.ts | 10 +- .../shared/cookies/cookies.service.spec.ts | 12 ++ src/app/shared/cookies/cookies.service.ts | 192 ++++++++++++++++++ src/assets/i18n/en.json5 | 35 ++++ src/modules/app/browser-app.module.ts | 5 + yarn.lock | 5 + 7 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/cookies/cookies.service.spec.ts create mode 100644 src/app/shared/cookies/cookies.service.ts diff --git a/package.json b/package.json index 52afb7c4c0..c58eb40d0a 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "json5": "^2.1.0", "jsonschema": "1.2.2", "jwt-decode": "^2.2.0", + "klaro": "^0.5.34", "moment": "^2.22.1", "morgan": "^1.9.1", "ng-mocks": "^8.1.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fae2df3220..4ee0da048e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -5,7 +5,7 @@ import { Component, HostListener, Inject, - OnInit, + OnInit, Optional, ViewEncapsulation } from '@angular/core'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; @@ -31,8 +31,7 @@ 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 { CookiesService } from './shared/cookies/cookies.service'; @Component({ selector: 'ds-app', @@ -69,7 +68,8 @@ export class AppComponent implements OnInit, AfterViewInit { private cssService: CSSVariableService, private menuService: MenuService, private windowService: HostWindowService, - private localeService: LocaleService + private localeService: LocaleService, + @Optional() private cookiesService: CookiesService ) { /* Use models object so all decorators are actually called */ this.models = models; @@ -91,6 +91,8 @@ export class AppComponent implements OnInit, AfterViewInit { console.info(environment); } this.storeCSSVariables(); + + this.cookiesService.initialize(); } ngOnInit() { diff --git a/src/app/shared/cookies/cookies.service.spec.ts b/src/app/shared/cookies/cookies.service.spec.ts new file mode 100644 index 0000000000..aebf8ab956 --- /dev/null +++ b/src/app/shared/cookies/cookies.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { CookiesService } from './cookies.service'; + +describe('CookiesService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: CookiesService = TestBed.get(CookiesService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/cookies/cookies.service.ts b/src/app/shared/cookies/cookies.service.ts new file mode 100644 index 0000000000..7f625681c5 --- /dev/null +++ b/src/app/shared/cookies/cookies.service.ts @@ -0,0 +1,192 @@ +import { Injectable } from '@angular/core'; +import * as Klaro from 'klaro' +import { BehaviorSubject } from 'rxjs'; +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'; +import { TranslateService } from '@ngx-translate/core'; + +export const HAS_AGREED_END_USER = 'hasAgreedEndUser'; +export const KLARO = 'klaro'; + +const cookieNameMessagePrefix = 'cookies.consent.app.title.'; +const cookieDescriptionMessagePrefix = 'cookies.consent.app.description.'; +const cookiePurposeMessagePrefix = 'cookies.consent.purpose.'; + +@Injectable({ + providedIn: 'root' +}) + +export class CookiesService { + + message$: BehaviorSubject = new BehaviorSubject(''); + + klaroConfig = { + /* + 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, + + htmlText: 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: { + consentNotice: { + description: 'We collect and process your personal information for the following purposes: {purposes}.
To learn more, please read our {privacyPolicy}' + }, + purposes: {} + } + }, + apps: [ + { + name: 'token_item', + purposes: ['authentication'], + required: true, + cookies: [ + TOKENITEM + ] + }, + { + name: 'impersonation', + purposes: ['authentication'], + required: true, + cookies: [ + IMPERSONATING_COOKIE + ] + }, + { + name: 'redirect', + purposes: ['authentication'], + required: true, + cookies: [ + REDIRECT_COOKIE + ] + }, + { + name: 'language', + purposes: ['preferences'], + required: true, + cookies: [ + LANG_COOKIE + ] + }, + { + name: 'klaro', + purposes: ['acknowledgement'], + required: true, + cookies: [ + KLARO + ] + }, + { + name: 'has_agreed_end_user', + purposes: ['acknowledgement'], + required: true, + cookies: [ + HAS_AGREED_END_USER + ] + }, + { + name: 'google-analytics', + purposes: ['statistics'], + 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', + ], + + /* + You can define an optional callback function that will be called each time the + consent state for the given app changes. The consent value will be passed as the + first parameter to the function (true=consented). The `app` config will be + passed as the second parameter. + */ + callback: (consent, app) => { + this.message$.next('User consent for app ' + app.name + ': consent=' + consent); + }, + /* + 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, + }, + ] + }; + + constructor(private translateService: TranslateService) { + } + + initialize() { + 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); + }) + }); + + Klaro.show(this.klaroConfig); + } + + private getTitleTranslation(title: string) { + return this.translateService.instant(cookieNameMessagePrefix + title); + } + + private getDescriptionTranslation(description: string) { + return this.translateService.instant(cookieDescriptionMessagePrefix + description); + } + + private getPurposeTranslation(purpose: string) { + return this.translateService.instant(cookiePurposeMessagePrefix + purpose); + } +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index e437548ae9..54a7ec7d2e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -937,6 +937,41 @@ + "cookies.consent.app.title.token_item": "User Token", + + "cookies.consent.app.description.token_item": "User login identification", + + + + "cookies.consent.app.title.impersonation": "Impersonation", + + "cookies.consent.app.description.impersonation": "Switching users", + + + + "cookies.consent.app.title.klaro": "Klaro", + + "cookies.consent.app.description.klaro": "Cookie consent preferences", + + + + + "cookies.consent.app.title.has_agreed_end_user": "Has agreed end user", + + "cookies.consent.app.description.has_agreed_end_user": "", + + + + "cookies.consent.purpose.authentication": "Authentication", + + "cookies.consent.purpose.statistics": "Statistics", + + "cookies.consent.purpose.preferences": "Preferences", + + "cookies.consent.purpose.acknowledgement": "Acknowledgement", + + + "curation-task.task.checklinks.label": "Check Links in Metadata", "curation-task.task.noop.label": "NOOP", diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index 7306e7db6c..be4402fc74 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -23,6 +23,7 @@ import { SubmissionService } from '../../app/submission/submission.service'; import { StatisticsModule } from '../../app/statistics/statistics.module'; import { HardRedirectService } from '../../app/core/services/hard-redirect.service'; import { BrowserHardRedirectService } from '../../app/core/services/browser-hard-redirect.service'; +import { CookiesService } from '../../app/shared/cookies/cookies.service'; export const REQ_KEY = makeStateKey('req'); @@ -83,6 +84,10 @@ export function locationProvider(): Location { provide: CookieService, useClass: ClientCookieService }, + { + provide: CookiesService, + useClass: CookiesService + }, { provide: SubmissionService, useClass: SubmissionService diff --git a/yarn.lock b/yarn.lock index 0b02259f38..5f918fd7e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6165,6 +6165,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.5.34: + version "0.5.34" + resolved "https://registry.yarnpkg.com/klaro/-/klaro-0.5.34.tgz#0b524be96a1bb177fe88ff2603e1be75f494fc98" + integrity sha512-M6KHqlBWpMyYoxOK1icoJMeYsaPT7YhzJIAQ3wdxZWGgBc0sV7xQsf0PsgMUVnuTD0AeC58QegCGEv0qYeq4gw== + 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"