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

XHR fetch when view first becomes visible #14837

Merged
merged 16 commits into from May 3, 2018
Merged

XHR fetch when view first becomes visible #14837

merged 16 commits into from May 3, 2018

Conversation

alabiaga
Copy link
Contributor

Fixes #14685

@@ -170,30 +167,30 @@ export class Xhr {
* XHRs are intercepted if all of the following are true:
* - The AMP doc is in single doc mode
* - The viewer has the `xhrInterceptor` capability
* - The Viewer is a trusted viewer or AMP is currently in developement mode
* - The Viewer is a trusted viewer or ApMP is currently in developement mode
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: AMP, looks like a typo!

@@ -14,7 +14,7 @@
* limitations under the License.
*/

import * as sinon from 'sinon';
import * as sinon from 'sinon';;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Double semi-colon?

});
ampdocServiceForStub.returns({
isSingleDoc: () => false,
getAmpDoc: () => { return ampdocViewerStub; },
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a huge deal since this is a test file however I believe you could reduce this down a bit.

ampdocServiceForStub.returns({
  isSingleDoc: () => false,
  getAmpDoc: () => ampdocViewerStub,
});

@@ -73,7 +75,17 @@ describe.configure().skipSafari().run('XHR', function() {
beforeEach(() => {
sandbox = sinon.sandbox.create();
ampdocServiceForStub = sandbox.stub(Services, 'ampdocServiceFor');
ampdocServiceForStub.returns({isSingleDoc: () => false});
ampdocViewerStub = sandbox.stub(Services, 'viewerForDoc');
ampdocViewerStub.returns({
Copy link
Contributor

Choose a reason for hiding this comment

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

Also here:

ampdocViewerStub.returns({
  whenFirstVisible: () => Promise.resolve(),
});

@alabiaga
Copy link
Contributor Author

@jridgewell sending out for early review. i just modified the tests so that existing tests now pass but I need to create a new test as well.

/** @private {?./ampdoc-impl.AmpDoc} */
this.ampdocSingle_ =
ampdocService.isSingleDoc() ? ampdocService.getAmpDoc() : null;
this.ampdocService = Services.ampdocServiceFor(win);
Copy link
Contributor

Choose a reason for hiding this comment

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

The isSingleDoc is still necessary (context).

Also, leave this as a private field, they get minified better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hey justin, i know we discussed this a bit yesterday but it might have been under a different context. Do we really need this private field. in my changes you can see that i check if amp is single doc in the fetch call and pass along that info to the methods that need that info. I dont think this private field is needed at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

Leaving this as a private is still valuable.

!Services.viewerForDoc(this.ampdocSingle_).hasBeenVisible()) {
dev().error('XHR', 'attempted to fetch %s before viewer was visible',
parseUrl(input).origin != this.win.location.origin) {
dev().error('XHR', 'attempted to fetch URL from different origin',
input);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This entire if statement can be removed, since we're now waiting for viewer visible regardless.

});
return Services.viewerForDoc(ampdocSingle).whenFirstVisible()
.then(() => {
return this.maybeIntercept_(input, init, ampdocSingle);
Copy link
Contributor

Choose a reason for hiding this comment

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

@zhangsu: What are the privacy guarantees of the Gmail interceptor? Does it need us to wait for the doc to be shown?

(Basically, should this be the first call, or should whenFirstVisible be first)

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for checking with us. Waiting for the email to be shown before firing XHRs sounds good to me since we also want the same level of privacy.

Just curious: what's an example today where XHRs can be fired before the viewer is visible (e.g. in prerender mode)? amp-list shouldn't do that because prerenderAllowed is false, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

👍, that makes it safer on our side as well.

what's an example today where XHRs can be fired before the viewer is visible (e.g. in prerender mode)?

Generally, these are bugs. There are exceptions for connecting to the CDN itself (or whatever origin is hosting the page), since they've already received the document request.

amp-list shouldn't do that because prerenderAllowed is false, right?

Correct. The prerenderAllowed is one way to catch them, but we're increasingly getting extensions that need to make requests before layout (ads 🙄, and one other). These make requests in #buildCallback or other earlier lifecycle callbacks, and they sometimes forget to wait for visible.

@alabiaga
Copy link
Contributor Author

alabiaga commented May 2, 2018

@jridgewell Hey Justin, pls take another look. I made changes to make the check for viewer visibility to encapsulate the logic where other viewer related checks are made as well and ensured that the fetch happens at all times which wasn't the case in my initial change.

/** @private {?./ampdoc-impl.AmpDoc} */
this.ampdocSingle_ =
ampdocService.isSingleDoc() ? ampdocService.getAmpDoc() : null;
this.ampdocService = Services.ampdocServiceFor(win);
Copy link
Contributor

Choose a reason for hiding this comment

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

Leaving this as a private is still valuable.

// getAmpDoc.
// TODO(alabiaga): This should be investigated and fixed.
const ampdoc = this.ampdocService.isSingleDoc() ?
this.ampdocService.getAmpDoc(this.win.document) : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

The this.win.document is unnecessary for single docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. The reason I added it was because I was trying to do away with the isSingleDoc() check..and just make the call getAmpDoc..which was then causing test-bind-impl.js to fail and I was trying to get it to work by passing in the node, though it was not sufficient as the AmpDoc.shellShadowDoc_ was not defined. I wrote a TODO on that line of code as it seems like buggy code to me..or the test is not set up properly. So i believe this is where your initial comment of why this is still required now makes sense...i had to actually debug through the code for it to actually make sense.

}
return (this.win.fetch || fetchPolyfill).apply(null, arguments);
});
return this.maybeIntercept_(input, init, ampdoc)
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to pass in ampdoc since it's a property on this. Just access it in the method, and remove its uses in this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed to use the assigned property ampdocSingle_ as it was originally.

maybeIntercept_(input, init) {
if (!this.ampdocSingle_) {
maybeIntercept_(input, init, ampdoc) {
if (!this.ampdocService.isSingleDoc()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Look at that, we're already accessing it as the property. 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fine you win. reverted to a field property 😄

return Promise.resolve();
}

const htmlElement = this.ampdocSingle_.getRootNode().documentElement;
const htmlElement = ampdoc.getRootNode().documentElement;
const docOptedIn = htmlElement.hasAttribute('allow-xhr-interception');
if (!docOptedIn) {
return Promise.resolve();
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: We still need to wait for viewer visible in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

if (!viewer.hasCapability('xhrInterceptor')) {
return Promise.resolve();
}
return viewer.isTrustedViewer().then(viewerTrusted => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: we can unnest this if we take extract out the "hasCapability" into a separate path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unnested hasCapability and docOptedIn checked

@@ -109,7 +109,15 @@ export class Xhr {
/** @const {!Window} */
this.win = win;

/** @private */
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a property, so can't be @private.

}
return viewer.isTrustedViewer();
}).then(viewerTrusted => {
if (!viewerTrusted && !getMode(this.win).development) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, this lead to confusion when I recommended unnesting. What I meant is that these should be two separate promise chains:

// Shorthand, don't actually write like this
maybeIntercept_(input, init) {
  if (!single) return Promise.resolve();

  const vis = viewer.whenFirstVisible();
  if (!hasCapability) return vis;

  return vis.then(() => {
    if (!optedIn) {
      return false;
    }
    return viewer.isTrusted()
  }).then(trusted => {
    if (!trusted) return;
    return sendMessage(...);
  });
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jridgewell no worries. hopefully i got it right this time. otherwise we can discuss in person. thanks

const docOptedIn = htmlElement.hasAttribute('allow-xhr-interception');
const isDevMode = getMode(this.win).development;
if (!docOptedIn) {
return Promise.resolve();
Copy link
Contributor

Choose a reason for hiding this comment

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

Return whenFirstVisible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right. Done. Thanks

@alabiaga alabiaga merged commit bdbb745 into ampproject:master May 3, 2018
noranazmy pushed a commit to noranazmy/amphtml that referenced this pull request May 10, 2018
* fetch when viewer is visible

* fetch by default when viewer first becomes visible

* fix typo

* fixes from kris' comments

* more fixes from kris' comments

* add test

* address jridgewell comments

* fix usage of spy

* fix xhr, to resolve earlier if not a single doc

* fix per justin comment.

* minor refactor

* get ampdoc fetch with isSingleDoc check to fix dev error thrown when fetching the amp doc. for test test-bind-impl

* address jridgewell comments

* address jridgewell comments

* address comments

* return promise fix
@jridgewell jridgewell added this to To do in Make Malte, Dima, Justin Happy via automation Sep 6, 2018
@jridgewell jridgewell moved this from To do to XSS Injections Caught in Make Malte, Dima, Justin Happy Sep 6, 2018
@jridgewell jridgewell moved this from XSS Injections Caught to Reviewed in Make Malte, Dima, Justin Happy Sep 6, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Development

Successfully merging this pull request may close these issues.

None yet

5 participants