const DEFAULT_TIMEOUT = 10000; function request(url, options, onSuccess, onError) { try { var req = new XMLHttpRequest(); var isTimeout = false; options = options || {}; req.onreadystatechange = function () { if (req.readyState === req.DONE) { if (req.status >= 200 && req.status < 400) { let jsonResponse; try { jsonResponse = JSON.parse(req.responseText); onSuccess(jsonResponse); } catch (error) { return onError(error); } } else if (req.status === 0 && req.timeout > 0) { // Possible timeout, waiting for ontimeout event // Timeout will throw a status = 0 request // onreadystatechange preempts ontimeout // And we can't know for sure at this stage if it's a timeout setTimeout(function () { if (isTimeout) { return; } return onError(new Error('Request to ' + url + ' failed with status: ' + req.status)); }, 500); } else { return onError(new Error('Request to ' + url + ' failed with status: ' + req.status)); } } }; req.open(options.method || 'GET', url, true); if (options.timeout) { req.timeout = options.timeout; } else { req.timeout = DEFAULT_TIMEOUT; } req.ontimeout = function () { isTimeout = true; return onError(new Error('Request to ' + url + ' timed out')); }; var headers = options.headers || {}; for (var name in headers) { req.setRequestHeader(name, headers[name]); } req.send(options.body || null); } catch (error) { return onError(error); } } function addUrlParameter(url, name, value) { url = parseUrl(url); var newParam = name.concat('=', value); var modified = false; findByKey(name, url.params, function (index) { url.params[index] = newParam; modified = true; }); if (!modified) { url.params.push(newParam); } return buildUrl(url); } function removeUrlParameter(url, name) { url = parseUrl(url); findByKey(name, url.params, function (index) { url.params.splice(index--, 1); }); return buildUrl(url); } function parseUrl(url) { var parts = url.split('?'); return { address: parts[0], params: (parts[1] || '').split('&').filter(Boolean), }; } function buildUrl(parts) { return [parts.address, parts.params.join('&')].join('?').replace(/\??$/, ''); } function findByKey(key, keyvals, callback) { key += '='; for (var index = 0; keyvals.length > index; index++) { if (keyvals[index].trim().slice(0, key.length) === key) { var value = keyvals[index].trim().slice(key.length); if (callback) { callback(index, value); } else { return value; } } } } function isCrossOrigin(link) { return (link.protocol !== window.location.protocol || link.hostname !== window.location.hostname || (link.port || '80') !== (window.location.port || '80')); } function getOriginFromLink(link) { var origin = link.protocol.concat('//', link.hostname); if (link.port && link.port !== '80' && link.port !== '443') { origin = origin.concat(':', link.port); } return origin; } function isBrowser() { return typeof module === 'undefined'; } function setCookie({ name, value, lifetime }) { // const encodedValue = encodeURIComponent(value) const encodedValue = value; document.cookie = name .concat('=', encodedValue) .concat('; path=/', '; max-age='.concat(lifetime.toString()), document.location.protocol === 'https:' ? '; Secure' : ''); } function getCookie(name, defaultValue) { name += '='; const cookies = document.cookie.split(';'); let cookie = null; for (let i = 0; i < cookies.length; i++) { let currentCookie = cookies[i].trim(); if (currentCookie.indexOf(name) === 0) { cookie = currentCookie; break; } } if (cookie) { return decodeURIComponent(cookie.trim().slice(name.length)); } return defaultValue || null; } function validateConsentObject(response) { try { if (typeof response !== 'object' || response === null) { return false; } var expectedKeys = ['essential', 'settings', 'usage', 'campaigns']; var allKeysPresent = true; var responseKeysCount = 0; for (var i = 0; i < expectedKeys.length; i++) { if (!(expectedKeys[i] in response)) { allKeysPresent = false; break; } } var allValuesBoolean = true; for (var key in response) { if (response.hasOwnProperty(key)) { responseKeysCount++; if (typeof response[key] !== 'boolean') { allValuesBoolean = false; break; } } } var correctNumberOfKeys = responseKeysCount === expectedKeys.length; } catch (err) { return false; } return allKeysPresent && allValuesBoolean && correctNumberOfKeys; } const COOKIE_DAYS = 365; class GovConsentConfig { constructor(baseUrl) { this.uidFromCookie = findByKey(GovConsentConfig.UID_KEY, document.cookie.split(';')); this.uidFromUrl = findByKey(GovConsentConfig.UID_KEY, parseUrl(location.href).params); this.baseUrl = baseUrl; } } GovConsentConfig.UID_KEY = 'gov_singleconsent_uid'; GovConsentConfig.CONSENTS_COOKIE_NAME = 'cookies_policy'; GovConsentConfig.PREFERENCES_SET_COOKIE_NAME = 'cookies_preferences_set'; GovConsentConfig.COOKIE_LIFETIME = COOKIE_DAYS * 24 * 60 * 60; class ApiV1 { constructor(baseUrl) { this.version = 'v1'; this.baseUrl = baseUrl; } buildUrl(endpoint, pathParam) { let url = `${this.baseUrl}/api/${this.version}${endpoint}`; if (pathParam) { url += `/${pathParam}`; } return url; } origins() { return this.buildUrl(ApiV1.Routes.origins); } consents(id) { return this.buildUrl(ApiV1.Routes.consents, id || ''); } } ApiV1.Routes = { origins: '/origins', consents: '/consent', }; class GovSingleConsent { constructor(consentsUpdateCallback, baseUrlOrEnv) { /** Initialises _GovConsent object by performing the following: 1. Removes 'uid' from URL. 2. Sets 'uid' attribute from cookie or URL. 3. Fetches consent status from API if 'uid' exists. 4. Notifies event listeners with API response. @arg baseUrl: string - the domain of where the single consent API is. Required. @arg consentsUpdateCallback: function(consents, consentsPreferencesSet, error) - callback when the consents are updated "consents": this is the consents object. It has the following properties: "essential", "usage", "campaigns", "settings". Each property is a boolean. "consentsPreferencesSet": true if the consents have been set for this user and this domain. Typically, only display the cookie banner if this is true. "error": if an error occurred, this is the error object. Otherwise, this is null. */ this.cachedConsentsCookie = null; this.validateConstructorArguments(baseUrlOrEnv, consentsUpdateCallback); const baseUrl = this.resolveBaseUrl(baseUrlOrEnv); this._consentsUpdateCallback = consentsUpdateCallback; this.config = new GovConsentConfig(baseUrl); this.urls = new ApiV1(this.config.baseUrl); window.cachedConsentsCookie = null; this.hideUIDParameter(); this.initialiseUIDandConsents(); } validateConstructorArguments(baseUrlOrEnv, consentsUpdateCallback) { if (!baseUrlOrEnv) { throw new Error('Argument baseUrl is required'); } if (typeof baseUrlOrEnv !== 'string') { throw new Error('Argument baseUrl must be a string'); } if (!consentsUpdateCallback) { throw new Error('Argument consentsUpdateCallback is required'); } if (typeof consentsUpdateCallback !== 'function') { throw new Error('Argument consentsUpdateCallback must be a function'); } } resolveBaseUrl(baseUrlOrEnv) { if (baseUrlOrEnv === 'staging') { return 'https://gds-single-consent-staging.app'; } else if (baseUrlOrEnv === 'production') { return 'https://gds-single-consent.app'; } // If not "staging" or "production", assume it's a custom URL return baseUrlOrEnv; } initialiseUIDandConsents() { const currentUID = this.getCurrentUID(); if (this.isNewUID(currentUID)) { this.handleNewUID(currentUID); } if (this.uid) { this.fetchAndUpdateConsents(); } else { this._consentsUpdateCallback(null, false, null); } } handleNewUID(newUID) { this.uid = newUID; this.updateLinksEventHandlers(newUID); this.setUIDCookie(newUID); } isNewUID(currentUID) { return currentUID && currentUID !== this.uid; } fetchAndUpdateConsents() { const consentsUrl = this.urls.consents(this.uid); request(consentsUrl, { timeout: 1000 }, (jsonResponse) => { if (!validateConsentObject(jsonResponse.status)) { const error = new Error('Invalid consents object returned from the API: ' + JSON.stringify(jsonResponse)); return this.defaultToRejectAllConsents(error); } const consents = jsonResponse.status; this.updateBrowserConsents(consents); this._consentsUpdateCallback(consents, GovSingleConsent.isConsentPreferencesSet(), null); }, (error) => this.defaultToRejectAllConsents(error)); } getCurrentUID() { // Get the current uid from URL or from the cookie if it exists return this.config.uidFromUrl || this.config.uidFromCookie; } setConsents(consents) { if (!consents) { throw new Error('consents is required in GovSingleConsent.setConsents()'); } const successCallback = (response) => { if (!response.uid) { throw new Error('No UID returned from the API'); } if (this.isNewUID(response.uid)) { this.handleNewUID(response.uid); } this.updateBrowserConsents(consents); this._consentsUpdateCallback(consents, GovSingleConsent.isConsentPreferencesSet(), null); }; const url = this.urls.consents(this.uid); request(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'status='.concat(JSON.stringify(consents)), }, successCallback, (error) => this.defaultToRejectAllConsents(error)); } defaultToRejectAllConsents(error) { this.updateBrowserConsents(GovSingleConsent.REJECT_ALL); this._consentsUpdateCallback(GovSingleConsent.REJECT_ALL, GovSingleConsent.isConsentPreferencesSet(), error); } static getConsents() { if (window.cachedConsentsCookie) { return window.cachedConsentsCookie; } const cookieValue = getCookie(GovConsentConfig.CONSENTS_COOKIE_NAME, null); if (cookieValue) { return JSON.parse(cookieValue); } return null; } static hasConsentedToEssential() { const consents = GovSingleConsent.getConsents(); return consents === null || consents === void 0 ? void 0 : consents.essential; } static hasConsentedToUsage() { const consents = GovSingleConsent.getConsents(); return consents === null || consents === void 0 ? void 0 : consents.usage; } static hasConsentedToCampaigns() { const consents = GovSingleConsent.getConsents(); return consents === null || consents === void 0 ? void 0 : consents.campaigns; } static hasConsentedToSettings() { const consents = GovSingleConsent.getConsents(); return consents === null || consents === void 0 ? void 0 : consents.settings; } static isConsentPreferencesSet() { const value = getCookie(GovConsentConfig.PREFERENCES_SET_COOKIE_NAME, null); return value === 'true'; } updateLinksEventHandlers(currentUID) { request(this.urls.origins(), {}, // Update links with UID (origins) => this.addUIDtoCrossOriginLinks(origins, currentUID), (error) => { throw error; }); } addUIDtoCrossOriginLinks(origins, uid) { /** * Adds uid URL parameter to consent sharing links. * Only links with known origins are updated. */ const links = document.querySelectorAll('a[href]'); Array.prototype.forEach.call(links, (link) => { if (isCrossOrigin(link) && origins.indexOf(getOriginFromLink(link)) >= 0) { link.addEventListener('click', (event) => { event.target.href = addUrlParameter(event.target.href, GovConsentConfig.UID_KEY, uid); }); } }); } setUIDCookie(uid) { setCookie({ name: GovConsentConfig.UID_KEY, value: uid, lifetime: GovConsentConfig.COOKIE_LIFETIME, }); } updateBrowserConsents(consents) { this.setConsentsCookie(consents); this.setPreferencesSetCookie(true); window.cachedConsentsCookie = consents; } setConsentsCookie(consents) { setCookie({ name: GovConsentConfig.CONSENTS_COOKIE_NAME, value: JSON.stringify(consents), lifetime: GovConsentConfig.COOKIE_LIFETIME, }); } setPreferencesSetCookie(value) { setCookie({ name: GovConsentConfig.PREFERENCES_SET_COOKIE_NAME, value: value.toString(), lifetime: GovConsentConfig.COOKIE_LIFETIME, }); } hideUIDParameter() { history.replaceState(null, null, removeUrlParameter(location.href, GovConsentConfig.UID_KEY)); } } GovSingleConsent.ACCEPT_ALL = { essential: true, usage: true, campaigns: true, settings: true, }; GovSingleConsent.REJECT_ALL = { essential: true, usage: false, campaigns: false, settings: false, }; if (isBrowser()) { window.GovSingleConsent = GovSingleConsent; } export { GovSingleConsent };