Skip to content

Commit 31f650e

Browse files
dgirardiDecayConstant
authored andcommitted
Core: add location method for cross-frame creatives and update creatives (prebid#11863)
* Core: add location method for cross-frame creatives and update creatives * improvements
1 parent 6604eda commit 31f650e

File tree

10 files changed

+94
-14
lines changed

10 files changed

+94
-14
lines changed

creative/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// eslint-disable-next-line prebid/validate-imports
2-
import { AD_RENDER_FAILED_REASON, EVENTS, MESSAGES } from '../src/constants.js';
2+
import {AD_RENDER_FAILED_REASON, EVENTS, MESSAGES} from '../src/constants.js';
33

4+
export {PB_LOCATOR} from '../src/constants.js';
45
export const MESSAGE_REQUEST = MESSAGES.REQUEST;
56
export const MESSAGE_RESPONSE = MESSAGES.RESPONSE;
67
export const MESSAGE_EVENT = MESSAGES.EVENT;

creative/crossDomain.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
22
ERROR_EXCEPTION,
3-
EVENT_AD_RENDER_FAILED, EVENT_AD_RENDER_SUCCEEDED,
3+
EVENT_AD_RENDER_FAILED,
4+
EVENT_AD_RENDER_SUCCEEDED,
45
MESSAGE_EVENT,
56
MESSAGE_REQUEST,
6-
MESSAGE_RESPONSE
7+
MESSAGE_RESPONSE,
8+
PB_LOCATOR
79
} from './constants.js';
810

911
const mkFrame = (() => {
@@ -24,14 +26,24 @@ const mkFrame = (() => {
2426
};
2527
})();
2628

29+
function isPrebidWindow(win) {
30+
return !!win.frames[PB_LOCATOR];
31+
}
32+
2733
export function renderer(win) {
34+
let target = win.parent;
35+
while (target !== win.top && !isPrebidWindow(target)) {
36+
target = target.parent;
37+
}
38+
if (!isPrebidWindow(target)) target = win.parent;
39+
2840
return function ({adId, pubUrl, clickUrl}) {
2941
const pubDomain = new URL(pubUrl, window.location).origin;
3042

3143
function sendMessage(type, payload, responseListener) {
3244
const channel = new MessageChannel();
3345
channel.port1.onmessage = guard(responseListener);
34-
win.parent.postMessage(JSON.stringify(Object.assign({message: type, adId}, payload)), pubDomain, [channel.port2]);
46+
target.postMessage(JSON.stringify(Object.assign({message: type, adId}, payload)), pubDomain, [channel.port2]);
3547
}
3648

3749
function onError(e) {
@@ -77,7 +89,7 @@ export function renderer(win) {
7789
W.Promise.resolve(W.render(data, {sendMessage, mkFrame}, win)).then(
7890
() => sendMessage(MESSAGE_EVENT, {event: EVENT_AD_RENDER_SUCCEEDED}),
7991
onError
80-
)
92+
);
8193
});
8294
win.document.body.appendChild(renderer);
8395
}

integrationExamples/gpt/x-domain/creative.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// creative will be rendered, e.g. GAM delivering a SafeFrame
33

44
// this code is autogenerated, also available in 'build/creative/creative.js'
5-
<script>!function(){"use strict";const e="Prebid Event",n=(()=>{const e={frameBorder:0,scrolling:"no",marginHeight:0,marginWidth:0,topMargin:0,leftMargin:0,allowTransparency:"true"};return(n,t)=>{const r=n.createElement("iframe");return Object.entries(Object.assign({},t,e)).forEach((([e,n])=>r.setAttribute(e,n))),r}})();var t;window.pbRender=(t=window,function({adId:r,pubUrl:s,clickUrl:o}){const i=new URL(s,window.location).origin;function a(e,n,s){const o=new MessageChannel;o.port1.onmessage=d(s),t.parent.postMessage(JSON.stringify(Object.assign({message:e,adId:r},n)),i,[o.port2])}function c(n){a(e,{event:"adRenderFailed",info:{reason:n?.reason||"exception",message:n?.message}}),n?.stack&&console.error(n)}function d(e){return function(){try{return e.apply(this,arguments)}catch(e){c(e)}}}a("Prebid Request",{options:{clickUrl:o}},(function(s){let o;try{o=JSON.parse(s.data)}catch(e){return}if("Prebid Response"===o.message&&o.adId===r){const r=n(t.document,{width:0,height:0,style:"display: none",srcdoc:`<script>${o.renderer}<\/script>`});r.onload=d((function(){const s=r.contentWindow;s.Promise.resolve(s.render(o,{sendMessage:a,mkFrame:n},t)).then((()=>a(e,{event:"adRenderSucceeded"})),c)})),t.document.body.appendChild(r)}}))})}();</script>
5+
<script>(()=>{"use strict";const e="Prebid Event",n=(()=>{const e={frameBorder:0,scrolling:"no",marginHeight:0,marginWidth:0,topMargin:0,leftMargin:0,allowTransparency:"true"};return(n,t)=>{const r=n.createElement("iframe");return Object.entries(Object.assign({},t,e)).forEach((([e,n])=>r.setAttribute(e,n))),r}})();function t(e){return!!e.frames.__pb_locator__}window.pbRender=function(r){let o=r.parent;for(;o!==r.top&&!t(o);)o=o.parent;return t(o)||(o=r.parent),function({adId:t,pubUrl:s,clickUrl:i}){const a=new URL(s,window.location).origin;function c(e,n,r){const s=new MessageChannel;s.port1.onmessage=l(r),o.postMessage(JSON.stringify(Object.assign({message:e,adId:t},n)),a,[s.port2])}function d(n){c(e,{event:"adRenderFailed",info:{reason:n?.reason||"exception",message:n?.message}}),n?.stack&&console.error(n)}function l(e){return function(){try{return e.apply(this,arguments)}catch(e){d(e)}}}c("Prebid Request",{options:{clickUrl:i}},(function(o){let s;try{s=JSON.parse(o.data)}catch(e){return}if("Prebid Response"===s.message&&s.adId===t){const t=n(r.document,{width:0,height:0,style:"display: none",srcdoc:`<script>${s.renderer}<\/script>`});t.onload=l((function(){const o=t.contentWindow;o.Promise.resolve(o.render(s,{sendMessage:c,mkFrame:n},r)).then((()=>c(e,{event:"adRenderSucceeded"})),d)})),r.document.body.appendChild(t)}}))}}(window)})();</script>
66

77
<script>
88
pbRender({

libraries/creative-renderer-display/renderer.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libraries/creative-renderer-native/renderer.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/adRendering.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import {createIframe, deepAccess, inIframe, insertElement, logError, logWarn, replaceMacros} from './utils.js';
1+
import {
2+
createIframe,
3+
createInvisibleIframe,
4+
deepAccess,
5+
inIframe,
6+
insertElement,
7+
logError,
8+
logWarn,
9+
replaceMacros
10+
} from './utils.js';
211
import * as events from './events.js';
3-
import { AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS, MESSAGES } from './constants.js';
12+
import {AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS, MESSAGES, PB_LOCATOR} from './constants.js';
413
import {config} from './config.js';
514
import {executeRenderer, isRendererRequired} from './Renderer.js';
615
import {VIDEO} from './mediaTypes.js';
@@ -235,3 +244,17 @@ export function renderAdDirect(doc, adId, options) {
235244
fail(EXCEPTION, e.message);
236245
}
237246
}
247+
248+
/**
249+
* Insert an invisible, named iframe that can be used by creatives to locate the window Prebid is running in
250+
* (by looking for one that has `.frames[PB_LOCATOR]` defined).
251+
* This is necessary because in some situations creatives may be rendered inside nested iframes - Prebid is not necessarily
252+
* in the immediate parent window.
253+
*/
254+
export function insertLocatorFrame() {
255+
if (!window.frames[PB_LOCATOR]) {
256+
const frame = createInvisibleIframe();
257+
frame.name = PB_LOCATOR;
258+
document.body.appendChild(frame);
259+
}
260+
}

src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,5 @@ export const MESSAGES = {
193193
NATIVE: 'Prebid Native',
194194
EVENT: 'Prebid Event'
195195
};
196+
197+
export const PB_LOCATOR = '__pb_locator__';

src/prebid.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {newMetrics, useMetrics} from './utils/perfMetrics.js';
3939
import {defer, GreedyPromise} from './utils/promise.js';
4040
import {enrichFPD} from './fpd/enrichment.js';
4141
import {allConsent} from './consentHandler.js';
42-
import {renderAdDirect} from './adRendering.js';
42+
import {insertLocatorFrame, renderAdDirect} from './adRendering.js';
4343
import {getHighestCpm} from './utils/reducers.js';
4444
import {fillVideoDefaults} from './video.js';
4545

@@ -976,6 +976,7 @@ function processQueue(queue) {
976976
* @alias module:pbjs.processQueue
977977
*/
978978
pbjsInstance.processQueue = function () {
979+
insertLocatorFrame();
979980
hook.ready();
980981
processQueue(pbjsInstance.que);
981982
processQueue(pbjsInstance.cmd);

test/spec/creative/crossDomainCreative_spec.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ import {
99

1010
describe('cross-domain creative', () => {
1111
const ORIGIN = 'https://example.com';
12-
let win, renderAd, messages, mkIframe;
12+
let win, top, renderAd, messages, mkIframe;
1313

1414
beforeEach(() => {
1515
messages = [];
1616
mkIframe = sinon.stub();
17+
top = {
18+
frames: {}
19+
};
20+
top.top = top;
1721
win = {
22+
top,
23+
frames: {},
1824
document: {
1925
body: {
2026
appendChild: sinon.stub(),
@@ -30,12 +36,14 @@ describe('cross-domain creative', () => {
3036
})
3137
},
3238
parent: {
39+
parent: top,
40+
frames: {'__pb_locator__': {}},
3341
postMessage: sinon.stub().callsFake((payload, targetOrigin, transfer) => {
3442
messages.push({payload: JSON.parse(payload), targetOrigin, transfer});
3543
})
3644
}
3745
};
38-
renderAd = renderer(win);
46+
renderAd = (...args) => renderer(win)(...args);
3947
})
4048

4149
function waitFor(predicate, timeout = 1000) {
@@ -64,6 +72,34 @@ describe('cross-domain creative', () => {
6472
expect(messages[0].targetOrigin).to.eql('https://domain.com:123')
6573
});
6674

75+
it('posts to first parent if no __pb_locator__ can be found', () => {
76+
delete win.parent.frames['__pb_locator__'];
77+
renderAd({pubUrl: 'https://www.example.com'});
78+
expect(messages.length).to.eql(1);
79+
})
80+
81+
describe('when there are multiple ancestors', () => {
82+
let target;
83+
beforeEach(() => {
84+
target = win.parent;
85+
win.parent = {
86+
top,
87+
frames: {},
88+
parent: {
89+
...target,
90+
parent: {
91+
frames: {'__pb_locator__': {}},
92+
top
93+
}
94+
}
95+
}
96+
})
97+
it('posts message to the first ancestor with __pb_locator__ child', () => {
98+
renderAd({pubUrl: 'https://www.example.com'});
99+
expect(messages.length).to.eql(1);
100+
});
101+
})
102+
67103
it('generates request message with adId and clickUrl', () => {
68104
renderAd({adId: '123', clickUrl: 'https://click-url.com', pubUrl: ORIGIN});
69105
expect(messages[0].payload).to.eql({

test/spec/unit/pbjs_api_spec.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {enrichFPD} from '../../../src/fpd/enrichment.js';
2626
import {mockFpdEnrichments} from '../../helpers/fpd.js';
2727
import {generateUUID} from '../../../src/utils.js';
2828
import {getCreativeRenderer} from '../../../src/creativeRenderers.js';
29-
import { BID_STATUS, EVENTS, GRANULARITY_OPTIONS, TARGETING_KEYS } from 'src/constants.js';
29+
import {BID_STATUS, EVENTS, GRANULARITY_OPTIONS, PB_LOCATOR, TARGETING_KEYS} from 'src/constants.js';
3030
import {getBidToRender} from '../../../src/adRendering.js';
3131

3232
var assert = require('chai').assert;
@@ -236,6 +236,11 @@ describe('Unit: Prebid Module', function () {
236236
getBidToRender.getHooks({hook: getBidToRenderHook}).remove();
237237
});
238238

239+
it('should insert a locator frame on the page', () => {
240+
$$PREBID_GLOBAL$$.processQueue();
241+
expect(window.frames[PB_LOCATOR]).to.exist;
242+
})
243+
239244
describe('and global adUnits', () => {
240245
const startingAdUnits = [
241246
{

0 commit comments

Comments
 (0)