import { Constant } from "@clarity-types/data"; import config from "@src/core/config"; import { decodeCookieValue, encodeCookieValue, supported } from "@src/data/util"; let rootDomain = null; export const COOKIE_SEP = Constant.Caret; export function start() { rootDomain = null; } export function stop() { rootDomain = null; } export function getCookie(key: string, limit = false): string { if (supported(document, Constant.Cookie)) { let cookies: string[] = document.cookie.split(Constant.Semicolon); if (cookies) { for (let i = 0; i < cookies.length; i++) { let pair: string[] = cookies[i].split(Constant.Equals); if (pair.length > 1 && pair[0] && pair[0].trim() === key) { // Some browsers automatically url encode cookie values if they are not url encoded. // We therefore encode and decode cookie values ourselves. // For backwards compatability we need to consider 3 cases: // * Cookie was previously not encoded by Clarity and browser did not encode it // * Cookie was previously not encoded by Clarity and browser encoded it once or more // * Cookie was previously encoded by Clarity and browser did not encode it let [isEncoded, decodedValue] = decodeCookieValue(pair[1]); while (isEncoded) { [isEncoded, decodedValue] = decodeCookieValue(decodedValue); } // If we are limiting cookies, check if the cookie value is limited if (limit) { return decodedValue.endsWith(`${Constant.Tilde}1`) ? decodedValue.substring(0, decodedValue.length - 2) : null; } return decodedValue; } } } } return null; } export function setCookie(key: string, value: string, time: number): void { // only write cookies if we are currently in a cookie writing mode (and they are supported) // OR if we are trying to write an empty cookie (i.e. clear the cookie value out) if ((config.track || value == Constant.Empty) && ((navigator && navigator.cookieEnabled) || supported(document, Constant.Cookie))) { // Some browsers automatically url encode cookie values if they are not url encoded. // We therefore encode and decode cookie values ourselves. let encodedValue = encodeCookieValue(value); let expiry = new Date(); expiry.setDate(expiry.getDate() + time); let expires = expiry ? Constant.Expires + expiry.toUTCString() : Constant.Empty; let cookie = `${key}=${encodedValue}${Constant.Semicolon}${expires}${Constant.Path}`; try { // Attempt to get the root domain only once and fall back to writing cookie on the current domain. if (rootDomain === null) { let hostname = location.hostname ? location.hostname.split(Constant.Dot) : []; // Walk backwards on a domain and attempt to set a cookie, until successful for (let i = hostname.length - 1; i >= 0; i--) { rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`; // We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net. // So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL) if (i < hostname.length - 1) { // Write the cookie on the current computed top level domain document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`; // Once written, check if the cookie exists and its value matches exactly with what we intended to set // Checking for exact value match helps us eliminate a corner case where the cookie may already be present with a different value // If the check is successful, no more action is required and we can return from the function since rootDomain cookie is already set // If the check fails, continue with the for loop until we can successfully set and verify the cookie if (getCookie(key) === value) { return; } } } // Finally, if we were not successful and gone through all the options, play it safe and reset rootDomain to be empty // This forces our code to fall back to always writing cookie to the current domain rootDomain = Constant.Empty; } } catch { rootDomain = Constant.Empty; } document.cookie = rootDomain ? `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}` : cookie; } }