prebid/Prebid.js

View on GitHub
modules/lotamePanoramaIdSystem.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * This module adds LotamePanoramaId to the User ID module
 * The {@link module:modules/userId} module is required
 * @module modules/lotamePanoramaId
 * @requires module:modules/userId
 */
import {
  timestamp,
  isStr,
  logError,
  isBoolean,
  buildUrl,
  isEmpty,
  isArray,
  isEmptyStr
} from '../src/utils.js';
import { ajax } from '../src/ajax.js';
import { submodule } from '../src/hook.js';
import {getStorageManager} from '../src/storageManager.js';
import { uspDataHandler } from '../src/adapterManager.js';
import {MODULE_TYPE_UID} from '../src/activities/modules.js';

/**
 * @typedef {import('../modules/userId/index.js').Submodule} Submodule
 * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
 * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
 * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
 */

const KEY_ID = 'panoramaId';
const KEY_EXPIRY = `${KEY_ID}_expiry`;
const KEY_PROFILE = '_cc_id';
const MODULE_NAME = 'lotamePanoramaId';
const NINE_MONTHS_MS = 23328000 * 1000;
const DAYS_TO_CACHE = 7;
const DAY_MS = 60 * 60 * 24 * 1000;
const MISSING_CORE_CONSENT = 111;
const GVLID = 95;
const ID_HOST = 'id.crwdcntrl.net';
const ID_HOST_COOKIELESS = 'c.ltmsphrcl.net';

export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME});
let cookieDomain;

/**
 * Set the Lotame First Party Profile ID in the first party namespace
 * @param {String} profileId
 */
function setProfileId(profileId) {
  if (storage.cookiesAreEnabled()) {
    let expirationDate = new Date(timestamp() + NINE_MONTHS_MS).toUTCString();
    storage.setCookie(
      KEY_PROFILE,
      profileId,
      expirationDate,
      'Lax',
      cookieDomain,
      undefined
    );
  }
  if (storage.hasLocalStorage()) {
    storage.setDataInLocalStorage(KEY_PROFILE, profileId, undefined);
  }
}

/**
 * Get the Lotame profile id by checking cookies first and then local storage
 */
function getProfileId() {
  let profileId;
  if (storage.cookiesAreEnabled()) {
    profileId = storage.getCookie(KEY_PROFILE, undefined);
  }
  if (!profileId && storage.hasLocalStorage()) {
    profileId = storage.getDataFromLocalStorage(KEY_PROFILE, undefined);
  }
  return profileId;
}

/**
 * Get a value from browser storage by checking cookies first and then local storage
 * @param {String} key
 */
function getFromStorage(key) {
  let value = null;
  if (storage.cookiesAreEnabled()) {
    value = storage.getCookie(key, undefined);
  }
  if (storage.hasLocalStorage() && value === null) {
    const storedValueExp = storage.getDataFromLocalStorage(
      `${key}_exp`, undefined
    );

    if (storedValueExp === '' || storedValueExp === null) {
      value = storage.getDataFromLocalStorage(key, undefined);
    } else if (storedValueExp) {
      if ((new Date(parseInt(storedValueExp, 10))).getTime() - Date.now() > 0) {
        value = storage.getDataFromLocalStorage(key, undefined);
      }
    }
  }
  return value;
}

/**
 * Save a key/value pair to the browser cache (cookies and local storage)
 * @param {String} key
 * @param {String} value
 * @param {Number} expirationTimestamp
 */
function saveLotameCache(
  key,
  value,
  expirationTimestamp = timestamp() + DAYS_TO_CACHE * DAY_MS
) {
  if (key && value) {
    let expirationDate = new Date(expirationTimestamp).toUTCString();
    if (storage.cookiesAreEnabled()) {
      storage.setCookie(
        key,
        value,
        expirationDate,
        'Lax',
        cookieDomain,
        undefined
      );
    }
    if (storage.hasLocalStorage()) {
      storage.setDataInLocalStorage(
        `${key}_exp`,
        String(expirationTimestamp),
        undefined
      );
      storage.setDataInLocalStorage(key, value, undefined);
    }
  }
}

/**
 * Retrieve all the cached values from cookies and/or local storage
 * @param {Number} clientId
 */
function getLotameLocalCache(clientId = undefined) {
  let cache = {
    data: getFromStorage(KEY_ID),
    expiryTimestampMs: 0,
    clientExpiryTimestampMs: 0,
  };

  try {
    if (clientId) {
      const rawClientExpiry = getFromStorage(`${KEY_EXPIRY}_${clientId}`);
      if (isStr(rawClientExpiry)) {
        cache.clientExpiryTimestampMs = parseInt(rawClientExpiry, 10);
      }
    }

    const rawExpiry = getFromStorage(KEY_EXPIRY);
    if (isStr(rawExpiry)) {
      cache.expiryTimestampMs = parseInt(rawExpiry, 10);
    }
  } catch (error) {
    logError(error);
  }

  return cache;
}

/**
 * Clear a cached value from cookies and local storage
 * @param {String} key
 */
function clearLotameCache(key) {
  if (key) {
    if (storage.cookiesAreEnabled()) {
      let expirationDate = new Date(0).toUTCString();
      storage.setCookie(
        key,
        '',
        expirationDate,
        'Lax',
        cookieDomain,
        undefined
      );
    }
    if (storage.hasLocalStorage()) {
      storage.removeDataFromLocalStorage(key, undefined);
    }
  }
}
/** @type {Submodule} */
export const lotamePanoramaIdSubmodule = {
  /**
   * used to link submodule with config
   * @type {string}
   */
  name: MODULE_NAME,

  /**
   * Vendor id of Lotame
   * @type {Number}
   */
  gvlid: GVLID,

  /**
   * Decode the stored id value for passing to bid requests
   * @function decode
   * @param {(Object|string)} value
   * @param {SubmoduleConfig|undefined} config
   * @returns {(Object|undefined)}
   */
  decode(value, config) {
    return isStr(value) ? { lotamePanoramaId: value } : undefined;
  },

  /**
   * Retrieve the Lotame Panorama Id
   * @function
   * @param {SubmoduleConfig} [config]
   * @param {ConsentData} [consentData]
   * @param {(Object|undefined)} cacheIdObj
   * @returns {IdResponse|undefined}
   */
  getId(config, consentData, cacheIdObj) {
    cookieDomain = lotamePanoramaIdSubmodule.findRootDomain();
    const configParams = (config && config.params) || {};
    const clientId = configParams.clientId;
    const hasCustomClientId = !isEmpty(clientId);
    const localCache = getLotameLocalCache(clientId);

    const hasExpiredPanoId = Date.now() > localCache.expiryTimestampMs;

    if (hasCustomClientId) {
      const hasFreshClientNoConsent = Date.now() < localCache.clientExpiryTimestampMs;
      if (hasFreshClientNoConsent) {
        // There is no consent
        return {
          id: undefined,
          reason: 'NO_CLIENT_CONSENT',
        };
      }
    }

    if (!hasExpiredPanoId) {
      return {
        id: localCache.data,
      };
    }

    const storedUserId = getProfileId();

    // Add CCPA Consent data handling
    const usp = uspDataHandler.getConsentData();

    let usPrivacy;
    if (typeof usp !== 'undefined' && !isEmpty(usp) && !isEmptyStr(usp)) {
      usPrivacy = usp;
    }
    if (!usPrivacy) {
      // fallback to 1st party cookie
      usPrivacy = getFromStorage('us_privacy');
    }

    const getRequestHost = function() {
      if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
        return ID_HOST_COOKIELESS;
      }
      return ID_HOST;
    }

    const resolveIdFunction = function (callback) {
      let queryParams = {};
      if (storedUserId) {
        queryParams.fp = storedUserId;
      }

      let consentString;
      if (consentData) {
        if (isBoolean(consentData.gdprApplies)) {
          queryParams.gdpr_applies = consentData.gdprApplies;
        }
        consentString = consentData.consentString;
      }
      // If no consent string, try to read it from 1st party cookies
      if (!consentString) {
        consentString = getFromStorage('eupubconsent-v2');
      }
      if (!consentString) {
        consentString = getFromStorage('euconsent-v2');
      }
      if (consentString) {
        queryParams.gdpr_consent = consentString;
      }

      // Add usPrivacy to the url
      if (usPrivacy) {
        queryParams.us_privacy = usPrivacy;
      }

      // Add clientId to the url
      if (hasCustomClientId) {
        queryParams.c = clientId;
      }

      const url = buildUrl({
        protocol: 'https',
        host: getRequestHost(),
        pathname: '/id',
        search: isEmpty(queryParams) ? undefined : queryParams,
      });
      ajax(
        url,
        (response) => {
          let coreId;
          if (response) {
            try {
              let responseObj = JSON.parse(response);
              const hasNoConsentErrors = !(
                isArray(responseObj.errors) &&
                responseObj.errors.indexOf(MISSING_CORE_CONSENT) !== -1
              );

              if (hasCustomClientId) {
                if (hasNoConsentErrors) {
                  clearLotameCache(`${KEY_EXPIRY}_${clientId}`);
                } else if (isStr(responseObj.no_consent) && responseObj.no_consent === 'CLIENT') {
                  saveLotameCache(
                    `${KEY_EXPIRY}_${clientId}`,
                    responseObj.expiry_ts,
                    responseObj.expiry_ts
                  );

                  // End Processing
                  callback();
                  return;
                }
              }

              saveLotameCache(KEY_EXPIRY, responseObj.expiry_ts, responseObj.expiry_ts);

              if (isStr(responseObj.profile_id)) {
                if (hasNoConsentErrors) {
                  setProfileId(responseObj.profile_id);
                }

                if (isStr(responseObj.core_id)) {
                  saveLotameCache(
                    KEY_ID,
                    responseObj.core_id,
                    responseObj.expiry_ts
                  );
                  coreId = responseObj.core_id;
                } else {
                  clearLotameCache(KEY_ID);
                }
              } else {
                if (hasNoConsentErrors) {
                  clearLotameCache(KEY_PROFILE);
                }
                clearLotameCache(KEY_ID);
              }
            } catch (error) {
              logError(error);
            }
          }
          callback(coreId);
        },
        undefined,
        {
          method: 'GET',
          withCredentials: true,
        }
      );
    };

    return { callback: resolveIdFunction };
  },
  eids: {
    lotamePanoramaId: {
      source: 'crwdcntrl.net',
      atype: 1,
    },
  },
};

submodule('userId', lotamePanoramaIdSubmodule);