Skip to content

Commit

Permalink
Restrict navigate firing when another window initiates the navigation
Browse files Browse the repository at this point in the history
Closes #75, although this leaves open potential issues with history traversal from other documents which we'll track in #78.
  • Loading branch information
domenic authored Mar 19, 2021
1 parent 172985f commit 590f061
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 11 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,13 @@ There are many types of navigations a given page can experience; see [this appen
First, the following navigations **will not fire `navigate`** at all:
- User-initiated [cross-document](#appendix-types-of-navigations) navigations via browser UI, such as the URL bar, back/forward button, or bookmarks.
- [Cross-document](#appendix-types-of-navigations) navigations initiated from other [cross origin-domain](https://html.spec.whatwg.org/multipage/origin.html#same-origin-domain) windows, e.g. via `window.open(url, nameOfYourWindow)`, or clicking on `<a href="..." target="nameOfYourWindow">`
- [`document.open()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/open), which can strip off the fragment from the current document's URL.
Navigations of the first sort are outside the scope of the webpage, and can never be intercepted or prevented. This is true even if they are to same-origin documents, e.g. if the browser is currently displaying `https://example.com/foo` and the user edits the URL bar to read `https://example.com/bar` and presses enter. On the other hand, we do allow the page to intercept user-initiated _same_-document navigations via browser UI, e.g. if the the browser is currently displaying `https://example.com/foo` and the user edits the URL bar to read `https://example.com/foo#fragment` and presses enter.

Similarly, cross-document navigations initiated from other windows are not something that can be intercepted today, and for security reasons, we don't want to introduce the ability for your origin to mess with the operation of another origin's scripts. (Even if the purpose of those scripts is to navigate your frame.)

As for `document.open()`, it is a terrible legacy API with lots of strange side effects, which makes supporting it not worth the implementation cost. Modern sites which use the app history API should never be using `document.open()`.

Second, the following navigations **cannot be canceled** using `event.preventDefault()`, and as such will have `event.cancelable` equal to false:
Expand Down Expand Up @@ -1038,7 +1041,7 @@ Traversal operates on the joint session history, which means that it's possible
- If code in either the inner frame or the outer frame calls `history.back()`, this will take the joint session history back to C, and thus update the inner frame's current app history entry from I2 to I1. (There is no impact on the outer frame.)
Note that as currently planned, any such programmatic navigations, including ones originating from other frames, are [interceptable and cancelable](#navigation-monitoring-and-interception) as part of the `navigate` event part of the proposal.
Note that as currently planned, any such programmatic navigations, including ones originating from other frames, are [interceptable and cancelable](#navigation-monitoring-and-interception) as part of the `navigate` event part of the proposal. However, this will probably be revised; see [#78](https://github.com/WICG/app-history/issues/78).
### Integration with navigation
Expand Down Expand Up @@ -1115,9 +1118,10 @@ The web platform has many ways of initiating a navigation. For the purposes of t
- Bookmarks
- `<a>` and `<area>` elements (both directly by users, and programmatically via `element.click()` etc.)
- `<form>` elements (both directly by users, and programmatically via `element.submit()` etc.)
- As a special case of the above, the `target="nameOfSomeWindow"` attribute on `<a>`, `<area>`, and `<form>` will navigate a window whose `window.name` is `nameOfSomeWindow`
- `<meta http-equiv="refresh">`
- The `Refresh` HTTP response header
- The `window.location` setter, the various `location.*` setters, and the `location.replace()`, `location.assign()`, and `location.reload()` methods
- The `window.location` setter, the various `location.*` setters, and the `location.replace()`, `location.assign()`, and `location.reload()` methods. Note that these can be called from other frames, including cross-origin ones.
- Calling `window.open(url, nameOfSomeWindow)` will navigate a window whose `window.name` is `nameOfSomeWindow`
- `history.back()`, `history.forward()`, and `history.go()`
- `history.pushState()` and `history.replaceState()`
Expand All @@ -1144,26 +1148,28 @@ Here's a summary table:
|Browser UI (back/forward,<br>cross-document)|Cross|No|—|—|—|
|Browser UI (non-back/forward<br>fragment change only)|Same|Yes|Yes|Yes|Yes|
|Browser UI (non-back/forward<br>other)|Cross|No|—|—|—|
|`<a>`/`<area>`|Either|Yes|Yes ‡|Yes|Yes *|
|`<form>`|Either|Yes|Yes ‡|Yes|Yes *|
|`<a>`/`<area>`/`<form>` (`target="_self"` or no `target=""`)|Either|Yes|Yes ‡|Yes|Yes *|
|`<a>`/`<area>`/`<form>`<br>(non-`_self` `target=""`)|Either|Yes Δ|Yes ‡|Yes|Yes *|
|`<meta http-equiv="refresh">`|Either ◊|Yes|No|Yes|Yes *|
|`Refresh` header|Either ◊|Yes|No|Yes|Yes *|
|`window.location`|Either|Yes|No|Yes|Yes *|
|`window.open(url, name)`|Either|Yes|No|Yes|Yes *|
|`window.location`|Either|Yes Δ|No|Yes|Yes *|
|`history.{back,forward,go}()`|Either|Yes|No|Yes|Yes †*|
|`history.{pushState,replaceState}()`|Same|Yes|No|Yes|Yes|
|`appHistory.{back,forward,navigateTo}()`|Either|Yes|No|Yes|Yes †*|
|`appHistory.{push,update}()`|Either|Yes|No|Yes|Yes *|
|`window.open(url, "_self")`|Either|Yes|No|Yes|Yes *|
|`window.open(url, name)`|Either|Yes Δ|No|Yes|Yes *|
|`document.open()`|Same|No|—|—|—|
- † = No if cross-document
- ‡ = No if triggered via, e.g., `element.click()`
- \* = No if the URL differs in components besides path/fragment/query
- \* = No if the URL differs from the page's current one in components besides path/fragment/query
- Δ = No if cross-document and initiated from a [cross origin-domain](https://html.spec.whatwg.org/multipage/origin.html#same-origin-domain) window, e.g. `frames['cross-origin-frame'].location.href = ...` or `<a target="cross-origin-frame">`
- ◊ = fragment navigations initiated by `<meta http-equiv="refresh">` or the `Refresh` header are only same-document in some browsers: [whatwg/html#6451](https://github.com/whatwg/html/issues/6451)
See the discussion on [restrictions](#restrictions-on-firing-canceling-and-responding) to understand the reasons why the last few columns are filled out in the way they are.
Note that today it is not possible to intercept cases where other frames or windows programatically navigate your frame, e.g. via `window.open(url, name)`, or `history.back()` happening in a subframe. So, firing the `navigate` event and allowing interception in such cases represents a new capability. We believe this is OK, but will report back after some implementation experience.
Note that today it is not possible to intercept or cancel cases where `history.back()` in a subframe or outer frame causes your frame to navigate, but this proposal currently allows that. We may further restrict that per discussions in [#78](https://github.com/WICG/app-history/issues/78).
_Spec details: the above comprehensive list does not fully match when the HTML Standard's [navigate](https://html.spec.whatwg.org/#navigate) algorithm is called. In particular, HTML does not handle non-fragment-related same-document navigations through the navigate algorithm; instead it uses the [URL and history update steps](https://html.spec.whatwg.org/#url-and-history-update-steps) for those. Also, HTML calls the navigate algorithm for the initial loads of new browsing contexts as they transition from the initial `about:blank`; our current thinking is that `appHistory` should just not work on the initial `about:blank` so we can avoid that edge case._
Expand Down
7 changes: 4 additions & 3 deletions spec.bs
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,12 @@ With the above infrastructure in place, we can actually fire and handle the {{Ap
</div>

<div algorithm="navigate" id="navigate-modifications">
Modify the <a spec="HTML">navigate</a> algorithm by inserting the following step right before the step which goes [=in parallel=]. Recall that per [[#user-initiated-patches]] we have introduced |userInvolvement| argument, and per [[#form-patches]] we have introduced a |entryList| argument.
Modify the <a spec="HTML">navigate</a> algorithm by inserting the following step right before the step which goes [=in parallel=]. Recall that per [[#user-initiated-patches]] we have introduced |userInvolvement| argument, and per [[#form-patches]] we have introduced an |entryList| argument.

1. If none of the following are true:
* <var ignore>historyHandling</var> is "<a for="history handling behavior">`entry update`</a>"
* <var ignore>userInvolvement</var> is "<code>[=user navigation involvement/browser UI=]</code>"
* <var ignore>browsingContext</var>'s [=active document=]'s [=Document/origin=] is not [=same origin-domain=] with the [=source browsing context=]'s [=active document=]'s [=Document/origin=]
* <var ignore>browsingContext</var>'s only entry in its <a spec="HTML">session history</a> is the `about:blank` {{Document}} that was added when <var ignore>browsingContext</var> was <a spec="HTML" lt="create a new browsing context">created</a>

then:
Expand All @@ -401,9 +402,9 @@ With the above infrastructure in place, we can actually fire and handle the {{Ap
1. Let |continue| be the result of [=firing a navigate event=] at |appHistory| given <var ignore>url</var>, with <i>[=fire a navigate event/isPush=]</i> set to |isPush|, <i>[=fire a navigate event/isSameDocument=]</i> set to false, <i>[=fire a navigate event/userInvolvement=]</i> set to |userInvolvement|, and <i>[=fire a navigate event/formDataEntryList=]</i> set to |entryList|.
1. If |continue| is false, return.

<p class="note">"<code>[=user navigation involvement/browser UI=]</code>" navigations that cause <a spec="HTML" lt="navigate to a fragment">fragment navigations</a> <em>do</em> fire the {{AppHistory/navigate}} event; those are handled as part of the <a spec="HTML">navigate to a fragment</a> algorithm called earlier in <a spec="HTML">navigate</a>, which is not guarded by this condition.

<p class="note">"<a for="history handling behavior">`entry update`</a>" is excluded since {{AppHistory/navigate}} would have fired earlier as part of <a spec="HTML">traversing the history by a delta</a>.

<p class="note">"<code>[=user navigation involvement/browser UI=]</code>" or [=same origin-domain|cross origin-domain=] navigations that cause <a spec="HTML" lt="navigate to a fragment">fragment navigations</a> <em>do</em> fire the {{AppHistory/navigate}} event; those are handled as part of the <a spec="HTML">navigate to a fragment</a> algorithm called earlier in <a spec="HTML">navigate</a>, which is not guarded by this condition.
</div>

<div algorithm="traverse the history by a delta">
Expand Down

0 comments on commit 590f061

Please sign in to comment.