UNPKG

klaro

Version:

A simple but powerful consent manager.

345 lines (303 loc) 12.5 kB
/* globals module, require, VERSION */ import React from 'react' import App from './components/app' import ContextualConsentNotice from './components/contextual-consent-notice' import ConsentManager from './consent-manager' import KlaroApi from './utils/api'; import {injectStyles} from './utils/styling' import {render as reactRender} from 'react-dom' import {convertToMap, update} from './utils/maps' import {t, language} from './utils/i18n' import {themes} from './themes' import {currentScript, dataset, applyDataset} from './utils/compat' export {update as updateConfig} from './utils/config' import './scss/klaro.scss' let defaultConfig const defaultTranslations = new Map([]) const eventHandlers = {} const events = {} // When webpack's hot loading is enabled, enable Preact's support for the // React Dev Tools browser extension. if(module.hot) require('preact/debug') export function getElementID(config, ide){ return (config.elementID || 'klaro') + (ide ? '-ide' : '') } export function getElement(config, ide){ const id = getElementID(config, ide) let element = document.getElementById(id) if (element === null){ element = document.createElement('div') element.id = id document.body.appendChild(element) } return element } export function addEventListener(eventType, handler){ if (eventHandlers[eventType] === undefined) eventHandlers[eventType] = [handler] else eventHandlers[eventType].push(handler) // this event did already fire, we call the handler if (events[eventType] !== undefined) for(const event of events[eventType]) if (handler(...event) === false) break } function executeEventHandlers(eventType, ...args){ const handlers = eventHandlers[eventType] if (events[eventType] === undefined) events[eventType] = [args] else events[eventType].push(args) if (handlers !== undefined) for(const handler of handlers){ if (handler(...args) === true) return true } } export function getConfigTranslations(config){ const trans = new Map([]) update(trans, defaultTranslations) update(trans, convertToMap(config.translations || {})) return trans } let cnt = 1 export function render(config, opts){ if (config === undefined) return opts = opts || {} config = validateConfig(config) executeEventHandlers("render", config, opts) // we are using a count here so that we're able to repeatedly open the modal... let showCnt = 0 if (opts.show) showCnt = cnt++ const element = getElement(config) const manager = getManager(config) if (opts.api !== undefined) manager.watch(opts.api) injectStyles(config, themes, element) const lang = language(config) const configTranslations = getConfigTranslations(config) const tt = (...args) => t(configTranslations, lang, config.fallbackLang || 'zz', ...args) const app = reactRender(<App t={tt} lang={lang} manager={manager} config={config} testing={opts.testing} modal={opts.modal} api={opts.api} show={showCnt} />, element) renderContextualConsentNotices(manager, tt, lang, config, opts) return app } export function renderContextualConsentNotices(manager, tt, lang, config, opts){ const notices = [] for(const service of config.services){ const consent = manager.getConsent(service.name) && manager.confirmed const elements = document.querySelectorAll("[data-name='"+service.name+"']") for(const element of elements){ const ds = dataset(element) if (ds.type === 'placeholder') continue if (element.tagName === 'IFRAME' || element.tagName === 'DIV'){ let placeholderElement = element.previousElementSibling if (placeholderElement !== null){ const ds = dataset(placeholderElement) if (ds.type !== "placeholder" || ds.name !== service.name) placeholderElement = null } if (placeholderElement === null){ placeholderElement = document.createElement("DIV") placeholderElement.style.maxWidth = element.width+"px" placeholderElement.style.height = element.height+"px" applyDataset({type: 'placeholder', name: service.name}, placeholderElement) // if consent is already given, we still insert an invisble placeholder that // might be revealed later if the user changes the consent decision if (consent) placeholderElement.style.display = 'none' element.parentElement.insertBefore(placeholderElement, element) const notice = reactRender(<ContextualConsentNotice t={tt} lang={lang} manager={manager} config={config} service={service} style={ds.style} testing={opts.testing} api={opts.api} />, placeholderElement) notices.push(notice) } if (element.tagName === 'IFRAME'){ ds['src'] = element.src } if (ds['modified-by-klaro'] === undefined && element.style.display === undefined) ds['original-display'] = element.style.display ds['modified-by-klaro'] = 'yes' applyDataset(ds, element) if (!consent){ element.src = '' element.style.display = 'none' } } } } return notices } function showKlaroIDE(script) { const baseName = /^(.*)(\/[^/]+)$/.exec(script.src)[1] || '' const element = document.createElement('script') element.src = baseName !== '' ? baseName + '/ide.js' : 'ide.js' element.type = "application/javascript" for(const attribute of element.attributes){ element.setAttribute(attribute.name, attribute.value) } document.head.appendChild(element) } function doOnceLoaded(handler){ if (/complete|interactive|loaded/.test(document.readyState)){ handler() } else { window.addEventListener('DOMContentLoaded', handler) } } function getKlaroId(script){ const klaroId = script.getAttribute('data-klaro-id') if (klaroId !== null) return klaroId const regexMatch = /.*\/privacy-managers\/([a-f0-9]+)\/klaro.*\.js/.exec(script.src) if (regexMatch !== null) return regexMatch[1] return null } function getKlaroApiUrl(script){ const klaroApiUrl = script.getAttribute('data-klaro-api-url') if (klaroApiUrl !== null) return klaroApiUrl const regexMatch = /(http(?:s)?:\/\/[^/]+)\/v1\/privacy-managers\/([a-f0-9]+)\/klaro.*\.js/.exec(script.src) if (regexMatch !== null) return regexMatch[1] return null } function getKlaroConfigName(hashParams, script){ // hash parameters always win if (hashParams.has('klaro-config')){ return hashParams.get('klaro-config') } // afterwards we check the script tag const klaroConfigName = script.getAttribute('data-klaro-config') if (klaroConfigName !== null) return klaroConfigName // if nothing works we return the default value return 'default' } function getHashParams(){ return new Map(decodeURI(location.hash.slice(1)).split("&").map(kv => kv.split("=")).map(kv => (kv.length === 1 ? [kv[0], true] : kv))) } export function validateConfig(config){ const validatedConfig = {...config} if (validatedConfig.version === 2) return validatedConfig if (validatedConfig.apps !== undefined && validatedConfig.services === undefined){ validatedConfig.services = validatedConfig.apps console.warn("Warning, your configuration file is outdated. Please change `apps` to `services`") delete validatedConfig.apps } if (validatedConfig.translations !== undefined){ if (validatedConfig.translations.apps !== undefined && validatedConfig.services === undefined){ validatedConfig.translations.services = validatedConfig.translations.apps console.warn("Warning, your configuration file is outdated. Please change `apps` to `services` in the `translations` key") delete validatedConfig.translations.apps } } return validatedConfig } export function setup(config){ // if no window object is given we return immediately if (window === undefined) return; const script = currentScript("klaro"); const hashParams = getHashParams(); const testing = hashParams.get('klaro-testing'); const initialize = (opts) => { const fullOpts = {...opts, testing: testing} if (!defaultConfig.noAutoLoad && ((!defaultConfig.testing) || fullOpts.testing)) render(defaultConfig, fullOpts) } if (config !== undefined){ // we initialize directly with a config defaultConfig = config; doOnceLoaded(() => initialize({})) } else if (script !== null) { // we initialize with a script tag const klaroId = getKlaroId(script) const klaroApiUrl = getKlaroApiUrl(script) const klaroConfigName = getKlaroConfigName(hashParams, script); if (klaroId !== null){ // we initialize with an API backend const api = new KlaroApi(klaroApiUrl, klaroId, {testing: testing}) if (window.klaroApiConfigs !== undefined){ // the configs were already supplied with the Klaro binary if (executeEventHandlers("apiConfigsLoaded", window.klaroApiConfigs, api) === true){ return } const config = window.klaroApiConfigs.find(config => config.name === klaroConfigName && (config.status === 'active' || testing)) if (config !== undefined){ defaultConfig = config doOnceLoaded(() => initialize({api: api})) } else { executeEventHandlers("apiConfigsFailed", {}) } } else { // we load the configs separately... api.loadConfig(klaroConfigName).then((config) => { // an event handler can interrupt the initialization, e.g. if it wants to perform // its own initialization given the API configs if (executeEventHandlers("apiConfigsLoaded", [config], api) === true){ return } defaultConfig = config doOnceLoaded(() => initialize({api: api})) }).catch((err) => { console.error(err, "cannot load Klaro configs") executeEventHandlers("apiConfigsFailed", err) }) } } else { // we initialize with a local config instead const configName = script.getAttribute('data-klaro-config') || "klaroConfig" defaultConfig = window[configName]; if (defaultConfig !== undefined) doOnceLoaded(() => initialize({})) } } // If requested, we show the Klaro IDE if (hashParams.has('klaro-ide')){ showKlaroIDE(script) } } export function show(config, modal, api){ config = config || defaultConfig render(config, {show: true, modal: modal, api: api}) return false } /* Consent Managers */ const managers = {} export function resetManagers(){ for(const key in Object.keys(managers)) delete managers[key] } export function getManager(config){ config = config || defaultConfig const name = config.storageName || config.cookieName || 'default' // deprecated: cookieName if (managers[name] === undefined) managers[name] = new ConsentManager(validateConfig(config)) return managers[name] } export function version(){ // we remove the 'v' if (VERSION[0] === 'v') return VERSION.slice(1) return VERSION } export {language, defaultConfig, defaultTranslations}