diff --git a/extensions/amp-slikeplayer/0.1/amp-slikeplayer.js b/extensions/amp-slikeplayer/0.1/amp-slikeplayer.js index 028cb4e9cad9..37613a05fb71 100644 --- a/extensions/amp-slikeplayer/0.1/amp-slikeplayer.js +++ b/extensions/amp-slikeplayer/0.1/amp-slikeplayer.js @@ -1,5 +1,10 @@ import {Deferred} from '#core/data-structures/promise'; import {dispatchCustomEvent} from '#core/dom'; +import { + fullscreenEnter, + fullscreenExit, + isFullscreenElement, +} from '#core/dom/fullscreen'; import {isLayoutSizeDefined} from '#core/dom/layout'; import {observeIntersections} from '#core/dom/layout/viewport-observer'; import {once} from '#core/types/function'; @@ -8,7 +13,7 @@ import {Services} from '#service'; import {installVideoManagerForDoc} from '#service/video-manager-impl'; import {getData, listen} from '#utils/event-helper'; -import {userAssert} from '#utils/log'; +import {dev, userAssert} from '#utils/log'; import {getConsentDataToForward} from '../../../src/consent'; import {disableScrollingOnIframe} from '../../../src/iframe-helper'; @@ -87,6 +92,9 @@ export class AmpSlikeplayer extends AMP.BaseElement { /** @private {string} */ this.baseUrl_ = 'https://tvid.in/player/amp.html'; + /** @private {string} Origin of the player iframe; target for postMessage. */ + this.targetOrigin_ = '*'; + /** @private {number} */ this.duration_ = 1; @@ -127,6 +135,16 @@ export class AmpSlikeplayer extends AMP.BaseElement { this.config_ = element.getAttribute('data-config') || ''; this.poster_ = element.getAttribute('poster') || ''; + // Resolve the player origin so messages target it explicitly rather than '*'. + try { + this.targetOrigin_ = new URL( + this.baseUrl_, + this.win.location.href + ).origin; + } catch { + this.targetOrigin_ = '*'; + } + // Read optional viewport visibility threshold from data-config if (this.config_) { try { @@ -181,10 +199,10 @@ export class AmpSlikeplayer extends AMP.BaseElement { /** @override */ layoutCallback() { - let src = `${this.baseUrl_}#apikey=${this.apikey_}&videoid=${this.videoid_}&baseurl=${this.win.location.origin}`; + let src = `${this.baseUrl_}#apikey=${this.apikey_}&videoid=${this.videoid_}&=1&baseurl=${this.win.location.origin}`; if (this.config_) { - src = `${this.baseUrl_}#apikey=${this.apikey_}&videoid=${this.videoid_}&${this.config_}&baseurl=${this.win.location.origin}`; + src = `${this.baseUrl_}#apikey=${this.apikey_}&videoid=${this.videoid_}&${this.config_}&=1&baseurl=${this.win.location.origin}`; } const frame = disableScrollingOnIframe( @@ -241,6 +259,40 @@ export class AmpSlikeplayer extends AMP.BaseElement { return false; } + /** @override */ + fullscreenEnter() { + if (!this.iframe_) { + return; + } + fullscreenEnter(dev().assertElement(this.iframe_)); + } + + /** @override */ + fullscreenExit() { + if (!this.iframe_) { + return; + } + fullscreenExit(dev().assertElement(this.iframe_)); + } + + /** @override */ + isFullscreen() { + if (!this.iframe_) { + return false; + } + return isFullscreenElement(dev().assertElement(this.iframe_)); + } + + /** @override */ + showControls() { + this.postMessage_('showControls', ''); + } + + /** @override */ + hideControls() { + this.postMessage_('hideControls', ''); + } + /** @private */ onReady_() { const {element} = this; @@ -261,6 +313,15 @@ export class AmpSlikeplayer extends AMP.BaseElement { return; } + // Defense-in-depth: ignore messages from an unexpected origin when known. + if ( + this.targetOrigin_ !== '*' && + messageEvent.origin && + messageEvent.origin !== this.targetOrigin_ + ) { + return; + } + const messageData = getData(messageEvent); if (!isJsonOrObj(messageData)) { return; @@ -290,6 +351,9 @@ export class AmpSlikeplayer extends AMP.BaseElement { case 'fullscreen': break; case 'meta': + if (detail.duration) { + this.duration_ = detail.duration; + } break; case 'mute': break; @@ -298,6 +362,9 @@ export class AmpSlikeplayer extends AMP.BaseElement { case 'time': const {currentTime} = detail; this.currentTime_ = currentTime; + if (detail.duration) { + this.duration_ = detail.duration; + } break; case 'adTime': const {position} = detail; @@ -429,7 +496,7 @@ export class AmpSlikeplayer extends AMP.BaseElement { 'method': method, 'optParams': optParams, }), - '*' + this.targetOrigin_ ); }); } diff --git a/extensions/amp-slikeplayer/0.1/test/test-amp-slikeplayer.js b/extensions/amp-slikeplayer/0.1/test/test-amp-slikeplayer.js index 44384b3f696e..57d15b730957 100644 --- a/extensions/amp-slikeplayer/0.1/test/test-amp-slikeplayer.js +++ b/extensions/amp-slikeplayer/0.1/test/test-amp-slikeplayer.js @@ -77,6 +77,25 @@ describes.realWin( expect(src).to.contain('viewport=50'); }); + it('adds amp=1 to iframe src with and without data-config', async () => { + const {el: noConfig} = await buildPlayer({ + 'data-apikey': 'a', + 'data-videoid': 'b', + }); + expect(noConfig.querySelector('iframe').getAttribute('src')).to.contain( + 'amp=1' + ); + + const {el: withConfig} = await buildPlayer({ + 'data-apikey': 'a', + 'data-videoid': 'b', + 'data-config': 'autoplay=true', + }); + expect(withConfig.querySelector('iframe').getAttribute('src')).to.contain( + 'amp=1' + ); + }); + it('parses viewport threshold from percent and ratio', async () => { const {impl: implPct} = await buildPlayer({ 'data-config': 'viewport=150', @@ -214,6 +233,78 @@ describes.realWin( await el.layoutCallback(); }); + it('fullscreenEnter/Exit delegate to the iframe, guarded by null', async () => { + const {iframe, impl} = await buildPlayer(); + const enterSpy = (iframe.requestFullscreen = env.sandbox.spy()); + const exitSpy = (iframe.exitFullscreen = env.sandbox.spy()); + + impl.fullscreenEnter(); + expect(enterSpy).to.have.been.calledOnce; + + impl.fullscreenExit(); + expect(exitSpy).to.have.been.calledOnce; + + // No iframe: methods are no-ops and must not throw. + impl.iframe_ = null; + expect(() => impl.fullscreenEnter()).to.not.throw(); + expect(() => impl.fullscreenExit()).to.not.throw(); + }); + + it('isFullscreen returns false without an iframe and by default', async () => { + const {impl} = await buildPlayer(); + expect(impl.isFullscreen()).to.be.false; + impl.iframe_ = null; + expect(impl.isFullscreen()).to.be.false; + }); + + it('showControls/hideControls post the matching methods', async () => { + const {impl} = await buildPlayer(); + const postSpy = env.sandbox.spy(impl, 'postMessage_'); + impl.showControls(); + impl.hideControls(); + await Promise.resolve(); + const methods = postSpy.getCalls().map((c) => c.args[0]); + expect(methods).to.include('showControls'); + expect(methods).to.include('hideControls'); + }); + + it('updates duration from meta and time events', async () => { + const {iframe, impl} = await buildPlayer(); + impl.onMessage_({ + source: iframe.contentWindow, + data: JSON.stringify({event: 'meta', detail: {duration: 120}}), + }); + expect(impl.getDuration()).to.equal(120); + + impl.onMessage_({ + source: iframe.contentWindow, + data: JSON.stringify({ + event: 'time', + detail: {currentTime: 5, duration: 200}, + }), + }); + expect(impl.getDuration()).to.equal(200); + }); + + it('ignores messages from an unexpected origin', async () => { + const {iframe, impl} = await buildPlayer(); + const initial = impl.getCurrentTime(); + impl.onMessage_({ + source: iframe.contentWindow, + origin: 'https://evil.example.com', + data: JSON.stringify({event: 'time', detail: {currentTime: 77}}), + }); + expect(impl.getCurrentTime()).to.equal(initial); + + // Matching origin is accepted. + impl.onMessage_({ + source: iframe.contentWindow, + origin: 'https://tvid.in', + data: JSON.stringify({event: 'time', detail: {currentTime: 88}}), + }); + expect(impl.getCurrentTime()).to.equal(88); + }); + it('calls sendConsentData_ on send-consent-data message', async () => { const consentData = { consentPolicyState: 1,