Skip to content

Commit 93d16a0

Browse files
authoredNov 4, 2021
Simplify navigate event firing for now
Closes WICG#78 and closes WICG#178 by implementing the conclusion in the latter, of firing non-cancelable navigate events for all traversals. WICG#32 remains open as a desired future feature, and is now explicitly called out as such in the README.
1 parent c9e16f1 commit 93d16a0

File tree

2 files changed

+27
-40
lines changed

2 files changed

+27
-40
lines changed
 

‎README.md

+17-12
Original file line numberDiff line numberDiff line change
@@ -464,28 +464,33 @@ There are many types of navigations a given page can experience; see [this appen
464464
465465
First, the following navigations **will not fire `navigate`** at all:
466466
467-
- User-initiated [cross-document](#appendix-types-of-navigations) navigations via browser UI, such as the URL bar, back/forward button, or bookmarks.
468-
- [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">`
469-
- [`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.
467+
- User-initiated [cross-document](#appendix-types-of-navigations) navigations via non-back/forward browser UI, such as the URL bar, bookmarks, or the reload button
468+
- [Cross-document](#appendix-types-of-navigations) navigations initiated from other cross origin windows, e.g. via `window.open(url, nameOfYourWindow)`, or clicking on `<a href="..." target="nameOfYourWindow">`
469+
- [`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
470470
471-
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.
471+
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. (We do fire a `navigate` event for browser-UI back/forward buttons; see more discussion below.)
472472

473473
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.)
474474

475475
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()`.
476476

477477
Second, the following navigations **cannot be canceled** using `event.preventDefault()`, and as such will have `event.cancelable` equal to false:
478478

479-
- User-initiated same-document navigations via the browser's back/forward buttons.
479+
- User-initiated traversals via the browser's back/forward buttons (either same- or cross-document)
480+
- Programmatic traversals via `history.back()`/`history.forward()`/`history.go()`
481+
- Programmatic traversals via `appHistory.back()`/`appHistory.forward()`/`appHistory.go()`
480482
481-
This is important to avoid abusive pages trapping the user by disabling their back button. Note that adding a same-origin restriction would not help here: imagine a user which navigates to `https://evil-in-disguise.example/`, and then clicks a link to `https://evil-in-disguise.example/2`. If `https://evil-in-disguise.example/2` were allowed to cancel same-origin browser back button navigations, they have effectively disabled the user's back button.
483+
We would like to make these cancelable in the future. However, we need to take care when doing so:
482484
483-
We're discussing this restriction in [#32](https://github.com/WICG/app-history/issues/32), as it does hurt some use cases, and we'd like to soften it in some way.
485+
- Canceling user-initiated traversals can be abused to trap the user by disabling their back button. Note that adding a same-origin restriction would not help here: imagine a user which navigates to `https://evil-in-disguise.example/`, and then clicks a link to `https://evil-in-disguise.example/2`. If `https://evil-in-disguise.example/2` were allowed to cancel same-origin browser back button navigations, they have effectively disabled the user's back button.
486+
- Both user-initiated and programmatic traversals of this sort are hard to intercept for technical reasons, as doing so can require cross-process communication.
487+
488+
See discussion in [#32](https://github.com/WICG/app-history/issues/32) about how we can make user-initiated traversals cancelable in a safe way, and [#178](https://github.com/WICG/app-history/issues/178) for the general discussion of loosening the cancelability restrictions over time.
484489

485490
Finally, the following navigations **cannot be replaced with same-document navigations** by using `event.transitionWhile()`, and as such will have `event.canTransition` equal to false:
486491

487492
- Any navigation to a URL which differs in scheme, username, password, host, or port. (I.e., you can only intercept URLs which differ in path, query, or fragment.)
488-
- Any programmatically-initiated [cross-document](#appendix-types-of-navigations) back/forward navigations. (Recall that _user_-initiated cross-document navigations will not fire the `navigate` event at all.) Transitioning two adjacent history entries from cross-document to same-document has unpleasant ripple effects on web application and browser implementation architecture.
493+
- Any [cross-document](#appendix-types-of-navigations) back/forward navigations. Transitioning two adjacent history entries from cross-document to same-document has unpleasant ripple effects on web application and browser implementation architecture.
489494

490495
We'll note that these restrictions still allow canceling cross-origin non-back/forward navigations. Although this might be surprising, in general it doesn't grant additional power. That is, web developers can already intercept `<a>` `click` events, or modify their code that would set `location.href`, even if the destination URL is cross-origin.
491496

@@ -1349,18 +1354,17 @@ Here's a summary table:
13491354
13501355
|Trigger|Cross- vs. same-document|Fires `navigate`?|`e.userInitiated`|`e.cancelable`|`e.canTransition`|
13511356
|-------|------------------------|-----------------|-----------------|--------------|--------------|
1352-
|Browser UI (back/forward,<br>same-document)|Same|Yes|Yes|No|Yes|
1353-
|Browser UI (back/forward,<br>cross-document)|Cross|No|—|—|—|
1357+
|Browser UI (back/forward)|Either|Yes|Yes|No ❖|Yes †*|
13541358
|Browser UI (non-back/forward<br>fragment change only)|Same|Yes|Yes|Yes|Yes|
13551359
|Browser UI (non-back/forward<br>other)|Cross|No|—|—|—|
13561360
|`<a>`/`<area>`/`<form>` (`target="_self"` or no `target=""`)|Either|Yes|Yes ‡|Yes|Yes *|
13571361
|`<a>`/`<area>`/`<form>`<br>(non-`_self` `target=""`)|Either|Yes Δ|Yes ‡|Yes|Yes *|
13581362
|`<meta http-equiv="refresh">`|Either ◊|Yes|No|Yes|Yes *|
13591363
|`Refresh` header|Either ◊|Yes|No|Yes|Yes *|
13601364
|`window.location`|Either|Yes Δ|No|Yes|Yes *|
1361-
|`history.{back,forward,go}()`|Either|Yes|No|Yes|Yes †*|
1365+
|`history.{back,forward,go}()`|Either|Yes|No|No ❖|Yes †*|
13621366
|`history.{pushState,replaceState}()`|Same|Yes|No|Yes|Yes|
1363-
|`appHistory.{back,forward,goTo}()`|Either|Yes|No|Yes|Yes †*|
1367+
|`appHistory.{back,forward,goTo}()`|Either|Yes|No|No ❖|Yes †*|
13641368
|`appHistory.navigate()`|Either|Yes|No|Yes|Yes *|
13651369
|`appHistory.reload()`|Cross|Yes|No|Yes|Yes|
13661370
|`window.open(url, "_self")`|Either|Yes|No|Yes|Yes *|
@@ -1372,6 +1376,7 @@ Here's a summary table:
13721376
- \* = No if the URL differs from the page's current one in components besides path/query/fragment, or is cross-origin from the current page and differs in any component besides fragment.
13731377
- Δ = 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">`
13741378
- ◊ = 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)
1379+
- ❖ = We would like to make these cancelable in the future, after additional implementation and spec work: see [#178](https://github.com/WICG/app-history/issues/178) and [#32](https://github.com/WICG/app-history/issues/32).
13751380
13761381
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.
13771382

‎spec.bs

+10-28
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,8 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
12121212
1. Set |destination|'s [=AppHistoryDestination/index=] to &minus;1.
12131213
1. Set |destination|'s [=AppHistoryDestination/state=] to null.
12141214
1. Set |destination|'s [=AppHistoryDestination/is same document=] to true if |destinationEntry|'s [=session history entry/document=] is equal to |appHistory|'s [=relevant global object=]'s [=associated Document=]; otherwise false.
1215-
1. Return the result of performing the [=inner navigate event firing algorithm=] given |appHistory|, "{{AppHistoryNavigationType/traverse}}", |event|, |destination|, |userInvolvement|, and null.
1215+
1. Let |result| be the result of performing the [=inner navigate event firing algorithm=] given |appHistory|, "{{AppHistoryNavigationType/traverse}}", |event|, |destination|, |userInvolvement|, and null.
1216+
1. Assert: |result| is true (traversals are never cancelable).
12161217
</div>
12171218

12181219
<div algorithm="fire a push or replace navigate event">
@@ -1235,15 +1236,13 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
12351236

12361237
1. [=AppHistory/Promote the upcoming navigation to ongoing=] given |appHistory| and |destination|'s [=AppHistoryDestination/key=].
12371238
1. Let |ongoingNavigation| be |appHistory|'s [=AppHistory/ongoing navigation=].
1238-
1. Let |document| be |appHistory|'s [=relevant global object=]'s [=associated document=].
1239-
1. If |document| <a spec="HTML">can have its URL rewritten</a> to |destination|'s [=AppHistoryDestination/URL=], and either |destination|'s [=AppHistoryDestination/is same document=] is true or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{AppHistoryNavigateEvent/canTransition}} to true. Otherwise, initialize it to false.
1240-
1. If either |userInvolvement| is not "<code>[=user navigation involvement/browser UI=]</code>" or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{Event/cancelable}} to true. Otherwise, initialize it to false.
1241-
1. If both |event|'s {{AppHistoryNavigateEvent/canTransition}} and |event|'s {{Event/cancelable}} are false, then return true.
1242-
<p class="note">In this case we are definitely performing a cross-document navigation or traversal. We don't clean up |ongoingNavigation| however, since we might end up [=finalizing with an aborted navigation error=] before the current {{Document}} unloads.
12431239
1. If |appHistory| [=AppHistory/has entries and events disabled=], then:
12441240
1. If |ongoingNavigation| is not null, then [=app history API navigation/clean up=] |ongoingNavigation|.
12451241
<p class="note">In this case the [=app history API navigation/committed promise=] and [=app history API navigation/finished promise=] will never fulfill, since we never create {{AppHistoryEntry}}s for the initial `about:blank` {{Document}} so we have nothing to [=resolve=] them with.
12461242
1. Return true.
1243+
1. Let |document| be |appHistory|'s [=relevant global object=]'s [=associated document=].
1244+
1. If |document| <a spec="HTML">can have its URL rewritten</a> to |destination|'s [=AppHistoryDestination/URL=], and either |destination|'s [=AppHistoryDestination/is same document=] is true or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{AppHistoryNavigateEvent/canTransition}} to true. Otherwise, initialize it to false.
1245+
1. If |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{Event/cancelable}} to true. Otherwise, initialize it to false.
12471246
1. Initialize |event|'s {{Event/type}} to "{{AppHistory/navigate}}".
12481247
1. Initialize |event|'s {{AppHistoryNavigateEvent/navigationType}} to |navigationType|.
12491248
1. Initialize |event|'s {{AppHistoryNavigateEvent/destination}} to |destination|.
@@ -1257,7 +1256,7 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
12571256
* |destination|'s [=AppHistoryDestination/URL=]'s [=url/fragment=] is not [=string/is|identical to=] |currentURL|'s [=url/fragment=]
12581257

12591258
then initialize |event|'s {{AppHistoryNavigateEvent/hashChange}} to true. Otherwise, initialize it to false.
1260-
1. If |userInvolvement| is "<code>[=user navigation involvement/none=]</code>", then initialize |event|'s {{AppHistoryNavigateEvent/userInitiated}} to false. Otherwise, initialize it to true.
1259+
1. If |userInvolvement| is not "<code>[=user navigation involvement/none=]</code>", then initialize |event|'s {{AppHistoryNavigateEvent/userInitiated}} to true. Otherwise, initialize it to false.
12611260
1. If |formDataEntryList| is not null, then initialize |event|'s {{AppHistoryNavigateEvent/formData}} to a [=new=] {{FormData}} created in |appHistory|'s [=relevant Realm=], associated to |formDataEntryList|. Otherwise, initialize it to null.
12621261
1. [=Assert=]: |appHistory|'s [=AppHistory/ongoing navigate event=] is null.
12631262
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to |event|.
@@ -1592,7 +1591,7 @@ With the above infrastructure in place, we can actually fire and handle the {{Ap
15921591
Modify the <a spec="HTML">navigate</a> algorithm to take an optional <dfn for="navigate">|appHistoryState|</dfn> argument (default null). Then, insert the following steps 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.)
15931592

15941593
1. Let |appHistory| be <var ignore>browsingContext</var>'s [=browsing context/active window=]'s [=Window/app history=].
1595-
1. If none of the following are true:
1594+
1. If all of the following are false:
15961595
* <var ignore>historyHandling</var> is "<a for="history handling behavior">`entry update`</a>"
15971596
* <var ignore>userInvolvement</var> is "<code>[=user navigation involvement/browser UI=]</code>"
15981597
* <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=]
@@ -1641,26 +1640,9 @@ With the above infrastructure in place, we can actually fire and handle the {{Ap
16411640
</div>
16421641

16431642
<div algorithm="apply the history step">
1644-
Modify the <a spec="HTML">apply the history step</a> algorithm as follows. Change <var ignore>checkForUserCancellation</var> to |fireBeforeunloadAndNavigate|. Add the |userInvolvement| parameter. Then, insert the following step after step 12 (which assembles |toTraverse| and other lists) but before step 13 (which checks if unloading is user-cancelled):
1643+
Modify the <a spec="HTML">apply the history step</a> algorithm as follows. Inside the loop over each <var ignore>navigable</var> of <var ignore>toTraverse</var>, inside the task that is posted, after the check if |targetEntry|'s document is |previousDocument| that might abort the algorithm, add the following steps:
16451644

1646-
1. If |fireBeforeunloadAndNavigate| is true and the result of <a>firing traversal `navigate` events</a> given |toTraverse|, <var ignore>step</var>, <var ignore>initiatorToCheck</var>, and |userInvolvement| is false, then return.
1647-
</div>
1648-
1649-
<div algorithm>
1650-
To <dfn>fire traversal `navigate` events</dfn> for a [=list=] of [=navigables=] |toTraverse|, an integer |step|, an [=origin=] |initiatorOrigin|, and a [=user navigation involvement=] |userInvolvement|:
1651-
1652-
1. Let |overallResult| be true.
1653-
1. Let |totalTasks| be the [=list/size=] of |toTraverse|.
1654-
1. Let |completedTasks| be 0.
1655-
1. [=list/For each=] |navigable| of |toTraverse|, [=queue a global task=] on the [=history traversal task source=] given |navigable|'s [=navigable/active document=]'s [=relevant global object=] to run these steps:
1656-
1. Let |destinationEntry| be the item in the result of [=navigable/getting the session history entries=] for |navigable| that has the greatest [=session history entry/step=] less than or equal to |step|.
1657-
1. If |destinationEntry|'s [=session history entry/document=] is not equal to |navigable|'s [=navigable/active document=], and |initiatorOrigin| is not [=same origin-domain=] with |navigable|'s [=navigable/active document=]'s [=Document/origin=], then abort these steps.
1658-
1. Let |appHistory| be |navigable|'s [=navigable/active document=]'s [=relevant global object=]'s [=Window/app history=].
1659-
1. Let |result| be the result of [=firing a traversal navigate event=] at |appHistory| with <i>[=fire a traversal navigate event/destinationEntry=]</i> set to |destinationEntry| and <i>[=fire a traversal navigate event/userInvolvement=]</i> set to |userInvolvement|.
1660-
1. If |result| is false, then set |overallResult| to false.
1661-
1. Increment |completedTasks|.
1662-
1. Wait for |completedTasks| to be |totalTasks|.
1663-
1. Return |overallResult|.
1645+
1. [=Fire a traversal navigate event=] at |previousDocument|'s [=relevant global object=]'s [=Window/app history=] with <i>[=fire a traversal navigate event/destinationEntry=]</i> set to |targetEntry| and <i>[=fire a traversal navigate event/userInvolvement=]</i> set to <var ignore>userInvolvement</var>.
16641646
</div>
16651647

16661648
<h2 id="session-history-patches">Patches to session history</h2>
@@ -1760,4 +1742,4 @@ The integration is then as follows:
17601742

17611743
* Wherever the spec ends up canceling not-yet-mature navigations for a [=browsing context=] |bc|, we also [=inform app history about canceling navigation=] in |bc|. (Regardless of whether or not there are any not-yet-mature navigations still in flight.)
17621744

1763-
* When the spec [=browsing context/discards=] a [=browsing context=] |bc|, we also [=inform app history about browsing context discarding=] given |bc|. (Regardless of whether or not there are any not-yet-mature navigations still in fligh, or any traversals queued up.)
1745+
* When the spec [=browsing context/discards=] a [=browsing context=] |bc|, we also [=inform app history about browsing context discarding=] given |bc|. (Regardless of whether or not there are any not-yet-mature navigations still in flight, or any traversals queued up.)

0 commit comments

Comments
 (0)
Please sign in to comment.