prebid/Prebid.js

View on GitHub
modules/nobidBidAdapter.js

Summary

Maintainability
F
1 wk
Test Coverage
import { logInfo, deepAccess, logWarn, isArray, getParameterByName } from '../src/utils.js';
import { config } from '../src/config.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { getStorageManager } from '../src/storageManager.js';
import { hasPurpose1Consent } from '../src/utils/gdpr.js';

/**
 * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
 * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
 * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse
 * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions
 * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync
 * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests
 */

const GVLID = 816;
const BIDDER_CODE = 'nobid';
const storage = getStorageManager({bidderCode: BIDDER_CODE});
window.nobidVersion = '1.3.4';
window.nobid = window.nobid || {};
window.nobid.bidResponses = window.nobid.bidResponses || {};
window.nobid.timeoutTotal = 0;
window.nobid.bidWonTotal = 0;
window.nobid.refreshCount = 0;
function log(msg, obj) {
  logInfo('-NoBid- ' + msg, obj)
}
function nobidSetCookie(cname, cvalue, hours) {
  var d = new Date();
  d.setTime(d.getTime() + (hours * 60 * 60 * 1000));
  var expires = 'expires=' + d.toUTCString();
  storage.setCookie(cname, cvalue, expires);
}
function nobidGetCookie(cname) {
  return storage.getCookie(cname);
}
function nobidBuildRequests(bids, bidderRequest) {
  var serializeState = function(divIds, siteId, adunits) {
    var filterAdUnitsByIds = function(divIds, adUnits) {
      var filtered = [];
      if (!divIds.length) {
        filtered = adUnits;
      } else if (adUnits) {
        var a = [];
        if (!(divIds instanceof Array)) a.push(divIds);
        else a = divIds;
        for (var i = 0, l = adUnits.length; i < l; i++) {
          var adUnit = adUnits[i];
          if (adUnit && adUnit.d && (a.indexOf(adUnit.d) > -1)) {
            filtered.push(adUnit);
          }
        }
      }
      return filtered;
    }
    var gdprConsent = function(bidderRequest) {
      var gdprConsent = {};
      if (bidderRequest && bidderRequest.gdprConsent) {
        gdprConsent = {
          consentString: bidderRequest.gdprConsent.consentString,
          // will check if the gdprApplies field was populated with a boolean value (ie from page config).  If it's undefined, then default to true
          consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : false
        }
      }
      return gdprConsent;
    }
    var uspConsent = function(bidderRequest) {
      var uspConsent = '';
      if (bidderRequest && bidderRequest.uspConsent) {
        uspConsent = bidderRequest.uspConsent;
      }
      return uspConsent;
    }
    var gppConsent = function(bidderRequest) {
      let gppConsent = null;
      if (bidderRequest?.gppConsent?.gppString && bidderRequest?.gppConsent?.applicableSections) {
        gppConsent = {};
        gppConsent.gpp = bidderRequest.gppConsent.gppString;
        gppConsent.gpp_sid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections : [];
      } else if (bidderRequest?.ortb2?.regs?.gpp && bidderRequest?.ortb2.regs?.gpp_sid) {
        gppConsent = {};
        gppConsent.gpp = bidderRequest.ortb2.regs.gpp;
        gppConsent.gpp_sid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid : [];
      }
      return gppConsent;
    }
    var schain = function(bids) {
      if (bids && bids.length > 0) {
        return bids[0].schain
      }
      return null;
    }
    var coppa = function() {
      if (config.getConfig('coppa') === true) {
        return {'coppa': true};
      }
      if (bids && bids.length > 0) {
        return bids[0].coppa
      }
      return null;
    }
    var topLocation = function(bidderRequest) {
      var ret = '';
      if (bidderRequest?.refererInfo?.page) {
        ret = bidderRequest.refererInfo.page;
      } else {
        // TODO: does this fallback make sense here?
        ret = (window.context && window.context.location && window.context.location.href) ? window.context.location.href : document.location.href;
      }
      return encodeURIComponent(ret.replace(/\%/g, ''));
    }
    var timestamp = function() {
      var date = new Date();
      var zp = function (val) { return (val <= 9 ? '0' + val : '' + val); }
      var d = date.getDate();
      var y = date.getFullYear();
      var m = date.getMonth() + 1;
      var h = date.getHours();
      var min = date.getMinutes();
      var s = date.getSeconds();
      return '' + y + '-' + zp(m) + '-' + zp(d) + ' ' + zp(h) + ':' + zp(min) + ':' + zp(s);
    };
    var clientDim = function() {
      try {
        var width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
        var height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
        return `${width}x${height}`;
      } catch (e) {
        logWarn('Could not parse screen dimensions, error details:', e);
      }
    }
    var getEIDs = function(eids) {
      if (isArray(eids) && eids.length > 0) {
        let src = [];
        eids.forEach((eid) => {
          let ids = [];
          if (eid.uids) {
            eid.uids.forEach(value => {
              ids.push({'id': value.id + ''});
            });
          }
          if (eid.source && ids.length > 0) {
            src.push({source: eid.source, uids: ids});
          }
        });
        return src;
      }
    }
    var state = {};
    state['sid'] = siteId;
    state['l'] = topLocation(bidderRequest);
    state['tt'] = encodeURIComponent(document.title);
    state['tt'] = state['tt'].replace(/'|;|quot;|39;|&amp;|&|#|\r\n|\r|\n|\t|\f|\%0A|\"|\%22|\%5C|\%23|\%26|\%26|\%09/gm, '');
    state['a'] = filterAdUnitsByIds(divIds, adunits || []);
    state['t'] = timestamp();
    state['tz'] = Math.round(new Date().getTimezoneOffset());
    state['r'] = clientDim();
    state['lang'] = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage;
    state['ref'] = document.referrer;
    state['gdpr'] = gdprConsent(bidderRequest);
    state['usp'] = uspConsent(bidderRequest);
    state['pjbdr'] = (bidderRequest && bidderRequest.bidderCode) ? bidderRequest.bidderCode : 'nobid';
    state['pbver'] = '$prebid.version$';
    const sch = schain(bids);
    if (sch) state['schain'] = sch;
    const cop = coppa();
    if (cop) state['coppa'] = cop;
    const eids = getEIDs(deepAccess(bids, '0.userIdAsEids'));
    if (eids && eids.length > 0) state['eids'] = eids;
    const gpp = gppConsent(bidderRequest);
    if (gpp?.gpp) state['gpp'] = gpp.gpp;
    if (gpp?.gpp_sid) state['gpp_sid'] = gpp.gpp_sid;
    if (bidderRequest && bidderRequest.ortb2) state['ortb2'] = bidderRequest.ortb2;
    return state;
  };
  function newAdunit(adunitObject, adunits) {
    var getAdUnit = function(divid, adunits) {
      for (var i = 0; i < adunits.length; i++) {
        if (adunits[i].d === divid) {
          return adunits[i];
        }
      }
      return false;
    }
    var removeByAttrValue = function(array, attribute, value) {
      for (var i = array.length - 1; i >= 0; i--) {
        var entry = array[i];
        if (entry[attribute] && entry[attribute] === value) {
          array.splice(i, 1);
        }
      }
    }
    var a = getAdUnit(adunitObject.div, adunits) || {};
    if (adunitObject.account) {
      a.s = adunitObject.account;
    }
    if (adunitObject.sizes) {
      a.z = adunitObject.sizes;
    }
    if (adunitObject.div) {
      a.d = adunitObject.div;
    }
    if (adunitObject.floor) {
      a.floor = adunitObject.floor;
    }
    if (adunitObject.targeting) {
      a.g = adunitObject.targeting;
    } else {
      a.g = {};
    }
    if (adunitObject.div) {
      removeByAttrValue(adunits, 'd', adunitObject.div);
    }
    if (adunitObject.sizeMapping) {
      a.sm = adunitObject.sizeMapping;
    }
    if (adunitObject.siteId) {
      a.sid = adunitObject.siteId;
    }
    if (adunitObject.placementId) {
      a.pid = adunitObject.placementId;
    }
    if (adunitObject.ad_type) {
      a.at = adunitObject.ad_type;
    }
    if (adunitObject.params) {
      a.params = adunitObject.params;
    }
    adunits.push(a);
    return adunits;
  }
  function getFloor (bid) {
    if (bid && typeof bid.getFloor === 'function' && bid.getFloor().floor) {
      return bid.getFloor().floor;
    }
    return null;
  }
  if (typeof window.nobid.refreshLimit !== 'undefined') {
    if (window.nobid.refreshLimit < window.nobid.refreshCount) return false;
  }
  let ublock = nobidGetCookie('_ublock');
  if (ublock) {
    log('Request blocked for user. hours: ', ublock);
    return false;
  }
  /* DISCOVER SLOTS */
  var divids = [];
  var siteId = 0;
  var adunits = [];
  for (var i = 0; i < bids.length; i++) {
    var bid = bids[i];
    var divid = bid.adUnitCode;
    divids.push(divid);
    var sizes = bid.sizes;
    siteId = (typeof bid.params['siteId'] != 'undefined' && bid.params['siteId']) ? bid.params['siteId'] : siteId;
    var placementId = bid.params['placementId'];

    let adType = 'banner';
    const videoMediaType = deepAccess(bid, 'mediaTypes.video');
    const context = deepAccess(bid, 'mediaTypes.video.context') || '';
    if (bid.mediaType === VIDEO || (videoMediaType && (context === 'instream' || context === 'outstream'))) {
      adType = 'video';
    }
    const floor = getFloor(bid);

    if (siteId) {
      newAdunit({
        div: divid,
        sizes: sizes,
        siteId: siteId,
        placementId: placementId,
        ad_type: adType,
        params: bid.params,
        floor: floor,
        ctx: context
      },
      adunits);
    }
  }
  if (siteId) {
    return serializeState(divids, siteId, adunits);
  } else {
    return false;
  }
}
function nobidInterpretResponse(response, bidRequest) {
  var findBid = function(divid, bids) {
    for (var i = 0; i < bids.length; i++) {
      if (bids[i].adUnitCode == divid) {
        return bids[i];
      }
    }
    return false;
  }
  var setRefreshLimit = function(response) {
    if (response && typeof response.rlimit !== 'undefined') window.nobid.refreshLimit = response.rlimit;
  }
  var setUserBlock = function(response) {
    if (response && typeof response.ublock !== 'undefined') {
      nobidSetCookie('_ublock', '1', response.ublock);
    }
  }
  setRefreshLimit(response);
  setUserBlock(response);
  var bidResponses = [];
  for (var i = 0; response.bids && i < response.bids.length; i++) {
    var bid = response.bids[i];
    if (bid.bdrid < 100 || !bidRequest || !bidRequest.bidderRequest || !bidRequest.bidderRequest.bids) continue;
    window.nobid.bidResponses['' + bid.id] = bid;
    var reqBid = findBid(bid.divid, bidRequest.bidderRequest.bids);
    if (!reqBid) continue;
    const bidResponse = {
      requestId: reqBid.bidId,
      cpm: 1 * ((bid.price) ? bid.price : (bid.bucket) ? bid.bucket : 0),
      width: bid.size.w,
      height: bid.size.h,
      creativeId: (bid.creativeid) || '',
      dealId: (bid.dealid) || '',
      currency: 'USD',
      netRevenue: true,
      ttl: 300,
      ad: bid.adm,
      mediaType: bid.atype || BANNER,
    };
    if (bid.vastUrl) {
      bidResponse.vastUrl = bid.vastUrl;
    }
    if (bid.vastXml) {
      bidResponse.vastXml = bid.vastXml;
    }
    if (bid.videoCacheKey) {
      bidResponse.videoCacheKey = bid.videoCacheKey;
    }
    if (bid.meta) {
      bidResponse.meta = bid.meta;
    }

    bidResponses.push(bidResponse);
  }
  return bidResponses;
};
window.nobid.renderTag = function(doc, id, win) {
  log('nobid.renderTag()', id);
  var bid = window.nobid.bidResponses['' + id];
  if (bid && bid.adm2) {
    log('nobid.renderTag() found tag', id);
    var markup = bid.adm2;
    doc.write(markup);
    doc.close();
    return;
  }
  log('nobid.renderTag() tag NOT FOUND *ERROR*', id);
}
window.addEventListener('message', function (event) {
  let key = event.message ? 'message' : 'data';
  var msg = '' + event[key];
  if (msg.substring(0, 'nbTagRenderer.requestAdMarkup|'.length) === 'nbTagRenderer.requestAdMarkup|') {
    log('Prebid received nbTagRenderer.requestAdMarkup event');
    var adId = msg.substring(msg.indexOf('|') + 1);
    if (window.nobid && window.nobid.bidResponses) {
      var bid = window.nobid.bidResponses['' + adId];
      if (bid && bid.adm2) {
        var markup = bid.adm2;
        if (markup) {
          event.source.postMessage('nbTagRenderer.renderAdInSafeFrame|' + markup, '*');
        }
      }
    }
  }
}, false);
export const spec = {
  code: BIDDER_CODE,
  gvlid: GVLID,
  aliases: [
    { code: 'duration', gvlid: 674 }
  ],
  supportedMediaTypes: [BANNER, VIDEO],
  /**
   * Determines whether or not the given bid request is valid.
   *
   * @param {BidRequest} bid The bid params to validate.
   * @return boolean True if this is a valid bid, and false otherwise.
   */
  isBidRequestValid: function(bid) {
    log('isBidRequestValid', bid);
    if (bid?.params?.siteId) 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: function(validBidRequests, bidderRequest) {
    function resolveEndpoint() {
      var ret = 'https://ads.servenobid.com/';
      var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env'));
      env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env;
      if (!env) ret = 'https://ads.servenobid.com/';
      else if (env == 'beta') ret = 'https://beta.servenobid.com/';
      else if (env == 'dev') ret = '//localhost:8282/';
      else if (env == 'qa') ret = 'https://qa-ads.nobid.com/';
      return ret;
    }
    var buildEndpoint = function() {
      return resolveEndpoint() + 'adreq?cb=' + Math.floor(Math.random() * 11000);
    }
    log('validBidRequests', validBidRequests);
    if (!validBidRequests || validBidRequests.length <= 0) {
      log('Empty validBidRequests');
      return;
    }
    const payload = nobidBuildRequests(validBidRequests, bidderRequest);
    if (!payload) return;
    window.nobid.refreshCount++;
    const payloadString = JSON.stringify(payload).replace(/'|&|#/g, '')
    const endpoint = buildEndpoint();

    let options = {};
    if (!hasPurpose1Consent(bidderRequest?.gdprConsent)) {
      options = { withCredentials: false };
    }

    return {
      method: 'POST',
      url: endpoint,
      data: payloadString,
      bidderRequest,
      options
    };
  },
  /**
   * Unpack the response from the server into a list of bids.
   *
   * @param {ServerResponse} serverResponse A successful response from the server.
   * @return {Bid[]} An array of bids which were nested inside the server.
   */
  interpretResponse: function(serverResponse, bidRequest) {
    log('interpretResponse -> serverResponse', serverResponse);
    log('interpretResponse -> bidRequest', bidRequest);
    return nobidInterpretResponse(serverResponse.body, bidRequest);
  },

  /**
   * Register the user sync pixels which should be dropped after the auction.
   *
   * @param {SyncOptions} syncOptions Which user syncs are allowed?
   * @param {ServerResponse[]} serverResponses List of server's responses.
   * @return {UserSync[]} The user syncs which should be dropped.
   */
  getUserSyncs: function(syncOptions, serverResponses, gdprConsent, usPrivacy, gppConsent) {
    if (syncOptions.iframeEnabled) {
      let params = '';
      if (gdprConsent && typeof gdprConsent.consentString === 'string') {
        // add 'gdpr' only if 'gdprApplies' is defined
        if (typeof gdprConsent.gdprApplies === 'boolean') {
          params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`;
        } else {
          params += `?gdpr_consent=${gdprConsent.consentString}`;
        }
      }
      if (usPrivacy) {
        if (params.length > 0) params += '&';
        else params += '?';
        params += 'usp_consent=' + usPrivacy;
      }
      if (gppConsent?.gppString && gppConsent?.applicableSections?.length) {
        if (params.length > 0) params += '&';
        else params += '?';
        params += 'gpp=' + encodeURIComponent(gppConsent.gppString);
        params += 'gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(','));
      }
      return [{
        type: 'iframe',
        url: 'https://public.servenobid.com/sync.html' + params
      }];
    } else if (syncOptions.pixelEnabled && serverResponses.length > 0) {
      let syncs = [];
      if (serverResponses[0].body.syncs && serverResponses[0].body.syncs.length > 0) {
        serverResponses[0].body.syncs.forEach(element => {
          syncs.push({
            type: 'image',
            url: element
          });
        });
      }
      return syncs;
    } else {
      logWarn('-NoBid- Please enable iframe based user sync.', syncOptions);
      return [];
    }
  },

  /**
   * Register bidder specific code, which will execute if bidder timed out after an auction
   * @param {data} Containing timeout specific data
   */
  onTimeout: function(data) {
    window.nobid.timeoutTotal++;
    log('Timeout total: ' + window.nobid.timeoutTotal, data);
    return window.nobid.timeoutTotal;
  },
  onBidWon: function(data) {
    window.nobid.bidWonTotal++;
    log('BidWon total: ' + window.nobid.bidWonTotal, data);
    return window.nobid.bidWonTotal;
  }
}
registerBidder(spec);