You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ClosesWICG#78 and closesWICG#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.
Copy file name to clipboardExpand all lines: README.md
+17-12
Original file line number
Diff line number
Diff line change
@@ -464,28 +464,33 @@ There are many types of navigations a given page can experience; see [this appen
464
464
465
465
First, the following navigations **will not fire `navigate`** at all:
466
466
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
470
470
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.)
472
472
473
473
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.)
474
474
475
475
As for`document.open()`, it is a terrible legacy APIwith 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()`.
476
476
477
477
Second, the following navigations **cannot be canceled** using `event.preventDefault()`, and as such will have `event.cancelable` equal to false:
478
478
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()`
480
482
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:
482
484
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 ofthis 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.
484
489
485
490
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:
486
491
487
492
- 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.
489
494
490
495
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.
491
496
@@ -1349,18 +1354,17 @@ Here's a summary table:
1349
1354
1350
1355
|Trigger|Cross- vs. same-document|Fires `navigate`?|`e.userInitiated`|`e.cancelable`|`e.canTransition`|
- \* = 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.
1373
1377
- Δ = 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">`
1374
1378
- ◊ = 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).
1375
1380
1376
1381
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.
Copy file name to clipboardExpand all lines: spec.bs
+10-28
Original file line number
Diff line number
Diff line change
@@ -1212,7 +1212,8 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
1212
1212
1. Set |destination|'s [=AppHistoryDestination/index=] to −1.
1213
1213
1. Set |destination|'s [=AppHistoryDestination/state=] to null.
1214
1214
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).
1216
1217
</div>
1217
1218
1218
1219
<div algorithm="fire a push or replace navigate event">
@@ -1235,15 +1236,13 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
1235
1236
1236
1237
1. [=AppHistory/Promote the upcoming navigation to ongoing=] given |appHistory| and |destination|'s [=AppHistoryDestination/key=].
1237
1238
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.
1243
1239
1. If |appHistory| [=AppHistory/has entries and events disabled=], then:
1244
1240
1. If |ongoingNavigation| is not null, then [=app history API navigation/clean up=] |ongoingNavigation|.
1245
1241
<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.
1246
1242
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.
1247
1246
1. Initialize |event|'s {{Event/type}} to "{{AppHistory/navigate}}".
1248
1247
1. Initialize |event|'s {{AppHistoryNavigateEvent/navigationType}} to |navigationType|.
1249
1248
1. Initialize |event|'s {{AppHistoryNavigateEvent/destination}} to |destination|.
@@ -1257,7 +1256,7 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
1257
1256
* |destination|'s [=AppHistoryDestination/URL=]'s [=url/fragment=] is not [=string/is|identical to=] |currentURL|'s [=url/fragment=]
1258
1257
1259
1258
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.
1261
1260
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.
1262
1261
1. [=Assert=]: |appHistory|'s [=AppHistory/ongoing navigate event=] is null.
1263
1262
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
1592
1591
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.)
1593
1592
1594
1593
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:
1596
1595
* <var ignore>historyHandling</var> is "<a for="history handling behavior">`entry update`</a>"
1597
1596
* <var ignore>userInvolvement</var> is "<code>[=user navigation involvement/browser UI=]</code>"
1598
1597
* <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
1641
1640
</div>
1642
1641
1643
1642
<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:
1645
1644
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>.
1664
1646
</div>
1665
1647
1666
1648
<h2 id="session-history-patches">Patches to session history</h2>
@@ -1760,4 +1742,4 @@ The integration is then as follows:
1760
1742
1761
1743
* 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.)
1762
1744
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