Skip to content

Commit 824040b

Browse files
epechuzalsa1omon
authored and
sa1omon
committed
Auto detect if we can bust out of iframe (prebid#15) (prebid#4099)
* 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 <[email protected]> * 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 <[email protected]> * Continue work on safeframe detection spec Co-authored-by: Josh Becker <[email protected]> * [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 <[email protected]> * Continue work on safeframe detection spec Co-authored-by: Josh Becker <[email protected]> * [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
1 parent 51ab445 commit 824040b

File tree

2 files changed

+143
-57
lines changed

2 files changed

+143
-57
lines changed

modules/sharethroughBidAdapter.js

+68-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { registerBidder } from '../src/adapters/bidderFactory';
22

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

8+
// this allows stubbing of utility function that is used internally by the sharethrough adapter
9+
export const sharethroughInternal = {
10+
b64EncodeUnicode,
11+
handleIframe,
12+
isLockedInFrame
13+
};
14+
815
export const sharethroughAdapterSpec = {
916
code: BIDDER_CODE,
1017

@@ -37,10 +44,10 @@ export const sharethroughAdapterSpec = {
3744
// Data that does not need to go to the server,
3845
// but we need as part of interpretResponse()
3946
const strData = {
40-
stayInIframe: bidRequest.params.iframe,
47+
skipIframeBusting: bidRequest.params.iframe,
4148
iframeSize: bidRequest.params.iframeSize,
4249
sizes: bidRequest.sizes
43-
}
50+
};
4451

4552
return {
4653
method: 'GET',
@@ -59,7 +66,7 @@ export const sharethroughAdapterSpec = {
5966
const creative = body.creatives[0];
6067
let size = DEFAULT_SIZE;
6168
if (req.strData.iframeSize || req.strData.sizes.length) {
62-
size = req.strData.iframeSize != undefined
69+
size = req.strData.iframeSize
6370
? req.strData.iframeSize
6471
: getLargestSize(req.strData.sizes);
6572
}
@@ -102,7 +109,7 @@ export const sharethroughAdapterSpec = {
102109

103110
// Empty implementation for prebid core to be able to find it
104111
onSetTargeting: (bid) => {}
105-
}
112+
};
106113

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

130-
if (req.strData.stayInIframe) {
137+
if (req.strData.skipIframeBusting) {
131138
// Don't break out of iframe
132-
adMarkup = adMarkup + `<script src="//native.sharethrough.com/assets/sfp.js"></script>`
139+
adMarkup = adMarkup + `<script src="//native.sharethrough.com/assets/sfp.js"></script>`;
133140
} else {
134-
// Break out of iframe
141+
// Add logic to the markup that detects whether or not in top level document is accessible
142+
// this logic will deploy sfp.js and/or iframe buster script(s) as appropriate
135143
adMarkup = adMarkup + `
136-
<script src="//native.sharethrough.com/assets/sfp-set-targeting.js"></script>
137144
<script>
138-
(function() {
139-
if (!(window.STR && window.STR.Tag) && !(window.top.STR && window.top.STR.Tag)) {
140-
var sfp_js = document.createElement('script');
141-
sfp_js.src = "//native.sharethrough.com/assets/sfp.js";
142-
sfp_js.type = 'text/javascript';
143-
sfp_js.charset = 'utf-8';
144-
try {
145-
window.top.document.getElementsByTagName('body')[0].appendChild(sfp_js);
146-
} catch (e) {
147-
console.log(e);
148-
}
149-
}
150-
})()
151-
</script>`
145+
(${sharethroughInternal.isLockedInFrame.toString()})()
146+
</script>
147+
<script>
148+
(${sharethroughInternal.handleIframe.toString()})()
149+
</script>`;
152150
}
153151

154152
return adMarkup;
155153
}
156154

155+
function handleIframe () {
156+
// only load iframe buster JS if we can access the top level document
157+
// if we are 'locked in' to this frame then no point trying to bust out: we may as well render in the frame instead
158+
var iframeBusterLoaded = false;
159+
if (!window.lockedInFrame) {
160+
var sfpIframeBusterJs = document.createElement('script');
161+
sfpIframeBusterJs.src = '//native.sharethrough.com/assets/sfp-set-targeting.js';
162+
sfpIframeBusterJs.type = 'text/javascript';
163+
try {
164+
window.document.getElementsByTagName('body')[0].appendChild(sfpIframeBusterJs);
165+
iframeBusterLoaded = true;
166+
} catch (e) {
167+
console.error(e);
168+
}
169+
}
170+
171+
var clientJsLoaded = (!iframeBusterLoaded) ? !!(window.STR && window.STR.Tag) : !!(window.top.STR && window.top.STR.Tag);
172+
if (!clientJsLoaded) {
173+
var sfpJs = document.createElement('script');
174+
sfpJs.src = '//native.sharethrough.com/assets/sfp.js';
175+
sfpJs.type = 'text/javascript';
176+
177+
// only add sfp js to window.top if iframe busting successfully loaded; otherwise, add to iframe
178+
try {
179+
if (iframeBusterLoaded) {
180+
window.top.document.getElementsByTagName('body')[0].appendChild(sfpJs);
181+
} else {
182+
window.document.getElementsByTagName('body')[0].appendChild(sfpJs);
183+
}
184+
} catch (e) {
185+
console.error(e);
186+
}
187+
}
188+
}
189+
190+
// determines if we are capable of busting out of the iframe we are in
191+
// if we catch a DOMException when trying to access top-level document, it means we're stuck in the frame we're in
192+
function isLockedInFrame () {
193+
window.lockedInFrame = false;
194+
try {
195+
window.lockedInFrame = !window.top.document;
196+
} catch (e) {
197+
window.lockedInFrame = (e instanceof DOMException);
198+
}
199+
}
200+
157201
// See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
158202
function b64EncodeUnicode(str) {
159203
return btoa(

test/spec/modules/sharethroughBidAdapter_spec.js

+75-33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from 'chai';
2-
import { sharethroughAdapterSpec } from 'modules/sharethroughBidAdapter';
2+
import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter';
33
import { newBidder } from 'src/adapters/bidderFactory';
44

55
const spec = newBidder(sharethroughAdapterSpec).getSpec();
@@ -46,7 +46,7 @@ const prebidRequests = [
4646
placement_key: 'pKey'
4747
},
4848
strData: {
49-
stayInIframe: false,
49+
skipIframeBusting: false,
5050
sizes: []
5151
}
5252
},
@@ -58,7 +58,7 @@ const prebidRequests = [
5858
placement_key: 'pKey'
5959
},
6060
strData: {
61-
stayInIframe: true,
61+
skipIframeBusting: true,
6262
sizes: [[300, 250], [300, 300], [250, 250], [600, 50]]
6363
}
6464
},
@@ -70,7 +70,7 @@ const prebidRequests = [
7070
placement_key: 'pKey'
7171
},
7272
strData: {
73-
stayInIframe: true,
73+
skipIframeBusting: true,
7474
iframeSize: [500, 500],
7575
sizes: [[300, 250], [300, 300], [250, 250], [600, 50]]
7676
}
@@ -83,7 +83,7 @@ const prebidRequests = [
8383
placement_key: 'pKey'
8484
},
8585
strData: {
86-
stayInIframe: false,
86+
skipIframeBusting: false,
8787
sizes: [[0, 0]]
8888
}
8989
},
@@ -95,7 +95,7 @@ const prebidRequests = [
9595
placement_key: 'pKey'
9696
},
9797
strData: {
98-
stayInIframe: false,
98+
skipIframeBusting: false,
9999
sizes: [[300, 250], [300, 300], [250, 250], [600, 50]]
100100
}
101101
},
@@ -120,27 +120,71 @@ const bidderResponse = {
120120
header: { get: (header) => header }
121121
};
122122

123-
// Mirrors the one in modules/sharethroughBidAdapter.js as the function is unexported
124-
const b64EncodeUnicode = (str) => {
125-
return btoa(
126-
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
127-
function toSolidBytes(match, p1) {
128-
return String.fromCharCode('0x' + p1);
129-
}));
130-
}
131-
132123
const setUserAgent = (str) => {
133124
window.navigator['__defineGetter__']('userAgent', function () {
134125
return str;
135126
});
136-
}
127+
};
128+
129+
describe('sharethrough internal spec', function () {
130+
let windowSpy, windowTopSpy;
131+
132+
beforeEach(function() {
133+
windowSpy = sinon.spy(window.document, 'getElementsByTagName');
134+
windowTopSpy = sinon.spy(window.top.document, 'getElementsByTagName');
135+
});
136+
137+
afterEach(function() {
138+
windowSpy.restore();
139+
windowTopSpy.restore();
140+
window.STR = undefined;
141+
window.top.STR = undefined;
142+
});
143+
144+
describe('we cannot access top level document', function () {
145+
beforeEach(function() {
146+
window.lockedInFrame = true;
147+
});
148+
149+
afterEach(function() {
150+
window.lockedInFrame = false;
151+
});
152+
153+
it('appends sfp.js to the safeframe', function () {
154+
sharethroughInternal.handleIframe();
155+
expect(windowSpy.calledOnce).to.be.true;
156+
});
157+
158+
it('does not append anything if sfp.js is already loaded in the safeframe', function () {
159+
window.STR = { Tag: true };
160+
sharethroughInternal.handleIframe();
161+
expect(windowSpy.notCalled).to.be.true;
162+
expect(windowTopSpy.notCalled).to.be.true;
163+
});
164+
});
165+
166+
describe('we are able to bust out of the iframe', function () {
167+
it('appends sfp.js to window.top', function () {
168+
sharethroughInternal.handleIframe();
169+
expect(windowSpy.calledOnce).to.be.true;
170+
expect(windowTopSpy.calledOnce).to.be.true;
171+
});
172+
173+
it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function () {
174+
window.top.STR = { Tag: true };
175+
sharethroughInternal.handleIframe();
176+
expect(windowSpy.calledOnce).to.be.true;
177+
expect(windowTopSpy.notCalled).to.be.true;
178+
});
179+
});
180+
});
137181

138182
describe('sharethrough adapter spec', function () {
139183
describe('.code', function () {
140184
it('should return a bidder code of sharethrough', function () {
141185
expect(spec.code).to.eql('sharethrough');
142186
});
143-
})
187+
});
144188

145189
describe('.isBidRequestValid', function () {
146190
it('should return false if req has no pkey', function () {
@@ -176,7 +220,7 @@ describe('sharethrough adapter spec', function () {
176220
expect(builtBidRequests[0].url).to.eq(
177221
'http://btlr.sharethrough.com/WYu2BXv1/v1');
178222
expect(builtBidRequests[1].url).to.eq(
179-
'http://btlr.sharethrough.com/WYu2BXv1/v1')
223+
'http://btlr.sharethrough.com/WYu2BXv1/v1');
180224
expect(builtBidRequests[0].method).to.eq('GET');
181225
});
182226

@@ -230,7 +274,7 @@ describe('sharethrough adapter spec', function () {
230274
const builtBidRequests = spec.buildRequests(bidRequests);
231275
expect(builtBidRequests[0]).to.deep.include({
232276
strData: {
233-
stayInIframe: undefined,
277+
skipIframeBusting: undefined,
234278
iframeSize: undefined,
235279
sizes: [[600, 300]]
236280
}
@@ -253,7 +297,7 @@ describe('sharethrough adapter spec', function () {
253297
});
254298
});
255299

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

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

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

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

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

331375
expect(() => btoa(JSON.stringify(bidderResponse))).to.throw();
332-
expect(() => resp = b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw();
376+
expect(() => resp = sharethroughInternal.b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw();
333377
expect(adMarkup).to.match(
334378
/data-str-native-key="pKey" data-stx-response-name=\"str_response_bidId\"/);
335379
expect(!!adMarkup.indexOf(resp)).to.eql(true);
336-
expect(adMarkup).to.match(
337-
/<script src="\/\/native.sharethrough.com\/assets\/sfp-set-targeting.js"><\/script>/);
338-
expect(adMarkup).to.match(
339-
/sfp_js.src = "\/\/native.sharethrough.com\/assets\/sfp.js";/);
340-
expect(adMarkup).to.match(
341-
/window.top.document.getElementsByTagName\('body'\)\[0\].appendChild\(sfp_js\);/)
380+
381+
// insert functionality to autodetect whether or not in safeframe, and handle JS insertion
382+
expect(adMarkup).to.match(/isLockedInFrame/);
383+
expect(adMarkup).to.match(/handleIframe/);
342384
});
343385

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

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

0 commit comments

Comments
 (0)