UNPKG

mk9-prebid

Version:

Header Bidding Management Library

1,308 lines (1,215 loc) 46.7 kB
import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; const ENDPOINT = 'https://hbopenbid.pubmatic.com/translator?source=prebid-client'; const USER_SYNC_URL_IFRAME = 'https://ads.pubmatic.com/AdServer/js/user_sync.html?kdntuid=1&p='; const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p='; const DEFAULT_CURRENCY = 'USD'; const AUCTION_TYPE = 1; const UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; const PREBID_NATIVE_HELP_LINK = 'http://prebid.org/dev-docs/show-native-ads.html'; const PUBLICATION = 'pubmatic'; // Your publication on Blue Billywig, potentially with environment (e.g. publication.bbvms.com or publication.test.bbvms.com) const RENDERER_URL = 'https://pubmatic.bbvms.com/r/'.concat('$RENDERER', '.js'); // URL of the renderer application const CUSTOM_PARAMS = { 'kadpageurl': '', // Custom page url 'gender': '', // User gender 'yob': '', // User year of birth 'lat': '', // User location - Latitude 'lon': '', // User Location - Longitude 'wiid': '', // OpenWrap Wrapper Impression ID 'profId': '', // OpenWrap Legacy: Profile ID 'verId': '' // OpenWrap Legacy: version ID }; const DATA_TYPES = { 'NUMBER': 'number', 'STRING': 'string', 'BOOLEAN': 'boolean', 'ARRAY': 'array', 'OBJECT': 'object' }; const VIDEO_CUSTOM_PARAMS = { 'mimes': DATA_TYPES.ARRAY, 'minduration': DATA_TYPES.NUMBER, 'maxduration': DATA_TYPES.NUMBER, 'startdelay': DATA_TYPES.NUMBER, 'playbackmethod': DATA_TYPES.ARRAY, 'api': DATA_TYPES.ARRAY, 'protocols': DATA_TYPES.ARRAY, 'w': DATA_TYPES.NUMBER, 'h': DATA_TYPES.NUMBER, 'battr': DATA_TYPES.ARRAY, 'linearity': DATA_TYPES.NUMBER, 'placement': DATA_TYPES.NUMBER, 'minbitrate': DATA_TYPES.NUMBER, 'maxbitrate': DATA_TYPES.NUMBER, 'skip': DATA_TYPES.NUMBER } const NATIVE_ASSETS = { 'TITLE': { ID: 1, KEY: 'title', TYPE: 0 }, 'IMAGE': { ID: 2, KEY: 'image', TYPE: 0 }, 'ICON': { ID: 3, KEY: 'icon', TYPE: 0 }, 'SPONSOREDBY': { ID: 4, KEY: 'sponsoredBy', TYPE: 1 }, // please note that type of SPONSORED is also 1 'BODY': { ID: 5, KEY: 'body', TYPE: 2 }, // please note that type of DESC is also set to 2 'CLICKURL': { ID: 6, KEY: 'clickUrl', TYPE: 0 }, 'VIDEO': { ID: 7, KEY: 'video', TYPE: 0 }, 'EXT': { ID: 8, KEY: 'ext', TYPE: 0 }, 'DATA': { ID: 9, KEY: 'data', TYPE: 0 }, 'LOGO': { ID: 10, KEY: 'logo', TYPE: 0 }, 'SPONSORED': { ID: 11, KEY: 'sponsored', TYPE: 1 }, // please note that type of SPONSOREDBY is also set to 1 'DESC': { ID: 12, KEY: 'data', TYPE: 2 }, // please note that type of BODY is also set to 2 'RATING': { ID: 13, KEY: 'rating', TYPE: 3 }, 'LIKES': { ID: 14, KEY: 'likes', TYPE: 4 }, 'DOWNLOADS': { ID: 15, KEY: 'downloads', TYPE: 5 }, 'PRICE': { ID: 16, KEY: 'price', TYPE: 6 }, 'SALEPRICE': { ID: 17, KEY: 'saleprice', TYPE: 7 }, 'PHONE': { ID: 18, KEY: 'phone', TYPE: 8 }, 'ADDRESS': { ID: 19, KEY: 'address', TYPE: 9 }, 'DESC2': { ID: 20, KEY: 'desc2', TYPE: 10 }, 'DISPLAYURL': { ID: 21, KEY: 'displayurl', TYPE: 11 }, 'CTA': { ID: 22, KEY: 'cta', TYPE: 12 } }; const NATIVE_ASSET_IMAGE_TYPE = { 'ICON': 1, 'LOGO': 2, 'IMAGE': 3 } // check if title, image can be added with mandatory field default values const NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS = [ { id: NATIVE_ASSETS.SPONSOREDBY.ID, required: true, data: { type: 1 } }, { id: NATIVE_ASSETS.TITLE.ID, required: true, }, { id: NATIVE_ASSETS.IMAGE.ID, required: true, } ] const NET_REVENUE = true; const dealChannelValues = { 1: 'PMP', 5: 'PREF', 6: 'PMPG' }; const FLOC_FORMAT = { 'EID': 1, 'SEGMENT': 2 } // BB stands for Blue BillyWig const BB_RENDERER = { bootstrapPlayer: function(bid) { const config = { code: bid.adUnitCode, }; if (bid.vastXml) config.vastXml = bid.vastXml; else if (bid.vastUrl) config.vastUrl = bid.vastUrl; if (!bid.vastXml && !bid.vastUrl) { utils.logWarn(`${LOG_WARN_PREFIX}: No vastXml or vastUrl on bid, bailing...`); return; } const rendererId = BB_RENDERER.getRendererId(PUBLICATION, bid.rendererCode); const ele = document.getElementById(bid.adUnitCode); // NB convention let renderer; for (let rendererIndex = 0; rendererIndex < window.bluebillywig.renderers.length; rendererIndex++) { if (window.bluebillywig.renderers[rendererIndex]._id === rendererId) { renderer = window.bluebillywig.renderers[rendererIndex]; break; } } if (renderer) renderer.bootstrap(config, ele); else utils.logWarn(`${LOG_WARN_PREFIX}: Couldn't find a renderer with ${rendererId}`); }, newRenderer: function(rendererCode, adUnitCode) { var rendererUrl = RENDERER_URL.replace('$RENDERER', rendererCode); const renderer = Renderer.install({ url: rendererUrl, loaded: false, adUnitCode }); try { renderer.setRender(BB_RENDERER.outstreamRender); } catch (err) { utils.logWarn(`${LOG_WARN_PREFIX}: Error tying to setRender on renderer`, err); } return renderer; }, outstreamRender: function(bid) { bid.renderer.push(function() { BB_RENDERER.bootstrapPlayer(bid) }); }, getRendererId: function(pub, renderer) { return `${pub}-${renderer}`; // NB convention! } }; const MEDIATYPE = [ BANNER, VIDEO, NATIVE ] let publisherId = 0; let isInvalidNativeRequest = false; let NATIVE_ASSET_ID_TO_KEY_MAP = {}; let NATIVE_ASSET_KEY_TO_ASSET_MAP = {}; // loading NATIVE_ASSET_ID_TO_KEY_MAP utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAsset.KEY }); // loading NATIVE_ASSET_KEY_TO_ASSET_MAP utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); function _getDomainFromURL(url) { let anchor = document.createElement('a'); anchor.href = url; return anchor.hostname; } function _parseSlotParam(paramName, paramValue) { if (!utils.isStr(paramValue)) { paramValue && utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue); return UNDEFINED; } switch (paramName) { case 'pmzoneid': return paramValue.split(',').slice(0, 50).map(id => id.trim()).join(); case 'kadfloor': return parseFloat(paramValue) || UNDEFINED; case 'lat': return parseFloat(paramValue) || UNDEFINED; case 'lon': return parseFloat(paramValue) || UNDEFINED; case 'yob': return parseInt(paramValue) || UNDEFINED; default: return paramValue; } } function _cleanSlot(slotName) { if (utils.isStr(slotName)) { return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); } if (slotName) { utils.logWarn(BIDDER_CODE + ': adSlot must be a string. Ignoring adSlot'); } return ''; } function _parseAdSlot(bid) { bid.params.adUnit = ''; bid.params.adUnitIndex = '0'; bid.params.width = 0; bid.params.height = 0; bid.params.adSlot = _cleanSlot(bid.params.adSlot); var slot = bid.params.adSlot; var splits = slot.split(':'); slot = splits[0]; if (splits.length == 2) { bid.params.adUnitIndex = splits[1]; } // check if size is mentioned in sizes array. in that case do not check for @ in adslot splits = slot.split('@'); bid.params.adUnit = splits[0]; if (splits.length > 1) { // i.e size is specified in adslot, so consider that and ignore sizes array splits = splits[1].split('x'); if (splits.length != 2) { utils.logWarn(LOG_WARN_PREFIX + 'AdSlot Error: adSlot not in required format'); return; } bid.params.width = parseInt(splits[0], 10); bid.params.height = parseInt(splits[1], 10); } else if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(BANNER) && bid.mediaTypes.banner.hasOwnProperty('sizes')) { var i = 0; var sizeArray = []; for (;i < bid.mediaTypes.banner.sizes.length; i++) { if (bid.mediaTypes.banner.sizes[i].length === 2) { // sizes[i].length will not be 2 in case where size is set as fluid, we want to skip that entry sizeArray.push(bid.mediaTypes.banner.sizes[i]); } } bid.mediaTypes.banner.sizes = sizeArray; if (bid.mediaTypes.banner.sizes.length >= 1) { // set the first size in sizes array in bid.params.width and bid.params.height. These will be sent as primary size. // The rest of the sizes will be sent in format array. bid.params.width = bid.mediaTypes.banner.sizes[0][0]; bid.params.height = bid.mediaTypes.banner.sizes[0][1]; bid.mediaTypes.banner.sizes = bid.mediaTypes.banner.sizes.splice(1, bid.mediaTypes.banner.sizes.length - 1); } } } function _initConf(refererInfo) { return { pageURL: (refererInfo && refererInfo.referer) ? refererInfo.referer : window.location.href, refURL: window.document.referrer }; } function _handleCustomParams(params, conf) { if (!conf.kadpageurl) { conf.kadpageurl = conf.pageURL; } var key, value, entry; for (key in CUSTOM_PARAMS) { if (CUSTOM_PARAMS.hasOwnProperty(key)) { value = params[key]; if (value) { entry = CUSTOM_PARAMS[key]; if (typeof entry === 'object') { // will be used in future when we want to process a custom param before using // 'keyname': {f: function() {}} value = entry.f(value, conf); } if (utils.isStr(value)) { conf[key] = value; } else { utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : ' + key + ' with value : ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value); } } } } return conf; } function _createOrtbTemplate(conf) { return { id: '' + new Date().getTime(), at: AUCTION_TYPE, cur: [DEFAULT_CURRENCY], imp: [], site: { page: conf.pageURL, ref: conf.refURL, publisher: {} }, device: { ua: navigator.userAgent, js: 1, dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, h: screen.height, w: screen.width, language: navigator.language }, user: {}, ext: {} }; } function _checkParamDataType(key, value, datatype) { var errMsg = 'Ignoring param key: ' + key + ', expects ' + datatype + ', found ' + typeof value; var functionToExecute; switch (datatype) { case DATA_TYPES.BOOLEAN: functionToExecute = utils.isBoolean; break; case DATA_TYPES.NUMBER: functionToExecute = utils.isNumber; break; case DATA_TYPES.STRING: functionToExecute = utils.isStr; break; case DATA_TYPES.ARRAY: functionToExecute = utils.isArray; break; } if (functionToExecute(value)) { return value; } utils.logWarn(LOG_WARN_PREFIX + errMsg); return UNDEFINED; } function _commonNativeRequestObject(nativeAsset, params) { var key = nativeAsset.KEY; return { id: nativeAsset.ID, required: params[key].required ? 1 : 0, data: { type: nativeAsset.TYPE, len: params[key].len, ext: params[key].ext } }; } function _createNativeRequest(params) { var nativeRequestObject = { assets: [] }; for (var key in params) { if (params.hasOwnProperty(key)) { var assetObj = {}; if (!(nativeRequestObject.assets && nativeRequestObject.assets.length > 0 && nativeRequestObject.assets.hasOwnProperty(key))) { switch (key) { case NATIVE_ASSETS.TITLE.KEY: if (params[key].len || params[key].length) { assetObj = { id: NATIVE_ASSETS.TITLE.ID, required: params[key].required ? 1 : 0, title: { len: params[key].len || params[key].length, ext: params[key].ext } }; } else { utils.logWarn(LOG_WARN_PREFIX + 'Error: Title Length is required for native ad: ' + JSON.stringify(params)); } break; case NATIVE_ASSETS.IMAGE.KEY: if (params[key].sizes && params[key].sizes.length > 0) { assetObj = { id: NATIVE_ASSETS.IMAGE.ID, required: params[key].required ? 1 : 0, img: { type: NATIVE_ASSET_IMAGE_TYPE.IMAGE, w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), wmin: params[key].wmin || params[key].minimumWidth || (params[key].minsizes ? params[key].minsizes[0] : UNDEFINED), hmin: params[key].hmin || params[key].minimumHeight || (params[key].minsizes ? params[key].minsizes[1] : UNDEFINED), mimes: params[key].mimes, ext: params[key].ext, } }; } else { utils.logWarn(LOG_WARN_PREFIX + 'Error: Image sizes is required for native ad: ' + JSON.stringify(params)); } break; case NATIVE_ASSETS.ICON.KEY: if (params[key].sizes && params[key].sizes.length > 0) { assetObj = { id: NATIVE_ASSETS.ICON.ID, required: params[key].required ? 1 : 0, img: { type: NATIVE_ASSET_IMAGE_TYPE.ICON, w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), } }; } else { utils.logWarn(LOG_WARN_PREFIX + 'Error: Icon sizes is required for native ad: ' + JSON.stringify(params)); }; break; case NATIVE_ASSETS.VIDEO.KEY: assetObj = { id: NATIVE_ASSETS.VIDEO.ID, required: params[key].required ? 1 : 0, video: { minduration: params[key].minduration, maxduration: params[key].maxduration, protocols: params[key].protocols, mimes: params[key].mimes, ext: params[key].ext } }; break; case NATIVE_ASSETS.EXT.KEY: assetObj = { id: NATIVE_ASSETS.EXT.ID, required: params[key].required ? 1 : 0, }; break; case NATIVE_ASSETS.LOGO.KEY: assetObj = { id: NATIVE_ASSETS.LOGO.ID, required: params[key].required ? 1 : 0, img: { type: NATIVE_ASSET_IMAGE_TYPE.LOGO, w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED) } }; break; case NATIVE_ASSETS.SPONSOREDBY.KEY: case NATIVE_ASSETS.BODY.KEY: case NATIVE_ASSETS.RATING.KEY: case NATIVE_ASSETS.LIKES.KEY: case NATIVE_ASSETS.DOWNLOADS.KEY: case NATIVE_ASSETS.PRICE.KEY: case NATIVE_ASSETS.SALEPRICE.KEY: case NATIVE_ASSETS.PHONE.KEY: case NATIVE_ASSETS.ADDRESS.KEY: case NATIVE_ASSETS.DESC2.KEY: case NATIVE_ASSETS.DISPLAYURL.KEY: case NATIVE_ASSETS.CTA.KEY: assetObj = _commonNativeRequestObject(NATIVE_ASSET_KEY_TO_ASSET_MAP[key], params); break; } } } if (assetObj && assetObj.id) { nativeRequestObject.assets[nativeRequestObject.assets.length] = assetObj; } }; // for native image adtype prebid has to have few required assests i.e. title,sponsoredBy, image // if any of these are missing from the request then request will not be sent var requiredAssetCount = NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.length; var presentrequiredAssetCount = 0; NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.forEach(ele => { var lengthOfExistingAssets = nativeRequestObject.assets.length; for (var i = 0; i < lengthOfExistingAssets; i++) { if (ele.id == nativeRequestObject.assets[i].id) { presentrequiredAssetCount++; break; } } }); if (requiredAssetCount == presentrequiredAssetCount) { isInvalidNativeRequest = false; } else { isInvalidNativeRequest = true; } return nativeRequestObject; } function _createBannerRequest(bid) { var sizes = bid.mediaTypes.banner.sizes; var format = []; var bannerObj; if (sizes !== UNDEFINED && utils.isArray(sizes)) { bannerObj = {}; if (!bid.params.width && !bid.params.height) { if (sizes.length === 0) { // i.e. since bid.params does not have width or height, and length of sizes is 0, need to ignore this banner imp bannerObj = UNDEFINED; utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); return bannerObj; } else { bannerObj.w = parseInt(sizes[0][0], 10); bannerObj.h = parseInt(sizes[0][1], 10); sizes = sizes.splice(1, sizes.length - 1); } } else { bannerObj.w = bid.params.width; bannerObj.h = bid.params.height; } if (sizes.length > 0) { format = []; sizes.forEach(function (size) { if (size.length > 1) { format.push({ w: size[0], h: size[1] }); } }); if (format.length > 0) { bannerObj.format = format; } } bannerObj.pos = 0; bannerObj.topframe = utils.inIframe() ? 0 : 1; } else { utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); bannerObj = UNDEFINED; } return bannerObj; } function _createVideoRequest(bid) { var videoData = utils.mergeDeep(utils.deepAccess(bid.mediaTypes, 'video'), bid.params.video); var videoObj; if (videoData !== UNDEFINED) { videoObj = {}; for (var key in VIDEO_CUSTOM_PARAMS) { if (videoData.hasOwnProperty(key)) { videoObj[key] = _checkParamDataType(key, videoData[key], VIDEO_CUSTOM_PARAMS[key]); } } // read playersize and assign to h and w. if (utils.isArray(bid.mediaTypes.video.playerSize[0])) { videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0][0], 10); videoObj.h = parseInt(bid.mediaTypes.video.playerSize[0][1], 10); } else if (utils.isNumber(bid.mediaTypes.video.playerSize[0])) { videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0], 10); videoObj.h = parseInt(bid.mediaTypes.video.playerSize[1], 10); } } else { videoObj = UNDEFINED; utils.logWarn(LOG_WARN_PREFIX + 'Error: Video config params missing for adunit: ' + bid.params.adUnit + ' with mediaType set as video. Ignoring video impression in the adunit.'); } return videoObj; } // support for PMP deals function _addPMPDealsInImpression(impObj, bid) { if (bid.params.deals) { if (utils.isArray(bid.params.deals)) { bid.params.deals.forEach(function(dealId) { if (utils.isStr(dealId) && dealId.length > 3) { if (!impObj.pmp) { impObj.pmp = { private_auction: 0, deals: [] }; } impObj.pmp.deals.push({ id: dealId }); } else { utils.logWarn(LOG_WARN_PREFIX + 'Error: deal-id present in array bid.params.deals should be a strings with more than 3 charaters length, deal-id ignored: ' + dealId); } }); } else { utils.logWarn(LOG_WARN_PREFIX + 'Error: bid.params.deals should be an array of strings.'); } } } function _addDealCustomTargetings(imp, bid) { var dctr = ''; var dctrLen; if (bid.params.dctr) { dctr = bid.params.dctr; if (utils.isStr(dctr) && dctr.length > 0) { var arr = dctr.split('|'); dctr = ''; arr.forEach(val => { dctr += (val.length > 0) ? (val.trim() + '|') : ''; }); dctrLen = dctr.length; if (dctr.substring(dctrLen, dctrLen - 1) === '|') { dctr = dctr.substring(0, dctrLen - 1); } imp.ext['key_val'] = dctr.trim() } else { utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value'); } } } function _addJWPlayerSegmentData(imp, bid) { var jwSegData = (bid.rtd && bid.rtd.jwplayer && bid.rtd.jwplayer.targeting) || undefined; var jwPlayerData = ''; const jwMark = 'jw-'; if (jwSegData === undefined || jwSegData === '' || !jwSegData.hasOwnProperty('segments')) return; var maxLength = jwSegData.segments.length; jwPlayerData += jwMark + 'id=' + jwSegData.content.id; // add the content id first for (var i = 0; i < maxLength; i++) { jwPlayerData += '|' + jwMark + jwSegData.segments[i] + '=1'; } const ext = imp.ext; (ext && ext.key_val === undefined) ? ext.key_val = jwPlayerData : ext.key_val += '|' + jwPlayerData; } function _createImpressionObject(bid, conf) { var impObj = {}; var bannerObj; var videoObj; var nativeObj = {}; var sizes = bid.hasOwnProperty('sizes') ? bid.sizes : []; var mediaTypes = ''; var format = []; impObj = { id: bid.bidId, tagid: bid.params.adUnit || undefined, bidfloor: _parseSlotParam('kadfloor', bid.params.kadfloor), secure: 1, ext: { pmZoneId: _parseSlotParam('pmzoneid', bid.params.pmzoneid) }, bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY }; _addPMPDealsInImpression(impObj, bid); _addDealCustomTargetings(impObj, bid); _addJWPlayerSegmentData(impObj, bid); if (bid.hasOwnProperty('mediaTypes')) { for (mediaTypes in bid.mediaTypes) { switch (mediaTypes) { case BANNER: bannerObj = _createBannerRequest(bid); if (bannerObj !== UNDEFINED) { impObj.banner = bannerObj; } break; case NATIVE: nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeParams)); if (!isInvalidNativeRequest) { impObj.native = nativeObj; } else { utils.logWarn(LOG_WARN_PREFIX + 'Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.'); } break; case VIDEO: videoObj = _createVideoRequest(bid); if (videoObj !== UNDEFINED) { impObj.video = videoObj; } break; } } } else { // mediaTypes is not present, so this is a banner only impression // this part of code is required for older testcases with no 'mediaTypes' to run succesfully. bannerObj = { pos: 0, w: bid.params.width, h: bid.params.height, topframe: utils.inIframe() ? 0 : 1 }; if (utils.isArray(sizes) && sizes.length > 1) { sizes = sizes.splice(1, sizes.length - 1); sizes.forEach(size => { format.push({ w: size[0], h: size[1] }); }); bannerObj.format = format; } impObj.banner = bannerObj; } _addImpressionFPD(impObj, bid); _addFloorFromFloorModule(impObj, bid); return impObj.hasOwnProperty(BANNER) || impObj.hasOwnProperty(NATIVE) || impObj.hasOwnProperty(VIDEO) ? impObj : UNDEFINED; } function _addImpressionFPD(imp, bid) { const ortb2 = {...utils.deepAccess(bid, 'ortb2Imp.ext.data')}; Object.keys(ortb2).forEach(prop => { /** * Prebid AdSlot * @type {(string|undefined)} */ if (prop === 'pbadslot') { if (typeof ortb2[prop] === 'string' && ortb2[prop]) utils.deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); } else if (prop === 'adserver') { /** * Copy GAM AdUnit and Name to imp */ ['name', 'adslot'].forEach(name => { /** @type {(string|undefined)} */ const value = utils.deepAccess(ortb2, `adserver.${name}`); if (typeof value === 'string' && value) { utils.deepSetValue(imp, `ext.data.adserver.${name.toLowerCase()}`, value); // copy GAM ad unit id as imp[].ext.dfp_ad_unit_code if (name === 'adslot') { utils.deepSetValue(imp, `ext.dfp_ad_unit_code`, value); } } }); } else { utils.deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); } }); } function _addFloorFromFloorModule(impObj, bid) { let bidFloor = -1; // get lowest floor from floorModule if (typeof bid.getFloor === 'function' && !config.getConfig('pubmatic.disableFloors')) { [BANNER, VIDEO, NATIVE].forEach(mediaType => { if (impObj.hasOwnProperty(mediaType)) { let floorInfo = bid.getFloor({ currency: impObj.bidfloorcur, mediaType: mediaType, size: '*' }); if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) { let mediaTypeFloor = parseFloat(floorInfo.floor); bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor)) } } }); } // get highest from impObj.bidfllor and floor from floor module // as we are using Math.max, it is ok if we have not got any floor from floorModule, then value of bidFloor will be -1 if (impObj.bidfloor) { bidFloor = Math.max(bidFloor, impObj.bidfloor) } // assign value only if bidFloor is > 0 impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : UNDEFINED); } function _getFlocId(validBidRequests, flocFormat) { var flocIdObject = null; var flocId = utils.deepAccess(validBidRequests, '0.userId.flocId'); if (flocId && flocId.id) { switch (flocFormat) { case FLOC_FORMAT.SEGMENT: flocIdObject = { id: 'FLOC', name: 'FLOC', ext: { ver: flocId.version }, segment: [{ id: flocId.id, name: 'chrome.com', value: flocId.id.toString() }] } break; case FLOC_FORMAT.EID: default: flocIdObject = { source: 'chrome.com', uids: [ { atype: 1, id: flocId.id, ext: { ver: flocId.version } }, ] } break; } } return flocIdObject; } function _handleFlocId(payload, validBidRequests) { var flocObject = _getFlocId(validBidRequests, FLOC_FORMAT.SEGMENT); if (flocObject) { if (!payload.user) { payload.user = {}; } if (!payload.user.data) { payload.user.data = []; } payload.user.data.push(flocObject); } } function _handleEids(payload, validBidRequests) { let bidUserIdAsEids = utils.deepAccess(validBidRequests, '0.userIdAsEids'); let flocObject = _getFlocId(validBidRequests, FLOC_FORMAT.EID); if (flocObject) { if (!bidUserIdAsEids) { bidUserIdAsEids = []; } bidUserIdAsEids.push(flocObject); } if (utils.isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { utils.deepSetValue(payload, 'user.eids', bidUserIdAsEids); } } function _checkMediaType(bid, newBid) { // Create a regex here to check the strings if (bid.ext && bid.ext['BidType'] != undefined) { newBid.mediaType = MEDIATYPE[bid.ext.BidType]; } else { utils.logInfo(LOG_WARN_PREFIX + 'bid.ext.BidType does not exist, checking alternatively for mediaType') var adm = bid.adm; var admStr = ''; var videoRegex = new RegExp(/VAST\s+version/); if (adm.indexOf('span class="PubAPIAd"') >= 0) { newBid.mediaType = BANNER; } else if (videoRegex.test(adm)) { newBid.mediaType = VIDEO; } else { try { admStr = JSON.parse(adm.replace(/\\/g, '')); if (admStr && admStr.native) { newBid.mediaType = NATIVE; } } catch (e) { utils.logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + adm); } } } } function _parseNativeResponse(bid, newBid) { newBid.native = {}; if (bid.hasOwnProperty('adm')) { var adm = ''; try { adm = JSON.parse(bid.adm.replace(/\\/g, '')); } catch (ex) { utils.logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + newBid.adm); return; } if (adm && adm.native && adm.native.assets && adm.native.assets.length > 0) { newBid.mediaType = NATIVE; for (let i = 0, len = adm.native.assets.length; i < len; i++) { switch (adm.native.assets[i].id) { case NATIVE_ASSETS.TITLE.ID: newBid.native.title = adm.native.assets[i].title && adm.native.assets[i].title.text; break; case NATIVE_ASSETS.IMAGE.ID: newBid.native.image = { url: adm.native.assets[i].img && adm.native.assets[i].img.url, height: adm.native.assets[i].img && adm.native.assets[i].img.h, width: adm.native.assets[i].img && adm.native.assets[i].img.w, }; break; case NATIVE_ASSETS.ICON.ID: newBid.native.icon = { url: adm.native.assets[i].img && adm.native.assets[i].img.url, height: adm.native.assets[i].img && adm.native.assets[i].img.h, width: adm.native.assets[i].img && adm.native.assets[i].img.w, }; break; case NATIVE_ASSETS.SPONSOREDBY.ID: case NATIVE_ASSETS.BODY.ID: case NATIVE_ASSETS.LIKES.ID: case NATIVE_ASSETS.DOWNLOADS.ID: case NATIVE_ASSETS.PRICE: case NATIVE_ASSETS.SALEPRICE.ID: case NATIVE_ASSETS.PHONE.ID: case NATIVE_ASSETS.ADDRESS.ID: case NATIVE_ASSETS.DESC2.ID: case NATIVE_ASSETS.CTA.ID: case NATIVE_ASSETS.RATING.ID: case NATIVE_ASSETS.DISPLAYURL.ID: newBid.native[NATIVE_ASSET_ID_TO_KEY_MAP[adm.native.assets[i].id]] = adm.native.assets[i].data && adm.native.assets[i].data.value; break; } } newBid.native.clickUrl = adm.native.link && adm.native.link.url; newBid.native.clickTrackers = (adm.native.link && adm.native.link.clicktrackers) || []; newBid.native.impressionTrackers = adm.native.imptrackers || []; newBid.native.jstracker = adm.native.jstracker || []; if (!newBid.width) { newBid.width = DEFAULT_WIDTH; } if (!newBid.height) { newBid.height = DEFAULT_HEIGHT; } } } } function _blockedIabCategoriesValidation(payload, blockedIabCategories) { blockedIabCategories = blockedIabCategories .filter(function(category) { if (typeof category === 'string') { // only strings return true; } else { utils.logWarn(LOG_WARN_PREFIX + 'bcat: Each category should be a string, ignoring category: ' + category); return false; } }) .map(category => category.trim()) // trim all .filter(function(category, index, arr) { // more than 3 charaters length if (category.length > 3) { return arr.indexOf(category) === index; // unique value only } else { utils.logWarn(LOG_WARN_PREFIX + 'bcat: Each category should have a value of a length of more than 3 characters, ignoring category: ' + category) } }); if (blockedIabCategories.length > 0) { utils.logWarn(LOG_WARN_PREFIX + 'bcat: Selected: ', blockedIabCategories); payload.bcat = blockedIabCategories; } } function _assignRenderer(newBid, request) { let bidParams, context, adUnitCode; if (request.bidderRequest && request.bidderRequest.bids) { for (let bidderRequestBidsIndex = 0; bidderRequestBidsIndex < request.bidderRequest.bids.length; bidderRequestBidsIndex++) { if (request.bidderRequest.bids[bidderRequestBidsIndex].bidId === newBid.requestId) { bidParams = request.bidderRequest.bids[bidderRequestBidsIndex].params; context = request.bidderRequest.bids[bidderRequestBidsIndex].mediaTypes[VIDEO].context; adUnitCode = request.bidderRequest.bids[bidderRequestBidsIndex].adUnitCode; } } if (context && context === 'outstream' && bidParams && bidParams.outstreamAU && adUnitCode) { newBid.rendererCode = bidParams.outstreamAU; newBid.renderer = BB_RENDERER.newRenderer(newBid.rendererCode, adUnitCode); } } } function isNonEmptyArray(test) { if (utils.isArray(test) === true) { if (test.length > 0) { return true; } } return false; } export const spec = { code: BIDDER_CODE, gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: bid => { if (bid && bid.params) { if (!utils.isStr(bid.params.publisherId)) { utils.logWarn(LOG_WARN_PREFIX + 'Error: publisherId is mandatory and cannot be numeric (wrap it in quotes in your config). Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid)); return false; } // video ad validation if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { // bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array let mediaTypesVideoMimes = utils.deepAccess(bid.mediaTypes, 'video.mimes'); let paramsVideoMimes = utils.deepAccess(bid, 'params.video.mimes'); if (isNonEmptyArray(mediaTypesVideoMimes) === false && isNonEmptyArray(paramsVideoMimes) === false) { utils.logWarn(LOG_WARN_PREFIX + 'Error: For video ads, bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array. Call to OpenBid will not be sent for ad unit:' + JSON.stringify(bid)); return false; } if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { utils.logError(`${LOG_WARN_PREFIX}: no context specified in bid. Rejecting bid: `, bid); return false; } if (bid.mediaTypes[VIDEO].context === 'outstream' && !utils.isStr(bid.params.outstreamAU) && !bid.hasOwnProperty('renderer') && !bid.mediaTypes[VIDEO].hasOwnProperty('renderer')) { // we are here since outstream ad-unit is provided without outstreamAU and renderer // so it is not a valid video ad-unit // but it may be valid banner or native ad-unit // so if mediaType banner or Native is present then we will remove media-type video and return true if (bid.mediaTypes.hasOwnProperty(BANNER) || bid.mediaTypes.hasOwnProperty(NATIVE)) { delete bid.mediaTypes[VIDEO]; utils.logWarn(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting mediatype Video of bid: `, bid); return true; } else { utils.logError(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting bid: `, bid); return false; } } } return true; } return false; }, /** * Make a server request from the list of BidRequests. * * @param {validBidRequests[]} - an array of bids * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { var refererInfo; if (bidderRequest && bidderRequest.refererInfo) { refererInfo = bidderRequest.refererInfo; } var conf = _initConf(refererInfo); var payload = _createOrtbTemplate(conf); var bidCurrency = ''; var dctrArr = []; var bid; var blockedIabCategories = []; validBidRequests.forEach(originalBid => { bid = utils.deepClone(originalBid); bid.params.adSlot = bid.params.adSlot || ''; _parseAdSlot(bid); if (bid.params.hasOwnProperty('video')) { // Nothing to do } else { // If we have a native mediaType configured alongside banner, its ok if the banner size is not set in width and height // The corresponding banner imp object will not be generated, but we still want the native object to be sent, hence the following check if (!(bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(NATIVE)) && bid.params.width === 0 && bid.params.height === 0) { utils.logWarn(LOG_WARN_PREFIX + 'Skipping the non-standard adslot: ', bid.params.adSlot, JSON.stringify(bid)); return; } } conf.pubId = conf.pubId || bid.params.publisherId; conf = _handleCustomParams(bid.params, conf); conf.transactionId = bid.transactionId; if (bidCurrency === '') { bidCurrency = bid.params.currency || UNDEFINED; } else if (bid.params.hasOwnProperty('currency') && bidCurrency !== bid.params.currency) { utils.logWarn(LOG_WARN_PREFIX + 'Currency specifier ignored. Only one currency permitted.'); } bid.params.currency = bidCurrency; // check if dctr is added to more than 1 adunit if (bid.params.hasOwnProperty('dctr') && utils.isStr(bid.params.dctr)) { dctrArr.push(bid.params.dctr); } if (bid.params.hasOwnProperty('bcat') && utils.isArray(bid.params.bcat)) { blockedIabCategories = blockedIabCategories.concat(bid.params.bcat); } var impObj = _createImpressionObject(bid, conf); if (impObj) { payload.imp.push(impObj); } }); if (payload.imp.length == 0) { return; } payload.site.publisher.id = conf.pubId.trim(); publisherId = conf.pubId.trim(); payload.ext.wrapper = {}; payload.ext.wrapper.profile = parseInt(conf.profId) || UNDEFINED; payload.ext.wrapper.version = parseInt(conf.verId) || UNDEFINED; payload.ext.wrapper.wiid = conf.wiid || bidderRequest.auctionId; // eslint-disable-next-line no-undef payload.ext.wrapper.wv = $$REPO_AND_VERSION$$; payload.ext.wrapper.transactionId = conf.transactionId; payload.ext.wrapper.wp = 'pbjs'; payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); payload.user.geo = {}; payload.user.geo.lat = _parseSlotParam('lat', conf.lat); payload.user.geo.lon = _parseSlotParam('lon', conf.lon); payload.user.yob = _parseSlotParam('yob', conf.yob); payload.device.geo = payload.user.geo; payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); payload.site.domain = _getDomainFromURL(payload.site.page); // add the content object from config in request if (typeof config.getConfig('content') === 'object') { payload.site.content = config.getConfig('content'); } // merge the device from config.getConfig('device') if (typeof config.getConfig('device') === 'object') { payload.device = Object.assign(payload.device, config.getConfig('device')); } // passing transactionId in source.tid utils.deepSetValue(payload, 'source.tid', conf.transactionId); // test bids if (window.location.href.indexOf('pubmaticTest=true') !== -1) { payload.test = 1; } // adding schain object if (validBidRequests[0].schain) { utils.deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain); } // Attaching GDPR Consent Params if (bidderRequest && bidderRequest.gdprConsent) { utils.deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); utils.deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); } // CCPA if (bidderRequest && bidderRequest.uspConsent) { utils.deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); } // coppa compliance if (config.getConfig('coppa') === true) { utils.deepSetValue(payload, 'regs.coppa', 1); } _handleEids(payload, validBidRequests); _blockedIabCategoriesValidation(payload, blockedIabCategories); _handleFlocId(payload, validBidRequests); // First Party Data const commonFpd = config.getConfig('ortb2') || {}; if (commonFpd.site) { utils.mergeDeep(payload, {site: commonFpd.site}); } if (commonFpd.user) { utils.mergeDeep(payload, {user: commonFpd.user}); } // Note: Do not move this block up // if site object is set in Prebid config then we need to copy required fields from site into app and unset the site object if (typeof config.getConfig('app') === 'object') { payload.app = config.getConfig('app'); // not copying domain from site as it is a derived value from page payload.app.publisher = payload.site.publisher; payload.app.ext = payload.site.ext || UNDEFINED; // We will also need to pass content object in app.content if app object is also set into the config; // BUT do not use content object from config if content object is present in app as app.content if (typeof payload.app.content !== 'object') { payload.app.content = payload.site.content || UNDEFINED; } delete payload.site; } return { method: 'POST', url: ENDPOINT, data: JSON.stringify(payload), bidderRequest: bidderRequest }; }, /** * Unpack the response from the server into a list of bids. * * @param {*} response A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: (response, request) => { const bidResponses = []; var respCur = DEFAULT_CURRENCY; let parsedRequest = JSON.parse(request.data); let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; try { if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) { // Supporting multiple bid responses for same adSize respCur = response.body.cur || respCur; response.body.seatbid.forEach(seatbidder => { seatbidder.bid && utils.isArray(seatbidder.bid) && seatbidder.bid.forEach(bid => { let newBid = { requestId: bid.impid, cpm: (parseFloat(bid.price) || 0).toFixed(2), width: bid.w, height: bid.h, creativeId: bid.crid || bid.id, dealId: bid.dealid, currency: respCur, netRevenue: NET_REVENUE, ttl: 300, referrer: parsedReferrer, ad: bid.adm, pm_seat: seatbidder.seat || null, pm_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null, partnerImpId: bid.id || '' // partner impression Id }; if (parsedRequest.imp && parsedRequest.imp.length > 0) { parsedRequest.imp.forEach(req => { if (bid.impid === req.id) { _checkMediaType(bid, newBid); switch (newBid.mediaType) { case BANNER: break; case VIDEO: newBid.width = bid.hasOwnProperty('w') ? bid.w : req.video.w; newBid.height = bid.hasOwnProperty('h') ? bid.h : req.video.h; newBid.vastXml = bid.adm; _assignRenderer(newBid, request); break; case NATIVE: _parseNativeResponse(bid, newBid); break; } } }); } if (bid.ext && bid.ext.deal_channel) { newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null; } newBid.meta = {}; if (bid.ext && bid.ext.dspid) { newBid.meta.networkId = bid.ext.dspid; } if (bid.ext && bid.ext.advid) { newBid.meta.buyerId = bid.ext.advid; } if (bid.adomain && bid.adomain.length > 0) { newBid.meta.advertiserDomains = bid.adomain; newBid.meta.clickUrl = bid.adomain[0]; } // adserverTargeting if (seatbidder.ext && seatbidder.ext.buyid) { newBid.adserverTargeting = { 'hb_buyid_pubmatic': seatbidder.ext.buyid }; } bidResponses.push(newBid); }); }); } } catch (error) { utils.logError(error); } return bidResponses; }, /** * Register User Sync. */ getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { let syncurl = '' + publisherId; // Attaching GDPR Consent Params in UserSync url if (gdprConsent) { syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); } // CCPA if (uspConsent) { syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); } // coppa compliance if (config.getConfig('coppa') === true) { syncurl += '&coppa=1'; } if (syncOptions.iframeEnabled) { return [{ type: 'iframe', url: USER_SYNC_URL_IFRAME + syncurl }]; } else { return [{ type: 'image', url: USER_SYNC_URL_IMAGE + syncurl }]; } }, /** * Covert bid param types for S2S * @param {Object} params bid params * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol * @return {Object} params bid params */ transformBidParams: function (params, isOpenRtb) { return utils.convertTypes({ 'publisherId': 'string', 'adSlot': 'string' }, params); } }; registerBidder(spec);