Skip to content

Commit d5d2754

Browse files
Clear-Site-Data must uncontrol affected service worker clients
Adds an immediate flag to unregister jobs for service worker registrations. When set, the immediate flag enables unregister jobs to run the "Clear Registration" algorithm immediately without waiting for the active worker's controllees to unload. After an immediate clear, the registration's controllees become uncontrolled. The controllees receive the 'controllerchange' event when the navigator.serviceWorker.controller is set to null. This change updates ServiceWorkerContextCore::DeleteForOrigin() to set the immediate unregister job flag to true. All other callers set the immediate flag to false. The storage quota manager uses ServiceWorkerContextCore::DeleteForOrigin() when clearing data for the origin. In addition to evicting origins to free up storage quota, this code path is used by scenarios like the Clear-Site-Data network header and the "clear browsing data" privacy UI setting. The change adds a new function to the ServiceWorkerRegistration class, DeleteAndClearImmediately(), which is used by ServiceWorkerUnregisterJob when the immediate flag is true. DeleteAndClearImmediately() is very similar to the existing function, ClearWhenReady(), but it does not wait to clear the registration when the active worker has controllees. ServiceWorkerContextCore::DeleteForOrigin() is also updated to call DeleteAndClearImmediately() on the origin's uninstalling registrations. Registrations stuck in the uninstalling state are waiting for the active worker's controllees to unload. ServiceWorkerContextCore::DeleteForOrigin() must stop waiting and clear the registration immediately. GetUninstallingRegistrationsForOrigin() is added to the ServiceWorkerRegistry to enable ServiceWorkerContextCore::DeleteForOrigin() to find the uninstalling registrations it needs to clear. For testing, the change adds a new unit test that sets the immediate flag to true in ServiceWorkerJobTest. The change also adds a new service worker WPT test, unregister-immediately.https.html. The new WPT test uses the Clear-Site-Data network header to unregister service workers with the immediate flag set to true. The WPT test adds a test case for each service worker state. For example, one test case uses Clear-Site-Data to clear a registration with a service worker in the 'parsing' state. Other test cases cover the other service worker states like 'installing', 'installed', 'activating' and 'activated'. Additional test cases verify controllee state, which includes verifying what happens to pending fetch events when the controller clears. Bug: 1014114 Change-Id: I3baee1117e6dbd349ec4c98c796d4e1b2bddde96 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2040533 Reviewed-by: Matt Falkenhagen <[email protected]> Reviewed-by: Makoto Shimazu <[email protected]> Commit-Queue: Steve Becker <[email protected]> Cr-Commit-Position: refs/heads/master@{#743477}
1 parent 95e1210 commit d5d2754

9 files changed

+295
-1
lines changed

Diff for: clear-site-data/storage.https.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
var serviceWorkerTestPageIFrame;
1818
function fetchFromIFrame() {
1919
return serviceWorkerTestPageIFrame.contentWindow
20-
.fetch('support/controlled-endpoint.py')
20+
.fetch('controlled-endpoint.py')
2121
.then((result) => {
2222
return result.text();
2323
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
self.addEventListener('activate', event => {
4+
event.waitUntil(new Promise(() => {
5+
// Use a promise that never resolves to prevent this service worker from
6+
// advancing past the 'activating' state.
7+
}));
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
3+
self.addEventListener('fetch', event => {
4+
if (event.request.url.endsWith('waituntil-forever')) {
5+
event.respondWith(new Promise(() => {
6+
// Use a promise that never resolves to prevent this fetch from
7+
// completing.
8+
}));
9+
}
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
self.addEventListener('install', event => {
4+
event.waitUntil(new Promise(() => {
5+
// Use a promise that never resolves to prevent this service worker from
6+
// advancing past the 'installing' state.
7+
}));
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
// Use an infinite loop to prevent this service worker from advancing past the
4+
// 'parsed' state.
5+
let i = 0;
6+
while (true) {
7+
++i;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
// Returns a promise for a network response that contains the Clear-Site-Data:
4+
// "storage" header.
5+
function clear_site_data() {
6+
return fetch('resources/blank.html?pipe=header(Clear-Site-Data,"storage")');
7+
}
8+
9+
async function assert_no_registrations_exist() {
10+
const registrations = await navigator.serviceWorker.getRegistrations();
11+
assert_equals(registrations.length, 0);
12+
}
13+
14+
async function add_controlled_iframe(test, url) {
15+
const frame = await with_iframe(url);
16+
test.add_cleanup(() => { frame.remove(); });
17+
assert_true(frame.contentWindow.navigator.serviceWorker.controller !== null);
18+
return frame;
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!doctype html>
2+
<meta charset=utf-8>
3+
<title>Use Clear-Site-Data to immediately unregister service workers</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script src="resources/test-helpers.sub.js"></script>
7+
<script src="resources/unregister-immediately-helpers.js"></script>
8+
<body>
9+
<script>
10+
'use strict';
11+
12+
// These tests use the Clear-Site-Data network response header to immediately
13+
// unregister a service worker registration with a worker whose state is
14+
// 'installing' or 'parsed'. Clear-Site-Data must delete the registration,
15+
// abort the installation and then clear the registration by setting the
16+
// worker's state to 'redundant'.
17+
18+
promise_test(async test => {
19+
// This test keeps the the service worker in the 'parsed' state by using a
20+
// script with an infinite loop.
21+
const script_url = 'resources/onparse-infiniteloop-worker.js';
22+
const scope_url =
23+
'resources/scope-for-unregister-immediately-with-parsed-worker';
24+
25+
await service_worker_unregister(test, /*scope=*/script_url);
26+
27+
// Clear-Site-Data must cause register() to fail.
28+
const register_promise = promise_rejects(test, 'AbortError',
29+
navigator.serviceWorker.register(script_url, { scope: scope_url}));;
30+
31+
await Promise.all([clear_site_data(), register_promise]);
32+
33+
await assert_no_registrations_exist();
34+
}, 'Clear-Site-Data must abort service worker registration.');
35+
36+
promise_test(async test => {
37+
// This test keeps the the service worker in the 'installing' state by using a
38+
// script with an install event waitUntil() promise that never resolves.
39+
const script_url = 'resources/oninstall-waituntil-forever.js';
40+
const scope_url =
41+
'resources/scope-for-unregister-immediately-with-installing-worker';
42+
43+
const registration = await service_worker_unregister_and_register(
44+
test, script_url, scope_url);
45+
const service_worker = registration.installing;
46+
47+
// Clear-Site-Data must cause install to fail.
48+
await Promise.all([
49+
clear_site_data(),
50+
wait_for_state(test, service_worker, 'redundant')]);
51+
52+
await assert_no_registrations_exist();
53+
}, 'Clear-Site-Data must unregister a registration with a worker '
54+
+ 'in the "installing" state.');
55+
56+
</script>
57+
</body>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!doctype html>
2+
<meta charset=utf-8>
3+
<title>Use Clear-Site-Data to immediately unregister service workers</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script src="resources/test-helpers.sub.js"></script>
7+
<script src="resources/unregister-immediately-helpers.js"></script>
8+
<body>
9+
<script>
10+
'use strict';
11+
12+
// These tests use the Clear-Site-Data network response header to immediately
13+
// unregister a service worker registration with a worker that has pending
14+
// extendable events. Clear-Site-Data must delete the registration,
15+
// abort all pending extendable events and then clear the registration by
16+
// setting the worker's state to 'redundant'
17+
18+
promise_test(async test => {
19+
// Use a service worker script that can produce fetch events with pending
20+
// respondWith() promises that never resolve.
21+
const script_url = 'resources/onfetch-waituntil-forever.js';
22+
const scope_url =
23+
'resources/blank.html?unregister-immediately-with-fetch-event';
24+
25+
const registration = await service_worker_unregister_and_register(
26+
test, script_url, scope_url);
27+
28+
await wait_for_state(test, registration.installing, 'activated');
29+
30+
const frame = await add_controlled_iframe(test, scope_url);
31+
32+
// Clear-Site-Data must cause the pending fetch promise to reject.
33+
const fetch_promise = promise_rejects(
34+
test, new TypeError(), frame.contentWindow.fetch('waituntil-forever'));
35+
36+
const event_watcher = new EventWatcher(
37+
test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
38+
39+
await Promise.all([
40+
clear_site_data(),
41+
fetch_promise,
42+
event_watcher.wait_for('controllerchange'),
43+
wait_for_state(test, registration.active, 'redundant'),]);
44+
45+
assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
46+
await assert_no_registrations_exist();
47+
}, 'Clear-Site-Data must fail pending subresource fetch events.');
48+
49+
</script>
50+
</body>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<!doctype html>
2+
<meta charset=utf-8>
3+
<title>Use Clear-Site-Data to immediately unregister service workers</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script src="resources/test-helpers.sub.js"></script>
7+
<script src="resources/unregister-immediately-helpers.js"></script>
8+
<body>
9+
<script>
10+
'use strict';
11+
12+
// These tests use the Clear-Site-Data network response header to immediately
13+
// unregister a service worker registration with a worker whose state is
14+
// 'installed', 'waiting', 'activating' or 'activated'. Immediately
15+
// unregistering runs the "Clear Registration" algorithm without waiting for the
16+
// active worker's controlled clients to unload.
17+
18+
promise_test(async test => {
19+
// This test keeps the the service worker in the 'activating' state by using a
20+
// script with an activate event waitUntil() promise that never resolves.
21+
const script_url = 'resources/onactivate-waituntil-forever.js';
22+
const scope_url =
23+
'resources/scope-for-unregister-immediately-with-waiting-worker';
24+
25+
const registration = await service_worker_unregister_and_register(
26+
test, script_url, scope_url);
27+
const service_worker = registration.installing;
28+
29+
await wait_for_state(test, service_worker, 'activating');
30+
31+
// Clear-Site-Data must cause activation to fail.
32+
await Promise.all([
33+
clear_site_data(),
34+
wait_for_state(test, service_worker, 'redundant')]);
35+
36+
await assert_no_registrations_exist();
37+
}, 'Clear-Site-Data must unregister a registration with a worker '
38+
+ 'in the "activating" state.');
39+
40+
promise_test(async test => {
41+
// Create an registration with two service workers: one activated and one
42+
// installed.
43+
const script_url = 'resources/update_shell.py';
44+
const scope_url =
45+
'resources/scope-for-unregister-immediately-with-with-update';
46+
47+
const registration = await service_worker_unregister_and_register(
48+
test, script_url, scope_url);
49+
const first_service_worker = registration.installing;
50+
51+
await wait_for_state(test, first_service_worker, 'activated');
52+
registration.update();
53+
54+
const event_watcher = new EventWatcher(test, registration, 'updatefound');
55+
await event_watcher.wait_for('updatefound');
56+
57+
const second_service_worker = registration.installing;
58+
await wait_for_state(test, second_service_worker, 'installed');
59+
60+
// Clear-Site-Data must clear both workers from the registration.
61+
await Promise.all([
62+
clear_site_data(),
63+
wait_for_state(test, first_service_worker, 'redundant'),
64+
wait_for_state(test, second_service_worker, 'redundant')]);
65+
66+
await assert_no_registrations_exist();
67+
}, 'Clear-Site-Data must unregister an activated registration with '
68+
+ 'an update waiting.');
69+
70+
promise_test(async test => {
71+
const script_url = 'resources/empty.js';
72+
const scope_url =
73+
'resources/blank.html?unregister-immediately-with-controlled-client';
74+
75+
const registration = await service_worker_unregister_and_register(
76+
test, script_url, scope_url);
77+
const service_worker = registration.installing;
78+
79+
await wait_for_state(test, service_worker, 'activated');
80+
const frame = await add_controlled_iframe(test, scope_url);
81+
const frame_registration =
82+
await frame.contentWindow.navigator.serviceWorker.ready;
83+
84+
const event_watcher = new EventWatcher(
85+
test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
86+
87+
// Clear-Site-Data must remove the iframe's controller.
88+
await Promise.all([
89+
clear_site_data(),
90+
event_watcher.wait_for('controllerchange'),
91+
wait_for_state(test, service_worker, 'redundant')]);
92+
93+
assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
94+
await assert_no_registrations_exist();
95+
96+
// The ready promise must continue to resolve with the unregistered
97+
// registration.
98+
assert_equals(frame_registration,
99+
await frame.contentWindow.navigator.serviceWorker.ready);
100+
}, 'Clear-Site-Data must unregister an activated registration with controlled '
101+
+ 'clients.');
102+
103+
promise_test(async test => {
104+
const script_url = 'resources/empty.js';
105+
const scope_url =
106+
'resources/blank.html?unregister-immediately-while-waiting-to-clear';
107+
108+
const registration = await service_worker_unregister_and_register(
109+
test, script_url, scope_url);
110+
const service_worker = registration.installing;
111+
112+
await wait_for_state(test, service_worker, 'activated');
113+
const frame = await add_controlled_iframe(test, scope_url);
114+
115+
const event_watcher = new EventWatcher(
116+
test, frame.contentWindow.navigator.serviceWorker, 'controllerchange');
117+
118+
// Unregister waits to clear the registration until no controlled clients
119+
// exist.
120+
await registration.unregister();
121+
122+
// Clear-Site-Data must clear the unregistered registration immediately.
123+
await Promise.all([
124+
clear_site_data(),
125+
event_watcher.wait_for('controllerchange'),
126+
wait_for_state(test, service_worker, 'redundant')]);
127+
128+
assert_equals(frame.contentWindow.navigator.serviceWorker.controller, null);
129+
await assert_no_registrations_exist();
130+
}, 'Clear-Site-Data must clear an unregistered registration waiting for '
131+
+ ' controlled clients to unload.');
132+
133+
</script>
134+
</body>

0 commit comments

Comments
 (0)