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

Forbid adding event listeners on appended iframes #2945

Merged
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
2 changes: 1 addition & 1 deletion extensions/amp-facebook/0.1/amp-facebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ class AmpFacebook extends AMP.BaseElement {
const iframe = getIframe(this.element.ownerDocument.defaultView,
this.element, 'facebook');
this.applyFillContent(iframe);
this.element.appendChild(iframe);
// Triggered by context.updateDimensions() inside the iframe.
listen(iframe, 'embed-size', data => {
iframe.height = data.height;
Expand All @@ -51,6 +50,7 @@ class AmpFacebook extends AMP.BaseElement {
amp.setAttribute('width', data.width);
this./*OK*/changeHeight(data.height);
}, /* opt_is3P */true);
this.element.appendChild(iframe);
return loadPromise(iframe);
}
};
Expand Down
4 changes: 2 additions & 2 deletions extensions/amp-iframe/0.1/amp-iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,6 @@ export class AmpIframe extends AMP.BaseElement {
setSandbox(this.element, iframe, this.sandbox_);
iframe.src = this.iframeSrc;

this.container_.appendChild(iframe);

if (!isTracking) {
this.intersectionObserver_ = new IntersectionObserver(this, iframe);
}
Expand Down Expand Up @@ -306,6 +304,8 @@ export class AmpIframe extends AMP.BaseElement {
listen(iframe, 'embed-ready', this.activateIframe_.bind(this));
}

this.container_.appendChild(iframe);

return loadPromise(iframe).then(() => {
// On iOS the iframe at times fails to render inside the `overflow:auto`
// container. To avoid this problem, we set the `overflow:auto` property
Expand Down
2 changes: 1 addition & 1 deletion extensions/amp-twitter/0.1/amp-twitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ class AmpTwitter extends AMP.BaseElement {
const iframe = getIframe(this.element.ownerDocument.defaultView,
this.element, 'twitter');
this.applyFillContent(iframe);
this.element.appendChild(iframe);
// Triggered by context.updateDimensions() inside the iframe.
listen(iframe, 'embed-size', data => {
// We only get the message if and when there is a tweet to display,
Expand All @@ -63,6 +62,7 @@ class AmpTwitter extends AMP.BaseElement {
amp.setAttribute('width', data.width);
this./*OK*/changeHeight(data.height);
}, /* opt_is3P */true);
this.element.appendChild(iframe);
return loadPromise(iframe);
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/iframe-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {parseUrl} from './url';
*/
export function listen(iframe, typeOfMessage, callback, opt_is3P) {
dev.assert(iframe.src, 'only iframes with src supported');
dev.assert(!iframe.parentNode, 'cannot register events on an attached ' +
'iframe. It will cause hair-pulling bugs like #2942');
const origin = parseUrl(iframe.src).origin;
let win = iframe.ownerDocument.defaultView;
const sentinel = getSentinel_(opt_is3P);
Expand Down
10 changes: 4 additions & 6 deletions test/fixtures/served/iframe-intersection.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@
el.value =
el.value + '\n[' + (new Date().getTime() - startTime) + '] ' + (m || '');
}
window.setTimeout(function() {
parent./*OK*/postMessage({
sentinel: 'amp',
type: 'send-intersections'
}, '*');
}, 1000);
parent./*OK*/postMessage({
sentinel: 'amp',
type: 'send-intersections'
}, '*');

window.addEventListener('message', function(event) {
if (event.data) {
Expand Down
27 changes: 24 additions & 3 deletions test/functional/test-iframe-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ describe('iframe-helper', function() {
let sandbox;
let container;

function insert(iframe) {
container.doc.body.appendChild(iframe);
}

beforeEach(() => {
sandbox = sinon.sandbox.create();
return createIframePromise().then(c => {
container = c;
const i = document.createElement('iframe');
const i = c.doc.createElement('iframe');
i.src = iframeSrc;
container.doc.body.appendChild(i);
testIframe = i;
});
});
Expand All @@ -42,6 +45,22 @@ describe('iframe-helper', function() {
sandbox.restore();
});

it('should assert src in iframe', () => {
const iframe = container.doc.createElement('iframe');
iframe.srcdoc = '<html>';
expect(() => {
IframeHelper.listen(iframe, 'test', () => {});
}).to.throw('only iframes with src supported');
});

it('should assert iframe is detached', () => {
const iframe = container.doc.createElement('iframe');
iframe.src = iframeSrc;
insert(iframe);
expect(() => {
IframeHelper.listen(iframe, 'test', () => {});
}).to.throw('cannot register events on an attached iframe');
});

it('should listen to iframe messages', () => {
const removeEventListenerSpy = sandbox.spy(container.win,
Expand All @@ -50,6 +69,7 @@ describe('iframe-helper', function() {
return new Promise(resolve => {
unlisten = IframeHelper.listen(testIframe, 'send-intersections',
resolve);
insert(testIframe);
}).then(() => {
expect(removeEventListenerSpy.callCount).to.equal(0);
unlisten();
Expand All @@ -62,6 +82,7 @@ describe('iframe-helper', function() {
'removeEventListener');
return new Promise(resolve => {
IframeHelper.listenOnce(testIframe, 'send-intersections', resolve);
insert(testIframe);
}).then(() => {
expect(removeEventListenerSpy.callCount).to.equal(1);
});
Expand All @@ -71,7 +92,6 @@ describe('iframe-helper', function() {
const removeEventListenerSpy = sandbox.spy(container.win,
'removeEventListener');
IframeHelper.listen(testIframe, 'send-intersections', function() {});
testIframe.parentElement.removeChild(testIframe);
expect(removeEventListenerSpy.callCount).to.equal(0);
container.win.postMessage('hello world', '*');
expect(removeEventListenerSpy.callCount).to.equal(0);
Expand All @@ -81,6 +101,7 @@ describe('iframe-helper', function() {
});

it('should set sentinel on postMessage data', () => {
insert(testIframe);
postMessageSpy = sinon/*OK*/.spy(testIframe.contentWindow, 'postMessage');
IframeHelper.postMessage(
testIframe, 'testMessage', {}, 'http://google.com');
Expand Down
17 changes: 13 additions & 4 deletions test/functional/test-intersection-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,13 @@ describe('IntersectionObserver', () => {
function getIframe(src) {
const i = document.createElement('iframe');
i.src = src;
document.body.appendChild(i);
return i;
}

function insert(iframe) {
document.body.appendChild(iframe);
}

beforeEach(() => {
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
Expand Down Expand Up @@ -221,20 +224,22 @@ describe('IntersectionObserver', () => {
});

it('should not send intersection', () => {
postMessageSpy = sinon/*OK*/.spy(testIframe.contentWindow, 'postMessage');
const ioInstance = new IntersectionObserver(element, testIframe);
insert(testIframe);
postMessageSpy = sinon/*OK*/.spy(testIframe.contentWindow, 'postMessage');
ioInstance.sendElementIntersection_();
expect(postMessageSpy.callCount).to.equal(0);
expect(ioInstance.pendingChanges_).to.have.length(0);
});

it('should send intersection', () => {
const messages = [];
const ioInstance = new IntersectionObserver(element, testIframe);
insert(testIframe);
sandbox.stub(testIframe.contentWindow, 'postMessage', message => {
// Copy because arg is modified in place.
messages.push(JSON.parse(JSON.stringify(message)));
});
const ioInstance = new IntersectionObserver(element, testIframe);
clock.tick(33);
ioInstance.startSendingIntersectionChanges_();
expect(getIntersectionChangeEntrySpy.callCount).to.equal(1);
Expand All @@ -251,11 +256,12 @@ describe('IntersectionObserver', () => {

it('should send more intersections', () => {
const messages = [];
const ioInstance = new IntersectionObserver(element, testIframe);
insert(testIframe);
sandbox.stub(testIframe.contentWindow, 'postMessage', message => {
// Copy because arg is modified in place.
messages.push(JSON.parse(JSON.stringify(message)));
});
const ioInstance = new IntersectionObserver(element, testIframe);
ioInstance.startSendingIntersectionChanges_();
expect(getIntersectionChangeEntrySpy.callCount).to.equal(1);
expect(messages).to.have.length(0);
Expand Down Expand Up @@ -287,6 +293,7 @@ describe('IntersectionObserver', () => {
it('should init listeners when element is in viewport', () => {
const fireSpy = sandbox.spy(IntersectionObserver.prototype, 'fire');
const ioInstance = new IntersectionObserver(element, testIframe);
insert(testIframe);
ioInstance.onViewportCallback(true);
expect(fireSpy.callCount).to.equal(1);
expect(onScrollSpy.callCount).to.equal(1);
Expand All @@ -297,6 +304,7 @@ describe('IntersectionObserver', () => {
it('should unlisten listeners when element is out of viewport', () => {
const fireSpy = sandbox.spy(IntersectionObserver.prototype, 'fire');
const ioInstance = new IntersectionObserver(element, testIframe);
insert(testIframe);
ioInstance.onViewportCallback(true);
ioInstance.onViewportCallback();
expect(fireSpy.callCount).to.equal(2);
Expand All @@ -306,6 +314,7 @@ describe('IntersectionObserver', () => {
it('should go into in-viewport state for initially visible element', () => {
element.isInViewport = () => true;
const ioInstance = new IntersectionObserver(element, testIframe);
insert(testIframe);
ioInstance.startSendingIntersectionChanges_();
expect(getIntersectionChangeEntrySpy.callCount).to.equal(2);
expect(onScrollSpy.callCount).to.equal(1);
Expand Down