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

Re-do close watcher user activation tracking #10168

Merged
merged 4 commits into from
Mar 14, 2024
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 154 additions & 108 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -78574,9 +78574,17 @@ interface <dfn interface>VisibilityStateEntry</dfn> : <span>PerformanceEntry</sp
data-x="concept-document-origin">origin</span> is <span>same origin</span> with
<var>document</var>'s <span data-x="concept-document-origin">origin</span>.</p></li>

<li><p><span data-x="list iterate">For each</span> <var>window</var> in <var>windows</var>, set
<var>window</var>'s <span>last activation timestamp</span> to the <span>current high resolution
time</span>.</p></li>
<li>
<p><span data-x="list iterate">For each</span> <var>window</var> in <var>windows</var>:</p>

<ol>
<li><p>Set <var>window</var>'s <span>last activation timestamp</span> to the <span>current
high resolution time</span>.</p></li>

<li><p><span>Notify the close watcher manager about user activation</span> given
<var>window</var>.</p></li>
</ol>
</li>
</ol>

<p>An <dfn>activation triggering input event</dfn> is any event whose <code
Expand Down Expand Up @@ -81790,9 +81798,9 @@ body { display:none }

<li><p>If <var>closedSomething</var> is true, then return.</p></li>

<li><p>Otherwise, there was nothing watching for a <span>close request</span>. The user agent
may instead interpret this interaction as some other action, instead of interpreting it as a
close request.</p></li>
<li id="close-request-fallback"><p>Otherwise, there was nothing watching for a <span>close
domenic marked this conversation as resolved.
Show resolved Hide resolved
request</span>. The user agent may instead interpret this interaction as some other action,
instead of interpreting it as a close request.</p></li>
</ol>

</div>
Expand All @@ -81807,25 +81815,83 @@ body { display:none }

<p class="example">On platforms where a back button is a potential <span>close request</span>, no
event is involved, so when the back button is pressed, the user agent proceeds directly to
<span>process close watchers</span>. If there is a <span>close watcher</span> on the <span>close
watcher stack</span>, then that will get triggered. If there is not, then the user agent can
interpret the back button press in another way, for example as a request to <span>traverse the
history by a delta</span> of &minus;1.</p>
<span>process close watchers</span>. If there is an <span
data-x="close-watcher-active">active</span> <span>close watcher</span>, then that will get
triggered. If there is not, then the user agent can interpret the back button press in another
way, for example as a request to <span>traverse the history by a delta</span> of &minus;1.</p>


<div w-nodev>

<h4>Close watcher infrastructure</h4>

<p>Each <code>Window</code> has a <dfn>close watcher stack</dfn>, a <span>list</span> of <span
data-x="close watcher">close watchers</span>, initially empty.</p>
<p>Each <code>Window</code> has a <dfn>close watcher manager</dfn>, which is a
<span>struct</span> with the following <span data-x="struct item">items</span>:</p>

<ul>
<li><p><dfn data-x="close watcher manager groups">Groups</dfn>, a <span>list</span> of <span
data-x="list">lists</span> of <span data-x="close watcher">close watchers</span>, initially
empty.</p></li>

<li><p><dfn data-x="close watcher manager allowed number of groups">Allowed number of
groups</dfn>, a number, initially 1.</p></li>

<li><p><dfn data-x="close watcher manager next">Next user interaction allows a new group</dfn>,
a boolean, initially true.</p></li>
</ul>

<p>Most of the complexity of the <span>close watcher manager</span> comes from anti-abuse
protections designed to prevent developers from disabling users' history traversal abilities, for
platforms where a <span>close request</span>'s <a href="#close-request-fallback">fallback
action</a> is the main mechanism of history traversal. In particular:</p>

<p>The grouping of <span data-x="close watcher">close watchers</span> is designed so that if
multiple close watchers are created without <span>history-action activation</span>, they are
grouped together, so that a user-triggered <span>close request</span> will close all of the close
watchers in a group. This ensures that web developers can't intercept an unlimited number of
close requests by creating close watchers; instead they can create a number equal to at most 1 +
the number of times the <span data-x="user activation">user activates the page</span>.</p>

<p>The <span data-x="close watcher manager next">next user interaction allows a new group</span>
boolean encourages web developers to create <span data-x="close watcher">close watchers</span> in
a way that is tied to individual <span data-x="user activation">user activations</span>. Without
it, each user activation would increase the <span data-x="close watcher manager allowed number of
groups">allowed number of groups</span>, even if the web developer isn't "using" those user
activations to create close watchers. In short:</p>

<ul>
<li><p>Allowed: user interaction; create a close watcher in its own group; user interaction;
create a close watcher in a second independent group.</p></li>

<li><p>Disallowed: user interaction; user interaction; create a close watcher in its own group;
create a close watcher in a second independent group.</p></li>

<li><p>Allowed: user interaction; user interaction; create a close watcher in its own group;
create a close watcher grouped with the previous one.</p></li>
</ul>

<p class="note">It isn't quite a proper <span>stack</span>, as <span data-x="list
item">items</span> can get <span data-x="list remove">removed</span> from the middle of it if a
<span>close watcher</span> is <span data-x="close-watcher-destroy">destroyed</span> or <span
data-x="close-watcher-close">closed</span> via web developer code. User-initiated <span
data-x="close request">close requests</span> always act on the top of the <span>close watcher
stack</span>, however.</p>
<p>This protection is <em>not</em> important for upholding our desired invariant of creating at
most (1 + the number of times the <span data-x="user activation">user activates the page</span>)
groups. A determined abuser will just create one close watcher per user interaction, "banking"
them for future abuse. But this system causes more predictable behavior for the normal case, and
encourages non-abusive developers to create close watchers directly in response to user
interactions.</p>

<p>To <dfn>notify the close watcher manager about user activation</dfn> given a
<code>Window</code> <var>window</var>:</p>

<ol>
<li><p>Let <var>manager</var> be <var>window</var>'s <span>close watcher manager</span>.</p></li>

<li><p>If <var>manager</var>'s <span data-x="close watcher manager next">next user interaction
allows a new group</span> is true, then increment <var>manager</var>'s <span data-x="close
watcher manager allowed number of groups">allowed number of groups</span>.</p></li>

<li><p>Set <var>manager</var>'s <span data-x="close watcher manager next">next user interaction
allows a new group</span> to false.</p></li>
</ol>

<hr>

<p>A <dfn export>close watcher</dfn> is a <span>struct</span> with the following <span
data-x="struct item">items</span>:</p>
Expand All @@ -81841,27 +81907,14 @@ body { display:none }
<li><p>A <dfn data-x="close-watcher-close-action">close action</dfn>, a list of steps. These
steps can never throw an exception.</p></li>

<li><p>A <dfn data-x="close-watcher-user-activation">was created with user activation</dfn>
boolean.</p></li>

<li><p>An <dfn data-x="close-watcher-grouped">is grouped with previous</dfn> boolean.</p></li>

<li><p>An <dfn data-x="close-watcher-is-running-cancel">is running cancel action</dfn>
boolean.</p></li>
</ul>

<p class="note">The <span data-x="close-watcher-grouped">is grouped with previous</span> boolean
is set if a <span>close watcher</span> is created without <span>history-action activation</span>.
(Except the very first close watcher in the <span>close watcher stack</span> that is created
without transient activation, which gets a pass on this restriction.) It causes a user-triggered
<span>close request</span> to close all such grouped-together close watchers, thus ensuring that
web developers can't make close requests ineffective by creating an excessive number of close
watchers.</p>

<p>A <span>close watcher</span> <var>closeWatcher</var> <dfn data-x="close-watcher-active">is
active</dfn> if <var>closeWatcher</var>'s <span data-x="close-watcher-window">window</span>'s
<span>close watcher stack</span> <span data-x="list contains">contains</span>
<var>closeWatcher</var>.</p>
<span>close watcher manager</span> <span data-x="list contains">contains</span> any list which
<span data-x="list contains">contains</span> <var>closeWatcher</var>.</p>

<hr>

Expand All @@ -81870,49 +81923,9 @@ body { display:none }
list of steps <dfn data-x="create-close-watcher-closeAction"><var>closeAction</var></dfn>:</p>

<ol>
<li><p><span>Assert</span>: <var>window</var>'s <span data-x="concept-document-window">associated
<code>Document</code></span> is <span>fully active</span>.</p></li>

<li><p>Let <var>wasCreatedWithUserActivation</var> and <var>isGroupedWithPrevious</var> be
null.</p></li>

<li>
<p>If <var>window</var> has <span>history-action activation</span>, then:</p>

<ol>
<li><p><span>Consume history-action user activation</span> given <var>window</var>.</p></li>

<li><p>Set <var>wasCreatedWithUserActivation</var> to true.</p></li>

<li><p>Set <var>isGroupedWithPrevious</var> to false.</p></li>
</ol>
</li>

<li>
<p>Otherwise, if there is no <span>close watcher</span> in <var>window</var>'s <span>close
watcher stack</span> whose <span data-x="close-watcher-user-activation">was created with user
activation</span> is false, then:</p>

<ol>
<li><p>Set <var>wasCreatedWithUserActivation</var> to false.</p></li>

<li><p>Set <var>isGroupedWithPrevious</var> to false.</p></li>
</ol>

<p class="note">This will be the one "free" close watcher created without user activation, which
is useful for cases like session inactivity timeout dialogs or notifications from the
server.</p>
</li>

<li>
<p>Otherwise:</p>

<ol>
<li><p>Set <var>wasCreatedWithUserActivation</var> to false.</p></li>

<li><p>Set <var>isGroupedWithPrevious</var> to true.</p></li>
</ol>
</li>
<li><p><span>Assert</span>: <var>window</var>'s <span
data-x="concept-document-window">associated <code>Document</code></span> is <span>fully
active</span>.</p></li>

<li>
<p>Let <var>closeWatcher</var> be a new <span>close watcher</span>, with</p>
Expand All @@ -81927,19 +81940,25 @@ body { display:none }
<dt><span data-x="close-watcher-close-action">close action</span></dt>
<dd><var>closeAction</var></dd>

<dt><span data-x="close-watcher-user-activation">was created with user activation</span></dt>
<dd><var>wasCreatedWithUserActivation</var></dd>

<dt><span data-x="close-watcher-grouped">is grouped with previous</span></dt>
<dd><var>isGroupedWithPrevious</var></dd>

<dt><span data-x="close-watcher-is-running-cancel">is running cancel action</span></dt>
<dd>false</dd>
</dl>
</li>

<li><p><span data-x="list append">Append</span> <var>closeWatcher</var> to <var>window</var>'s
<span>close watcher stack</span>.</p></li>
<li><p>Let <var>manager</var> be <var>window</var>'s <span>close watcher manager</span>.</p></li>

<li><p>If <var>manager</var>'s <span data-x="close watcher manager groups">groups</span>'s <span
data-x="list size">size</span> is less than <var>manager</var>'s <span data-x="close watcher
manager allowed number of groups">allowed number of groups</span>, then <span data-x="list
append">append</span> « <var>closeWatcher</var> » to <var>manager</var>'s <span data-x="close
watcher manager groups">groups</span>.</p></li>

<li><p>Otherwise, <span data-x="list append">append</span> <var>closeWatcher</var> to
domenic marked this conversation as resolved.
Show resolved Hide resolved
<var>manager</var>'s <span data-x="close watcher manager groups">groups</span>'s last <span
data-x="list item">item</span>.</p>

<li><p>Set <var>manager</var>'s <span data-x="close watcher manager next">next user interaction
allows a new group</span> to true.</p></li>

<li><p>Return <var>closeWatcher</var>.</p></li>
</ol>
Expand All @@ -81949,19 +81968,23 @@ body { display:none }

<ol>
<li><p>If <var>closeWatcher</var> <span data-x="close-watcher-active">is not active</span>, then
return.</p></li>
return true.</p></li>

<li><p>If <var>closeWatcher</var>'s <span data-x="close-watcher-is-running-cancel">is running
cancel action</span> is true, then return.</p></li>
cancel action</span> is true, then return true.</p></li>

<li><p>Let <var>window</var> be <var>closeWatcher</var>'s <span
data-x="close-watcher-window">window</span>.</p></li>

<li><p>If <var>window</var>'s <span data-x="concept-document-window">associated
<code>Document</code></span> is not <span>fully active</span>, then return.</p></li>
<code>Document</code></span> is not <span>fully active</span>, then return true.</p></li>

<li>
<p>If <var>window</var> has <span>history-action activation</span>, then:</p>
<p>If <var>window</var>'s <span>close watcher manager</span>'s <span data-x="close watcher
manager groups">groups</span>'s <span data-x="list size">size</span> is less than
<var>window</var>'s <span>close watcher manager</span>'s <span data-x="close watcher manager
domenic marked this conversation as resolved.
Show resolved Hide resolved
allowed number of groups">allowed number of groups</span>, and <var>window</var> has
<span>history-action activation</span>, then:</p>

<ol>
<li><p>Set <var>closeWatcher</var>'s <span data-x="close-watcher-is-running-cancel">is running
Expand All @@ -81979,21 +82002,20 @@ body { display:none }
<ol>
<li><p><span>Consume history-action user activation</span> given <var>window</var>.</p></li>

<li><p>Return.</p></li>
<li><p>Return false.</p></li>
</ol>
</li>
</ol>

<p class="note">The guard on these substeps means that the <span
data-x="close-watcher-cancel-action">cancel action</span> only has a chance to prevent us from
continuing onward to the <span data-x="close-watcher-close-action">close action</span> when
<var>window</var> has <span>history-action activation</span>. In particular, since these
substeps <span>consume history-action user activation</span>, <span
data-x="close-watcher-request-close">requesting to close</span> a <span>close watcher</span>
twice without any intervening <span>user activation</span> will skip these substeps.</p>
<p class="note">Note that since these substeps <span>consume history-action user
activation</span>, <span data-x="close-watcher-request-close">requesting to close</span> a
<span>close watcher</span> twice without any intervening <span>user activation</span> will skip
these substeps.</p>
</li>

<li><p><span data-x="close-watcher-close">Close</span> <var>closeWatcher</var>.</p></li>

<li><p>Return true.</p></li>
</ol>

<p>To <dfn data-x="close-watcher-close">close</dfn> a <span>close watcher</span>
Expand All @@ -82014,34 +82036,58 @@ body { display:none }
</ol>

<p>To <dfn data-x="close-watcher-destroy">destroy</dfn> a <span>close watcher</span>
<var>closeWatcher</var>, <span data-x="list remove">remove</span> <var>closeWatcher</var> from
<var>closeWatcher</var>'s <span data-x="close-watcher-window">window</span>'s <span>close watcher
stack</span>.</p>
<var>closeWatcher</var>:</p>

<ol>
<li><p>Let <var>manager</var> be <var>closeWatcher</var>'s <span
data-x="close-watcher-window">window</span>'s <span>close watcher manager</span>.</p></li>

<li><p><span data-x="list iterate">For each</span> <var>group</var> of <var>manager</var>'s
<span data-x="close watcher manager groups">groups</span>: <span data-x="list
remove">remove</span> <var>closeWatcher</var> from <var>group</var>.</p></li>

<li><p><span data-x="list remove">Remove</span> any item from <var>manager</var>'s <span
data-x="close watcher manager groups">groups</span> that <span data-x="list is empty">is
empty</span>.</p></li>
</ol>

<hr>

<p>To <dfn>process close watchers </dfn> given a <code>Window</code> <var>window</var>:</p>
<p>To <dfn>process close watchers</dfn> given a <code>Window</code> <var>window</var>:</p>

<ol>
<li><p>Let <var>processedACloseWatcher</var> be false.</p></li>

<li>
<p>While <var>window</var>'s <span>close watcher stack</span> is not empty:</p>
<p>If <var>window</var>'s <span>close watcher manager</span>'s <span data-x="close watcher
manager groups">groups</span> is not empty:</p>

<ol>
<li><p>Let <var>closeWatcher</var> be the last <span data-x="list item">item</span> in
<var>window</var>'s <span>close watcher stack</span>.</p></li>
<li><p>Let <var>group</var> be the last <span data-x="list item">item</span> in
<var>window</var>'s <span>close watcher manager</span>'s <span data-x="close watcher manager
groups">groups</span>.</p></li>

<li>
<p><span data-x="list iterate">For each</span> <var>closeWatcher</var> of <var>group</var>,
in reverse order:</p>

<li><p><span data-x="close-watcher-request-close">Request to close</span>
<var>closeWatcher</var>.</p></li>
<ol>
<li><p>Set <var>processedACloseWatcher</var> to true.</p></li>

<li><p>Set <var>processedACloseWatcher</var> to true.</p></li>
<li><p>Let <var>shouldProceed</var> be the result of <span
data-x="close-watcher-request-close">requesting to close</span>
<var>closeWatcher</var>.</p></li>

<li><p>If <var>closeWatcher</var>'s <span data-x="close-watcher-grouped">is grouped with
previous</span> is false, then <span>break</span>.</p></li>
<li><p>If <var>shouldProceed</var> is false, then <span>break</span>.</p></li>
</ol>
</li>
</ol>
</li>

<li><p>If <var>window</var>'s <span>close watcher manager</span>'s <span data-x="close watcher
manager allowed number of groups">allowed number of groups</span> is greater than 1, decrement
it by 1.</p></li>
domenic marked this conversation as resolved.
Show resolved Hide resolved

<li><p>Return <var>processedACloseWatcher</var>.</p></li>
</ol>

Expand Down
Loading