diff --git a/src/service/standard-actions-impl.js b/src/service/standard-actions-impl.js index 950fd62e9211..d2e82f424116 100644 --- a/src/service/standard-actions-impl.js +++ b/src/service/standard-actions-impl.js @@ -68,6 +68,17 @@ export class StandardActions { /** @const @private {!./viewport/viewport-impl.Viewport} */ this.viewport_ = Services.viewportForDoc(ampdoc); + // A meta[name="amp-action-whitelist"] tag, if present, contains, + // in its content attribute, a whitelist of actions on the special AMP target. + const meta = + this.ampdoc.getRootNode().head.querySelector('meta[name="amp-action-whitelist"]'); + // Cache the whitelist of allowed AMP actions (if provided). + if (meta) { + /** @const @private {!Array} */ + this.ampActionWhitelist_ = meta.getAttribute('content').split(',') + .map(action => action.trim()); + } + this.installActions_(this.actions_); } @@ -101,10 +112,14 @@ export class StandardActions { * @param {number=} opt_actionIndex * @param {!Array=} opt_actionInfos * @return {?Promise} - * @throws {Error} If action is not recognized. + * @throws {Error} If action is not recognized or is not whitelisted. */ handleAmpTarget(invocation, opt_actionIndex, opt_actionInfos) { const method = invocation.method; + if (this.ampActionWhitelist_ && + !this.ampActionWhitelist_.includes(method)) { + throw user().createError('AMP action', method, 'is not whitelisted'); + } switch (method) { case 'pushState': case 'setState': diff --git a/test/functional/test-standard-actions.js b/test/functional/test-standard-actions.js index 922fe1e1ff69..c08ac0a57f0d 100644 --- a/test/functional/test-standard-actions.js +++ b/test/functional/test-standard-actions.js @@ -20,6 +20,7 @@ import {StandardActions} from '../../src/service/standard-actions-impl'; import {Services} from '../../src/services'; import {installHistoryServiceForDoc} from '../../src/service/history-impl'; import {setParentWindow} from '../../src/service'; +import {createElementWithAttributes} from '../../src/dom'; describes.sandboxed('StandardActions', {}, () => { @@ -352,6 +353,64 @@ describes.sandboxed('StandardActions', {}, () => { standardActions.handleAmpTarget(invocation); expect(printStub).to.be.calledOnce; }); + + it('should not implement print when not whitelisted', () => { + window.document.head.appendChild( + createElementWithAttributes(window.document, 'meta', { + name: 'amp-action-whitelist', + content: 'pushState,setState', + })); + + standardActions = new StandardActions(ampdoc); + + const windowApi = { + print: () => {}, + }; + const printStub = sandbox.stub(windowApi, 'print'); + const invocation = { + method: 'print', + satisfiesTrust: () => true, + target: { + ownerDocument: { + defaultView: windowApi, + }, + }, + }; + expect(() => standardActions.handleAmpTarget(invocation)).to.throw(); + expect(printStub).to.not.be.called; + }); + + it('should implement pushState when whitelisted', () => { + window.document.head.appendChild( + createElementWithAttributes(window.document, 'meta', { + name: 'amp-action-whitelist', + content: 'setState, pushState', + })); + + standardActions = new StandardActions(ampdoc); + + const pushStateWithExpression = sandbox.stub(); + // Bind.pushStateWithExpression() doesn't resolve with a value, + // but add one here to check that the promise is chained. + pushStateWithExpression.returns(Promise.resolve('push-state-complete')); + + window.services.bind = { + obj: {pushStateWithExpression}, + }; + + const args = { + [OBJECT_STRING_ARGS_KEY]: '{foo: 123}', + }; + const target = ampdoc; + const satisfiesTrust = () => true; + const pushState = {method: 'pushState', args, target, satisfiesTrust}; + + return standardActions.handleAmpTarget(pushState, 0, []).then(result => { + expect(result).to.equal('push-state-complete'); + expect(pushStateWithExpression).to.be.calledOnce; + expect(pushStateWithExpression).to.be.calledWith('{foo: 123}'); + }); + }); }); describes.fakeWin('adoptEmbedWindow', {}, env => {