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

Add AMP.navigateTo action #9932

Merged
merged 8 commits into from
Jun 16, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/standard-actions.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
</head>
<body>

<div class="button-set">
<h3>AMP.navigateTo()</h3>
<button on="tap:AMP.navigateTo(url='http://google.com')">google.com</button>
</div>

<div class="button-set">
<h3>amp-img:</h3>
<button on="tap:img-on-viewport.show">Show</button>
Expand Down
6 changes: 5 additions & 1 deletion src/document-submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import {ActionTrust} from './action-trust';
import {actionServiceForDoc} from './services';
import {dev, user} from './log';
import {
Expand Down Expand Up @@ -128,6 +129,9 @@ export function onDocumentFormSubmit_(e) {
// handling of the event in cases were we are delegating to action service
// to deliver the submission event.
e.stopImmediatePropagation();
actionServiceForDoc(form).execute(form, 'submit', /*args*/ null, form, e);

const actions = actionServiceForDoc(form);
// TODO(choumx, #9699): HIGH.
actions.execute(form, 'submit', /*args*/ null, form, e, ActionTrust.MEDIUM);
}
}
13 changes: 5 additions & 8 deletions src/service/action-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,7 @@ export class ActionService {
this.root_.addEventListener('click', event => {
if (!event.defaultPrevented) {
const element = dev().assertElement(event.target);
// TODO(choumx, #9699): HIGH.
this.trigger(element, name, event, ActionTrust.MEDIUM);
this.trigger(element, name, event, ActionTrust.HIGH);
}
});
this.root_.addEventListener('keydown', event => {
Expand All @@ -229,8 +228,7 @@ export class ActionService {
if (!event.defaultPrevented &&
element.getAttribute('role') == 'button') {
event.preventDefault();
// TODO(choumx, #9699): HIGH.
this.trigger(element, name, event, ActionTrust.MEDIUM);
this.trigger(element, name, event, ActionTrust.HIGH);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jridgewell FYI I think this is the correct trust level for clicking a button via keyboard.

}
}
});
Expand All @@ -245,7 +243,7 @@ export class ActionService {
const element = dev().assertElement(event.target);
// Only `change` events from <select> elements have high trust.
const trust = element.tagName == 'SELECT'
? ActionTrust.MEDIUM // TODO(choumx, #9699): HIGH.
? ActionTrust.HIGH
: ActionTrust.MEDIUM;
this.addInputDetails_(event);
this.trigger(element, name, event, trust);
Expand Down Expand Up @@ -341,10 +339,9 @@ export class ActionService {
* @param {?JSONType} args
* @param {?Element} source
* @param {?ActionEventDef} event
* @param {ActionTrust} trust
*/
execute(target, method, args, source, event) {
// Invocation of actions by the runtime has the highest trust of all.
const trust = ActionTrust.MEDIUM; // TODO(choumx, #9699): HIGH.
execute(target, method, args, source, event, trust) {
this.invoke_(target, method, args, source, event, trust, null);
}

Expand Down
31 changes: 28 additions & 3 deletions src/service/standard-actions-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
import {ActionTrust} from '../action-trust';
import {OBJECT_STRING_ARGS_KEY} from '../service/action-impl';
import {Layout, getLayoutClass} from '../layout';
import {actionServiceForDoc} from '../services';
import {actionServiceForDoc, urlReplacementsForDoc} from '../services';
import {bindForDoc} from '../services';
import {computedStyle, getStyle, toggle} from '../style';
import {dev, user} from '../log';
import {registerServiceBuilderForDoc} from '../service';
import {historyForDoc} from '../services';
import {isProtocolValid} from '../url';
import {registerServiceBuilderForDoc} from '../service';
import {resourcesForDoc} from '../services';
import {computedStyle, getStyle, toggle} from '../style';
import {vsyncFor} from '../services';

/**
Expand Down Expand Up @@ -55,6 +56,9 @@ export class StandardActions {
/** @const @private {!./resources-impl.Resources} */
this.resources_ = resourcesForDoc(ampdoc);

/** @const @private {!./url-replacements-impl.UrlReplacements} */
this.urlReplacements_ = urlReplacementsForDoc(ampdoc);

this.installActions_(this.actions_);
}

Expand Down Expand Up @@ -87,6 +91,9 @@ export class StandardActions {
case 'setState':
this.handleAmpSetState_(invocation);
return;
case 'navigateTo':
this.handleAmpNavigateTo_(invocation);
return;
case 'goBack':
this.handleAmpGoBack_(invocation);
return;
Expand Down Expand Up @@ -121,6 +128,24 @@ export class StandardActions {
});
}

/**
* @param {!./action-impl.ActionInvocation} invocation
* @private
*/
handleAmpNavigateTo_(invocation) {
if (!invocation.satisfiesTrust(ActionTrust.HIGH)) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a user().error() log here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already happens inside satisfiesTrust.

}
const url = invocation.args['url'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use isProtocolValid from url.js to assert that url is not dangerous (also a test for navigateTo(javascript:) to ensure the validation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would an error message here be better, or would you prefer it silently fail?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Added a user error.

if (!isProtocolValid(url)) {
return;
}
const expandedUrl = this.urlReplacements_.expandUrlSync(url);
const node = invocation.target;
const win = (node.ownerDocument || node).defaultView;
win.location = expandedUrl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we wanna take an optional target?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For nested iframes? I haven't heard this particular FR and it can be accomplished by binding to iframe[src] with amp-bind. We could extend this to myIframeId.navigateTo(url=) if requested.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly was thinking about blank rather than nested case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Looks like window.open() has a few additional params and doesn't support opening in new tabs. If it's alright with you, I'd like to defer this until we receive a FR so we can spec it appropriately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sgtm

}

/**
* @param {!./action-impl.ActionInvocation} invocation
* @private
Expand Down
46 changes: 42 additions & 4 deletions test/functional/test-standard-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,44 @@ describes.sandboxed('StandardActions', {}, () => {
});

describe('"AMP" global target', () => {
it('should implement navigateTo', () => {
const expandUrlStub = sandbox.stub(standardActions.urlReplacements_,
'expandUrlSync', url => url);

const win = {
location: 'http://foo.com',
};
const invocation = {
method: 'navigateTo',
args: {
url: 'http://bar.com',
},
target: {
ownerDocument: {
defaultView: win,
},
},
};

// Should check trust and fail.
invocation.satisfiesTrust = () => false;
standardActions.handleAmpTarget(invocation);
expect(win.location).to.equal('http://foo.com');
expect(expandUrlStub).to.not.be.called;

// Should succeed.
invocation.satisfiesTrust = () => true;
standardActions.handleAmpTarget(invocation);
expect(win.location).to.equal('http://bar.com');
expect(expandUrlStub.calledOnce);

// Invalid protocols should fail.
invocation.args.url = /*eslint no-script-url: 0*/ 'javascript:alert(1)';
standardActions.handleAmpTarget(invocation);
expect(win.location).to.equal('http://bar.com');
expect(expandUrlStub.calledOnce);
});

it('should implement goBack', () => {
installHistoryServiceForDoc(ampdoc);
const history = historyForDoc(ampdoc);
Expand All @@ -173,10 +211,10 @@ describes.sandboxed('StandardActions', {}, () => {
});

it('should implement setState', () => {
const setStateWithExpressionSpy = sandbox.spy();
const spy = sandbox.spy();
window.services.bind = {
obj: {
setStateWithExpression: setStateWithExpressionSpy,
setStateWithExpression: spy,
},
};

Expand All @@ -191,8 +229,8 @@ describes.sandboxed('StandardActions', {}, () => {
};
standardActions.handleAmpTarget(invocation);
return bindForDoc(ampdoc).then(() => {
expect(setStateWithExpressionSpy).to.be.calledOnce;
expect(setStateWithExpressionSpy).to.be.calledWith('{foo: 123}');
expect(spy).to.be.calledOnce;
expect(spy).to.be.calledWith('{foo: 123}');
});
});
});
Expand Down