meta-capi-param-builder-clientjs
Version:
Conversions API parameter builder for Client-side JavaScript
307 lines (270 loc) • 8.71 kB
JavaScript
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
cookiePack,
cookieUnpack,
maybeUpdatePayload,
} from '../model/cookies.js';
import {
MAX_INT_9_DECIMAL_DIGITS,
NINETY_DAYS_IN_MS,
CLICK_ID_PARAMETER,
CLICKTHROUGH_COOKIE_NAME,
DOMAIN_SCOPED_BROWSER_ID_COOKIE_NAME,
DEFAULT_FBC_PARAMS,
APPENDIX_NET_NEW,
APPENDIX_NO_CHANGE,
} from '../model/constants.js';
import { getURLParametersFromUrlList } from '../utils/urlUtil.js';
import { getAppendix } from '../utils/appendixUtil.js';
function readCookieRaw(name) {
const result = [];
try {
const cookie = document.cookie.split(';');
const regexString = `^\\s*${name}=\\s*(.*?)\\s*$`;
const regexName = new RegExp(regexString);
for (let ii = 0; ii < cookie.length; ii++) {
const match = cookie[ii].match(regexName);
match && result.push(match[1]);
}
return result && result.hasOwnProperty(0) && typeof result[0] === 'string'
? result[0]
: '';
} catch (e) {
throw new Error('Fail to read from cookie: ' + e.message);
}
}
function getSubdomainAtIndex(domain, subdomainIndex) {
return domain.slice(domain.length - 1 - subdomainIndex).join('.');
}
function getCookieTTL(duration) {
return new Date(Date.now() + Math.round(duration)).toUTCString();
}
function getIsChrome() {
const isChromium = window.chrome;
const winNav = window.navigator;
const vendorName = winNav.vendor;
const isOpera = window.opr !== undefined;
const isIEedge = winNav.userAgent.indexOf('Edg') > -1;
const isIOSChrome = winNav.userAgent.match('CriOS');
return (
!isIOSChrome &&
isChromium !== null &&
isChromium !== undefined &&
vendorName === 'Google Inc.' &&
isOpera === false &&
isIEedge === false
);
}
function writeCookieRaw(name, value, domain, ttlInMs) {
const expires = getCookieTTL(ttlInMs);
if (
typeof name !== 'string' ||
typeof value !== 'string' ||
typeof domain !== 'string'
) {
return;
}
try {
const cookieValue = encodeURIComponent(value); // to prevent malicious manual setting e.g adding ';' after the url param
document.cookie =
`${name}=${cookieValue};` +
`expires=${expires};` +
`domain=.${domain};` + // '.' prefix says "subdomains are ok"
`${getIsChrome() ? `SameSite=Lax;` : ''}` +
`path=/`; // This is only accessible from the same domain
} catch (e) {
throw new Error('Fail to write cookie: ' + e.message);
}
}
function readPackedCookie(name) {
const rawCookie = readCookieRaw(name);
if (typeof rawCookie !== 'string' || rawCookie === '') {
return null;
}
return cookieUnpack(rawCookie);
}
function writeNewCookie(name, payload) {
const fullHostname = window.location.hostname;
const domainParts = fullHostname.split('.');
// Attempt to write to the highest level domain we can
for (
let subdomainIndex = 0;
subdomainIndex < domainParts.length;
subdomainIndex++
) {
const domain = getSubdomainAtIndex(domainParts, subdomainIndex);
// Sites seem to be incorrectly polyfilling Date.now, so we have to check
// if it's a number and fall back to the getTime otherwise.
const now = Date.now();
const newCookie = cookiePack(
subdomainIndex,
typeof now === 'number' ? now : new Date().getTime(),
payload,
getAppendix(APPENDIX_NET_NEW)
);
writeCookieRaw(name, newCookie, domain, NINETY_DAYS_IN_MS);
if (readCookieRaw(name) !== '') {
return newCookie;
}
}
return null;
}
function writeExistingCookie(name, existingCookie) {
const fullHostname = window.location.hostname;
const domainParts = fullHostname.split('.');
if (
existingCookie['subdomainIndex'] == null ||
existingCookie['creationTime'] == null ||
existingCookie['payload'] == null
) {
throw new Error(name + ` only partially set on cookie.`);
}
const domain = getSubdomainAtIndex(
domainParts,
existingCookie['subdomainIndex']
);
const appendix = existingCookie['appendix']
? existingCookie['appendix']
: getAppendix(APPENDIX_NO_CHANGE);
const updatedExistingCookie = cookiePack(
existingCookie['subdomainIndex'],
existingCookie['creationTime'],
existingCookie['payload'],
appendix
);
writeCookieRaw(name, updatedExistingCookie, domain, NINETY_DAYS_IN_MS);
return updatedExistingCookie;
}
function mintDomainScopedBrowserIDCookieValue() {
const left = Math.floor(Math.random() * MAX_INT_9_DECIMAL_DIGITS);
const right = Math.floor(Math.random() * MAX_INT_9_DECIMAL_DIGITS);
return left.toString() + right.toString();
}
function updateClickIdCookieIfNecessary(url) {
const newClickID = getFbcFromQuery(url);
if (newClickID != null && newClickID.length > 500) {
return null;
}
const existingCookie = readPackedCookie(CLICKTHROUGH_COOKIE_NAME);
if (newClickID != null) {
if (existingCookie == null) {
return writeNewCookie(CLICKTHROUGH_COOKIE_NAME, newClickID);
}
let newCookie = maybeUpdatePayload(existingCookie, newClickID);
newCookie = writeExistingCookie(CLICKTHROUGH_COOKIE_NAME, newCookie);
return newCookie;
} else if (existingCookie != null) {
const updatedExistingCookie = writeExistingCookie(
CLICKTHROUGH_COOKIE_NAME,
existingCookie
);
return updatedExistingCookie;
}
return existingCookie;
}
function getFbcFromQuery(url) {
let newParamValue = null;
DEFAULT_FBC_PARAMS.forEach((param) => {
const value = getURLParametersFromUrlList(
[url, window.location.href, document.referrer],
param['query']
);
if (value) {
newParamValue = buildParamConfigs(newParamValue, value, param);
}
});
return newParamValue;
}
function buildParamConfigs(existingParamValue, newValue, paramConfig) {
const isClickID = paramConfig['query'] === CLICK_ID_PARAMETER;
const separator = isClickID ? '' : '_';
// Prevent duplication
if (
existingParamValue &&
existingParamValue.includes(separator + paramConfig['prefix'] + separator)
) {
return existingParamValue;
}
const newSegment = paramConfig['prefix'] + separator + newValue;
if (existingParamValue == null) {
return newSegment;
}
return existingParamValue + separator + newSegment;
}
function updateDomainScopedBrowserIdCookieIfNecessary() {
const existingCookie = readPackedCookie(DOMAIN_SCOPED_BROWSER_ID_COOKIE_NAME);
if (existingCookie != null) {
const updatedExistingCookie = writeExistingCookie(
DOMAIN_SCOPED_BROWSER_ID_COOKIE_NAME,
existingCookie
);
return updatedExistingCookie;
}
const browserID = mintDomainScopedBrowserIDCookieValue();
const newFbpCookie = writeNewCookie(
DOMAIN_SCOPED_BROWSER_ID_COOKIE_NAME,
browserID
);
return newFbpCookie;
}
function writeCookieWithToken(name, value, needEncoding, ttlInMs, appendix) {
if (!value) return false;
const fullHostname = window.location.hostname;
const domainParts = fullHostname.split('.');
const valueWithAppendix = [value, appendix].join('.');
// Attempt to write to the highest level domain we can
for (
let subdomainIndex = 0;
subdomainIndex < domainParts.length;
subdomainIndex++
) {
const domain = getSubdomainAtIndex(domainParts, subdomainIndex);
if (needEncoding) {
writeCookieRaw(name, valueWithAppendix, domain, ttlInMs);
} else {
writeCookieRawWithoutEncoding(name, valueWithAppendix, domain, ttlInMs);
}
if (readCookieRaw(name) === valueWithAppendix) {
return true;
}
}
return false;
}
// This function is used to write a cookie without encoding the value.
// Need to make sure there is no cookie breaking characters in the value.
function writeCookieRawWithoutEncoding(name, value, domain, ttlInMs) {
const expires = getCookieTTL(ttlInMs);
if (
typeof name !== 'string' ||
typeof value !== 'string' ||
typeof domain !== 'string'
) {
return;
}
try {
document.cookie =
`${name}=${value};` +
`expires=${expires};` +
`domain=.${domain};` + // '.' prefix says "subdomains are ok"
`${getIsChrome() ? `SameSite=Lax;` : ''}` +
`path=/`; // This is only accessible from the same domain
} catch (e) {
throw new Error('Fail to write cookie: ' + e.message);
}
}
export {
readPackedCookie,
writeNewCookie,
writeExistingCookie,
readCookieRaw,
mintDomainScopedBrowserIDCookieValue,
updateClickIdCookieIfNecessary,
updateDomainScopedBrowserIdCookieIfNecessary,
writeCookieWithToken,
};