UNPKG

mk9-prebid

Version:

Header Bidding Management Library

1,297 lines (1,143 loc) • 37.8 kB
/* eslint-disable no-console */ import { config } from './config.js'; import clone from 'just-clone'; import find from 'core-js-pure/features/array/find.js'; import includes from 'core-js-pure/features/array/includes.js'; const CONSTANTS = require('./constants.json'); export { default as deepAccess } from 'dlv/index.js'; export { default as deepSetValue } from 'dset'; var tArr = 'Array'; var tStr = 'String'; var tFn = 'Function'; var tNumb = 'Number'; var tObject = 'Object'; var tBoolean = 'Boolean'; var toString = Object.prototype.toString; let consoleExists = Boolean(window.console); let consoleLogExists = Boolean(consoleExists && window.console.log); let consoleInfoExists = Boolean(consoleExists && window.console.info); let consoleWarnExists = Boolean(consoleExists && window.console.warn); let consoleErrorExists = Boolean(consoleExists && window.console.error); var events = require('./events.js'); // this allows stubbing of utility functions that are used internally by other utility functions export const internal = { checkCookieSupport, createTrackPixelIframeHtml, getWindowSelf, getWindowTop, getWindowLocation, insertUserSyncIframe, insertElement, isFn, triggerPixel, logError, logWarn, logMessage, logInfo, parseQS, formatQS, deepEqual }; let prebidInternal = {} /** * Returns object that is used as internal prebid namespace */ export function getPrebidInternal() { return prebidInternal; } var uniqueRef = {}; export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === uniqueRef ? Function.prototype.bind : function(bind) { var self = this; var args = Array.prototype.slice.call(arguments, 1); return function() { return self.apply(bind, args.concat(Array.prototype.slice.call(arguments))); }; }; /* utility method to get incremental integer starting from 1 */ var getIncrementalInteger = (function () { var count = 0; return function () { count++; return count; }; })(); // generate a random string (to be used as a dynamic JSONP callback) export function getUniqueIdentifierStr() { return getIncrementalInteger() + Math.random().toString(16).substr(2); } /** * Returns a random v4 UUID of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, * where each x is replaced with a random hexadecimal digit from 0 to f, * and y is replaced with a random hexadecimal digit from 8 to b. * https://gist.github.com/jed/982883 via node-uuid */ export function generateUUID(placeholder) { return placeholder ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16) : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); } /** * Returns random data using the Crypto API if available and Math.random if not * Method is from https://gist.github.com/jed/982883 like generateUUID, direct link https://gist.github.com/jed/982883#gistcomment-45104 */ function _getRandomData() { if (window && window.crypto && window.crypto.getRandomValues) { return crypto.getRandomValues(new Uint8Array(1))[0] % 16; } else { return Math.random() * 16; } } export function getBidIdParameter(key, paramsObj) { if (paramsObj && paramsObj[key]) { return paramsObj[key]; } return ''; } export function tryAppendQueryString(existingUrl, key, value) { if (value) { return existingUrl + key + '=' + encodeURIComponent(value) + '&'; } return existingUrl; } // parse a query string object passed in bid params // bid params should be an object such as {key: "value", key1 : "value1"} // aliases to formatQS export function parseQueryStringParameters(queryObj) { let result = ''; for (var k in queryObj) { if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; } } result = result.replace(/&$/, ''); return result; } // transform an AdServer targeting bids into a query string to send to the adserver export function transformAdServerTargetingObj(targeting) { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { return getKeys(targeting) .map(key => `${key}=${encodeURIComponent(getValue(targeting, key))}`).join('&'); } else { return ''; } } /** * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` * @param {object} adUnit one adUnit object from the normal list of adUnits * @returns {Array.<number[]>} array of arrays containing numeric sizes */ export function getAdUnitSizes(adUnit) { if (!adUnit) { return; } let sizes = []; if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { let bannerSizes = adUnit.mediaTypes.banner.sizes; if (Array.isArray(bannerSizes[0])) { sizes = bannerSizes; } else { sizes.push(bannerSizes); } // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders } else if (Array.isArray(adUnit.sizes)) { if (Array.isArray(adUnit.sizes[0])) { sizes = adUnit.sizes; } else { sizes.push(adUnit.sizes); } } return sizes; } /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' * @param {(Array.<number[]>|Array.<number>)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.<string>} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ export function parseSizesInput(sizeObj) { var parsedSizes = []; // if a string for now we can assume it is a single size, like "300x250" if (typeof sizeObj === 'string') { // multiple sizes will be comma-separated var sizes = sizeObj.split(','); // regular expression to match strigns like 300x250 // start of line, at least 1 number, an "x" , then at least 1 number, and the then end of the line var sizeRegex = /^(\d)+x(\d)+$/i; if (sizes) { for (var curSizePos in sizes) { if (hasOwn(sizes, curSizePos) && sizes[curSizePos].match(sizeRegex)) { parsedSizes.push(sizes[curSizePos]); } } } } else if (typeof sizeObj === 'object') { var sizeArrayLength = sizeObj.length; // don't process empty array if (sizeArrayLength > 0) { // if we are a 2 item array of 2 numbers, we must be a SingleSize array if (sizeArrayLength === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { parsedSizes.push(parseGPTSingleSizeArray(sizeObj)); } else { // otherwise, we must be a MultiSize array for (var i = 0; i < sizeArrayLength; i++) { parsedSizes.push(parseGPTSingleSizeArray(sizeObj[i])); } } } } return parsedSizes; } // Parse a GPT style single size array, (i.e [300, 250]) // into an AppNexus style string, (i.e. 300x250) export function parseGPTSingleSizeArray(singleSize) { if (isValidGPTSingleSize(singleSize)) { return singleSize[0] + 'x' + singleSize[1]; } } // Parse a GPT style single size array, (i.e [300, 250]) // into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250}) export function parseGPTSingleSizeArrayToRtbSize(singleSize) { if (isValidGPTSingleSize(singleSize)) { return {w: singleSize[0], h: singleSize[1]}; } } function isValidGPTSingleSize(singleSize) { // if we aren't exactly 2 items in this array, it is invalid return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1])); } export function getWindowTop() { return window.top; } export function getWindowSelf() { return window.self; } export function getWindowLocation() { return window.location; } /** * Wrappers to console.(log | info | warn | error). Takes N arguments, the same as the native methods */ export function logMessage() { if (debugTurnedOn() && consoleLogExists) { console.log.apply(console, decorateLog(arguments, 'MESSAGE:')); } } export function logInfo() { if (debugTurnedOn() && consoleInfoExists) { console.info.apply(console, decorateLog(arguments, 'INFO:')); } } export function logWarn() { if (debugTurnedOn() && consoleWarnExists) { console.warn.apply(console, decorateLog(arguments, 'WARNING:')); } events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'WARNING', arguments: arguments}); } export function logError() { if (debugTurnedOn() && consoleErrorExists) { console.error.apply(console, decorateLog(arguments, 'ERROR:')); } events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'ERROR', arguments: arguments}); } function decorateLog(args, prefix) { args = [].slice.call(args); let bidder = config.getCurrentBidder(); prefix && args.unshift(prefix); if (bidder) { args.unshift(label('#aaa')); } args.unshift(label('#3b88c3')); args.unshift('%cPrebid' + (bidder ? `%c${bidder}` : '')); return args; function label(color) { return `display: inline-block; color: #fff; background: ${color}; padding: 1px 4px; border-radius: 3px;` } } export function hasConsoleLogger() { return consoleLogExists; } export function debugTurnedOn() { return !!config.getConfig('debug'); } export function createInvisibleIframe() { var f = document.createElement('iframe'); f.id = getUniqueIdentifierStr(); f.height = 0; f.width = 0; f.border = '0px'; f.hspace = '0'; f.vspace = '0'; f.marginWidth = '0'; f.marginHeight = '0'; f.style.border = '0'; f.scrolling = 'no'; f.frameBorder = '0'; f.src = 'about:blank'; f.style.display = 'none'; return f; } /* * Check if a given parameter name exists in query string * and if it does return the value */ export function getParameterByName(name) { return parseQS(getWindowLocation().search)[name] || ''; } /** * Return if the object is of the * given type. * @param {*} object to test * @param {String} _t type string (e.g., Array) * @return {Boolean} if object is of type _t */ export function isA(object, _t) { return toString.call(object) === '[object ' + _t + ']'; } export function isFn(object) { return isA(object, tFn); } export function isStr(object) { return isA(object, tStr); } export function isArray(object) { return isA(object, tArr); } export function isNumber(object) { return isA(object, tNumb); } export function isPlainObject(object) { return isA(object, tObject); } export function isBoolean(object) { return isA(object, tBoolean); } /** * Return if the object is "empty"; * this includes falsey, no keys, or no items at indices * @param {*} object object to test * @return {Boolean} if object is empty */ export function isEmpty(object) { if (!object) return true; if (isArray(object) || isStr(object)) { return !(object.length > 0); } for (var k in object) { if (hasOwnProperty.call(object, k)) return false; } return true; } /** * Return if string is empty, null, or undefined * @param str string to test * @returns {boolean} if string is empty */ export function isEmptyStr(str) { return isStr(str) && (!str || str.length === 0); } /** * Iterate object with the function * falls back to es5 `forEach` * @param {Array|Object} object * @param {Function(value, key, object)} fn */ export function _each(object, fn) { if (isEmpty(object)) return; if (isFn(object.forEach)) return object.forEach(fn, this); var k = 0; var l = object.length; if (l > 0) { for (; k < l; k++) fn(object[k], k, object); } else { for (k in object) { if (hasOwnProperty.call(object, k)) fn.call(this, object[k], k); } } } export function contains(a, obj) { if (isEmpty(a)) { return false; } if (isFn(a.indexOf)) { return a.indexOf(obj) !== -1; } var i = a.length; while (i--) { if (a[i] === obj) { return true; } } return false; } /** * Map an array or object into another array * given a function * @param {Array|Object} object * @param {Function(value, key, object)} callback * @return {Array} */ export function _map(object, callback) { if (isEmpty(object)) return []; if (isFn(object.map)) return object.map(callback); var output = []; _each(object, function (value, key) { output.push(callback(value, key, object)); }); return output; } export function hasOwn(objectToCheck, propertyToCheckFor) { if (objectToCheck.hasOwnProperty) { return objectToCheck.hasOwnProperty(propertyToCheckFor); } else { return (typeof objectToCheck[propertyToCheckFor] !== 'undefined') && (objectToCheck.constructor.prototype[propertyToCheckFor] !== objectToCheck[propertyToCheckFor]); } }; /* * Inserts an element(elm) as targets child, by default as first child * @param {HTMLElement} elm * @param {HTMLElement} [doc] * @param {HTMLElement} [target] * @param {Boolean} [asLastChildChild] * @return {HTMLElement} */ export function insertElement(elm, doc, target, asLastChildChild) { doc = doc || document; let parentEl; if (target) { parentEl = doc.getElementsByTagName(target); } else { parentEl = doc.getElementsByTagName('head'); } try { parentEl = parentEl.length ? parentEl : doc.getElementsByTagName('body'); if (parentEl.length) { parentEl = parentEl[0]; let insertBeforeEl = asLastChildChild ? null : parentEl.firstChild; return parentEl.insertBefore(elm, insertBeforeEl); } } catch (e) {} } /** * Inserts an image pixel with the specified `url` for cookie sync * @param {string} url URL string of the image pixel to load * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process */ export function triggerPixel(url, done) { const img = new Image(); if (done && internal.isFn(done)) { img.addEventListener('load', done); img.addEventListener('error', done); } img.src = url; } export function callBurl({ source, burl }) { if (source === CONSTANTS.S2S.SRC && burl) { internal.triggerPixel(burl); } } /** * Inserts an empty iframe with the specified `html`, primarily used for tracking purposes * (though could be for other purposes) * @param {string} htmlCode snippet of HTML code used for tracking purposes */ export function insertHtmlIntoIframe(htmlCode) { if (!htmlCode) { return; } let iframe = document.createElement('iframe'); iframe.id = getUniqueIdentifierStr(); iframe.width = 0; iframe.height = 0; iframe.hspace = '0'; iframe.vspace = '0'; iframe.marginWidth = '0'; iframe.marginHeight = '0'; iframe.style.display = 'none'; iframe.style.height = '0px'; iframe.style.width = '0px'; iframe.scrolling = 'no'; iframe.frameBorder = '0'; iframe.allowtransparency = 'true'; internal.insertElement(iframe, document, 'body'); iframe.contentWindow.document.open(); iframe.contentWindow.document.write(htmlCode); iframe.contentWindow.document.close(); } /** * Inserts empty iframe with the specified `url` for cookie sync * @param {string} url URL to be requested * @param {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process */ export function insertUserSyncIframe(url, done) { let iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin'); let div = document.createElement('div'); div.innerHTML = iframeHtml; let iframe = div.firstChild; if (done && internal.isFn(done)) { iframe.addEventListener('load', done); iframe.addEventListener('error', done); } internal.insertElement(iframe, document, 'html', true); }; /** * Creates a snippet of HTML that retrieves the specified `url` * @param {string} url URL to be requested * @return {string} HTML snippet that contains the img src = set to `url` */ export function createTrackPixelHtml(url) { if (!url) { return ''; } let escapedUrl = encodeURI(url); let img = '<div style="position:absolute;left:0px;top:0px;visibility:hidden;">'; img += '<img src="' + escapedUrl + '"></div>'; return img; }; /** * Creates a snippet of Iframe HTML that retrieves the specified `url` * @param {string} url plain URL to be requested * @param {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true * @param {string} sandbox string if provided the sandbox attribute will be included with the given value * @return {string} HTML snippet that contains the iframe src = set to `url` */ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') { if (!url) { return ''; } if (encodeUri) { url = encodeURI(url); } if (sandbox) { sandbox = `sandbox="${sandbox}"`; } return `<iframe ${sandbox} id="${getUniqueIdentifierStr()}" frameborder="0" allowtransparency="true" marginheight="0" marginwidth="0" width="0" hspace="0" vspace="0" height="0" style="height:0px;width:0px;display:none;" scrolling="no" src="${url}"> </iframe>`; } export function getValueString(param, val, defaultValue) { if (val === undefined || val === null) { return defaultValue; } if (isStr(val)) { return val; } if (isNumber(val)) { return val.toString(); } internal.logWarn('Unsuported type for param: ' + param + ' required type: String'); } export function uniques(value, index, arry) { return arry.indexOf(value) === index; } export function flatten(a, b) { return a.concat(b); } export function getBidRequest(id, bidderRequests) { if (!id) { return; } let bidRequest; bidderRequests.some(bidderRequest => { let result = find(bidderRequest.bids, bid => ['bidId', 'adId', 'bid_id'].some(type => bid[type] === id)); if (result) { bidRequest = result; } return result; }); return bidRequest; } export function getKeys(obj) { return Object.keys(obj); } export function getValue(obj, key) { return obj[key]; } /** * Get the key of an object for a given value */ export function getKeyByValue(obj, value) { for (let prop in obj) { if (obj.hasOwnProperty(prop)) { if (obj[prop] === value) { return prop; } } } } export function getBidderCodes(adUnits = $$PREBID_GLOBAL$$.adUnits) { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) .reduce(flatten, [])).reduce(flatten).filter(uniques); } export function isGptPubadsDefined() { if (window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)) { return true; } } export function isApnGetTagDefined() { if (window.apntag && isFn(window.apntag.getTag)) { return true; } } // This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond export const getHighestCpm = getHighestCpmCallback('timeToRespond', (previous, current) => previous > current); // This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first // Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 export const getOldestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous > current); // This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last // Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 export const getLatestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous < current); function getHighestCpmCallback(useTieBreakerProperty, tieBreakerCallback) { return (previous, current) => { if (previous.cpm === current.cpm) { return tieBreakerCallback(previous[useTieBreakerProperty], current[useTieBreakerProperty]) ? current : previous; } return previous.cpm < current.cpm ? current : previous; } } /** * Fisher–Yates shuffle * http://stackoverflow.com/a/6274398 * https://bost.ocks.org/mike/shuffle/ * istanbul ignore next */ export function shuffle(array) { let counter = array.length; // while there are elements in the array while (counter > 0) { // pick a random index let index = Math.floor(Math.random() * counter); // decrease counter by 1 counter--; // and swap the last element with it let temp = array[counter]; array[counter] = array[index]; array[index] = temp; } return array; } export function adUnitsFilter(filter, bid) { return includes(filter, bid && bid.adUnitCode); } export function deepClone(obj) { return clone(obj); } export function inIframe() { try { return internal.getWindowSelf() !== internal.getWindowTop(); } catch (e) { return true; } } export function isSafariBrowser() { return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } export function replaceAuctionPrice(str, cpm) { if (!str) return; return str.replace(/\$\{AUCTION_PRICE\}/g, cpm); } export function replaceClickThrough(str, clicktag) { if (!str || !clicktag || typeof clicktag !== 'string') return; return str.replace(/\${CLICKTHROUGH}/g, clicktag); } export function timestamp() { return new Date().getTime(); } /** * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now * @returns {number} */ export function getPerformanceNow() { return (window.performance && window.performance.now && window.performance.now()) || 0; } /** * When the deviceAccess flag config option is false, no cookies should be read or set * @returns {boolean} */ export function hasDeviceAccess() { return config.getConfig('deviceAccess') !== false; } /** * @returns {(boolean|undefined)} */ export function checkCookieSupport() { if (window.navigator.cookieEnabled || !!document.cookie.length) { return true; } } /** * Given a function, return a function which only executes the original after * it's been called numRequiredCalls times. * * Note that the arguments from the previous calls will *not* be forwarded to the original function. * Only the final call's arguments matter. * * @param {function} func The function which should be executed, once the returned function has been executed * numRequiredCalls times. * @param {int} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ export function delayExecution(func, numRequiredCalls) { if (numRequiredCalls < 1) { throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`); } let numCalls = 0; return function () { numCalls++; if (numCalls === numRequiredCalls) { func.apply(this, arguments); } } } /** * https://stackoverflow.com/a/34890276/428704 * @export * @param {array} xs * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} */ export function groupBy(xs, key) { return xs.reduce(function(rv, x) { (rv[x[key]] = rv[x[key]] || []).push(x); return rv; }, {}); } /** * Build an object consisting of only defined parameters to avoid creating an * object with defined keys and undefined values. * @param {Object} object The object to pick defined params out of * @param {string[]} params An array of strings representing properties to look for in the object * @returns {Object} An object containing all the specified values that are defined */ export function getDefinedParams(object, params) { return params .filter(param => object[param]) .reduce((bid, param) => Object.assign(bid, { [param]: object[param] }), {}); } /** * @typedef {Object} MediaTypes * @property {Object} banner banner configuration * @property {Object} native native configuration * @property {Object} video video configuration */ /** * Validates an adunit's `mediaTypes` parameter * @param {MediaTypes} mediaTypes mediaTypes parameter to validate * @return {boolean} If object is valid */ export function isValidMediaTypes(mediaTypes) { const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video']; const SUPPORTED_STREAM_TYPES = ['instream', 'outstream', 'adpod']; const types = Object.keys(mediaTypes); if (!types.every(type => includes(SUPPORTED_MEDIA_TYPES, type))) { return false; } if (mediaTypes.video && mediaTypes.video.context) { return includes(SUPPORTED_STREAM_TYPES, mediaTypes.video.context); } return true; } export function getBidderRequest(bidRequests, bidder, adUnitCode) { return find(bidRequests, request => { return request.bids .filter(bid => bid.bidder === bidder && bid.adUnitCode === adUnitCode).length > 0; }) || { start: null, auctionId: null }; } /** * Returns user configured bidder params from adunit * @param {Object} adUnits * @param {string} adUnitCode code * @param {string} bidder code * @return {Array} user configured param for the given bidder adunit configuration */ export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { return adUnits .filter(adUnit => adUnit.code === adUnitCode) .map((adUnit) => adUnit.bids) .reduce(flatten, []) .filter((bidderData) => bidderData.bidder === bidder) .map((bidderData) => bidderData.params || {}); } /** * Returns the origin */ export function getOrigin() { // IE10 does not have this property. https://gist.github.com/hbogs/7908703 if (!window.location.origin) { return window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); } else { return window.location.origin; } } /** * Returns Do Not Track state */ export function getDNT() { return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes'; } const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; /** * Returns filter function to match adUnitCode in slot * @param {Object} slot GoogleTag slot * @return {function} filter function */ export function isAdUnitCodeMatchingSlot(slot) { return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode); } /** * Returns filter function to match adUnitCode in slot * @param {string} adUnitCode AdUnit code * @return {function} filter function */ export function isSlotMatchingAdUnitCode(adUnitCode) { return (slot) => compareCodeAndSlot(slot, adUnitCode); } /** * @summary Uses the adUnit's code in order to find a matching gptSlot on the page */ export function getGptSlotInfoForAdUnitCode(adUnitCode) { let matchingSlot; if (isGptPubadsDefined()) { // find the first matching gpt slot on the page matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); } if (matchingSlot) { return { gptSlot: matchingSlot.getAdUnitPath(), divId: matchingSlot.getSlotElementId() } } return {}; }; /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped * @param {string} bidder bidder code that is not compatible with the adUnit * @return {string} warning message to display when condition is met */ export function unsupportedBidderMessage(adUnit, bidder) { const mediaType = Object.keys(adUnit.mediaTypes || {'banner': 'banner'}).join(', '); return ` ${adUnit.code} is a ${mediaType} ad unit containing bidders that don't support ${mediaType}: ${bidder}. This bidder won't fetch demand. `; } /** * Checks input is integer or not * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger * @param {*} value */ export function isInteger(value) { if (Number.isInteger) { return Number.isInteger(value); } else { return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; } } /** * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' * @param {string} value string value to convert */ export function convertCamelToUnderscore(value) { return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { return '_' + y.toLowerCase() }).replace(/^_/, ''); } /** * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ export function cleanObj(obj) { return Object.keys(obj).reduce((newObj, key) => { if (typeof obj[key] !== 'undefined') { newObj[key] = obj[key]; } return newObj; }, {}) } /** * Create a new object with selected properties. Also allows property renaming and transform functions. * @param obj the original object * @param properties An array of desired properties */ export function pick(obj, properties) { if (typeof obj !== 'object') { return {}; } return properties.reduce((newObj, prop, i) => { if (typeof prop === 'function') { return newObj; } let newProp = prop; let match = prop.match(/^(.+?)\sas\s(.+?)$/i); if (match) { prop = match[1]; newProp = match[2]; } let value = obj[prop]; if (typeof properties[i + 1] === 'function') { value = properties[i + 1](value, newObj); } if (typeof value !== 'undefined') { newObj[newProp] = value; } return newObj; }, {}); } /** * Converts an object of arrays (either strings or numbers) into an array of objects containing key and value properties * normally read from bidder params * eg { foo: ['bar', 'baz'], fizz: ['buzz'] } * becomes [{ key: 'foo', value: ['bar', 'baz']}, {key: 'fizz', value: ['buzz']}] * @param {Object} keywords object of arrays representing keyvalue pairs * @param {string} paramName name of parent object (eg 'keywords') containing keyword data, used in error handling */ export function transformBidderParamKeywords(keywords, paramName = 'keywords') { let arrs = []; _each(keywords, (v, k) => { if (isArray(v)) { let values = []; _each(v, (val) => { val = getValueString(paramName + '.' + k, val); if (val || val === '') { values.push(val); } }); v = values; } else { v = getValueString(paramName + '.' + k, v); if (isStr(v)) { v = [v]; } else { return; } // unsuported types - don't send a key } arrs.push({key: k, value: v}); }); return arrs; } /** * Try to convert a value to a type. * If it can't be done, the value will be returned. * * @param {string} typeToConvert The target type. e.g. "string", "number", etc. * @param {*} value The value to be converted into typeToConvert. */ function tryConvertType(typeToConvert, value) { if (typeToConvert === 'string') { return value && value.toString(); } else if (typeToConvert === 'number') { return Number(value); } else { return value; } } export function convertTypes(types, params) { Object.keys(types).forEach(key => { if (params[key]) { if (isFn(types[key])) { params[key] = types[key](params[key]); } else { params[key] = tryConvertType(types[key], params[key]); } // don't send invalid values if (isNaN(params[key])) { delete params.key; } } }); return params; } export function isArrayOfNums(val, size) { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } /** * Creates an array of n length and fills each item with the given value */ export function fill(value, length) { let newArray = []; for (let i = 0; i < length; i++) { let valueToPush = isPlainObject(value) ? deepClone(value) : value; newArray.push(valueToPush); } return newArray; } /** * http://npm.im/chunk * Returns an array with *size* chunks from given array * * Example: * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => * [['a', 'b'], ['c', 'd'], ['e']] */ export function chunk(array, size) { let newArray = []; for (let i = 0; i < Math.ceil(array.length / size); i++) { let start = i * size; let end = start + size; newArray.push(array.slice(start, end)); } return newArray; } export function getMinValueFromArray(array) { return Math.min(...array); } export function getMaxValueFromArray(array) { return Math.max(...array); } /** * This function will create compare function to sort on object property * @param {string} property * @returns {function} compare function to be used in sorting */ export function compareOn(property) { return function compare(a, b) { if (a[property] < b[property]) { return 1; } if (a[property] > b[property]) { return -1; } return 0; } } export function parseQS(query) { return !query ? {} : query .replace(/^\?/, '') .split('&') .reduce((acc, criteria) => { let [k, v] = criteria.split('='); if (/\[\]$/.test(k)) { k = k.replace('[]', ''); acc[k] = acc[k] || []; acc[k].push(v); } else { acc[k] = v || ''; } return acc; }, {}); } export function formatQS(query) { return Object .keys(query) .map(k => Array.isArray(query[k]) ? query[k].map(v => `${k}[]=${v}`).join('&') : `${k}=${query[k]}`) .join('&'); } export function parseUrl(url, options) { let parsed = document.createElement('a'); if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { parsed.href = url; } else { parsed.href = decodeURIComponent(url); } // in window.location 'search' is string, not object let qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString); return { href: parsed.href, protocol: (parsed.protocol || '').replace(/:$/, ''), hostname: parsed.hostname, port: +parsed.port, pathname: parsed.pathname.replace(/^(?!\/)/, '/'), search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''), hash: (parsed.hash || '').replace(/^#/, ''), host: parsed.host || window.location.host }; } export function buildUrl(obj) { return (obj.protocol || 'http') + '://' + (obj.host || obj.hostname + (obj.port ? `:${obj.port}` : '')) + (obj.pathname || '') + (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') + (obj.hash ? `#${obj.hash}` : ''); } /** * This function deeply compares two objects checking for their equivalence. * @param {Object} obj1 * @param {Object} obj2 * @returns {boolean} */ export function deepEqual(obj1, obj2) { if (obj1 === obj2) return true; else if ((typeof obj1 === 'object' && obj1 !== null) && (typeof obj2 === 'object' && obj2 !== null)) { if (Object.keys(obj1).length !== Object.keys(obj2).length) return false; for (let prop in obj1) { if (obj2.hasOwnProperty(prop)) { if (!deepEqual(obj1[prop], obj2[prop])) { return false; } } else { return false; } } return true; } else { return false; } } export function mergeDeep(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isPlainObject(target) && isPlainObject(source)) { for (const key in source) { if (isPlainObject(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); mergeDeep(target[key], source[key]); } else if (isArray(source[key])) { if (!target[key]) { Object.assign(target, { [key]: source[key] }); } else if (isArray(target[key])) { target[key] = target[key].concat(source[key]); } } else { Object.assign(target, { [key]: source[key] }); } } } return mergeDeep(target, ...sources); } /** * returns a hash of a string using a fast algorithm * source: https://stackoverflow.com/a/52171480/845390 * @param str * @param seed (optional) * @returns {string} */ export function cyrb53Hash(str, seed = 0) { // IE doesn't support imul // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill let imul = function(opA, opB) { if (isFn(Math.imul)) { return Math.imul(opA, opB); } else { opB |= 0; // ensure that opB is an integer. opA will automatically be coerced. // floating points give us 53 bits of precision to work with plus 1 sign bit // automatically handled for our convienence: // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001 // 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ var result = (opA & 0x003fffff) * opB; // 2. We can remove an integer coersion from the statement above because: // 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001 // 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB | 0; return result | 0; } }; let h1 = 0xdeadbeef ^ seed; let h2 = 0x41c6ce57 ^ seed; for (let i = 0, ch; i < str.length; i++) { ch = str.charCodeAt(i); h1 = imul(h1 ^ ch, 2654435761); h2 = imul(h2 ^ ch, 1597334677); } h1 = imul(h1 ^ (h1 >>> 16), 2246822507) ^ imul(h2 ^ (h2 >>> 13), 3266489909); h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909); return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); }