Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto detect if we can bust out of iframe (#15) #4099

Merged
merged 1 commit into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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