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

Reacting to changes in storage #184

Open
jakearchibald opened this issue Apr 28, 2020 · 10 comments
Open

Reacting to changes in storage #184

jakearchibald opened this issue Apr 28, 2020 · 10 comments
Labels
design work needed An acknowledged gap in the proposal that needs design work to resolve

Comments

@jakearchibald
Copy link
Collaborator

Cross-origin portals will have no access to storage while portaled. However, they are eligible for first-party storage once activated.

But this creates the following new situation:

  1. jakearchibald.com contains a portal to social.example.com. Due to a lack of storage access, social.example.com appears logged out, or even broken.

This is fine. Iframes already behave in a similar way in Safari. The URL bar says jakearchibald.com, so any broken UI should seem the responsibility of jakearchibald.com, not social.example.com.

  1. The portal to social.example.com is activated.

This is problematic. The URL bar now says social.example.com, and unless social.example.com takes steps to update their page in reaction to the change in storage, the experience will be broken, and reflect badly on social.example.com.

This is a new capability. Until now, there was no way for origin A to load origin B as a top-level navigation without credentials. Similar things, like strict SameSite cookies, are opt-in from origin B.

@jakearchibald
Copy link
Collaborator Author

jakearchibald commented Apr 28, 2020

Potential solution:

With a cross origin portal activation from origin A to origin B, origin B must signal that it recognises the situation, and is prepared to handle it. Otherwise, on activation, origin B will be reloaded.

addEventListener('portalactivate', async (event) => {
  if (!event.hasStorageAccess) {
    // The page doesn't have storage access, meaning it was a cross-origin activation.
    // By default, this page will reload, unless…
    await document.requestStorageAccess();
    // If storage access is granted (if it's top-level, it'll be granted without permission UI)
    // the page won't be reloaded.
  }
});

requestStorageAccess needs to be called within the dispatch of the event to prevent the auto-reload, which is why I've made hasStorageAccess available synchronously.

Alternatively, if we can't integrate with requestStorageAccess:

addEventListener('portalactivate', (event) => {
  if (event.storageChanged) {
    // The page didn't have storage access, but now it does, meaning it was a cross-origin activation.
    // By default, this page will reload, unless…
    event.preventDefault();
  }
});

As usual, event.preventDefault() needs to be called within the dispatch of the event.

@jeremyroman
Copy link
Collaborator

requestStorageAccess needs to be called within the dispatch of the event to prevent the auto-reload, which is why I've made hasStorageAccess available synchronously.

Having document.requestStorageAccess invoke magical behavior related to portalactivate event dispatch seems kinda mysterious. If we need this, I think I prefer something along the lines of:

  1. The preventDefault trick you mention (though it seems non-obvious to me that reload would be the default behavior).

  2. Have it declare some kind of upfront policy (the resolution of Per-Frame or Per-Page Storage Access privacycg/storage-access#3 is interesting here and should probably shape our thinking). Strawman:

<meta name="storage-access" content="on-upgrade=none">

The on-upgrade directive would have these possible values:

  • none: do nothing implicitly when storage entitlement is upgraded; storage access continues to fail until requestStorageAccess is called (except fire an event to notify script)
  • silent: immediately change the storage flag as though requestStorageAccess had been called
  • refresh: refresh the document
  • auto (the default): computes to refresh if the document is top-level, and silent otherwise

@jakearchibald
Copy link
Collaborator Author

Having document.requestStorageAccess invoke magical behavior related to portalactivate event dispatch seems kinda mysterious.

Is it magic? The rule is "if you remain third party after activation, your page will be reloaded", and document.requestStorageAccess is the current mechanism for going from third party to first party.

  1. The preventDefault trick you mention (though it seems non-obvious to me that reload would be the default behavior).

Yeah I feel the same. I guess there could be another method on the event object? But then, it feels like it'd just be the same as document.requestStorageAccess.

<meta name="storage-access" content="on-upgrade=none">

I'm less keen on solutions that separate declaring an action will be performed, and performing the action.

For example:

<meta name="storage-access" content="on-upgrade=silent">
<script>
addEventListener('portalactivate', (event) => {
  if (event.storageChanged) {
    updateUI(appConfig.user?.whatever);
  }
});
</script>

Here, the app has declared it'll handle the storage change, whereas the script that actually handles it may hit a parse error in some browsers due to user?.. This leaves the page in a broken state. This could also happen if the script just fails to load.

addEventListener('portalactivate', async (event) => {
  if (!event.hasStorageAccess) {
    await document.requestStorageAccess();
    updateUI(appConfig.user?.whatever);
  }
});

In this case, if the script fails to load, or it hits a parse error, the declaration of "I can handle this situation" is also lost, so the page refreshes.

@domenic
Copy link
Collaborator

domenic commented May 5, 2020

Taking a step back: to me, if a portal reloads on activation, then it's not really a portal. It's a restricted iframe with a convenience method for doing location.href = iframe.src, roughly speaking. As such, I'm hesitant about introducing any solution where portals, by default, behave like this.

Instead, my takeaway from

This is a new capability. Until now, there was no way for origin A to load origin B as a top-level navigation without credentials. Similar things, like strict SameSite cookies, are opt-in from origin B.

is that it would make the most sense for cross-origin portals to be completely opt-in. That is, unless the cross-origin page provides the appropriate header, attempting to portal it would be a navigation error.

In other words, if a site hasn't explicitly allowed themselves to be portaled, then you should really just iframe them, not portal them.

@jakearchibald
Copy link
Collaborator Author

Taking a step back: to me, if a portal reloads on activation, then it's not really a portal. It's a restricted iframe with a convenience method for doing location.href = iframe.src

Yeah, pretty much.

unless the cross-origin page provides the appropriate header, attempting to portal it would be a navigation error.

I'm still unsure about solutions that separate declaring an action will be performed, and performing the action. Also, headers seem to be a huge barrier to entry since they can't be set on some static hosts.

But yeah, it seems better to fail early than force a reload.

@jeremyroman
Copy link
Collaborator

Maybe. It seems kinda frustrating because it means that no existing content works, even trivial static documents. And then there's the question of how they would opt-in. Would it be a response header evaluated at load time, or a <meta> tag evaluated at activate time? It's my understanding that response headers are still difficult to deploy in many web deployment environments by small to medium size development shops (e.g. bucket of stuff dumped into IIS/Apache/etc public_html).

@domenic
Copy link
Collaborator

domenic commented May 5, 2020

Well, I guess I'm confused as to whether it's a goal for existing content to work or not. The OP points out that this might be undesired, since then origin A can load origin B as a top-level navigation without credentials.

If we think it's desirable to allow existing content to work despite this drawback, then that does change the design space. But I think it also means we should stop considering designs like the OP's which cause any existing content to be reloaded.

@jeremyroman
Copy link
Collaborator

I think it's obviously desirable, but perhaps non-critical. If we want to drop that, then we should think about whether we can make this detectable in some way, or whether we are expecting authors to know in advance whether the content they are referring to can be loaded in a portal, bearing in mind that this might change. And then if it fails do we want it to fail into doing nothing, or fail into holding onto the request URL until activation and then using that to do an ordinary navigation then (so that preload scenarios degrade into navigation instead of brokenness, at a cost of the API looking kinda weird).

I agree that implicit reload is not a great behavior to have if we can avoid it.

@jakearchibald
Copy link
Collaborator Author

fwiw, this is why I keep pushing a "navigate" event. Since the developer is hooking into an actual navigation, and the portal is somewhat browser-controlled, the portal can be created with first party access, even if it's cross-origin. It just works with existing content.

You'd still need some kind of opt-in for the cross-origin prerender use-case, where the portal is created before the navigation.

@jeremyroman
Copy link
Collaborator

@jakearchibald summarized some thoughts on the ephemeral/deadlined portal idea in #192 since they're slightly separate from this issue (though of course everything is intertwined); apologies if I didn't do it justice or missed something critical

@domenic domenic added the design work needed An acknowledged gap in the proposal that needs design work to resolve label May 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design work needed An acknowledged gap in the proposal that needs design work to resolve
Projects
None yet
Development

No branches or pull requests

3 participants