From c015d7958f611f4017e8001c3045b68d1d716745 Mon Sep 17 00:00:00 2001
From: xwang202 <57196235+xwang202@users.noreply.github.com>
Date: Fri, 3 Jan 2020 13:49:09 -0500
Subject: [PATCH] freewheelSSPBidAdapter (#4645)
* freewheelSSPBidAdapter Update the freewheelSSPBidAdapter based on Prebid 3.0 changes, add max and min size limit as per client's requirement
* freewheelSSPBidAdapter fix the path issue
* freewheelSSPBidAdapter fix the indentation
* update the test
* rename freewheel bid adapter name
* freewheelSSPBidAdapter use bidderRequest.refererInfo instead of utils.getTopWindowUrl(), remove the unused code
* freewheelSSPBidAdapter add spec file
* freewheelSSPBidAdapter update the gdpr and refererInfo condition check
---
modules/freewheel-sspBidAdapter.js | 371 ++++++++++++++++++
modules/freewheel-sspBidAdapter.md | 12 +-
.../modules/freewheel-sspBidAdapter_spec.js | 228 +++++++++++
3 files changed, 608 insertions(+), 3 deletions(-)
create mode 100644 modules/freewheel-sspBidAdapter.js
create mode 100644 test/spec/modules/freewheel-sspBidAdapter_spec.js
diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js
new file mode 100644
index 00000000000..b9164d7c122
--- /dev/null
+++ b/modules/freewheel-sspBidAdapter.js
@@ -0,0 +1,371 @@
+import * as utils from '../src/utils';
+import { registerBidder } from '../src/adapters/bidderFactory';
+
+const BIDDER_CODE = 'freewheel-ssp';
+
+const PROTOCOL = getProtocol();
+const FREEWHEEL_ADSSETUP = PROTOCOL + '://ads.stickyadstv.com/www/delivery/swfIndex.php';
+const MUSTANG_URL = PROTOCOL + '://cdn.stickyadstv.com/mustang/mustang.min.js';
+const PRIMETIME_URL = PROTOCOL + '://cdn.stickyadstv.com/prime-time/';
+const USER_SYNC_URL = PROTOCOL + '://ads.stickyadstv.com/auto-user-sync';
+
+function getProtocol() {
+ return 'https';
+}
+
+function isValidUrl(str) {
+ if (!str) {
+ return false;
+ }
+
+ // regExp for url validation
+ var pattern = /^(https?|ftp|file):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
+ return pattern.test(str);
+}
+
+function getBiggerSize(array) {
+ var result = [0, 0];
+ for (var i = 0; i < array.length; i++) {
+ if (array[i][0] * array[i][1] > result[0] * result[1]) {
+ result = array[i];
+ }
+ }
+ return result;
+}
+
+function getBiggerSizeWithLimit(array, minSizeLimit, maxSizeLimit) {
+ var minSize = minSizeLimit || [0, 0];
+ var maxSize = maxSizeLimit || [Number.MAX_VALUE, Number.MAX_VALUE];
+ var candidates = [];
+
+ for (var i = 0; i < array.length; i++) {
+ if (array[i][0] * array[i][1] >= minSize[0] * minSize[1] && array[i][0] * array[i][1] <= maxSize[0] * maxSize[1]) {
+ candidates.push(array[i]);
+ }
+ }
+
+ return getBiggerSize(candidates);
+}
+
+/*
+* read the pricing extension with this format: 1.0000
+* @return {object} pricing data in format: {currency: "EUR", price:"1.000"}
+*/
+function getPricing(xmlNode) {
+ var pricingExtNode;
+ var princingData = {};
+
+ var extensions = xmlNode.querySelectorAll('Extension');
+ // Nodelist.forEach is not supported in IE and Edge
+ // Workaround given here https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10638731/
+ Array.prototype.forEach.call(extensions, function(node) {
+ if (node.getAttribute('type') === 'StickyPricing') {
+ pricingExtNode = node;
+ }
+ });
+
+ if (pricingExtNode) {
+ var priceNode = pricingExtNode.querySelector('Price');
+ princingData = {
+ currency: priceNode.getAttribute('currency'),
+ price: priceNode.textContent || priceNode.innerText
+ };
+ } else {
+ utils.logWarn('PREBID - ' + BIDDER_CODE + ': Can\'t get pricing data. Is price awareness enabled?');
+ }
+
+ return princingData;
+}
+
+function hashcode(inputString) {
+ var hash = 0;
+ var char;
+ if (inputString.length == 0) return hash;
+ for (var i = 0; i < inputString.length; i++) {
+ char = inputString.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32bit integer
+ }
+ return hash;
+}
+
+function getCreativeId(xmlNode) {
+ var creaId = '';
+ var adNodes = xmlNode.querySelectorAll('Ad');
+ // Nodelist.forEach is not supported in IE and Edge
+ // Workaround given here https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10638731/
+ Array.prototype.forEach.call(adNodes, function(el) {
+ creaId += '[' + el.getAttribute('id') + ']';
+ });
+
+ return creaId;
+}
+
+/**
+* returns the top most accessible window
+*/
+function getTopMostWindow() {
+ var res = window;
+
+ try {
+ while (top !== res) {
+ if (res.parent.location.href.length) { res = res.parent; }
+ }
+ } catch (e) {}
+
+ return res;
+}
+
+function getComponentId(inputFormat) {
+ var component = 'mustang'; // default component id
+
+ if (inputFormat && inputFormat !== 'inbanner') {
+ // format identifiers are equals to their component ids.
+ component = inputFormat;
+ }
+
+ return component;
+}
+
+function getAPIName(componentId) {
+ componentId = componentId || '';
+
+ // remove dash in componentId to get API name
+ return componentId.replace('-', '');
+}
+
+function formatAdHTML(bid, size) {
+ var integrationType = bid.params.format;
+
+ var divHtml = '
';
+
+ var script = '';
+ var libUrl = '';
+ if (integrationType && integrationType !== 'inbanner') {
+ libUrl = PRIMETIME_URL + getComponentId(bid.params.format) + '.min.js';
+ script = getOutstreamScript(bid, size);
+ } else {
+ libUrl = MUSTANG_URL;
+ script = getInBannerScript(bid, size);
+ }
+
+ return divHtml +
+ '';
+}
+
+var getInBannerScript = function(bid, size) {
+ return 'var config = {' +
+ ' preloadedVast:vast,' +
+ ' autoPlay:true' +
+ ' };' +
+ ' var ad = new window.com.stickyadstv.vpaid.Ad(document.getElementById("freewheelssp_prebid_target"),config);' +
+ ' (new window.com.stickyadstv.tools.ASLoader(' + bid.params.zoneId + ', \'' + getComponentId(bid.params.format) + '\')).registerEvents(ad);' +
+ ' ad.initAd(' + size[0] + ',' + size[1] + ',"",0,"","");';
+};
+
+var getOutstreamScript = function(bid) {
+ var config = bid.params;
+
+ // default placement if no placement is set
+ if (!config.hasOwnProperty('domId') && !config.hasOwnProperty('auto') && !config.hasOwnProperty('p') && !config.hasOwnProperty('article')) {
+ if (config.format === 'intext-roll') {
+ config.iframeMode = 'dfp';
+ } else {
+ config.domId = 'freewheelssp_prebid_target';
+ }
+ }
+
+ var script = 'var config = {' +
+ ' preloadedVast:vast,' +
+ ' ASLoader:new window.com.stickyadstv.tools.ASLoader(' + bid.params.zoneId + ', \'' + getComponentId(bid.params.format) + '\')';
+
+ for (var key in config) {
+ // dont' send format parameter
+ // neither zone nor vastUrlParams value as Vast is already loaded
+ if (config.hasOwnProperty(key) && key !== 'format' && key !== 'zone' && key !== 'zoneId' && key !== 'vastUrlParams') {
+ script += ',' + key + ':"' + config[key] + '"';
+ }
+ }
+ script += '};' +
+
+ 'window.com.stickyadstv.' + getAPIName(bid.params.format) + '.start(config);';
+
+ return script;
+};
+
+export const spec = {
+ code: BIDDER_CODE,
+ supportedMediaTypes: ['banner', 'video'],
+ aliases: ['stickyadstv'], // former name for freewheel-ssp
+ /**
+ * Determines whether or not the given bid request is valid.
+ *
+ * @param {object} bid The bid to validate.
+ * @return boolean True if this is a valid bid, and false otherwise.
+ */
+ isBidRequestValid: function(bid) {
+ return !!(bid.params.zoneId);
+ },
+
+ /**
+ * Make a server request from the list of BidRequests.
+ *
+ * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server.
+ * @return ServerRequest Info describing the request to the server.
+ */
+ buildRequests: function(bidRequests, bidderRequest) {
+ // var currency = config.getConfig(currency);
+
+ var currentBidRequest = bidRequests[0];
+ if (bidRequests.length > 1) {
+ utils.logMessage('Prebid.JS - freewheel bid adapter: only one ad unit is required.');
+ }
+
+ var zone = currentBidRequest.params.zoneId;
+ var timeInMillis = new Date().getTime();
+ var keyCode = hashcode(zone + '' + timeInMillis);
+ var requestParams = {
+ reqType: 'AdsSetup',
+ protocolVersion: '2.0',
+ zoneId: zone,
+ componentId: getComponentId(currentBidRequest.params.format),
+ timestamp: timeInMillis,
+ pKey: keyCode
+ };
+
+ // Add GDPR flag and consent string
+ if (bidderRequest && bidderRequest.gdprConsent) {
+ requestParams._fw_gdpr_consent = bidderRequest.gdprConsent.consentString;
+
+ if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') {
+ requestParams._fw_gdpr = bidderRequest.gdprConsent.gdprApplies;
+ }
+ }
+
+ if (currentBidRequest.params.gdpr_consented_providers) {
+ requestParams._fw_gdpr_consented_providers = currentBidRequest.params.gdpr_consented_providers;
+ }
+
+ var vastParams = currentBidRequest.params.vastUrlParams;
+ if (typeof vastParams === 'object') {
+ for (var key in vastParams) {
+ if (vastParams.hasOwnProperty(key)) {
+ requestParams[key] = vastParams[key];
+ }
+ }
+ }
+
+ var location = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.referer : getTopMostWindow().location.href;
+ if (isValidUrl(location)) {
+ requestParams.loc = location;
+ }
+
+ var playerSize = getBiggerSizeWithLimit(currentBidRequest.mediaTypes.banner.sizes, currentBidRequest.mediaTypes.banner.minSizeLimit, currentBidRequest.mediaTypes.banner.maxSizeLimit);
+
+ if (playerSize[0] > 0 || playerSize[1] > 0) {
+ requestParams.playerSize = playerSize[0] + 'x' + playerSize[1];
+ }
+
+ return {
+ method: 'GET',
+ url: FREEWHEEL_ADSSETUP,
+ data: requestParams,
+ bidRequest: currentBidRequest
+ };
+ },
+
+ /**
+ * Unpack the response from the server into a list of bids.
+ *
+ * @param {*} serverResponse A successful response from the server.
+ * @param {object} request: the built request object containing the initial bidRequest.
+ * @return {Bid[]} An array of bids which were nested inside the server.
+ */
+ interpretResponse: function(serverResponse, request) {
+ var bidrequest = request.bidRequest;
+ var playerSize = getBiggerSizeWithLimit(bidrequest.mediaTypes.banner.sizes, bidrequest.mediaTypes.banner.minSizeLimit, bidrequest.mediaTypes.banner.maxSizeLimit);
+
+ if (typeof serverResponse == 'object' && typeof serverResponse.body == 'string') {
+ serverResponse = serverResponse.body;
+ }
+
+ var xmlDoc;
+ try {
+ var parser = new DOMParser();
+ xmlDoc = parser.parseFromString(serverResponse, 'application/xml');
+ } catch (err) {
+ utils.logWarn('Prebid.js - ' + BIDDER_CODE + ' : ' + err);
+ return;
+ }
+
+ const princingData = getPricing(xmlDoc);
+ const creativeId = getCreativeId(xmlDoc);
+
+ const topWin = getTopMostWindow();
+ if (!topWin.freewheelssp_cache) {
+ topWin.freewheelssp_cache = {};
+ }
+ topWin.freewheelssp_cache[bidrequest.adUnitCode] = serverResponse;
+
+ const bidResponses = [];
+
+ if (princingData.price) {
+ const bidResponse = {
+ requestId: bidrequest.bidId,
+ cpm: princingData.price,
+ width: playerSize[0],
+ height: playerSize[1],
+ creativeId: creativeId,
+ currency: princingData.currency,
+ netRevenue: true,
+ ttl: 360
+ };
+
+ var mediaTypes = bidrequest.mediaTypes || {};
+ if (mediaTypes.video) {
+ // bidResponse.vastXml = serverResponse;
+ bidResponse.mediaType = 'video';
+
+ var blob = new Blob([serverResponse], {type: 'application/xml'});
+ bidResponse.vastUrl = window.URL.createObjectURL(blob);
+ } else {
+ bidResponse.ad = formatAdHTML(bidrequest, playerSize);
+ }
+
+ bidResponses.push(bidResponse);
+ }
+
+ return bidResponses;
+ },
+
+ getUserSyncs: function(syncOptions) {
+ if (syncOptions && syncOptions.pixelEnabled) {
+ return [{
+ type: 'image',
+ url: USER_SYNC_URL
+ }];
+ } else {
+ return [];
+ }
+ },
+
+}
+registerBidder(spec);
diff --git a/modules/freewheel-sspBidAdapter.md b/modules/freewheel-sspBidAdapter.md
index 70ab2415279..0086aac6567 100644
--- a/modules/freewheel-sspBidAdapter.md
+++ b/modules/freewheel-sspBidAdapter.md
@@ -13,15 +13,21 @@ Module that connects to Freewheel ssp's demand sources
var adUnits = [
{
code: 'test-div',
- sizes: [[300, 250]], // a display size
+
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]], // a display size
+ }
+ },
+
bids: [
{
bidder: "freewheel-ssp",
params: {
- zoneId : '41852'
+ zoneId : '277225'
}
}
]
}
];
-```
\ No newline at end of file
+```
diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js
new file mode 100644
index 00000000000..ba085978ec6
--- /dev/null
+++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js
@@ -0,0 +1,228 @@
+import { expect } from 'chai';
+import { spec } from 'modules/freewheel-sspBidAdapter';
+import { newBidder } from 'src/adapters/bidderFactory';
+
+const ENDPOINT = '//ads.stickyadstv.com/www/delivery/swfIndex.php';
+
+describe('freewheelSSP BidAdapter Test', () => {
+ const adapter = newBidder(spec);
+
+ describe('inherited functions', () => {
+ it('exists and is a function', () => {
+ expect(adapter.callBids).to.exist.and.to.be.a('function');
+ });
+ });
+
+ describe('isBidRequestValid', () => {
+ let bid = {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'mediaTypes': {
+ 'banner': {
+ 'sizes': [
+ [300, 250], [300, 600]
+ ]
+ }
+ },
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ };
+
+ it('should return true when required params found', () => {
+ expect(spec.isBidRequestValid(bid)).to.equal(true);
+ });
+
+ it('should return false when required params are not passed', () => {
+ let bid = Object.assign({}, bid);
+ delete bid.params;
+ bid.params = {
+ wrong: 'missing zone id'
+ };
+ expect(spec.isBidRequestValid(bid)).to.equal(false);
+ });
+ });
+
+ describe('buildRequests', () => {
+ let bidRequests = [
+ {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'mediaTypes': {
+ 'banner': {
+ 'sizes': [
+ [300, 250], [300, 600]
+ ]
+ }
+ },
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ }
+ ];
+
+ it('should add parameters to the tag', () => {
+ const request = spec.buildRequests(bidRequests);
+ console.log(request.data);
+
+ const payload = request.data;
+ expect(payload.reqType).to.equal('AdsSetup');
+ expect(payload.protocolVersion).to.equal('2.0');
+ expect(payload.zoneId).to.equal('277225');
+ expect(payload.componentId).to.equal('mustang');
+ expect(payload.playerSize).to.equal('300x600');
+ });
+
+ it('sends bid request to ENDPOINT via GET', () => {
+ const request = spec.buildRequests(bidRequests);
+ expect(request.url).to.contain(ENDPOINT);
+ expect(request.method).to.equal('GET');
+ });
+ })
+
+ describe('interpretResponse', () => {
+ let bidRequests = [
+ {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'mediaTypes': {
+ 'banner': {
+ 'sizes': [
+ [300, 250], [300, 600]
+ ]
+ }
+ },
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ }
+ ];
+
+ let formattedBidRequests = [
+ {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225',
+ 'format': 'floorad'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'mediaTypes': {
+ 'banner': {
+ 'sizes': [
+ [300, 250], [300, 600]
+ ]
+ }
+ },
+ 'sizes': [[600, 250], [300, 600]],
+ 'bidId': '30b3other1c1838de1e',
+ 'bidderRequestId': '22edbae273other3bf6',
+ 'auctionId': '1d1a03079test0a475',
+ },
+ {
+ 'bidder': 'stickyadstv',
+ 'params': {
+ 'zoneId': '277225',
+ 'format': 'test'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'mediaTypes': {
+ 'banner': {
+ 'sizes': [
+ [300, 600]
+ ]
+ }
+ },
+ 'sizes': [[300, 600]],
+ 'bidId': '2',
+ 'bidderRequestId': '3',
+ 'auctionId': '4',
+ }
+ ];
+
+ let response = '' +
+ '' +
+ ' ' +
+ ' Adswizz' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' 00:00:09' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' 0.2000' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '';
+
+ let ad = '';
+ let formattedAd = '';
+
+ it('should get correct bid response', () => {
+ var request = spec.buildRequests(bidRequests);
+
+ let expectedResponse = [
+ {
+ requestId: '30b31c1838de1e',
+ cpm: '0.2000',
+ width: 300,
+ height: 600,
+ creativeId: '28517153',
+ currency: 'EUR',
+ netRevenue: true,
+ ttl: 360,
+ ad: ad
+ }
+ ];
+
+ let result = spec.interpretResponse(response, request);
+ expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0]));
+ });
+
+ it('should get correct bid response with formated ad', () => {
+ var request = spec.buildRequests(formattedBidRequests);
+
+ let expectedResponse = [
+ {
+ requestId: '30b31c1838de1e',
+ cpm: '0.2000',
+ width: 300,
+ height: 600,
+ creativeId: '28517153',
+ currency: 'EUR',
+ netRevenue: true,
+ ttl: 360,
+ ad: formattedAd
+ }
+ ];
+
+ let result = spec.interpretResponse(response, request);
+ expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0]));
+ });
+
+ it('handles nobid responses', () => {
+ var reqest = spec.buildRequests(formattedBidRequests);
+ let response = '';
+
+ let result = spec.interpretResponse(response, reqest);
+ expect(result.length).to.equal(0);
+ });
+ });
+});