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 all 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
11 changes: 11 additions & 0 deletions examples/standard-actions.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@
</head>
<body>

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

<select on="change:AMP.navigateTo(url=event.value)">
<option value="http://google.com">google.com</option>
<option value="http://yahoo.com">yahoo.com</option>
<option value="http://bing.com">bing.com</option>
</select>
</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);
}
}
19 changes: 8 additions & 11 deletions src/service/action-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import {
installServiceInEmbedScope,
} from '../service';
import {getMode} from '../mode';
import {hasOwn, map} from '../utils/object';
import {isArray, isFiniteNumber} from '../types';
import {map} from '../utils/object';
import {timerFor} from '../services';
import {vsyncFor} from '../services';

Expand Down 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 Expand Up @@ -774,8 +771,8 @@ function getActionInfoArgValue(tokens) {
let current = data;
// Traverse properties of `data` per token values.
for (let i = 0; i < tokens.length; i++) {
const value = tokens[i].value;
if (current && current.hasOwnProperty(value)) {
const value = String(tokens[i].value);
if (current && hasOwn(current, value)) {
current = current[value];
} else {
return null;
Expand Down
39 changes: 34 additions & 5 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 All @@ -35,6 +36,9 @@ function isShowable(element) {
|| element.hasAttribute('hidden');
}

/** @const {string} */
const TAG = 'STANDARD-ACTIONS';

/**
* This service contains implementations of some of the most typical actions,
* such as hiding DOM elements.
Expand All @@ -55,6 +59,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 +94,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 +131,25 @@ 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)) {
user().error(TAG, 'Cannot navigate to invalid protocol: ' + url);
return;
}
const expandedUrl = this.urlReplacements_.expandUrlSync(url);
const node = invocation.target;
const win = (node.ownerDocument || node).defaultView;
win.location = expandedUrl;
}

/**
* @param {!./action-impl.ActionInvocation} invocation
* @private
Expand Down Expand Up @@ -161,7 +190,7 @@ export class StandardActions {

if (target.classList.contains(getLayoutClass(Layout.NODISPLAY))) {
user().warn(
'STANDARD-ACTIONS',
TAG,
'Elements with layout=nodisplay cannot be dynamically shown.',
target);

Expand All @@ -173,7 +202,7 @@ export class StandardActions {
!isShowable(target)) {

user().warn(
'STANDARD-ACTIONS',
TAG,
'Elements can only be dynamically shown when they have the ' +
'"hidden" attribute set or when they were dynamically hidden.',
target);
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