diff --git a/modules/h12mediaBidAdapter.js b/modules/h12mediaBidAdapter.js
new file mode 100644
index 00000000000..0d2c22a3f68
--- /dev/null
+++ b/modules/h12mediaBidAdapter.js
@@ -0,0 +1,209 @@
+import * as utils from '../src/utils.js';
+import { registerBidder } from '../src/adapters/bidderFactory.js';
+import find from 'core-js-pure/features/array/find.js';
+const BIDDER_CODE = 'h12media';
+const DEFAULT_URL = 'https://bidder.h12-media.com/prebid/';
+const DEFAULT_CURRENCY = 'USD';
+const DEFAULT_TTL = 360;
+const DEFAULT_NET_REVENUE = false;
+
+export const spec = {
+ code: BIDDER_CODE,
+ aliases: ['h12'],
+
+ isBidRequestValid: function(bid) {
+ return !!(bid.params && bid.params.pubid);
+ },
+
+ buildRequests: function(validBidRequests, bidderRequest) {
+ const requestUrl = validBidRequests[0].params.endpointdom || DEFAULT_URL;
+ const isiframe = !((window.self === window.top) || window.frameElement);
+ const screenSize = getClientDimensions();
+ const docSize = getDocumentDimensions();
+
+ const bidrequests = validBidRequests.map((bidRequest) => {
+ const bidderParams = bidRequest.params;
+ const adUnitElement = document.getElementById(bidRequest.adUnitCode);
+ const ishidden = !isVisible(adUnitElement);
+ const coords = {
+ x: adUnitElement && adUnitElement.getBoundingClientRect().x,
+ y: adUnitElement && adUnitElement.getBoundingClientRect().y,
+ };
+
+ return {
+ bidId: bidRequest.bidId,
+ transactionId: bidRequest.transactionId,
+ adunitId: bidRequest.adUnitCode,
+ pubid: bidderParams.pubid,
+ placementid: bidderParams.placementid || '',
+ size: bidderParams.size || '',
+ adunitSize: bidRequest.mediaTypes.banner.sizes || [],
+ coords,
+ ishidden,
+ };
+ });
+
+ return {
+ method: 'POST',
+ url: requestUrl,
+ options: {withCredentials: false},
+ data: {
+ gdpr: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? Boolean(bidderRequest.gdprConsent.gdprApplies & 1) : false,
+ gdpr_cs: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? bidderRequest.gdprConsent.consentString : '',
+ topLevelUrl: window.top.location.href,
+ refererUrl: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer : '',
+ isiframe,
+ version: '$prebid.version$',
+ visitorInfo: {
+ localTime: getLocalDateFormatted(),
+ dayOfWeek: new Date().getDay(),
+ screenWidth: screenSize[0],
+ screenHeight: screenSize[1],
+ docWidth: docSize[0],
+ docHeight: docSize[1],
+ scrollbarx: window.scrollX,
+ scrollbary: window.scrollY,
+ },
+ bidrequests,
+ },
+ };
+ },
+
+ interpretResponse: function(serverResponse, bidRequests) {
+ let bidResponses = [];
+ try {
+ const serverBody = serverResponse.body;
+ if (serverBody) {
+ if (serverBody.bids) {
+ serverBody.bids.forEach(bidBody => {
+ const bidRequest = find(bidRequests.data.bidrequests, bid => bid.bidId === bidBody.bidId);
+ const bidResponse = {
+ currency: serverBody.currency || DEFAULT_CURRENCY,
+ netRevenue: serverBody.netRevenue || DEFAULT_NET_REVENUE,
+ ttl: serverBody.ttl || DEFAULT_TTL,
+ requestId: bidBody.bidId,
+ cpm: bidBody.cpm,
+ width: bidBody.width,
+ height: bidBody.height,
+ creativeId: bidBody.creativeId,
+ ad: bidBody.ad,
+ meta: bidBody.meta,
+ mediaType: 'banner',
+ };
+ if (bidRequest) {
+ bidResponse.pubid = bidRequest.pubid;
+ bidResponse.placementid = bidRequest.placementid;
+ bidResponse.size = bidRequest.size;
+ }
+ bidResponses.push(bidResponse);
+ });
+ }
+ }
+ return bidResponses;
+ } catch (err) {
+ utils.logError(err);
+ }
+ },
+
+ getUserSyncs: function(syncOptions, serverResponses, gdprConsent) {
+ const serverBody = serverResponses[0].body;
+ const syncs = [];
+ gdprConsent = gdprConsent || {
+ gdprApplies: false, consentString: '',
+ };
+
+ if (serverBody) {
+ if (serverBody.bids) {
+ serverBody.bids.forEach(bidBody => {
+ const userSyncUrls = bidBody.usersync || [];
+ const userSyncUrlProcess = url => {
+ return url
+ .replace('{gdpr}', gdprConsent.gdprApplies)
+ .replace('{gdpr_cs}', gdprConsent.consentString);
+ }
+
+ userSyncUrls.forEach(sync => {
+ if (syncOptions.iframeEnabled && sync.type === 'iframe' && sync.url) {
+ syncs.push({
+ type: 'iframe',
+ url: userSyncUrlProcess(sync.url),
+ });
+ }
+ if (syncOptions.pixelEnabled && sync.type === 'image' && sync.url) {
+ syncs.push({
+ type: 'image',
+ url: userSyncUrlProcess(sync.url),
+ });
+ }
+ });
+ });
+ }
+ }
+
+ return syncs;
+ },
+}
+
+function getContext(elem) {
+ return elem && window.document.body.contains(elem) ? window : (window.top.document.body.contains(elem) ? top : undefined);
+}
+
+function isDefined(val) {
+ return (val !== null) && (typeof val !== 'undefined');
+}
+
+function getIsHidden(elem) {
+ let lastElem = elem;
+ let elemHidden = false;
+ let m;
+ m = 0;
+
+ do {
+ m = m + 1;
+ try {
+ if (
+ getContext(elem).getComputedStyle(lastElem).getPropertyValue('display') === 'none' ||
+ getContext(elem).getComputedStyle(lastElem).getPropertyValue('visibility') === 'hidden'
+ ) {
+ return true;
+ } else {
+ elemHidden = false;
+ lastElem = lastElem.parentElement;
+ }
+ } catch (o) {
+ return false;
+ }
+ } while ((m < 250) && (lastElem != null) && (elemHidden === false))
+ return elemHidden;
+}
+
+function isVisible(element) {
+ return element && isDefined(getContext(element)) && !getIsHidden(element);
+}
+
+function getClientDimensions() {
+ try {
+ const t = window.top.innerWidth || window.top.document.documentElement.clientWidth || window.top.document.body.clientWidth;
+ const e = window.top.innerHeight || window.top.document.documentElement.clientHeight || window.top.document.body.clientHeight;
+ return [Math.round(t), Math.round(e)];
+ } catch (i) {
+ return [0, 0];
+ }
+}
+
+function getDocumentDimensions() {
+ try {
+ const D = window.top.document;
+ return [D.body.offsetWidth, Math.max(D.body.scrollHeight, D.documentElement.scrollHeight, D.body.offsetHeight, D.documentElement.offsetHeight, D.body.clientHeight, D.documentElement.clientHeight)]
+ } catch (t) {
+ return [-1, -1]
+ }
+}
+
+function getLocalDateFormatted() {
+ const two = num => ('0' + num).slice(-2);
+ const d = new Date();
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())} ${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`;
+}
+
+registerBidder(spec);
diff --git a/modules/h12mediaBidAdapter.md b/modules/h12mediaBidAdapter.md
new file mode 100644
index 00000000000..7430bf11b90
--- /dev/null
+++ b/modules/h12mediaBidAdapter.md
@@ -0,0 +1,48 @@
+# Overview
+
+Module Name: H12 Media Bidder Adapter
+Module Type: Bidder Adapter
+Maintainer: contact@h12-media.com
+
+# Description
+
+Module that connects to H12 Media demand source to fetch bids.
+
+# Test Parameters
+```
+ var adUnits = [
+ {
+ code: 'div-1',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]]
+ }
+ },
+ bids: [
+ {
+ bidder: "h12media",
+ params: {
+ pubid: '123',
+ sizes: '300x250',
+ }
+ }
+ ]
+ },{
+ code: 'div-2',
+ mediaTypes: {
+ banner: {
+ sizes: [[728, 90]]
+ }
+ },
+ bids: [
+ {
+ bidder: "h12media",
+ params: {
+ pubid: '123',
+ placementid: '321'
+ }
+ }
+ ]
+ }
+ ];
+```
diff --git a/test/spec/modules/h12mediaBidAdapter_spec.js b/test/spec/modules/h12mediaBidAdapter_spec.js
new file mode 100644
index 00000000000..08a83ce981f
--- /dev/null
+++ b/test/spec/modules/h12mediaBidAdapter_spec.js
@@ -0,0 +1,388 @@
+import {expect} from 'chai';
+import {spec} from 'modules/h12mediaBidAdapter';
+import {newBidder} from 'src/adapters/bidderFactory';
+
+describe('H12 Media Adapter', function () {
+ const DEFAULT_CURRENCY = 'USD';
+ const DEFAULT_TTL = 360;
+ const DEFAULT_NET_REVENUE = false;
+ const adapter = newBidder('spec');
+
+ const validBid = {
+ adUnitCode: 'div-gpt-ad-1460505748561-0',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300, 600]],
+ }
+ },
+ bidder: 'h12media',
+ bidId: '1c5e8a1a84522d',
+ bidderRequestId: '1d0c4017f02458',
+ auctionId: '9adc85ed-43ee-4a78-816b-52b7e578f313',
+ params: {
+ pubid: 123321,
+ },
+ };
+
+ const validBid2 = {
+ adUnitCode: 'div-gpt-ad-1460505748561-1',
+ mediaTypes: {
+ banner: {}
+ },
+ bidder: 'h12media',
+ bidId: '2c5e8a1a84522d',
+ bidderRequestId: '2d0c4017f02458',
+ auctionId: '9adc85ed-43ee-4a78-816b-52b7e578f314',
+ params: {
+ pubid: 123321,
+ size: '100x200'
+ },
+ };
+
+ const invalidBid = {
+ adUnitCode: 'div-gpt-ad-1460505748561-2',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300, 600]],
+ }
+ },
+ bidder: 'h12media',
+ bidId: '3c5e8a1a84522d',
+ bidderRequestId: '3d0c4017f02458',
+ auctionId: '9adc85ed-43ee-4a78-816b-52b7e578f315',
+ };
+
+ const bidderRequest = {
+ refererInfo: {
+ referer: 'https://localhost'
+ },
+ gdprConsent: {
+ gdprApplies: 1,
+ consentString: 'concentDataString',
+ vendorData: {
+ vendorConsents: {
+ '90': 1
+ },
+ },
+ },
+ uspConsent: 'consentUspString'
+ };
+
+ const serverResponse = {
+ currency: 'EUR',
+ netRevenue: true,
+ ttl: 500,
+ bids: [{
+ bidId: validBid.bidId,
+ cpm: 0.33,
+ width: 300,
+ height: 600,
+ creativeId: '335566',
+ ad: '
my ad
',
+ usersync: [
+ {url: 'https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies={gdpr}&gdpr_consent_string={gdpr_cs}', type: 'image'},
+ {url: 'https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies={gdpr}&gdpr_consent_string={gdpr_cs}', type: 'iframe'}
+ ],
+ meta: {
+ advertiserId: '54321',
+ advertiserName: 'My advertiser',
+ advertiserDomains: ['test.com']
+ }
+ }]
+ };
+
+ const serverResponse2 = {
+ bids: [{
+ bidId: validBid2.bidId,
+ cpm: 0.33,
+ width: 300,
+ height: 600,
+ creativeId: '335566',
+ ad: 'my ad 2
',
+ }]
+ };
+
+ function removeElement(id) {
+ if (document.getElementById(id)) {
+ document.body.removeChild(document.getElementById(id));
+ }
+ }
+
+ function createElement(id) {
+ const div = document.createElement('div');
+ div.id = id;
+ div.style.width = '50px';
+ div.style.height = '50px';
+ if (frameElement) {
+ frameElement.style.width = '100px';
+ frameElement.style.height = '100px';
+ }
+ div.style.background = 'black';
+ document.body.appendChild(div);
+ return div;
+ }
+ function createElementVisible(id) {
+ const element = createElement(id);
+ sandbox.stub(element, 'getBoundingClientRect').returns({
+ x: 10,
+ y: 10,
+ });
+ return element;
+ }
+ function createElementInvisible(id) {
+ const element = document.createElement('div');
+ element.id = id;
+ document.body.appendChild(element);
+ element.style.display = 'none';
+ return element;
+ }
+
+ function createElementHidden(id) {
+ const element = createElement(id);
+ document.body.appendChild(element);
+ element.style.visibility = 'hidden';
+ sandbox.stub(element, 'getBoundingClientRect').returns({
+ x: 100,
+ y: 100,
+ });
+ return element;
+ }
+
+ let sandbox;
+
+ beforeEach(function () {
+ sandbox = sinon.sandbox.create();
+ });
+
+ afterEach(function () {
+ removeElement(validBid.adUnitCode);
+ removeElement(validBid2.adUnitCode);
+ removeElement(invalidBid.adUnitCode);
+ sandbox.restore();
+ });
+
+ after(function() {
+ sandbox.reset();
+ })
+
+ describe('inherited functions', function () {
+ it('exists and is a function', function () {
+ expect(adapter.callBids).to.exist.and.to.be.a('function');
+ });
+ });
+
+ describe('isBidRequestValid', function () {
+ it('should return true when bid is valid', function () {
+ expect(spec.isBidRequestValid(validBid)).to.equal(true);
+ });
+
+ it('should return false when bid does not have pubid parameter', function () {
+ expect(spec.isBidRequestValid(invalidBid)).to.equal(false);
+ });
+ });
+
+ describe('buildRequests', function () {
+ it('should return adUnit size', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const requests = spec.buildRequests([validBid, validBid2], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData.bidrequests[0]).to.include({adunitSize: validBid.mediaTypes.banner.sizes});
+ });
+
+ it('should return empty bid size', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const requests = spec.buildRequests([validBid, validBid2], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData.bidrequests[1]).to.deep.include({adunitSize: []});
+ });
+
+ it('should return bid size from params', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const requests = spec.buildRequests([validBid, validBid2], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData.bidrequests[1]).to.include({size: validBid2.params.size});
+ });
+
+ it('should return GDPR info', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const requests = spec.buildRequests([validBid, validBid2], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData).to.include({gdpr: true, gdpr_cs: bidderRequest.gdprConsent.consentString});
+ });
+
+ it('should not have error on empty GDPR', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const bidderRequestWithoutGDRP = {...bidderRequest, gdprConsent: null};
+ const requests = spec.buildRequests([validBid, validBid2], bidderRequestWithoutGDRP);
+ const requestsData = requests.data;
+
+ expect(requestsData).to.include({gdpr: false});
+ });
+
+ it('should create single POST', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const requests = spec.buildRequests([validBid, validBid2], bidderRequest);
+
+ expect(requests.method).to.equal('POST');
+ });
+ });
+
+ describe('creative viewability', function () {
+ it('should return coords', function () {
+ createElementVisible(validBid.adUnitCode);
+ const requests = spec.buildRequests([validBid], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData.bidrequests[0]).to.deep.include({coords: {x: 10, y: 10}});
+ });
+
+ it('should define not iframe', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const requests = spec.buildRequests([validBid, validBid2], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData).to.include({isiframe: false});
+ });
+
+ it('should define visible element', function () {
+ createElementVisible(validBid.adUnitCode);
+ const requests = spec.buildRequests([validBid], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData.bidrequests[0]).to.include({ishidden: false});
+ });
+
+ it('should define invisible element', function () {
+ createElementInvisible(validBid.adUnitCode);
+ const requests = spec.buildRequests([validBid], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData.bidrequests[0]).to.include({ishidden: true});
+ });
+
+ it('should define hidden element', function () {
+ createElementHidden(validBid.adUnitCode);
+ const requests = spec.buildRequests([validBid], bidderRequest);
+ const requestsData = requests.data;
+
+ expect(requestsData.bidrequests[0]).to.include({ishidden: true});
+ });
+ });
+
+ describe('interpretResponse', function () {
+ it('should return no bids if the response is not valid', function () {
+ const bidResponse = spec.interpretResponse({ body: null }, validBid);
+
+ expect(bidResponse.length).to.equal(0);
+ });
+
+ it('should return no bids if the response is empty', function () {
+ const bidResponse = spec.interpretResponse({ body: [] }, { validBid });
+
+ expect(bidResponse.length).to.equal(0);
+ });
+
+ it('should return valid bid responses', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const request = spec.buildRequests([validBid, validBid2], bidderRequest);
+ const bidResponse = spec.interpretResponse({body: serverResponse}, request);
+
+ expect(bidResponse[0]).to.deep.include({
+ requestId: validBid.bidId,
+ ad: serverResponse.bids[0].ad,
+ mediaType: 'banner',
+ creativeId: serverResponse.bids[0].creativeId,
+ cpm: serverResponse.bids[0].cpm,
+ width: serverResponse.bids[0].width,
+ height: serverResponse.bids[0].height,
+ currency: 'EUR',
+ netRevenue: true,
+ ttl: 500,
+ meta: serverResponse.bids[0].meta,
+ pubid: validBid.params.pubid
+ });
+ });
+
+ it('should return default bid params', function () {
+ createElementVisible(validBid.adUnitCode);
+ createElementVisible(validBid2.adUnitCode);
+ const request = spec.buildRequests([validBid, validBid2], bidderRequest);
+ const bidResponse = spec.interpretResponse({body: serverResponse2}, request);
+
+ expect(bidResponse[0]).to.deep.include({
+ requestId: validBid2.bidId,
+ ad: serverResponse2.bids[0].ad,
+ mediaType: 'banner',
+ creativeId: serverResponse2.bids[0].creativeId,
+ cpm: serverResponse2.bids[0].cpm,
+ width: serverResponse2.bids[0].width,
+ height: serverResponse2.bids[0].height,
+ meta: serverResponse2.bids[0].meta,
+ pubid: validBid2.params.pubid,
+ currency: DEFAULT_CURRENCY,
+ netRevenue: DEFAULT_NET_REVENUE,
+ ttl: DEFAULT_TTL,
+ });
+ });
+ });
+
+ describe('getUserSyncs', function () {
+ let syncOptions
+ beforeEach(function () {
+ syncOptions = {
+ enabledBidders: ['h12media'],
+ pixelEnabled: true,
+ iframeEnabled: true
+ }
+ });
+
+ it('should success with usersync pixel url', function () {
+ const result = {
+ type: 'image',
+ url: `https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies=${bidderRequest.gdprConsent.gdprApplies}&gdpr_consent_string=${bidderRequest.gdprConsent.consentString}`,
+ };
+ const syncs = spec.getUserSyncs(syncOptions, [{body: serverResponse}], bidderRequest.gdprConsent);
+
+ expect(syncs).to.deep.include(result);
+ });
+
+ it('should success with usersync iframe url', function () {
+ const result = {
+ type: 'iframe',
+ url: `https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies=${bidderRequest.gdprConsent.gdprApplies}&gdpr_consent_string=${bidderRequest.gdprConsent.consentString}`,
+ };
+ const syncs = spec.getUserSyncs(syncOptions, [{body: serverResponse}], bidderRequest.gdprConsent);
+
+ expect(syncs).to.deep.include(result);
+ });
+
+ it('should success without GDRP', function () {
+ const result = {
+ type: 'iframe',
+ url: `https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies=false&gdpr_consent_string=`,
+ };
+
+ expect(spec.getUserSyncs(syncOptions, [{body: serverResponse}], null)).to.deep.include(result);
+ });
+
+ it('should success without usersync url', function () {
+ expect(spec.getUserSyncs(syncOptions, [{body: serverResponse2}], bidderRequest.gdprConsent)).to.deep.equal([]);
+ });
+
+ it('should return empty usersync on empty response', function () {
+ expect(spec.getUserSyncs(syncOptions, [{body: {}}])).to.deep.equal([]);
+ });
+ });
+});