Skip to content

Commit

Permalink
Auto detect if we can bust out of iframe (#15) (#4099)
Browse files Browse the repository at this point in the history
* Add HTML5 video support param to bid requests

* Use const instead of var for consistency

* Update supported sizes

- Default size returned changed from 0x0 to 1x1 to support PrebidServer
- Now will always respect the bid sizes supported when configured

Co-authored-by: Josh Becker <jbecker@sharethrough.com>

* Update maintainer contact email

* Support Prebid.js User ID module

- Add support for Unified ID solution of User ID module by
  checking for `bidRequest.userId.tdid` param in `buildRequests`
  method of Sharethrough's adapter
- Update specs, maintain 80%+ code coverage

* Update logic for changing userAgent string in tests

* Add 3 pbjs callbacks to the adapter

* Add comments on empty implementations

* Update Sharethrough endpoint

* Add logic to detect safeframe

* Remove console.log statements
Fix issue with clientjs detection
Small refactors (linting)

Co-authored-by: Josh Becker <jbecker@sharethrough.com>

* Continue work on safeframe detection spec

Co-authored-by: Josh Becker <jbecker@sharethrough.com>

* [WIP]

* update version of sharethrough adapter from 3.0.1 to 3.1.0
* create sharethroughInternal const in adapter so that we can properly stub methods for testing, and utilize utility functions
* rename safeframe detection and iframe JS tag insertion code

* Finish iframe handler specs
Refactor spec file

* Change method of detecting whether locked in a frame or not

* Add logic to detect safeframe

* Remove console.log statements
Fix issue with clientjs detection
Small refactors (linting)

Co-authored-by: Josh Becker <jbecker@sharethrough.com>

* Continue work on safeframe detection spec

Co-authored-by: Josh Becker <jbecker@sharethrough.com>

* [WIP]

* update version of sharethrough adapter from 3.0.1 to 3.1.0
* create sharethroughInternal const in adapter so that we can properly stub methods for testing, and utilize utility functions
* rename safeframe detection and iframe JS tag insertion code

* Finish iframe handler specs
Refactor spec file

* Change method of detecting whether locked in a frame or not
  • Loading branch information
epechuzal authored and sumit116 committed Sep 3, 2019
1 parent a7ad5ef commit 3fe149a
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 57 deletions.
92 changes: 68 additions & 24 deletions modules/sharethroughBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { registerBidder } from '../src/adapters/bidderFactory';

const VERSION = '3.0.1';
const VERSION = '3.1.0';
const BIDDER_CODE = 'sharethrough';
const STR_ENDPOINT = document.location.protocol + '//btlr.sharethrough.com/WYu2BXv1/v1';
const DEFAULT_SIZE = [1, 1];

// this allows stubbing of utility function that is used internally by the sharethrough adapter
export const sharethroughInternal = {
b64EncodeUnicode,
handleIframe,
isLockedInFrame
};

export const sharethroughAdapterSpec = {
code: BIDDER_CODE,

Expand Down Expand Up @@ -37,10 +44,10 @@ export const sharethroughAdapterSpec = {
// Data that does not need to go to the server,
// but we need as part of interpretResponse()
const strData = {
stayInIframe: bidRequest.params.iframe,
skipIframeBusting: bidRequest.params.iframe,
iframeSize: bidRequest.params.iframeSize,
sizes: bidRequest.sizes
}
};

return {
method: 'GET',
Expand All @@ -59,7 +66,7 @@ export const sharethroughAdapterSpec = {
const creative = body.creatives[0];
let size = DEFAULT_SIZE;
if (req.strData.iframeSize || req.strData.sizes.length) {
size = req.strData.iframeSize != undefined
size = req.strData.iframeSize
? req.strData.iframeSize
: getLargestSize(req.strData.sizes);
}
Expand Down Expand Up @@ -102,7 +109,7 @@ export const sharethroughAdapterSpec = {

// Empty implementation for prebid core to be able to find it
onSetTargeting: (bid) => {}
}
};

function getLargestSize(sizes) {
function area(size) {
Expand All @@ -125,35 +132,72 @@ function generateAd(body, req) {
<div data-str-native-key="${req.data.placement_key}" data-stx-response-name="${strRespId}">
</div>
<script>var ${strRespId} = "${b64EncodeUnicode(JSON.stringify(body))}"</script>
`
`;

if (req.strData.stayInIframe) {
if (req.strData.skipIframeBusting) {
// Don't break out of iframe
adMarkup = adMarkup + `<script src="//native.sharethrough.com/assets/sfp.js"></script>`
adMarkup = adMarkup + `<script src="//native.sharethrough.com/assets/sfp.js"></script>`;
} else {
// Break out of iframe
// Add logic to the markup that detects whether or not in top level document is accessible
// this logic will deploy sfp.js and/or iframe buster script(s) as appropriate
adMarkup = adMarkup + `
<script src="//native.sharethrough.com/assets/sfp-set-targeting.js"></script>
<script>
(function() {
if (!(window.STR && window.STR.Tag) && !(window.top.STR && window.top.STR.Tag)) {
var sfp_js = document.createElement('script');
sfp_js.src = "//native.sharethrough.com/assets/sfp.js";
sfp_js.type = 'text/javascript';
sfp_js.charset = 'utf-8';
try {
window.top.document.getElementsByTagName('body')[0].appendChild(sfp_js);
} catch (e) {
console.log(e);
}
}
})()
</script>`
(${sharethroughInternal.isLockedInFrame.toString()})()
</script>
<script>
(${sharethroughInternal.handleIframe.toString()})()
</script>`;
}

return adMarkup;
}

function handleIframe () {
// only load iframe buster JS if we can access the top level document
// if we are 'locked in' to this frame then no point trying to bust out: we may as well render in the frame instead
var iframeBusterLoaded = false;
if (!window.lockedInFrame) {
var sfpIframeBusterJs = document.createElement('script');
sfpIframeBusterJs.src = '//native.sharethrough.com/assets/sfp-set-targeting.js';
sfpIframeBusterJs.type = 'text/javascript';
try {
window.document.getElementsByTagName('body')[0].appendChild(sfpIframeBusterJs);
iframeBusterLoaded = true;
} catch (e) {
console.error(e);
}
}

var clientJsLoaded = (!iframeBusterLoaded) ? !!(window.STR && window.STR.Tag) : !!(window.top.STR && window.top.STR.Tag);
if (!clientJsLoaded) {
var sfpJs = document.createElement('script');
sfpJs.src = '//native.sharethrough.com/assets/sfp.js';
sfpJs.type = 'text/javascript';

// only add sfp js to window.top if iframe busting successfully loaded; otherwise, add to iframe
try {
if (iframeBusterLoaded) {
window.top.document.getElementsByTagName('body')[0].appendChild(sfpJs);
} else {
window.document.getElementsByTagName('body')[0].appendChild(sfpJs);
}
} catch (e) {
console.error(e);
}
}
}

// determines if we are capable of busting out of the iframe we are in
// if we catch a DOMException when trying to access top-level document, it means we're stuck in the frame we're in
function isLockedInFrame () {
window.lockedInFrame = false;
try {
window.lockedInFrame = !window.top.document;
} catch (e) {
window.lockedInFrame = (e instanceof DOMException);
}
}

// See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
function b64EncodeUnicode(str) {
return btoa(
Expand Down
108 changes: 75 additions & 33 deletions test/spec/modules/sharethroughBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { sharethroughAdapterSpec } from 'modules/sharethroughBidAdapter';
import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter';
import { newBidder } from 'src/adapters/bidderFactory';

const spec = newBidder(sharethroughAdapterSpec).getSpec();
Expand Down Expand Up @@ -46,7 +46,7 @@ const prebidRequests = [
placement_key: 'pKey'
},
strData: {
stayInIframe: false,
skipIframeBusting: false,
sizes: []
}
},
Expand All @@ -58,7 +58,7 @@ const prebidRequests = [
placement_key: 'pKey'
},
strData: {
stayInIframe: true,
skipIframeBusting: true,
sizes: [[300, 250], [300, 300], [250, 250], [600, 50]]
}
},
Expand All @@ -70,7 +70,7 @@ const prebidRequests = [
placement_key: 'pKey'
},
strData: {
stayInIframe: true,
skipIframeBusting: true,
iframeSize: [500, 500],
sizes: [[300, 250], [300, 300], [250, 250], [600, 50]]
}
Expand All @@ -83,7 +83,7 @@ const prebidRequests = [
placement_key: 'pKey'
},
strData: {
stayInIframe: false,
skipIframeBusting: false,
sizes: [[0, 0]]
}
},
Expand All @@ -95,7 +95,7 @@ const prebidRequests = [
placement_key: 'pKey'
},
strData: {
stayInIframe: false,
skipIframeBusting: false,
sizes: [[300, 250], [300, 300], [250, 250], [600, 50]]
}
},
Expand All @@ -120,27 +120,71 @@ const bidderResponse = {
header: { get: (header) => header }
};

// Mirrors the one in modules/sharethroughBidAdapter.js as the function is unexported
const b64EncodeUnicode = (str) => {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}

const setUserAgent = (str) => {
window.navigator['__defineGetter__']('userAgent', function () {
return str;
});
}
};

describe('sharethrough internal spec', function () {
let windowSpy, windowTopSpy;

beforeEach(function() {
windowSpy = sinon.spy(window.document, 'getElementsByTagName');
windowTopSpy = sinon.spy(window.top.document, 'getElementsByTagName');
});

afterEach(function() {
windowSpy.restore();
windowTopSpy.restore();
window.STR = undefined;
window.top.STR = undefined;
});

describe('we cannot access top level document', function () {
beforeEach(function() {
window.lockedInFrame = true;
});

afterEach(function() {
window.lockedInFrame = false;
});

it('appends sfp.js to the safeframe', function () {
sharethroughInternal.handleIframe();
expect(windowSpy.calledOnce).to.be.true;
});

it('does not append anything if sfp.js is already loaded in the safeframe', function () {
window.STR = { Tag: true };
sharethroughInternal.handleIframe();
expect(windowSpy.notCalled).to.be.true;
expect(windowTopSpy.notCalled).to.be.true;
});
});

describe('we are able to bust out of the iframe', function () {
it('appends sfp.js to window.top', function () {
sharethroughInternal.handleIframe();
expect(windowSpy.calledOnce).to.be.true;
expect(windowTopSpy.calledOnce).to.be.true;
});

it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function () {
window.top.STR = { Tag: true };
sharethroughInternal.handleIframe();
expect(windowSpy.calledOnce).to.be.true;
expect(windowTopSpy.notCalled).to.be.true;
});
});
});

describe('sharethrough adapter spec', function () {
describe('.code', function () {
it('should return a bidder code of sharethrough', function () {
expect(spec.code).to.eql('sharethrough');
});
})
});

describe('.isBidRequestValid', function () {
it('should return false if req has no pkey', function () {
Expand Down Expand Up @@ -176,7 +220,7 @@ describe('sharethrough adapter spec', function () {
expect(builtBidRequests[0].url).to.eq(
'http://btlr.sharethrough.com/WYu2BXv1/v1');
expect(builtBidRequests[1].url).to.eq(
'http://btlr.sharethrough.com/WYu2BXv1/v1')
'http://btlr.sharethrough.com/WYu2BXv1/v1');
expect(builtBidRequests[0].method).to.eq('GET');
});

Expand Down Expand Up @@ -230,7 +274,7 @@ describe('sharethrough adapter spec', function () {
const builtBidRequests = spec.buildRequests(bidRequests);
expect(builtBidRequests[0]).to.deep.include({
strData: {
stayInIframe: undefined,
skipIframeBusting: undefined,
iframeSize: undefined,
sizes: [[600, 300]]
}
Expand All @@ -253,7 +297,7 @@ describe('sharethrough adapter spec', function () {
});
});

it('returns a correctly parsed out response with largest size when strData.stayInIframe is true', function () {
it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function () {
expect(spec.interpretResponse(bidderResponse, prebidRequests[1])[0]).to.include(
{
width: 300,
Expand All @@ -267,7 +311,7 @@ describe('sharethrough adapter spec', function () {
});
});

it('returns a correctly parsed out response with explicitly defined size when strData.stayInIframe is true and strData.iframeSize is provided', function () {
it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function () {
expect(spec.interpretResponse(bidderResponse, prebidRequests[2])[0]).to.include(
{
width: 500,
Expand All @@ -281,7 +325,7 @@ describe('sharethrough adapter spec', function () {
});
});

it('returns a correctly parsed out response with explicitly defined size when strData.stayInIframe is false and strData.sizes contains [0, 0] only', function () {
it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function () {
expect(spec.interpretResponse(bidderResponse, prebidRequests[3])[0]).to.include(
{
width: 0,
Expand All @@ -295,7 +339,7 @@ describe('sharethrough adapter spec', function () {
});
});

it('returns a correctly parsed out response with explicitly defined size when strData.stayInIframe is false and strData.sizes contains multiple sizes', function () {
it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function () {
expect(spec.interpretResponse(bidderResponse, prebidRequests[4])[0]).to.include(
{
width: 300,
Expand Down Expand Up @@ -324,29 +368,27 @@ describe('sharethrough adapter spec', function () {
expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty;
});

it('correctly generates ad markup', function () {
it('correctly generates ad markup when skipIframeBusting is false', function () {
const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[0])[0].ad;
let resp = null;

expect(() => btoa(JSON.stringify(bidderResponse))).to.throw();
expect(() => resp = b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw();
expect(() => resp = sharethroughInternal.b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw();
expect(adMarkup).to.match(
/data-str-native-key="pKey" data-stx-response-name=\"str_response_bidId\"/);
expect(!!adMarkup.indexOf(resp)).to.eql(true);
expect(adMarkup).to.match(
/<script src="\/\/native.sharethrough.com\/assets\/sfp-set-targeting.js"><\/script>/);
expect(adMarkup).to.match(
/sfp_js.src = "\/\/native.sharethrough.com\/assets\/sfp.js";/);
expect(adMarkup).to.match(
/window.top.document.getElementsByTagName\('body'\)\[0\].appendChild\(sfp_js\);/)

// insert functionality to autodetect whether or not in safeframe, and handle JS insertion
expect(adMarkup).to.match(/isLockedInFrame/);
expect(adMarkup).to.match(/handleIframe/);
});

it('correctly generates ad markup for staying in iframe', function () {
it('correctly generates ad markup when skipIframeBusting is true', function () {
const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[1])[0].ad;
let resp = null;

expect(() => btoa(JSON.stringify(bidderResponse))).to.throw();
expect(() => resp = b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw();
expect(() => resp = sharethroughInternal.b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw();
expect(adMarkup).to.match(
/data-str-native-key="pKey" data-stx-response-name=\"str_response_bidId\"/);
expect(!!adMarkup.indexOf(resp)).to.eql(true);
Expand Down

0 comments on commit 3fe149a

Please sign in to comment.