diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js
new file mode 100644
index 00000000000..90eb7e7ed9f
--- /dev/null
+++ b/modules/ccxBidAdapter.js
@@ -0,0 +1,232 @@
+import * as utils from '../src/utils'
+import { registerBidder } from '../src/adapters/bidderFactory'
+import { config } from '../src/config'
+const BIDDER_CODE = 'ccx'
+const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid'
+const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6]
+const SUPPORTED_VIDEO_MIMES = ['video/mp4', 'video/x-flv']
+const SUPPORTED_VIDEO_PLAYBACK_METHODS = [1, 2, 3, 4]
+
+function _getDeviceObj () {
+ let device = {}
+ device.w = screen.width
+ device.y = screen.height
+ device.ua = navigator.userAgent
+ return device
+}
+
+function _getSiteObj (bidderRequest) {
+ let site = {}
+ let url = config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href');
+ if (url.length > 0) {
+ url = url.split('?')[0]
+ }
+ site.page = url
+
+ return site
+}
+
+function _validateSizes (sizeObj, type) {
+ if (!utils.isArray(sizeObj) || typeof sizeObj[0] === 'undefined') {
+ return false
+ }
+
+ if (type === 'video' && (!utils.isArray(sizeObj[0]) || sizeObj[0].length !== 2)) {
+ return false
+ }
+
+ let result = true
+
+ if (type === 'banner') {
+ utils._each(sizeObj, function (size) {
+ if (!utils.isArray(size) || (size.length !== 2)) {
+ result = false
+ }
+ })
+ return result
+ }
+
+ if (type === 'old') {
+ if (!utils.isArray(sizeObj[0]) && sizeObj.length !== 2) {
+ result = false
+ } else if (utils.isArray(sizeObj[0])) {
+ utils._each(sizeObj, function (size) {
+ if (!utils.isArray(size) || (size.length !== 2)) {
+ result = false
+ }
+ })
+ }
+ return result;
+ }
+
+ return true
+}
+
+function _buildBid (bid) {
+ let placement = {}
+ placement.id = bid.bidId
+ placement.secure = 1
+
+ let sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes') || utils.deepAccess(bid, 'mediaTypes.video.playerSize') || utils.deepAccess(bid, 'sizes')
+
+ if (utils.deepAccess(bid, 'mediaTypes.banner') || utils.deepAccess(bid, 'mediaType') === 'banner' || (!utils.deepAccess(bid, 'mediaTypes.video') && !utils.deepAccess(bid, 'mediaType'))) {
+ placement.banner = {'format': []}
+ if (utils.isArray(sizes[0])) {
+ utils._each(sizes, function (size) {
+ placement.banner.format.push({'w': size[0], 'h': size[1]})
+ })
+ } else {
+ placement.banner.format.push({'w': sizes[0], 'h': sizes[1]})
+ }
+ } else if (utils.deepAccess(bid, 'mediaTypes.video') || utils.deepAccess(bid, 'mediaType') === 'video') {
+ placement.video = {}
+
+ if (typeof sizes !== 'undefined') {
+ if (utils.isArray(sizes[0])) {
+ placement.video.w = sizes[0][0]
+ placement.video.h = sizes[0][1]
+ } else {
+ placement.video.w = sizes[0]
+ placement.video.h = sizes[1]
+ }
+ }
+
+ placement.video.protocols = utils.deepAccess(bid, 'params.video.protocols') || SUPPORTED_VIDEO_PROTOCOLS
+ placement.video.mimes = utils.deepAccess(bid, 'params.video.mimes') || SUPPORTED_VIDEO_MIMES
+ placement.video.playbackmethod = utils.deepAccess(bid, 'params.video.playbackmethod') || SUPPORTED_VIDEO_PLAYBACK_METHODS
+ placement.video.skip = utils.deepAccess(bid, 'params.video.skip') || 0
+ if (placement.video.skip === 1 && utils.deepAccess(bid, 'params.video.skipafter')) {
+ placement.video.skipafter = utils.deepAccess(bid, 'params.video.skipafter')
+ }
+ }
+
+ placement.ext = {'pid': bid.params.placementId}
+
+ return placement
+}
+
+function _buildResponse (bid, currency, ttl) {
+ let resp = {
+ requestId: bid.impid,
+ cpm: bid.price,
+ width: bid.w,
+ height: bid.h,
+ creativeId: bid.crid,
+ netRevenue: false,
+ ttl: ttl,
+ currency: currency
+ }
+
+ if (bid.ext.type === 'video') {
+ resp.vastXml = bid.adm
+ } else {
+ resp.ad = bid.adm
+ }
+
+ if (utils.deepAccess(bid, 'dealid')) {
+ resp.dealId = bid.dealid
+ }
+
+ return resp
+}
+
+export const spec = {
+ code: BIDDER_CODE,
+ supportedMediaTypes: ['banner', 'video'],
+
+ isBidRequestValid: function (bid) {
+ if (!utils.deepAccess(bid, 'params.placementId')) {
+ utils.logWarn('placementId param is reqeuired.')
+ return false
+ }
+ if (utils.deepAccess(bid, 'mediaTypes.banner.sizes')) {
+ let isValid = _validateSizes(bid.mediaTypes.banner.sizes, 'banner')
+ if (!isValid) {
+ utils.logWarn('Bid sizes are invalid.')
+ }
+ return isValid
+ } else if (utils.deepAccess(bid, 'mediaTypes.video.playerSize')) {
+ let isValid = _validateSizes(bid.mediaTypes.video.playerSize, 'video')
+ if (!isValid) {
+ utils.logWarn('Bid sizes are invalid.')
+ }
+ return isValid
+ } else if (utils.deepAccess(bid, 'sizes')) {
+ let isValid = _validateSizes(bid.sizes, 'old')
+ if (!isValid) {
+ utils.logWarn('Bid sizes are invalid.')
+ }
+ return isValid
+ } else {
+ utils.logWarn('Bid sizes are required.')
+ return false
+ }
+ },
+ buildRequests: function (validBidRequests, bidderRequest) {
+ // check if validBidRequests is not empty
+ if (validBidRequests.length > 0) {
+ let requestBody = {}
+ requestBody.imp = []
+ requestBody.site = _getSiteObj(bidderRequest)
+ requestBody.device = _getDeviceObj()
+ requestBody.id = bidderRequest.bids[0].auctionId
+ requestBody.ext = {'ce': (utils.cookiesAreEnabled() ? 1 : 0)}
+
+ // Attaching GDPR Consent Params
+ if (bidderRequest && bidderRequest.gdprConsent) {
+ requestBody.user = {
+ ext: {
+ consent: bidderRequest.gdprConsent.consentString
+ }
+ };
+
+ requestBody.regs = {
+ ext: {
+ gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)
+ }
+ };
+ }
+
+ utils._each(validBidRequests, function (bid) {
+ requestBody.imp.push(_buildBid(bid))
+ })
+ // Return the server request
+ return {
+ 'method': 'POST',
+ 'url': BID_URL,
+ 'data': JSON.stringify(requestBody)
+ }
+ }
+ },
+ interpretResponse: function (serverResponse, request) {
+ const bidResponses = []
+
+ // response is not empty (HTTP 204)
+ if (!utils.isEmpty(serverResponse.body)) {
+ utils._each(serverResponse.body.seatbid, function (seatbid) {
+ utils._each(seatbid.bid, function (bid) {
+ bidResponses.push(_buildResponse(bid, serverResponse.body.cur, serverResponse.body.ext.ttl))
+ })
+ })
+ }
+
+ return bidResponses
+ },
+ getUserSyncs: function (syncOptions, serverResponses) {
+ const syncs = []
+
+ if (utils.deepAccess(serverResponses[0], 'body.ext.usersync') && !utils.isEmpty(serverResponses[0].body.ext.usersync)) {
+ utils._each(serverResponses[0].body.ext.usersync, function (match) {
+ if ((syncOptions.iframeEnabled && match.type === 'iframe') || (syncOptions.pixelEnabled && match.type === 'image')) {
+ syncs.push({
+ type: match.type,
+ url: match.url
+ })
+ }
+ })
+ }
+
+ return syncs
+ }
+}
+registerBidder(spec)
diff --git a/test/spec/modules/ccxBidAdapter_spec.js b/test/spec/modules/ccxBidAdapter_spec.js
new file mode 100644
index 00000000000..01e52c3559a
--- /dev/null
+++ b/test/spec/modules/ccxBidAdapter_spec.js
@@ -0,0 +1,428 @@
+import { expect } from 'chai';
+import { spec } from 'modules/ccxBidAdapter';
+import * as utils from 'src/utils';
+
+describe('ccxAdapter', function () {
+ let bids = [
+ {
+ adUnitCode: 'banner',
+ auctionId: '0b9de793-8eda-481e-a548-c187d58b28d9',
+ bidId: '2e56e1af51a5d7',
+ bidder: 'ccx',
+ bidderRequestId: '17e7b9f58a607e',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]]
+ }
+ },
+ params: {
+ placementId: 607
+ },
+ sizes: [[300, 250]],
+ transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9'
+ },
+ {
+ adUnitCode: 'video',
+ auctionId: '0b9de793-8eda-481e-a548-c187d58b28d9',
+ bidId: '3u94t90ut39tt3t',
+ bidder: 'ccx',
+ bidderRequestId: '23ur20r239r2r',
+ mediaTypes: {
+ video: {
+ playerSize: [[640, 480]]
+ }
+ },
+ params: {
+ placementId: 608
+ },
+ sizes: [[640, 480]],
+ transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9'
+ }
+ ];
+ describe('isBidRequestValid', function () {
+ it('Valid bid requests', function () {
+ expect(spec.isBidRequestValid(bids[0])).to.be.true;
+ expect(spec.isBidRequestValid(bids[1])).to.be.true;
+ });
+ it('Invalid bid reqeusts - no placementId', function () {
+ let bidsClone = utils.deepClone(bids);
+ bidsClone[0].params = undefined;
+ expect(spec.isBidRequestValid(bidsClone[0])).to.be.false;
+ });
+ it('Invalid bid reqeusts - invalid banner sizes', function () {
+ let bidsClone = utils.deepClone(bids);
+ bidsClone[0].mediaTypes.banner.sizes = [300, 250];
+ expect(spec.isBidRequestValid(bidsClone[0])).to.be.false;
+ bidsClone[0].mediaTypes.banner.sizes = [[300, 250], [750]];
+ expect(spec.isBidRequestValid(bidsClone[0])).to.be.false;
+ bidsClone[0].mediaTypes.banner.sizes = [];
+ expect(spec.isBidRequestValid(bidsClone[0])).to.be.false;
+ });
+ it('Invalid bid reqeusts - invalid video sizes', function () {
+ let bidsClone = utils.deepClone(bids);
+ bidsClone[1].mediaTypes.video.playerSize = [];
+ expect(spec.isBidRequestValid(bidsClone[1])).to.be.false;
+ bidsClone[1].mediaTypes.video.sizes = [640, 480];
+ expect(spec.isBidRequestValid(bidsClone[1])).to.be.false;
+ });
+ it('Valid bid reqeust - old style sizes', function () {
+ let bidsClone = utils.deepClone(bids);
+ delete (bidsClone[0].mediaTypes);
+ delete (bidsClone[1].mediaTypes);
+ expect(spec.isBidRequestValid(bidsClone[0])).to.be.true;
+ expect(spec.isBidRequestValid(bidsClone[1])).to.be.true;
+ bidsClone[0].sizes = [300, 250];
+ expect(spec.isBidRequestValid(bidsClone[0])).to.be.true;
+ });
+ });
+ describe('buildRequests', function () {
+ it('No valid bids', function () {
+ expect(spec.buildRequests([])).to.be.undefined;
+ });
+
+ it('Valid bid request - default', function () {
+ let response = spec.buildRequests(bids, {bids});
+ expect(response).to.be.not.empty;
+ expect(response.data).to.be.not.empty;
+
+ let data = JSON.parse(response.data);
+
+ expect(data).to.be.an('object');
+ expect(data).to.have.keys('site', 'imp', 'id', 'ext', 'device');
+
+ let imps = [
+ {
+ banner: {
+ format: [
+ {
+ w: 300,
+ h: 250
+ }
+ ]
+ },
+ ext: {
+ pid: 607
+ },
+ id: '2e56e1af51a5d7',
+ secure: 1
+ },
+ {
+ video: {
+ w: 640,
+ h: 480,
+ protocols: [2, 3, 5, 6],
+ mimes: ['video/mp4', 'video/x-flv'],
+ playbackmethod: [1, 2, 3, 4],
+ skip: 0
+ },
+ id: '3u94t90ut39tt3t',
+ secure: 1,
+ ext: {
+ pid: 608
+ }
+ }
+ ];
+ expect(data.imp).to.deep.have.same.members(imps);
+ });
+
+ it('Valid bid request - custom', function () {
+ let bidsClone = utils.deepClone(bids);
+ let imps = [
+ {
+ banner: {
+ format: [
+ {
+ w: 300,
+ h: 250
+ }
+ ]
+ },
+ ext: {
+ pid: 607
+ },
+ id: '2e56e1af51a5d7',
+ secure: 1
+ },
+ {
+ video: {
+ w: 640,
+ h: 480,
+ protocols: [5, 6],
+ mimes: ['video/mp4'],
+ playbackmethod: [3],
+ skip: 1,
+ skipafter: 5
+ },
+ id: '3u94t90ut39tt3t',
+ secure: 1,
+ ext: {
+ pid: 608
+ }
+ }
+ ];
+
+ bidsClone[1].params.video = {};
+ bidsClone[1].params.video.protocols = [5, 6];
+ bidsClone[1].params.video.mimes = ['video/mp4'];
+ bidsClone[1].params.video.playbackmethod = [3];
+ bidsClone[1].params.video.skip = 1;
+ bidsClone[1].params.video.skipafter = 5;
+
+ let response = spec.buildRequests(bidsClone, {'bids': bidsClone});
+ let data = JSON.parse(response.data);
+
+ expect(data.imp).to.deep.have.same.members(imps);
+ });
+ it('Valid bid request - sizes old style', function () {
+ let bidsClone = utils.deepClone(bids);
+ delete (bidsClone[0].mediaTypes);
+ delete (bidsClone[1].mediaTypes);
+ bidsClone[0].mediaType = 'banner';
+ bidsClone[1].mediaType = 'video';
+
+ let imps = [
+ {
+ banner: {
+ format: [
+ {
+ w: 300,
+ h: 250
+ }
+ ]
+ },
+ ext: {
+ pid: 607
+ },
+ id: '2e56e1af51a5d7',
+ secure: 1
+ },
+ {
+ video: {
+ w: 640,
+ h: 480,
+ protocols: [2, 3, 5, 6],
+ mimes: ['video/mp4', 'video/x-flv'],
+ playbackmethod: [1, 2, 3, 4],
+ skip: 0
+ },
+ id: '3u94t90ut39tt3t',
+ secure: 1,
+ ext: {
+ pid: 608
+ }
+ }
+ ];
+
+ let response = spec.buildRequests(bidsClone, {'bids': bidsClone});
+ let data = JSON.parse(response.data);
+
+ expect(data.imp).to.deep.have.same.members(imps);
+ });
+ it('Valid bid request - sizes old style - no media type', function () {
+ let bidsClone = utils.deepClone(bids);
+ delete (bidsClone[0].mediaTypes);
+ delete (bidsClone[1]);
+
+ let imps = [
+ {
+ banner: {
+ format: [
+ {
+ w: 300,
+ h: 250
+ }
+ ]
+ },
+ ext: {
+ pid: 607
+ },
+ id: '2e56e1af51a5d7',
+ secure: 1
+ }
+ ];
+
+ let response = spec.buildRequests(bidsClone, {'bids': bidsClone});
+ let data = JSON.parse(response.data);
+
+ expect(data.imp).to.deep.have.same.members(imps);
+ });
+ });
+
+ describe('GDPR conformity', function () {
+ it('should transmit correct data', function () {
+ let bidsClone = utils.deepClone(bids);
+ let gdprConsent = {
+ consentString: 'awefasdfwefasdfasd',
+ gdprApplies: true
+ };
+ let response = spec.buildRequests(bidsClone, {'bids': bidsClone, 'gdprConsent': gdprConsent});
+ let data = JSON.parse(response.data);
+
+ expect(data.regs.ext.gdpr).to.equal(1);
+ expect(data.user.ext.consent).to.equal('awefasdfwefasdfasd');
+ });
+ });
+
+ describe('GDPR absence conformity', function () {
+ it('should transmit correct data', function () {
+ let response = spec.buildRequests(bids, {bids});
+ let data = JSON.parse(response.data);
+
+ expect(data.regs).to.be.undefined;
+ expect(data.user).to.be.undefined;
+ });
+ });
+
+ let response = {
+ id: '0b9de793-8eda-481e-a548-c187d58b28d9',
+ seatbid: [
+ {
+ bid: [
+ {
+ id: '2e56e1af51a5d7_221',
+ impid: '2e56e1af51a5d7',
+ price: 8.1,
+ adid: '221',
+ adm: '',
+ adomain: ['clickonometrics.com'],
+ crid: '221',
+ w: 300,
+ h: 250,
+ ext: {
+ type: 'standard'
+ }
+ },
+ {
+ id: '2e56e1af51a5d8_222',
+ impid: '2e56e1af51a5d8',
+ price: 5.68,
+ adid: '222',
+ adm: '',
+ adomain: ['clickonometrics.com'],
+ crid: '222',
+ w: 640,
+ h: 480,
+ ext: {
+ type: 'video'
+ }
+ }
+ ]
+ }
+ ],
+ cur: 'PLN',
+ ext: {
+ ttl: 5,
+ usersync: [
+ {
+ type: 'image',
+ url: 'http://foo.sync?param=1'
+ },
+ {
+ type: 'iframe',
+ url: 'http://foo.sync?param=2'
+ }
+ ]
+ }
+ };
+
+ describe('interpretResponse', function () {
+ it('Valid bid response - multi', function () {
+ let bidResponses = [
+ {
+ requestId: '2e56e1af51a5d7',
+ cpm: 8.1,
+ width: 300,
+ height: 250,
+ creativeId: '221',
+ netRevenue: false,
+ ttl: 5,
+ currency: 'PLN',
+ ad: ''
+ },
+ {
+ requestId: '2e56e1af51a5d8',
+ cpm: 5.68,
+ width: 640,
+ height: 480,
+ creativeId: '222',
+ netRevenue: false,
+ ttl: 5,
+ currency: 'PLN',
+ vastXml: ''
+ }
+ ];
+ expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses);
+ });
+
+ it('Valid bid response - single', function () {
+ delete response.seatbid[0].bid[1];
+ let bidResponses = [
+ {
+ requestId: '2e56e1af51a5d7',
+ cpm: 8.1,
+ width: 300,
+ height: 250,
+ creativeId: '221',
+ netRevenue: false,
+ ttl: 5,
+ currency: 'PLN',
+ ad: ''
+ }
+ ];
+ expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses);
+ });
+
+ it('Empty bid response', function () {
+ expect(spec.interpretResponse({})).to.be.empty;
+ });
+ });
+ describe('getUserSyncs', function () {
+ it('Valid syncs - all', function () {
+ let syncOptions = {
+ iframeEnabled: true,
+ pixelEnabled: true
+ };
+
+ let expectedSyncs = [
+ {
+ type: 'image',
+ url: 'http://foo.sync?param=1'
+ },
+ {
+ type: 'iframe',
+ url: 'http://foo.sync?param=2'
+ }
+ ];
+ expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs);
+ });
+
+ it('Valid syncs - only image', function () {
+ let syncOptions = {
+ iframeEnabled: false,
+ pixelEnabled: true
+ };
+ let expectedSyncs = [
+ {
+ type: 'image', url: 'http://foo.sync?param=1'
+ }
+ ];
+ expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs);
+ });
+
+ it('Valid syncs - only iframe', function () {
+ let syncOptions = {iframeEnabled: true, pixelEnabled: false};
+ let expectedSyncs = [
+ {
+ type: 'iframe', url: 'http://foo.sync?param=2'
+ }
+ ];
+ expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs);
+ });
+
+ it('Valid syncs - empty', function () {
+ let syncOptions = {iframeEnabled: true, pixelEnabled: true};
+ response.ext.usersync = {};
+ expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.be.empty;
+ });
+ });
+});